#include "keymap.h"
#include <string.h>
#include <ctype.h>
#include <stddef.h>

KeyBinding g_keymap[ACTION_COUNT];

void keymap_set_defaults(KeyBinding *out) {
    out[ACTION_SAVE]        = (KeyBinding){ SDLK_s, true, false, false };
    out[ACTION_OPEN]        = (KeyBinding){ SDLK_o, true, false, false };
    out[ACTION_COPY]        = (KeyBinding){ SDLK_c, true, false, false };
    out[ACTION_PASTE]       = (KeyBinding){ SDLK_v, true, false, false };
    out[ACTION_UNDO]        = (KeyBinding){ SDLK_z, true, false, false };
    out[ACTION_REDO]        = (KeyBinding){ SDLK_z, true, true, false };
    out[ACTION_QUIT]        = (KeyBinding){ SDLK_x, true, false, false };
    out[ACTION_HELP]        = (KeyBinding){ SDLK_SLASH, true, false, false };
    out[ACTION_SOURCE_VIEW] = (KeyBinding){ SDLK_m, true, false, false };
    out[ACTION_ZOOM_IN]     = (KeyBinding){ SDLK_PLUS, true, false, false };
    out[ACTION_ZOOM_OUT]    = (KeyBinding){ SDLK_MINUS, true, false, false };
    out[ACTION_ZOOM_RESET]  = (KeyBinding){ SDLK_0, true, false, false };
}

static const char *trim(const char *s, int len, int *outLen) {
    while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
    while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
    *outLen = len;
    return s;
}

static bool token_is(const char *tok, int len, const char *name) {
    size_t n = strlen(name);
    if ((size_t)len != n) return false;
    for (size_t i = 0; i < n; i++) {
        if (tolower((unsigned char)tok[i]) != (unsigned char)name[i]) return false;
    }
    return true;
}

static int lookup_key_token(const char *tok, int len) {
    if (len == 1) {
        char c = tok[0];
        if (c >= 'a' && c <= 'z') return SDLK_a + (c - 'a');
        if (c >= 'A' && c <= 'Z') return SDLK_a + (c - 'A');
        if (c >= '0' && c <= '9') return SDLK_0 + (c - '0');
        switch (c) {
            case '/': return SDLK_SLASH;
            case '=': return SDLK_EQUALS;
            case '+': return SDLK_PLUS;
            case '-': return SDLK_MINUS;
            case ',': return SDLK_COMMA;
            case '.': return SDLK_PERIOD;
            case ';': return SDLK_SEMICOLON;
            case '\'': return SDLK_QUOTE;
            case '\\': return SDLK_BACKSLASH;
            case '[': return SDLK_LEFTBRACKET;
            case ']': return SDLK_RIGHTBRACKET;
            case '`': return SDLK_BACKQUOTE;
        }
        return SDLK_UNKNOWN;
    }

    static const struct { const char *name; int key; } NAMED[] = {
        { "f1", SDLK_F1 }, { "f2", SDLK_F2 }, { "f3", SDLK_F3 }, { "f4", SDLK_F4 },
        { "f5", SDLK_F5 }, { "f6", SDLK_F6 }, { "f7", SDLK_F7 }, { "f8", SDLK_F8 },
        { "f9", SDLK_F9 }, { "f10", SDLK_F10 }, { "f11", SDLK_F11 }, { "f12", SDLK_F12 },
        { "slash", SDLK_SLASH }, { "equal", SDLK_EQUALS }, { "plus", SDLK_PLUS }, { "minus", SDLK_MINUS },
        { "comma", SDLK_COMMA }, { "period", SDLK_PERIOD }, { "semicolon", SDLK_SEMICOLON },
        { "apostrophe", SDLK_QUOTE }, { "backslash", SDLK_BACKSLASH },
        { "leftbracket", SDLK_LEFTBRACKET }, { "rightbracket", SDLK_RIGHTBRACKET },
        { "grave", SDLK_BACKQUOTE }, { "space", SDLK_SPACE }, { "enter", SDLK_RETURN },
        { "escape", SDLK_ESCAPE }, { "tab", SDLK_TAB }, { "backspace", SDLK_BACKSPACE },
        { "delete", SDLK_DELETE }, { "home", SDLK_HOME }, { "end", SDLK_END },
        { "pageup", SDLK_PAGEUP }, { "pagedown", SDLK_PAGEDOWN },
        { "up", SDLK_UP }, { "down", SDLK_DOWN }, { "left", SDLK_LEFT }, { "right", SDLK_RIGHT },
        { "insert", SDLK_INSERT },
    };
    for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++) {
        if (token_is(tok, len, NAMED[i].name)) return NAMED[i].key;
    }
    return SDLK_UNKNOWN;
}

bool keymap_parse_spec(const char *spec, int len, KeyBinding *out) {
    int trimmedLen;
    const char *s = trim(spec, len, &trimmedLen);
    if (trimmedLen == 0) return false;

    bool ctrl = false, shift = false, alt = false;
    int key = SDLK_UNKNOWN;

    const char *p = s;
    int remaining = trimmedLen;
    while (remaining > 0) {
        const char *plus = memchr(p, '+', (size_t)remaining);
        /* A '+' that's the very last character left can't be a separator (nothing follows it
           to separate) -- treat it as the literal '+' key instead, so e.g. "ctrl++" parses as
           ctrl + the '+' key, not ctrl + an empty trailing token. */
        if (plus && (plus - p) == remaining - 1) plus = NULL;
        int tokLen = plus ? (int)(plus - p) : remaining;
        int tlen;
        const char *tok = trim(p, tokLen, &tlen);
        if (tlen == 0) return false;

        if (token_is(tok, tlen, "ctrl") || token_is(tok, tlen, "control")) ctrl = true;
        else if (token_is(tok, tlen, "shift")) shift = true;
        else if (token_is(tok, tlen, "alt")) alt = true;
        else {
            int k = lookup_key_token(tok, tlen);
            if (k == SDLK_UNKNOWN) return false;
            key = k;
        }

        if (!plus) break;
        remaining -= (int)(plus - p) + 1;
        p = plus + 1;
    }

    if (key == SDLK_UNKNOWN) return false;
    out->key = key;
    out->ctrl = ctrl;
    out->shift = shift;
    out->alt = alt;
    return true;
}

static const char *ACTION_CONFIG_NAMES[ACTION_COUNT] = {
    [ACTION_SAVE] = "key.save",
    [ACTION_OPEN] = "key.open",
    [ACTION_COPY] = "key.copy",
    [ACTION_PASTE] = "key.paste",
    [ACTION_UNDO] = "key.undo",
    [ACTION_REDO] = "key.redo",
    [ACTION_QUIT] = "key.quit",
    [ACTION_HELP] = "key.help",
    [ACTION_SOURCE_VIEW] = "key.source_view",
    [ACTION_ZOOM_IN] = "key.zoom_in",
    [ACTION_ZOOM_OUT] = "key.zoom_out",
    [ACTION_ZOOM_RESET] = "key.zoom_reset",
};

bool keymap_parse_config_line(const char *key, int keyLen, const char *value, int valueLen, KeyBinding *keymap) {
    for (int i = 0; i < ACTION_COUNT; i++) {
        size_t nameLen = strlen(ACTION_CONFIG_NAMES[i]);
        if ((size_t)keyLen == nameLen && memcmp(key, ACTION_CONFIG_NAMES[i], nameLen) == 0) {
            KeyBinding b;
            if (!keymap_parse_spec(value, valueLen, &b)) return false;
            keymap[i] = b;
            return true;
        }
    }
    return false;
}

#define KEYMAP_MAX_KEYDOWNS_PER_FRAME 16
static SDL_Keycode s_keydowns[KEYMAP_MAX_KEYDOWNS_PER_FRAME];
static int s_keydownCount;

void keymap_begin_frame(void) {
    s_keydownCount = 0;
}

void keymap_note_keydown(SDL_Keycode key) {
    if (s_keydownCount < KEYMAP_MAX_KEYDOWNS_PER_FRAME) s_keydowns[s_keydownCount++] = key;
}

bool keymap_key_pressed_raw(SDL_Keycode key) {
    for (int i = 0; i < s_keydownCount; i++) {
        if (s_keydowns[i] == key) return true;
    }
    return false;
}

/* Unlike raylib's physical-scancode KEY_*, SDL_Keycode for a symbol/punctuation key already
   reflects whatever the CURRENT layout needs to produce it -- e.g. "=" is Shift+0 on a Swedish
   keyboard, so a KEYDOWN event carrying SDLK_EQUALS arrives with KMOD_SHIFT set. Requiring an
   *additional* exact shift==false match on top of that (as stored in a "ctrl+=" binding, where
   `shift` just means "the word 'shift' wasn't in the spec") would make such a binding permanently
   unsatisfiable on layouts where shift is needed to type the base character at all. Letters and
   digits don't have this problem -- SDLK_a/SDLK_0 etc. stay constant regardless of shift, so an
   explicit shift modifier there (e.g. "ctrl+shift+z" vs "ctrl+z") is a real, independent, and
   layout-independent distinction worth enforcing exactly. */
static bool key_is_alnum(int key) {
    return (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_0 && key <= SDLK_9);
}

static bool modifiers_match(const KeyBinding *b, bool ctrl, bool shift, bool alt) {
    if (b->ctrl != ctrl || b->alt != alt) return false;
    if (key_is_alnum(b->key)) return b->shift == shift;
    return true;
}

bool keymap_pressed(Action a, bool ctrl, bool shift, bool alt) {
    KeyBinding *b = &g_keymap[a];
    if (b->key == SDLK_UNKNOWN) return false;
    if (!modifiers_match(b, ctrl, shift, alt)) return false;
    return keymap_key_pressed_raw(b->key);
}

bool keymap_repeat_scancode(SDL_Scancode sc, double now, double *nextRepeatAt) {
    if (!SDL_GetKeyboardState(NULL)[sc]) { *nextRepeatAt = 0.0; return false; }
    if (now >= *nextRepeatAt) { *nextRepeatAt = now + (*nextRepeatAt == 0.0 ? 0.4 : 0.03); return true; }
    return false;
}

bool keymap_repeat(Action a, bool ctrl, bool shift, bool alt, double now, double *nextRepeatAt) {
    KeyBinding *b = &g_keymap[a];
    /* Deliberately exact-match shift here (unlike keymap_pressed above), even for non-alnum
       keys: this resolves the binding's keycode back to a physical scancode via
       SDL_GetScancodeFromKey, which is a static/unshifted reverse mapping -- on a layout where
       two different keycodes share one physical key (e.g. Swedish's "0" key types "0" plain,
       "=" with shift), SDLK_EQUALS and SDLK_0 would both resolve to the same scancode, and
       relaxing shift here would make holding plain Ctrl+0 ambiguously also satisfy a "ctrl+="
       binding. The numpad fallbacks main.c wires in independently cover zoom reliably regardless
       of whether this primary binding's repeat path is reachable on a given layout. */
    if (b->key == SDLK_UNKNOWN || b->ctrl != ctrl || b->shift != shift || b->alt != alt) {
        *nextRepeatAt = 0.0;
        return false;
    }
    return keymap_repeat_scancode(SDL_GetScancodeFromKey(b->key), now, nextRepeatAt);
}
