#include "core/rendering/screen.hpp"

#include <algorithm>
#include <cstdio>
#include <iostream>

#include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#endif
#include "game/defines.hpp"
#include "game/options.hpp"
#include "utils/utils.hpp"

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>

// --- Fix per a fullscreen/resize en Emscripten ---
//
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
// anterior i el viewport queda desencuadrat.
//
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
// del event loop (el canvas encara no està estable en el moment del callback)
// i re-sincronitzar SDL cridant SDL_SetWindowFullscreen + applyFallbackPresentation.
// La crida interna a SDL_SetWindowFullscreen és la peça que realment fa
// resincronitzar l'estat intern de SDL — sense això la logical presentation
// no encaixa amb el canvas real.
namespace {
    Screen* g_screen_instance = nullptr;

    void deferredCanvasResize(void* /*userData*/) {
        if (g_screen_instance != nullptr) {
            g_screen_instance->handleCanvasResized();
        }
    }

    EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent* event, void* /*userData*/) {
        if (g_screen_instance != nullptr && event != nullptr) {
            g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
        }
        emscripten_async_call(deferredCanvasResize, nullptr, 0);
        return EM_FALSE;
    }

    EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*userData*/) {
        emscripten_async_call(deferredCanvasResize, nullptr, 0);
        return EM_FALSE;
    }
}  // namespace
#endif  // __EMSCRIPTEN__

std::unique_ptr<Screen> Screen::instance;

void Screen::init() {
    instance = std::unique_ptr<Screen>(new Screen());
}

void Screen::destroy() {
    instance.reset();
}

auto Screen::get() -> Screen* {
    return instance.get();
}

Screen::Screen() {
    // Carrega opcions guardades
    zoom_ = Options::window.zoom;
    fullscreen_ = Options::window.fullscreen;

    calculateMaxZoom();

    zoom_ = std::max(zoom_, 1);
    zoom_ = std::min(zoom_, max_zoom_);

    // Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
    // ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
    // canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
    // de monitor), baixem al màxim suportat.
    Options::video.internal_resolution = std::max(Options::video.internal_resolution, 1);
    Options::video.internal_resolution = std::min(Options::video.internal_resolution, max_zoom_);

    int w = GAME_WIDTH * zoom_;
    int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;

    window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
    renderer_ = SDL_CreateRenderer(window_, nullptr);

    texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
    applyFallbackPresentation();

    // Inicialitza backend GPU si l'acceleració està activada
    initShaders();

    std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";

#ifdef __EMSCRIPTEN__
    // IMPORTANT: NO registrem resize callback genèric. En mòbil, fer scroll
    // fa que el navegador oculti/mostri la barra d'URL, disparant un resize
    // del DOM per cada scroll. Això portaria a re-aplicar logical presentation
    // per cada scroll i corrompria el viewport intern de SDL.
    g_screen_instance = this;
    emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
    emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}

Screen::~Screen() {
#ifdef __EMSCRIPTEN__
    g_screen_instance = nullptr;
#endif

    // Guarda opcions abans de destruir
    Options::window.zoom = zoom_;
    Options::window.fullscreen = fullscreen_;

    // Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
    if (shader_backend_) {
#ifndef NO_SHADERS
        auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
        if (gpu != nullptr) {
            gpu->destroy();
        }
#endif
        shader_backend_.reset();
    }

    if (internal_texture_sdl_ != nullptr) {
        SDL_DestroyTexture(internal_texture_sdl_);
    }
    if (texture_ != nullptr) {
        SDL_DestroyTexture(texture_);
    }
    if (renderer_ != nullptr) {
        SDL_DestroyRenderer(renderer_);
    }
    if (window_ != nullptr) {
        SDL_DestroyWindow(window_);
    }
}

void Screen::initShaders() {
#ifdef NO_SHADERS
    // Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està
    // disponible). Es salta tota la inicialització — shader_backend_ es
    // queda nul·lptr i tots els `if (shader_backend_)` del render path
    // curtcircuiten cap al fallback SDL_Renderer.
    return;
#else
    if (!Options::video.gpu_acceleration) {
        return;
    }

    shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();

    const std::string FALLBACK_DRIVER = "none";
    shader_backend_->setPreferredDriver(
        Options::video.gpu_acceleration ? "" : FALLBACK_DRIVER);

    // init() rep la finestra i la textura (la textura s'usa com a referència, el GPU fa uploadPixels)
    if (!shader_backend_->init(window_, texture_, "", "")) {
        std::cerr << "GPU shader backend initialization failed, using SDL_Renderer fallback\n";
        shader_backend_.reset();
        return;
    }

    gpu_driver_ = shader_backend_->getDriverName();
    std::cout << "GPU driver: " << gpu_driver_ << '\n';

    // Aplica opcions de vídeo
    shader_backend_->setScalingMode(Options::video.scaling_mode);
    shader_backend_->setVSync(Options::video.vsync);
    shader_backend_->setTextureFilter(Options::video.texture_filter);
    shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
    shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);

    if (Options::video.supersampling) {
        shader_backend_->setOversample(3);
    }

    shader_backend_->setInternalResolution(Options::video.internal_resolution);

    // Resol el shader actiu des del config
    if (Options::video.current_shader == "crtpi") {
        shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
    } else {
        shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
    }

    // Resol presets per nom
    for (int i = 0; i < static_cast<int>(Options::postfx_presets.size()); i++) {
        if (Options::postfx_presets[i].name == Options::video.current_postfx_preset) {
            Options::current_postfx_preset = i;
            break;
        }
    }
    for (int i = 0; i < static_cast<int>(Options::crtpi_presets.size()); i++) {
        if (Options::crtpi_presets[i].name == Options::video.current_crtpi_preset) {
            Options::current_crtpi_preset = i;
            break;
        }
    }

    applyCurrentPostFXPreset();
    applyCurrentCrtPiPreset();
#endif
}

void Screen::present(Uint32* pixel_data) {
    fps_.increment();
    fps_.calculate(SDL_GetTicks());
    updateRenderInfo();
    Overlay::render(pixel_data);

    if (shader_backend_ && shader_backend_->isHardwareAccelerated() && Options::video.shader_enabled) {
        // Path GPU: puja els píxels i renderitza amb shaders
        shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
        shader_backend_->render();
    } else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
        // GPU activa però shaders desactivats: renderitza net (sense efectes).
        // Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI,
        // els seus efectes (scanlines, curvatura) seguirien aplicant-se encara
        // que shader_enabled sigui false. Restaurem l'actiu al final per a
        // no trencar la selecció de l'usuari.
        Rendering::PostFXParams clean{};
        shader_backend_->setPostFXParams(clean);
        const auto PREV_SHADER = shader_backend_->getActiveShader();
        if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
            shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
        }
        shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
        shader_backend_->render();
        if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
            shader_backend_->setActiveShader(PREV_SHADER);
        }
    } else {
        // Fallback SDL_Renderer. A MULT=1, flux directe original: logical
        // presentation (setada per applyFallbackPresentation) + scale mode de
        // texture_ segons l'opció. A MULT>1, la còpia intermèdia crea la
        // font ampliada (NN via GPU), i es presenta via logical presentation
        // a la mida de la font intermèdia.
        SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));

        const int MULT = Options::video.internal_resolution;
        if (MULT > 1) {
            ensureFallbackInternalTexture();
            if (internal_texture_sdl_ != nullptr) {
                // Còpia NN a la textura intermèdia (MULT·game). Sampler NN
                // per construcció: volem píxels grans i nets.
                SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
                SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
                SDL_RenderClear(renderer_);
                SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
                SDL_SetRenderTarget(renderer_, nullptr);

                // Filtre global al pas final → finestra (via logical presentation
                // que applyFallbackPresentation ja configura amb mida game·MULT).
                SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
                    ? SDL_SCALEMODE_LINEAR
                    : SDL_SCALEMODE_NEAREST;
                SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale);
                SDL_RenderClear(renderer_);
                SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr);
                SDL_RenderPresent(renderer_);
                return;
            }
            // Si la creació de la textura intermèdia ha fallat, caiem al path normal.
        }
        // MULT=1 (o fallback-del-fallback): texture_ directament. El scale mode
        // el manté applyFallbackPresentation — però el re-apliquem per si la
        // ruta MULT>1 el va sobreescriure anteriorment.
        SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
            ? SDL_SCALEMODE_LINEAR
            : SDL_SCALEMODE_NEAREST;
        SDL_SetTextureScaleMode(texture_, direct_scale);
        SDL_RenderClear(renderer_);
        SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
        SDL_RenderPresent(renderer_);
    }
}

void Screen::toggleFullscreen() {
    fullscreen_ = !fullscreen_;
    SDL_SetWindowFullscreen(window_, fullscreen_);
    if (!fullscreen_) {
        adjustWindowSize();
    }
}

void Screen::incZoom() {
    if (fullscreen_ || zoom_ >= max_zoom_) {
        return;
    }
    zoom_++;
    adjustWindowSize();
}

void Screen::decZoom() {
    if (fullscreen_ || zoom_ <= 1) {
        return;
    }
    zoom_--;
    adjustWindowSize();
}

void Screen::setZoom(int zoom) {
    if (zoom < 1 || zoom > max_zoom_ || fullscreen_) {
        return;
    }
    zoom_ = zoom;
    adjustWindowSize();
}

void Screen::toggleShaders() {
    Options::video.shader_enabled = !Options::video.shader_enabled;
    if (Options::video.shader_enabled) {
        applyCurrentPostFXPreset();
    }
}

auto Screen::toggleSupersampling() -> bool {
    // SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
    // i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
    // submostreig intern i no usa aquesta via).
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return false;
    }
    if (!Options::video.shader_enabled) {
        return false;
    }
    if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) {
        return false;
    }
    Options::video.supersampling = !Options::video.supersampling;
    shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
    return true;
}

void Screen::toggleAspectRatio() {
    Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
    if (shader_backend_) {
        shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
    } else {
        applyFallbackPresentation();
    }
    if (!fullscreen_) {
        adjustWindowSize();
    }
}

void Screen::cycleScalingMode(int dir) {
    constexpr int N = 5;  // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER
    int cur = static_cast<int>(Options::video.scaling_mode);
    int step = (dir >= 0) ? 1 : -1;
    cur = ((cur + step) % N + N) % N;
    Options::video.scaling_mode = static_cast<Options::ScalingMode>(cur);
    if (shader_backend_) {
        shader_backend_->setScalingMode(Options::video.scaling_mode);
    } else {
        applyFallbackPresentation();
    }
}

void Screen::toggleVSync() {
    Options::video.vsync = !Options::video.vsync;
    if (shader_backend_) {
        shader_backend_->setVSync(Options::video.vsync);
    }
}

void Screen::cycleTextureFilter(int dir) {
    // NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar)
    (void)dir;
    Options::video.texture_filter =
        (Options::video.texture_filter == Options::TextureFilter::LINEAR)
        ? Options::TextureFilter::NEAREST
        : Options::TextureFilter::LINEAR;
    if (shader_backend_) {
        shader_backend_->setTextureFilter(Options::video.texture_filter);
    } else {
        applyFallbackPresentation();
    }
}

void Screen::changeInternalResolution(int dir) {
    int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
    next = std::max(next, 1);
    next = std::min(next, max_zoom_);
    if (next == Options::video.internal_resolution) {
        return;
    }
    Options::video.internal_resolution = next;

    // Propaga al backend actiu. Al fallback path, la textura es recrea al
    // pròxim present via ensureFallbackInternalTexture.
    if (shader_backend_) {
        shader_backend_->setInternalResolution(next);
    } else {
        applyFallbackPresentation();
    }
}

auto Screen::nextShaderType() -> bool {
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return false;
    }
    if (!Options::video.shader_enabled) {
        return false;
    }

    if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
        shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
        Options::video.current_shader = "crtpi";
        applyCurrentCrtPiPreset();
    } else {
        shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
        Options::video.current_shader = "postfx";
        applyCurrentPostFXPreset();
    }
    return true;
}

auto Screen::nextPreset() -> bool {
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return false;
    }
    if (!Options::video.shader_enabled) {
        return false;
    }

    if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
        if (Options::postfx_presets.empty()) {
            return false;
        }
        Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
        Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
        applyCurrentPostFXPreset();
    } else {
        if (Options::crtpi_presets.empty()) {
            return false;
        }
        Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
        Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
        applyCurrentCrtPiPreset();
    }
    return true;
}

auto Screen::prevShaderType() -> bool {
    // Només dues opcions — prev == next
    return nextShaderType();
}

auto Screen::prevPreset() -> bool {
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return false;
    }
    if (!Options::video.shader_enabled) {
        return false;
    }

    if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
        if (Options::postfx_presets.empty()) {
            return false;
        }
        int n = static_cast<int>(Options::postfx_presets.size());
        Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
        Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
        applyCurrentPostFXPreset();
    } else {
        if (Options::crtpi_presets.empty()) {
            return false;
        }
        int n = static_cast<int>(Options::crtpi_presets.size());
        Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
        Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
        applyCurrentCrtPiPreset();
    }
    return true;
}

auto Screen::getCurrentPresetName() const -> const char* {
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return "---";
    }
    if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
        if (Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size())) {
            return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
        }
    } else {
        if (Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
            return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
        }
    }
    return "---";
}

void Screen::setActiveShader(Rendering::ShaderType type) {
    if (shader_backend_) {
        shader_backend_->setActiveShader(type);
    }
}

void Screen::applyCurrentPostFXPreset() {
    if (!shader_backend_ || Options::postfx_presets.empty()) {
        return;
    }
    const auto& preset = Options::postfx_presets[Options::current_postfx_preset];
    Rendering::PostFXParams p;
    p.vignette = preset.vignette;
    p.scanlines = preset.scanlines;
    p.chroma = preset.chroma;
    p.mask = preset.mask;
    p.gamma = preset.gamma;
    p.curvature = preset.curvature;
    p.bleeding = preset.bleeding;
    p.flicker = preset.flicker;
    shader_backend_->setPostFXParams(p);
}

void Screen::applyCurrentCrtPiPreset() {
    if (!shader_backend_ || Options::crtpi_presets.empty()) {
        return;
    }
    const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset];
    Rendering::CrtPiParams p;
    p.scanline_weight = preset.scanline_weight;
    p.scanline_gap_brightness = preset.scanline_gap_brightness;
    p.bloom_factor = preset.bloom_factor;
    p.input_gamma = preset.input_gamma;
    p.output_gamma = preset.output_gamma;
    p.mask_brightness = preset.mask_brightness;
    p.curvature_x = preset.curvature_x;
    p.curvature_y = preset.curvature_y;
    p.mask_type = preset.mask_type;
    p.enable_scanlines = preset.enable_scanlines;
    p.enable_multisample = preset.enable_multisample;
    p.enable_gamma = preset.enable_gamma;
    p.enable_curvature = preset.enable_curvature;
    p.enable_sharper = preset.enable_sharper;
    shader_backend_->setCrtPiParams(p);
}

auto Screen::isHardwareAccelerated() const -> bool {
    return shader_backend_ && shader_backend_->isHardwareAccelerated();
}

auto Screen::getActiveShaderName() const -> const char* {
    if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
        return "SENSE GPU";
    }
    return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI";
}

void Screen::updateRenderInfo() {
    static const Uint32 START_TICKS = SDL_GetTicks();
    std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);

    // Segment 0: FPS + driver (sempre visible)
    std::string fps_driver = std::to_string(fps_.last_value) + " fps - " + driver;

    // Segment 1: shader + preset (només si shaders actius)
    std::string shader_seg;
    if (Options::video.shader_enabled) {
        shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName());
    }

    // Segment 2: supersampling indicator
    const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr;

    // Segment 3: hora (només si show_time)
    char time_buf[32] = {0};
    if (Options::render_info.show_time) {
        Uint32 elapsed = SDL_GetTicks() - START_TICKS;
        int minutes = elapsed / 60000;
        int seconds = (elapsed / 1000) % 60;
        int centis = (elapsed / 10) % 100;
        snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
    }

    // Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien
    // contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no
    Overlay::setRenderInfoSegments(
        fps_driver.c_str(),
        shader_seg.empty() ? nullptr : shader_seg.c_str(),
        ss_seg,
        (time_buf[0] != 0) ? time_buf : nullptr,
        0b1001);
}

void Screen::applyFallbackPresentation() {
    // Fallback SDL_Renderer (p.ex. emscripten/WebGL2 sense shaders GPU).
    // Filtre global (texture_filter) s'aplica sempre, independent de 4:3.
    SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
        ? SDL_SCALEMODE_LINEAR
        : SDL_SCALEMODE_NEAREST;
    if (texture_ != nullptr) {
        SDL_SetTextureScaleMode(texture_, scale);
    }

    // Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
    // l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans
    // d'escollir el mode de finestra; en fallback no tenim eixa capa intermèdia).
    SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
    if (Options::video.aspect_ratio_4_3) {
        mode = SDL_LOGICAL_PRESENTATION_STRETCH;
    } else {
        switch (Options::video.scaling_mode) {
            case Options::ScalingMode::DISABLED:
                mode = SDL_LOGICAL_PRESENTATION_DISABLED;
                break;
            case Options::ScalingMode::STRETCH:
                mode = SDL_LOGICAL_PRESENTATION_STRETCH;
                break;
            case Options::ScalingMode::LETTERBOX:
                mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
                break;
            case Options::ScalingMode::OVERSCAN:
                mode = SDL_LOGICAL_PRESENTATION_OVERSCAN;
                break;
            case Options::ScalingMode::INTEGER:
                mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
                break;
        }
    }
    // Amb resolució interna N > 1, la mida lògica creix proporcionalment
    // perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
    const int MULT = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
    SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * MULT, GAME_HEIGHT * MULT, mode);
}

void Screen::ensureFallbackInternalTexture() {
    if (renderer_ == nullptr) {
        return;
    }
    const int MULT = Options::video.internal_resolution;
    if (MULT <= 1) {
        // No cal textura intermèdia — recicla si la teníem.
        if (internal_texture_sdl_ != nullptr) {
            SDL_DestroyTexture(internal_texture_sdl_);
            internal_texture_sdl_ = nullptr;
            internal_texture_mult_ = 0;
        }
        return;
    }
    if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == MULT) {
        return;
    }

    if (internal_texture_sdl_ != nullptr) {
        SDL_DestroyTexture(internal_texture_sdl_);
        internal_texture_sdl_ = nullptr;
    }
    internal_texture_sdl_ = SDL_CreateTexture(renderer_,
        SDL_PIXELFORMAT_ABGR8888,
        SDL_TEXTUREACCESS_TARGET,
        GAME_WIDTH * MULT,
        GAME_HEIGHT * MULT);
    if (internal_texture_sdl_ == nullptr) {
        std::cerr << "Screen: failed to create fallback internal texture (×" << MULT << "): "
                  << SDL_GetError() << '\n';
        internal_texture_mult_ = 0;
        return;
    }
    internal_texture_mult_ = MULT;
}

void Screen::adjustWindowSize() {
    int w = GAME_WIDTH * zoom_;
    // Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2)
    int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
    SDL_SetWindowSize(window_, w, h);
    SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}

void Screen::calculateMaxZoom() {
    SDL_DisplayID display = SDL_GetPrimaryDisplay();
    const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
    if (mode != nullptr) {
        int max_w = mode->w / GAME_WIDTH;
        int max_h = mode->h / GAME_HEIGHT;
        max_zoom_ = (max_w < max_h) ? max_w : max_h;
        max_zoom_ = std::max(max_zoom_, 1);
    }
}

#ifdef __EMSCRIPTEN__
// ============================================================================
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
// ============================================================================

void Screen::handleCanvasResized() {
    if (window_ == nullptr) return;
    // Re-sincronitza l'estat intern de SDL amb el canvas HTML real. La crida
    // a SDL_SetWindowFullscreen és l'única manera de forçar SDL a reconèixer
    // la mida actual del canvas; després re-apliquem la logical presentation
    // (el path WASM sempre va pel fallback SDL_Renderer, sense shaders GPU).
    SDL_SetWindowFullscreen(window_, fullscreen_);
    applyFallbackPresentation();
}

void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) {
    fullscreen_ = is_fullscreen;
    Options::window.fullscreen = is_fullscreen;
}
#endif
