#include "game/scenes/title.hpp"

#include <SDL3/SDL.h>  // Para SDL_GetTicks, SDL_Event, SDL_Keycode, SDL_PollEvent, SDLK_A, SDLK_C, SDLK_D, SDLK_F, SDLK_S, SDLK_V, SDLK_X, SDLK_Z, SDL_EventType, Uint64

#include <ranges>  // Para __find_if_fn, find_if
#include <string>  // Para basic_string, char_traits, operator+, to_string, string
#include <vector>  // Para vector

#include "core/audio/audio.hpp"              // Para Audio
#include "core/input/global_inputs.hpp"      // Para check
#include "core/input/input.hpp"              // Para Input
#include "core/input/input_types.hpp"        // Para InputAction
#include "core/locale/lang.hpp"              // Para getText
#include "core/rendering/fade.hpp"           // Para Fade
#include "core/rendering/screen.hpp"         // Para Screen
#include "core/rendering/sprite/sprite.hpp"  // Para Sprite
#include "core/rendering/text.hpp"           // Para Text
#include "core/rendering/tiled_bg.hpp"       // Para TiledBG, TiledBGMode
#include "core/resources/resource.hpp"       // Para Resource
#include "core/system/global_events.hpp"     // Para handle
#include "core/system/section.hpp"           // Para Name, name, Options, options, AttractMode, attract_mode
#include "game/entities/player.hpp"          // Para Player
#include "game/gameplay/game_logo.hpp"       // Para GameLogo
#include "game/options.hpp"                  // Para Gamepad, GamepadManager, gamepad_manager, Settings, settings, Keyboard, keyboard, getPlayerWhoUsesKeyboard, swapControllers, swapKeyboard
#include "game/ui/notifier.hpp"              // Para Notifier
#include "game/ui/service_menu.hpp"          // Para ServiceMenu
#include "utils/color.hpp"                   // Para Color, NO_COLOR_MOD, TITLE_SHADOW_TEXT
#include "utils/param.hpp"                   // Para Param, param, ParamGame, ParamTitle, ParamFade
#include "utils/utils.hpp"                   // Para Zone, BLOCK

class Texture;

// Constructor
Title::Title()
    : text_(Resource::get()->getText("smb2_grad")),
      fade_(std::make_unique<Fade>()),
      tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::RANDOM)),
      game_logo_(std::make_unique<GameLogo>(param.game.game_area.center_x, param.title.title_c_c_position)),
      mini_logo_sprite_(std::make_unique<Sprite>(Resource::get()->getTexture("logo_jailgames_mini.png"))),
      state_(State::LOGO_ANIMATING),
      num_controllers_(Input::get()->getNumGamepads()) {
    // Configura objetos
    tiled_bg_->setColor(param.title.bg_color);
    tiled_bg_->setSpeed(0.0F);
    game_logo_->enable();
    mini_logo_sprite_->setX(param.game.game_area.center_x - (mini_logo_sprite_->getWidth() / 2));
    fade_->setColor(param.fade.color);
    fade_->setType(Fade::Type::RANDOM_SQUARE2);
    fade_->setPostDuration(param.fade.post_duration_ms);
    initPlayers();

    // Asigna valores a otras variables
    Section::options = Section::Options::TITLE_1;
    const bool IS_TITLE_TO_DEMO = (Section::attract_mode == Section::AttractMode::TITLE_TO_DEMO);
    next_section_ = IS_TITLE_TO_DEMO ? Section::Name::GAME_DEMO : Section::Name::LOGO;
    Section::attract_mode = IS_TITLE_TO_DEMO ? Section::AttractMode::TITLE_TO_LOGO : Section::AttractMode::TITLE_TO_DEMO;

    // Define los anclajes de los elementos
    anchor_.mini_logo = (param.game.height / MINI_LOGO_Y_DIVISOR * MINI_LOGO_Y_FACTOR) + BLOCK;
    mini_logo_sprite_->setY(anchor_.mini_logo);
    anchor_.copyright_text = anchor_.mini_logo + mini_logo_sprite_->getHeight() + COPYRIGHT_TEXT_SPACING;

    // Inicializa el timer de delta time para el primer frame del callback
    last_time_ = SDL_GetTicks();
}

// Destructor
Title::~Title() {
    Audio::get()->stopAllSounds();
    if (Section::name == Section::Name::LOGO) {
        Audio::get()->fadeOutMusic(MUSIC_FADE_OUT_SHORT_MS);
    }

    // Desregistra los jugadores de Options
    Options::keyboard.clearPlayers();
    Options::gamepad_manager.clearPlayers();
}

// Actualiza las variables del objeto
void Title::update(float delta_time) {
    static auto* const SCREEN = Screen::get();
    SCREEN->update(delta_time);  // Actualiza el objeto screen
    Audio::update();             // Actualiza el objeto audio

    updateFade();
    updateState(delta_time);
    updateStartPrompt(delta_time);
    updatePlayers(delta_time);
}

// Calcula el tiempo transcurrido desde el último frame
auto Title::calculateDeltaTime() -> float {
    const Uint64 CURRENT_TIME = SDL_GetTicks();
    const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F;  // Convert ms to seconds
    last_time_ = CURRENT_TIME;
    return DELTA_TIME;
}

// Dibuja el objeto en pantalla
void Title::render() {
    static auto* const SCREEN = Screen::get();

    SCREEN->start();
    SCREEN->clean();

    tiled_bg_->render();
    game_logo_->render();
    renderPlayers();
    renderStartPrompt();
    renderCopyright();
    fade_->render();

    SCREEN->render();
}

// Comprueba los eventos
void Title::checkEvents() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_EVENT_KEY_DOWN) {
            handleKeyDownEvent(event);
        }

        GlobalEvents::handle(event);
    }
}

void Title::handleKeyDownEvent(const SDL_Event& /*event*/) {
}

// Comprueba las entradas
void Title::checkInput() {
    Input::get()->update();

    if (!ServiceMenu::get()->isEnabled()) {
        processControllerInputs();
        processKeyboardStart();
    }

    GlobalInputs::check();
}

void Title::processKeyboardStart() {
    if (!canProcessStartButton()) {
        return;
    }

    const bool START_PRESSED = Input::get()->checkAction(
        Input::Action::START,
        Input::DO_NOT_ALLOW_REPEAT,
        Input::CHECK_KEYBOARD);

    if (START_PRESSED) {
        switch (Options::keyboard.player_id) {
            case Player::Id::PLAYER1:
                processPlayer1Start();
                break;
            case Player::Id::PLAYER2:
                processPlayer2Start();
                break;
            default:
                break;
        }
    }
}

void Title::processControllerInputs() {
    for (const auto& controller : Options::gamepad_manager) {
        if (isStartButtonPressed(&controller)) {
            handleStartButtonPress(&controller);
        }
    }
}

auto Title::isStartButtonPressed(const Options::Gamepad* controller) -> bool {
    return Input::get()->checkAction(
        Input::Action::START,
        Input::DO_NOT_ALLOW_REPEAT,
        Input::DO_NOT_CHECK_KEYBOARD,
        controller->instance);
}

void Title::handleStartButtonPress(const Options::Gamepad* controller) {
    if (!canProcessStartButton()) {
        return;
    }

    if (controller->player_id == Player::Id::PLAYER1) {
        processPlayer1Start();
    } else if (controller->player_id == Player::Id::PLAYER2) {
        processPlayer2Start();
    }
}

auto Title::canProcessStartButton() const -> bool {
    return (state_ != State::LOGO_ANIMATING || ALLOW_TITLE_ANIMATION_SKIP);
}

void Title::processPlayer1Start() {
    if (!player1_start_pressed_) {
        player1_start_pressed_ = true;
        activatePlayerAndSetState(Player::Id::PLAYER1);
    }
}

void Title::processPlayer2Start() {
    if (!player2_start_pressed_) {
        player2_start_pressed_ = true;
        activatePlayerAndSetState(Player::Id::PLAYER2);
    }
}

void Title::activatePlayerAndSetState(Player::Id player_id) {
    getPlayer(player_id)->setPlayingState(Player::State::TITLE_ANIMATION);
    setState(State::START_HAS_BEEN_PRESSED);
    counter_time_ = 0.0F;
}

// Avanza un frame (llamado desde Director::iterate)
void Title::iterate() {
    const float DELTA_TIME = calculateDeltaTime();
    checkInput();
    update(DELTA_TIME);
    render();
}

// Procesa un evento (llamado desde Director::handleEvent)
void Title::handleEvent(const SDL_Event& event) {
    if (event.type == SDL_EVENT_KEY_DOWN) {
        handleKeyDownEvent(event);
    }
}

// Bucle para el titulo del juego (fallback legacy)
void Title::run() {
    last_time_ = SDL_GetTicks();

    while (Section::name == Section::Name::TITLE) {
        const float DELTA_TIME = calculateDeltaTime();

        checkInput();
        update(DELTA_TIME);
        checkEvents();  // Tiene que ir antes del render
        render();
    }
}

// Reinicia el contador interno
void Title::resetCounter() { counter_time_ = 0.0F; }

// Intercambia la asignación de mandos a los jugadores
void Title::swapControllers() {
    if (Input::get()->getNumGamepads() == 0) {
        return;
    }

    Options::swapControllers();
    showControllers();
}

// Intercambia el teclado de jugador
void Title::swapKeyboard() {
    Options::swapKeyboard();
    std::string text = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Options::getPlayerWhoUsesKeyboard())) + ": " + Lang::getText("[DEFINE_BUTTONS] KEYBOARD");
    Notifier::get()->show({text});
}

// Muestra información sobre los controles y los jugadores
void Title::showControllers() {
    // Crea los textos
    std::string text1 = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Player::Id::PLAYER1)) + ": " + Options::gamepad_manager.getGamepad(Player::Id::PLAYER1).name;
    std::string text2 = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Player::Id::PLAYER2)) + ": " + Options::gamepad_manager.getGamepad(Player::Id::PLAYER2).name;

    // Muestra la notificación
    Notifier::get()->show({text1, text2});
}

// Actualiza el fade
void Title::updateFade() {
    fade_->update();
    if (fade_->hasEnded()) {
        const int COMBO = (player1_start_pressed_ ? 1 : 0) | (player2_start_pressed_ ? 2 : 0);

        switch (COMBO) {
            case 0:  // Ningún jugador ha pulsado Start
                Section::name = next_section_;
                break;

            case 1:  // Solo el jugador 1 ha pulsado Start
                Section::name = Section::Name::GAME;
                Section::options = Section::Options::GAME_PLAY_1P;
                Audio::get()->stopMusic();
                break;

            case 2:  // Solo el jugador 2 ha pulsado Start
                Section::name = Section::Name::GAME;
                Section::options = Section::Options::GAME_PLAY_2P;
                Audio::get()->stopMusic();
                break;

            case 3:  // Ambos jugadores han pulsado Start
                Section::name = Section::Name::GAME;
                Section::options = Section::Options::GAME_PLAY_BOTH;
                Audio::get()->stopMusic();
                break;
        }
    }
}

// Actualiza el estado
void Title::updateState(float delta_time) {
    game_logo_->update(delta_time);
    tiled_bg_->update(delta_time);

    // Establece la lógica según el estado
    switch (state_) {
        case State::LOGO_ANIMATING: {
            if (game_logo_->hasFinished()) {
                setState(State::LOGO_FINISHED);
            }
            break;
        }
        case State::LOGO_FINISHED: {
            counter_time_ += delta_time;

            if (counter_time_ >= param.title.title_duration) {
                // El menu ha hecho time out
                fade_->setPostDuration(0);
                fade_->activate();
                selection_ = Section::Options::TITLE_TIME_OUT;
            }
            break;
        }
        case State::START_HAS_BEEN_PRESSED: {
            counter_time_ += delta_time;

            if (counter_time_ >= START_PRESSED_DELAY_S) {
                fade_->activate();
            }
            break;
        }

        default:
            break;
    }
}

void Title::updateStartPrompt(float delta_time) {
    blink_accumulator_ += delta_time;

    bool condition_met = false;
    float period = 0.0F;
    float on_time = 0.0F;

    switch (state_) {
        case State::LOGO_FINISHED:
            period = LOGO_BLINK_PERIOD_S;
            on_time = LOGO_BLINK_ON_TIME_S;
            break;

        case State::START_HAS_BEEN_PRESSED:
            period = START_BLINK_PERIOD_S;
            on_time = START_BLINK_ON_TIME_S;
            break;

        default:
            break;
    }

    if (period > 0.0F) {
        // Reset accumulator when it exceeds the period
        if (blink_accumulator_ >= period) {
            blink_accumulator_ -= period;
        }

        // Check if we're in the "on" time of the blink cycle
        condition_met = blink_accumulator_ >= (period - on_time);
    }

    should_render_start_prompt_ = condition_met;
}

void Title::renderStartPrompt() {
    if (should_render_start_prompt_) {
        text_->writeDX(Text::CENTER | Text::SHADOW,
            param.game.game_area.center_x,
            param.title.press_start_position,
            Lang::getText("[TITLE] PRESS_BUTTON_TO_PLAY"),
            1,
            Colors::NO_COLOR_MOD,
            1,
            Colors::TITLE_SHADOW_TEXT);
    }
}

void Title::renderCopyright() {
    if (state_ != State::LOGO_ANIMATING) {
        // Mini logo
        mini_logo_sprite_->render();

        // Texto con el copyright
        text_->writeDX(Text::CENTER | Text::SHADOW,
            param.game.game_area.center_x,
            anchor_.copyright_text,
            std::string(TEXT_COPYRIGHT),
            1,
            Colors::NO_COLOR_MOD,
            1,
            Colors::TITLE_SHADOW_TEXT);
    }
}

// Cambia el estado
void Title::setState(State state) {
    if (state_ == state) {
        return;
    }

    state_ = state;
    switch (state_) {
        case State::LOGO_ANIMATING:
            break;
        case State::LOGO_FINISHED:
            Audio::get()->playMusic("title.ogg");
            tiled_bg_->changeSpeedTo(60.0F, 0.5F);
            blink_accumulator_ = 0.0F;  // Resetea el timer para empezar el parpadeo desde el inicio
            break;
        case State::START_HAS_BEEN_PRESSED:
            Audio::get()->fadeOutMusic(MUSIC_FADE_OUT_LONG_MS);
            blink_accumulator_ = 0.0F;  // Resetea el timer para empezar el parpadeo desde el inicio
            break;
    }
}

// Inicializa los jugadores
void Title::initPlayers() {
    std::vector<std::vector<std::shared_ptr<Texture>>> player_textures;  // Vector con todas las texturas de los jugadores;
    std::vector<std::vector<std::string>> player1_animations;            // Vector con las animaciones del jugador 1
    std::vector<std::vector<std::string>> player2_animations;            // Vector con las animaciones del jugador 2

    // Texturas - Player1
    std::vector<std::shared_ptr<Texture>> player1_textures;
    player1_textures.emplace_back(Resource::get()->getTexture("player1_pal0"));
    player1_textures.emplace_back(Resource::get()->getTexture("player1_pal1"));
    player1_textures.emplace_back(Resource::get()->getTexture("player1_pal2"));
    player1_textures.emplace_back(Resource::get()->getTexture("player1_pal3"));
    player1_textures.emplace_back(Resource::get()->getTexture("player1_power.png"));
    player_textures.push_back(player1_textures);

    // Texturas - Player2
    std::vector<std::shared_ptr<Texture>> player2_textures;
    player2_textures.emplace_back(Resource::get()->getTexture("player2_pal0"));
    player2_textures.emplace_back(Resource::get()->getTexture("player2_pal1"));
    player2_textures.emplace_back(Resource::get()->getTexture("player2_pal2"));
    player2_textures.emplace_back(Resource::get()->getTexture("player2_pal3"));
    player2_textures.emplace_back(Resource::get()->getTexture("player2_power.png"));
    player_textures.push_back(player2_textures);

    // Animaciones -- Jugador
    player1_animations.emplace_back(Resource::get()->getAnimation("player1.ani"));
    player1_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));
    player2_animations.emplace_back(Resource::get()->getAnimation("player2.ani"));
    player2_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));

    // Crea los dos jugadores
    const int Y = param.title.press_start_position - (Player::HEIGHT / 2);
    constexpr bool DEMO = false;

    Player::Config config_player1;
    config_player1.id = Player::Id::PLAYER1;
    config_player1.x = param.game.game_area.center_x - (Player::WIDTH / 2);
    config_player1.y = Y;
    config_player1.demo = DEMO;
    config_player1.play_area = &param.game.play_area.rect;
    config_player1.texture = player_textures.at(0);
    config_player1.animations = player1_animations;
    config_player1.hi_score_table = &Options::settings.hi_score_table;
    config_player1.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER1) - 1);
    players_.emplace_back(std::make_unique<Player>(config_player1));
    players_.back()->setPlayingState(Player::State::TITLE_HIDDEN);

    Player::Config config_player2;
    config_player2.id = Player::Id::PLAYER2;
    config_player2.x = param.game.game_area.center_x - (Player::WIDTH / 2);
    config_player2.y = Y;
    config_player2.demo = DEMO;
    config_player2.play_area = &param.game.play_area.rect;
    config_player2.texture = player_textures.at(1);
    config_player2.animations = player2_animations;
    config_player2.hi_score_table = &Options::settings.hi_score_table;
    config_player2.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER2) - 1);
    players_.emplace_back(std::make_unique<Player>(config_player2));
    players_.back()->setPlayingState(Player::State::TITLE_HIDDEN);

    // Registra los jugadores en Options
    for (const auto& player : players_) {
        Options::keyboard.addPlayer(player);
        Options::gamepad_manager.addPlayer(player);
    }
}

// Actualiza los jugadores
void Title::updatePlayers(float delta_time) {
    for (auto& player : players_) {
        player->update(delta_time);
    }
}

// Renderiza los jugadores
void Title::renderPlayers() {
    for (auto const& player : players_) {
        player->render();
    }
}

// Obtiene un jugador a partir de su "id"
auto Title::getPlayer(Player::Id id) -> std::shared_ptr<Player> {
    auto it = std::ranges::find_if(players_, [id](const auto& player) -> auto { return player->getId() == id; });

    if (it != players_.end()) {
        return *it;
    }
    return nullptr;
}