/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2016 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/

#include "../../SDL_internal.h"

#if SDL_VIDEO_DRIVER_WAYLAND

#include "SDL_stdinc.h"
#include "SDL_assert.h"

#include "../../events/SDL_sysevents.h"
#include "../../events/SDL_events_c.h"
#include "../../events/scancodes_xfree86.h"

#include "SDL_waylandvideo.h"
#include "SDL_waylandevents_c.h"
#include "SDL_waylandwindow.h"

#include "SDL_waylanddyn.h"

#include <linux/input.h>
#include <sys/select.h>
#include <sys/mman.h>
#include <poll.h>
#include <errno.h>
#include <unistd.h>
#include <xkbcommon/xkbcommon.h>

struct SDL_WaylandInput {
    SDL_VideoData *display;
    struct wl_seat *seat;
    struct wl_pointer *pointer;
    struct wl_keyboard *keyboard;
    SDL_WindowData *pointer_focus;
    SDL_WindowData *keyboard_focus;

    /* Last motion location */
    wl_fixed_t sx_w;
    wl_fixed_t sy_w;
    
    struct {
        struct xkb_keymap *keymap;
        struct xkb_state *state;
    } xkb;
};

void
Wayland_PumpEvents(_THIS)
{
    SDL_VideoData *d = _this->driverdata;
    struct pollfd pfd[1];

    pfd[0].fd = WAYLAND_wl_display_get_fd(d->display);
    pfd[0].events = POLLIN;
    poll(pfd, 1, 0);

    if (pfd[0].revents & POLLIN)
        WAYLAND_wl_display_dispatch(d->display);
    else
        WAYLAND_wl_display_dispatch_pending(d->display);
}

static void
pointer_handle_enter(void *data, struct wl_pointer *pointer,
                     uint32_t serial, struct wl_surface *surface,
                     wl_fixed_t sx_w, wl_fixed_t sy_w)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window;

    if (!surface) {
        /* enter event for a window we've just destroyed */
        return;
    }
    
    /* This handler will be called twice in Wayland 1.4
     * Once for the window surface which has valid user data
     * and again for the mouse cursor surface which does not have valid user data
     * We ignore the later
     */

    window = (SDL_WindowData *)wl_surface_get_user_data(surface);
    
    if (window) {
        input->pointer_focus = window;
        SDL_SetMouseFocus(window->sdlwindow);
    }
}

static void
pointer_handle_leave(void *data, struct wl_pointer *pointer,
                     uint32_t serial, struct wl_surface *surface)
{
    struct SDL_WaylandInput *input = data;

    if (input->pointer_focus) {
        SDL_SetMouseFocus(NULL);
        input->pointer_focus = NULL;
    }
}

static void
pointer_handle_motion(void *data, struct wl_pointer *pointer,
                      uint32_t time, wl_fixed_t sx_w, wl_fixed_t sy_w)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window = input->pointer_focus;
    input->sx_w = sx_w;
    input->sy_w = sy_w;
    if (input->pointer_focus) {
        const int sx = wl_fixed_to_int(sx_w);
        const int sy = wl_fixed_to_int(sy_w);
        SDL_SendMouseMotion(window->sdlwindow, 0, 0, sx, sy);
    }
}

static SDL_bool
ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial)
{
    SDL_WindowData *window_data = input->pointer_focus;
    SDL_Window *window = window_data->sdlwindow;

    if (window->hit_test) {
        const SDL_Point point = { wl_fixed_to_int(input->sx_w), wl_fixed_to_int(input->sy_w) };
        const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data);
        static const uint32_t directions[] = {
            WL_SHELL_SURFACE_RESIZE_TOP_LEFT, WL_SHELL_SURFACE_RESIZE_TOP,
            WL_SHELL_SURFACE_RESIZE_TOP_RIGHT, WL_SHELL_SURFACE_RESIZE_RIGHT,
            WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT, WL_SHELL_SURFACE_RESIZE_BOTTOM,
            WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT, WL_SHELL_SURFACE_RESIZE_LEFT
        };
        switch (rc) {
            case SDL_HITTEST_DRAGGABLE:
                wl_shell_surface_move(window_data->shell_surface, input->seat, serial);
                return SDL_TRUE;

            case SDL_HITTEST_RESIZE_TOPLEFT:
            case SDL_HITTEST_RESIZE_TOP:
            case SDL_HITTEST_RESIZE_TOPRIGHT:
            case SDL_HITTEST_RESIZE_RIGHT:
            case SDL_HITTEST_RESIZE_BOTTOMRIGHT:
            case SDL_HITTEST_RESIZE_BOTTOM:
            case SDL_HITTEST_RESIZE_BOTTOMLEFT:
            case SDL_HITTEST_RESIZE_LEFT:
                wl_shell_surface_resize(window_data->shell_surface, input->seat, serial, directions[rc - SDL_HITTEST_RESIZE_TOPLEFT]);
                return SDL_TRUE;

            default: return SDL_FALSE;
        }
    }

    return SDL_FALSE;
}

static void
pointer_handle_button(void *data, struct wl_pointer *pointer, uint32_t serial,
                      uint32_t time, uint32_t button, uint32_t state_w)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window = input->pointer_focus;
    enum wl_pointer_button_state state = state_w;
    uint32_t sdl_button;
    
    if  (input->pointer_focus) {
        switch (button) {
            case BTN_LEFT:
                sdl_button = SDL_BUTTON_LEFT;
                if (ProcessHitTest(data, serial)) {
                    return;  /* don't pass this event on to app. */
                }
                break;
            case BTN_MIDDLE:
                sdl_button = SDL_BUTTON_MIDDLE;
                break;
            case BTN_RIGHT:
                sdl_button = SDL_BUTTON_RIGHT;
                break;
            case BTN_SIDE:
                sdl_button = SDL_BUTTON_X1;
                break;
            case BTN_EXTRA:
                sdl_button = SDL_BUTTON_X2;
                break;
            default:
                return;
        }

        SDL_SendMouseButton(window->sdlwindow, 0,
                            state ? SDL_PRESSED : SDL_RELEASED, sdl_button);
    }
}

static void
pointer_handle_axis(void *data, struct wl_pointer *pointer,
                    uint32_t time, uint32_t axis, wl_fixed_t value)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window = input->pointer_focus;
    enum wl_pointer_axis a = axis;
    int x, y;

    if (input->pointer_focus) {
        switch (a) {
            case WL_POINTER_AXIS_VERTICAL_SCROLL:
                x = 0;
                y = wl_fixed_to_int(value);
                break;
            case WL_POINTER_AXIS_HORIZONTAL_SCROLL:
                x = wl_fixed_to_int(value);
                y = 0;
                break;
            default:
                return;
        }

        SDL_SendMouseWheel(window->sdlwindow, 0, x, y, SDL_MOUSEWHEEL_NORMAL);
    }
}

static const struct wl_pointer_listener pointer_listener = {
    pointer_handle_enter,
    pointer_handle_leave,
    pointer_handle_motion,
    pointer_handle_button,
    pointer_handle_axis,
};

static void
keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
                       uint32_t format, int fd, uint32_t size)
{
    struct SDL_WaylandInput *input = data;
    char *map_str;

    if (!data) {
        close(fd);
        return;
    }

    if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
        close(fd);
        return;
    }

    map_str = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    if (map_str == MAP_FAILED) {
        close(fd);
        return;
    }

    input->xkb.keymap = WAYLAND_xkb_keymap_new_from_string(input->display->xkb_context,
                                                map_str,
                                                XKB_KEYMAP_FORMAT_TEXT_V1,
                                                0);
    munmap(map_str, size);
    close(fd);

    if (!input->xkb.keymap) {
        fprintf(stderr, "failed to compile keymap\n");
        return;
    }

    input->xkb.state = WAYLAND_xkb_state_new(input->xkb.keymap);
    if (!input->xkb.state) {
        fprintf(stderr, "failed to create XKB state\n");
        WAYLAND_xkb_keymap_unref(input->xkb.keymap);
        input->xkb.keymap = NULL;
        return;
    }
}

static void
keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
                      uint32_t serial, struct wl_surface *surface,
                      struct wl_array *keys)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window;

    if (!surface) {
        /* enter event for a window we've just destroyed */
        return;
    }
 
    window = wl_surface_get_user_data(surface);

    input->keyboard_focus = window;
    window->keyboard_device = input;
    if (window) {
        SDL_SetKeyboardFocus(window->sdlwindow);
    }
}

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

static void
keyboard_handle_key(void *data, struct wl_keyboard *keyboard,
                    uint32_t serial, uint32_t time, uint32_t key,
                    uint32_t state_w)
{
    struct SDL_WaylandInput *input = data;
    SDL_WindowData *window = input->keyboard_focus;
    enum wl_keyboard_key_state state = state_w;
    const xkb_keysym_t *syms;
    uint32_t scancode;
    char text[8];
    int size;

    if (key < SDL_arraysize(xfree86_scancode_table2)) {
        scancode = xfree86_scancode_table2[key];

        // TODO when do we get WL_KEYBOARD_KEY_STATE_REPEAT?
        if (scancode != SDL_SCANCODE_UNKNOWN)
            SDL_SendKeyboardKey(state == WL_KEYBOARD_KEY_STATE_PRESSED ?
                                SDL_PRESSED : SDL_RELEASED, scancode);
    }

    if (!window || window->keyboard_device != input || !input->xkb.state)
        return;

    // TODO can this happen?
    if (WAYLAND_xkb_state_key_get_syms(input->xkb.state, key + 8, &syms) != 1)
        return;

    if (state) {
        size = WAYLAND_xkb_keysym_to_utf8(syms[0], text, sizeof text);

        if (size > 0) {
            text[size] = 0;
            SDL_SendKeyboardText(text);
        }
    }
}

static void
keyboard_handle_modifiers(void *data, struct wl_keyboard *keyboard,
                          uint32_t serial, uint32_t mods_depressed,
                          uint32_t mods_latched, uint32_t mods_locked,
                          uint32_t group)
{
    struct SDL_WaylandInput *input = data;

    WAYLAND_xkb_state_update_mask(input->xkb.state, mods_depressed, mods_latched,
                          mods_locked, 0, 0, group);
}

static const struct wl_keyboard_listener keyboard_listener = {
    keyboard_handle_keymap,
    keyboard_handle_enter,
    keyboard_handle_leave,
    keyboard_handle_key,
    keyboard_handle_modifiers,
};

static void
seat_handle_capabilities(void *data, struct wl_seat *seat,
                         enum wl_seat_capability caps)
{
    struct SDL_WaylandInput *input = data;

    if ((caps & WL_SEAT_CAPABILITY_POINTER) && !input->pointer) {
        input->pointer = wl_seat_get_pointer(seat);
        input->display->pointer = input->pointer;
        wl_pointer_set_user_data(input->pointer, input);
        wl_pointer_add_listener(input->pointer, &pointer_listener,
                                input);
    } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && input->pointer) {
        wl_pointer_destroy(input->pointer);
        input->pointer = NULL;
    }

    if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !input->keyboard) {
        input->keyboard = wl_seat_get_keyboard(seat);
        wl_keyboard_set_user_data(input->keyboard, input);
        wl_keyboard_add_listener(input->keyboard, &keyboard_listener,
                                 input);
    } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && input->keyboard) {
        wl_keyboard_destroy(input->keyboard);
        input->keyboard = NULL;
    }
}

static const struct wl_seat_listener seat_listener = {
    seat_handle_capabilities,
};

void
Wayland_display_add_input(SDL_VideoData *d, uint32_t id)
{
    struct SDL_WaylandInput *input;

    input = SDL_calloc(1, sizeof *input);
    if (input == NULL)
        return;

    input->display = d;
    input->seat = wl_registry_bind(d->registry, id, &wl_seat_interface, 1);
    input->sx_w = wl_fixed_from_int(0);
    input->sy_w = wl_fixed_from_int(0);
    d->input = input;

    wl_seat_add_listener(input->seat, &seat_listener, input);
    wl_seat_set_user_data(input->seat, input);

    WAYLAND_wl_display_flush(d->display);
}

void Wayland_display_destroy_input(SDL_VideoData *d)
{
    struct SDL_WaylandInput *input = d->input;

    if (!input)
        return;

    if (input->keyboard)
        wl_keyboard_destroy(input->keyboard);

    if (input->pointer)
        wl_pointer_destroy(input->pointer);

    if (input->seat)
        wl_seat_destroy(input->seat);

    if (input->xkb.state)
        WAYLAND_xkb_state_unref(input->xkb.state);

    if (input->xkb.keymap)
        WAYLAND_xkb_keymap_unref(input->xkb.keymap);

    SDL_free(input);
    d->input = NULL;
}

#endif /* SDL_VIDEO_DRIVER_WAYLAND */

/* vi: set ts=4 sw=4 expandtab: */
