#include "engine.hpp"

#include <SDL3/SDL_error.h>    // for SDL_GetError
#include <SDL3/SDL_events.h>   // for SDL_Event, SDL_PollEvent
#include <SDL3/SDL_init.h>     // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO
#include <SDL3/SDL_keycode.h>  // for SDL_Keycode
#include <SDL3/SDL_render.h>   // for SDL_SetRenderDrawColor, SDL_RenderPresent
#include <SDL3/SDL_timer.h>    // for SDL_GetTicks
#include <SDL3/SDL_video.h>    // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds

#include <array>       // for std::array
#include <algorithm>   // for std::min, std::max, std::sort
#include <cmath>       // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
#include <cstdlib>     // for rand, srand
#include <cstring>     // for strlen
#include <ctime>       // for time
#include <filesystem>  // for path operations
#include <iostream>    // for cout
#include <string>      // for string

#include "resource_manager.hpp"  // for ResourceManager

#ifdef _WIN32
#include <windows.h>  // for GetModuleFileName
#endif

#include "ball.hpp"              // for Ball
#include "external/mouse.hpp"    // for Mouse namespace
#include "external/texture.hpp"  // for Texture
#include "shapes/png_shape.hpp"  // for PNGShape (dynamic_cast en callbacks LOGO)

// Implementación de métodos públicos
auto Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) -> bool {  // NOLINT(readability-function-cognitive-complexity)
    bool success = true;

    // Obtener resolución de pantalla para validación
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << '\n';
        return false;
    }

    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    const auto* dm = ((displays != nullptr) && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;

    int screen_w = (dm != nullptr) ? dm->w : 1920;  // Fallback si falla
    int screen_h = (dm != nullptr) ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;

    if (displays != nullptr) {
        SDL_free(displays);
    }

    // Usar parámetros o valores por defecto
    int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
    int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT;
    int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM;

    // VALIDACIÓN 1: Si resolución > pantalla → reset a default
    if (logical_width > screen_w || logical_height > screen_h) {
        std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height
                  << " excede pantalla " << screen_w << "x" << screen_h
                  << ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n";
        logical_width = DEFAULT_SCREEN_WIDTH;
        logical_height = DEFAULT_SCREEN_HEIGHT;
        window_zoom = DEFAULT_WINDOW_ZOOM;  // Reset zoom también
    }

    // VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario
    int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height);
    if (max_zoom < 1) {
        // Resolució lògica no cap en pantalla ni a zoom=1: escalar-la per fer-la càpida
        float scale = std::min(static_cast<float>(screen_w) / logical_width,
            static_cast<float>(screen_h) / logical_height);
        logical_width = std::max(320, static_cast<int>(logical_width * scale));
        logical_height = std::max(240, static_cast<int>(logical_height * scale));
        window_zoom = 1;
        std::cout << "Advertencia: Resolución no cabe en pantalla. Ajustando a "
                  << logical_width << "x" << logical_height << "\n";
    } else if (window_zoom > max_zoom) {
        std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom
                  << " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n";
        window_zoom = max_zoom;
    }

    // Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1
    if ((width > 0 || height > 0) && zoom == 0) {
        window_zoom = 1;
    }

    // Guardar escala inicial (siempre 1.0 salvo que CLI haya pedido zoom > 1)
    current_window_scale_ = static_cast<float>(window_zoom);

    // Calcular tamaño de ventana
    int window_width = logical_width * window_zoom;
    int window_height = logical_height * window_zoom;

    // Guardar resolución base (configurada por CLI o default)
    base_screen_width_ = logical_width;
    base_screen_height_ = logical_height;
    current_screen_width_ = logical_width;
    current_screen_height_ = logical_height;

    // SDL ya inicializado arriba para validación
    {
        // Crear ventana principal (fullscreen si se especifica)
        // SDL_WINDOW_HIGH_PIXEL_DENSITY removido — DPI detectado con SDL_GetWindowSizeInPixels()
        // SDL_WINDOW_OPENGL eliminado — SDL_GPU usa Metal/Vulkan/D3D12 directamente
        Uint32 window_flags = SDL_WINDOW_HIDDEN;  // Oculta hasta que el benchmark termine
        if (fullscreen) {
            window_flags |= SDL_WINDOW_FULLSCREEN;
        }

        window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags);
        if (window_ == nullptr) {
            std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << '\n';
            success = false;
        } else {
            // Centrar ventana en pantalla si no está en fullscreen
            if (!fullscreen) {
                SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
            }

            // Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal)
            gpu_ctx_ = std::make_unique<GpuContext>();
            if (!gpu_ctx_->init(window_)) {
                std::cout << "¡No se pudo inicializar SDL_GPU!" << '\n';
                success = false;
            } else {
                gpu_ctx_->setVSync(vsync_enabled_);

                // Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU)
                // Renderiza a ui_surface_, que luego se sube como textura GPU overlay
                ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32);
                if (ui_surface_ != nullptr) {
                    ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
                }
                if (ui_renderer_ == nullptr) {
                    std::cout << "Advertencia: no se pudo crear el renderer de UI software" << '\n';
                    // No es crítico — el juego funciona sin texto
                }
            }
        }
    }

    // Inicializar otros componentes si SDL se inicializó correctamente
    if (success) {
        // Cargar todas las texturas disponibles desde data/balls/
        std::string resources_dir = getResourcesDirectory();
        std::string balls_dir = resources_dir + "/data/balls";

        struct TextureInfo {
                std::string name;
                std::shared_ptr<Texture> texture;  // legacy (para physics sizing)
                std::string path;                  // resource path para GPU upload
                int width;
        };
        std::vector<TextureInfo> texture_files;

        // Buscar todas las texturas PNG en data/balls/
        namespace fs = std::filesystem;
        if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
            for (const auto& entry : fs::directory_iterator(balls_dir)) {
                if (entry.is_regular_file() && entry.path().extension() == ".png") {
                    std::string filename = entry.path().stem().string();
                    std::string fullpath = entry.path().string();

                    // Cargar textura legacy (usa ui_renderer_ en lugar del renderer_ eliminado)
                    auto texture = std::make_shared<Texture>(ui_renderer_, fullpath);
                    int width = texture->getWidth();

                    texture_files.push_back({filename, texture, fullpath, width});
                }
            }
        } else {
            // Fallback: cargar texturas desde pack
            if (ResourceManager::isPackLoaded()) {
                auto pack_resources = ResourceManager::getResourceList();

                for (const auto& resource : pack_resources) {
                    if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
                        std::string tex_name = resource.substr(6);
                        std::string name = tex_name.substr(0, tex_name.find('.'));

                        auto texture = std::make_shared<Texture>(ui_renderer_, resource);
                        int width = texture->getWidth();

                        texture_files.push_back({name, texture, resource, width});
                    }
                }
            }
        }

        // Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
        std::ranges::sort(texture_files, [](const TextureInfo& a, const TextureInfo& b) {
            return a.width > b.width;
        });

        // Guardar texturas en orden + crear texturas GPU
        for (const auto& info : texture_files) {
            textures_.push_back(info.texture);
            texture_names_.push_back(info.name);

            // Cargar textura GPU para renderizado de sprites
            auto gpu_tex = std::make_unique<GpuTexture>();
            if (gpu_ctx_ && !gpu_tex->fromFile(gpu_ctx_->device(), info.path)) {
                std::cerr << "Advertencia: no se pudo cargar textura GPU: " << info.name << '\n';
            }
            gpu_textures_.push_back(std::move(gpu_tex));
        }

        // Verificar que se cargaron texturas
        if (textures_.empty()) {
            std::cerr << "ERROR: No se pudieron cargar texturas" << '\n';
            success = false;
        }

        // Buscar índice de "normal" para usarlo como textura inicial
        current_texture_index_ = 0;
        for (size_t i = 0; i < texture_names_.size(); i++) {
            if (texture_names_[i] == "normal") {
                current_texture_index_ = i;
                break;
            }
        }
        texture_ = textures_[current_texture_index_];
        current_ball_size_ = texture_->getWidth();
        // Initialize GPU pipeline, sprite batch, and render textures
        if (gpu_ctx_ && success) {
            SDL_GPUTextureFormat offscreen_fmt = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;

            gpu_pipeline_ = std::make_unique<GpuPipeline>();
            if (!gpu_pipeline_->init(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(), offscreen_fmt)) {
                std::cerr << "ERROR: No se pudo crear el pipeline GPU" << '\n';
                success = false;
            }

            // Capacidad = número de bolas del escenario máximo (o custom si es mayor).
            // addBackground() no usa el guard de pushQuad(), así que no consume slots aquí.
            // init() reserva internamente +1 quad extra garantizado para el overlay.
            int sprite_capacity = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
            if (custom_scenario_enabled_ && custom_scenario_balls_ > sprite_capacity) {
                sprite_capacity = custom_scenario_balls_;
            }

            sprite_batch_ = std::make_unique<GpuSpriteBatch>();
            if (!sprite_batch_->init(gpu_ctx_->device(), sprite_capacity)) {
                std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << '\n';
                success = false;
            }

            gpu_ball_buffer_ = std::make_unique<GpuBallBuffer>();
            if (!gpu_ball_buffer_->init(gpu_ctx_->device())) {
                std::cerr << "ERROR: No se pudo crear el ball buffer GPU" << '\n';
                success = false;
            }
            ball_gpu_data_.reserve(GpuBallBuffer::MAX_BALLS);

            offscreen_tex_ = std::make_unique<GpuTexture>();
            if (!offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
                    current_screen_width_,
                    current_screen_height_,
                    offscreen_fmt)) {
                std::cerr << "ERROR: No se pudo crear render target offscreen" << '\n';
                success = false;
            }

            white_tex_ = std::make_unique<GpuTexture>();
            if (!white_tex_->createWhite(gpu_ctx_->device())) {
                std::cerr << "ERROR: No se pudo crear textura blanca" << '\n';
                success = false;
            }

            // Create UI overlay texture (render target usage so GPU can sample it)
            ui_tex_ = std::make_unique<GpuTexture>();
            if (!ui_tex_->createRenderTarget(gpu_ctx_->device(),
                    logical_width,
                    logical_height,
                    SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)) {
                std::cerr << "Advertencia: no se pudo crear textura UI GPU" << '\n';
            }
        }

        srand(static_cast<unsigned>(time(nullptr)));

        // Inicializar InputHandler (sin estado)
        input_handler_ = std::make_unique<InputHandler>();

        // Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager)
        theme_manager_ = std::make_unique<ThemeManager>();
        theme_manager_->initialize();
        {
            int max_balls = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
            if (custom_scenario_enabled_ && custom_scenario_balls_ > max_balls) {
                max_balls = custom_scenario_balls_;
            }
            theme_manager_->setMaxBallCount(max_balls);
        }

        // Inicializar SceneManager (gestión de bolas y física)
        scene_manager_ = std::make_unique<SceneManager>(current_screen_width_, current_screen_height_);
        scene_manager_->initialize(0, texture_, theme_manager_.get());  // Escenario 0 (10 bolas) por defecto

        // Propagar configuración custom si fue establecida antes de initialize()
        if (custom_scenario_enabled_) {
            scene_manager_->setCustomBallCount(custom_scenario_balls_);
        }

        // Calcular tamaño físico de ventana ANTES de inicializar UIManager
        // NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe
        // Calcular manualmente para poder pasar valores al constructor de UIManager
        int window_w = 0;
        int window_h = 0;
        SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
        physical_window_width_ = window_w;
        physical_window_height_ = window_h;

        // Inicializar UIManager (HUD, FPS, notificaciones)
        // NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
        ui_manager_ = std::make_unique<UIManager>();
        ui_manager_->initialize(ui_renderer_, theme_manager_.get(), physical_window_width_, physical_window_height_, current_screen_width_, current_screen_height_);

        // Inicializar ShapeManager (gestión de figuras 3D)
        shape_manager_ = std::make_unique<ShapeManager>();
        shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr, current_screen_width_, current_screen_height_);

        // Inicializar StateManager (gestión de estados DEMO/LOGO)
        state_manager_ = std::make_unique<StateManager>();
        state_manager_->initialize(this, scene_manager_.get(), theme_manager_.get(), shape_manager_.get());

        // Establecer modo inicial si no es SANDBOX (default)
        // Usar métodos de alto nivel que ejecutan las acciones de configuración
        if (initial_mode == AppMode::DEMO) {
            state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
            // Como estamos en SANDBOX (default), toggleDemoMode() cambiará a DEMO + randomizará
        } else if (initial_mode == AppMode::DEMO_LITE) {
            state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
            // Como estamos en SANDBOX (default), toggleDemoLiteMode() cambiará a DEMO_LITE + randomizará
        } else if (initial_mode == AppMode::LOGO) {
            size_t initial_ball_count = scene_manager_->getBallCount();
            state_manager_->enterLogoMode(false, current_screen_width_, current_screen_height_, initial_ball_count);
            // enterLogoMode() hace: setState(LOGO) + configuración visual completa
        }

        // Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero)
        shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_);

        // Inicializar BoidManager (gestión de comportamiento de enjambre)
        boid_manager_ = std::make_unique<BoidManager>();
        boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_);

        // Inicializar AppLogo (logo periódico en pantalla)
        app_logo_ = std::make_unique<AppLogo>();
        if (!app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_)) {
            std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << '\n';
            // No es crítico, continuar sin logo
            app_logo_.reset();
        }

        // Benchmark de rendimiento (determina max_auto_scenario_ para modos automáticos)
        if (!skip_benchmark_) {
            runPerformanceBenchmark();
        } else if (custom_scenario_enabled_) {
            custom_auto_available_ = true;  // benchmark omitido: confiar en que el hardware lo soporta
        }

        // Precalentar caché: shapes PNG (evitar I/O en primera activación de PNG_SHAPE)
        {
            unsigned char* tmp = nullptr;
            size_t tmp_size = 0;
            ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size);
            delete[] tmp;
        }

        // Mostrar ventana ahora que el benchmark terminó
        SDL_ShowWindow(window_);
    }

    return success;
}

void Engine::run() {
    while (!should_exit_) {
        calculateDeltaTime();

        // Procesar eventos de entrada (teclado, ratón, ventana)
        if (InputHandler::processEvents(*this)) {
            should_exit_ = true;
        }

        update();
        render();
    }
}

void Engine::shutdown() {
    // Wait for GPU idle before releasing GPU resources
    if (gpu_ctx_) {
        SDL_WaitForGPUIdle(gpu_ctx_->device());
    }

    // Release GPU sprite textures
    gpu_textures_.clear();

    // Release GPU render targets and utility textures
    if (gpu_ctx_) {
        if (ui_tex_) {
            ui_tex_->destroy(gpu_ctx_->device());
            ui_tex_.reset();
        }
        if (white_tex_) {
            white_tex_->destroy(gpu_ctx_->device());
            white_tex_.reset();
        }
        if (offscreen_tex_) {
            offscreen_tex_->destroy(gpu_ctx_->device());
            offscreen_tex_.reset();
        }
        if (sprite_batch_) {
            sprite_batch_->destroy(gpu_ctx_->device());
            sprite_batch_.reset();
        }
        if (gpu_ball_buffer_) {
            gpu_ball_buffer_->destroy(gpu_ctx_->device());
            gpu_ball_buffer_.reset();
        }
        if (gpu_pipeline_) {
            gpu_pipeline_->destroy(gpu_ctx_->device());
            gpu_pipeline_.reset();
        }
    }

    // Destroy software UI renderer and surface
    if (ui_renderer_ != nullptr) {
        SDL_DestroyRenderer(ui_renderer_);
        ui_renderer_ = nullptr;
    }
    if (ui_surface_ != nullptr) {
        SDL_DestroySurface(ui_surface_);
        ui_surface_ = nullptr;
    }

    // Destroy GPU context (releases device and window claim)
    if (gpu_ctx_) {
        gpu_ctx_->destroy();
        gpu_ctx_.reset();
    }

    if (window_ != nullptr) {
        SDL_DestroyWindow(window_);
        window_ = nullptr;
    }
    SDL_Quit();
}

// Métodos privados - esqueleto básico por ahora
void Engine::calculateDeltaTime() {
    Uint64 current_time = SDL_GetTicks();

    // En el primer frame, inicializar el tiempo anterior
    if (last_frame_time_ == 0) {
        last_frame_time_ = current_time;
        delta_time_ = 1.0f / 60.0f;  // Asumir 60 FPS para el primer frame
        return;
    }

    // Calcular delta time en segundos
    delta_time_ = (current_time - last_frame_time_) / 1000.0f;
    last_frame_time_ = current_time;

    // Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.)
    if (delta_time_ > 0.05f) {       // Máximo 50ms (20 FPS mínimo)
        delta_time_ = 1.0f / 60.0f;  // Fallback a 60 FPS
    }
}

void Engine::update() {
    // Accumulate time for PostFX uniforms
    postfx_uniforms_.screen_height = static_cast<float>(current_screen_height_);

    // Actualizar visibilidad del cursor (auto-ocultar tras inactividad)
    Mouse::updateCursorVisibility();

    // Obtener tiempo actual
    Uint64 current_time = SDL_GetTicks();

    // Actualizar UI (FPS, notificaciones, texto obsoleto) - delegado a UIManager
    ui_manager_->update(current_time, delta_time_);

    // Bifurcar actualización según modo activo
    if (current_mode_ == SimulationMode::PHYSICS) {
        // Modo física normal: actualizar física de cada pelota (delegado a SceneManager)
        scene_manager_->update(delta_time_);
    } else if (current_mode_ == SimulationMode::SHAPE) {
        // Modo Figura 3D: actualizar figura polimórfica
        updateShape();
    } else if (current_mode_ == SimulationMode::BOIDS) {
        // CPU boids: actualizar comportamiento de enjambre (delegado a BoidManager)
        boid_manager_->update(delta_time_);
    }

    // Actualizar Modo DEMO/LOGO (delegado a StateManager)
    state_manager_->update(delta_time_, shape_manager_->getConvergence(), shape_manager_->getActiveShape());

    // Actualizar transiciones de temas (delegado a ThemeManager)
    theme_manager_->update(delta_time_);

    // Actualizar AppLogo (logo periódico)
    if (app_logo_) {
        app_logo_->update(delta_time_, state_manager_->getCurrentMode());
    }
}

// === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER ===

// Gravedad y física
void Engine::handleGravityToggle() {
    // Si estamos en modo boids, salir a modo física CON GRAVEDAD OFF
    // Según RULES.md: "BOIDS a PHYSICS: Pulsando la tecla G: Gravedad OFF"
    if (current_mode_ == SimulationMode::BOIDS) {
        toggleBoidsMode(false);  // Cambiar a PHYSICS sin activar gravedad (preserva inercia)
        // NO llamar a forceBallsGravityOff() porque aplica impulsos que destruyen la inercia de BOIDS
        // La gravedad ya está desactivada por BoidManager::activateBoids() y se mantiene al salir
        showNotificationForAction("Modo física, gravedad off");
        return;
    }

    // Si estamos en modo figura, salir a modo física SIN GRAVEDAD
    if (current_mode_ == SimulationMode::SHAPE) {
        toggleShapeModeInternal(false);  // Desactivar figura sin forzar gravedad ON
        showNotificationForAction("Gravedad off");
    } else {
        scene_manager_->switchBallsGravity();  // Toggle normal en modo física
        // Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON)
        const Ball* first_ball = scene_manager_->getFirstBall();
        bool gravity_on = (first_ball == nullptr) ? true : (first_ball->getGravityForce() != 0.0f);
        showNotificationForAction(gravity_on ? "Gravedad on" : "Gravedad off");
    }
}

void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) {
    // Si estamos en modo boids, salir a modo física primero PRESERVANDO VELOCIDAD
    if (current_mode_ == SimulationMode::BOIDS) {
        current_mode_ = SimulationMode::PHYSICS;
        boid_manager_->deactivateBoids(false);  // NO activar gravedad aún (preservar momentum)
        scene_manager_->forceBallsGravityOn();  // Activar gravedad SIN impulsos (preserva velocidad)
    }
    // Si estamos en modo figura, salir a modo física CON gravedad
    else if (current_mode_ == SimulationMode::SHAPE) {
        toggleShapeModeInternal();  // Desactivar figura (activa gravedad automáticamente)
    } else {
        scene_manager_->enableBallsGravityIfDisabled();  // Reactivar gravedad si estaba OFF
    }

    scene_manager_->changeGravityDirection(direction);
    showNotificationForAction(notification_text);
}

// Display y depuración
void Engine::toggleDebug() {
    ui_manager_->toggleDebug();
}

void Engine::toggleHelp() {
    ui_manager_->toggleHelp();
}

// Figuras 3D
void Engine::toggleShapeMode() {
    toggleShapeModeInternal();
    // Mostrar notificación según el modo actual después del toggle
    if (current_mode_ == SimulationMode::PHYSICS) {
        showNotificationForAction("Modo Física");
    } else {
        // Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType)
        // Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE
        constexpr std::array<const char*, 10> SHAPE_NAMES = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
        showNotificationForAction(SHAPE_NAMES[static_cast<int>(shape_manager_->getCurrentShapeType())]);
    }
}

void Engine::activateShape(ShapeType type, const char* notification_text) {
    activateShapeInternal(type);
    showNotificationForAction(notification_text);
}

void Engine::handleShapeScaleChange(bool increase) {
    // Delegar a ShapeManager (gestiona escala y muestra notificación en SANDBOX)
    shape_manager_->handleShapeScaleChange(increase);
}

void Engine::resetShapeScale() {
    // Delegar a ShapeManager (resetea escala y muestra notificación en SANDBOX)
    shape_manager_->resetShapeScale();
}

void Engine::toggleDepthZoom() {
    // Delegar a ShapeManager (toggle depth zoom y muestra notificación en SANDBOX)
    shape_manager_->toggleDepthZoom();
}

// Boids (comportamiento de enjambre)
auto Engine::isScenarioAllowedForBoids(int scenario_id) const -> bool {
    int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX)
        ? custom_scenario_balls_
        : BALL_COUNT_SCENARIOS[scenario_id];
    return ball_count <= BOIDS_MAX_BALLS;
}

void Engine::toggleBoidsMode(bool force_gravity_on) {
    if (current_mode_ != SimulationMode::BOIDS) {
        // Intentando activar BOIDS — verificar escenario actual
        if (!isScenarioAllowedForBoids(scene_manager_->getCurrentScenario())) {
            showNotificationForAction("Boids: máximo 1.000 pelotas");
            return;
        }
    }
    if (current_mode_ == SimulationMode::BOIDS) {
        // Salir del modo boids
        current_mode_ = SimulationMode::PHYSICS;
        boid_manager_->deactivateBoids(force_gravity_on);
    } else {
        // Entrar al modo boids (desde PHYSICS o SHAPE)
        if (current_mode_ == SimulationMode::SHAPE) {
            shape_manager_->toggleShapeMode(false);
            current_mode_ = SimulationMode::PHYSICS;
        }

        // Activar modo boids en CPU (configura gravedad OFF, inicializa velocidades)
        current_mode_ = SimulationMode::BOIDS;
        boid_manager_->activateBoids();
    }
}

// Temas de colores
void Engine::cycleTheme(bool forward) {
    if (forward) {
        theme_manager_->cycleTheme();
    } else {
        theme_manager_->cyclePrevTheme();
    }
    showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}

void Engine::switchThemeByNumpad(int numpad_key) {
    // Mapear tecla numpad a índice de tema según página actual
    int theme_index = -1;

    if (theme_page_ == 0) {
        // Página 0: Temas 0-9 (estáticos + SUNRISE)
        if (numpad_key >= 0 && numpad_key <= 9) {
            theme_index = (numpad_key == 0) ? 9 : (numpad_key - 1);
        }
    } else {
        // Página 1: Temas 10-14 (dinámicos)
        if (numpad_key >= 1 && numpad_key <= 5) {
            theme_index = 9 + numpad_key;
        }
    }

    if (theme_index != -1) {
        theme_manager_->switchToTheme(theme_index);
        showNotificationForAction(theme_manager_->getCurrentThemeNameES());
    }
}

void Engine::toggleThemePage() {
    theme_page_ = (theme_page_ == 0) ? 1 : 0;
    showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2");
}

void Engine::pauseDynamicTheme() {
    theme_manager_->pauseDynamic();
}

// Sprites/Texturas
void Engine::switchTexture() {
    switchTextureInternal(true);  // Mostrar notificación en modo manual
}

// Control manual del benchmark (--skip-benchmark, --max-balls)
void Engine::setSkipBenchmark() {
    skip_benchmark_ = true;
}

void Engine::setMaxBallsOverride(int n) {
    skip_benchmark_ = true;
    int best = DEMO_AUTO_MIN_SCENARIO;
    for (int i = DEMO_AUTO_MIN_SCENARIO; i <= DEMO_AUTO_MAX_SCENARIO; ++i) {
        if (BALL_COUNT_SCENARIOS[i] <= n) {
            best = i;
        } else {
            break;
        }
    }
    max_auto_scenario_ = best;
}

// Escenario custom (--custom-balls)
void Engine::setCustomScenario(int balls) {
    custom_scenario_balls_ = balls;
    custom_scenario_enabled_ = true;
    // scene_manager_ puede no existir aún (llamada pre-init); propagación en initialize()
    if (scene_manager_) {
        scene_manager_->setCustomBallCount(balls);
    }
}

// Escenarios (número de pelotas)
void Engine::changeScenario(int scenario_id, const char* notification_text) {
    if (current_mode_ == SimulationMode::BOIDS) {
        if (!isScenarioAllowedForBoids(scenario_id)) {
            showNotificationForAction("Boids: máximo 1.000 pelotas");
            return;
        }
    }
    // Pasar el modo actual al SceneManager para inicialización correcta
    scene_manager_->changeScenario(scenario_id, current_mode_);

    // Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
    if (current_mode_ == SimulationMode::SHAPE) {
        generateShape();
        scene_manager_->enableShapeAttractionAll(true);  // Crítico tras changeScenario
    }

    // Si estamos en modo BOIDS, desactivar gravedad (modo BOIDS = gravedad OFF siempre)
    if (current_mode_ == SimulationMode::BOIDS) {
        scene_manager_->forceBallsGravityOff();
    }

    showNotificationForAction(notification_text);
}

// Zoom y fullscreen
void Engine::handleZoomIn() {
    if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
        zoomIn();
    }
}

void Engine::handleZoomOut() {
    if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
        zoomOut();
    }
}

// Modos de aplicación (DEMO/LOGO) - Delegados a StateManager
void Engine::toggleDemoMode() {
    AppMode prev_mode = state_manager_->getCurrentMode();
    state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
    AppMode new_mode = state_manager_->getCurrentMode();

    // Mostrar notificación según el modo resultante
    if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
        showNotificationForAction("Modo sandbox");
    } else if (new_mode == AppMode::DEMO && prev_mode != AppMode::DEMO) {
        showNotificationForAction("Modo demo");
    }
}

void Engine::toggleDemoLiteMode() {
    AppMode prev_mode = state_manager_->getCurrentMode();
    state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
    AppMode new_mode = state_manager_->getCurrentMode();

    // Mostrar notificación según el modo resultante
    if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
        showNotificationForAction("Modo sandbox");
    } else if (new_mode == AppMode::DEMO_LITE && prev_mode != AppMode::DEMO_LITE) {
        showNotificationForAction("Modo demo lite");
    }
}

void Engine::toggleLogoMode() {
    AppMode prev_mode = state_manager_->getCurrentMode();
    state_manager_->toggleLogoMode(current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
    AppMode new_mode = state_manager_->getCurrentMode();

    // Mostrar notificación según el modo resultante
    if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
        showNotificationForAction("Modo sandbox");
    } else if (new_mode == AppMode::LOGO && prev_mode != AppMode::LOGO) {
        showNotificationForAction("Modo logo");
    }
}

void Engine::render() {  // NOLINT(readability-function-cognitive-complexity)
    if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) {
        return;
    }

    // === Render UI text to software surface ===
    renderUIToSurface();

    // === Acquire command buffer ===
    SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
    if (cmd == nullptr) {
        return;
    }

    // === Upload UI surface to GPU texture (inline copy pass) ===
    uploadUISurface(cmd);

    // === Build sprite batch ===
    sprite_batch_->beginFrame();

    // Background gradient
    float top_r = 0;
    float top_g = 0;
    float top_b = 0;
    float bot_r = 0;
    float bot_g = 0;
    float bot_b = 0;
    theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b);
    sprite_batch_->addBackground(
        static_cast<float>(current_screen_width_),
        static_cast<float>(current_screen_height_),
        top_r,
        top_g,
        top_b,
        bot_r,
        bot_g,
        bot_b);

    // Sprites (balls)
    const auto& balls = scene_manager_->getBalls();
    const auto SW = static_cast<float>(current_screen_width_);
    const auto SH = static_cast<float>(current_screen_height_);

    if (current_mode_ == SimulationMode::SHAPE) {
        // SHAPE mode: bucket sort by depth Z (Painter's Algorithm), with depth scale.
        // Uses the sprite batch (supports per-sprite scale, needed for depth zoom).
        for (size_t i = 0; i < balls.size(); i++) {
            int b = static_cast<int>(balls[i]->getDepthBrightness() * (DEPTH_SORT_BUCKETS - 1));
            depth_buckets_[std::clamp(b, 0, DEPTH_SORT_BUCKETS - 1)].push_back(i);
        }
        for (int b = 0; b < DEPTH_SORT_BUCKETS; b++) {
            for (size_t idx : depth_buckets_[b]) {
                SDL_FRect pos = balls[idx]->getPosition();
                Color color = theme_manager_->getInterpolatedColor(idx);
                float brightness = balls[idx]->getDepthBrightness();
                float depth_scale = balls[idx]->getDepthScale();
                float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
                sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h, color.r / 255.0f * bf, color.g / 255.0f * bf, color.b / 255.0f * bf, 1.0f, depth_scale, SW, SH);
            }
            depth_buckets_[b].clear();
        }
    } else {
        // PHYSICS / CPU-BOIDS mode: build instanced ball buffer (GPU instanced rendering).
        // 32 bytes per ball instead of 4×32 bytes per quad — 4× less upload bandwidth.
        ball_gpu_data_.clear();
        for (size_t idx = 0; idx < balls.size(); idx++) {
            SDL_FRect pos = balls[idx]->getPosition();
            Color color = theme_manager_->getInterpolatedColor(idx);
            // Convert to NDC center + NDC half-size (both positive)
            float cx = (((pos.x + pos.w * 0.5f) / SW) * 2.0f) - 1.0f;
            float cy = 1.0f - (((pos.y + pos.h * 0.5f) / SH) * 2.0f);
            float hw = pos.w / SW;
            float hh = pos.h / SH;
            ball_gpu_data_.push_back({cx, cy, hw, hh, color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, 1.0f});
        }
    }

    // UI overlay quad (drawn in Pass 2 over the postfx output)
    sprite_batch_->addFullscreenOverlay();

    // Upload sprite batch (background + SHAPE balls + UI overlay quad)
    if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
        GpuContext::submit(cmd);
        return;
    }

    // Upload instanced ball buffer (PHYSICS / CPU-BOIDS modes)
    bool use_instanced_balls = (current_mode_ != SimulationMode::SHAPE) && !ball_gpu_data_.empty();
    if (use_instanced_balls) {
        gpu_ball_buffer_->upload(gpu_ctx_->device(), cmd, ball_gpu_data_.data(), static_cast<int>(ball_gpu_data_.size()));
    }

    GpuTexture* sprite_tex = (!gpu_textures_.empty())
        ? gpu_textures_[current_texture_index_].get()
        : nullptr;

    // === Pass 1: Render background + balls to offscreen texture ===
    if (offscreen_tex_ && offscreen_tex_->isValid() && (sprite_tex != nullptr) && sprite_tex->isValid()) {
        SDL_GPUColorTargetInfo ct = {};
        ct.texture = offscreen_tex_->texture();
        ct.load_op = SDL_GPU_LOADOP_CLEAR;
        ct.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};
        ct.store_op = SDL_GPU_STOREOP_STORE;

        SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);

        // Background (white texture tinted by vertex color, via sprite batch)
        SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
        {
            SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
            SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
            SDL_BindGPUVertexBuffers(pass1, 0, &vb, 1);
            SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
        }
        if (white_tex_ && white_tex_->isValid() && sprite_batch_->bgIndexCount() > 0) {
            SDL_GPUTextureSamplerBinding tsb = {white_tex_->texture(), white_tex_->sampler()};
            SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
            SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->bgIndexCount(), 1, 0, 0, 0);
        }

        if (use_instanced_balls && gpu_ball_buffer_->count() > 0) {
            // PHYSICS / CPU-BOIDS: instanced rendering — 6 procedural vertices per instance
            SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->ballPipeline());
            SDL_GPUBufferBinding ball_vb = {gpu_ball_buffer_->buffer(), 0};
            SDL_BindGPUVertexBuffers(pass1, 0, &ball_vb, 1);
            SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
            SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
            SDL_DrawGPUPrimitives(pass1, 6, static_cast<Uint32>(gpu_ball_buffer_->count()), 0, 0);
        } else if (!use_instanced_balls && sprite_batch_->spriteIndexCount() > 0) {
            // SHAPE: sprite batch with depth sort (re-bind sprite pipeline + buffers)
            SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
            SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
            SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
            SDL_BindGPUVertexBuffers(pass1, 0, &vb, 1);
            SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
            SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
            SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
            SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->spriteIndexCount(), 1, sprite_batch_->spriteIndexOffset(), 0, 0);
        }

        SDL_EndGPURenderPass(pass1);
    }

    // === Pass 2+: External multi-pass shader OR native PostFX → swapchain ===
    Uint32 sw_w = 0;
    Uint32 sw_h = 0;
    SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
    if ((swapchain != nullptr) && offscreen_tex_ && offscreen_tex_->isValid()) {
        // Helper lambda for viewport/scissor (used in the final pass)
        auto apply_viewport = [&](SDL_GPURenderPass* rp) {
            if (!fullscreen_enabled_) {
                return;
            }
            float vp_x;
            float vp_y;
            float vp_w;
            float vp_h;
            if (current_scaling_mode_ == ScalingMode::STRETCH) {
                vp_x = 0.0f;
                vp_y = 0.0f;
                vp_w = static_cast<float>(sw_w);
                vp_h = static_cast<float>(sw_h);
            } else if (current_scaling_mode_ == ScalingMode::INTEGER) {
                int scale = static_cast<int>(std::min(sw_w / static_cast<Uint32>(base_screen_width_),
                    sw_h / static_cast<Uint32>(base_screen_height_)));
                scale = std::max(scale, 1);
                vp_w = static_cast<float>(base_screen_width_ * scale);
                vp_h = static_cast<float>(base_screen_height_ * scale);
                vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f;
                vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
            } else {  // LETTERBOX
                float scale = std::min(static_cast<float>(sw_w) / base_screen_width_,
                    static_cast<float>(sw_h) / base_screen_height_);
                vp_w = base_screen_width_ * scale;
                vp_h = base_screen_height_ * scale;
                vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f;
                vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
            }
            SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f};
            SDL_SetGPUViewport(rp, &vp);
            SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y), static_cast<int>(vp_w), static_cast<int>(vp_h)};
            SDL_SetGPUScissor(rp, &scissor);
        };

        {
            // --- Native PostFX path ---
            SDL_GPUColorTargetInfo ct = {};
            ct.texture = swapchain;
            ct.load_op = SDL_GPU_LOADOP_CLEAR;
            ct.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};
            ct.store_op = SDL_GPU_STOREOP_STORE;

            SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
            apply_viewport(pass2);

            // PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
            SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
            SDL_GPUTextureSamplerBinding scene_tsb = {offscreen_tex_->texture(), offscreen_tex_->sampler()};
            SDL_BindGPUFragmentSamplers(pass2, 0, &scene_tsb, 1);
            SDL_PushGPUFragmentUniformData(cmd, 0, &postfx_uniforms_, sizeof(PostFXUniforms));
            SDL_DrawGPUPrimitives(pass2, 3, 1, 0, 0);

            // UI overlay (alpha-blended, uses sprite pipeline)
            if (ui_tex_ && ui_tex_->isValid() && sprite_batch_->overlayIndexCount() > 0) {
                SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->spritePipeline());
                SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
                SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
                SDL_BindGPUVertexBuffers(pass2, 0, &vb, 1);
                SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
                SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()};
                SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1);
                SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1, sprite_batch_->overlayIndexOffset(), 0, 0);
            }

            SDL_EndGPURenderPass(pass2);
        }  // end native PostFX
    }  // end if (swapchain && ...)

    GpuContext::submit(cmd);
}

void Engine::showNotificationForAction(const std::string& text) {
    // IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales)
    // NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación

    // Delegar a UIManager
    ui_manager_->showNotification(text, NOTIFICATION_DURATION);
}

void Engine::pushBallsAwayFromGravity() {
    scene_manager_->pushBallsAwayFromGravity();
}

void Engine::toggleVSync() {
    vsync_enabled_ = !vsync_enabled_;

    // Actualizar texto en UIManager
    ui_manager_->updateVSyncText(vsync_enabled_);

    // Aplicar el cambio de V-Sync al contexto GPU
    if (gpu_ctx_) {
        gpu_ctx_->setVSync(vsync_enabled_);
    }
}

void Engine::toggleFullscreen() {
    // Si está en modo real fullscreen, primero salir de él
    if (real_fullscreen_enabled_) {
        toggleRealFullscreen();  // Esto lo desactiva
    }

    fullscreen_enabled_ = !fullscreen_enabled_;
    SDL_SetWindowFullscreen(window_, fullscreen_enabled_);

    // Si acabamos de salir de fullscreen, restaurar tamaño de ventana
    if (!fullscreen_enabled_) {
        int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
        int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
        SDL_SetWindowSize(window_, restore_w, restore_h);
        SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
    }

    // Actualizar dimensiones físicas después del cambio
    updatePhysicalWindowSize();
}

void Engine::toggleRealFullscreen() {
    // Si está en modo fullscreen normal, primero desactivarlo
    if (fullscreen_enabled_) {
        fullscreen_enabled_ = false;
        SDL_SetWindowFullscreen(window_, false);
    }

    real_fullscreen_enabled_ = !real_fullscreen_enabled_;

    if (real_fullscreen_enabled_) {
        // Obtener resolución del escritorio
        int num_displays = 0;
        SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
        if (displays != nullptr && num_displays > 0) {
            const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
            if (dm != nullptr) {
                // Cambiar a resolución nativa del escritorio
                current_screen_width_ = dm->w;
                current_screen_height_ = dm->h;

                // Recrear ventana con nueva resolución
                SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
                SDL_SetWindowFullscreen(window_, true);

                // Recrear render target offscreen con nueva resolución
                recreateOffscreenTexture();

                // Actualizar tamaño físico de ventana y fuentes
                updatePhysicalWindowSize();

                // Reinicar la escena con nueva resolución
                scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
                scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);

                // Actualizar tamaño de pantalla para boids (wrapping boundaries)
                boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);

                // Actualizar ShapeManager con nueva resolución (siempre, independientemente del modo actual)
                shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);

                // Actualizar AppLogo con nueva resolución
                if (app_logo_) {
                    app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
                }

                // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
                if (current_mode_ == SimulationMode::SHAPE) {
                    generateShape();                                 // Regenerar figura con nuevas dimensiones de pantalla
                    scene_manager_->enableShapeAttractionAll(true);  // Crítico tras changeScenario
                }
            }
            SDL_free(displays);
        }
    } else {
        // Volver a resolución base (configurada por CLI o default)
        current_screen_width_ = base_screen_width_;
        current_screen_height_ = base_screen_height_;
        current_field_scale_ = 1.0f;  // Resetear escala de campo al salir de fullscreen real

        // Restaurar ventana normal con la escala actual
        SDL_SetWindowFullscreen(window_, false);
        int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
        int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
        SDL_SetWindowSize(window_, restore_w, restore_h);
        SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);

        // Recrear render target offscreen con resolución base
        recreateOffscreenTexture();

        // Actualizar tamaño físico de ventana y fuentes
        updatePhysicalWindowSize();

        // Reinicar la escena con resolución original
        scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
        scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);

        // Actualizar ShapeManager con resolución restaurada (siempre, independientemente del modo actual)
        shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);

        // Actualizar AppLogo con resolución restaurada
        if (app_logo_) {
            app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
        }

        // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
        if (current_mode_ == SimulationMode::SHAPE) {
            generateShape();                                 // Regenerar figura con nuevas dimensiones de pantalla
            scene_manager_->enableShapeAttractionAll(true);  // Crítico tras changeScenario
        }
    }
}

void Engine::applyPostFXPreset(int mode) {
    static constexpr std::array<std::array<float, 3>, 4> PRESETS = {{
        {0.8f, 0.0f, 0.0f},  // 0: Vinyeta
        {0.8f, 0.0f, 0.8f},  // 1: Scanlines
        {0.8f, 0.2f, 0.0f},  // 2: Cromàtica
        {0.8f, 0.2f, 0.8f},  // 3: Complet
    }};
    postfx_uniforms_.vignette_strength = PRESETS[mode][0];
    postfx_uniforms_.chroma_strength = PRESETS[mode][1];
    postfx_uniforms_.scanline_strength = PRESETS[mode][2];
    // Reaplicar overrides de CLI si están activos
    if (postfx_override_vignette_ >= 0.f) {
        postfx_uniforms_.vignette_strength = postfx_override_vignette_;
    }
    if (postfx_override_chroma_ >= 0.f) {
        postfx_uniforms_.chroma_strength = postfx_override_chroma_;
    }
}

void Engine::handlePostFXCycle() {
    cycleShader();
}

void Engine::handlePostFXToggle() {
    static constexpr std::array<const char*, 4> NAMES = {
        "PostFX viñeta",
        "PostFX scanlines",
        "PostFX cromática",
        "PostFX completo"};
    postfx_enabled_ = !postfx_enabled_;
    if (postfx_enabled_) {
        applyPostFXPreset(postfx_effect_mode_);
        showNotificationForAction(NAMES[postfx_effect_mode_]);
    } else {
        postfx_uniforms_.vignette_strength = 0.0f;
        postfx_uniforms_.chroma_strength = 0.0f;
        postfx_uniforms_.scanline_strength = 0.0f;
        showNotificationForAction("PostFX desactivado");
    }
}

void Engine::setInitialPostFX(int mode) {
    postfx_effect_mode_ = mode;
    postfx_enabled_ = true;
    applyPostFXPreset(mode);
}

void Engine::setPostFXParamOverrides(float vignette, float chroma) {
    postfx_override_vignette_ = vignette;
    postfx_override_chroma_ = chroma;
    postfx_enabled_ = true;
    // Aplicar inmediatamente sobre el preset activo
    if (vignette >= 0.f) {
        postfx_uniforms_.vignette_strength = vignette;
    }
    if (chroma >= 0.f) {
        postfx_uniforms_.chroma_strength = chroma;
    }
}

void Engine::cycleShader() {
    // X no hace nada si PostFX está desactivado
    if (!postfx_enabled_) {
        return;
    }

    // Cicla solo entre los 4 modos (sin OFF)
    postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 4;
    applyPostFXPreset(postfx_effect_mode_);

    static constexpr std::array<const char*, 4> NAMES = {
        "PostFX viñeta",
        "PostFX scanlines",
        "PostFX cromática",
        "PostFX completo"};
    showNotificationForAction(NAMES[postfx_effect_mode_]);
}

void Engine::toggleIntegerScaling() {
    // Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
    switch (current_scaling_mode_) {
        case ScalingMode::INTEGER:
            current_scaling_mode_ = ScalingMode::LETTERBOX;
            break;
        case ScalingMode::LETTERBOX:
            current_scaling_mode_ = ScalingMode::STRETCH;
            break;
        case ScalingMode::STRETCH:
            current_scaling_mode_ = ScalingMode::INTEGER;
            break;
    }

    const char* mode_name = nullptr;
    switch (current_scaling_mode_) {
        case ScalingMode::INTEGER:
            mode_name = "entero";
            break;
        case ScalingMode::LETTERBOX:
            mode_name = "letterbox";
            break;
        case ScalingMode::STRETCH:
            mode_name = "stretch";
            break;
    }
    showNotificationForAction(std::string("Escalado ") + mode_name);
}

void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
    if (!sprite_batch_) {
        return;
    }
    sprite_batch_->addSprite(x, y, w, h, r / 255.0f, g / 255.0f, b / 255.0f, 1.0f, scale, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
}

// Sistema de escala de ventana (pasos del 10%)
auto Engine::calculateMaxWindowScale() const -> float {
    SDL_Rect bounds;
    if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) {  // bool: false = error
        return WINDOW_SCALE_MIN;                                    // Fallback solo si falla de verdad
    }
    float max_by_w = static_cast<float>(bounds.w - (2 * WINDOW_DESKTOP_MARGIN)) / base_screen_width_;
    float max_by_h = static_cast<float>(bounds.h - (2 * WINDOW_DESKTOP_MARGIN) - WINDOW_DECORATION_HEIGHT) / base_screen_height_;
    float result = std::max(WINDOW_SCALE_MIN, std::min(max_by_w, max_by_h));
    return result;
}

// Redimensiona la ventana física manteniéndo su centro, con clamping a pantalla.
static void resizeWindowCentered(SDL_Window* window, int new_w, int new_h) {
    int cur_x;
    int cur_y;
    int cur_w;
    int cur_h;
    SDL_GetWindowPosition(window, &cur_x, &cur_y);
    SDL_GetWindowSize(window, &cur_w, &cur_h);

    int new_x = cur_x + ((cur_w - new_w) / 2);
    int new_y = cur_y + ((cur_h - new_h) / 2);

    SDL_Rect bounds;
    if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) {
        new_x = std::max(WINDOW_DESKTOP_MARGIN,
            std::min(new_x, bounds.w - new_w - WINDOW_DESKTOP_MARGIN));
        new_y = std::max(WINDOW_DESKTOP_MARGIN,
            std::min(new_y, bounds.h - new_h - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT));
    }

    SDL_SetWindowSize(window, new_w, new_h);
    SDL_SetWindowPosition(window, new_x, new_y);
}

void Engine::setWindowScale(float new_scale) {
    float max_scale = calculateMaxWindowScale();
    new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
    new_scale = std::round(new_scale * 10.0f) / 10.0f;

    if (new_scale == current_window_scale_) {
        return;
    }

    int new_width = static_cast<int>(std::round(current_screen_width_ * new_scale));
    int new_height = static_cast<int>(std::round(current_screen_height_ * new_scale));

    resizeWindowCentered(window_, new_width, new_height);
    current_window_scale_ = new_scale;

    updatePhysicalWindowSize();
}

void Engine::zoomIn() {
    float prev = current_window_scale_;
    setWindowScale(current_window_scale_ + WINDOW_SCALE_STEP);
    if (current_window_scale_ != prev) {
        std::array<char, 32> buf{};
        std::snprintf(buf.data(), buf.size(), "Zoom %.0f%%", current_window_scale_ * 100.0f);
        showNotificationForAction(buf.data());
    }
}

void Engine::zoomOut() {
    float prev = current_window_scale_;
    setWindowScale(current_window_scale_ - WINDOW_SCALE_STEP);
    if (current_window_scale_ != prev) {
        std::array<char, 32> buf{};
        std::snprintf(buf.data(), buf.size(), "Zoom %.0f%%", current_window_scale_ * 100.0f);
        showNotificationForAction(buf.data());
    }
}

void Engine::setFieldScale(float new_scale) {
    float max_scale = calculateMaxWindowScale();
    new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
    new_scale = std::round(new_scale * 10.0f) / 10.0f;
    if (new_scale == current_field_scale_) {
        return;
    }

    current_field_scale_ = new_scale;
    current_screen_width_ = static_cast<int>(std::round(base_screen_width_ * new_scale));
    current_screen_height_ = static_cast<int>(std::round(base_screen_height_ * new_scale));

    // Ajustar ventana física: campo lógico × zoom actual, manteniendo centro
    int phys_w = static_cast<int>(std::round(current_screen_width_ * current_window_scale_));
    int phys_h = static_cast<int>(std::round(current_screen_height_ * current_window_scale_));
    resizeWindowCentered(window_, phys_w, phys_h);

    // Recrear render target con nueva resolución lógica
    recreateOffscreenTexture();
    updatePhysicalWindowSize();

    // Reiniciar escena (igual que F4)
    scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
    scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
    boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
    shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
    if (app_logo_) {
        app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
    }
    if (current_mode_ == SimulationMode::SHAPE) {
        generateShape();
        scene_manager_->enableShapeAttractionAll(true);
    }

    showNotificationForAction("Campo " + std::to_string(current_screen_width_) +
        " x " + std::to_string(current_screen_height_));
}

void Engine::fieldSizeUp() { setFieldScale(current_field_scale_ + WINDOW_SCALE_STEP); }
void Engine::fieldSizeDown() { setFieldScale(current_field_scale_ - WINDOW_SCALE_STEP); }

void Engine::updatePhysicalWindowSize() {
    if (real_fullscreen_enabled_) {
        // En fullscreen real (F4), usar resolución del display
        physical_window_width_ = current_screen_width_;
        physical_window_height_ = current_screen_height_;
    } else if (fullscreen_enabled_) {
        // En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico)
        int num_displays = 0;
        SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
        if (displays != nullptr && num_displays > 0) {
            const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
            if (dm != nullptr) {
                physical_window_width_ = dm->w;
                physical_window_height_ = dm->h;
            }
            SDL_free(displays);
        }
    } else {
        // En modo ventana, obtener tamaño FÍSICO real del framebuffer
        int window_w = 0;
        int window_h = 0;
        SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
        physical_window_width_ = window_w;
        physical_window_height_ = window_h;
    }

    // Notificar a UIManager del cambio de tamaño (delegado)
    // Pasar current_screen_height_ para que UIManager actualice la altura lógica
    // (necesario en F4 donde la resolución lógica cambia a la del display)
    ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_, current_screen_height_);
}

// ============================================================================
// MÉTODOS PÚBLICOS PARA STATEMANAGER (automatización sin notificación)
// ============================================================================

void Engine::enterShapeMode(ShapeType type) {
    activateShapeInternal(type);
}

void Engine::exitShapeMode(bool force_gravity) {
    toggleShapeModeInternal(force_gravity);
}

void Engine::switchTextureSilent() {
    switchTextureInternal(false);
}

void Engine::setTextureByIndex(size_t index) {
    if (index >= textures_.size()) {
        return;
    }
    current_texture_index_ = index;
    texture_ = textures_[current_texture_index_];
    int new_size = texture_->getWidth();
    current_ball_size_ = new_size;
    scene_manager_->updateBallTexture(texture_, new_size);
}

// Toggle manual del Modo Logo (tecla K)
// Sistema de cambio de sprites dinámico
void Engine::switchTextureInternal(bool show_notification) {
    if (textures_.empty()) {
        return;
    }

    // Cambiar a siguiente textura (ciclar)
    current_texture_index_ = (current_texture_index_ + 1) % textures_.size();
    texture_ = textures_[current_texture_index_];

    // Obtener nuevo tamaño de la textura
    int new_size = texture_->getWidth();
    current_ball_size_ = new_size;

    // Actualizar texturas y tamaños de todas las pelotas (delegado a SceneManager)
    scene_manager_->updateBallTexture(texture_, new_size);

    // Mostrar notificación con el nombre de la textura (solo si se solicita)
    if (show_notification) {
        std::string texture_name = texture_names_[current_texture_index_];
        std::ranges::transform(texture_name, texture_name.begin(), ::tolower);
        showNotificationForAction("Textura " + texture_name);
    }
}

// ============================================================================
// Sistema de Figuras 3D - THIN WRAPPERS (delegan a ShapeManager)
// ============================================================================

void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) {
    shape_manager_->toggleShapeMode(force_gravity_on_exit);
    current_mode_ = shape_manager_->getCurrentMode();
}

void Engine::activateShapeInternal(ShapeType type) {
    shape_manager_->activateShape(type);
    current_mode_ = SimulationMode::SHAPE;
}

void Engine::generateShape() {
    shape_manager_->generateShape();
}

void Engine::updateShape() {
    shape_manager_->update(delta_time_);
}

// ============================================================================
// BENCHMARK DE RENDIMIENTO
// ============================================================================

void Engine::runPerformanceBenchmark() {
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    float monitor_hz = 60.0f;
    if ((displays != nullptr) && num_displays > 0) {
        const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
        if ((dm != nullptr) && dm->refresh_rate > 0) {
            monitor_hz = dm->refresh_rate;
        }
        SDL_free(displays);
    }

    // Benchmark CPU-only: sin render(), sin GPU, ventana permanece oculta
    const int BENCH_DURATION_MS = 600;
    const int WARMUP_ITERS = 10;

    SimulationMode original_mode = current_mode_;

    auto restore = [&]() {
        current_mode_ = original_mode;
        if (shape_manager_->isShapeModeActive()) {
            shape_manager_->toggleShapeMode(false);
        }
        scene_manager_->changeScenario(0, original_mode);
        last_frame_time_ = 0;
    };

    custom_auto_available_ = false;
    if (custom_scenario_enabled_) {
        scene_manager_->changeScenario(CUSTOM_SCENARIO_IDX, SimulationMode::SHAPE);
        activateShapeInternal(ShapeType::SPHERE);
        last_frame_time_ = 0;
        for (int w = 0; w < WARMUP_ITERS; ++w) {
            calculateDeltaTime();
            update();
        }
        int frame_count = 0;
        Uint64 start = SDL_GetTicks();
        while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
            calculateDeltaTime();
            update();
            ++frame_count;
        }
        float ups = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
        custom_auto_available_ = (ups >= monitor_hz);
        std::cout << "[Benchmark CPU] Custom (" << custom_scenario_balls_ << " bolas): "
                  << ups << " ups → " << (custom_auto_available_ ? "OK" : "insuficiente") << "\n";
    }

    for (int idx = DEMO_AUTO_MAX_SCENARIO; idx >= DEMO_AUTO_MIN_SCENARIO; --idx) {
        scene_manager_->changeScenario(idx, SimulationMode::SHAPE);
        activateShapeInternal(ShapeType::SPHERE);

        last_frame_time_ = 0;
        for (int w = 0; w < WARMUP_ITERS; ++w) {
            calculateDeltaTime();
            update();
        }

        int frame_count = 0;
        Uint64 start = SDL_GetTicks();
        while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
            calculateDeltaTime();
            update();
            ++frame_count;
        }

        float measured_ups = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
        int ball_count = BALL_COUNT_SCENARIOS[idx];
        if (measured_ups >= monitor_hz) {
            std::cout << "[Benchmark CPU] Escenario " << idx << " (" << ball_count << " bolas): "
                      << measured_ups << " ups → OK\n";
            std::cout << "[Benchmark CPU] Resultado: max escenario auto = " << idx
                      << " (" << ball_count << " bolas)\n";
            max_auto_scenario_ = idx;
            restore();
            return;
        }
        std::cout << "[Benchmark CPU] Escenario " << idx << " (" << ball_count << " bolas): "
                  << measured_ups << " ups → insuficiente\n";
    }

    std::cout << "[Benchmark CPU] Resultado: max escenario auto = " << DEMO_AUTO_MIN_SCENARIO
              << " (" << BALL_COUNT_SCENARIOS[DEMO_AUTO_MIN_SCENARIO] << " bolas)\n";
    max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
    restore();
}

// ============================================================================
// GPU HELPERS
// ============================================================================

auto Engine::loadGpuSpriteTexture(size_t index) -> bool {
    if (!gpu_ctx_ || index >= gpu_textures_.size()) {
        return false;
    }
    return gpu_textures_[index] && gpu_textures_[index]->isValid();
}

void Engine::recreateOffscreenTexture() {
    if (!gpu_ctx_ || !offscreen_tex_) {
        return;
    }
    SDL_WaitForGPUIdle(gpu_ctx_->device());

    offscreen_tex_->destroy(gpu_ctx_->device());
    offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
        current_screen_width_,
        current_screen_height_,
        SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);

    // Recreate UI texture to match new screen size
    if (ui_tex_) {
        ui_tex_->destroy(gpu_ctx_->device());
        ui_tex_->createRenderTarget(gpu_ctx_->device(),
            current_screen_width_,
            current_screen_height_,
            SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
    }

    // Recreate renderer de software (DESTRUIR renderer PRIMER, després surface)
    if (ui_renderer_ != nullptr) {
        SDL_DestroyRenderer(ui_renderer_);
        ui_renderer_ = nullptr;
    }
    if (ui_surface_ != nullptr) {
        SDL_DestroySurface(ui_surface_);
        ui_surface_ = nullptr;
    }
    ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_, SDL_PIXELFORMAT_RGBA32);
    if (ui_surface_ != nullptr) {
        ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
    }
    // Re-inicialitzar components UI amb nou renderer
    if ((ui_renderer_ != nullptr) && ui_manager_) {
        ui_manager_->initialize(ui_renderer_, theme_manager_.get(), current_screen_width_, current_screen_height_,  // physical
            base_screen_width_,
            base_screen_height_);  // logical (font size based on base)
    }

    if ((ui_renderer_ != nullptr) && app_logo_) {
        app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_);
    }
}

void Engine::renderUIToSurface() {
    if ((ui_renderer_ == nullptr) || (ui_surface_ == nullptr)) {
        return;
    }

    // Clear surface (fully transparent)
    SDL_SetRenderDrawColor(ui_renderer_, 0, 0, 0, 0);
    SDL_RenderClear(ui_renderer_);

    // Render UI (HUD, FPS counter, notifications)
    ui_manager_->render(ui_renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(), shape_manager_->getActiveShape(), shape_manager_->getConvergence(), physical_window_width_, physical_window_height_, current_screen_width_);

    // Render periodic logo overlay
    if (app_logo_) {
        app_logo_->render();
    }

    SDL_RenderPresent(ui_renderer_);  // Flush software renderer to surface
}

void Engine::uploadUISurface(SDL_GPUCommandBuffer* cmd_buf) {
    if (!ui_tex_ || !ui_tex_->isValid() || (ui_surface_ == nullptr) || !gpu_ctx_) {
        return;
    }

    int w = ui_surface_->w;
    int h = ui_surface_->h;
    auto data_size = static_cast<Uint32>(w * h * 4);  // RGBA = 4 bytes/pixel

    SDL_GPUTransferBufferCreateInfo tb_info = {};
    tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
    tb_info.size = data_size;

    SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(gpu_ctx_->device(), &tb_info);
    if (transfer == nullptr) {
        return;
    }

    void* mapped = SDL_MapGPUTransferBuffer(gpu_ctx_->device(), transfer, true);
    if (mapped == nullptr) {
        SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
        return;
    }
    memcpy(mapped, ui_surface_->pixels, data_size);
    SDL_UnmapGPUTransferBuffer(gpu_ctx_->device(), transfer);

    SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);

    SDL_GPUTextureTransferInfo src = {};
    src.transfer_buffer = transfer;
    src.offset = 0;
    src.pixels_per_row = static_cast<Uint32>(w);
    src.rows_per_layer = static_cast<Uint32>(h);

    SDL_GPUTextureRegion dst = {};
    dst.texture = ui_tex_->texture();
    dst.mip_level = 0;
    dst.layer = 0;
    dst.x = dst.y = dst.z = 0;
    dst.w = static_cast<Uint32>(w);
    dst.h = static_cast<Uint32>(h);
    dst.d = 1;

    SDL_UploadToGPUTexture(copy, &src, &dst, false);
    SDL_EndGPUCopyPass(copy);

    SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
}