#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

// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
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 || !*name) 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,"
        "lefttrigger:b6,righttrigger:b7,"
        "back:b8,start:b9,"
        "leftstick:b10,rightstick:b11,"
        "dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
        "guide:b16,"
        "leftx:a0,lefty:a1,rightx:a2,righty:a3,"
        "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, const std::string& gamepad_configs_file) {
    Input::instance = new Input(game_controller_db_path, gamepad_configs_file);
}

// 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, std::string gamepad_configs_file)
    : gamepad_mappings_file_(std::move(game_controller_db_path)),
      gamepad_configs_file_(std::move(gamepad_configs_file)) {
    // Inicializa el subsistema SDL_INIT_GAMEPAD
    initSDLGamePad();
}

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

// 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;
        }
    }

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

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

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

    // --- Comprobación del Mando ---
    // Comprobamos si hay mandos y si el índice solicitado es válido.
    if (gamepad != nullptr) {
        // Iteramos sobre todas las acciones, no sobre el número de mandos.
        for (const auto& pair : 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);
}

// Convierte un InputAction a std::string
auto Input::inputToString(Action action) -> std::string {
    switch (action) {
        case Action::FIRE_LEFT:
            return "input_fire_left";
        case Action::FIRE_CENTER:
            return "input_fire_center";
        case Action::FIRE_RIGHT:
            return "input_fire_right";
        case Action::START:
            return "input_start";
        case Action::SERVICE:
            return "input_service";
        default:
            return "";
    }
}

// Convierte un std::string a InputAction
auto Input::stringToInput(const std::string& name) -> Action {
    static const std::unordered_map<std::string, Action> INPUT_MAP = {
        {"input_fire_left", Action::FIRE_LEFT},
        {"input_fire_center", Action::FIRE_CENTER},
        {"input_fire_right", Action::FIRE_RIGHT},
        {"input_start", Action::START},
        {"input_service", Action::SERVICE}};

    auto it = INPUT_MAP.find(name);
    return it != INPUT_MAP.end() ? it->second : Action::NONE;
}

// Comprueba el eje del mando
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
    // Umbral para considerar el eje como activo
    bool axis_active_now = false;

    switch (action) {
        case Action::LEFT:
            axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
            break;
        case Action::RIGHT:
            axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
            break;
        case Action::UP:
            axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTY) < -AXIS_THRESHOLD;
            break;
        case Action::DOWN:
            axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTY) > AXIS_THRESHOLD;
            break;
        default:
            return false;
    }

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

    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::initSDLGamePad() {
    if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
        if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
            std::cout << "SDL_GAMEPAD could not initialize! SDL Error: " << SDL_GetError() << '\n';
        } else {
            addGamepadMappingsFromFile();
            loadGamepadConfigs();
            // Los mandos ya conectados llegan como SDL_EVENT_GAMEPAD_ADDED en el
            // primer pase del pump de eventos (antes del primer SDL_AppIterate),
            // por lo que no hace falta enumerarlos aquí a mano.
        }
    }
}

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 (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);
    }
    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';
    applyGamepadConfig(gamepad);
    saveGamepadConfigFromGamepad(gamepad);
    gamepads_.push_back(std::move(gamepad));
    return name + " CONNECTED";
}

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 + " DISCONNECTED";
    }
    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';
    }
}

void Input::loadGamepadConfigs() {
    if (GamepadConfigManager::fileExists(gamepad_configs_file_)) {
        GamepadConfigManager::readFromJson(gamepad_configs_, gamepad_configs_file_);
    }
}

void Input::saveGamepadConfigs() {
    GamepadConfigManager::writeToJson(gamepad_configs_, gamepad_configs_file_);
}

void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
    if (!gamepad || gamepad->path.empty()) {  // No podemos aplicar config sin una ruta
        return;
    }

    // --- Buscar configuración por RUTA (path) ---
    auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad](const GamepadConfig& config) -> bool {
        return config.path == gamepad->path;
    });

    // Fallback por nombre: si el mismo dispositivo se enchufa a otro puerto, su
    // path cambia pero el nombre suele mantenerse. Recuperamos su configuración
    // y actualizamos el path guardado al actual. Solo se acepta un match cuyo
    // path guardado NO corresponda a otro mando ya conectado, para no arruinar
    // setups con varios mandos idénticos en puertos distintos (cabinet arcade).
    if (config_it == gamepad_configs_.end() && !gamepad->name.empty()) {
        auto is_path_active = [this](const std::string& query_path) -> bool {
            return std::ranges::any_of(gamepads_, [&query_path](const std::shared_ptr<Gamepad>& g) -> bool {
                return g && g->path == query_path;
            });
        };
        config_it = std::ranges::find_if(gamepad_configs_, [&gamepad, &is_path_active](const GamepadConfig& config) -> bool {
            return config.name == gamepad->name && !is_path_active(config.path);
        });
        if (config_it != gamepad_configs_.end()) {
            std::cout << "Gamepad '" << gamepad->name << "' found by name, refreshing path to: " << gamepad->path << '\n';
            config_it->path = gamepad->path;
            saveGamepadConfigs();
        }
    }

    if (config_it != gamepad_configs_.end()) {
        // Se encontró una configuración específica para este puerto/dispositivo. La aplicamos.
        std::cout << "Applying custom config for gamepad at path: " << gamepad->path << '\n';
        for (const auto& [action, button] : config_it->bindings) {
            if (gamepad->bindings.contains(action)) {
                gamepad->bindings[action].button = button;
            }
        }
    }
}

void Input::saveGamepadConfigFromGamepad(std::shared_ptr<Gamepad> gamepad) {
    if (!gamepad || gamepad->path.empty()) {  // No podemos guardar una config sin una ruta
        return;
    }

    // --- CAMBIO CLAVE: Buscar si ya existe una configuración por RUTA (path) ---
    auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad](const GamepadConfig& config) -> bool {
        return config.path == gamepad->path;
    });

    // Crear nueva configuración desde el gamepad, incluyendo nombre y ruta
    GamepadConfig new_config(gamepad->name, gamepad->path);  // <--- CAMBIO: Pasamos ambos
    new_config.bindings.clear();

    // Copiar todos los bindings actuales del gamepad
    for (const auto& [action, buttonState] : gamepad->bindings) {
        new_config.bindings[action] = static_cast<SDL_GamepadButton>(buttonState.button);
    }

    if (config_it != gamepad_configs_.end()) {
        // Sobreescribir configuración existente para esta ruta
        *config_it = new_config;
    } else {
        // Añadir nueva configuración
        gamepad_configs_.push_back(new_config);
    }

    // Guardar cambios inmediatamente
    saveGamepadConfigs();
}

// Método para establecer el archivo de configuración (opcional)
void Input::setGamepadConfigsFile(const std::string& filename) {
    gamepad_configs_file_ = filename;
    loadGamepadConfigs();  // Recargar con el nuevo archivo
}

// Método para obtener configuración de un gamepad específico (opcional)
auto Input::getGamepadConfig(const std::string& gamepad_name) -> GamepadConfig* {
    auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad_name](const GamepadConfig& config) -> bool {
        return config.name == gamepad_name;
    });

    return (config_it != gamepad_configs_.end()) ? &(*config_it) : nullptr;
}

// Método para eliminar configuración de gamepad (opcional)
auto Input::removeGamepadConfig(const std::string& gamepad_name) -> bool {
    auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad_name](const GamepadConfig& config) -> bool {
        return config.name == gamepad_name;
    });

    if (config_it != gamepad_configs_.end()) {
        gamepad_configs_.erase(config_it);
        saveGamepadConfigs();
        return true;
    }

    return false;
}

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;
}