#include "options.hpp"

#include <SDL3/SDL.h>  // Para SDL_ScaleMode, SDL_LogCategory, SDL_LogError, SDL_LogInfo, SDL_LogWarn

#include <algorithm>   // Para clamp, __any_of_fn, any_of
#include <cstddef>     // Para size_t
#include <fstream>     // Para basic_ostream, operator<<, basic_ofstream, basic_ostream::operator<<, basic_istream, basic_ifstream, ifstream, istringstream, ofstream
#include <functional>  // Para function
#include <map>         // Para map, operator==, _Rb_tree_const_iterator
#include <sstream>     // Para basic_istringstream
#include <stdexcept>   // Para invalid_argument, out_of_range
#include <string>      // Para char_traits, basic_string, stoi, string, operator<<, operator+, operator==, operator>>, getline, operator<=>, to_string
#include <utility>     // Para pair
#include <vector>      // Para vector

#include "difficulty.hpp"  // Para Code, init
#include "input.hpp"       // Para Input
#include "lang.hpp"        // Para getText, Code
#include "ui/logger.hpp"   // Para info
#include "utils.hpp"       // Para stringToBool, boolToString, getFileName

namespace Options {
// --- Variables globales ---
Window window;                   // Opciones de la ventana
Settings settings;               // Opciones del juego
Video video;                     // Opciones de vídeo
Audio audio;                     // Opciones de audio
GamepadManager gamepad_manager;  // Opciones de mando para cada jugador
Keyboard keyboard;               // Opciones para el teclado
PendingChanges pending_changes;  // Opciones que se aplican al cerrar

// Declaraciones
auto set(const std::string& var, const std::string& value) -> bool;

// Establece el fichero de configuración
void setConfigFile(const std::string& file_path) { settings.config_file = file_path; };

// Establece el fichero de configuración de mandos
void setControllersFile(const std::string& file_path) { settings.controllers_file = file_path; };

// Inicializa las opciones del programa
void init() {
    // Dificultades
    Difficulty::init();

    // Opciones de control
    gamepad_manager.init();
    setKeyboardToPlayer(Player::Id::PLAYER1);

    // Opciones de cambios pendientes
    pending_changes.new_language = settings.language;
    pending_changes.new_difficulty = settings.difficulty;
    pending_changes.has_pending_changes = false;
}

// Carga el fichero de configuración
auto loadFromFile() -> bool {
    // 1. Inicializa las opciones con valores por defecto.
    init();

    std::ifstream file(settings.config_file);
    bool file_exists = file.is_open();  // Guardamos si el fichero existía al abrirlo

    // 2. Si el fichero existe, lo leemos para obtener los nombres de los mandos.
    if (file_exists) {
        // --- CASO: EL FICHERO EXISTE ---
        // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\nReading file: %s", getFileName(settings.config_file).c_str());
        Logger::info("Reading file: " + getFileName(settings.config_file));
        std::string line;
        std::string param_name;
        std::string param_value;

        while (std::getline(file, line)) {
            // Elimina comentarios
            auto comment_pos = line.find('#');
            if (comment_pos != std::string::npos) {
                line.resize(comment_pos);
            }

            // Si la línea contiene '=', lo reemplazamos por un espacio para compatibilidad
            auto equals_pos = line.find('=');
            if (equals_pos != std::string::npos) {
                line[equals_pos] = ' ';
            }

            // Usa un stream para separar palabras (elimina automáticamente espacios extra)
            std::istringstream iss(line);
            if (iss >> param_name >> param_value) {
                if (!set(param_name, param_value)) {
                    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unknown parameter: %s", param_name.c_str());
                }
            }
        }
        file.close();
    }

    // 3. Llamamos al asignador inteligente.
    gamepad_manager.assignAndLinkGamepads();

    // 4. Si el fichero no existía, lo creamos ahora con la configuración por defecto.
    if (!file_exists) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Config file not found. Creating default settings.");
        saveToFile();
    }

    return true;
}

// Guarda el fichero de configuración
auto saveToFile() -> bool {
    std::ofstream file(settings.config_file);

    if (!file.good()) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: %s can't be opened", getFileName(settings.config_file).c_str());
        return false;
    }

    // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Writing file: %s", getFileName(settings.config_file).c_str());
    Logger::info("Writing file: " + getFileName(settings.config_file));

    applyPendingChanges();

    // Versión del archivo
    file << "# Coffee Crisis Arcade Edition - Configuration File\n";
    file << "# Format: key value\n";
    file << "config.version " << settings.config_version << "\n";

    // Opciones de ventana
    file << "\n# WINDOW\n";
    file << "window.zoom " << window.zoom << "\n";

    // Opciones de video
    file << "\n# VIDEO\n";
    file << "# video.scale_mode [" << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": linear]\n";
    file << "video.fullscreen      " << boolToString(video.fullscreen) << "\n";
    file << "video.scale_mode      " << static_cast<int>(video.scale_mode) << "\n";
    file << "video.vsync           " << boolToString(video.vsync) << "\n";
    file << "video.integer_scale   " << boolToString(video.integer_scale) << "\n";
    file << "video.shaders         " << boolToString(video.shaders) << "\n";

    // Opciones de audio
    file << "\n# AUDIO\n";
    file << "# volume range: [0 .. 100]\n";
    file << "audio.enabled         " << boolToString(audio.enabled) << "\n";
    file << "audio.volume          " << audio.volume << "\n";
    file << "audio.music.enabled   " << boolToString(audio.music.enabled) << "\n";
    file << "audio.music.volume    " << audio.music.volume << "\n";
    file << "audio.sound.enabled   " << boolToString(audio.sound.enabled) << "\n";
    file << "audio.sound.volume    " << audio.sound.volume << "\n";

    // Opciones del juego
    file << "\n# GAME\n";
    file << "# game.language [0: spanish, 1: valencian, 2: english]\n";
    file << "# game.difficulty [" << static_cast<int>(Difficulty::Code::EASY) << ": easy, " << static_cast<int>(Difficulty::Code::NORMAL) << ": normal, " << static_cast<int>(Difficulty::Code::HARD) << ": hard]\n";
    file << "game.language         " << static_cast<int>(settings.language) << "\n";
    file << "game.difficulty       " << static_cast<int>(settings.difficulty) << "\n";
    file << "game.autofire         " << boolToString(settings.autofire) << "\n";
    file << "game.shutdown_enabled " << boolToString(settings.shutdown_enabled) << "\n";
    file << "game.params_file      " << settings.params_file << "\n";

    // Opciones de mandos
    file << "\n# CONTROLLERS\n";
    gamepad_manager.saveToFile(file);

    // Opciones de teclado
    file << "\n# KEYBOARD\n";
    file << "keyboard.player " << static_cast<int>(keyboard.player_id) << "\n";

    // Cierra el fichero
    file.close();

    return true;
}

// Función auxiliar para analizar la configuración del mando y reducir duplicación
void parseAndSetController(const std::string& var, const std::string& value) {
    size_t first_dot = var.find('.');
    size_t second_dot = var.find('.', first_dot + 1);

    if (first_dot == std::string::npos || second_dot == std::string::npos) {
        return;  // Formato inválido
    }

    try {
        int controller_index = std::stoi(var.substr(first_dot + 1, second_dot - first_dot - 1));
        std::string setting_key = var.substr(second_dot + 1);

        gamepad_manager.setControllerProperty(controller_index, setting_key, value);
    } catch (const std::exception&) {
        // Error en parsing
    }
}

auto set(const std::string& var, const std::string& value) -> bool {
    // Clausula de protección: ignora líneas vacías o comentarios
    if (var.empty() || var.starts_with("#")) {
        return true;
    }

    // Un mapa estático asegura que se inicializa solo una vez
    static const std::map<std::string, std::function<void(const std::string&)>> SETTINGS_MAP = {
        // Configuración
        {"config.version", [](const auto& val) { settings.config_version = std::stoi(val); }},
        // Ventana
        {"window.zoom", [](const auto& val) { window.zoom = std::stoi(val); }},
        // Vídeo
        {"video.fullscreen", [](const auto& val) { video.fullscreen = stringToBool(val); }},
        {"video.scale_mode", [](const auto& val) { video.scale_mode = static_cast<SDL_ScaleMode>(std::stoi(val)); }},
        {"video.shaders", [](const auto& val) { video.shaders = stringToBool(val); }},
        {"video.integer_scale", [](const auto& val) { video.integer_scale = stringToBool(val); }},
        {"video.vsync", [](const auto& val) { video.vsync = stringToBool(val); }},
        // Audio
        {"audio.enabled", [](const auto& val) { audio.enabled = stringToBool(val); }},
        {"audio.volume", [](const auto& val) { audio.volume = std::clamp(std::stoi(val), 0, 100); }},
        {"audio.music.enabled", [](const auto& val) { audio.music.enabled = stringToBool(val); }},
        {"audio.music.volume", [](const auto& val) { audio.music.volume = std::clamp(std::stoi(val), 0, 100); }},
        {"audio.sound.enabled", [](const auto& val) { audio.sound.enabled = stringToBool(val); }},
        {"audio.sound.volume", [](const auto& val) { audio.sound.volume = std::clamp(std::stoi(val), 0, 100); }},
        // Juego
        {"game.language", [](const auto& val) {
             settings.language = static_cast<Lang::Code>(std::stoi(val));

             if (settings.language != Lang::Code::ENGLISH &&
                 settings.language != Lang::Code::VALENCIAN &&
                 settings.language != Lang::Code::SPANISH) {
                 settings.language = Lang::Code::ENGLISH;
             }
             pending_changes.new_language = settings.language;
         }},
        {"game.difficulty", [](const auto& val) {
             settings.difficulty = static_cast<Difficulty::Code>(std::stoi(val));
             pending_changes.new_difficulty = settings.difficulty;
         }},
        {"game.autofire", [](const auto& val) { settings.autofire = stringToBool(val); }},
        {"game.shutdown_enabled", [](const auto& val) { settings.shutdown_enabled = stringToBool(val); }},
        {"game.params_file", [](const auto& val) { settings.params_file = val; }},
        // Teclado
        {"keyboard.player", [](const auto& val) { keyboard.player_id = static_cast<Player::Id>(stoi(val)); }}};

    // Maneja por separado la configuración general de los mandos
    if (var.starts_with("controller.")) {
        try {
            parseAndSetController(var, value);
            return true;
        } catch (const std::out_of_range& e) {
            // Error: por ejemplo, índice de mando fuera de rango
            return false;
        } catch (const std::invalid_argument& e) {
            // Error: por ejemplo, fallo en std::stoi
            return false;
        }
    }

    // Busca el nombre de la variable en el mapa
    if (auto it = SETTINGS_MAP.find(var); it != SETTINGS_MAP.end()) {
        try {
            // Ejecuta la función lambda asociada
            it->second(value);
            return true;
        } catch (const std::invalid_argument& e) {
            // Maneja casos donde std::stoi falla por entrada inválida
            return false;
        }
    }

    // Si la clave no se encontró en el mapa ni en la lógica de mandos
    return false;
}

// Asigna el teclado al jugador
void setKeyboardToPlayer(Player::Id player_id) {
    keyboard.player_id = player_id;
}

// Intercambia el teclado de jugador
void swapKeyboard() {
    keyboard.player_id = keyboard.player_id == Player::Id::PLAYER1 ? Player::Id::PLAYER2 : Player::Id::PLAYER1;
}

// Intercambia los jugadores asignados a los dos primeros mandos
void swapControllers() {
    gamepad_manager.swapPlayers();
}

// Averigua quien está usando el teclado
auto getPlayerWhoUsesKeyboard() -> Player::Id {
    return keyboard.player_id;
}

// Aplica los cambios pendientes copiando los valores a sus variables
void applyPendingChanges() {
    if (pending_changes.has_pending_changes) {
        settings.language = pending_changes.new_language;
        settings.difficulty = pending_changes.new_difficulty;
        pending_changes.has_pending_changes = false;
    }
}

void checkPendingChanges() {
    pending_changes.has_pending_changes = settings.language != pending_changes.new_language ||
        settings.difficulty != pending_changes.new_difficulty;
}

// Buscar y asignar un mando disponible por nombre
auto assignGamepadByName(const std::string& gamepad_name_to_find, Player::Id player_id) -> bool {
    auto found_gamepad = Input::get()->findAvailableGamepadByName(gamepad_name_to_find);

    if (found_gamepad) {
        return gamepad_manager.assignGamepadToPlayer(player_id, found_gamepad, found_gamepad->name);
    }
    return false;
}

// Obtener información de un gamepad específico
auto getGamepadInfo(Player::Id player_id) -> std::string {
    try {
        const auto& gamepad = gamepad_manager.getGamepad(player_id);
        return "Player " + std::to_string(static_cast<int>(player_id)) +
            ": " + (gamepad.name.empty() ? "No gamepad" : gamepad.name);
    } catch (const std::exception&) {
        return "Invalid player";
    }
}

// Asigna los mandos físicos basándose en la configuración actual.
void GamepadManager::assignAndLinkGamepads() {
    // 1. Obtenemos los mandos físicos conectados.
    auto physical_gamepads = Input::get()->getGamepads();

    // 2. Reiniciamos las asignaciones actuales.
    std::array<std::string, MAX_PLAYERS> desired_paths;
    for (size_t i = 0; i < MAX_PLAYERS; ++i) {
        desired_paths[i] = gamepads_[i].path;
        gamepads_[i].instance = nullptr;
    }

    // 3. Vector para rastrear los mandos ya asignados.
    std::vector<std::shared_ptr<Input::Gamepad>> assigned_instances;

    // --- Ejecutamos las pasadas de asignación y limpieza ---
    // Pasada 1: Intenta asignar por la ruta guardada.
    assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances);

    // Pasada 2: Asigna los mandos restantes a los jugadores libres.
    assignRemainingGamepads(physical_gamepads, assigned_instances);

    // Pasada 3: Limpia los datos de los slots que se quedaron sin mando.
    clearUnassignedGamepadSlots();
}

// --- PRIMERA PASADA: Intenta asignar mandos basándose en la ruta guardada ---
void GamepadManager::assignGamepadsByPath(
    const std::array<std::string, MAX_PLAYERS>& desired_paths,
    const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
    std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
    for (size_t i = 0; i < MAX_PLAYERS; ++i) {
        const std::string& desired_path = desired_paths[i];
        if (desired_path.empty()) {
            continue;  // No hay ruta guardada para este slot.
        }

        // Buscamos un mando físico que coincida con la ruta y no esté ya asignado.
        for (const auto& physical_gamepad : physical_gamepads) {
            if (physical_gamepad->path == desired_path && !isGamepadAssigned(physical_gamepad, assigned_instances)) {
                // Asignamos y actualizamos TODOS los datos.
                gamepads_[i].instance = physical_gamepad;
                gamepads_[i].name = physical_gamepad->name;  // <--- LA LÍNEA QUE FALTABA
                // No es necesario actualizar la path aquí porque ya coincide.

                assigned_instances.push_back(physical_gamepad);
                break;  // Mando encontrado para este jugador, pasamos al siguiente.
            }
        }
    }
}

// --- SEGUNDA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
void GamepadManager::assignRemainingGamepads(
    const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
    std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
    for (size_t i = 0; i < MAX_PLAYERS; ++i) {
        if (gamepads_[i].instance != nullptr) {
            continue;  // Este jugador ya tiene un mando.
        }

        // Buscamos un mando físico que todavía esté libre.
        for (const auto& physical_gamepad : physical_gamepads) {
            if (!isGamepadAssigned(physical_gamepad, assigned_instances)) {
                gamepads_[i].instance = physical_gamepad;

                // MUY IMPORTANTE: Actualizamos la configuración para reflejar la realidad.
                gamepads_[i].name = physical_gamepad->name;
                gamepads_[i].path = physical_gamepad->path;

                assigned_instances.push_back(physical_gamepad);
                break;  // Mando encontrado, pasamos al siguiente jugador.
            }
        }
    }
}

// --- TERCERA PASADA: Limpia la información "fantasma" de los slots no asignados ---
void GamepadManager::clearUnassignedGamepadSlots() {
    // Recorremos los slots de jugador una última vez.
    for (auto& gamepad_config : gamepads_) {
        // Si un slot no tiene una instancia física enlazada (instance == nullptr),
        // significa que no hay un mando para él.
        if (gamepad_config.instance == nullptr) {
            // Limpiamos sus datos de configuración para no mostrar información
            // de un mando que ya no está conectado.
            gamepad_config.name = Lang::getText("[SERVICE_MENU] NO_CONTROLLER");
            gamepad_config.path = "";
        }
    }
}

// Función auxiliar para comprobar si un mando físico ya está en la lista de asignados.
// Devuelve 'true' si ya ha sido asignado, 'false' en caso contrario.
auto GamepadManager::isGamepadAssigned(
    const std::shared_ptr<Input::Gamepad>& physical_gamepad,
    const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool {
    return std::ranges::any_of(assigned_instances,
        [&physical_gamepad](const auto& assigned) {
            return assigned == physical_gamepad;
        });
}

// Convierte un player id a texto segun Lang
auto playerIdToString(Player::Id player_id) -> std::string {
    switch (player_id) {
        case Player::Id::PLAYER1:
            return Lang::getText("[SERVICE_MENU] PLAYER1");
        case Player::Id::PLAYER2:
            return Lang::getText("[SERVICE_MENU] PLAYER2");
        default:
            return "";
    }
}

// Convierte un texto a player id segun Lang
auto stringToPlayerId(const std::string& name) -> Player::Id {
    if (name == Lang::getText("[SERVICE_MENU] PLAYER1")) {
        return Player::Id::PLAYER1;
    }
    if (name == Lang::getText("[SERVICE_MENU] PLAYER2")) {
        return Player::Id::PLAYER2;
    }
    return Player::Id::NO_PLAYER;
}
}  // namespace Options