#include "townengine/input/internal_api.h"
#include "townengine/context.h"
#include "townengine/util.h"

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

#include <stdbool.h>
#include <stdlib.h>


static void update_action_pressed_state(struct input_state *input, struct action *action) {
    for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
        switch (action->bindings[i].source) {
        case BUTTON_SOURCE_NOT_SET:
            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;
                action->position.x = (float)input->mouse_window_position.x;

                /* 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 void input_bind_code_to_action(struct input_state *input,
                                      char *action_name,
                                      enum button_source source,
                                      union button_code code)
{
    struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
    if (action_item == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return;
    }
    struct action *action = &action_item->value;

    /* check every binding to make sure this code isn't already bound */
    for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
        struct 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;
        }

        if (is_already_bound) {
            log_warn("(%s) Code already bound to action \"%s\".", __func__, action_name);
            return;
        }
    }

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

    action->bindings[action->num_bindings++] = (struct button) {
        .source = source,
        .code = code,
    };
}


static void input_unbind_code_from_action(struct input_state *input,
                                          char *action_name,
                                          enum button_source source,
                                          union button_code code)
{
    struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
    if (action_item == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return;
    }
    struct 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 < SDL_arraysize(action->bindings); ++index) {
        struct 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;
        }

        if (is_bound)
            break;
    }

    if (!is_bound) {
        log_warn("(%s) Code is not bound to action \"%s\".", __func__, action_name);
        return;
    }

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


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


void input_state_deinit(struct input_state *input) {
    shfree(input->action_hash);
}


void input_state_update(struct input_state *input) {
    input->keyboard_state = SDL_GetKeyboardState(NULL);
    input->mouse_state = SDL_GetMouseState(&input->mouse_window_position.x,
                                           &input->mouse_window_position.y);

    SDL_GetRelativeMouseState(&input->mouse_relative_position.x,
                              &input->mouse_relative_position.y);

    for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
        struct action *action = &input->action_hash[i].value;
        update_action_pressed_state(input, action);
    }
}


void input_bind_action_scancode(struct input_state *input,
                                char *action_name,
                                SDL_Scancode scancode)
{
    input_bind_code_to_action(input,
                              action_name,
                              BUTTON_SOURCE_KEYBOARD_PHYSICAL,
                              (union button_code) { .scancode = scancode });
}


void input_unbind_action_scancode(struct input_state *input,
                                  char *action_name,
                                  SDL_Scancode scancode)
{
    input_unbind_code_from_action(input,
                                  action_name,
                                  BUTTON_SOURCE_KEYBOARD_PHYSICAL,
                                  (union button_code) { .scancode = scancode });
}


void input_bind_action_mouse(struct input_state *input,
                             char *action_name,
                             uint8_t mouse_button)
{
    input_bind_code_to_action(input,
                              action_name,
                              BUTTON_SOURCE_MOUSE,
                              (union button_code) { .mouse_button = mouse_button});
}


void input_unbind_action_mouse(struct input_state *input,
                               char *action_name,
                               uint8_t mouse_button)
{
    input_unbind_code_from_action(input,
                                  action_name,
                                  BUTTON_SOURCE_MOUSE,
                                  (union button_code) { .mouse_button = mouse_button});
}


void input_add_action(struct input_state *input, char *action_name) {
    if (shgeti(input->action_hash, action_name) >= 0) {
        log_warn("(%s) Action \"%s\" is already registered.", __func__, action_name);
        return;
    }

    shput(input->action_hash, action_name, (struct action) { 0 });
}


void input_delete_action(struct input_state *input, char *action_name) {
    if (shdel(input->action_hash, action_name) == 0)
        log_warn("(%s) Action \"%s\" is not registered.", __func__, action_name);
}


bool input_is_action_pressed(struct input_state *input, char *action_name) {
    struct action_hash_item *action = shgetp_null(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_is_action_just_pressed(struct input_state *input, char *action_name) {
    struct action_hash_item *action = shgetp_null(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_is_action_just_released(struct input_state *input, char *action_name) {
    struct action_hash_item *action = shgetp_null(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;
}


t_fvec2 input_get_action_position(struct input_state *input, char *action_name) {
    struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
    if (action == NULL) {
        log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
        return (t_fvec2) { 0 };
    }

    return action->value.position;
}


void input_set_mouse_captured(struct input_state *input, bool enabled) {
    /* TODO: returns -1 if not supported, but like... do we care? */
    SDL_SetRelativeMouseMode(enabled);
}


bool input_is_mouse_captured(struct input_state *input) {
    return SDL_GetRelativeMouseMode();
}


void input_reset_state(struct input_state *input) {
    stbds_shfree(input->action_hash);
}
