#include "core/rendering/screen.hpp"

#include <SDL3/SDL.h>  // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_SyncWindow, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif

#include <algorithm>  // Para min, max
#include <cstring>    // Para memcpy
#include <iostream>   // Para std::cout
#include <memory>     // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string>     // Para basic_string, operator+, char_traits, to_string, string
#include <vector>     // Para vector

#include "core/input/mouse.hpp"  // Para updateCursorVisibility
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"  // Para SDL3GPUShader
#endif
#include "core/rendering/text.hpp"      // Para Text
#include "core/rendering/texture.hpp"   // Para Texture
#include "core/resources/asset.hpp"     // Para Asset
#include "core/resources/resource.hpp"  // Para Resource
#include "core/system/director.hpp"     // Para Director::debug_config
#include "game/options.hpp"             // Para Video, video, Window, window
#include "game/ui/notifier.hpp"         // Para Notifier
#include "game/ui/service_menu.hpp"     // Para ServiceMenu
#include "utils/param.hpp"              // Para Param, param, ParamGame, ParamDebug
#include "utils/utils.hpp"              // Para toLower

// Singleton
Screen* Screen::instance = nullptr;

#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauración del canvas en wasm/Emscripten
// ============================================================================
// SDL3 + Emscripten no notifica de forma fiable los cambios de estado del
// canvas HTML (fullscreen exit vía Esc, rotación del dispositivo, etc.).
// Registramos callbacks nativos de Emscripten que delegan en
// Screen::handleCanvasResized(), el cual re-aplica el modo de fullscreen y
// reajusta la ventana para que SDL salga de su estado interno de fullscreen.
//
// Los callbacks difieren el trabajo con emscripten_async_call(0ms) porque el
// navegador todavía no ha acabado de redimensionar el canvas cuando el evento
// se dispara; posponer al siguiente tick garantiza valores estables.
//
// Referencias: libsdl-org/SDL#13300, libsdl-org/SDL#11389.
// ============================================================================
namespace {
    void deferredCanvasResize(void* /*user_data*/) {
        if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
    }

    auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
        // Sincronizamos Options::video.fullscreen con el estado real del navegador
        // antes de diferir la restauración: cuando el usuario sale con Esc no pasa
        // por toggleFullscreen() y el estado interno quedaría desincronizado.
        Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
        emscripten_async_call(deferredCanvasResize, nullptr, 0);
        return EM_FALSE;
    }

    auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
        emscripten_async_call(deferredCanvasResize, nullptr, 0);
        return EM_FALSE;
    }
}  // namespace
#endif

// Inicializa la instancia única del singleton
void Screen::init() {
    Screen::instance = new Screen();
    Screen::initShaders();  // Llamar aquí para que Screen::get() ya devuelva la instancia
}

// Libera la instancia
void Screen::destroy() { delete Screen::instance; }

// Obtiene la instancia
auto Screen::get() -> Screen* { return Screen::instance; }

// Constructor
Screen::Screen()
    : window_(nullptr),
      renderer_(nullptr),
      game_canvas_(nullptr),
      service_menu_(nullptr),
      notifier_(nullptr),
      src_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
      dst_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}) {
    // Arranca SDL VIDEO, crea la ventana y el renderizador
    initSDLVideo();

    // Crea la textura de destino
    game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
    SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);

    // Inicializar buffer de píxeles para SDL3GPU
    pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));

    // Crea el objeto de texto
    createText();

#ifdef _DEBUG
    debug_info_.text = text_;
    setDebugInfoEnabled(Director::debug_config.show_render_info);
#endif

    // Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
    SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);

    // Aplicar la configuración inicial completa (vsync + logical presentation +
    // fullscreen + tamaño de ventana). En Emscripten es necesario porque el
    // canvas HTML tiene un tamaño propio y SDL_CreateWindow solo no basta para
    // que SDL sincronice su viewport interno con el canvas real: sin este
    // applySettings el canvas inicial sale descolocado con barras negras a los
    // lados y el juego pequeño en el centro hasta el primer toggle de fullscreen.
    applySettings();

    // Limpiar renderer
    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
    SDL_RenderClear(renderer_);
    SDL_RenderPresent(renderer_);
}

// Destructor
Screen::~Screen() {
    SDL_DestroyTexture(game_canvas_);
    SDL_DestroyRenderer(renderer_);
    SDL_DestroyWindow(window_);
}

// Limpia la pantalla
void Screen::clean(Color color) {
    SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
    SDL_RenderClear(renderer_);
}

// Prepara para empezar a dibujar en la textura de juego
void Screen::start() { SDL_SetRenderTarget(renderer_, game_canvas_); }

// Vuelca el contenido del renderizador en pantalla
void Screen::render() {
    fps_.increment();
    renderOverlays();  // Renderiza todos los overlays y efectos
    renderPresent();   // Renderiza el contenido del game_canvas_
}

// Vuelca el contenido del renderizador en pantalla exceptuando ciertas partes
void Screen::coreRender() {
    /*fps_.increment();
#ifdef _DEBUG
    renderInfo();
#endif*/
    renderPresent();  // Renderiza el contenido del game_canvas_
}

// Renderiza el contenido del game_canvas_
void Screen::renderPresent() {
#ifndef NO_SHADERS
    if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
        // Leer píxeles de game_canvas_ con la API SDL3 (devuelve SDL_Surface*)
        SDL_SetRenderTarget(renderer_, game_canvas_);
        SDL_Surface* surface = SDL_RenderReadPixels(renderer_, nullptr);
        if (surface != nullptr) {
            if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
                std::memcpy(pixel_buffer_.data(), surface->pixels, pixel_buffer_.size() * sizeof(Uint32));
            } else {
                SDL_Surface* converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
                if (converted != nullptr) {
                    std::memcpy(pixel_buffer_.data(), converted->pixels, pixel_buffer_.size() * sizeof(Uint32));
                    SDL_DestroySurface(converted);
                }
            }
            SDL_DestroySurface(surface);
        }
        SDL_SetRenderTarget(renderer_, nullptr);
        // Subir a GPU y presentar con PostFX
        shader_backend_->uploadPixels(pixel_buffer_.data(), param.game.width, param.game.height);
        shader_backend_->render();
        return;
    }
#endif
    // Fallback: SDL_Renderer
    SDL_SetRenderTarget(renderer_, nullptr);
    clean();
    SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
    SDL_RenderPresent(renderer_);
}

// Establece el modo de video
void Screen::setFullscreenMode() {
    SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
}

// Cambia entre pantalla completa y ventana. Usamos applySettings en vez de
// setFullscreenMode porque applySettings también re-aplica la logical
// presentation — sin eso, al entrar en fullscreen SDL no recalcula el viewport
// y el juego se ve pequeño (especialmente en Android).
void Screen::toggleFullscreen() {
    Options::video.fullscreen = !Options::video.fullscreen;
    applySettings();
}

// Re-sincroniza SDL con el estado real del canvas del navegador. Lo invocan los
// callbacks nativos de Emscripten (vegeu el bloc al principi del fitxer) cuando
// se detecta un fullscreenchange o un orientationchange. Re-aplica fullscreen y
// reajusta la ventana porque SDL no emite SDL_EVENT_WINDOW_LEAVE_FULLSCREEN en
// Emscripten y su estado interno queda desincronizado al salir con Esc.
// Fuera de Emscripten es un no-op (desktop sí emite los events correctamente).
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
    // SDL_SetWindowFullscreen es imprescindible para sacar a SDL de su estado
    // interno de fullscreen cuando el usuario ha salido sin pasar por
    // toggleFullscreen (onEmFullscreenChange ya ha actualizado Options).
    SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
    SDL_SyncWindow(window_);
    adjustWindowSize();
#endif
}

// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
void Screen::registerEmscriptenEventCallbacks() {  // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
    emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
    emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}

// Cambia el tamaño de la ventana
void Screen::setWindowZoom(int zoom) {
    Options::window.zoom = zoom;
    adjustWindowSize();
}

// Reduce el tamaño de la ventana
auto Screen::decWindowSize() -> bool {
    if (!Options::video.fullscreen) {
        const int PREVIOUS_ZOOM = Options::window.zoom;
        --Options::window.zoom;
        Options::window.zoom = std::max(Options::window.zoom, 1);

        if (Options::window.zoom != PREVIOUS_ZOOM) {
            adjustWindowSize();
            return true;
        }
    }

    return false;
}

// Aumenta el tamaño de la ventana
auto Screen::incWindowSize() -> bool {
    if (!Options::video.fullscreen) {
        const int PREVIOUS_ZOOM = Options::window.zoom;
        ++Options::window.zoom;
        Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);

        if (Options::window.zoom != PREVIOUS_ZOOM) {
            adjustWindowSize();
            return true;
        }
    }

    return false;
}

// Recibe deltaTime de las secciones y actualiza la lógica
void Screen::update(float delta_time) {
    fps_.calculate(SDL_GetTicks());
    shake_effect_.update(src_rect_, dst_rect_, delta_time);
    flash_effect_.update(delta_time);
    if (service_menu_ != nullptr) {
        service_menu_->update(delta_time);
    }
    if (notifier_ != nullptr) {
        notifier_->update(delta_time);
    }
    Mouse::updateCursorVisibility();
}

// Actualiza los elementos mínimos
void Screen::coreUpdate() {
    fps_.calculate(SDL_GetTicks());
    Mouse::updateCursorVisibility();
}

// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::renderFlash() {
    if (flash_effect_.isRendarable()) {
        SDL_SetRenderDrawColor(renderer_, flash_effect_.color.r, flash_effect_.color.g, flash_effect_.color.b, 0xFF);
        SDL_RenderClear(renderer_);
    }
}

// Aplica el efecto de agitar la pantalla
void Screen::renderShake() {
    if (shake_effect_.enabled) {
        // Guarda el renderizador actual para dejarlo despues como estaba
        auto* current_target = SDL_GetRenderTarget(renderer_);

        // Crea una textura temporal
        auto* temp_texture = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);

        // Vuelca game_canvas_ a la textura temporal
        SDL_SetRenderTarget(renderer_, temp_texture);
        SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);

        // Vuelca textura temporal a game_canvas_
        SDL_SetRenderTarget(renderer_, game_canvas_);
        SDL_RenderTexture(renderer_, temp_texture, &src_rect_, &dst_rect_);

        // Elimina la textura temporal
        SDL_DestroyTexture(temp_texture);

        // Restaura el renderizador de destino original
        SDL_SetRenderTarget(renderer_, current_target);
    }
}
#ifdef _DEBUG
// Muestra información por pantalla
void Screen::renderInfo() const {
    if (debug_info_.show) {
        const Color GOLD(0xFF, 0xD7, 0x00);
        const Color GOLD_SHADOW = GOLD.DARKEN(150);

        // Construir texto: fps - driver - preset
        std::string info_text = std::to_string(fps_.last_value) + " fps";

        // Driver GPU
        if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
            const std::string DRIVER = shader_backend_->getDriverName();
            if (!DRIVER.empty()) {
                info_text += " - " + toLower(DRIVER);
            }
        } else {
            info_text += " - sdl";
        }

        // Shader + preset
        if (Options::video.shader.enabled) {
            if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
                const std::string PRESET_NAME = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset)).name;
                info_text += " - crtpi " + toLower(PRESET_NAME);
            } else {
                const std::string PRESET_NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
                info_text += " - postfx " + toLower(PRESET_NAME);
                if (Options::video.supersampling.enabled) { info_text += " (ss)"; }
            }
        }

        // Centrado arriba
        const int TEXT_WIDTH = debug_info_.text->length(info_text);
        const int X_POS = (static_cast<int>(param.game.width) - TEXT_WIDTH) / 2;
        debug_info_.text->writeDX(Text::COLOR | Text::STROKE, X_POS, 1, info_text, 1, GOLD, 1, GOLD_SHADOW);

#ifdef RECORDING
        const std::string REC_TEXT = "recording";
        const int REC_WIDTH = debug_info_.text->length(REC_TEXT);
        const int REC_X = (static_cast<int>(param.game.width) - REC_WIDTH) / 2;
        debug_info_.text->writeDX(Text::COLOR | Text::STROKE, REC_X, 1 + debug_info_.text->getCharacterSize(), REC_TEXT, 1, GOLD, 1, GOLD_SHADOW);
#endif
    }
}
#endif
// Inicializa shaders (SDL3GPU)
void Screen::initShaders() {
#ifndef NO_SHADERS
    auto* self = Screen::get();
    if (self == nullptr) {
        std::cout << "Screen::initShaders: instance is null, skipping" << '\n';
        return;
    }
    if (!self->shader_backend_) {
        self->shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
        const std::string FALLBACK_DRIVER = "none";
        self->shader_backend_->setPreferredDriver(
            Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
    }
    if (!self->shader_backend_->isHardwareAccelerated()) {
        const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", "");
        std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
    }
    if (self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
        self->shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
        self->shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
        self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);

        if (!Options::video.shader.enabled) {
            // Passthrough: POSTFX con parámetros a cero
            self->shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
            self->shader_backend_->setPostFXParams(Rendering::PostFXParams{});
        } else if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
            self->shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
            self->applyCurrentCrtPiPreset();
        } else {
            self->shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
            self->applyCurrentPostFXPreset();
        }
    }
#endif
}

// Calcula el tamaño de la ventana
void Screen::adjustWindowSize() {
    if (!Options::video.fullscreen) {
        // Establece el nuevo tamaño
        const int WIDTH = param.game.width * Options::window.zoom;
        const int HEIGHT = param.game.height * Options::window.zoom;

        int old_width;
        int old_height;
        SDL_GetWindowSize(window_, &old_width, &old_height);

        int old_pos_x;
        int old_pos_y;
        SDL_GetWindowPosition(window_, &old_pos_x, &old_pos_y);

        const int NEW_POS_X = old_pos_x + ((old_width - WIDTH) / 2);
        const int NEW_POS_Y = old_pos_y + ((old_height - HEIGHT) / 2);

        SDL_SetWindowPosition(window_, std::max(NEW_POS_X, WINDOWS_DECORATIONS), std::max(NEW_POS_Y, 0));
        SDL_SetWindowSize(window_, WIDTH, HEIGHT);
    }
}

// Renderiza todos los overlays y efectos
void Screen::renderOverlays() {
    // Dibuja efectos y elementos sobre el game_canvas_
    renderShake();
    renderFlash();
    renderAttenuate();
    service_menu_->render();
    notifier_->render();
#ifdef _DEBUG
    renderInfo();
#endif
}

// Atenua la pantalla
void Screen::renderAttenuate() {
    if (attenuate_effect_) {
        SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 64);
        SDL_RenderFillRect(renderer_, nullptr);
    }
}

// Arranca SDL VIDEO y crea la ventana
auto Screen::initSDLVideo() -> bool {
    // Inicializar SDL
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::cout << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
        return false;
    }

    // Obtener información de la pantalla
    getDisplayInfo();

    // Configurar hint para renderizado
#ifdef __APPLE__
    if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
        std::cout << "Warning: Failed to set Metal hint!" << '\n';
    }
#endif

    // Crear ventana
#ifdef __APPLE__
    SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
#else
    SDL_WindowFlags window_flags = 0;
#endif
    if (Options::video.fullscreen) {
        window_flags |= SDL_WINDOW_FULLSCREEN;
    }
    window_flags |= SDL_WINDOW_HIDDEN;
    window_ = SDL_CreateWindow(
        Options::window.caption.c_str(),
        param.game.width * Options::window.zoom,
        param.game.height * Options::window.zoom,
        window_flags);

    if (window_ == nullptr) {
        std::cout << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
        SDL_Quit();
        return false;
    }

    // Crear renderer
    renderer_ = SDL_CreateRenderer(window_, nullptr);
    if (renderer_ == nullptr) {
        std::cout << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
        SDL_DestroyWindow(window_);
        window_ = nullptr;
        SDL_Quit();
        return false;
    }

    // Configurar renderer
    SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
    SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);

    registerEmscriptenEventCallbacks();

    return true;
}

// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() {
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    if (displays != nullptr) {
        const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);

        // Guarda información del monitor en display_monitor_
        const char* first_display_name = SDL_GetDisplayName(displays[0]);
        display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
        display_monitor_.width = static_cast<int>(dm->w);
        display_monitor_.height = static_cast<int>(dm->h);
        display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);

        // Calcula el máximo factor de zoom que se puede aplicar a la pantalla
        Options::window.max_zoom = std::min(dm->w / param.game.width, dm->h / param.game.height);
        Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);

        // Obtiene la cadena con la información sobre la resolución y el refresco
        Options::video.info = std::to_string(dm->w) + "x" +
            std::to_string(dm->h) + " @ " +
            std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";

        // Calcula el máximo factor de zoom que se puede aplicar a la pantalla
        const int MAX_ZOOM = std::min(dm->w / param.game.width, (dm->h - WINDOWS_DECORATIONS) / param.game.height);

        // Normaliza los valores de zoom
        Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);

        SDL_free(displays);
    }
}

// Alterna activar/desactivar shaders
void Screen::toggleShaders() {
    Options::video.shader.enabled = !Options::video.shader.enabled;
    auto* self = Screen::get();
    if (self != nullptr) {
        if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
            self->applyCurrentCrtPiPreset();
        } else {
            self->applyCurrentPostFXPreset();
        }
    }
}

// Cambia entre PostFX y CrtPi
void Screen::nextShader() {
    auto* self = Screen::get();
    if (self == nullptr || !self->shader_backend_ || !self->shader_backend_->isHardwareAccelerated()) { return; }

    const auto NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
        ? Rendering::ShaderType::CRTPI
        : Rendering::ShaderType::POSTFX;

    Options::video.shader.current_shader = NEXT;
    self->shader_backend_->setActiveShader(NEXT);

    if (NEXT == Rendering::ShaderType::CRTPI) {
        self->applyCurrentCrtPiPreset();
    } else {
        self->applyCurrentPostFXPreset();
    }
}

// Avanza al siguiente preset PostFX
void Screen::nextPostFXPreset() {
    if (Options::postfx_presets.empty()) { return; }
    Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
    auto* self = Screen::get();
    if (self != nullptr) {
        self->applyCurrentPostFXPreset();
    }
}

// Avanza al siguiente preset CrtPi
void Screen::nextCrtPiPreset() {
    if (Options::crtpi_presets.empty()) { return; }
    Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
    auto* self = Screen::get();
    if (self != nullptr) {
        self->applyCurrentCrtPiPreset();
    }
}

// Alterna supersampling
void Screen::toggleSupersampling() {
    Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
    auto* self = Screen::get();
    if (self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
        self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
        if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
            self->applyCurrentCrtPiPreset();
        } else {
            self->applyCurrentPostFXPreset();
        }
    }
}

// Aplica el preset PostFX activo al backend
void Screen::applyCurrentPostFXPreset() {
    if (!shader_backend_) { return; }
    shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
    Rendering::PostFXParams p{};
    if (Options::video.shader.enabled && !Options::postfx_presets.empty()) {
        const auto& preset = Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset));
        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;
        std::cout << "Screen::applyCurrentPostFXPreset: preset='" << preset.name << "' scan=" << p.scanlines << " vign=" << p.vignette << " chroma=" << p.chroma << '\n';
    }
    shader_backend_->setPostFXParams(p);
}

// Aplica el preset CrtPi activo al backend
void Screen::applyCurrentCrtPiPreset() {
    if (!shader_backend_) { return; }
    if (Options::video.shader.enabled && !Options::crtpi_presets.empty()) {
        const auto& preset = Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset));
        Rendering::CrtPiParams p{
            .scanline_weight = preset.scanline_weight,
            .scanline_gap_brightness = preset.scanline_gap_brightness,
            .bloom_factor = preset.bloom_factor,
            .input_gamma = preset.input_gamma,
            .output_gamma = preset.output_gamma,
            .mask_brightness = preset.mask_brightness,
            .curvature_x = preset.curvature_x,
            .curvature_y = preset.curvature_y,
            .mask_type = preset.mask_type,
            .enable_scanlines = preset.enable_scanlines,
            .enable_multisample = preset.enable_multisample,
            .enable_gamma = preset.enable_gamma,
            .enable_curvature = preset.enable_curvature,
            .enable_sharper = preset.enable_sharper,
        };
        shader_backend_->setCrtPiParams(p);
        std::cout << "Screen::applyCurrentCrtPiPreset: preset='" << preset.name << "'" << '\n';
    }
}

// Alterna entre activar y desactivar el escalado entero
void Screen::toggleIntegerScale() {
    Options::video.integer_scale = !Options::video.integer_scale;
    SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
    if (shader_backend_) {
        shader_backend_->setScaleMode(Options::video.integer_scale);
    }
}

// Alterna entre activar y desactivar el V-Sync
void Screen::toggleVSync() {
    Options::video.vsync = !Options::video.vsync;
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
    if (shader_backend_) {
        shader_backend_->setVSync(Options::video.vsync);
    }
}

// Aplica Options::video.scale_mode a la textura del canvas de juego
void Screen::applyFilter() {
    SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
}

// Alterna el modo de filtrado entre nearest y linear
void Screen::toggleFilter() {
    Options::video.scale_mode = (Options::video.scale_mode == SDL_SCALEMODE_NEAREST)
        ? SDL_SCALEMODE_LINEAR
        : SDL_SCALEMODE_NEAREST;
    applyFilter();
}

// Devuelve true si el backend SDL3GPU está activo y con aceleración hardware
auto Screen::isHardwareAccelerated() -> bool {
    auto* self = Screen::get();
    return self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated();
}

// Establece el estado del V-Sync
void Screen::setVSync(bool enabled) {
    Options::video.vsync = enabled;
    SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}

// Obtiene los punteros a los singletones
void Screen::getSingletons() {
    service_menu_ = ServiceMenu::get();
    notifier_ = Notifier::get();
#ifdef _DEBUG
    // Actualizar la fuente de debug a 8bithud (ahora Resource está disponible)
    if (Resource::get() != nullptr) {
        auto hud_text = Resource::get()->getText("8bithud");
        if (hud_text) {
            debug_info_.text = hud_text;
        }
    }
#endif
}

// Aplica los valores de las opciones.
// IMPORTANTE: el orden importa. SDL_SetRenderLogicalPresentation calcula el
// viewport en función del tamaño actual de la ventana SDL, así que DEBE llamarse
// DESPUÉS de setFullscreenMode/adjustWindowSize — si no, al entrar en fullscreen
// el viewport queda cacheado al tamaño de la ventana pequeña previa y el juego
// se ve pequeño y centrado con barras negras alrededor.
void Screen::applySettings() {
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
    setFullscreenMode();
    adjustWindowSize();
    SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
}

// Crea el objeto de texto
void Screen::createText() {
    auto texture = std::make_shared<Texture>(getRenderer(), Asset::get()->getPath("aseprite.png"));
    text_ = std::make_shared<Text>(texture, Asset::get()->getPath("aseprite.txt"));
}