// game_scene.cpp - Implementació de la lógica del juego
// © 2026 JailDesigner

#include "game_scene.hpp"

#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <iostream>

#include "core/audio/audio.hpp"
#include "core/entities/entity_loader.hpp"
#include "core/input/input.hpp"
#include "core/input/input_types.hpp"
#include "core/locale/locale.hpp"
#include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp"
#include "game/entities/bullet_registry.hpp"
#include "game/entities/enemy_registry.hpp"
#include "game/entities/player_config.hpp"
#include "game/stage_system/stage_loader.hpp"
#include "game/systems/collision_system.hpp"
#include "game/systems/continue_system.hpp"
#include "game/systems/enemy_ai_system.hpp"
#include "game/systems/init_hud_animator.hpp"

// Using declarations per simplificar el codi
using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType;
using Option = SceneContext::Option;

namespace {
    // Attract mode: durada fixa de la demo. Amb vides infinites, sempre dura
    // això (les morts respawnegen); només input o aquest timeout la tallen.
    constexpr float DEMO_DURATION = 35.0F;

    // Qualsevol d'aquestes accions trenca la demo i torna al títol.
    constexpr std::array<InputAction, 5> DEMO_EXIT_ACTIONS = {
        InputAction::LEFT,
        InputAction::RIGHT,
        InputAction::THRUST,
        InputAction::SHOOT,
        InputAction::START};
}  // namespace

GameScene::GameScene(SDLManager& sdl, SceneContext& context)
    : sdl_(sdl),
      context_(context),
      debris_manager_(sdl.getRenderer()),
      firework_manager_(sdl.getRenderer()),
      floating_score_manager_(sdl.getRenderer()),
      trail_manager_(sdl.getRenderer()),
      text_(sdl.getRenderer()),
      starfield_parallax_(sdl.getRenderer()),
      playfield_(sdl.getRenderer()),
      border_(sdl.getRenderer()),
      curtain_(sdl.getRenderer()) {
    // Recuperar configuración de match des del context
    match_config_ = context_.getMatchConfig();

    // Debug output de la configuración
    std::cout << "[GameScene] Configuración de match - P1: "
              << (match_config_.player1_active ? "ACTIU" : "INACTIU")
              << ", P2: "
              << (match_config_.player2_active ? "ACTIU" : "INACTIU")
              << '\n';

    // Consumir opciones (preparació per MODE_DEMO futur)
    auto option = context_.consumeOption();
    (void)option;  // Suprimir warning de variable no usada

    // Carregar la configuració del player des de YAML. Sense fallback: si
    // falla, abortem (la nau no és construïble sense paràmetres).
    auto player_yaml = Entities::EntityLoader::load("player");
    if (!player_yaml) {
        std::cerr << "[GameScene] FATAL: no s'ha pogut carregar data/entities/player/player.yaml\n";
        std::exit(EXIT_FAILURE);
    }
    auto player_config = PlayerConfig::fromYaml(*player_yaml);
    if (!player_config) {
        std::cerr << "[GameScene] FATAL: player.yaml mal format\n";
        std::exit(EXIT_FAILURE);
    }

    // Carregar les configuracions dels 3 enemics. Sense fallback: si falla,
    // abortem (els enemics no es poden construir sense els seus paràmetres).
    if (!EnemyRegistry::loadAll()) {
        std::cerr << "[GameScene] FATAL: no s'han pogut carregar els enemics YAML\n";
        std::exit(EXIT_FAILURE);
    }

    // Carregar la configuració de la bala. Cal abans de construir el pool de
    // bullets, ja que cada Bullet llegeix el registry al seu ctor.
    if (!BulletRegistry::load()) {
        std::cerr << "[GameScene] FATAL: no s'ha pogut carregar bullet.yaml\n";
        std::exit(EXIT_FAILURE);
    }

    // Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
    ships_[0] = Ship(sdl.getRenderer(), *player_config);                    // Jugador 1: nau estàndard
    ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship/wedge.shp");  // Jugador 2: triangle amb cercle central

    // Inicialitzar balas con renderer
    std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));

    // Inicialitzar enemigos con renderer
    std::ranges::fill(enemies_, Enemy(sdl.getRenderer()));

    // Inicialitzar generador de números aleatoris
    // Basat en el codi Pascal original: line 376
    std::srand(static_cast<unsigned>(std::time(nullptr)));

    // Configurar el mundo físico con los límites de la zone de juego.
    physics_world_.clear();
    physics_world_.setBounds(Defaults::Zones::PLAYAREA);

    // Connectar els impactes contra les parets al border (bump + flash).
    physics_world_.setBoundsHitCallback([this](const Physics::BoundsHit& hit) {
        if (hit.impact_speed < Defaults::Border::BUMP_MIN_VELOCITY) {
            return;
        }
        const float STRENGTH = std::min(
            1.0F,
            hit.impact_speed / Defaults::Border::BUMP_VELOCITY_REFERENCE);
        border_.bumpAt(hit.contact_point, STRENGTH);
    });

    // Fireworks generen una ripple gran al playfield (ona d'aigua centrada al burst).
    firework_manager_.setSpawnCallback([this](Vec2 origin) {
        playfield_.notifyExplosion(origin);
    });

    // Explosions properes a una paret també generen bump (falloff lineal amb la distància).
    debris_manager_.setExplosionCallback([this](Vec2 center) {
        const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
        const float DIST_LEFT = std::abs(center.x - zone.x);
        const float DIST_RIGHT = std::abs((zone.x + zone.w) - center.x);
        const float DIST_TOP = std::abs(center.y - zone.y);
        const float DIST_BOTTOM = std::abs((zone.y + zone.h) - center.y);
        const float MIN_DIST = std::min({DIST_LEFT, DIST_RIGHT, DIST_TOP, DIST_BOTTOM});
        if (MIN_DIST > Defaults::Border::EXPLOSION_FALLOFF_PX) {
            return;
        }
        const float FALLOFF = 1.0F - (MIN_DIST / Defaults::Border::EXPLOSION_FALLOFF_PX);
        border_.bumpAt(center, Defaults::Border::EXPLOSION_BASE_STRENGTH * FALLOFF);
    });

    // Load stage configuration
    stage_config_ = StageSystem::StageLoader::load("data/stages/stages.yaml");
    if (!stage_config_) {
        std::cerr << "[GameScene] Error: no s'ha pogut load stages.yaml" << '\n';
        // Continue without stage system (will crash, but helps debugging)
    }

    // Initialize stage manager
    stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
    if (match_config_.mode == GameConfig::Mode::DEMO) {
        // Attract mode: arrencar directament en PLAYING a l'escenari curat
        // actual (partida "ja començada") i avançar l'índex perquè la pròxima
        // demo mostri un escenari diferent. El nombre de jugadors ja l'ha fixat
        // TitleScene al match_config llegint el mateix escenari.
        const Systems::Demo::Scenario SC = Systems::Demo::scenario(context_.demoScenarioIndex());
        context_.advanceDemoScenario();
        stage_manager_->initDemo(SC.stage);
        demo_timer_ = DEMO_DURATION;
        // Silenciar els SFX durant la demo (la música segueix). Guardem l'estat
        // previ per restaurar-lo al destructor sense xafar la preferència de l'usuari.
        sound_was_enabled_ = Audio::get()->isSoundEnabled();
        Audio::get()->enableSound(false);
        // El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa.
        playfield_.completeBuild();
        // La cortinilla arrenca tapant i cau per destapar la demo (continua el
        // moviment iniciat al títol, que va acabar amb la pantalla negra).
        curtain_.reveal(Defaults::Game::Curtain::REVEAL_DURATION);
    } else {
        stage_manager_->init();
    }

    // Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
    stage_manager_->getWaveRunner().setShipPosition(&ships_[0].getCenter());

    // Inicialitzar timers de muerte per player
    hit_timer_per_player_[0] = 0.0F;
    hit_timer_per_player_[1] = 0.0F;

    // Initialize lives and game over state (independent lives per player)
    lives_per_player_[0] = Defaults::Game::STARTING_LIVES;
    lives_per_player_[1] = Defaults::Game::STARTING_LIVES;
    game_over_state_ = GameOverState::NONE;
    continue_counter_ = 0;
    continue_tick_timer_ = 0.0F;
    continues_used_ = 0;
    game_over_timer_ = 0.0F;

    // Initialize scores (separate per player)
    score_per_player_[0] = 0;
    score_per_player_[1] = 0;
    floating_score_manager_.reset();

    // Inicialitzar naves segons configuración (solo jugadors active)
    for (uint8_t i = 0; i < 2; i++) {
        bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;

        if (player_active) {
            // Jugador active: init normalment
            Vec2 spawn_pos = getSpawnPoint(i);
            ships_[i].init(&spawn_pos, false);  // No invulnerability at start
            // Registrar el cuerpo físico de la nave en el mundo
            physics_world_.addBody(&ships_[i].getBody());
            std::cout << "[GameScene] Jugador " << (i + 1) << " inicialitzat\n";
        } else {
            // Jugador inactiu: marcar como a mort permanent
            ships_[i].markHit();
            hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
            lives_per_player_[i] = 0;  // Sin vides
            std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n";
        }
    }

    // Initialize enemies as inactive (stage system will spawn them).
    // Registramos el body al world incluso inactivo: con radius=0 no colisiona
    // ni se mueve, y al init() del stage system se activa sin re-registrar.
    for (auto& enemy : enemies_) {
        enemy.setShips(ships_.data(), &ships_[1]);
        physics_world_.addBody(&enemy.getBody());
        // DON'T call enemy.init() here - stage system handles spawning
    }

    // Inicialitzar balas.
    // Se registran en el physics_world para integración cinemática.
    // Como su body_.radius=0, no colisionan físicamente con nadie (las
    // colisiones de gameplay se gestionan en detectar_col·lisions_*).
    for (auto& bullet : bullets_) {
        bullet.init();
        physics_world_.addBody(&bullet.getBody());
    }

    // Reset flag de sons de animación
    init_hud_rect_sound_played_ = false;
}

GameScene::~GameScene() {
    // Si la demo havia silenciat els SFX, restaurar l'estat previ en sortir.
    if (match_config_.mode == GameConfig::Mode::DEMO) {
        Audio::get()->enableSound(sound_was_enabled_);
    }
}

auto GameScene::isFinished() const -> bool {
    return context_.nextScene() != SceneType::GAME;
}

void GameScene::handleEvent(const SDL_Event& event) {
    // GameScene no procesa eventos puntuales SDL: la lógica de input se
    // resuelve en update() consultando Input::checkAction.
    (void)event;
}

void GameScene::update(float delta_time) {
    // Pausa global: mentre el menu de servei esta obert, congelem la lògica
    // de joc. El draw() segueix executant-se per a mantenir l'escena visible
    // sota el menu.
    if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
        return;
    }

    // Orquestador delgado: cada paso vive en su propia función para
    // mantener update() legible y reducir complejidad cognitiva.
    stepPhysics(delta_time);

    if (match_config_.mode == GameConfig::Mode::DEMO) {
        // Mode demo (attract): salida por input/timeout/muerte + control del pilot.
        if (stepDemo(delta_time)) {
            return;
        }
    } else if (game_over_state_ == GameOverState::NONE) {
        stepShootingInput();
        stepMidGameJoin();
    }
    if (stepContinueScreen(delta_time)) {
        return;
    }
    if (stepGameOver(delta_time)) {
        return;
    }
    stepDeathSequence(delta_time);
    stepStageStateMachine(delta_time);
}

void GameScene::stepPhysics(float delta_time) {
    // Las fuerzas aplicadas en el frame N-1 por processInput/AI se integran
    // ahora; postUpdate sincroniza los mirrors (center_/angle_) antes de la
    // lógica de juego que los lee.
    physics_world_.update(delta_time);
    for (auto& ship : ships_) {
        ship.postUpdate(delta_time);
    }
    for (auto& enemy : enemies_) {
        enemy.postUpdate(delta_time);
    }
    for (auto& bullet : bullets_) {
        bullet.postUpdate(delta_time);
    }
    trail_manager_.update(delta_time, ships_);
    // Starfield: world_velocity = -mitjana_de_naus_actives. Si dues naus van en
    // sentits oposats, es cancel·len → estrelles quietes (cap jugador "guanya").
    // Si només n'hi ha una activa, segueix la seva velocitat.
    Vec2 ship_vel_avg{.x = 0.0F, .y = 0.0F};
    int n_active = 0;
    for (const auto& ship : ships_) {
        if (ship.isActive()) {
            const Vec2 V = ship.getVelocityVector();
            ship_vel_avg.x += V.x;
            ship_vel_avg.y += V.y;
            n_active++;
        }
    }
    if (n_active > 0) {
        ship_vel_avg.x /= static_cast<float>(n_active);
        ship_vel_avg.y /= static_cast<float>(n_active);
    }
    starfield_parallax_.update(delta_time, Vec2{.x = -ship_vel_avg.x, .y = -ship_vel_avg.y});
    playfield_.update(delta_time);
    border_.update(delta_time);

    // Notificar al playfield que la nau es mou (genera ripples petites a cadència).
    for (std::size_t id = 0; id < ships_.size(); id++) {
        if (ships_[id].isActive()) {
            playfield_.notifyShipMoving(static_cast<std::uint8_t>(id),
                ships_[id].getCenter(),
                ships_[id].getSpeed(),
                delta_time);
        }
    }
}

void GameScene::stepShootingInput() {
    auto* input = Input::get();
    if (match_config_.player1_active &&
        input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
        fireBullet(0);
    }
    if (match_config_.player2_active &&
        input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
        fireBullet(1);
    }
}

void GameScene::updateShipsControl(float delta_time) {
    const bool DEMO = (match_config_.mode == GameConfig::Mode::DEMO);
    for (uint8_t i = 0; i < 2; i++) {
        const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
        if (!ACTIU || hit_timer_per_player_[i] != 0.0F) {
            continue;
        }
        // En demo, cada nau activa es mou amb el seu pilot IA (control calculat
        // a stepDemo); la resta de casos llegeixen Input com sempre.
        if (DEMO) {
            ships_[i].applyMovement(demo_ctrls_[i].left, demo_ctrls_[i].right, demo_ctrls_[i].thrust, delta_time);
        } else {
            ships_[i].processInput(delta_time, i);
        }
        ships_[i].update(delta_time);
    }
}

auto GameScene::stepDemo(float delta_time) -> bool {
    curtain_.update(delta_time);  // cortinilla que destapa la demo

    // Qualsevol input trenca la demo i torna al títol (música intacta).
    if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) {
        context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN);
        return true;
    }

    // Vides infinites: la demo dura sempre el temps fix; en morir, stepDeathSequence
    // respawneja (no acaba ni passa per CONTINUE/GAME_OVER).
    demo_timer_ -= delta_time;
    if (demo_timer_ <= 0.0F) {
        endDemo();
        return true;
    }

    // Control de cada nau activa per al frame; el disparo el dispara GameScene.
    for (uint8_t i = 0; i < 2; i++) {
        const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
        if (!ACTIU || hit_timer_per_player_[i] != 0.0F) {
            demo_ctrls_[i] = {};  // nau inactiva/morta: sense control
            continue;
        }
        demo_ctrls_[i] = demo_pilots_[i].compute(
            ships_[i],
            enemies_,
            bullets_,
            Defaults::Zones::PLAYAREA,
            delta_time);
        if (demo_ctrls_[i].shoot) {
            fireBullet(i);
        }
    }
    return false;
}

void GameScene::endDemo() {
    // No parem la música: title.ogg segueix sonant durant el cicle atrae.
    context_.setNextScene(SceneType::LOGO);
}

void GameScene::stepMidGameJoin() {
    // Permitir join solo durante PLAYING.
    if (stage_manager_->getState() != StageSystem::EstatStage::PLAYING) {
        return;
    }

    // Solo se permite join si hay al menos un jugador vivo (no se puede
    // hacer join en pantalla vacía).
    const bool ALGU_VIU =
        (match_config_.player1_active && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
        (match_config_.player2_active && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
    if (!ALGU_VIU) {
        return;
    }

    auto* input = Input::get();
    for (uint8_t pid = 0; pid < 2; pid++) {
        const bool ACTIU = (pid == 0) ? match_config_.player1_active
                                      : match_config_.player2_active;
        const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
        if (ACTIU && !MUERTO_SIN_VIDAS) {
            continue;  // jugador ya está jugando
        }
        const bool START_PRESSED = (pid == 0)
            ? input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)
            : input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
        if (START_PRESSED) {
            joinPlayer(pid);
        }
    }
}

auto GameScene::stepContinueScreen(float delta_time) -> bool {
    if (game_over_state_ != GameOverState::CONTINUE) {
        return false;
    }

    Systems::ContinueScreen::Context cont_ctx{
        .state = game_over_state_,
        .counter = continue_counter_,
        .tick_timer = continue_tick_timer_,
        .continues_used = continues_used_,
        .game_over_timer = game_over_timer_,
        .lives_per_player = lives_per_player_,
        .score_per_player = score_per_player_,
        .hit_timer_per_player = hit_timer_per_player_,
        .ships = ships_,
        .match_config = match_config_,
        .get_spawn_point = [this](uint8_t pid) { return getSpawnPoint(pid); },
    };
    Systems::ContinueScreen::update(cont_ctx, delta_time);
    Systems::ContinueScreen::processInput(cont_ctx);

    // Enemies, bullets y efectos siguen moviéndose en background.
    for (auto& enemy : enemies_) {
        Systems::EnemyAi::move(enemy, delta_time);
        enemy.update(delta_time);
    }
    for (auto& bullet : bullets_) {
        bullet.update(delta_time);
    }
    Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
    debris_manager_.update(delta_time);
    firework_manager_.update(delta_time);
    floating_score_manager_.update(delta_time);
    return true;
}

auto GameScene::stepGameOver(float delta_time) -> bool {
    if (game_over_state_ != GameOverState::GAME_OVER) {
        return false;
    }

    game_over_timer_ -= delta_time;
    if (game_over_timer_ <= 0.0F) {
        Audio::get()->stopMusic();
        context_.setNextScene(SceneType::LOGO);
        return true;
    }

    // Enemies, bullets y efectos siguen moviéndose como fondo.
    for (auto& enemy : enemies_) {
        Systems::EnemyAi::move(enemy, delta_time);
        enemy.update(delta_time);
    }
    for (auto& bullet : bullets_) {
        bullet.update(delta_time);
    }
    Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
    debris_manager_.update(delta_time);
    firework_manager_.update(delta_time);
    floating_score_manager_.update(delta_time);
    return true;
}

void GameScene::stepDeathSequence(float delta_time) {
    bool algun_mort = false;
    for (uint8_t i = 0; i < 2; i++) {
        if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) {
            continue;
        }
        algun_mort = true;
        hit_timer_per_player_[i] += delta_time;

        if (hit_timer_per_player_[i] < Defaults::Game::DEATH_DURATION) {
            continue;
        }

        // *** PHASE 3: RESPAWN OR GAME OVER ***
        // Mode demo: vides infinites. Respawn sempre, sense decrementar vides ni
        // passar mai per CONTINUE/GAME_OVER — la demo dura el seu temps fix.
        if (match_config_.mode == GameConfig::Mode::DEMO) {
            Vec2 spawn_pos = getSpawnPoint(i);
            ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true);
            hit_timer_per_player_[i] = 0.0F;
            continue;
        }

        lives_per_player_[i]--;
        if (lives_per_player_[i] > 0) {
            Vec2 spawn_pos = getSpawnPoint(i);
            ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true);
            hit_timer_per_player_[i] = 0.0F;
            continue;
        }

        // Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
        hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
        const bool P1_DEAD = !match_config_.player1_active || lives_per_player_[0] <= 0;
        const bool P2_DEAD = !match_config_.player2_active || lives_per_player_[1] <= 0;
        if (P1_DEAD && P2_DEAD) {
            game_over_state_ = GameOverState::CONTINUE;
            continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
            continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
        }
    }

    // Si hay algún muerto, los enemigos/balas/efectos siguen actualizándose
    // aunque otros jugadores aún jueguen.
    if (algun_mort) {
        for (auto& enemy : enemies_) {
            Systems::EnemyAi::move(enemy, delta_time);
            enemy.update(delta_time);
        }
        for (auto& bullet : bullets_) {
            bullet.update(delta_time);
        }
        debris_manager_.update(delta_time);
        firework_manager_.update(delta_time);
        floating_score_manager_.update(delta_time);
    }
    // El bool 'algun_mort' es puramente interno: no aporta nada al caller
    // (la stage state machine sigue corriendo aunque haya jugadores muriendo),
    // así que la función no devuelve nada.
}

void GameScene::stepStageStateMachine(float delta_time) {
    const StageSystem::EstatStage STATE = stage_manager_->getState();
    switch (STATE) {
        case StageSystem::EstatStage::INIT_HUD:
            runStageInitHud(delta_time);
            break;
        case StageSystem::EstatStage::LEVEL_START:
            runStageLevelStart(delta_time);
            break;
        case StageSystem::EstatStage::PLAYING:
            runStagePlaying(delta_time);
            break;
        case StageSystem::EstatStage::LEVEL_COMPLETED:
            runStageLevelCompleted(delta_time);
            break;
    }
}

void GameScene::runStageInitHud(float delta_time) {
    // Update stage manager timer (puede cambiar el state).
    stage_manager_->update(delta_time);
    // Si el state cambió, salir para no usar el timer del nuevo state.
    if (stage_manager_->getState() != StageSystem::EstatStage::INIT_HUD) {
        return;
    }

    float global_progress = 1.0F - (stage_manager_->getTransitionTimer() / Defaults::Game::INIT_HUD_DURATION);
    global_progress = std::min(1.0F, global_progress);

    const float SHIP1_P = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
        Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
    const float SHIP2_P = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
        Defaults::Game::INIT_HUD_SHIP2_RATIO_END);

    if (match_config_.player1_active && SHIP1_P < 1.0F) {
        ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0)));
    }
    if (match_config_.player2_active && SHIP2_P < 1.0F) {
        ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(1)));
    }
}

void GameScene::runStageLevelStart(float delta_time) {
    stage_manager_->update(delta_time);

    // Ambas naves pueden moverse y disparar durante el intro.
    updateShipsControl(delta_time);
    for (auto& bullet : bullets_) {
        bullet.update(delta_time);
    }
    Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
    debris_manager_.update(delta_time);
    firework_manager_.update(delta_time);
}

void GameScene::runStagePlaying(float delta_time) {
    const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
    stage_manager_->getWaveRunner().update(delta_time, enemies_, PAUSE_SPAWN);

    // Stage completado: cuando al menos un jugador está vivo y todas las onades emeses y arena buida.
    const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
    if (ALGU_VIU && stage_manager_->getWaveRunner().stageComplete(enemies_)) {
        stage_manager_->markStageCompleted();
        Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
        return;
    }

    // Gameplay normal: ships activos + entidades + colisiones + efectos.
    updateShipsControl(delta_time);
    auto ai_ctx = buildCollisionContext();
    for (std::size_t i = 0; i < enemies_.size(); ++i) {
        Systems::EnemyAi::tick(ai_ctx, enemies_[i], i, delta_time);
        enemies_[i].update(delta_time);
    }

    // Col·lisions primer, després desactivació per fora-de-zone: així una bala que
    // el mateix frame xoca amb un enemic i alhora surt del PLAYAREA es compta com a
    // impacte abans no se la trenqui per sortir.
    runCollisionDetections();
    for (auto& bullet : bullets_) {
        bullet.update(delta_time);
    }
    Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
    debris_manager_.update(delta_time);
    firework_manager_.update(delta_time);
    floating_score_manager_.update(delta_time);
}

void GameScene::runStageLevelCompleted(float delta_time) {
    stage_manager_->update(delta_time);
    updateShipsControl(delta_time);
    for (auto& bullet : bullets_) {
        bullet.update(delta_time);
    }
    Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
    debris_manager_.update(delta_time);
    firework_manager_.update(delta_time);
    floating_score_manager_.update(delta_time);
}

auto GameScene::buildCollisionContext() -> Systems::Collision::Context {
    return Systems::Collision::Context{
        .ships = ships_,
        .enemies = enemies_,
        .bullets = bullets_,
        .hit_timer_per_player = hit_timer_per_player_,
        .score_per_player = score_per_player_,
        .lives_per_player = lives_per_player_,
        .debris_manager = debris_manager_,
        .firework_manager = firework_manager_,
        .floating_score_manager = floating_score_manager_,
        .match_config = match_config_,
        .on_player_hit = [this](uint8_t pid, const Vec2& bv) { tocado(pid, bv); },
    };
}

void GameScene::runCollisionDetections() {
    auto col_ctx = buildCollisionContext();
    Systems::Collision::detectAll(col_ctx);
}

void GameScene::draw() {
    if (game_over_state_ == GameOverState::CONTINUE) {
        drawContinueState();
        return;
    }

    if (game_over_state_ == GameOverState::GAME_OVER) {
        drawGameOverState();
        return;
    }

    switch (stage_manager_->getState()) {
        case StageSystem::EstatStage::INIT_HUD:
            drawInitHudState();
            break;
        case StageSystem::EstatStage::LEVEL_START:
            drawLevelStartState();
            break;
        case StageSystem::EstatStage::PLAYING:
            drawPlayingState();
            break;
        case StageSystem::EstatStage::LEVEL_COMPLETED:
            drawLevelCompletedState();
            break;
    }

    // Cortinilla d'entrada de la demo: per damunt de tot. No-op fora del mode
    // DEMO (curtain_ mai s'arrenca) i quan ja ha sortit per baix.
    curtain_.draw();
}

void GameScene::drawEnemies() const {
    for (const auto& enemy : enemies_) {
        enemy.draw();
    }
}

void GameScene::drawBullets() const {
    for (const auto& bullet : bullets_) {
        bullet.draw();
    }
}

void GameScene::drawActiveShipsAlive() const {
    for (uint8_t i = 0; i < 2; i++) {
        bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
        if (player_active && hit_timer_per_player_[i] == 0.0F) {
            ships_[i].draw();
        }
    }
}

void GameScene::drawContinueState() {
    starfield_parallax_.draw();
    border_.draw();
    drawEnemies();
    drawBullets();
    debris_manager_.draw();
    firework_manager_.draw();
    floating_score_manager_.draw();
    drawScoreboard();
    drawContinue();
}

void GameScene::drawGameOverState() {
    starfield_parallax_.draw();
    border_.draw();
    drawEnemies();
    drawBullets();
    debris_manager_.draw();
    firework_manager_.draw();
    floating_score_manager_.draw();

    const std::string GAME_OVER_TEXT = Locale::get().text("game_screen.game_over");
    constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE;
    constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING;

    const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
    float center_x = play_area.x + (play_area.w / 2.0F);
    float center_y = play_area.y + (play_area.h / 2.0F);

    text_.renderCentered(GAME_OVER_TEXT, {.x = center_x, .y = center_y}, SCALE, SPACING);

    drawScoreboard();
}

void GameScene::drawInitHudState() {
    float timer = stage_manager_->getTransitionTimer();
    float total_time = Defaults::Game::INIT_HUD_DURATION;
    float global_progress = 1.0F - (timer / total_time);

    float rect_progress = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_RECT_RATIO_INIT,
        Defaults::Game::INIT_HUD_RECT_RATIO_END);

    float score_progress = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_SCORE_RATIO_INIT,
        Defaults::Game::INIT_HUD_SCORE_RATIO_END);

    float ship1_progress = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
        Defaults::Game::INIT_HUD_SHIP1_RATIO_END);

    float ship2_progress = Systems::InitHud::computeRangeProgress(
        global_progress,
        Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
        Defaults::Game::INIT_HUD_SHIP2_RATIO_END);

    // Capa de fons més profunda: estrelles 2D (apareixen senceres des del frame 0).
    starfield_parallax_.draw();
    // Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD).
    playfield_.draw();

    if (rect_progress > 0.0F) {
        if (!init_hud_rect_sound_played_) {
            Audio::get()->playSound(Defaults::Sound::INIT_HUD, Audio::Group::GAME);
            init_hud_rect_sound_played_ = true;
        }
        Systems::InitHud::drawBordersAnimated(sdl_.getRenderer(), rect_progress);
    }

    if (score_progress > 0.0F) {
        Systems::InitHud::drawScoreboardAnimated(sdl_.getRenderer(), text_, buildScoreboardData(), score_progress);
    }

    if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
        ships_[0].draw();
    }

    if (ship2_progress > 0.0F && match_config_.player2_active && ships_[1].isActive()) {
        ships_[1].draw();
    }
}

void GameScene::drawLevelStartState() {
    starfield_parallax_.draw();
    playfield_.draw();
    border_.draw();
    trail_manager_.draw();
    drawActiveShipsAlive();
    drawBullets();
    debris_manager_.draw();
    firework_manager_.draw();
    floating_score_manager_.draw();
    drawStageMessage(stage_manager_->getLevelStartMessage());
    drawScoreboard();
}

void GameScene::drawPlayingState() {
    starfield_parallax_.draw();
    playfield_.draw();
    border_.draw();
    trail_manager_.draw();
    drawActiveShipsAlive();
    drawEnemies();
    drawBullets();
    debris_manager_.draw();
    firework_manager_.draw();
    floating_score_manager_.draw();
    drawScoreboard();
}

void GameScene::drawLevelCompletedState() {
    starfield_parallax_.draw();
    playfield_.draw();
    border_.draw();
    trail_manager_.draw();
    drawActiveShipsAlive();
    drawBullets();
    debris_manager_.draw();
    firework_manager_.draw();
    floating_score_manager_.draw();
    drawStageMessage(Locale::get().text("stage.completed"));
    drawScoreboard();
}

void GameScene::tocado(uint8_t player_id, const Vec2& bullet_velocity) {
    // Death sequence: 3 phases
    // Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
    // Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
    // Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in update()

    if (hit_timer_per_player_[player_id] == 0.0F) {
        // *** PHASE 1: TRIGGER DEATH ***

        // Capturar velocitat ABANS del markHit (que la reseteja a zero).
        // Sense això, els debris no hereten cap inèrcia de la nau.
        const Vec2 SHIP_VEL_PRE_DEATH = ships_[player_id].getVelocityVector();
        const Vec2 SHIP_POS = ships_[player_id].getCenter();
        const float SHIP_ANGLE = ships_[player_id].getAngle();
        const float SHIP_BRIGHT = ships_[player_id].getBrightness();

        // Mark ship as dead (stops rendering and input)
        ships_[player_id].markHit();

        const Vec2 INHERITED_VEL = SHIP_VEL_PRE_DEATH *
            Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;

        // Mateixa dispersió i efecte que els debris d'enemic (lifetime,
        // friction, segment_multiplier alineats); només canvien sound i color.
        // bullet_velocity arriba a explode() com a impuls extra independent
        // de la inèrcia del cos del ship — els trossos volen amb la força
        // de la bala encara que el ship estiga quiet.
        debris_manager_.explode(
            ships_[player_id].getShape(),
            SHIP_POS,
            SHIP_ANGLE,
            1.0F,
            Defaults::Physics::Debris::SPEED_BASE,
            SHIP_BRIGHT,
            INHERITED_VEL,
            0.0F,  // sense herència angular
            0.0F,  // sin herencia visual
            Defaults::Sound::PLAYER_EXPLOSION,
            ships_[player_id].getConfig().colors.normal,
            Defaults::Physics::Debris::ENEMY_LIFETIME,
            Defaults::Physics::Debris::ENEMY_FRICTION,
            Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
            bullet_velocity);

        // Start death timer (non-zero to avoid re-triggering)
        hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
    }
    // Phase 2 is automatic (debris updates in update())
    // Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION
}

void GameScene::drawScoreboard() {
    const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
    const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
    const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
    const Vec2 CENTER = {
        .x = scoreboard_zone.w / 2.0F,
        .y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F),
    };
    // En mode demo (attract) el marcador no té sentit: substituïm puntuacions i
    // vides per un rètol que indica que és una demo i convida a jugar.
    if (match_config_.mode == GameConfig::Mode::DEMO) {
        text_.renderCentered(Locale::get().text("demo.banner"), CENTER, SCALE, SPACING);
        return;
    }
    Systems::InitHud::drawScoreboardAt(sdl_.getRenderer(), text_, buildScoreboardData(), CENTER.y, SCALE, SPACING);
}

auto GameScene::buildScoreboardData() const -> Systems::InitHud::ScoreboardData {
    Systems::InitHud::ScoreboardData out;

    // Puntuació a 6 dígits amb zeros a l'esquerra (inactiu → tot zeros, 0 vides).
    const auto FORMAT_SCORE = [](int score) {
        const std::string S = std::to_string(score);
        return std::string(6 - std::min(6, static_cast<int>(S.length())), '0') + S;
    };

    out.score_p1 = match_config_.player1_active ? FORMAT_SCORE(score_per_player_[0]) : "000000";
    out.lives_p1 = match_config_.player1_active ? lives_per_player_[0] : 0;
    out.score_p2 = match_config_.player2_active ? FORMAT_SCORE(score_per_player_[1]) : "000000";
    out.lives_p2 = match_config_.player2_active ? lives_per_player_[1] : 0;

    // Shapes de les naus per a les icones de vides (reutilitza la geometria ja
    // carregada de cada Ship).
    out.shape_p1 = ships_[0].getShape();
    out.shape_p2 = ships_[1].getShape();

    // Nivell: etiqueta localitzada + número a 2 dígits (separats per pintar-los
    // amb tonalitats distintes).
    const uint8_t STAGE_NUM = stage_manager_->getCurrentStage();
    out.level_label = Locale::get().text("hud.level");
    out.level_value = (STAGE_NUM < 10) ? "0" + std::to_string(STAGE_NUM)
                                       : std::to_string(STAGE_NUM);
    return out;
}

// [NEW] Stage system helper methods

void GameScene::drawStageMessage(const std::string& message) {
    constexpr float BASE_SCALE = 1.0F;
    constexpr float SPACING = 2.0F;

    const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
    const float MAX_WIDTH = play_area.w * Defaults::Game::STAGE_MESSAGE_MAX_WIDTH_RATIO;

    // ========== TYPEWRITER EFFECT (PARAMETRIZED) ==========
    // Get state-specific timing configuration
    float total_time;
    float typing_ratio;

    if (stage_manager_->getState() == StageSystem::EstatStage::LEVEL_START) {
        total_time = Defaults::Game::LEVEL_START_DURATION;
        typing_ratio = Defaults::Game::LEVEL_START_TYPING_RATIO;
    } else {  // LEVEL_COMPLETED
        total_time = Defaults::Game::LEVEL_COMPLETED_DURATION;
        typing_ratio = Defaults::Game::LEVEL_COMPLETED_TYPING_RATIO;
    }

    // Calculate progress from timer (0.0 at start → 1.0 at end)
    float remaining_time = stage_manager_->getTransitionTimer();
    float progress = 1.0F - (remaining_time / total_time);

    // Determine how many characters to show
    size_t visible_chars;

    if (typing_ratio > 0.0F && progress < typing_ratio) {
        // Typewriter phase: show partial text
        float typing_progress = progress / typing_ratio;  // Normalize to 0.0-1.0
        visible_chars = static_cast<size_t>(message.length() * typing_progress);
        if (visible_chars == 0 && progress > 0.0F) {
            visible_chars = 1;  // Show at least 1 character after first frame
        }
    } else {
        // Display phase: show complete text
        // (Either after typing phase, or immediately if typing_ratio == 0.0)
        visible_chars = message.length();
    }

    // Create partial message (substring for typewriter)
    std::string partial_message = message.substr(0, visible_chars);
    // ===================================================

    // Calculate text width at base scale (using FULL message for position calculation)
    float text_width_at_base = Graphics::VectorText::getTextWidth(message, BASE_SCALE, SPACING);

    // Auto-scale if text exceeds max width
    float scale = (text_width_at_base <= MAX_WIDTH)
        ? BASE_SCALE
        : MAX_WIDTH / text_width_at_base;

    // Recalculate dimensions with final scale (using FULL message for centering)
    float full_text_width = Graphics::VectorText::getTextWidth(message, scale, SPACING);
    float text_height = Graphics::VectorText::getTextHeight(scale);

    // Calculate position as if FULL text was there (for fixed position typewriter)
    float x = play_area.x + ((play_area.w - full_text_width) / 2.0F);
    float y = play_area.y + (play_area.h * Defaults::Game::STAGE_MESSAGE_Y_RATIO) - (text_height / 2.0F);

    // Render only the partial message (typewriter effect) amb el color
    // ambre neon del "PRESS START" del títol — unifica el feel dels missatges.
    Vec2 pos = {.x = x, .y = y};
    text_.render(partial_message, pos, scale, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START);
}

// ========================================
// Helper methods for 2-player support
// ========================================

auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
    const SDL_FRect& zone = Defaults::Zones::PLAYAREA;

    float x_ratio;
    if (match_config_.isSinglePlayer()) {
        // Un sol player: spawn al centro (50%)
        x_ratio = 0.5F;
    } else {
        // Dos jugadors: spawn a posicions separades
        x_ratio = (player_id == 0)
            ? Defaults::Game::P1_SPAWN_X_RATIO   // 33%
            : Defaults::Game::P2_SPAWN_X_RATIO;  // 67%
    }

    return {
        .x = zone.x + (zone.w * x_ratio),
        .y = zone.y + (zone.h * Defaults::Game::SPAWN_Y_RATIO)};
}

void GameScene::fireBullet(uint8_t player_id) {
    // Verificar que el player está vivo
    if (hit_timer_per_player_[player_id] > 0.0F) {
        return;
    }
    if (!ships_[player_id].isActive()) {
        return;
    }

    // Calcular posición en la punta de la nave
    const Vec2& ship_centre = ships_[player_id].getCenter();
    float ship_angle = ships_[player_id].getAngle();

    constexpr float LOCAL_TIP_X = Defaults::Hud::Tips::LOCAL_X;
    constexpr float LOCAL_TIP_Y = Defaults::Hud::Tips::LOCAL_Y;
    float cos_a = std::cos(ship_angle);
    float sin_a = std::sin(ship_angle);
    float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x;
    float tip_y = (LOCAL_TIP_X * sin_a) + (LOCAL_TIP_Y * cos_a) + ship_centre.y;
    Vec2 fire_position = {.x = tip_x, .y = tip_y};

    // Buscar primera bullet inactiva en el pool del player.
    // El pool global té MAX_BULLETS slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]).
    constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BULLETS;
    const int START_IDX = player_id * SLOTS_PER_PLAYER;
    for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
        if (!bullets_[i].isActive()) {
            bullets_[i].fire(fire_position, ship_angle, player_id, ships_[player_id].getConfig().weapon.bullet_speed);
            break;
        }
    }
}

void GameScene::drawContinue() {
    const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
    constexpr float SPACING = 4.0F;

    // "CONTINUE" text (using constants)
    const std::string CONTINUE_TEXT = Locale::get().text("game_screen.continue");
    float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
    float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;

    float center_x = play_area.x + (play_area.w / 2.0F);
    float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue);

    text_.renderCentered(CONTINUE_TEXT, {.x = center_x, .y = centre_y_continue}, escala_continue, SPACING);

    // Countdown number (using constants)
    const std::string COUNTER_STR = std::to_string(continue_counter_);
    float escala_counter = Defaults::Game::ContinueScreen::COUNTER_TEXT_SCALE;
    float y_ratio_counter = Defaults::Game::ContinueScreen::COUNTER_TEXT_Y_RATIO;

    float centre_y_counter = play_area.y + (play_area.h * y_ratio_counter);

    text_.renderCentered(COUNTER_STR, {.x = center_x, .y = centre_y_counter}, escala_counter, SPACING);

    // "CONTINUES LEFT" (conditional + using constants)
    if (!Defaults::Game::INFINITE_CONTINUES) {
        const std::string CONTINUES_TEXT = localeSubstitute(
            Locale::get().text("game_screen.continues_left"),
            "{n}",
            std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_));
        float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE;
        float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO;

        float centre_y_info = play_area.y + (play_area.h * y_ratio_info);

        text_.renderCentered(CONTINUES_TEXT, {.x = center_x, .y = centre_y_info}, escala_info, SPACING);
    }
}

void GameScene::joinPlayer(uint8_t player_id) {
    // Activate player
    if (player_id == 0) {
        match_config_.player1_active = true;
    } else {
        match_config_.player2_active = true;
    }

    // Reset stats
    lives_per_player_[player_id] = Defaults::Game::STARTING_LIVES;
    score_per_player_[player_id] = 0;
    hit_timer_per_player_[player_id] = 0.0F;

    // Spawn with invulnerability
    Vec2 spawn_pos = getSpawnPoint(player_id);
    ships_[player_id].init(&spawn_pos, true);

    // Registrar el cos físic al món. Si el jugador començà inactiu, el
    // constructor no l'havia afegit; sense això, applyForce s'acumula
    // però mai s'integra → la nau no es desplaça.
    physics_world_.addBody(&ships_[player_id].getBody());

    std::cout << "[GameScene] Jugador " << (int)(player_id + 1) << " s'ha unit a la match!" << '\n';
}
