/*
 * xkb.c - keyboard layout control functions
 *
 * Copyright © 2015 Aleksey Fedotov <lexa@cfotr.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

/**
 * @module awesome
 */

#include "xkb.h"
#include "globalconf.h"
#include "xwindow.h"
#include "objects/client.h"
#include "common/atoms.h"

#include <xcb/xkb.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>

/**
 * Switch keyboard layout.
 *
 * @staticfct xkb_set_layout_group
 * @tparam integer num keyboard layout number, integer from 0 to 3
 */
int
luaA_xkb_set_layout_group(lua_State *L)
{
    unsigned group = luaL_checkinteger(L, 1);
    if (!globalconf.have_xkb)
    {
        luaA_warn(L, "XKB not supported");
        return 0;
    }
    xcb_xkb_latch_lock_state (globalconf.connection, XCB_XKB_ID_USE_CORE_KBD,
                              0, 0, true, group, 0, 0, 0);
    return 0;
}

/**
 * Get current layout number.
 *
 * @staticfct xkb_get_layout_group
 * @treturn integer num Current layout number, integer from 0 to 3.
 */
int
luaA_xkb_get_layout_group(lua_State *L)
{
    if (!globalconf.have_xkb)
    {
        luaA_warn(L, "XKB not supported");
        return 0;
    }

    xcb_xkb_get_state_cookie_t state_c;
    state_c = xcb_xkb_get_state_unchecked (globalconf.connection,
                                           XCB_XKB_ID_USE_CORE_KBD);
    xcb_xkb_get_state_reply_t* state_r;
    state_r = xcb_xkb_get_state_reply (globalconf.connection,
                                       state_c, NULL);
    if (!state_r)
    {
        free(state_r);
        return 0;
    }
    lua_pushinteger(L, state_r->group);
    free(state_r);
    return 1;
}

/**
 * Get layout short names.
 *
 * @staticfct xkb_get_group_names
 * @treturn string A string describing the current layout settings,
 *   e.g.: 'pc+us+de:2+inet(evdev)+group(alt_shift_toggle)+ctrl(nocaps)'
 */
int
luaA_xkb_get_group_names(lua_State *L)
{
    if (!globalconf.have_xkb)
    {
        luaA_warn(L, "XKB not supported");
        return 0;
    }

    xcb_xkb_get_names_cookie_t name_c;
    name_c = xcb_xkb_get_names_unchecked (globalconf.connection,
                                          XCB_XKB_ID_USE_CORE_KBD,
                                          XCB_XKB_NAME_DETAIL_SYMBOLS);
    xcb_xkb_get_names_reply_t* name_r;
    name_r = xcb_xkb_get_names_reply (globalconf.connection, name_c, NULL);

    if (!name_r)
    {
        luaA_warn(L, "Failed to get xkb symbols name");
        return 0;
    }

    xcb_xkb_get_names_value_list_t name_list;
    void *buffer = xcb_xkb_get_names_value_list(name_r);
    xcb_xkb_get_names_value_list_unpack (
        buffer, name_r->nTypes, name_r->indicators,
        name_r->virtualMods, name_r->groupNames, name_r->nKeys,
        name_r->nKeyAliases, name_r->nRadioGroups, name_r->which,
        &name_list);

    xcb_get_atom_name_cookie_t atom_name_c;
    atom_name_c = xcb_get_atom_name_unchecked(globalconf.connection, name_list.symbolsName);
    xcb_get_atom_name_reply_t *atom_name_r;
    atom_name_r = xcb_get_atom_name_reply(globalconf.connection, atom_name_c, NULL);
    if (!atom_name_r) {
        luaA_warn(L, "Failed to get atom symbols name");
        free(name_r);
        return 0;
    }

    const char *name = xcb_get_atom_name_name(atom_name_r);
    size_t name_len = xcb_get_atom_name_name_length(atom_name_r);
    lua_pushlstring(L, name, name_len);

    free(atom_name_r);
    free(name_r);
    return 1;
}

static bool
fill_rmlvo_from_root(struct xkb_rule_names *xkb_names)
{
    xcb_get_property_reply_t *prop_reply = xcb_get_property_reply(globalconf.connection,
            xcb_get_property_unchecked(globalconf.connection, false, globalconf.screen->root, _XKB_RULES_NAMES, XCB_GET_PROPERTY_TYPE_ANY, 0, UINT_MAX),
            NULL);
    if (!prop_reply)
        return false;

    if (prop_reply->value_len == 0)
    {
        p_delete(&prop_reply);
        return false;
    }

    const char *walk = (const char *) xcb_get_property_value(prop_reply);
    unsigned int remaining = xcb_get_property_value_length(prop_reply);
    for (int i = 0; i < 5 && remaining > 0; i++)
    {
        const int len = strnlen(walk, remaining);
        switch (i) {
        case 0:
            xkb_names->rules = strndup(walk, len);
            break;
        case 1:
            xkb_names->model = strndup(walk, len);
            break;
        case 2:
            xkb_names->layout = strndup(walk, len);
            break;
        case 3:
            xkb_names->variant = strndup(walk, len);
            break;
        case 4:
            xkb_names->options = strndup(walk, len);
            break;
        }
        remaining -= len + 1;
        walk = &walk[len + 1];
    }

    p_delete(&prop_reply);
    return true;
}

/** Fill globalconf.xkb_state based on connection and context
*/
static void
xkb_fill_state(void)
{
    xcb_connection_t *conn = globalconf.connection;

    int32_t device_id = -1;
    if (globalconf.have_xkb)
    {
        device_id = xkb_x11_get_core_keyboard_device_id(conn);
        if (device_id == -1)
            warn("Failed while getting XKB device id");
    }

    if (device_id != -1)
    {
        struct xkb_keymap *xkb_keymap = xkb_x11_keymap_new_from_device(
                                    globalconf.xkb_ctx,
                                    conn,
                                    device_id,
                                    XKB_KEYMAP_COMPILE_NO_FLAGS);


        if (!xkb_keymap)
            fatal("Failed while getting XKB keymap from device");

        globalconf.xkb_state = xkb_x11_state_new_from_device(xkb_keymap,
                                                             conn,
                                                             device_id);
        if (!globalconf.xkb_state)
            fatal("Failed while getting XKB state from device");

        /* xkb_keymap is no longer referenced directly; decreasing refcount */
        xkb_keymap_unref(xkb_keymap);
    }
    else
    {
        struct xkb_rule_names names = { NULL, NULL, NULL, NULL, NULL };
        if (!fill_rmlvo_from_root(&names))
            warn("Could not get _XKB_RULES_NAMES from root window, falling back to defaults.");

        struct xkb_keymap *xkb_keymap = xkb_keymap_new_from_names(globalconf.xkb_ctx, &names, 0);

        globalconf.xkb_state = xkb_state_new(xkb_keymap);
        if (!globalconf.xkb_state)
            fatal("Failed while creating XKB state");

        /* xkb_keymap is no longer referenced directly; decreasing refcount */
        xkb_keymap_unref(xkb_keymap);
        p_delete(&names.rules);
        p_delete(&names.model);
        p_delete(&names.layout);
        p_delete(&names.variant);
        p_delete(&names.options);
    }
}


/** Loads xkb context, state and keymap to globalconf.
 * These variables should be freed by xkb_free_keymap() afterwards
*/
static void
xkb_init_keymap(void)
{
    globalconf.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    if (!globalconf.xkb_ctx)
        fatal("Failed while getting XKB context");

    xkb_fill_state();
}

/** Frees xkb context, state and keymap from globalconf.
 * This should be used when these variables will not be used anymore
 */
static void
xkb_free_keymap(void)
{
    xkb_state_unref(globalconf.xkb_state);
    xkb_context_unref(globalconf.xkb_ctx);
}

/** Rereads the state of keyboard from X.
 * This call should be used after receiving NewKeyboardNotify or MapNotify,
 * as written in http://xkbcommon.org/doc/current/group__x11.html
 */
static void
xkb_reload_keymap(void)
{
    assert(globalconf.have_xkb);

    xkb_state_unref(globalconf.xkb_state);
    xkb_fill_state();

    /* Free and then allocate the key symbols */
    xcb_key_symbols_free(globalconf.keysyms);
    globalconf.keysyms = xcb_key_symbols_alloc(globalconf.connection);

    /* Regrab key bindings on the root window */
    xcb_screen_t *s = globalconf.screen;
    xwindow_grabkeys(s->root, &globalconf.keys);

    /* Regrab key bindings on clients */
    foreach(_c, globalconf.clients)
    {
        client_t *c = *_c;
        xwindow_grabkeys(c->window, &c->keys);
        if (c->nofocus_window)
            xwindow_grabkeys(c->nofocus_window, &c->keys);
    }
}

static gboolean
xkb_refresh(gpointer unused)
{
    lua_State *L = globalconf_get_lua_State();

    globalconf.xkb_update_pending = false;
    if (globalconf.xkb_reload_keymap)
        xkb_reload_keymap();
    if (globalconf.xkb_map_changed)
        signal_object_emit(L, &global_signals, "xkb::map_changed", 0);
    if (globalconf.xkb_group_changed)
        signal_object_emit(L, &global_signals, "xkb::group_changed", 0);

    globalconf.xkb_reload_keymap = false;
    globalconf.xkb_map_changed = false;
    globalconf.xkb_group_changed = false;

    return G_SOURCE_REMOVE;
}

static void
xkb_schedule_refresh(void)
{
    if (globalconf.xkb_update_pending)
        return;
    globalconf.xkb_update_pending = true;
    g_idle_add_full(G_PRIORITY_LOW, xkb_refresh, NULL, NULL);
}

/** The xkb notify event handler.
 * \param event The event.
 */
void
event_handle_xkb_notify(xcb_generic_event_t* event)
{
    assert(globalconf.have_xkb);

    /* The pad0 field of xcb_generic_event_t contains the event sub-type,
     * unfortunately xkb doesn't provide a usable struct for getting this in a
     * nicer way*/
    switch (event->pad0)
    {
      case XCB_XKB_NEW_KEYBOARD_NOTIFY:
        {
          xcb_xkb_new_keyboard_notify_event_t *new_keyboard_event = (void*)event;

          globalconf.xkb_reload_keymap = true;

          if (new_keyboard_event->changed & XCB_XKB_NKN_DETAIL_KEYCODES)
              globalconf.xkb_map_changed = true;
          xkb_schedule_refresh();
          break;
        }
      case XCB_XKB_MAP_NOTIFY:
        {
          globalconf.xkb_reload_keymap = true;
          globalconf.xkb_map_changed = true;
          xkb_schedule_refresh();
          break;
        }
      case XCB_XKB_STATE_NOTIFY:
        {
          xcb_xkb_state_notify_event_t *state_notify_event = (void*)event;

          xkb_state_update_mask(globalconf.xkb_state,
                                state_notify_event->baseMods,
                                state_notify_event->latchedMods,
                                state_notify_event->lockedMods,
                                state_notify_event->baseGroup,
                                state_notify_event->latchedGroup,
                                state_notify_event->lockedGroup);

          if (state_notify_event->changed & XCB_XKB_STATE_PART_GROUP_STATE)
          {
              globalconf.xkb_group_changed = true;
              xkb_schedule_refresh();
          }

          break;
        }
    }
}

/** Initialize XKB support
 * This call allocates resources, that should be freed by calling xkb_free()
 */
void
xkb_init(void)
{
    globalconf.xkb_update_pending = false;
    globalconf.xkb_reload_keymap = false;
    globalconf.xkb_map_changed = false;
    globalconf.xkb_group_changed = false;

    int success_xkb = xkb_x11_setup_xkb_extension(globalconf.connection,
                                              XKB_X11_MIN_MAJOR_XKB_VERSION,
                                              XKB_X11_MIN_MINOR_XKB_VERSION,
                                              0,
                                              NULL,
                                              NULL,
                                              NULL,
                                              NULL);

    globalconf.have_xkb = success_xkb;

    if (!success_xkb) {
        warn("XKB not found or not supported");
        xkb_init_keymap();
        return;
    }

    uint16_t map = XCB_XKB_EVENT_TYPE_STATE_NOTIFY | XCB_XKB_EVENT_TYPE_MAP_NOTIFY | XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY;

    //
    // These maps are provided to allow key remapping,
    // that could be used in awesome
    //
    uint16_t map_parts = XCB_XKB_MAP_PART_KEY_TYPES |
                         XCB_XKB_MAP_PART_KEY_SYMS |
                         XCB_XKB_MAP_PART_MODIFIER_MAP |
                         XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
                         XCB_XKB_MAP_PART_KEY_ACTIONS |
                         XCB_XKB_MAP_PART_KEY_BEHAVIORS |
                         XCB_XKB_MAP_PART_VIRTUAL_MODS |
                         XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;

    /* Enable detectable auto-repeat, but ignore failures */
    xcb_discard_reply(globalconf.connection,
            xcb_xkb_per_client_flags(globalconf.connection,
                                     XCB_XKB_ID_USE_CORE_KBD,
                                     XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT,
                                     XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT,
                                     0,
                                     0,
                                     0)
            .sequence);

    xcb_xkb_select_events(globalconf.connection,
                          XCB_XKB_ID_USE_CORE_KBD,
                          map,
                          0,
                          map,
                          map_parts,
                          map_parts,
                          0);

    /* load keymap to use when resolving keypresses */
    xkb_init_keymap();
}

/** Frees resources allocated by xkb_init()
 */
void
xkb_free(void)
{
    if (globalconf.have_xkb)
        // unsubscribe from all events
        xcb_xkb_select_events(globalconf.connection,
                              XCB_XKB_ID_USE_CORE_KBD,
                              0,
                              0,
                              0,
                              0,
                              0,
                              0);
    xkb_free_keymap();
}

// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80