#include "game/ui/console.hpp"

#include <SDL3/SDL.h>

#include <algorithm>  // Para ranges::transform
#include <cctype>     // Para toupper
#include <sstream>    // Para std::istringstream
#include <string>     // Para string
#include <vector>     // Para vector

#include "core/input/key_config.hpp"          // Para KeyConfig
#include "core/rendering/screen.hpp"          // Para Screen
#include "core/rendering/sprite/sprite.hpp"   // Para Sprite
#include "core/rendering/surface.hpp"         // Para Surface
#include "core/rendering/text.hpp"            // Para Text
#include "core/resources/resource_cache.hpp"  // Para Resource
#include "game/options.hpp"                   // Para Options
#include "game/ui/notifier.hpp"               // Para Notifier
#include "utils/easing_functions.hpp"         // Para Easing::cubicInOut

// ── Helpers de texto ──────────────────────────────────────────────────────────

// Convierte la entrada a uppercase y la divide en tokens por espacios
static auto parseTokens(const std::string& input) -> std::vector<std::string> {
    std::vector<std::string> tokens;
    std::string token;
    for (unsigned char c : input) {
        if (c == ' ') {
            if (!token.empty()) {
                tokens.push_back(token);
                token.clear();
            }
        } else {
            token += static_cast<char>(std::toupper(c));
        }
    }
    if (!token.empty()) {
        tokens.push_back(token);
    }
    return tokens;
}

// ── Helpers ───────────────────────────────────────────────────────────────────

// Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input)
static auto calcTargetHeight(int num_msg_lines) -> float {
    constexpr int PADDING_IN_V = Console::TEXT_SIZE / 2;
    return static_cast<float>((Console::LINE_HEIGHT * (num_msg_lines + 1)) + (PADDING_IN_V * 2));
}

// Divide text en líneas respetando los \n existentes y haciendo word-wrap por ancho en píxeles
auto Console::wrapText(const std::string& text) const -> std::vector<std::string> {
    constexpr int PADDING_IN_H = TEXT_SIZE;  // Simétrico a ambos lados
    const int MAX_PX = static_cast<int>(Options::game.width) - (2 * PADDING_IN_H);

    std::vector<std::string> result;
    std::istringstream segment_stream(text);
    std::string segment;

    while (std::getline(segment_stream, segment)) {
        if (segment.empty()) {
            result.emplace_back();
            continue;
        }
        std::string current_line;
        std::istringstream word_stream(segment);
        std::string word;
        while (word_stream >> word) {
            std::string test;
            if (current_line.empty()) {
                test = word;
            } else {
                test = current_line;
                test += ' ';
                test += word;
            }
            if (text_->length(test) <= MAX_PX) {
                current_line = test;
            } else {
                if (!current_line.empty()) { result.push_back(current_line); }
                current_line = word;
            }
        }
        if (!current_line.empty()) { result.push_back(current_line); }
    }

    if (result.empty()) { result.emplace_back(); }
    return result;
}

// ── Singleton ─────────────────────────────────────────────────────────────────

// [SINGLETON]
Console* Console::console = nullptr;

// [SINGLETON]
void Console::init(const std::string& font_name) {
    Console::console = new Console(font_name);
}

// [SINGLETON]
void Console::destroy() {
    delete Console::console;
    Console::console = nullptr;
}

// [SINGLETON]
auto Console::get() -> Console* {
    return Console::console;
}

// Constructor
Console::Console(const std::string& font_name)
    : text_(Resource::Cache::get()->getText(font_name)) {
    msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
    height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
    target_height_ = height_;
    y_ = -height_;

    // Cargar comandos desde YAML
    registry_.load("data/console/commands.yaml");

    buildSurface();
}

// Crea la Surface con el aspecto visual de la consola
void Console::buildSurface() {
    const float WIDTH = Options::game.width;

    surface_ = std::make_shared<Surface>(WIDTH, height_);

    // Posición inicial (fuera de pantalla por arriba)
    SDL_FRect sprite_rect = {.x = 0, .y = y_, .w = WIDTH, .h = height_};
    sprite_ = std::make_shared<Sprite>(surface_, sprite_rect);

    // Dibujo inicial del texto
    redrawText();
}

// Redibuja el texto dinámico sobre la surface (fondo + borde + líneas)
void Console::redrawText() {
    const float WIDTH = Options::game.width;
    constexpr int PADDING_IN_H = TEXT_SIZE;
    constexpr int PADDING_IN_V = TEXT_SIZE / 2;
    constexpr Uint8 TRANSPARENT_SENTINEL = 255;  // Coincide con Surface::transparent_color_ por defecto

    auto previous_renderer = Screen::get()->getRendererSurface();
    Screen::get()->setRendererSurface(surface_);

    // Colores leídos de Options (modificables en caliente con el comando CONSOLE)
    const Uint8 BG_COLOR = Options::console.transparent
        ? TRANSPARENT_SENTINEL
        : static_cast<Uint8>(Options::console.bg_color);
    const auto MSG_COLOR = static_cast<Uint8>(Options::console.msg_color);
    const auto PROMPT_COLOR = static_cast<Uint8>(Options::console.prompt_color);
    const auto COMMAND_COLOR = static_cast<Uint8>(Options::console.command_color);

    // Fondo y borde
    surface_->clear(BG_COLOR);
    SDL_FRect rect = {.x = 0, .y = 0, .w = WIDTH, .h = height_};
    surface_->drawRectBorder(&rect, PROMPT_COLOR);

    // La surface de la fuente tiene transparent_color_=255 por defecto,
    // pero sus píxeles de fondo son color 0. Cambiarlo temporalmente a 0
    // para que no pinte el fondo negro de los glyphs sobre la consola.
    auto* font_surface = text_->getSprite()->getSurface().get();
    const Uint8 PREV_TRANSPARENT = font_surface->getTransparentColor();
    font_surface->setTransparentColor(0);

    // Líneas de mensaje con efecto typewriter (solo muestra los primeros typewriter_chars_)
    int y_pos = PADDING_IN_V;
    int remaining = typewriter_chars_;
    for (const auto& line : msg_lines_) {
        if (remaining <= 0) { break; }
        const int VISIBLE = std::min(remaining, static_cast<int>(line.size()));
        text_->writeColored(PADDING_IN_H, y_pos, line.substr(0, VISIBLE), MSG_COLOR);
        remaining -= VISIBLE;
        y_pos += LINE_HEIGHT;
    }

    // Línea de input (siempre la última): prompt en PROMPT_COLOR, comando + cursor en COMMAND_COLOR
    const bool SHOW_CURSOR = cursor_visible_ && (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS);
    text_->writeColored(PADDING_IN_H, y_pos, prompt_, PROMPT_COLOR);
    const int PROMPT_PX = text_->length(prompt_);
    const std::string CMD_STR = input_line_ + (SHOW_CURSOR ? "_" : "");
    text_->writeColored(PADDING_IN_H + PROMPT_PX, y_pos, CMD_STR, COMMAND_COLOR);

    // Restaurar transparent_color_ original de la fuente
    font_surface->setTransparentColor(PREV_TRANSPARENT);

    Screen::get()->setRendererSurface(previous_renderer);
}

// Actualiza la animación de la consola
void Console::update(float delta_time) {  // NOLINT(readability-function-cognitive-complexity)
    if (status_ == Status::HIDDEN) {
        return;
    }

    // Parpadeo del cursor (solo cuando activa)
    if (status_ == Status::ACTIVE) {
        cursor_timer_ += delta_time;
        const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
        if (cursor_timer_ >= THRESHOLD) {
            cursor_timer_ = 0.0F;
            cursor_visible_ = !cursor_visible_;
        }
    }

    // Efecto typewriter: revelar letras una a una (solo cuando ACTIVE)
    if (status_ == Status::ACTIVE) {
        int total_chars = 0;
        for (const auto& line : msg_lines_) { total_chars += static_cast<int>(line.size()); }
        if (typewriter_chars_ < total_chars) {
            typewriter_timer_ += delta_time;
            while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < total_chars) {
                typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
                ++typewriter_chars_;
            }
        }
    }

    // Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
    if (status_ == Status::ACTIVE && height_ != target_height_) {
        if (anim_progress_ == 0.0F) {
            // Iniciar animación de resize
            anim_start_ = height_;
            anim_end_ = target_height_;
        }
        anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
        const float PREV_HEIGHT = height_;
        height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
        if (anim_progress_ >= 1.0F) {
            height_ = target_height_;
            anim_progress_ = 0.0F;
        }
        // Actualizar el Notifier incrementalmente con el delta de altura
        if (Notifier::get() != nullptr) {
            const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
            if (DELTA_PX > 0) {
                Notifier::get()->addYOffset(DELTA_PX);
                notifier_offset_applied_ += DELTA_PX;
            } else if (DELTA_PX < 0) {
                Notifier::get()->removeYOffset(-DELTA_PX);
                notifier_offset_applied_ += DELTA_PX;
            }
        }
        // Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
        const float WIDTH = Options::game.width;
        surface_ = std::make_shared<Surface>(WIDTH, height_);
        sprite_->setSurface(surface_);
    }

    // Redibujar texto cada frame
    redrawText();

    // Animación de apertura/cierre (basada en tiempo)
    if (status_ == Status::RISING || status_ == Status::VANISHING) {
        anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
        y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));

        if (anim_progress_ >= 1.0F) {
            y_ = anim_end_;
            anim_progress_ = 0.0F;
            if (status_ == Status::RISING) {
                status_ = Status::ACTIVE;
            } else {
                status_ = Status::HIDDEN;
                msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
                target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
            }
        }
    }

    SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
    sprite_->setPosition(rect);
    sprite_->setClip({.x = 0.0F, .y = 0.0F, .w = Options::game.width, .h = height_});
}

// Renderiza la consola
void Console::render() {
    if (status_ == Status::HIDDEN) {
        return;
    }
    sprite_->render();
}

// Activa o desactiva la consola
void Console::toggle() {
    switch (status_) {
        case Status::HIDDEN:
            // Al abrir: la consola siempre empieza con 1 línea de mensaje (altura base)
            target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
            height_ = target_height_;
            y_ = -height_;
            anim_start_ = y_;
            anim_end_ = 0.0F;
            anim_progress_ = 0.0F;
            status_ = Status::RISING;
            input_line_.clear();
            cursor_timer_ = 0.0F;
            cursor_visible_ = true;
            // El mensaje inicial (nombre de la consola) aparece completo, sin typewriter
            typewriter_chars_ = static_cast<int>(msg_lines_[0].size());
            typewriter_timer_ = 0.0F;
            SDL_StartTextInput(SDL_GetKeyboardFocus());
            if (Notifier::get() != nullptr) {
                const int OFFSET = static_cast<int>(height_);
                Notifier::get()->addYOffset(OFFSET);
                notifier_offset_applied_ = OFFSET;
            }
            if (on_toggle) { on_toggle(true); }
            break;
        case Status::ACTIVE:
            // Al cerrar: mantener el texto visible hasta que esté completamente oculta
            anim_start_ = y_;
            anim_end_ = -height_;
            anim_progress_ = 0.0F;
            status_ = Status::VANISHING;
            target_height_ = height_;  // No animar altura durante VANISHING
            history_index_ = -1;
            saved_input_.clear();
            SDL_StopTextInput(SDL_GetKeyboardFocus());
            if (Notifier::get() != nullptr) {
                Notifier::get()->removeYOffset(notifier_offset_applied_);
                notifier_offset_applied_ = 0;
            }
            if (on_toggle) { on_toggle(false); }
            break;
        default:
            // Durante RISING o VANISHING no se hace nada
            break;
    }
}

// Procesa el evento SDL: entrada de texto, Backspace, Enter
void Console::handleEvent(const SDL_Event& event) {  // NOLINT(readability-function-cognitive-complexity)
    if (status_ != Status::ACTIVE) { return; }

    if (event.type == SDL_EVENT_TEXT_INPUT) {
        // Filtrar caracteres de control (tab, newline, etc.)
        if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
        // Ignorar texto si la tecla toggle está pulsada (evita escribir su carácter)
        if (KeyConfig::get() != nullptr) {
            SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console");
            SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr);
            if (toggle_sc != SDL_SCANCODE_UNKNOWN) {
                const bool* ks = SDL_GetKeyboardState(nullptr);
                if (ks[toggle_sc]) { return; }
            }
        }
        if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
            input_line_ += event.text.text;
        }
        tab_matches_.clear();
        return;
    }

    if (event.type == SDL_EVENT_KEY_DOWN) {
        switch (event.key.scancode) {
            case SDL_SCANCODE_BACKSPACE:
                tab_matches_.clear();
                if (!input_line_.empty()) { input_line_.pop_back(); }
                break;
            case SDL_SCANCODE_RETURN:
            case SDL_SCANCODE_KP_ENTER:
                processCommand();
                break;
            case SDL_SCANCODE_UP:
                // Navegar hacia atrás en el historial
                tab_matches_.clear();
                if (history_index_ < static_cast<int>(history_.size()) - 1) {
                    if (history_index_ == -1) { saved_input_ = input_line_; }
                    ++history_index_;
                    input_line_ = history_[static_cast<size_t>(history_index_)];
                }
                break;
            case SDL_SCANCODE_DOWN:
                // Navegar hacia el presente en el historial
                tab_matches_.clear();
                if (history_index_ >= 0) {
                    --history_index_;
                    input_line_ = (history_index_ == -1)
                        ? saved_input_
                        : history_[static_cast<size_t>(history_index_)];
                }
                break;
            case SDL_SCANCODE_TAB: {
                if (tab_matches_.empty()) {
                    // Calcular el input actual en mayúsculas
                    std::string upper;
                    for (unsigned char c : input_line_) { upper += static_cast<char>(std::toupper(c)); }

                    const size_t SPACE_POS = upper.rfind(' ');
                    if (SPACE_POS == std::string::npos) {
                        // Modo comando: ciclar keywords visibles que empiecen por el prefijo
                        for (const auto& kw : registry_.getVisibleKeywords()) {
                            if (upper.empty() || kw.starts_with(upper)) {
                                tab_matches_.emplace_back(kw);
                            }
                        }
                    } else {
                        const std::string BASE_CMD = upper.substr(0, SPACE_POS);
                        const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1);
                        const auto OPTS = registry_.getCompletions(BASE_CMD);
                        for (const auto& arg : OPTS) {
                            if (SUB_PREFIX.empty() || std::string_view{arg}.starts_with(SUB_PREFIX)) {
                                std::string match = BASE_CMD;
                                match += ' ';
                                match += arg;
                                tab_matches_.emplace_back(std::move(match));
                            }
                        }
                    }
                    tab_index_ = -1;
                }
                if (tab_matches_.empty()) { break; }
                tab_index_ = (tab_index_ + 1) % static_cast<int>(tab_matches_.size());
                std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
                for (char& c : result) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); }
                input_line_ = result;
                break;
            }
            default:
                break;
        }
    }
}

// Ejecuta el comando introducido y reinicia la línea de input
void Console::processCommand() {
    if (!input_line_.empty()) {
        // Añadir al historial (sin duplicados consecutivos)
        if (history_.empty() || history_.front() != input_line_) {
            history_.push_front(input_line_);
            if (static_cast<int>(history_.size()) > MAX_HISTORY_SIZE) {
                history_.pop_back();
            }
        }

        const auto TOKENS = parseTokens(input_line_);
        if (!TOKENS.empty()) {
            const std::string& cmd = TOKENS[0];
            const std::vector<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end());
            std::string result;
            bool instant = false;

            const auto* def = registry_.findCommand(cmd);
            if (def != nullptr) {
                result = registry_.execute(cmd, ARGS);
                instant = def->instant;
            } else {
                std::string cmd_lower = cmd;
                std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower);
                result = "Unknown: " + cmd_lower;
            }

            // Word-wrap automático según el ancho disponible en píxeles
            msg_lines_ = wrapText(result);

            // Actualizar la altura objetivo para animar el resize
            target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));

            // Typewriter: instantáneo si el comando lo requiere, letra a letra si no
            if (instant) {
                int total = 0;
                for (const auto& l : msg_lines_) { total += static_cast<int>(l.size()); }
                typewriter_chars_ = total;
            } else {
                typewriter_chars_ = 0;
            }
            typewriter_timer_ = 0.0F;
        }
    }
    input_line_.clear();
    history_index_ = -1;
    saved_input_.clear();
    tab_matches_.clear();
    cursor_timer_ = 0.0F;
    cursor_visible_ = true;
}

// Indica si la consola está activa (visible o en animación)
auto Console::isActive() -> bool {
    return status_ != Status::HIDDEN;
}

// Devuelve los píxeles visibles de la consola (sincronizado con la animación)
auto Console::getVisibleHeight() -> int {
    if (status_ == Status::HIDDEN) { return 0; }
    return static_cast<int>(y_ + height_);
}

// Scope de comandos
void Console::setScope(const std::string& scope) { registry_.setScope(scope); }
auto Console::getScope() const -> std::string { return registry_.getScope(); }
