/**
 * @file input.c
 * @brief Wayland input events for VLC media player
 */
/*****************************************************************************
 * Copyright © 2018 Rémi Denis-Courmont
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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.
 *****************************************************************************/

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

#include <assert.h>
#include <inttypes.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <linux/input-event-codes.h>
#include <wayland-client.h>
#ifdef HAVE_XKBCOMMON
# include <xkbcommon/xkbcommon.h>
# include "../xcb/vlc_xkb.h"
#endif
#include <vlc_common.h>
#include <vlc_threads.h>
#include <vlc_window.h>
#include <vlc_mouse.h>
#include <vlc_fs.h>

#include "input.h"

struct seat_data
{
    vlc_window_t *owner;
    struct wl_seat *seat;

    struct wl_pointer *pointer;
    vlc_tick_t cursor_timeout;
    vlc_tick_t cursor_deadline;
    uint32_t cursor_serial;

#ifdef HAVE_XKBCOMMON
    struct xkb_context *xkb;
    struct wl_keyboard *keyboard;
    struct xkb_keymap *keymap;
    struct xkb_state *keystate;
#endif

    uint32_t name;
    uint32_t version;
    struct wl_list node;
};

static void pointer_show(struct seat_data *sd, struct wl_pointer *pointer)
{
    int hsx, hsy;
    struct wl_surface *surface = window_get_cursor(sd->owner, &hsx, &hsy);

    if (surface != NULL)
    {
        wl_pointer_set_cursor(pointer, sd->cursor_serial, surface, hsx, hsy);
        sd->cursor_deadline = vlc_tick_now() + sd->cursor_timeout;
    }
}

static void pointer_enter_cb(void *data, struct wl_pointer *pointer,
                             uint32_t serial, struct wl_surface *surface,
                             wl_fixed_t sx, wl_fixed_t sy)
{
    struct seat_data *sd = data;

    sd->cursor_serial = serial;
    pointer_show(sd, pointer);
    (void) surface; (void) sx; (void) sy;
}

static void pointer_leave_cb(void *data, struct wl_pointer *pointer,
                             uint32_t serial, struct wl_surface *surface)
{
    (void) data; (void) pointer; (void) serial; (void) surface;
}

static void pointer_motion_cb(void *data, struct wl_pointer *pointer,
                              uint32_t time, wl_fixed_t sx, wl_fixed_t sy)
{
    struct seat_data *sd = data;

    pointer_show(sd, pointer);
    vlc_window_ReportMouseMoved(sd->owner,
                                 wl_fixed_to_int(sx), wl_fixed_to_int(sy));
    (void) time;
}

static void pointer_button_cb(void *data, struct wl_pointer *pointer,
                              uint32_t serial, uint32_t time,
                              uint32_t keycode, uint32_t state)
{
    struct seat_data *sd = data;
    int button;

    pointer_show(sd, pointer);

    switch (keycode)
    {
        case BTN_LEFT:
            button = MOUSE_BUTTON_LEFT;
            break;
        case BTN_RIGHT:
            button = MOUSE_BUTTON_RIGHT;
            break;
        case BTN_MIDDLE:
            button = MOUSE_BUTTON_CENTER;
            break;
        default:
            return;
    }

    switch (state)
    {
        case WL_POINTER_BUTTON_STATE_RELEASED:
            vlc_window_ReportMouseReleased(sd->owner, button);
            break;
        case WL_POINTER_BUTTON_STATE_PRESSED:
            vlc_window_ReportMousePressed(sd->owner, button);
            break;
    }

    (void) serial; (void) time;
}

static void pointer_axis_cb(void *data, struct wl_pointer *pointer,
                            uint32_t serial, uint32_t type, wl_fixed_t value)
{
    struct seat_data *sd = data;
    vlc_window_t *wnd = sd->owner;
    int button;
    bool plus = value > 0;

    pointer_show(sd, pointer);
    value = abs(value);

    switch (type)
    {
        case WL_POINTER_AXIS_VERTICAL_SCROLL:
            button = plus ? MOUSE_BUTTON_WHEEL_DOWN : MOUSE_BUTTON_WHEEL_UP;
            break;
        case WL_POINTER_AXIS_HORIZONTAL_SCROLL:
            button = plus ? MOUSE_BUTTON_WHEEL_RIGHT : MOUSE_BUTTON_WHEEL_LEFT;
            break;
        default:
            return;
    }

    while (value > 0)
    {
        vlc_window_ReportMousePressed(wnd, button);
        vlc_window_ReportMouseReleased(wnd, button);
        value -= wl_fixed_from_int(10);
    }
    (void) serial;
}

static void pointer_frame_cb(void *data, struct wl_pointer *pointer)
{
    (void) data; (void) pointer;
}

static void pointer_axis_source(void *data, struct wl_pointer *pointer,
                                uint32_t source)
{
    (void) data; (void) pointer; (void) source;
}

static void pointer_axis_stop(void *data, struct wl_pointer *pointer,
                              uint32_t time, uint32_t axis)
{
    (void) data; (void) pointer; (void) time; (void) axis;
}

static void pointer_axis_discrete(void *data, struct wl_pointer *pointer,
                                  uint32_t type, int steps)
{
    (void) data; (void) pointer; (void) type; (void) steps;
}

static const struct wl_pointer_listener pointer_cbs =
{
    pointer_enter_cb,
    pointer_leave_cb,
    pointer_motion_cb,
    pointer_button_cb,
    pointer_axis_cb,
    pointer_frame_cb,
    pointer_axis_source,
    pointer_axis_stop,
    pointer_axis_discrete,
#ifdef WL_POINTER_AXIS_VALUE120_SINCE_VERSION
    NULL, /* We don't support axis_value120 for now */
#endif
#ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_SINCE_VERSION
    NULL, /* We don't support axis_relative_direction for now */
#endif
};

static void pointer_create(struct seat_data *sd)
{
    if (sd->pointer != NULL)
        return;

    sd->pointer = wl_seat_get_pointer(sd->seat);
    if (likely(sd->pointer != NULL))
        wl_pointer_add_listener(sd->pointer, &pointer_cbs, sd);

    sd->cursor_timeout = VLC_TICK_FROM_MS( var_InheritInteger(sd->owner, "mouse-hide-timeout") );
    sd->cursor_deadline = VLC_TICK_MAX;
}

static void pointer_destroy(struct seat_data *sd)
{
    if (sd->pointer == NULL)
        return;

    if (sd->version >= WL_POINTER_RELEASE_SINCE_VERSION)
        wl_pointer_release(sd->pointer);
    else
        wl_pointer_destroy(sd->pointer);

    sd->pointer = NULL;
}

#ifdef HAVE_XKBCOMMON
static void keyboard_keymap_cb(void *data, struct wl_keyboard *keyboard,
                               uint32_t format, int fd, uint32_t size)
{
    struct seat_data *sd = data;
    vlc_window_t *wnd = sd->owner;
    void *map;

    msg_Dbg(wnd, "format %"PRIu32" keymap of %"PRIu32" bytes", format, size);

    if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
    {
        msg_Err(wnd, "unsupported keymap format %"PRIu32, format);
        goto out;
    }

    size++; /* trailing nul */

    map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED)
        goto out;

    assert(((char *)map)[size - 1] == '\0');
    sd->keymap = xkb_keymap_new_from_string(sd->xkb, map,
                                            XKB_KEYMAP_FORMAT_TEXT_V1,
                                            XKB_KEYMAP_COMPILE_NO_FLAGS);
    munmap(map, size);

    if (sd->keymap != NULL)
        sd->keystate = xkb_state_new(sd->keymap);
    else
        msg_Err(wnd, "keymap parse error");

out:
    vlc_close(fd);
    (void) keyboard;
}

static void keyboard_enter_cb(void *data, struct wl_keyboard *keyboard,
                              uint32_t serial, struct wl_surface *surface,
                              struct wl_array *keys)
{
    (void) data; (void) keyboard; (void) serial; (void) surface; (void) keys;
}

static void keyboard_leave_cb(void *data, struct wl_keyboard *keyboard,
                              uint32_t serial, struct wl_surface *surface)
{
    (void) data; (void) keyboard; (void) serial; (void) surface;}

static void keyboard_key_cb(void *data, struct wl_keyboard *keyboard,
                            uint32_t serial, uint32_t time, uint32_t keycode,
                            uint32_t state)
{
    struct seat_data *sd = data;
    vlc_window_t *wnd = sd->owner;
    uint_fast32_t vk;

    if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
        return;
    if (unlikely(sd->keystate == NULL))
        return;

    vk = vlc_xkb_get_one(sd->keystate, keycode + 8);
    if (vk)
    {
        msg_Dbg(wnd, "key: 0x%08"PRIxFAST32" (XKB: 0x%04"PRIx32")",
                vk, keycode);
        vlc_window_ReportKeyPress(wnd, vk);
    }

    (void) keyboard; (void) serial; (void) time;
}

static void keyboard_modifiers_cb(void *data, struct wl_keyboard *keyboard,
                                  uint32_t serial, uint32_t depressed,
                                  uint32_t latched, uint32_t locked,
                                  uint32_t group)
{
    struct seat_data *sd = data;

    if (unlikely(sd->keystate == NULL))
        return;

    xkb_state_update_mask(sd->keystate, depressed, latched, locked,
                          0, 0, group);

    (void) keyboard; (void) serial;
}

static void keyboard_repeat_info_cb(void *data, struct wl_keyboard *keyboard,
                                    int32_t rate, int32_t delay)
{
    struct seat_data *sd = data;
    vlc_window_t *wnd = sd->owner;

    msg_Dbg(wnd, "keyboard repeat info: %d Hz after %d ms", rate, delay);
    (void) keyboard;
}

static const struct wl_keyboard_listener keyboard_cbs =
{
    keyboard_keymap_cb,
    keyboard_enter_cb,
    keyboard_leave_cb,
    keyboard_key_cb,
    keyboard_modifiers_cb,
    keyboard_repeat_info_cb,
};

static void keyboard_create(struct seat_data *sd)
{
    if (sd->keyboard != NULL)
        return;

    sd->keyboard = wl_seat_get_keyboard(sd->seat);
    if (likely(sd->keyboard != NULL))
    {
        sd->keymap = NULL;
        wl_keyboard_add_listener(sd->keyboard, &keyboard_cbs, sd);
    }
}

static void keyboard_destroy(struct seat_data *sd)
{
    if (sd->keyboard == NULL)
        return;

    if (sd->version >= WL_KEYBOARD_RELEASE_SINCE_VERSION)
        wl_keyboard_release(sd->keyboard);
    else
        wl_keyboard_destroy(sd->keyboard);

    if (sd->keymap != NULL)
    {
        if (sd->keystate != NULL)
            xkb_state_unref(sd->keystate);
        xkb_keymap_unref(sd->keymap);
    }

    sd->keyboard = NULL;
}
#endif /* HAVE_XKBCOMMON */

static void seat_capabilities_cb(void *data, struct wl_seat *seat,
                                 uint32_t capabilities)
{
    struct seat_data *sd = data;
    struct vlc_window *wnd = sd->owner;

    msg_Dbg(wnd, "seat capabilities: 0x%"PRIx32, capabilities);
    (void) seat;

    if (capabilities & WL_SEAT_CAPABILITY_POINTER)
    {
        if (var_InheritBool(wnd, "mouse-events"))
            pointer_create(sd);
    }
    else
        pointer_destroy(sd);

#ifdef HAVE_XKBCOMMON
    if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)
    {
        if (sd->xkb != NULL)
            keyboard_create(sd);
    }
    else
        keyboard_destroy(sd);
#endif
}

static void seat_name_cb(void *data, struct wl_seat *seat, const char *name)
{
    struct seat_data *sd = data;

    msg_Dbg(sd->owner, "seat name: %s", name);
    (void) seat;
}

static const struct wl_seat_listener seat_cbs =
{
    seat_capabilities_cb,
    seat_name_cb,
};

int seat_create(vlc_window_t *wnd, struct wl_registry *registry,
                uint32_t name, uint32_t version, struct wl_list *list)
{
    struct seat_data *sd = malloc(sizeof (*sd));
    if (unlikely(sd == NULL))
        return -1;

    if (version > 5)
        version = 5;

    sd->seat = wl_registry_bind(registry, name, &wl_seat_interface, version);
    if (unlikely(sd->seat == NULL))
    {
        free(sd);
        return -1;
    }

    sd->owner = wnd;
    sd->pointer = NULL;
#ifdef HAVE_XKBCOMMON
    if (var_InheritBool(wnd, "keyboard-events"))
        sd->xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);

    sd->keyboard = NULL;
#endif
    sd->version = version;

    wl_seat_add_listener(sd->seat, &seat_cbs, sd);
    wl_list_insert(list, &sd->node);
    return 0;
}

static vlc_tick_t seat_next_deadline(const struct seat_data *sd)
{
    return (sd->pointer != NULL) ? sd->cursor_deadline : VLC_TICK_MAX;
}

static void seat_refresh(struct seat_data *sd, vlc_tick_t now)
{
    if (sd->pointer != NULL && sd->cursor_deadline <= now)
    {   /* Hide cursor */
        wl_pointer_set_cursor(sd->pointer, sd->cursor_serial, NULL, 0, 0);
        sd->cursor_deadline = VLC_TICK_MAX;
    }
}

static void seat_destroy(struct seat_data *sd)
{
    wl_list_remove(&sd->node);

#ifdef HAVE_XKBCOMMON
    keyboard_destroy(sd);

    if (sd->xkb != NULL)
        xkb_context_unref(sd->xkb);
#endif
    pointer_destroy(sd);

    if (sd->version >= WL_SEAT_RELEASE_SINCE_VERSION)
        wl_seat_release(sd->seat);
    else
        wl_seat_destroy(sd->seat);
    free(sd);
}

int seat_destroy_one(struct wl_list *list, uint32_t name)
{
    struct seat_data *sd;

    wl_list_for_each(sd, list, node)
    {
        if (sd->name == name)
        {
            seat_destroy(sd);
            /* Note: return here so no needs for safe walk variant */
            return 0;
        }
    }

    return -1;
}

void seat_destroy_all(struct wl_list *list)
{
    while (!wl_list_empty(list))
        seat_destroy(container_of(list->next, struct seat_data, node));
}

int seat_next_timeout(const struct wl_list *list)
{
    struct seat_data *sd;
    vlc_tick_t deadline = VLC_TICK_MAX;

    wl_list_for_each(sd, list, node)
    {
        vlc_tick_t d = seat_next_deadline(sd);
        if (deadline > d)
            deadline = d;
    }

    if (deadline == VLC_TICK_MAX)
        return -1;

    vlc_tick_t now = vlc_tick_now();
    if (now >= deadline)
        return 0;

    return (deadline - now) / 1000 + 1;
}

void seat_timeout(struct wl_list *list)
{
    struct seat_data *sd;
    vlc_tick_t now = vlc_tick_now();

    wl_list_for_each(sd, list, node)
        seat_refresh(sd, now);
}
