/*
 * DWN - Desktop Window Manager
 * retoor <retoor@molodetz.nl>
 * X11 Atoms management implementation
 */

#include "atoms.h"
#include "dwn.h"
#include "util.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

EWMHAtoms ewmh;
ICCCMAtoms icccm;
MiscAtoms misc_atoms;

static Window ewmh_check_window = None;

#define ATOM(name) XInternAtom(display, name, False)

void atoms_init(Display *display)
{
    LOG_DEBUG("Initializing X11 atoms");

    ewmh.NET_SUPPORTED = ATOM("_NET_SUPPORTED");
    ewmh.NET_SUPPORTING_WM_CHECK = ATOM("_NET_SUPPORTING_WM_CHECK");
    ewmh.NET_CLIENT_LIST = ATOM("_NET_CLIENT_LIST");
    ewmh.NET_CLIENT_LIST_STACKING = ATOM("_NET_CLIENT_LIST_STACKING");
    ewmh.NET_NUMBER_OF_DESKTOPS = ATOM("_NET_NUMBER_OF_DESKTOPS");
    ewmh.NET_DESKTOP_GEOMETRY = ATOM("_NET_DESKTOP_GEOMETRY");
    ewmh.NET_DESKTOP_VIEWPORT = ATOM("_NET_DESKTOP_VIEWPORT");
    ewmh.NET_CURRENT_DESKTOP = ATOM("_NET_CURRENT_DESKTOP");
    ewmh.NET_DESKTOP_NAMES = ATOM("_NET_DESKTOP_NAMES");
    ewmh.NET_ACTIVE_WINDOW = ATOM("_NET_ACTIVE_WINDOW");
    ewmh.NET_WORKAREA = ATOM("_NET_WORKAREA");

    ewmh.NET_WM_NAME = ATOM("_NET_WM_NAME");
    ewmh.NET_WM_VISIBLE_NAME = ATOM("_NET_WM_VISIBLE_NAME");
    ewmh.NET_WM_DESKTOP = ATOM("_NET_WM_DESKTOP");
    ewmh.NET_WM_WINDOW_TYPE = ATOM("_NET_WM_WINDOW_TYPE");
    ewmh.NET_WM_STATE = ATOM("_NET_WM_STATE");
    ewmh.NET_WM_ALLOWED_ACTIONS = ATOM("_NET_WM_ALLOWED_ACTIONS");
    ewmh.NET_WM_STRUT = ATOM("_NET_WM_STRUT");
    ewmh.NET_WM_STRUT_PARTIAL = ATOM("_NET_WM_STRUT_PARTIAL");
    ewmh.NET_WM_PID = ATOM("_NET_WM_PID");

    ewmh.NET_WM_WINDOW_TYPE_DESKTOP = ATOM("_NET_WM_WINDOW_TYPE_DESKTOP");
    ewmh.NET_WM_WINDOW_TYPE_DOCK = ATOM("_NET_WM_WINDOW_TYPE_DOCK");
    ewmh.NET_WM_WINDOW_TYPE_TOOLBAR = ATOM("_NET_WM_WINDOW_TYPE_TOOLBAR");
    ewmh.NET_WM_WINDOW_TYPE_MENU = ATOM("_NET_WM_WINDOW_TYPE_MENU");
    ewmh.NET_WM_WINDOW_TYPE_UTILITY = ATOM("_NET_WM_WINDOW_TYPE_UTILITY");
    ewmh.NET_WM_WINDOW_TYPE_SPLASH = ATOM("_NET_WM_WINDOW_TYPE_SPLASH");
    ewmh.NET_WM_WINDOW_TYPE_DIALOG = ATOM("_NET_WM_WINDOW_TYPE_DIALOG");
    ewmh.NET_WM_WINDOW_TYPE_NORMAL = ATOM("_NET_WM_WINDOW_TYPE_NORMAL");
    ewmh.NET_WM_WINDOW_TYPE_NOTIFICATION = ATOM("_NET_WM_WINDOW_TYPE_NOTIFICATION");

    ewmh.NET_WM_STATE_MODAL = ATOM("_NET_WM_STATE_MODAL");
    ewmh.NET_WM_STATE_STICKY = ATOM("_NET_WM_STATE_STICKY");
    ewmh.NET_WM_STATE_MAXIMIZED_VERT = ATOM("_NET_WM_STATE_MAXIMIZED_VERT");
    ewmh.NET_WM_STATE_MAXIMIZED_HORZ = ATOM("_NET_WM_STATE_MAXIMIZED_HORZ");
    ewmh.NET_WM_STATE_SHADED = ATOM("_NET_WM_STATE_SHADED");
    ewmh.NET_WM_STATE_SKIP_TASKBAR = ATOM("_NET_WM_STATE_SKIP_TASKBAR");
    ewmh.NET_WM_STATE_SKIP_PAGER = ATOM("_NET_WM_STATE_SKIP_PAGER");
    ewmh.NET_WM_STATE_HIDDEN = ATOM("_NET_WM_STATE_HIDDEN");
    ewmh.NET_WM_STATE_FULLSCREEN = ATOM("_NET_WM_STATE_FULLSCREEN");
    ewmh.NET_WM_STATE_ABOVE = ATOM("_NET_WM_STATE_ABOVE");
    ewmh.NET_WM_STATE_BELOW = ATOM("_NET_WM_STATE_BELOW");
    ewmh.NET_WM_STATE_DEMANDS_ATTENTION = ATOM("_NET_WM_STATE_DEMANDS_ATTENTION");
    ewmh.NET_WM_STATE_FOCUSED = ATOM("_NET_WM_STATE_FOCUSED");

    ewmh.NET_WM_ACTION_MOVE = ATOM("_NET_WM_ACTION_MOVE");
    ewmh.NET_WM_ACTION_RESIZE = ATOM("_NET_WM_ACTION_RESIZE");
    ewmh.NET_WM_ACTION_MINIMIZE = ATOM("_NET_WM_ACTION_MINIMIZE");
    ewmh.NET_WM_ACTION_SHADE = ATOM("_NET_WM_ACTION_SHADE");
    ewmh.NET_WM_ACTION_STICK = ATOM("_NET_WM_ACTION_STICK");
    ewmh.NET_WM_ACTION_MAXIMIZE_HORZ = ATOM("_NET_WM_ACTION_MAXIMIZE_HORZ");
    ewmh.NET_WM_ACTION_MAXIMIZE_VERT = ATOM("_NET_WM_ACTION_MAXIMIZE_VERT");
    ewmh.NET_WM_ACTION_FULLSCREEN = ATOM("_NET_WM_ACTION_FULLSCREEN");
    ewmh.NET_WM_ACTION_CHANGE_DESKTOP = ATOM("_NET_WM_ACTION_CHANGE_DESKTOP");
    ewmh.NET_WM_ACTION_CLOSE = ATOM("_NET_WM_ACTION_CLOSE");

    ewmh.NET_CLOSE_WINDOW = ATOM("_NET_CLOSE_WINDOW");
    ewmh.NET_MOVERESIZE_WINDOW = ATOM("_NET_MOVERESIZE_WINDOW");
    ewmh.NET_WM_MOVERESIZE = ATOM("_NET_WM_MOVERESIZE");
    ewmh.NET_REQUEST_FRAME_EXTENTS = ATOM("_NET_REQUEST_FRAME_EXTENTS");
    ewmh.NET_FRAME_EXTENTS = ATOM("_NET_FRAME_EXTENTS");

    ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE");
    ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0");
    ewmh.NET_SYSTEM_TRAY_ORIENTATION = ATOM("_NET_SYSTEM_TRAY_ORIENTATION");
    ewmh.NET_SYSTEM_TRAY_VISUAL = ATOM("_NET_SYSTEM_TRAY_VISUAL");
    ewmh.MANAGER = ATOM("MANAGER");
    ewmh.XEMBED = ATOM("_XEMBED");
    ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO");

    icccm.WM_PROTOCOLS = ATOM("WM_PROTOCOLS");
    icccm.WM_DELETE_WINDOW = ATOM("WM_DELETE_WINDOW");
    icccm.WM_TAKE_FOCUS = ATOM("WM_TAKE_FOCUS");
    icccm.WM_STATE = ATOM("WM_STATE");
    icccm.WM_CHANGE_STATE = ATOM("WM_CHANGE_STATE");
    icccm.WM_CLASS = ATOM("WM_CLASS");
    icccm.WM_NAME = ATOM("WM_NAME");
    icccm.WM_TRANSIENT_FOR = ATOM("WM_TRANSIENT_FOR");
    icccm.WM_CLIENT_LEADER = ATOM("WM_CLIENT_LEADER");
    icccm.WM_WINDOW_ROLE = ATOM("WM_WINDOW_ROLE");

    misc_atoms.UTF8_STRING = ATOM("UTF8_STRING");
    misc_atoms.COMPOUND_TEXT = ATOM("COMPOUND_TEXT");
    misc_atoms.MOTIF_WM_HINTS = ATOM("_MOTIF_WM_HINTS");
    misc_atoms.CLIPBOARD = ATOM("CLIPBOARD");
    misc_atoms.PRIMARY = ATOM("PRIMARY");
    misc_atoms.DWN_RESTART = ATOM("_DWN_RESTART");

    LOG_DEBUG("X11 atoms initialized");
}


void atoms_setup_ewmh(void)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    Display *dpy = dwn->display;
    Window root = dwn->root;

    ewmh_check_window = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0);
    Window check = ewmh_check_window;

    XChangeProperty(dpy, root, ewmh.NET_SUPPORTING_WM_CHECK,
                    XA_WINDOW, 32, PropModeReplace,
                    (unsigned char *)&check, 1);
    XChangeProperty(dpy, check, ewmh.NET_SUPPORTING_WM_CHECK,
                    XA_WINDOW, 32, PropModeReplace,
                    (unsigned char *)&check, 1);

    XChangeProperty(dpy, check, ewmh.NET_WM_NAME,
                    misc_atoms.UTF8_STRING, 8, PropModeReplace,
                    (unsigned char *)DWN_NAME, strlen(DWN_NAME));

    Atom supported[] = {
        ewmh.NET_SUPPORTED,
        ewmh.NET_SUPPORTING_WM_CHECK,
        ewmh.NET_CLIENT_LIST,
        ewmh.NET_CLIENT_LIST_STACKING,
        ewmh.NET_NUMBER_OF_DESKTOPS,
        ewmh.NET_CURRENT_DESKTOP,
        ewmh.NET_DESKTOP_NAMES,
        ewmh.NET_ACTIVE_WINDOW,
        ewmh.NET_WM_NAME,
        ewmh.NET_WM_DESKTOP,
        ewmh.NET_WM_WINDOW_TYPE,
        ewmh.NET_WM_STATE,
        ewmh.NET_WM_STATE_FULLSCREEN,
        ewmh.NET_WM_STATE_HIDDEN,
        ewmh.NET_WM_STATE_DEMANDS_ATTENTION,
        ewmh.NET_CLOSE_WINDOW,
        ewmh.NET_WM_STRUT_PARTIAL,
        ewmh.NET_FRAME_EXTENTS
    };

    XChangeProperty(dpy, root, ewmh.NET_SUPPORTED,
                    XA_ATOM, 32, PropModeReplace,
                    (unsigned char *)supported, sizeof(supported) / sizeof(Atom));

    atoms_set_number_of_desktops(MAX_WORKSPACES);

    atoms_set_current_desktop(0);

    atoms_update_desktop_names();

    LOG_INFO("EWMH compliance initialized");
}

void atoms_update_client_list(void)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    Window windows[MAX_CLIENTS];
    int count = 0;

    for (Client *c = dwn->client_list; c != NULL && count < MAX_CLIENTS; c = c->next) {
        windows[count++] = c->window;
    }

    XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST,
                    XA_WINDOW, 32, PropModeReplace,
                    (unsigned char *)windows, count);

    XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST_STACKING,
                    XA_WINDOW, 32, PropModeReplace,
                    (unsigned char *)windows, count);
}

void atoms_update_desktop_names(void)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    /* Buffer needs to account for: 
     - MAX_WORKSPACES names
     - Each name up to sizeof(Workspace.name) - 1 = 31 chars
     - Plus null terminator for each name
     - Plus 8 characters for numeric fallback (e.g., "9\0")
     */
    char names[MAX_WORKSPACES * (32 + 1)];
    size_t offset = 0;
    size_t max_offset = sizeof(names);

    for (int i = 0; i < MAX_WORKSPACES; i++) {
        const char *name = dwn->workspaces[i].name;
        size_t remaining = max_offset - offset;
        
        if (remaining < 2) {  /* Need at least room for one char + null */
            break;
        }
        
        int written;
        if (name[0] == '\0') {
            written = snprintf(names + offset, remaining, "%d", i + 1);
        } else {
            written = snprintf(names + offset, remaining, "%s", name);
        }
        
        if (written < 0 || (size_t)written >= remaining) {
            /* Truncation or error - ensure null termination */
            names[offset] = '\0';
            offset++;
        } else {
            offset += (size_t)written + 1;  /* Include null terminator */
        }
    }

    XChangeProperty(dwn->display, dwn->root, ewmh.NET_DESKTOP_NAMES,
                    misc_atoms.UTF8_STRING, 8, PropModeReplace,
                    (unsigned char *)names, (int)offset);
}

void atoms_set_current_desktop(int desktop)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    long data = desktop;
    XChangeProperty(dwn->display, dwn->root, ewmh.NET_CURRENT_DESKTOP,
                    XA_CARDINAL, 32, PropModeReplace,
                    (unsigned char *)&data, 1);
}

void atoms_set_active_window(Window window)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    XChangeProperty(dwn->display, dwn->root, ewmh.NET_ACTIVE_WINDOW,
                    XA_WINDOW, 32, PropModeReplace,
                    (unsigned char *)&window, 1);
}

void atoms_set_number_of_desktops(int count)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    long data = count;
    XChangeProperty(dwn->display, dwn->root, ewmh.NET_NUMBER_OF_DESKTOPS,
                    XA_CARDINAL, 32, PropModeReplace,
                    (unsigned char *)&data, 1);
}


bool atoms_get_window_type(Window window, Atom *type)
{
    if (dwn == NULL || dwn->display == NULL || type == NULL) {
        return false;
    }

    Atom actual_type;
    int actual_format;
    unsigned long nitems, bytes_after;
    unsigned char *data = NULL;

    if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_WINDOW_TYPE,
                           0, 1, False, XA_ATOM,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success && data != NULL) {
        *type = *(Atom *)data;
        XFree(data);
        return true;
    }

    return false;
}

bool atoms_get_window_desktop(Window window, int *desktop)
{
    if (dwn == NULL || dwn->display == NULL || desktop == NULL) {
        return false;
    }

    Atom actual_type;
    int actual_format;
    unsigned long nitems, bytes_after;
    unsigned char *data = NULL;

    if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_DESKTOP,
                           0, 1, False, XA_CARDINAL,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success && data != NULL) {
        *desktop = *(int *)data;
        XFree(data);
        return true;
    }

    return false;
}

bool atoms_set_window_desktop(Window window, int desktop)
{
    if (dwn == NULL || dwn->display == NULL) {
        return false;
    }

    long data = desktop;
    XChangeProperty(dwn->display, window, ewmh.NET_WM_DESKTOP,
                    XA_CARDINAL, 32, PropModeReplace,
                    (unsigned char *)&data, 1);
    return true;
}

char *atoms_get_window_name(Window window)
{
    if (dwn == NULL || dwn->display == NULL) {
        return NULL;
    }

    Display *dpy = dwn->display;
    Atom actual_type;
    int actual_format;
    unsigned long nitems, bytes_after;
    unsigned char *data = NULL;

    if (XGetWindowProperty(dpy, window, ewmh.NET_WM_NAME,
                           0, 256, False, AnyPropertyType,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success && data != NULL && nitems > 0) {
        char *name = dwn_strdup((char *)data);
        XFree(data);
        return name;
    }

    if (XGetWindowProperty(dpy, window, icccm.WM_NAME,
                           0, 256, False, AnyPropertyType,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success && data != NULL && nitems > 0) {
        char *name = dwn_strdup((char *)data);
        XFree(data);
        return name;
    }

    char *name = NULL;
    if (XFetchName(dpy, window, &name) && name != NULL) {
        char *result = dwn_strdup(name);
        XFree(name);
        return result;
    }

    return dwn_strdup("Untitled");
}

bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len)
{
    if (dwn == NULL || dwn->display == NULL) {
        return false;
    }

    XClassHint hint;
    if (XGetClassHint(dwn->display, window, &hint)) {
        if (class_name != NULL && hint.res_class != NULL) {
            strncpy(class_name, hint.res_class, len - 1);
            class_name[len - 1] = '\0';
        }
        if (instance_name != NULL && hint.res_name != NULL) {
            strncpy(instance_name, hint.res_name, len - 1);
            instance_name[len - 1] = '\0';
        }
        if (hint.res_class) XFree(hint.res_class);
        if (hint.res_name) XFree(hint.res_name);
        return true;
    }

    return false;
}


bool atoms_window_supports_protocol(Window window, Atom protocol)
{
    if (dwn == NULL || dwn->display == NULL) {
        return false;
    }

    Atom *protocols = NULL;
    int count = 0;

    if (XGetWMProtocols(dwn->display, window, &protocols, &count)) {
        for (int i = 0; i < count; i++) {
            if (protocols[i] == protocol) {
                XFree(protocols);
                return true;
            }
        }
        XFree(protocols);
    }

    return false;
}

void atoms_send_protocol(Window window, Atom protocol, Time timestamp)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    XEvent ev;
    memset(&ev, 0, sizeof(ev));
    ev.type = ClientMessage;
    ev.xclient.window = window;
    ev.xclient.message_type = icccm.WM_PROTOCOLS;
    ev.xclient.format = 32;
    ev.xclient.data.l[0] = protocol;
    ev.xclient.data.l[1] = timestamp;

    XSendEvent(dwn->display, window, False, NoEventMask, &ev);
}

void atoms_send_client_message(Window window, Atom message_type,
                               long data0, long data1, long data2, long data3, long data4)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    XEvent ev;
    memset(&ev, 0, sizeof(ev));
    ev.type = ClientMessage;
    ev.xclient.window = window;
    ev.xclient.message_type = message_type;
    ev.xclient.format = 32;
    ev.xclient.data.l[0] = data0;
    ev.xclient.data.l[1] = data1;
    ev.xclient.data.l[2] = data2;
    ev.xclient.data.l[3] = data3;
    ev.xclient.data.l[4] = data4;

    XSendEvent(dwn->display, dwn->root, False,
               SubstructureNotifyMask | SubstructureRedirectMask, &ev);
}

bool atoms_update_wm_state(Window window, Atom state, bool add)
{
    if (dwn == NULL || dwn->display == NULL || window == None) {
        return false;
    }

    Atom actual_type;
    int actual_format;
    unsigned long nitems, bytes_after;
    unsigned char *data = NULL;
    Atom *states = NULL;
    int state_count = 0;

    if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_STATE,
                           0, 32, False, XA_ATOM,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success && data != NULL) {
        states = (Atom *)data;
        state_count = (int)nitems;
    }

    bool found = false;
    int found_index = -1;
    for (int i = 0; i < state_count; i++) {
        if (states[i] == state) {
            found = true;
            found_index = i;
            break;
        }
    }

    if (add && found) {
        if (data) XFree(data);
        return true;
    }
    if (!add && !found) {
        if (data) XFree(data);
        return true;
    }

    Atom new_states[32];
    int new_count = 0;

    if (add) {
        for (int i = 0; i < state_count && new_count < 31; i++) {
            new_states[new_count++] = states[i];
        }
        if (new_count < 32) {
            new_states[new_count++] = state;
        }
    } else {
        for (int i = 0; i < state_count && new_count < 32; i++) {
            if (i != found_index) {
                new_states[new_count++] = states[i];
            }
        }
    }

    if (data) XFree(data);

    if (new_count > 0) {
        XChangeProperty(dwn->display, window, ewmh.NET_WM_STATE,
                        XA_ATOM, 32, PropModeReplace,
                        (unsigned char *)new_states, new_count);
    } else {
        XDeleteProperty(dwn->display, window, ewmh.NET_WM_STATE);
    }

    return true;
}

void atoms_cleanup(void)
{
    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (ewmh_check_window != None) {
        XDestroyWindow(dwn->display, ewmh_check_window);
        ewmh_check_window = None;
        LOG_DEBUG("EWMH check window destroyed");
    }
}
