#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;
}

void Director::applyRestart() {
    restart_requested_ = false;
    Audio::get()->stopMusic();
    Audio::get()->stopAllSounds();
    initGameContext();
    Info::ctx.num_piramide = 255;
    current_scene_.reset();
    game_state_ = 1;
    has_frame_ = false;
    Menu::close();
    Ji::setInputBlocked(false);
}

void Director::maybeStartTitleCredits() {
    static bool credits_triggered_ = false;
    if (credits_triggered_ || Info::ctx.num_piramide != 0) {
        return;
    }
    if (Options::game.show_title_credits) {
        Overlay::startCredits();
    }
    credits_triggered_ = true;
}

auto Director::tickActiveScene() -> bool {
    if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
        game_state_ = current_scene_->nextState();
        current_scene_.reset();
    }
    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();
    }

    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);

    Jd8::flip();
    std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
    has_frame_ = true;
    return true;
}

auto Director::iterate() -> bool {
    if (quit_requested_) {
        Jg::quitSignal();
        current_scene_.reset();
        return false;
    }
    if (restart_requested_) {
        applyRestart();
    }
    if (!context_initialized_) {
        initGameContext();
        context_initialized_ = true;
    }

    constexpr Uint32 FRAME_MS_VSYNC = 16;
    constexpr Uint32 FRAME_MS_NO_VSYNC = 4;
    const Uint32 FRAME_START = SDL_GetTicks();

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

    maybeStartTitleCredits();

    if (esc_blocked_ && !Overlay::isEscConsumed()) {
        esc_blocked_ = false;
    }

    if (!paused_) {
        if (!tickActiveScene()) {
            return false;
        }
    }

    if (has_frame_) {
        std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
        Screen::get()->present(presentation_buffer_);
    }

    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);
    }
}

auto Director::handleMenuEvent(const SDL_Event& event) -> bool {
    // Empassar-se el KEY_UP d'una 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 true;
    }
    const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
    // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot.
    if (Menu::isCapturing() && KEY_DOWN) {
        Menu::captureKey(event.key.scancode);
        menu_keys_held_[event.key.scancode] = true;
        return true;
    }
    // Pausa / menú toggle.
    if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) {
        togglePause();
        menu_keys_held_[event.key.scancode] = true;
        return true;
    }
    if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) {
        Menu::toggle();
        Ji::setInputBlocked(Menu::isOpen());
        menu_keys_held_[event.key.scancode] = true;
        return true;
    }
    // Si el menú està obert, consumeix tot l'input de teclat.
    if (Menu::isOpen() && KEY_DOWN) {
        if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
            Menu::close();
            Ji::setInputBlocked(false);
            esc_swallow_until_release_ = true;
        } else {
            Menu::handleKey(event.key.scancode);
            if (!Menu::isOpen()) {
                Ji::setInputBlocked(false);
            }
        }
        menu_keys_held_[event.key.scancode] = true;
        return true;
    }
    if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
        return true;
    }
    return false;
}

auto Director::handleEscapeEvent(const SDL_Event& event) -> bool {
    // Salta els crèdits amb qualsevol tecla que arribe al joc.
    if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
        Overlay::cancelCredits();
        return true;
    }
    // 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 true;
    }
    // ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida.
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
        esc_blocked_ = true;
        if (!Overlay::isEscConsumed()) {
            Overlay::handleEscape();
        } else {
            esc_blocked_ = false;
            key_pressed_ = true;
            Jg::quitSignal();
            paused_ = false;
        }
        return true;
    }
    return false;
}

void Director::handleEvent(const SDL_Event& event) {
    if (event.type == SDL_EVENT_QUIT) {
        Jg::quitSignal();
        requestQuit();
    }
    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;
    }
    if (handleMenuEvent(event)) {
        return;
    }
    if (handleEscapeEvent(event)) {
        return;
    }
    if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) {
        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);
}
