#include "core/system/director.hpp"

#include <cstring>
#include <iostream>

#include "core/audio/audio.hpp"
#include "core/input/gamepad.hpp"
#include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/input/key_remap.hpp"
#include "core/input/mouse.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/info.hpp"
#include "game/modulegame.hpp"
#include "game/options.hpp"
#include "game/scenes/banner_scene.hpp"
#include "game/scenes/boot_loader_scene.hpp"
#include "game/scenes/credits_scene.hpp"
#include "game/scenes/intro_new_logo_scene.hpp"
#include "game/scenes/intro_scene.hpp"
#include "game/scenes/menu_scene.hpp"
#include "game/scenes/mort_scene.hpp"
#include "game/scenes/scene.hpp"
#include "game/scenes/scene_registry.hpp"
#include "game/scenes/secreta_scene.hpp"
#include "game/scenes/slides_scene.hpp"

std::unique_ptr<Director> Director::instance_;

Director::~Director() = default;

void Director::initGameContext() {
    info::ctx.num_habitacio = Options::game.habitacio_inicial;
    info::ctx.num_piramide = Options::game.piramide_inicial;
    info::ctx.diners = Options::game.diners_inicial;
    info::ctx.diamants = Options::game.diamants_inicial;
    info::ctx.vida = Options::game.vides;
    info::ctx.momies = 0;
    info::ctx.nou_personatge = false;
    info::ctx.pepe_activat = false;

    FILE* ini = fopen("trick.ini", "rb");
    if (ini != nullptr) {
        info::ctx.nou_personatge = true;
        fclose(ini);
    }
}

auto Director::createNextScene() const -> std::unique_ptr<scenes::Scene> {
    // Mentre el Resource::Cache no haja acabat de precarregar, executem
    // el BootLoaderScene — pinta una barra de progrés i avança la
    // càrrega per pressupost de temps. Quan acaba, retorna i tornem ací
    // amb el cache plenament disponible per a la resta d'escenes.
    if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) {
        return std::make_unique<scenes::BootLoaderScene>();
    }
    if (game_state_ == 0) {
        // Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
        return std::make_unique<ModuleGame>();
    }
    // game_state_ == 1: dispatch al registry per num_piramide. Replica
    // del redirect que el vell ModuleSequence::Go() feia: si el jugador
    // arriba a la Secreta (6) sense prou diners, salta als slides de
    // fracàs (7) abans de buscar l'escena al registry.
    if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
        info::ctx.num_piramide = 7;
    }
    return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
}

void Director::init() {
    instance_ = std::unique_ptr<Director>(new Director());
    Gamepad::init();

    // Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
    // amb una factory de `scenes::Scene`. iterate() consulta aquest
    // registry per a tots els states de seqüència (game_state_ == 1); si
    // una clau no apareix ací, Director surt ordenadament.
    auto& registry = scenes::SceneRegistry::instance();
    registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
    registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
    // BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
    // pel switch intern llegint info::ctx.num_piramide).
    for (int p = 2; p <= 5; ++p) {
        registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
    }
    // SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
    // - num_piramide == 1: slides narratius inicials (entrada al joc)
    // - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
    //   l'usuari no té prou diners per a la Secreta)
    registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
    registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
    registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
    registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
    // State 255 (intro): dues variants segons `Options::game.use_new_logo`.
    // La factory tria a runtime — així es pot togglar des del menú sense
    // re-registrar. Les dues escenes construeixen una IntroSpritesScene
    // com a sub-escena per a la part d'animacions de sprites.
    registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
        if (Options::game.use_new_logo) {
            return std::make_unique<scenes::IntroNewLogoScene>();
        }
        return std::make_unique<scenes::IntroScene>();
    });
}

void Director::destroy() {
    Gamepad::destroy();
    instance_.reset();
}

auto Director::get() -> Director* {
    return instance_.get();
}

void Director::togglePause() {
    paused_ = !paused_;
    if (paused_) {
        Audio::get()->pauseMusic();
    } else {
        Audio::get()->resumeMusic();
    }
}

void Director::setup() {
    // Els buffers són membres (director.hpp); només els inicialitzem.
    std::memset(game_frame_, 0, sizeof(game_frame_));
    std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
    has_frame_ = false;
}

auto Director::iterate() -> bool {
    if (quit_requested_) {
        Jg::quitSignal();
        current_scene_.reset();  // destrueix l'escena actual ordenadament
        return false;
    }

    // Reinici "suau": processat al començament del frame per no manipular
    // l'escena des d'una lambda del menú mentre encara s'està executant.
    if (restart_requested_) {
        restart_requested_ = false;
        Audio::get()->stopMusic();
        Audio::get()->stopAllSounds();
        // Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
        // en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
        initGameContext();
        // Força l'intro independentment de `piramide_inicial` (que pot estar
        // configurat a una piràmide intermèdia per a proves ràpides).
        info::ctx.num_piramide = 255;
        current_scene_.reset();
        game_state_ = 1;  // 1 = dispatch via SceneRegistry per num_piramide
        has_frame_ = false;
        Menu::close();
        Ji::setInputBlocked(false);  // el menú ho havia bloquejat — cal desfer-ho
    }

    if (!context_initialized_) {
        initGameContext();
        context_initialized_ = true;
    }

    constexpr Uint32 FRAME_MS_VSYNC = 16;    // ~60 FPS amb VSync
    constexpr Uint32 FRAME_MS_NO_VSYNC = 4;  // ~250 FPS sense VSync (límit superior)

    const Uint32 frame_start = SDL_GetTicks();

    Gamepad::update();
    KeyRemap::update();
    GlobalInputs::handle();
    Mouse::updateCursorVisibility();

    // Bombeig de l'àudio: reomple l'stream de música i para els canals
    // drenats. Substituïx el callback de SDL_AddTimer de la versió
    // antiga — imprescindible per al port a emscripten.
    Audio::update();

    // Dispara els crèdits cinematogràfics la primera vegada que el joc
    // arriba al menú del títol (info::ctx.num_piramide == 0).
    static bool credits_triggered = false;
    if (!credits_triggered && info::ctx.num_piramide == 0) {
        if (Options::game.show_title_credits) {
            Overlay::startCredits();
        }
        credits_triggered = true;
    }

    // Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
    if (esc_blocked_ && !Overlay::isEscConsumed()) {
        esc_blocked_ = false;
    }

    // Avança l'escena (si no estem pausats). En pausa, es manté l'escena
    // congelada i re-presentem l'últim frame amb l'overlay fresc per
    // damunt.
    if (!paused_) {
        // Transicions: si l'escena actual ha acabat (o s'ha senyalat
        // quit), llegim el seu next state i la destruïm per crear la
        // següent a continuació.
        if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
            game_state_ = current_scene_->nextState();
            current_scene_.reset();
        }

        // Si no hi ha escena activa, construeix la pròxima segons
        // game_state_ i info::ctx. Si és impossible (game_state_ == -1,
        // quit, o state no registrat), eixim del loop.
        if (!current_scene_) {
            if (game_state_ == -1 || Jg::quitting()) {
                return false;
            }
            current_scene_ = createNextScene();
            if (!current_scene_) {
                return false;
            }
            current_scene_->onEnter();
            last_tick_ms_ = SDL_GetTicks();
        }

        // Tick de l'escena. Ji::update refresca key_pressed/any_key; el
        // delta_ms és el temps real transcorregut des de l'últim tick.
        Ji::update();
        const Uint32 now = SDL_GetTicks();
        const int delta_ms = static_cast<int>(now - last_tick_ms_);
        last_tick_ms_ = now;
        current_scene_->tick(delta_ms);

        // Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
        // actual. Jd8::flip ja no fa yield (Phase B.2 eliminà els fibers);
        // ara només omple el framebuffer perquè el Director l'aprofite.
        Jd8::flip();
        std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
        has_frame_ = true;
    }

    // Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
    if (has_frame_) {
        std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
        Screen::get()->present(presentation_buffer_);
    }

    // Límit de framerate segons VSync.
    // Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
    // emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
    // que es basa en el timing intern de SDL en lloc del delay explícit.
    const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
    const Uint32 elapsed = SDL_GetTicks() - frame_start;
    if (elapsed < target_ms) {
        SDL_Delay(target_ms - elapsed);
    }

    return true;
}

void Director::teardown() {
    // Senyal de quit i descàrrega ordenada de l'escena en curs. Els
    // destructors de cada escena són no-bloquejants — ja no fan fades
    // bloquejants. La resta de cleanup la gestiona `destroy()`.
    Jg::quitSignal();
    current_scene_.reset();
}

void Director::run() {
    setup();
    while (true) {
        pollAllEvents();
        if (!iterate()) {
            break;
        }
    }
    teardown();
}

void Director::pollAllEvents() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        handleEvent(event);
    }
}

void Director::handleEvent(const SDL_Event& event) {
    if (event.type == SDL_EVENT_QUIT) {
        Jg::quitSignal();
        requestQuit();
    }
    // Hot-plug de gamepad (a Emscripten els dispositius web entren com
    // JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
    if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
        event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
        Gamepad::handleEvent(event);
        return;
    }
    // Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
    if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
        event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
        menu_keys_held_[event.key.scancode] = false;
        return;
    }
    // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
    if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
        Menu::captureKey(event.key.scancode);
        menu_keys_held_[event.key.scancode] = true;
        return;
    }
    // Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
    // No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
    // superior dreta (pintat per Overlay) ja comunica l'estat.
    if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
        event.key.scancode == KeyConfig::scancode("pause_toggle")) {
        togglePause();
        menu_keys_held_[event.key.scancode] = true;
        return;
    }
    // Menú: F12 (o tecla configurada) obre/tanca el menú flotant
    if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
        event.key.scancode == KeyConfig::scancode("menu_toggle")) {
        Menu::toggle();
        Ji::setInputBlocked(Menu::isOpen());
        menu_keys_held_[event.key.scancode] = true;
        return;
    }
    // Si el menú està obert, consumeix tot l'input de teclat
    if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
        if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
            Menu::close();
            Ji::setInputBlocked(false);
            // Empassa l'ESC fins al release perquè el joc no la veja per polling
            esc_swallow_until_release_ = true;
        } else {
            Menu::handleKey(event.key.scancode);
            // El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
            if (!Menu::isOpen()) {
                Ji::setInputBlocked(false);
            }
        }
        menu_keys_held_[event.key.scancode] = true;
        return;
    }
    if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
        return;  // no deixem passar KEY_UP al joc tampoc
    }
    // Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
    // del toggle del menú/pausa i del handling del menú obert — així F12 i
    // SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
    // navegació per dins del menú tampoc els anul·la.
    if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
        Overlay::cancelCredits();
        return;
    }
    // Allibera el bloqueig d'ESC quan l'usuari la deixa anar
    if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
        esc_swallow_until_release_ = false;
        return;
    }
    // ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
        esc_blocked_ = true;  // Bloqueja ESC per polling immediatament
        if (!Overlay::isEscConsumed()) {
            // Primera pulsació: mostra notificació
            Overlay::handleEscape();
        } else {
            // Segona pulsació: senyal d'eixida al joc
            esc_blocked_ = false;
            key_pressed_ = true;
            Jg::quitSignal();
            // Si estem en pausa, la desactivem: el fiber del joc està
            // congelat i necessita ser reprès per veure la senyal de
            // quit i poder tornar de forma natural.
            paused_ = false;
        }
        return;  // no processa més aquest event
    }
    if (event.type == SDL_EVENT_KEY_UP) {
        if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
            // Ja processat a KEY_DOWN, només deixem netejar el bloqueig
            // quan l'overlay faça timeout
            return;
        }  // Comprova si és una tecla d'UI registrada (no passa al joc).
        // KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
        // incloent pause_toggle i menu_toggle (defensa en profunditat:
        // aquestes ja s'haurien hagut de menjar al swallow d'amunt).
        const auto sc = event.key.scancode;
        if (!KeyConfig::isGuiKey(sc)) {
            key_pressed_ = true;
            Ji::moveCheats(sc);
        }
    }
    Mouse::handleEvent(event);
}

void Director::requestQuit() {
    quit_requested_ = true;
    Jg::quitSignal();
}

void Director::requestRestart() {
    restart_requested_ = true;
}

auto Director::consumeKeyPressed() -> bool {
    return key_pressed_.exchange(false);
}
