#include "core/input/input.hpp"

#include <SDL3/SDL.h>  // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode

#include <iostream>       // Para basic_ostream, operator<<, cout, cerr
#include <memory>         // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <ranges>         // Para __find_if_fn, find_if
#include <unordered_map>  // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility>        // Para pair, move

#include "game/options.hpp"  // Para Options::controls

// Emscripten-only: SDL 3.4+ no casa el GUID de los mandos de Chrome Android
// con gamecontrollerdb (el gamepad.id de Android no lleva Vendor/Product, el
// parser extrae valores basura, el GUID resultante no está en la db y el
// gamepad queda abierto con un mapping incorrecto). Como la W3C Gamepad API
// garantiza el layout estándar cuando el navegador reporta mapping=="standard",
// inyectamos un mapping SDL con ese layout para el GUID del joystick antes
// de abrirlo como gamepad. Fuera de Emscripten es un no-op.
//
// OJO: el layout W3C "standard" NO se expone tal cual al joystick SDL. El
// driver de SDL3 (src/joystick/emscripten/SDL_sysjoystick.c) reempaqueta el
// gamepad W3C antes de entregarlo: los 4 botones del d-pad (W3C b12-b15) se
// convierten en un hat (hat 0), los 2 botones de triggers (W3C b6-b7) se
// convierten en ejes analógicos (a4-a5 si el mando tenía 4 ejes) y los
// botones restantes se renumeran compactándose. Por eso el mapping tiene que
// referirse a la numeración *después* del remap, no a la del W3C.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
    SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
    char guidStr[33];
    SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
    const char* name = SDL_GetJoystickNameForID(jid);
    if ((name == nullptr) || (*name == 0)) { name = "Standard Gamepad"; }

    char mapping[512];
    SDL_snprintf(mapping, sizeof(mapping),
        "%s,%s,"
        "a:b0,b:b1,x:b2,y:b3,"
        "leftshoulder:b4,rightshoulder:b5,"
        "back:b6,start:b7,"
        "leftstick:b8,rightstick:b9,"
        "dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,"
        "leftx:a0,lefty:a1,rightx:a2,righty:a3,"
        "lefttrigger:a4,righttrigger:a5,"
        "platform:Emscripten",
        guidStr,
        name);
    SDL_AddGamepadMapping(mapping);
#else
    (void)jid;
#endif
}

// Singleton
Input* Input::instance = nullptr;

// Inicializa la instancia única del singleton
void Input::init(const std::string& game_controller_db_path) {
    Input::instance = new Input(game_controller_db_path);
}

// Libera la instancia
void Input::destroy() { delete Input::instance; }

// Obtiene la instancia
auto Input::get() -> Input* { return Input::instance; }

// Constructor
Input::Input(std::string game_controller_db_path)
    : gamepad_mappings_file_(std::move(game_controller_db_path)) {
    // Bindings de gameplay (rebindeables por el jugador vía Options)
    // Las teclas de sistema (F1-F12, B, S, `, Esc...) se cargan desde keys.yaml
    // vía KeyConfig::applyGlobalBindings() — NO duplicar aquí.
    keyboard_.bindings = {
        {Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
        {Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
        {Action::JUMP, KeyState{.scancode = SDL_SCANCODE_UP}},
        {Action::DOWN, KeyState{.scancode = SDL_SCANCODE_DOWN}},
        {Action::ACCEPT, KeyState{.scancode = SDL_SCANCODE_RETURN}},
        {Action::CANCEL, KeyState{.scancode = SDL_SCANCODE_ESCAPE}},
    };

    initSDLGamePad();  // Inicializa el subsistema SDL_INIT_GAMEPAD
}

// Asigna inputs a teclas
void Input::bindKey(Action action, SDL_Scancode code) {
    keyboard_.bindings[action].scancode = code;
}

// Aplica las teclas configuradas desde Options
void Input::applyKeyboardBindingsFromOptions() {
    bindKey(Action::LEFT, Options::keyboard_controls.key_left);
    bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
    bindKey(Action::JUMP, Options::keyboard_controls.key_jump);
}

// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
void Input::applyGamepadBindingsFromOptions() {
    // Si no hay gamepads conectados, no hay nada que hacer
    if (gamepads_.empty()) {
        return;
    }

    // Obtener el primer gamepad conectado
    const auto& gamepad = gamepads_[0];

    // Aplicar bindings desde Options
    // Los valores pueden ser:
    // - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
    // - 100: L2 trigger
    // - 101: R2 trigger
    // - 200+: Ejes del stick analógico
    gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
    gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
    gamepad->bindings[Action::JUMP].button = Options::gamepad_controls.button_jump;
}

// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
    if (gamepad != nullptr) {
        gamepad->bindings[action].button = button;
    }
}

// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) {
    if (gamepad != nullptr) {
        gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
    }
}

// Comprueba si alguna acción está activa
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
    bool success_keyboard = false;
    bool success_controller = false;

    if (check_keyboard) {
        if (repeat) {  // El usuario quiere saber si está pulsada (estado mantenido)
            success_keyboard = keyboard_.bindings[action].is_held;
        } else {  // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
            success_keyboard = keyboard_.bindings[action].just_pressed;
        }
    }

    // Si gamepad es nullptr pero hay mandos conectados, usar el primero
    std::shared_ptr<Gamepad> active_gamepad = gamepad;
    if (active_gamepad == nullptr && !gamepads_.empty()) {
        active_gamepad = gamepads_[0];
    }

    if (active_gamepad != nullptr) {
        success_controller = checkAxisInput(action, active_gamepad, repeat);

        if (!success_controller) {
            success_controller = checkTriggerInput(action, active_gamepad, repeat);
        }

        if (!success_controller) {
            if (repeat) {  // El usuario quiere saber si está pulsada (estado mantenido)
                success_controller = active_gamepad->bindings[action].is_held;
            } else {  // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
                success_controller = active_gamepad->bindings[action].just_pressed;
            }
        }
    }

    return (success_keyboard || success_controller);
}

// Comprueba si hay almenos una acción activa
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
    // Obtenemos el número total de acciones posibles para iterar sobre ellas.

    // --- Comprobación del Teclado ---
    if (check_keyboard) {
        for (const auto& pair : keyboard_.bindings) {
            // Simplemente leemos el estado pre-calculado por Input::update().
            // Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
            if (pair.second.just_pressed) {
                return true;  // Se encontró una acción recién pulsada.
            }
        }
    }

    // Si gamepad es nullptr pero hay mandos conectados, usar el primero
    std::shared_ptr<Gamepad> active_gamepad = gamepad;
    if (active_gamepad == nullptr && !gamepads_.empty()) {
        active_gamepad = gamepads_[0];
    }

    // --- Comprobación del Mando ---
    // Comprobamos si hay mandos y si el índice solicitado es válido.
    if (active_gamepad != nullptr) {
        // Iteramos sobre todas las acciones, no sobre el número de mandos.
        for (const auto& pair : active_gamepad->bindings) {
            // Leemos el estado pre-calculado para el mando y la acción específicos.
            if (pair.second.just_pressed) {
                return true;  // Se encontró una acción recién pulsada en el mando.
            }
        }
    }

    // Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
    return false;
}

// Comprueba si hay algún botón pulsado
auto Input::checkAnyButton(bool repeat) -> bool {
    // Solo comprueba los botones definidos previamente
    for (auto bi : BUTTON_INPUTS) {
        // Comprueba el teclado
        if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
            return true;
        }

        // Comprueba los mandos
        for (const auto& gamepad : gamepads_) {
            if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
                return true;
            }
        }
    }

    return false;
}

// Comprueba si hay algun mando conectado
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }

// Obten el nombre de un mando de juego
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
    return gamepad == nullptr ? std::string() : gamepad->name;
}

// Obtiene la lista de nombres de mandos
auto Input::getControllerNames() const -> std::vector<std::string> {
    std::vector<std::string> names;
    for (const auto& gamepad : gamepads_) {
        names.push_back(gamepad->name);
    }
    return names;
}

// Obten el número de mandos conectados
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }

// Obtiene el gamepad a partir de un event.id
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> {
    for (const auto& gamepad : gamepads_) {
        if (gamepad->instance_id == id) {
            return gamepad;
        }
    }
    return nullptr;
}

auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> {
    for (const auto& gamepad : gamepads_) {
        if (gamepad && gamepad->name == name) {
            return gamepad;
        }
    }
    return nullptr;
}

// Obtiene el SDL_GamepadButton asignado a un action
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton {
    return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
}

// Comprueba el eje del mando
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
    // Obtener el binding configurado para esta acción
    auto& binding = gamepad->bindings[action];

    // Solo revisar ejes si el binding está configurado como eje (valores 200+)
    // 200 = Left stick izquierda, 201 = Left stick derecha
    if (binding.button < 200) {
        // El binding no es un eje, no revisar axis
        return false;
    }

    // Determinar qué eje y dirección revisar según el binding
    bool axis_active_now = false;

    if (binding.button == 200) {
        // Left stick izquierda
        axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
    } else if (binding.button == 201) {
        // Left stick derecha
        axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
    } else {
        // Binding de eje no soportado
        return false;
    }

    if (repeat) {
        // Si se permite repetir, simplemente devolvemos el estado actual
        return axis_active_now;
    }  // Si no se permite repetir, aplicamos la lógica de transición
    if (axis_active_now && !binding.axis_active) {
        // Transición de inactivo a activo
        binding.axis_active = true;
        return true;
    }
    if (!axis_active_now && binding.axis_active) {
        // Transición de activo a inactivo
        binding.axis_active = false;
    }
    // Mantener el estado actual
    return false;
}

// Comprueba los triggers del mando como botones digitales
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
    // Solo manejamos botones específicos que pueden ser triggers
    if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
        // Solo procesamos L2 y R2 como triggers
        int button = gamepad->bindings[action].button;

        // Verificar si el botón mapeado corresponde a un trigger virtual
        // (Para esto necesitamos valores especiales que representen L2/R2 como botones)
        bool trigger_active_now = false;

        // Usamos constantes especiales para L2 y R2 como botones
        if (button == TRIGGER_L2_AS_BUTTON) {  // L2 como botón
            Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
            trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
        } else if (button == TRIGGER_R2_AS_BUTTON) {  // R2 como botón
            Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
            trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
        } else {
            return false;  // No es un trigger
        }

        // Referencia al binding correspondiente
        auto& binding = gamepad->bindings[action];

        if (repeat) {
            // Si se permite repetir, simplemente devolvemos el estado actual
            return trigger_active_now;
        }

        // Si no se permite repetir, aplicamos la lógica de transición
        if (trigger_active_now && !binding.trigger_active) {
            // Transición de inactivo a activo
            binding.trigger_active = true;
            return true;
        }
        if (!trigger_active_now && binding.trigger_active) {
            // Transición de activo a inactivo
            binding.trigger_active = false;
        }

        // Mantener el estado actual
        return false;
    }

    return false;
}

void Input::addGamepadMappingsFromFile() {
    if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
        std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
    }
}

void Input::discoverGamepads() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        handleEvent(event);  // Comprueba mandos conectados
    }
}

void Input::initSDLGamePad() {
    if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
        if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
        } else {
            addGamepadMappingsFromFile();
            discoverGamepads();
            std::cout << "\n** INPUT SYSTEM **\n";
            std::cout << "Input System initialized successfully\n";
        }
    }
}

void Input::resetInputStates() {
    // Resetear todos los KeyBindings.active a false
    for (auto& key : keyboard_.bindings) {
        key.second.is_held = false;
        key.second.just_pressed = false;
    }
    // Resetear todos los ControllerBindings.active a false
    for (const auto& gamepad : gamepads_) {
        for (auto& binding : gamepad->bindings) {
            binding.second.is_held = false;
            binding.second.just_pressed = false;
            binding.second.trigger_active = false;
        }
    }
}

void Input::update() {
    // --- TECLADO ---
    const bool* key_states = SDL_GetKeyboardState(nullptr);

    for (auto& binding : keyboard_.bindings) {
        bool key_is_down_now = key_states[binding.second.scancode];

        // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
        binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
        binding.second.is_held = key_is_down_now;
    }

    // --- MANDOS ---
    for (const auto& gamepad : gamepads_) {
        for (auto& binding : gamepad->bindings) {
            bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;

            // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
            binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
            binding.second.is_held = button_is_down_now;
        }
    }
}

auto Input::handleEvent(const SDL_Event& event) -> std::string {
    switch (event.type) {
        case SDL_EVENT_GAMEPAD_ADDED:
            return addGamepad(event.gdevice.which);
        case SDL_EVENT_GAMEPAD_REMOVED:
            return removeGamepad(event.gdevice.which);
        default:
            return {};
    }
}

auto Input::addGamepad(int device_index) -> std::string {
    installWebStandardMapping(device_index);
    SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
    if (pad == nullptr) {
        std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
        return {};
    }

    auto gamepad = std::make_shared<Gamepad>(pad);
    auto name = gamepad->name;
    std::cout << "Gamepad connected (" << name << ")" << '\n';
    gamepads_.push_back(std::move(gamepad));

    // Aplica los bindings de Options al nuevo gamepad. En hot-plug (incluido wasm,
    // donde el navegador sólo expone los gamepads tras activación del usuario) el
    // ctor ya llamó a applyGamepadBindingsFromOptions() pero gamepads_ estaba vacío.
    applyGamepadBindingsFromOptions();

    return name;
}

auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
    auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) -> bool {
        return gamepad->instance_id == id;
    });

    if (it != gamepads_.end()) {
        std::string name = (*it)->name;
        std::cout << "Gamepad disconnected (" << name << ")" << '\n';
        gamepads_.erase(it);
        return name;
    }
    std::cerr << "No se encontró el gamepad con ID " << id << '\n';
    return {};
}

void Input::printConnectedGamepads() const {
    if (gamepads_.empty()) {
        std::cout << "No hay gamepads conectados." << '\n';
        return;
    }

    std::cout << "Gamepads conectados:\n";
    for (const auto& gamepad : gamepads_) {
        std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
        std::cout << "  - ID: " << gamepad->instance_id
                  << ", Nombre: " << name << ")" << '\n';
    }
}

auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> {
    // Si no hay gamepads disponibles, devolver gamepad por defecto
    if (gamepads_.empty()) {
        return nullptr;
    }

    // Buscar por nombre
    for (const auto& gamepad : gamepads_) {
        if (gamepad && gamepad->name == gamepad_name) {
            return gamepad;
        }
    }

    // Si no se encuentra por nombre, devolver el primer gamepad válido
    for (const auto& gamepad : gamepads_) {
        if (gamepad) {
            return gamepad;
        }
    }

    // Si llegamos aquí, no hay gamepads válidos
    return nullptr;
}