/* Copyright (c) 2013 Jess VanDerwalker <jvanderw@freedesktop.org>
 *
 * atoms.c
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <xcb/xcb.h>
#include <xcb/xcb_atom.h>
#include <xcb/xcb_icccm.h>
#include <xcb/xcb_ewmh.h>
#include <xcwm/xcwm.h>
#include "xcwm_internal.h"

/* Local functions */

/* Check to make sure another EWMH window manager is not running */
int
check_wm_cm_owner(xcwm_context_t *context);

/* Create the window required to show a EWMH compliant window manager
 * is running */
void
create_wm_cm_window(xcwm_context_t *context);

/*
  The table of xcwm_property_t represents the window properties we take note of,
  the function to update our internal state on a change of that property, and
  the event we send when that state changes.
 */
typedef struct xcwm_property_t xcwm_property_t;

typedef void (xcwm_property_change_fn_t)(xcwm_window_t *window, xcwm_property_t *property);

struct xcwm_property_t
{
    const char *name;
    xcwm_property_change_fn_t *prop_change_fn;
    xcwm_event_type_t event;
    xcb_atom_t atom;
};

/* Determine the window type */
static void
setup_window_type(xcwm_window_t *window, xcwm_property_t *property);

/* Get and set the size hints for the window */
static void
set_window_size_hints(xcwm_window_t *window, xcwm_property_t *property);

/* Get opacity hint */
static void
set_window_opacity(xcwm_window_t *window, xcwm_property_t *property);

/* Get window name */
static void
set_window_name(xcwm_window_t *window, xcwm_property_t *property);

static xcwm_property_t *property_table = NULL;
static unsigned int property_table_entries = 0;;

static xcb_atom_t
_xcwm_atom_get(xcwm_context_t *context, const char *atomName)
{
    xcb_intern_atom_reply_t *atom_reply;
    xcb_intern_atom_cookie_t atom_cookie;
    xcb_atom_t atom = XCB_NONE;

    atom_cookie = xcb_intern_atom(context->conn,
                                  0,
                                  strlen(atomName),
                                  atomName);
    atom_reply = xcb_intern_atom_reply(context->conn,
                                       atom_cookie,
                                       NULL);
    if (atom_reply) {
        atom = atom_reply->atom;
        free(atom_reply);
    }
    return atom;
}

static xcwm_property_t *
_xcwm_atom_register(xcwm_context_t *context, const char *name, xcwm_property_change_fn_t *prop_change_fn, xcwm_event_type_t event)
{
    // XXX: what if atom is already registered?
    property_table_entries++;
    property_table = realloc(property_table, property_table_entries * sizeof(struct xcwm_property_t));

    xcwm_property_t *new_property = &(property_table[property_table_entries - 1]);
    new_property->name = name;
    new_property->prop_change_fn = prop_change_fn;
    new_property->event = event;
    new_property->atom = _xcwm_atom_get(context, name);

    return new_property;
}

xcb_atom_t
xcwm_atom_register(xcwm_context_t *context, const char *atom, xcwm_event_type_t event)
{
    return _xcwm_atom_register(context, atom, NULL, event)->atom;
}

int
_xcwm_atoms_init(xcwm_context_t *context)
{
    xcb_intern_atom_cookie_t *atom_cookies;
    xcb_generic_error_t *error;

    /* Initialization for the xcb_ewmh connection and EWMH atoms */
    atom_cookies = xcb_ewmh_init_atoms(context->conn,
                                       &context->atoms.ewmh_conn);
    if (!xcb_ewmh_init_atoms_replies(&context->atoms.ewmh_conn,
                                     atom_cookies, &error)) {
        return error->major_code;;
    }

    /* Set the _NET_SUPPORTED atom for this context
     * Most of these are defined as MUSTs in the
     * EWMH standards for window managers.
     * NOTE: Starting with only a limited set of _NET_WM_STATE. This
     * may be expanded. */
    xcb_atom_t supported[] =
        {
            context->atoms.ewmh_conn.WM_PROTOCOLS,
            context->atoms.ewmh_conn._NET_SUPPORTED,
            context->atoms.ewmh_conn._NET_SUPPORTING_WM_CHECK,
            context->atoms.ewmh_conn._NET_CLOSE_WINDOW,
            context->atoms.ewmh_conn._NET_WM_NAME,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_TOOLBAR,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_MENU,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_UTILITY,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_SPLASH,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_DIALOG,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_DROPDOWN_MENU,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_POPUP_MENU,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_TOOLTIP,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_NOTIFICATION,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_COMBO,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_DND,
            context->atoms.ewmh_conn._NET_WM_WINDOW_TYPE_NORMAL,
            context->atoms.ewmh_conn._NET_WM_STATE,
            context->atoms.ewmh_conn._NET_WM_STATE_MODAL,
            context->atoms.ewmh_conn._NET_WM_STATE_HIDDEN
        };

    xcb_ewmh_set_supported(&context->atoms.ewmh_conn,
                           context->conn_screen,
                           21,  /* Length of supported[] */
                           supported);

    /* Used erroneously instead of _NET_WM_WINDOW_TYPE_SPLASH by some applications */
    context->atoms.net_wm_window_type_splashscreen = _xcwm_atom_get(context, "_NET_WM_WINDOW_TYPE_SPLASHSCREEN");

    /* Get the ICCCM atoms we need that are not included in the
     * xcb_ewmh_connection_t. */
    _xcwm_atom_register(context, "_NET_WM_NAME",           set_window_name,       XCWM_EVENT_WINDOW_NAME);
    _xcwm_atom_register(context, "WM_NAME",                set_window_name,       XCWM_EVENT_WINDOW_NAME);
    _xcwm_atom_register(context, "_NET_WM_WINDOW_TYPE",    setup_window_type,     XCWM_EVENT_WINDOW_APPEARANCE);
    _xcwm_atom_register(context, "WM_NORMAL_HINTS",        set_window_size_hints, 0);
    _xcwm_atom_register(context, "_NET_WM_WINDOW_OPACITY", set_window_opacity,    XCWM_EVENT_WINDOW_APPEARANCE);

    /* WM_DELETE_WINDOW atom */
    context->atoms.wm_delete_window_atom = _xcwm_atom_get(context, "WM_DELETE_WINDOW");

    if (!check_wm_cm_owner(context)) {
        return XCB_WINDOW;
    }
    create_wm_cm_window(context);

    /* WM_STATE atom */
    context->atoms.wm_state_atom = _xcwm_atom_get(context, "WM_STATE");

    /* WM_CHANGE_STATE atom */
    context->atoms.wm_change_state_atom = _xcwm_atom_get(context, "WM_CHANGE_STATE");

    return 0;
}

int
check_wm_cm_owner(xcwm_context_t *context)
{
    xcb_get_selection_owner_cookie_t cookie;
    xcb_window_t wm_owner;

    cookie = xcb_ewmh_get_wm_cm_owner(&context->atoms.ewmh_conn,
                                      context->conn_screen);
    xcb_ewmh_get_wm_cm_owner_reply(&context->atoms.ewmh_conn,
                                   cookie, &wm_owner, NULL);
    if (wm_owner != XCB_NONE) {
        return 0;
    }

    return 1;
}

void
create_wm_cm_window(xcwm_context_t *context)
{
    context->wm_cm_window = xcb_generate_id(context->conn);

    printf("Created WM/CM window with XID 0x%08x\n", context->wm_cm_window);

    xcb_create_window(context->conn,
                      XCB_COPY_FROM_PARENT,
                      context->wm_cm_window,
                      context->root_window->window_id,
                      0, 0, 1, 1, 0, /* 1x1 size, no border */
                      XCB_COPY_FROM_PARENT,
                      XCB_COPY_FROM_PARENT,
                      0, NULL);

    /* Set the name for the window */
    xcb_ewmh_set_wm_name(&context->atoms.ewmh_conn,
                         context->wm_cm_window,
                         strlen("xcwm"), "xcwm");

    /* Set the atoms for the window */
    xcb_ewmh_set_supporting_wm_check(&context->atoms.ewmh_conn,
                                     context->root_window->window_id,
                                     context->wm_cm_window);

    xcb_ewmh_set_supporting_wm_check(&context->atoms.ewmh_conn,
                                     context->wm_cm_window,
                                     context->wm_cm_window);

    /* Take & announce ownership of the _NET_CM_Sn selection */
    xcb_ewmh_set_wm_cm_owner(&context->atoms.ewmh_conn,
                             context->conn_screen,
                             context->wm_cm_window,
                             XCB_CURRENT_TIME,
                             0, 0);

    /* Likewise WM_Sn selection */

    /* XXX: Need to do this in a way which complies with ICCCM "Manager Selections" i.e.
     use an actual timestamp, so in the case of races, we either acquire selection or don't */
}

void
_xcwm_atoms_init_window(xcwm_window_t *window)
{
    _xcwm_atoms_set_wm_delete(window);

    /* Put the value of all properties we consider into effect */
    unsigned int i;
    for (i = 0; i < property_table_entries; i++)
    {
        if (property_table[i].prop_change_fn)
            (property_table[i].prop_change_fn)(window, &(property_table[i]));
    }
}

int
_xcwm_atom_change_to_event(xcb_atom_t atom, xcwm_window_t *window, xcwm_event_type_t *event)
{
    unsigned int i;
    for (i = 0; i < property_table_entries; i++) {
        if (property_table[i].atom == atom) {
            /* Take the value into consideration */
            if (property_table[i].prop_change_fn)
                (property_table[i].prop_change_fn)(window, &(property_table[i]));

            /* and translate to XCWM_ event */
            *event = property_table[i].event;
            return 1;
        }
    }

    return 0;
}

void
set_window_name(xcwm_window_t *window, xcwm_property_t *property)
{
    xcb_get_property_cookie_t cookie;
    xcb_icccm_get_text_property_reply_t reply;
    xcb_ewmh_get_utf8_strings_reply_t data;

    /* Check _NET_WM_NAME first */
    cookie = xcb_ewmh_get_wm_name(&window->context->atoms.ewmh_conn,
                                  window->window_id);
    if (xcb_ewmh_get_wm_name_reply(&window->context->atoms.ewmh_conn,
                                   cookie, &data, NULL)) {
        window->name = strndup(data.strings, data.strings_len);
        xcb_ewmh_get_utf8_strings_reply_wipe(&data);
        return;
    }

    cookie = xcb_icccm_get_wm_name(window->context->conn, window->window_id);
    if (!xcb_icccm_get_wm_name_reply(window->context->conn,
                                     cookie, &reply, NULL)) {
        window->name = malloc(sizeof(char));
        window->name[0] = '\0';
        return;
    }

    window->name = strndup(reply.name, reply.name_len);
    xcb_icccm_get_text_property_reply_wipe(&reply);
}


void
_xcwm_atoms_set_wm_delete(xcwm_window_t *window)
{
    xcb_get_property_cookie_t cookie;
    xcb_icccm_get_wm_protocols_reply_t reply;
    xcb_generic_error_t *error;
    int i;

    /* Get the WM_PROTOCOLS */
    cookie = xcb_icccm_get_wm_protocols(window->context->conn,
                                        window->window_id,
                                        window->context->atoms.ewmh_conn.WM_PROTOCOLS);

    if (xcb_icccm_get_wm_protocols_reply(window->context->conn,
                                         cookie, &reply, &error) == 1) {
        /* See if the WM_DELETE_WINDOW is set in WM_PROTOCOLS */
        for (i = 0; i < reply.atoms_len; i++) {
            if (reply.atoms[i] == window->context->atoms.wm_delete_window_atom) {
                window->wm_delete_set = 1;
                break;
            }
        }
    } else {
        window->wm_delete_set = 0;
        return;
    }
    xcb_icccm_get_wm_protocols_reply_wipe(&reply);

    return;
}

static void
setup_window_type(xcwm_window_t *window, xcwm_property_t *property)
{
    xcb_get_property_cookie_t cookie;
    xcb_window_t transient;
    xcb_ewmh_get_atoms_reply_t type;
    xcb_ewmh_connection_t ewmh_conn = window->context->atoms.ewmh_conn;
    int i;

    /* if nothing below matches, set the default to unknown */
    window->type = XCWM_WINDOW_TYPE_UNKNOWN;

    /* Get the window this one is transient for */
    cookie = xcb_icccm_get_wm_transient_for(window->context->conn,
                                            window->window_id);
    if (xcb_icccm_get_wm_transient_for_reply(window->context->conn, cookie,
                                             &transient, NULL)) {
        window->transient_for = _xcwm_get_window_node_by_window_id(transient);
        window->type = XCWM_WINDOW_TYPE_DIALOG;
        // not if override-redirect
    } else {
        window->transient_for = NULL;
        window->type = XCWM_WINDOW_TYPE_NORMAL;
    }

    /* Check and see if the client has set the _NET_WM_WINDOW_TYPE
     * atom. Since the "type" is a list of window types, ordered by
     * preference, we need to loop through to make sure we get a
     * match. */
    cookie = xcb_ewmh_get_wm_window_type(&ewmh_conn, window->window_id);
    if (xcb_ewmh_get_wm_window_type_reply(&ewmh_conn, cookie, &type, NULL)) {
        for (i = 0; i < type.atoms_len; i++) {
            if (type.atoms[i] ==  ewmh_conn._NET_WM_WINDOW_TYPE_DESKTOP) {
                window->type = XCWM_WINDOW_TYPE_DESKTOP;
                break;
            } else if (type.atoms[i] ==  ewmh_conn._NET_WM_WINDOW_TYPE_DOCK) {
                window->type = XCWM_WINDOW_TYPE_DOCK;
                break;
            } else if (type.atoms[i] ==  ewmh_conn._NET_WM_WINDOW_TYPE_TOOLBAR) {
                window->type = XCWM_WINDOW_TYPE_TOOLBAR;
                break;
            } else if (type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_MENU) {
                window->type = XCWM_WINDOW_TYPE_MENU;
                break;
            } else if (type.atoms[i]
                       == ewmh_conn._NET_WM_WINDOW_TYPE_UTILITY) {
                window->type = XCWM_WINDOW_TYPE_UTILITY;
                break;
            } else if ((type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_SPLASH) ||
                       (type.atoms[i] == window->context->atoms.net_wm_window_type_splashscreen)) {
                window->type = XCWM_WINDOW_TYPE_SPLASH;
                break;
            } else if (type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_DIALOG) {
                window->type = XCWM_WINDOW_TYPE_DIALOG;
                break;
            } else if (type.atoms[i]
                       == ewmh_conn._NET_WM_WINDOW_TYPE_DROPDOWN_MENU) {
                window->type = XCWM_WINDOW_TYPE_DROPDOWN_MENU;
                break;
            } else if (type.atoms[i]
                       == ewmh_conn._NET_WM_WINDOW_TYPE_POPUP_MENU) {
                window->type = XCWM_WINDOW_TYPE_POPUP_MENU;
                break;
            } else if (type.atoms[i]
                       == ewmh_conn._NET_WM_WINDOW_TYPE_TOOLTIP) {
                window->type = XCWM_WINDOW_TYPE_TOOLTIP;
                break;
            } else if (type.atoms[i]
                       == ewmh_conn._NET_WM_WINDOW_TYPE_NOTIFICATION) {
                window->type = XCWM_WINDOW_TYPE_NOTIFICATION;
                break;
            } else if (type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_COMBO) {
                window->type = XCWM_WINDOW_TYPE_COMBO;
                break;
            } else if (type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_DND) {
                window->type = XCWM_WINDOW_TYPE_DND;
                break;
            } else if (type.atoms[i] == ewmh_conn._NET_WM_WINDOW_TYPE_NORMAL) {
                window->type = XCWM_WINDOW_TYPE_NORMAL;
                break;
            }
        }
    }
}

void
set_window_size_hints(xcwm_window_t *window, xcwm_property_t *property)
{
    xcb_get_property_cookie_t cookie;
    cookie = xcb_icccm_get_wm_normal_hints(window->context->conn,
                                           window->window_id);
    if (!xcb_icccm_get_wm_normal_hints_reply(window->context->conn,
                                             cookie, &(window->size_hints), NULL)) {
        /* Use 0 for all values (as set in calloc), or previous values */
        return;
    }
}

void
set_window_opacity(xcwm_window_t *window, xcwm_property_t *property)
{
  xcb_get_property_cookie_t cookie;
  cookie = xcb_get_property(window->context->conn, 0, window->window_id, property->atom, XCB_ATOM_CARDINAL, 0L, 4L);

  xcb_get_property_reply_t *reply = xcb_get_property_reply(window->context->conn, cookie, NULL);
  if (reply)
    {
      int nitems = xcb_get_property_value_length(reply);
      uint32_t *value = xcb_get_property_value(reply);

      if (value && nitems == 4)
        {
          window->opacity = *value;
        }

      free(reply);
    }
}

void
_xcwm_atoms_set_wm_state(xcwm_window_t *window, xcwm_window_state_t state)
{
    printf("_xcwm_atoms_set_wm_state: XID 0x%08x state %d\n",
           window->window_id, state);

    /* xcb_icccm_wm_state_t icccm_state; */

    uint32_t icccm_state[2];
    xcb_atom_t *ewmh_state = NULL;
    int ewmh_atom_cnt = 0;

    switch (state) {
    case XCWM_WINDOW_STATE_NORMAL:
    {
        icccm_state[0] = XCB_ICCCM_WM_STATE_NORMAL;
        icccm_state[1] = XCB_NONE;
        break;
    }
    case XCWM_WINDOW_STATE_ICONIC:
    {
        ewmh_atom_cnt = 1;
        icccm_state[0] = XCB_ICCCM_WM_STATE_ICONIC;
        icccm_state[1] = XCB_NONE;

        ewmh_state = calloc(ewmh_atom_cnt, sizeof(xcb_atom_t));
        ewmh_state[0] = window->context->atoms.ewmh_conn._NET_WM_STATE_HIDDEN;
        break;
    }
    case XCWM_WINDOW_STATE_WITHDRAWN:
    {
        icccm_state[0] = XCB_ICCCM_WM_STATE_WITHDRAWN;
        icccm_state[1] = XCB_NONE;
        break;
    }
    default:
    {
        /* No need to attempt to update the state */
        return;
    }
    }

    /* Only set for top-level windows */
    if (!window->transient_for && !window->override_redirect) {
        xcb_change_property(window->context->conn,
                            XCB_PROP_MODE_REPLACE,
                            window->window_id,
                            window->context->atoms.wm_state_atom,
                            window->context->atoms.wm_state_atom,
                            32,
                            2,
                            icccm_state);
    }

    xcb_ewmh_set_wm_state(&window->context->atoms.ewmh_conn,
                          window->window_id,
                          ewmh_atom_cnt,
                          ewmh_state);

    xcb_flush(window->context->conn);

    if (ewmh_state) {
        free(ewmh_state);
    }
}

xcwm_window_state_t
_xcwm_atoms_get_wm_state(xcwm_window_t *window)
{
    xcwm_window_state_t state = XCWM_WINDOW_STATE_WITHDRAWN;
    xcb_get_property_cookie_t cookie;

    cookie = xcb_get_property(window->context->conn,
                              0,
                              window->window_id,
                              window->context->atoms.wm_state_atom,
                              window->context->atoms.wm_state_atom,
                              0L,
                              1L);

    xcb_get_property_reply_t *reply = xcb_get_property_reply(window->context->conn, cookie, NULL);
    if (reply) {
        int length = xcb_get_property_value_length(reply);
        uint32_t *value = xcb_get_property_value(reply);

        if (value && length == 4) {
            switch (*value) {
            case XCB_ICCCM_WM_STATE_NORMAL:
                state = XCWM_WINDOW_STATE_NORMAL;
                break;

            case XCB_ICCCM_WM_STATE_ICONIC:
                state = XCWM_WINDOW_STATE_ICONIC;
                break;

            default:
                printf("window XID %08x has an unknown WM_STATE value %d\n", window->window_id, *value);
                /* fall through */

            case XCB_ICCCM_WM_STATE_WITHDRAWN:
                state = XCWM_WINDOW_STATE_WITHDRAWN;
                break;
            }
        }

        free(reply);
    }

    return state;
}

void
_xcwm_atoms_release(xcwm_context_t *context)
{

    /* Free the xcb_ewmh_connection_t */
    xcb_ewmh_connection_wipe(&context->atoms.ewmh_conn);

    /* Close the wm window */
    xcb_destroy_window(context->conn, context->wm_cm_window);
}
