/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "decompose.h"
#include "keyboard.h"
#include "keymap.h"
#include "spice.h"
#include "spice-constants.h"

#include <guacamole/client.h>

#include <stdlib.h>

/**
 * Translates the given keysym into the corresponding lock flag, as would be
 * required by the SPICE synchronize event. If the given keysym does not
 * represent a lock key, zero is returned.
 *
 * @param keysym
 *     The keysym to translate into a SPICE lock flag.
 *
 * @return
 *     The SPICE lock flag which corresponds to the given keysym, or zero if the
 *     given keysym does not represent a lock key.
 */
static int guac_spice_keyboard_lock_flag(int keysym) {

    /* Translate keysym into corresponding lock flag */
    switch (keysym) {

        /* Scroll lock */
        case GUAC_SPICE_KEYSYM_SCROLL_LOCK:
            return SPICE_INPUTS_SCROLL_LOCK;

        /* Num lock */
        case GUAC_SPICE_KEYSYM_NUM_LOCK:
            return SPICE_INPUTS_NUM_LOCK;

        /* Caps lock */
        case GUAC_SPICE_KEYSYM_CAPS_LOCK:
            return SPICE_INPUTS_CAPS_LOCK;

    }

    /* Not a lock key */
    return 0;

}

/**
 * Immediately sends an SPICE key event having the given scancode and flags.
 *
 * @param spice_client
 *     The SPICE client instance associated with the SPICE session along which the
 *     key event should be sent.
 *
 * @param scancode
 *     The scancode of the key to press or release via the SPICE key event.
 *
 * @param flags
 *     Any SPICE-specific flags required for the provided scancode to have the
 *     intended meaning, such as KBD_FLAGS_EXTENDED. The possible flags and
 *     their meanings are dictated by SPICE. KBD_FLAGS_DOWN and KBD_FLAGS_UP
 *     need not be specified here - they will automatically be added depending
 *     on the value specified for the pressed parameter.
 *
 * @param pressed
 *     Non-zero if the key is being pressed, zero if the key is being released.
 */
static void guac_spice_send_key_event(guac_spice_client* spice_client,
        int scancode, int flags, int pressed) {

    /* Send actual key press or release */
    pthread_mutex_lock(&(spice_client->message_lock));
    if (pressed)
        spice_inputs_channel_key_press(spice_client->inputs_channel, scancode);
    else
        spice_inputs_channel_key_release(spice_client->inputs_channel, scancode);
    pthread_mutex_unlock(&(spice_client->message_lock));

}

/**
 * Immediately sends an SPICE synchonize event having the given flags. An SPICE
 * synchronize event sets the state of remote lock keys absolutely, where a
 * lock key will be active only if its corresponding flag is set in the event.
 *
 * @param spice_client
 *     The SPICE client instance associated with the SPICE session along which the
 *     synchronize event should be sent.
 *
 * @param modifiers
 *     Bitwise OR of the flags representing the lock keys which should be set,
 *     if any, as dictated by the SPICE protocol. If no flags are set, then no
 *     lock keys will be active.
 */
static void guac_spice_send_synchronize_event(guac_spice_client* spice_client,
        unsigned int modifiers) {

    /* Skip if inputs channel is not connected */
    if (spice_client->inputs_channel == NULL)
        return;

    /* Synchronize lock key states */
    pthread_mutex_lock(&(spice_client->message_lock));
    spice_inputs_channel_set_key_locks(spice_client->inputs_channel, modifiers);
    pthread_mutex_unlock(&(spice_client->message_lock));

}

/**
 * Given a keyboard instance and X11 keysym, returns a pointer to the
 * keys_by_keysym entry that represents the key having that keysym within the
 * keyboard, regardless of whether the key is currently defined. If no such key
 * can exist (the keysym cannot be mapped or is out of range), NULL is
 * returned.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param keysym
 *     The keysym of the key to lookup within the given keyboard.
 *
 * @return
 *     A pointer to the keys_by_keysym entry which represents or can represent
 *     the key having the given keysym, or NULL if no such keysym can be
 *     defined within a guac_spice_keyboard structure.
 */
static guac_spice_key** guac_spice_keyboard_map_key(guac_spice_keyboard* keyboard,
        int keysym) {

    int index;

    /* Map keysyms between 0x0000 and 0xFFFF directly */
    if (keysym >= 0x0000 && keysym <= 0xFFFF)
        index = keysym;

    /* Map all Unicode keysyms from U+0000 to U+FFFF */
    else if (keysym >= 0x1000000 && keysym <= 0x100FFFF)
        index = 0x10000 + (keysym & 0xFFFF);

    /* All other keysyms are unmapped */
    else
        return NULL;

    /* Corresponding key mapping (defined or not) has been located */
    return &(keyboard->keys_by_keysym[index]);

}

/**
 * Returns the number of bits that are set within the given integer (the number
 * of 1s in the binary expansion of the given integer).
 *
 * @param value
 *     The integer to read.
 *
 * @return
 *     The number of bits that are set within the given integer.
 */
static int guac_spice_count_bits(unsigned int value) {

    int bits = 0;

    while (value) {
        bits += value & 1;
        value >>= 1;
    }

    return bits;

}

/**
 * Returns an estimated cost for sending the necessary SPICE events to type the
 * key described by the given guac_spice_keysym_desc, given the current lock and
 * modifier state of the keyboard. A higher cost value indicates that a greater
 * number of events are expected to be required.
 *
 * Lower-cost approaches should be preferred when multiple alternatives exist
 * for typing a particular key, as the lower cost implies fewer additional key
 * events required to produce the expected behavior. For example, if Caps Lock
 * is enabled, typing an uppercase "A" by pressing the "A" key has a lower cost
 * than disabling Caps Lock and pressing Shift+A.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param def
 *     The guac_spice_keysym_desc that describes the key being pressed, as well
 *     as any requirements that must be satisfied for the key to be interpreted
 *     as expected.
 *
 * @return
 *     An arbitrary integer value which indicates the overall estimated
 *     complexity of typing the given key.
 */
static int guac_spice_keyboard_get_cost(guac_spice_keyboard* keyboard,
        const guac_spice_keysym_desc* def) {

    unsigned int modifiers = guac_spice_keyboard_get_modifier_flags(keyboard);

    /* Each change to any key requires one event, by definition */
    int cost = 1;

    /* Each change to a lock requires roughly two key events */
    unsigned int update_locks = (def->set_modifiers & ~keyboard->modifiers) | (def->clear_modifiers & keyboard->modifiers);
    cost += guac_spice_count_bits(update_locks) * 2;

    /* Each change to a modifier requires one key event */
    unsigned int update_modifiers = (def->clear_modifiers & modifiers) | (def->set_modifiers & ~modifiers);
    cost += guac_spice_count_bits(update_modifiers);

    return cost;

}

/**
 * Returns a pointer to the guac_spice_key structure representing the
 * definition(s) and state of the key having the given keysym. If no such key
 * is defined within the keyboard layout of the SPICE server, NULL is returned.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param keysym
 *     The keysym of the key to lookup within the given keyboard.
 *
 * @return
 *     A pointer to the guac_spice_key structure representing the definition(s)
 *     and state of the key having the given keysym, or NULL if no such key is
 *     defined within the keyboard layout of the SPICE server.
 */
static guac_spice_key* guac_spice_keyboard_get_key(guac_spice_keyboard* keyboard,
        int keysym) {

    /* Verify that the key is actually defined */
    guac_spice_key** key_by_keysym = guac_spice_keyboard_map_key(keyboard, keysym);
    if (key_by_keysym == NULL)
        return NULL;

    return *key_by_keysym;

}

/**
 * Given a key which may have multiple possible definitions, returns the
 * definition that currently has the lowest cost, taking into account the
 * current keyboard lock and modifier states.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param key
 *     The key whose lowest-cost possible definition should be retrieved.
 *
 * @return
 *     A pointer to the guac_spice_keysym_desc which defines the current
 *     lowest-cost method of typing the given key.
 */
static const guac_spice_keysym_desc* guac_spice_keyboard_get_definition(guac_spice_keyboard* keyboard,
        guac_spice_key* key) {

    /* Consistently map the same entry so long as the key is held */
    if (key->pressed != NULL)
        return key->pressed;

    /* Calculate cost of first definition of key (there must always be at least
     * one definition) */
    const guac_spice_keysym_desc* best_def = key->definitions[0];
    int best_cost = guac_spice_keyboard_get_cost(keyboard, best_def);

    /* If further definitions exist, choose the definition with the lowest
     * overall cost */
    for (int i = 1; i < key->num_definitions; i++) {

        const guac_spice_keysym_desc* def = key->definitions[i];
        int cost = guac_spice_keyboard_get_cost(keyboard, def);

        if (cost < best_cost) {
            best_def = def;
            best_cost = cost;
        }

    }

    return best_def;

}

/**
 * Adds the keysym/scancode mapping described by the given guac_spice_keysym_desc
 * to the internal mapping of the keyboard. If insufficient space remains for
 * additional keysyms, or the given keysym has already reached the maximum
 * number of possible definitions, the mapping is ignored and the failure is
 * logged.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param mapping
 *     The keysym/scancode mapping that should be added to the given keyboard.
 */
static void guac_spice_keyboard_add_mapping(guac_spice_keyboard* keyboard,
        const guac_spice_keysym_desc* mapping) {

    /* Locate corresponding keysym-to-key translation entry within keyboard
     * structure */
    guac_spice_key** key_by_keysym = guac_spice_keyboard_map_key(keyboard, mapping->keysym);
    if (key_by_keysym == NULL) {
        guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Ignoring unmappable keysym 0x%X", mapping->keysym);
        return;
    }

    /* If not yet pointing to a key, point keysym-to-key translation entry at
     * next available storage */
    if (*key_by_keysym == NULL) {

        if (keyboard->num_keys == GUAC_SPICE_KEYBOARD_MAX_KEYSYMS) {
            guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Key definition "
                    "for keysym 0x%X dropped: Keymap exceeds maximum "
                    "supported number of keysyms",
                    mapping->keysym);
            return;
        }

        *key_by_keysym = &keyboard->keys[keyboard->num_keys++];

    }

    guac_spice_key* key = *key_by_keysym;

    /* Add new definition only if sufficient space remains */
    if (key->num_definitions == GUAC_SPICE_KEY_MAX_DEFINITIONS) {
        guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Key definition "
                "for keysym 0x%X dropped: Maximum number of possible "
                "definitions has been reached for this keysym",
                mapping->keysym);
        return;
    }

    /* Store new possible definition of key */
    key->definitions[key->num_definitions++] = mapping;

}

/**
 * Loads all keysym/scancode mappings declared within the given keymap and its
 * parent keymap, if any. These mappings are stored within the given
 * guac_spice_keyboard structure for future use in translating keysyms to the
 * scancodes required by SPICE key events.
 *
 * @param keyboard
 *     The guac_spice_keyboard which should be initialized with the
 *     keysym/scancode mapping defined in the given keymap.
 *
 * @param keymap
 *     The keymap to use to populate the given client's keysym/scancode
 *     mapping.
 */
static void guac_spice_keyboard_load_keymap(guac_spice_keyboard* keyboard,
        const guac_spice_keymap* keymap) {

    /* If parent exists, load parent first */
    if (keymap->parent != NULL)
        guac_spice_keyboard_load_keymap(keyboard, keymap->parent);

    /* Log load */
    guac_client_log(keyboard->client, GUAC_LOG_INFO,
            "Loading keymap \"%s\"", keymap->name);

    /* Copy mapping into keymap */
    const guac_spice_keysym_desc* mapping = keymap->mapping;
    while (mapping->keysym != 0) {
        guac_spice_keyboard_add_mapping(keyboard, mapping++);
    }

}

guac_spice_keyboard* guac_spice_keyboard_alloc(guac_client* client,
        const guac_spice_keymap* keymap) {

    guac_spice_keyboard* keyboard = calloc(1, sizeof(guac_spice_keyboard));
    keyboard->client = client;

    /* Load keymap into keyboard */
    guac_spice_keyboard_load_keymap(keyboard, keymap);

    return keyboard;

}

void guac_spice_keyboard_free(guac_spice_keyboard* keyboard) {
    free(keyboard);
}

int guac_spice_keyboard_is_defined(guac_spice_keyboard* keyboard, int keysym) {

    /* Return whether the mapping actually exists */
    return guac_spice_keyboard_get_key(keyboard, keysym) != NULL;

}

int guac_spice_keyboard_is_pressed(guac_spice_keyboard* keyboard, int keysym) {

    guac_spice_key* key = guac_spice_keyboard_get_key(keyboard, keysym);
    return key != NULL && key->pressed != NULL;

}

unsigned int guac_spice_keyboard_get_modifier_flags(guac_spice_keyboard* keyboard) {

    unsigned int modifier_flags = 0;

    /* Shift */
    if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LSHIFT)
            || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RSHIFT))
        modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_SHIFT;

    /* Dedicated AltGr key */
    if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RALT)
            || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_ALTGR))
        modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_ALTGR;

    /* AltGr via Ctrl+Alt */
    if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LALT)
            && (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RCTRL)
                || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LCTRL)))
        modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_ALTGR;

    return modifier_flags;

}

/**
 * Presses/releases the requested key by sending one or more SPICE key events, as
 * defined within the keymap defining that key.
 *
 * @param keyboard
 *     The guac_spice_keyboard associated with the current SPICE session.
 *
 * @param key
 *     The guac_spice_keysym_desc of the key being pressed or released, as
 *     retrieved from the relevant keymap.
 *
 * @param pressed
 *     Zero if the key is being released, non-zero otherwise.
 *
 * @return
 *     Zero if the key was successfully pressed/released, non-zero if the key
 *     cannot be sent using SPICE key events.
 */
static const guac_spice_keysym_desc* guac_spice_keyboard_send_defined_key(guac_spice_keyboard* keyboard,
        guac_spice_key* key, int pressed) {

    guac_client* client = keyboard->client;
    guac_spice_client* spice_client = (guac_spice_client*) client->data;

    const guac_spice_keysym_desc* keysym_desc = guac_spice_keyboard_get_definition(keyboard, key);
    if (keysym_desc->scancode == 0)
        return NULL;

    /* Update state of required locks and modifiers only when key is just
     * now being pressed */
    if (pressed) {
        guac_spice_keyboard_update_locks(keyboard,
                keysym_desc->set_locks,
                keysym_desc->clear_locks);

        guac_spice_keyboard_update_modifiers(keyboard,
                keysym_desc->set_modifiers,
                keysym_desc->clear_modifiers);
    }

    /* Fire actual key event for target key */
    guac_client_log(client, GUAC_LOG_TRACE, "Firing scancode event: %08x", keysym_desc->scancode);
    guac_spice_send_key_event(spice_client, keysym_desc->scancode,
            keysym_desc->flags, pressed);

    return keysym_desc;

}

void guac_spice_keyboard_update_locks(guac_spice_keyboard* keyboard,
        unsigned int set_modifiers, unsigned int clear_modifiers) {

    guac_client* client = keyboard->client;
    guac_spice_client* spice_client = (guac_spice_client*) client->data;

    /* Calculate updated lock flags */
    unsigned int modifiers = (keyboard->modifiers | set_modifiers) & ~clear_modifiers;

    /* Synchronize remote side only if lock flags have changed */
    if (modifiers != keyboard->modifiers) {
        guac_spice_send_synchronize_event(spice_client, modifiers);
        keyboard->modifiers = modifiers;
    }

}

void guac_spice_keyboard_update_modifiers(guac_spice_keyboard* keyboard,
        unsigned int set_flags, unsigned int clear_flags) {

    unsigned int modifier_flags = guac_spice_keyboard_get_modifier_flags(keyboard);

    /* Only clear modifiers that are set */
    clear_flags &= modifier_flags;

    /* Only set modifiers that are currently cleared */
    set_flags &= ~modifier_flags;

    /* Press/release Shift as needed */
    if (set_flags & GUAC_SPICE_KEYMAP_MODIFIER_SHIFT) {
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LSHIFT, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
    }
    else if (clear_flags & GUAC_SPICE_KEYMAP_MODIFIER_SHIFT) {
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LSHIFT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RSHIFT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
    }

    /* Press/release AltGr as needed */
    if (set_flags & GUAC_SPICE_KEYMAP_MODIFIER_ALTGR) {
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_ALTGR, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
    }
    else if (clear_flags & GUAC_SPICE_KEYMAP_MODIFIER_ALTGR) {
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_ALTGR, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LALT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RALT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LCTRL, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
        guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RCTRL, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
    }

}

int guac_spice_keyboard_update_keysym(guac_spice_keyboard* keyboard,
        int keysym, int pressed, guac_spice_key_source source) {

    /* Synchronize lock keys states, if this has not yet been done */
    if (!keyboard->synchronized) {

        guac_client* client = keyboard->client;
        guac_spice_client* spice_client = (guac_spice_client*) client->data;

        /* Synchronize remote lock key states with local state */
        guac_spice_send_synchronize_event(spice_client, keyboard->modifiers);
        keyboard->synchronized = 1;

    }

    guac_spice_key* key = guac_spice_keyboard_get_key(keyboard, keysym);

    /* Update tracking of client-side keyboard state but only for keys which
     * are tracked server-side, as well (to ensure that the key count remains
     * correct, even if a user sends extra unbalanced or excessive press and
     * release events) */
    if (source == GUAC_SPICE_KEY_SOURCE_CLIENT && key != NULL) {
        if (pressed && !key->user_pressed) {
            keyboard->user_pressed_keys++;
            key->user_pressed = 1;
        }
        else if (!pressed && key->user_pressed) {
            keyboard->user_pressed_keys--;
            key->user_pressed = 0;
        }
    }

    /* Send events and update server-side lock state only if server-side key
     * state is changing (or if server-side state of this key is untracked) */
    if (key == NULL || (pressed && key->pressed == NULL) || (!pressed && key->pressed != NULL)) {

        /* Toggle locks on keydown */
        if (pressed)
            keyboard->modifiers ^= guac_spice_keyboard_lock_flag(keysym);

        /* If key is known, update state and attempt to send using normal SPICE key
         * events */
        const guac_spice_keysym_desc* definition = NULL;
        if (key != NULL) {
            definition = guac_spice_keyboard_send_defined_key(keyboard, key, pressed);
            key->pressed = pressed ? definition : NULL;
        }

        /* Fall back to dead keys or Unicode events if otherwise undefined inside
         * current keymap (note that we only handle "pressed" here, as neither
         * Unicode events nor dead keys can have a pressed/released state) */
        if (definition == NULL && pressed) {
            guac_client_log(keyboard->client, GUAC_LOG_WARNING, 
                "Undefined key will not be sent: %d", keysym);
        }

    }

    /* Reset SPICE server keyboard state (releasing any automatically
     * pressed keys) once all keys have been released on the client
     * side */
    if (source == GUAC_SPICE_KEY_SOURCE_CLIENT && keyboard->user_pressed_keys == 0)
        guac_spice_keyboard_reset(keyboard);

    return 0;

}

void guac_spice_keyboard_reset(guac_spice_keyboard* keyboard) {

    /* Release all pressed keys */
    for (int i = 0; i < keyboard->num_keys; i++) {
        guac_spice_key* key = &keyboard->keys[i];
        if (key->pressed != NULL)
            guac_spice_keyboard_update_keysym(keyboard, key->pressed->keysym, 0,
                    GUAC_SPICE_KEY_SOURCE_SYNTHETIC);
    }

}

void guac_spice_keyboard_set_indicators(SpiceChannel* channel, guac_client* client) {

    guac_spice_client* spice_client = (guac_spice_client*) client->data;

    pthread_rwlock_rdlock(&(spice_client->lock));

    /* Skip if keyboard not yet ready */
    guac_spice_keyboard* keyboard = spice_client->keyboard;
    if (keyboard == NULL)
        goto complete;
    
    unsigned int modifiers;
    g_object_get(channel, SPICE_PROPERTY_KEY_MODIFIERS, &modifiers, NULL);

    /* Update with received locks */
    guac_client_log(client, GUAC_LOG_DEBUG, "Received updated keyboard lock flags from SPICE server: 0x%X", modifiers);
    keyboard->modifiers = modifiers;

complete:
    pthread_rwlock_unlock(&(spice_client->lock));

}
