#include <keyboard.h>
#include <keyboard/port.h>
#include <keyboard/key.h>
#include <SDL.h>
#include <ctype.h>
#include <stddef.h>
#include <stdio.h>

static void append_key(const enum keyboard_key key, const char ch,
    const Uint8 scancode, struct keyboard *const k)
{
    struct keyboard_combo *const c = &k->input.combo;

    for (size_t i = 0; i < sizeof c->keys / sizeof *c->keys; i++)
    {
        enum keyboard_key *const sel = &c->keys[i];
        char *const pch = &k->input.chars.c[i];
        Uint8 *const sc = &k->port.scancodes[i];

        if (*sel == KEYBOARD_KEY_NONE && !*pch)
        {
            *sel = key;
            *pch = ch;
            *sc = scancode;
            break;
        }
    }
}

static enum keyboard_key char_to_key(const char ch)
{
    if (ch >= 'a' && ch <= 'b')
        return KEYBOARD_KEY_A + (ch - 'a');
    else if (ch >= 'A' && ch <= 'Z')
        return KEYBOARD_KEY_A + (ch - 'A');
    else if (ch >= '0' && ch <= '9')
        return KEYBOARD_KEY_0 + (ch - '0');
    else
    {
        static const struct map
        {
            enum keyboard_key key;
            char ch;
        } map[] =
        {
            {.key = KEYBOARD_KEY_DOT, .ch = '.'},
            {.key = KEYBOARD_KEY_SPACE, .ch = ' '},
            {.key = KEYBOARD_KEY_MINUS, .ch = '-'},
            {.key = KEYBOARD_KEY_SLASH, .ch = '/'}
        };

        for (size_t i = 0; i < sizeof map / sizeof *map; i++)
        {
            const struct map *const m = &map[i];

            if (ch == m->ch)
                return m->key;
        }
    }

    return KEYBOARD_KEY_NONE;
}

static void on_pressed(const SDL_KeyboardEvent *const ev,
    struct keyboard *const k)
{
    static const struct keymap
    {
        enum keyboard_key key;
        SDLKey sdl_key;
    } keymap[] =
    {
        {.key = KEYBOARD_KEY_BACKSPACE, .sdl_key = SDLK_BACKSPACE},
        {.key = KEYBOARD_KEY_LEFT, .sdl_key = SDLK_LEFT},
        {.key = KEYBOARD_KEY_RIGHT, .sdl_key = SDLK_RIGHT},
        {.key = KEYBOARD_KEY_UP, .sdl_key = SDLK_UP},
        {.key = KEYBOARD_KEY_DOWN, .sdl_key = SDLK_DOWN},
        {.key = KEYBOARD_KEY_ESC, .sdl_key = SDLK_ESCAPE},
        {.key = KEYBOARD_KEY_F11, .sdl_key = SDLK_F11},
        {.key = KEYBOARD_KEY_LCTRL, .sdl_key = SDLK_LCTRL},
        {.key = KEYBOARD_KEY_RCTRL, .sdl_key = SDLK_RCTRL},
        {.key = KEYBOARD_KEY_LSHIFT, .sdl_key = SDLK_LSHIFT},
        {.key = KEYBOARD_KEY_RSHIFT, .sdl_key = SDLK_RSHIFT},
        {.key = KEYBOARD_KEY_RETURN, .sdl_key = SDLK_KP_ENTER},
        {.key = KEYBOARD_KEY_RETURN, .sdl_key = SDLK_RETURN}
    };

    const char ch = ev->keysym.unicode;

    /* According to the man page for isprint(3):
     * The standards require that the argument c for these functions is
     * either EOF or a value that is representable in the type unsigned
     * char. If the argument is of type char, it must be cast to
     * unsigned char. */
    if (isprint((unsigned char)ch))
        append_key(char_to_key(ch), ch, ev->keysym.scancode, k);
    else
    {
        for (size_t i = 0; i < sizeof keymap / sizeof *keymap; i++)
        {
            const struct keymap *const km = &keymap[i];

            if (ev->keysym.sym == km->sdl_key)
            {
                append_key(km->key, '\0', ev->keysym.scancode, k);
                break;
            }
        }
    }
}

static void remove_key(const Uint8 scancode, struct keyboard *const k)
{
    struct keyboard_port *const p = &k->port;

    for (size_t i = 0; i < sizeof p->scancodes / sizeof *p->scancodes; i++)
    {
        Uint8 *const sc = &p->scancodes[i];

        if (*sc == scancode)
        {
            struct keyboard_combo *const c = &k->input.combo;
            struct keyboard_chars *const ch = &k->input.chars;

            c->keys[i] = KEYBOARD_KEY_NONE;
            ch->c[i] = '\0';
            *sc = 0;
            break;
        }
    }
}

static void key_event(const SDL_KeyboardEvent *const ev,
    struct keyboard *const k)
{
    if (ev->state == SDL_PRESSED)
        on_pressed(ev, k);
    else
        /* SDL allows reading Unicode only for pressed events.
         * In order to know whether a Unicode was released, its
         * corresponding scancode must be inspected. */
        remove_key(ev->keysym.scancode, k);
}

void keyboard_update(struct keyboard *const k)
{
    SDL_Event ev;
    int n;

    k->oldinput = k->input;

    while ((n = SDL_PeepEvents(&ev, 1, SDL_GETEVENT,
                SDL_KEYEVENTMASK | SDL_QUITMASK)) > 0)
    {
        switch (ev.type)
        {
            case SDL_KEYDOWN:
                /* Fall through. */
            case SDL_KEYUP:
                key_event(&ev.key, k);
                break;

            case SDL_QUIT:
                append_key(KEYBOARD_KEY_EXIT, '\0', 0, k);
                break;

            default:
                fprintf(stderr, "%s: unexpected SDL_Event %d\n",
                    __func__, ev.type);
                break;
        }
    }

    if (n < 0)
        fprintf(stderr, "%s: SDL_PeepEvents: %s\n", __func__, SDL_GetError());
}

bool keyboard_available(void)
{
    /* Keyboard availability is always assumed in SDL. */
    return true;
}
