/*  InputLeap -- mouse and keyboard sharing utility
    Copyright (C) InputLeap contributors

    This package is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    found in the file LICENSE that should have accompanied this file.

    This package 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, see <http://www.gnu.org/licenses/>.
*/

#include "platform/EiKeyState.h"
#include "platform/XKBUtil.h"

#include "base/Log.h"

#include <cstddef>
#include <algorithm>
#include <memory>
#include <unistd.h>

#include <xkbcommon/xkbcommon.h>

namespace inputleap {

EiKeyState::EiKeyState(EiScreen* screen, IEventQueue* events) :
    KeyState(events),
    screen_{screen}
{
    xkb_ = xkb_context_new(XKB_CONTEXT_NO_FLAGS);

    // FIXME: PrimaryClient->enable() calls into our keymap, so we must have
    // one during initial startup - even before we know what our actual keymap is.
    // Once we get the actual keymap from EIS, we swap it out so hopefully that's enough.
    init_default_keymap();
}

void EiKeyState::init_default_keymap()
{
    if (xkb_keymap_) {
        xkb_keymap_unref(xkb_keymap_);
    }
    xkb_keymap_ = xkb_keymap_new_from_names(xkb_, nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS);

    if (xkb_state_) {
        xkb_state_unref(xkb_state_);
    }
    xkb_state_ = xkb_state_new(xkb_keymap_);
}

void EiKeyState::init(int fd, size_t len)
{
    auto buffer = std::make_unique<char[]>(len + 1);
    lseek(fd, 0, SEEK_SET);
    auto sz = read(fd, buffer.get(), len);

    if ((size_t)sz < len) {
        LOG_NOTE("Failed to create XKB context: %s", strerror(errno));
        return;
    }

    // See xkbcommon/libxkbcommon issue #307, xkb_keymap_new_from_buffer fails if we have a
    // terminating null byte. Since we can't control whether the other end sends that byte,
    // enforce null-termination in our buffer and pass the whole thing as string.

    buffer[len] = '\0'; // guarantee null-termination
    auto keymap = xkb_keymap_new_from_string(xkb_, buffer.get(),
                                             XKB_KEYMAP_FORMAT_TEXT_V1,
                                             XKB_KEYMAP_COMPILE_NO_FLAGS);
    if (!keymap) {
        LOG_NOTE("Failed to compile keymap, falling back to defaults");
        // Falling back to layout "us" is a lot more useful than segfaulting
        init_default_keymap();
        return;
    }

    if (xkb_keymap_) {
        xkb_keymap_unref(xkb_keymap_);
    }
    xkb_keymap_ = keymap;

    if (xkb_state_) {
        xkb_state_unref(xkb_state_);
    }
    xkb_state_ = xkb_state_new(xkb_keymap_);
}


EiKeyState::~EiKeyState()
{
    xkb_context_unref(xkb_);
    xkb_keymap_unref(xkb_keymap_);
    xkb_state_unref(xkb_state_);
}

bool EiKeyState::fakeCtrlAltDel()
{
    // pass keys through unchanged
    return false;
}

KeyModifierMask EiKeyState::pollActiveModifiers() const
{
    std::uint32_t xkb_mask = xkb_state_serialize_mods(xkb_state_, XKB_STATE_MODS_EFFECTIVE);
    return convert_mod_mask(xkb_mask);
}

std::int32_t EiKeyState::pollActiveGroup() const
{
    return xkb_state_serialize_layout(xkb_state_, XKB_STATE_LAYOUT_EFFECTIVE);
}

void EiKeyState::pollPressedKeys(KeyButtonSet& pressedKeys) const
{
    // FIXME
    return;
}

std::uint32_t EiKeyState::convert_mod_mask(std::uint32_t xkb_mask) const
{
    std::uint32_t barrier_mask = 0;

    for (xkb_mod_index_t xkbmod = 0; xkbmod < xkb_keymap_num_mods(xkb_keymap_); xkbmod++) {
        if ((xkb_mask & (1 << xkbmod)) == 0)
            continue;

        const char *name = xkb_keymap_mod_get_name(xkb_keymap_, xkbmod);
        if (strcmp(XKB_MOD_NAME_SHIFT, name) == 0)
            barrier_mask |= (1 << kKeyModifierBitShift);
        else if (strcmp(XKB_MOD_NAME_CAPS, name) == 0)
            barrier_mask |= (1 << kKeyModifierBitCapsLock);
        else if (strcmp(XKB_MOD_NAME_CTRL, name) == 0)
            barrier_mask |= (1 << kKeyModifierBitControl);
        else if (strcmp(XKB_MOD_NAME_ALT, name) == 0)
            barrier_mask |= (1 << kKeyModifierBitAlt);
        else if (strcmp(XKB_MOD_NAME_LOGO, name) == 0)
            barrier_mask |= (1 << kKeyModifierBitSuper);
    }

    return barrier_mask;
}

// Only way to figure out whether a key is a modifier key is to press it,
// check if a modifier changed state and then release it again.
// Luckily xkbcommon allows us to do this in a separate state
void EiKeyState::assign_generated_modifiers(std::uint32_t keycode, inputleap::KeyMap::KeyItem& item)
{
    std::uint32_t mods_generates = 0;
    auto state = xkb_state_new(xkb_keymap_);
    enum xkb_state_component changed = xkb_state_update_key(state, keycode, XKB_KEY_DOWN);

    if (changed) {
        for (xkb_mod_index_t m = 0; m < xkb_keymap_num_mods(xkb_keymap_); m++) {
            if (xkb_state_mod_index_is_active(state, m, XKB_STATE_MODS_LOCKED))
                item.m_lock = true;

            if (xkb_state_mod_index_is_active(state, m, XKB_STATE_MODS_EFFECTIVE)) {
                mods_generates |= (1 << m);
            }
        }
    }
    xkb_state_update_key(state, keycode, XKB_KEY_UP);
    xkb_state_unref(state);

    item.m_generates = convert_mod_mask(mods_generates);
}

void EiKeyState::getKeyMap(inputleap::KeyMap& keyMap)
{
    auto min_keycode = xkb_keymap_min_keycode(xkb_keymap_);
    auto max_keycode = xkb_keymap_max_keycode(xkb_keymap_);

    // X keycodes are evdev keycodes + 8 (libei gives us evdev keycodes)
    for (auto keycode = min_keycode; keycode <= max_keycode; keycode++) {

        // skip keys with no groups (they generate no symbols)
        if (xkb_keymap_num_layouts_for_key(xkb_keymap_, keycode) == 0)
            continue;

        for (auto group = 0U; group < xkb_keymap_num_layouts(xkb_keymap_); group++) {
            for (auto level = 0U;
                 level < xkb_keymap_num_levels_for_key(xkb_keymap_, keycode, group);
                 level++) {
                const xkb_keysym_t *syms;
                xkb_mod_mask_t masks[64];
                auto nmasks = xkb_keymap_key_get_mods_for_level(xkb_keymap_, keycode, group,
                                                                level, masks, 64);
                auto nsyms = xkb_keymap_key_get_syms_by_level(xkb_keymap_, keycode, group, level, &syms);

                if (nsyms == 0)
                    continue;

                if (nsyms > 1)
                    LOG_WARN(" Multiple keysyms per keycode are not supported, keycode %d", keycode);

                inputleap::KeyMap::KeyItem item{};
                xkb_keysym_t keysym = syms[0];
                KeySym sym = static_cast<KeyID>(keysym);
                item.m_id = XKBUtil::mapKeySymToKeyID(sym);
                item.m_button   = static_cast<KeyButton>(keycode) - 8; // X keycode offset
                item.m_group = group;

                // For debugging only
                char keysym_name[128] = {0};
                xkb_keysym_get_name(keysym, keysym_name, sizeof(keysym_name));

                // Set to all modifiers this key may be affected by
                uint32_t mods_sensitive = 0;
                for (auto n = 0U; n < nmasks; n++) {
                    mods_sensitive |= masks[n];
                }
                item.m_sensitive = convert_mod_mask(mods_sensitive);

                uint32_t mods_required = 0;
                for (std::size_t m = 0; m < nmasks; m++) {
                    mods_required |= masks[m];
                }
                item.m_required = convert_mod_mask(mods_required);

                assign_generated_modifiers(keycode, item);

                // add capslock version of key is sensitive to capslock
                if (item.m_sensitive & KeyModifierShift && item.m_sensitive & KeyModifierCapsLock) {
                    item.m_required &= ~KeyModifierShift;
                    item.m_required |=  KeyModifierCapsLock;
                    keyMap.addKeyEntry(item);
                    item.m_required |=  KeyModifierShift;
                    item.m_required &= ~KeyModifierCapsLock;
                }

                keyMap.addKeyEntry(item);
            }
        }
    }

    // allow composition across groups
    keyMap.allowGroupSwitchDuringCompose();
}

void EiKeyState::fakeKey(const Keystroke& keystroke)
{
    switch (keystroke.m_type) {
    case Keystroke::kButton:
        LOG_DEBUG1("  %03x (%08x) %s", keystroke.m_data.m_button.m_button,
             keystroke.m_data.m_button.m_client,
             keystroke.m_data.m_button.m_press ? "down" : "up");
        screen_->fakeKey(keystroke.m_data.m_button.m_button,
                         keystroke.m_data.m_button.m_press);
        break;
    default:
        break;
    }
}

KeyID EiKeyState::map_key_from_keyval(uint32_t keyval) const
{
    LOG_DEBUG1("map_key_from_keyval keyval=%d", keyval);

    // FIXME: That might be a bit crude...?
    xkb_keysym_t xkb_keysym = xkb_state_key_get_one_sym(xkb_state_, keyval);
    KeySym keysym = static_cast<KeySym>(xkb_keysym);
    LOG_DEBUG1("mapped code=%d to keysym=0x%04lx", keyval, keysym);

    KeyID keyid = XKBUtil::mapKeySymToKeyID(keysym);
    LOG_DEBUG1("mapped keysym=0x%04lx to keyID=%d", keysym, keyid);

    return keyid;
}

void EiKeyState::update_xkb_state(uint32_t keyval, bool is_pressed)
{
    LOG_DEBUG1("update_xkb_state keyval=%d pressed=%i", keyval, is_pressed);
    xkb_state_update_key(xkb_state_, keyval, is_pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
}

} // namespace inputleap
