#include "twn_input_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_control.h"
#include "twn_engine_context_c.h"
#include "twn_input.h"

#include <SDL2/SDL.h>
#include <stb_ds.h>

#include <stdbool.h>


static void update_action_pressed_state(InputState *input, Action *action) {
    for (size_t i = 0; i < (uint64_t)ctx.keybind_slots; ++i) {
        switch (action->bindings[i].source) {
        case BUTTON_SOURCE_NOT_SET:
            break;
        case BUTTON_SOURCE_KEYBOARD_CHARACTER:
            CRY("Action pressed state updated failed", "BUTTON_SOURCE_KEYBOARD_CHARACTER isn't handled");
            break;
        case BUTTON_SOURCE_GAMEPAD:
            CRY("Action pressed state updated failed", "BUTTON_SOURCE_GAMEPAD isn't handled");
            break;
        case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
            /* not pressed */
            if (input->keyboard_state[action->bindings[i].code.scancode] == 0) {
                action->just_changed = action->is_pressed;
                action->is_pressed = false;
            }
            /* pressed */
            else {
                action->just_changed = !action->is_pressed;
                action->is_pressed = true;
                return;
            }
            break;
        case BUTTON_SOURCE_MOUSE:
            /* not pressed */
            if ((input->mouse_state & action->bindings[i].code.mouse_button) == 0) {
                action->just_changed = action->is_pressed;
                action->is_pressed = false;
            }
            /* pressed */
            else {
                action->just_changed = !action->is_pressed;
                action->is_pressed = true;
                action->position.x = ((float)input->mouse_window_position.x - ctx.viewport_rect.x) / ctx.viewport_scale;
                action->position.y = ((float)input->mouse_window_position.y - ctx.viewport_rect.y) / ctx.viewport_scale;

                /* TODO: */
                /*
                 * SDL_RenderWindowToLogical will turn window mouse
                 * coords into a position inside the logical render
                 * area. this has to be done to get an accurate point
                 * that can actually be used in game logic
                 */
                // SDL_RenderWindowToLogical(input->renderer,
                //                           input->mouse_window_position.x,
                //                           input->mouse_window_position.y,
                //                           &action->position.x,
                //                           &action->position.y);
                return;
            }
            break;
        default:
            break;
        }
    }
}


static ActionHashItem *input_add_action(char const *action_name) {
    SDL_assert(action_name);

    Action new_action = { 0 };
    new_action.bindings = SDL_calloc(ctx.keybind_slots, sizeof *new_action.bindings);
    shput(ctx.input.action_hash, action_name, new_action);
    return shgetp(ctx.input.action_hash, action_name);
}


static void input_delete_action(char const *action_name) {
    SDL_assert(action_name);

    ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
    SDL_assert(action);

    SDL_free(action->value.bindings);
    shdel(ctx.input.action_hash, action_name);
}


static void input_bind_code_to_action(InputState *input,
                                      char const *action_name,
                                      ButtonSource source,
                                      union ButtonCode code)
{
    ActionHashItem *action_item = shgetp_null(input->action_hash, action_name);
    if (!action_item)
        action_item = input_add_action(action_name);

    Action *action = &action_item->value;

    /* check every binding to make sure this code isn't already bound */
    for (size_t i = 0; i < action->num_bindings; ++i) {
        Button *binding = &action->bindings[i];

        if (binding->source != source)
            break;

        bool is_already_bound = false;
        switch (binding->source) {
        case BUTTON_SOURCE_NOT_SET:
            break;
        case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
            is_already_bound = binding->code.scancode == code.scancode;
            break;
        case BUTTON_SOURCE_KEYBOARD_CHARACTER:
            is_already_bound = binding->code.keycode == code.keycode;
            break;
        case BUTTON_SOURCE_GAMEPAD:
            is_already_bound = binding->code.gamepad_button == code.gamepad_button;
            break;
        case BUTTON_SOURCE_MOUSE:
            is_already_bound = binding->code.mouse_button == code.mouse_button;
            break;
        default:
            SDL_assert(false);
        }

        if (is_already_bound) {
            /* keep it alive */
            binding->in_use = true;
            return;
        }
    }

    /* if we're at max bindings, forget the first element and shift the rest */
    if (action->num_bindings == (uint64_t)ctx.keybind_slots) {
        --action->num_bindings;
        size_t shifted_size = sizeof action->bindings[0] * (ctx.keybind_slots - 1);
        SDL_memmove(action->bindings, action->bindings + 1, shifted_size);
    }

    action->bindings[action->num_bindings++] = (Button) {
        .source = source,
        .code = code,
        .in_use = true,
    };
}


static void input_unbind_code_from_action(InputState *input,
                                          char const *action_name,
                                          ButtonSource source,
                                          union ButtonCode code)
{
    ActionHashItem *action_item = shgetp_null(input->action_hash, action_name);
    if (!action_item)
        action_item = input_add_action(action_name);

    Action *action = &action_item->value;

    /* check every binding to make sure this code is bound */
    size_t index = 0;
    bool is_bound = false;
    for (index = 0; index < action->num_bindings; ++index) {
        Button *binding = &action->bindings[index];

        if (binding->source != source)
            continue;

        switch (binding->source) {
        case BUTTON_SOURCE_NOT_SET:
            break;
        case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
            is_bound = binding->code.scancode == code.scancode;
            break;
        case BUTTON_SOURCE_KEYBOARD_CHARACTER:
            is_bound = binding->code.keycode == code.keycode;
            break;
        case BUTTON_SOURCE_GAMEPAD:
            is_bound = binding->code.gamepad_button == code.gamepad_button;
            break;
        case BUTTON_SOURCE_MOUSE:
            is_bound = binding->code.mouse_button == code.mouse_button;
            break;
        default:
            SDL_assert(false);
        }

        /* stop early, this won't change */
        if (is_bound)
            break;
    }

    if (!is_bound)
        return;

    /* remove the element to unbind and shift the rest so there isn't a gap */
    size_t elements_after_index = action->num_bindings - index;
    size_t shifted_size = elements_after_index * sizeof action->bindings[0];
    SDL_memmove(action->bindings + index, action->bindings + index + 1, shifted_size);
    --action->num_bindings;
}


void input_state_init(InputState *input) {
    sh_new_strdup(input->action_hash);
}


void input_state_deinit(InputState *input) {
    input_reset_state(input);
}


void input_state_update(InputState *input) {
    /* TODO: don't spam it if it happens */
    if (SDL_SetRelativeMouseMode(input->mouse_captured) != 0)
        log_warn("(%s) Mouse capture isn't supported.", __func__);

    int x, y;

    input->keyboard_state = SDL_GetKeyboardState(NULL);
    input->mouse_state = SDL_GetMouseState(&x, &y);
    input->mouse_window_position = (Vec2){ (float)x, (float)y };

    SDL_GetRelativeMouseState(&x, &y);
    input->mouse_relative_position = (Vec2){ (float)x, (float)y };

    ctx.game.mouse_position = input->mouse_window_position;
    ctx.game.mouse_movement = input->mouse_relative_position;

    for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
        Action *action = &input->action_hash[i].value;
        /* collect unused */
        for (size_t u = 0; u < action->num_bindings; ++u) {
            Button *button = &action->bindings[u];
            if (!button->in_use)
                input_unbind_code_from_action(input, input->action_hash[i].key, button->source, button->code);
            else 
                button->in_use = false;
        }

        update_action_pressed_state(input, action);
    }

    size_t removed = 0;
    for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
        if (input->action_hash[i - removed].value.num_bindings == 0)
            input_delete_action(input->action_hash[i - removed].key);
    }
}


void input_action(char const *action_name,
                               Control control)
{
    SDL_assert_always(action_name);

    if (CONTROL_SCANCODE_START <= control && control < CONTROL_SCANCODE_LIMIT)
        input_bind_code_to_action(&ctx.input,
                                  action_name,
                                  BUTTON_SOURCE_KEYBOARD_PHYSICAL,
                                  (union ButtonCode) { .scancode = (SDL_Scancode)control });

    else if (CONTROL_MOUSECODE_START <= control && control < CONTROL_MOUSECODE_LIMIT) {
        uint8_t const mouse_button = (uint8_t)(control - CONTROL_MOUSECODE_START);
        input_bind_code_to_action(&ctx.input,
                                  action_name,
                                  BUTTON_SOURCE_MOUSE,
                                  (union ButtonCode) { .mouse_button = (uint8_t)SDL_BUTTON(mouse_button)});
    } else
        log_warn("(%s) Invalid control value given: %i.", __func__, control);
}


bool input_action_pressed(char const *action_name) {
    SDL_assert_always(action_name);

    ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
    if (action == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return false;
    }
    return action->value.is_pressed;
}


bool input_action_just_pressed(char const *action_name) {
    SDL_assert_always(action_name);

    ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
    if (action == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return false;
    }
    return action->value.is_pressed && action->value.just_changed;
}


bool input_action_just_released(char const *action_name) {
    SDL_assert_always(action_name);

    ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
    if (action == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return false;
    }
    return !action->value.is_pressed && action->value.just_changed;
}


Vec2 input_action_position(char const *action_name) {
    SDL_assert_always(action_name);

    ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
    if (action == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return (Vec2) { 0 };
    }

    return action->value.position;
}


void input_mouse_captured(bool enabled) {
    ctx.input.mouse_captured = enabled;
}


void input_reset_state(InputState *input) {
    for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
        Action *action = &input->action_hash[i].value;
        SDL_free(action->bindings);
    }

    stbds_shfree(input->action_hash);
}
