#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 <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/atom_shape.hpp"         // for AtomShape
#include "shapes/cube_shape.hpp"         // for CubeShape
#include "shapes/cylinder_shape.hpp"     // for CylinderShape
#include "shapes/helix_shape.hpp"        // for HelixShape
#include "shapes/icosahedron_shape.hpp"  // for IcosahedronShape
#include "shapes/lissajous_shape.hpp"    // for LissajousShape
#include "shapes/png_shape.hpp"          // for PNGShape
#include "shapes/sphere_shape.hpp"       // for SphereShape
#include "shapes/torus_shape.hpp"        // for TorusShape

// getExecutableDirectory() ya está definido en defines.h como inline

// Implementación de métodos públicos
bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) {
    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() << std::endl;
        return false;
    }

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

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

    if (displays) 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 zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4)
    current_window_zoom_ = 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)
        // NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
        // El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
        Uint32 window_flags = SDL_WINDOW_OPENGL;
        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() << std::endl;
            success = false;
        } else {
            // Centrar ventana en pantalla si no está en fullscreen
            if (!fullscreen) {
                SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
            }

            // Crear renderizador
            renderer_ = SDL_CreateRenderer(window_, nullptr);
            if (renderer_ == nullptr) {
                std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
                success = false;
            } else {
                // Establecer color inicial del renderizador
                SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);

                // Establecer tamaño lógico para el renderizado (resolución interna)
                SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);

                // Configurar V-Sync inicial
                SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
            }
        }
    }

    // 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;
                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)) {
            // Cargar todas las texturas desde disco
            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 y obtener dimensiones
                    auto texture = std::make_shared<Texture>(renderer_, fullpath);
                    int width = texture->getWidth();

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

                // Filtrar solo los recursos en balls/ con extensión .png
                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);                  // Quitar "balls/"
                        std::string name = tex_name.substr(0, tex_name.find('.'));  // Quitar extensión

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

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

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

        // Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
        for (const auto& info : texture_files) {
            textures_.push_back(info.texture);
            texture_names_.push_back(info.name);
        }

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

        // Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
        current_texture_index_ = 0;  // Fallback
        for (size_t i = 0; i < texture_names_.size(); i++) {
            if (texture_names_[i] == "normal") {
                current_texture_index_ = i;  // Iniciar en "normal" (índice 1)
                break;
            }
        }
        texture_ = textures_[current_texture_index_];
        current_ball_size_ = texture_->getWidth();  // Obtener tamaño dinámicamente

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

        // 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, 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(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);  // Callback al Engine

        // 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) + executeEnterLogoMode() (tema, PNG_SHAPE, etc.)
        }

        // 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(renderer_, current_screen_width_, current_screen_height_)) {
            std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl;
            // 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;
        }
    }

    return success;
}

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

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

        update();
        render();
    }
}

void Engine::shutdown() {
    // Limpiar recursos SDL
    if (renderer_) {
        SDL_DestroyRenderer(renderer_);
        renderer_ = nullptr;
    }
    if (window_) {
        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() {
    // 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) {
        // Modo 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_convergence_, active_shape_.get());

    // 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
        const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
        showNotificationForAction(shape_names[static_cast<int>(current_shape_type_)]);
    }
}

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

void Engine::handleShapeScaleChange(bool increase) {
    if (current_mode_ == SimulationMode::SHAPE) {
        if (increase) {
            shape_scale_factor_ += SHAPE_SCALE_STEP;
        } else {
            shape_scale_factor_ -= SHAPE_SCALE_STEP;
        }
        clampShapeScale();
        showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
    }
}

void Engine::resetShapeScale() {
    if (current_mode_ == SimulationMode::SHAPE) {
        shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
        showNotificationForAction("Escala 100%");
    }
}

void Engine::toggleDepthZoom() {
    if (current_mode_ == SimulationMode::SHAPE) {
        depth_zoom_enabled_ = !depth_zoom_enabled_;
        showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off");
    }
}

// Boids (comportamiento de enjambre)
void Engine::toggleBoidsMode(bool force_gravity_on) {
    if (current_mode_ == SimulationMode::BOIDS) {
        // Salir del modo boids (velocidades ya son time-based, no requiere conversión)
        current_mode_ = SimulationMode::PHYSICS;
        boid_manager_->deactivateBoids(force_gravity_on);  // Pasar parámetro para control preciso
    } else {
        // Entrar al modo boids (desde PHYSICS o SHAPE)
        if (current_mode_ == SimulationMode::SHAPE) {
            // Si estamos en modo shape, salir primero sin forzar gravedad
            current_mode_ = SimulationMode::PHYSICS;

            // Desactivar atracción de figuras
            auto& balls = scene_manager_->getBallsMutable();
            for (auto& ball : balls) {
                ball->enableShapeAttraction(false);
                ball->setDepthScale(1.0f);
            }
        }

        // Activar modo boids
        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) {
    // 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();

        // Activar atracción física en las bolas nuevas (crítico tras changeScenario)
        auto& balls = scene_manager_->getBallsMutable();
        for (auto& ball : balls) {
            ball->enableShapeAttraction(true);
        }
    }

    // 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() {
    // Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);  // Negro para barras de letterbox/integer
    SDL_RenderClear(renderer_);

    // Renderizar fondo degradado (delegado a ThemeManager)
    {
        float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
        theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b);

        // Crear quad de pantalla completa con degradado
        SDL_Vertex bg_vertices[4];

        // Vértice superior izquierdo
        bg_vertices[0].position = {0, 0};
        bg_vertices[0].tex_coord = {0.0f, 0.0f};
        bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};

        // Vértice superior derecho
        bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
        bg_vertices[1].tex_coord = {1.0f, 0.0f};
        bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};

        // Vértice inferior derecho
        bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
        bg_vertices[2].tex_coord = {1.0f, 1.0f};
        bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};

        // Vértice inferior izquierdo
        bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
        bg_vertices[3].tex_coord = {0.0f, 1.0f};
        bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};

        // Índices para 2 triángulos
        int bg_indices[6] = {0, 1, 2, 2, 3, 0};

        // Renderizar sin textura (nullptr)
        SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
    }

    // Limpiar batches del frame anterior
    batch_vertices_.clear();
    batch_indices_.clear();

    // Obtener referencia a las bolas desde SceneManager
    const auto& balls = scene_manager_->getBalls();

    if (current_mode_ == SimulationMode::SHAPE) {
        // MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
        // Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero

        // Bucket sort per profunditat Z (O(N) vs O(N log N))
        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);
        }

        // Renderizar en orden de profundidad (bucket 0 = fons, bucket 255 = davant)
        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);  // Usar color interpolado (LERP)
                float brightness = balls[idx]->getDepthBrightness();
                float depth_scale = balls[idx]->getDepthScale();

                // Mapear brightness de 0-1 a rango MIN-MAX
                float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;

                // Aplicar factor de brillo al color
                int r_mod = static_cast<int>(color.r * brightness_factor);
                int g_mod = static_cast<int>(color.g * brightness_factor);
                int b_mod = static_cast<int>(color.b * brightness_factor);

                addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
            }
            depth_buckets_[b].clear();  // netejar per al proper frame
        }
    } else {
        // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
        const auto& balls = scene_manager_->getBalls();
        size_t idx = 0;
        for (auto& ball : balls) {
            SDL_FRect pos = ball->getPosition();
            Color color = theme_manager_->getInterpolatedColor(idx);  // Usar color interpolado (LERP)
            addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
            idx++;
        }
    }

    // Renderizar todas las bolas en una sola llamada
    if (!batch_vertices_.empty()) {
        SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
    }

    // SISTEMA DE TEXTO ANTIGUO DESHABILITADO
    // Reemplazado completamente por el sistema de notificaciones (Notifier)
    // El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
    /*
    if (show_text_) {
        // Obtener datos del tema actual (delegado a ThemeManager)
        int text_color_r, text_color_g, text_color_b;
        theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
        const char* theme_name_es = theme_manager_->getCurrentThemeNameES();

        // Calcular espaciado dinámico
        int line_height = text_renderer_.getTextHeight();
        int margin = 8;

        // Texto del número de pelotas con color del tema
        text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);

        // Mostrar nombre del tema en castellano debajo del número de pelotas
        // (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
        if (theme_name_es != nullptr && text_ != theme_name_es) {
            int theme_text_width = text_renderer_.getTextWidth(theme_name_es);
            int theme_x = (current_screen_width_ - theme_text_width) / 2;  // Centrar horizontalmente
            int theme_y = margin + line_height;  // Espaciado dinámico

            // Texto del nombre del tema con el mismo color
            text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
        }
    }
    */

    // Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager
    ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
                       active_shape_.get(), shape_convergence_,
                       physical_window_width_, physical_window_height_, current_screen_width_);

    // Renderizar AppLogo (logo periódico) - después de UI, antes de present
    if (app_logo_) {
        app_logo_->render();
    }

    SDL_RenderPresent(renderer_);
}

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 renderizador
    SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
}

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_) {
        SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
        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);

                // Actualizar presentación lógica del renderizador
                SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);

                // 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 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

                    // Activar atracción física en las bolas nuevas (crítico tras changeScenario)
                    auto& balls = scene_manager_->getBallsMutable();
                    for (auto& ball : balls) {
                        ball->enableShapeAttraction(true);
                    }
                }
            }
            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_;

        // Restaurar ventana normal con el zoom actual (no hardcoded)
        SDL_SetWindowFullscreen(window_, false);
        SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
        SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);

        // Restaurar presentación lógica base
        SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);

        // 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 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

            // Activar atracción física en las bolas nuevas (crítico tras changeScenario)
            auto& balls = scene_manager_->getBallsMutable();
            for (auto& ball : balls) {
                ball->enableShapeAttraction(true);
            }
        }
    }
}

void Engine::toggleIntegerScaling() {
    // Solo permitir cambio si estamos en modo fullscreen normal (F3)
    if (!fullscreen_enabled_) {
        return;  // No hacer nada si no estamos en fullscreen
    }

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

    // Aplicar el nuevo modo de escalado
    SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
    const char* mode_name = "INTEGER";

    switch (current_scaling_mode_) {
        case ScalingMode::INTEGER:
            presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
            mode_name = "INTEGER";
            break;
        case ScalingMode::LETTERBOX:
            presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX;
            mode_name = "LETTERBOX";
            break;
        case ScalingMode::STRETCH:
            presentation = SDL_LOGICAL_PRESENTATION_STRETCH;
            mode_name = "STRETCH";
            break;
    }

    SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);

    // Mostrar notificación del cambio
    std::string notification = std::string("Escalado: ") + mode_name;
    ui_manager_->showNotification(notification);
}

void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
    int vertex_index = static_cast<int>(batch_vertices_.size());

    // Crear 4 vértices para el quad (2 triángulos)
    SDL_Vertex vertices[4];

    // Convertir colores de int (0-255) a float (0.0-1.0)
    float rf = r / 255.0f;
    float gf = g / 255.0f;
    float bf = b / 255.0f;

    // Aplicar escala al tamaño (centrado en el punto x, y)
    float scaled_w = w * scale;
    float scaled_h = h * scale;
    float offset_x = (w - scaled_w) / 2.0f;  // Offset para centrar
    float offset_y = (h - scaled_h) / 2.0f;

    // Vértice superior izquierdo
    vertices[0].position = {x + offset_x, y + offset_y};
    vertices[0].tex_coord = {0.0f, 0.0f};
    vertices[0].color = {rf, gf, bf, 1.0f};

    // Vértice superior derecho
    vertices[1].position = {x + offset_x + scaled_w, y + offset_y};
    vertices[1].tex_coord = {1.0f, 0.0f};
    vertices[1].color = {rf, gf, bf, 1.0f};

    // Vértice inferior derecho
    vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h};
    vertices[2].tex_coord = {1.0f, 1.0f};
    vertices[2].color = {rf, gf, bf, 1.0f};

    // Vértice inferior izquierdo
    vertices[3].position = {x + offset_x, y + offset_y + scaled_h};
    vertices[3].tex_coord = {0.0f, 1.0f};
    vertices[3].color = {rf, gf, bf, 1.0f};

    // Añadir vértices al batch
    for (int i = 0; i < 4; i++) {
        batch_vertices_.push_back(vertices[i]);
    }

    // Añadir índices para 2 triángulos
    batch_indices_.push_back(vertex_index + 0);
    batch_indices_.push_back(vertex_index + 1);
    batch_indices_.push_back(vertex_index + 2);
    batch_indices_.push_back(vertex_index + 2);
    batch_indices_.push_back(vertex_index + 3);
    batch_indices_.push_back(vertex_index + 0);
}

// Sistema de zoom dinámico
int Engine::calculateMaxWindowZoom() const {
    // Obtener información del display usando el método de Coffee Crisis
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    if (displays == nullptr || num_displays == 0) {
        return WINDOW_ZOOM_MIN;  // Fallback si no se puede obtener
    }

    // Obtener el modo de display actual
    const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
    if (dm == nullptr) {
        SDL_free(displays);
        return WINDOW_ZOOM_MIN;
    }

    // Calcular zoom máximo usando la fórmula de Coffee Crisis
    const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);

    SDL_free(displays);

    // Aplicar límites
    return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX));
}

void Engine::setWindowZoom(int new_zoom) {
    // Validar zoom
    int max_zoom = calculateMaxWindowZoom();
    new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom));

    if (new_zoom == current_window_zoom_) {
        return;  // No hay cambio
    }

    // Obtener posición actual del centro de la ventana
    int current_x, current_y;
    SDL_GetWindowPosition(window_, &current_x, &current_y);
    int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
    int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;

    // Calcular nuevo tamaño
    int new_width = base_screen_width_ * new_zoom;
    int new_height = base_screen_height_ * new_zoom;

    // Calcular nueva posición (centrada en el punto actual)
    int new_x = current_center_x - new_width / 2;
    int new_y = current_center_y - new_height / 2;

    // Obtener límites del escritorio para no salirse
    SDL_Rect display_bounds;
    if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) {
        // Aplicar márgenes
        int min_x = WINDOW_DESKTOP_MARGIN;
        int min_y = WINDOW_DESKTOP_MARGIN;
        int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN;
        int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT;

        // Limitar posición
        new_x = std::max(min_x, std::min(new_x, max_x));
        new_y = std::max(min_y, std::min(new_y, max_y));
    }

    // Aplicar cambios
    SDL_SetWindowSize(window_, new_width, new_height);
    SDL_SetWindowPosition(window_, new_x, new_y);
    current_window_zoom_ = new_zoom;

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

void Engine::zoomIn() {
    setWindowZoom(current_window_zoom_ + 1);
}

void Engine::zoomOut() {
    setWindowZoom(current_window_zoom_ - 1);
}

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, 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)
    ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_);
}

// ============================================================================
// CALLBACKS PARA STATEMANAGER
// ============================================================================
// StateManager coordina los estados y timers, Engine proporciona implementación
// Estos callbacks permiten que StateManager ejecute acciones complejas que
// requieren acceso a múltiples componentes (SceneManager, ThemeManager, etc.)
// Este enfoque es pragmático y mantiene la separación de responsabilidades

// Callback para ejecutar acciones de LOGO MODE (máquina de estados compleja)
void Engine::performLogoAction(bool logo_waiting_for_flip) {
    // Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
    if (state_manager_->getCurrentMode() == AppMode::SANDBOX) return;

    // Actualizar timer
    demo_timer_ += delta_time_;

    // Determinar si es hora de ejecutar acción (depende del modo)
    bool should_trigger = false;

    if (state_manager_->getCurrentMode() == AppMode::LOGO) {
        // LOGO MODE: Dos caminos posibles
        if (logo_waiting_for_flip_) {
            // CAMINO B: Esperando a que ocurran flips
            // Obtener referencia a PNGShape si está activa
            PNGShape* png_shape = nullptr;
            if (active_shape_ && current_mode_ == SimulationMode::SHAPE) {
                png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
            }

            if (png_shape) {
                int current_flip_count = png_shape->getFlipCount();

                // Detectar nuevo flip completado
                if (current_flip_count > logo_current_flip_count_) {
                    logo_current_flip_count_ = current_flip_count;
                }

                // Si estamos EN o DESPUÉS del flip objetivo
                // +1 porque queremos actuar DURANTE el flip N, no después de completarlo
                if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
                    // Monitorear progreso del flip actual
                    if (png_shape->isFlipping()) {
                        float flip_progress = png_shape->getFlipProgress();
                        if (flip_progress >= logo_target_flip_percentage_) {
                            should_trigger = true;  // ¡Trigger durante el flip!
                        }
                    }
                }
            }
        } else {
            // CAMINO A: Esperar convergencia + tiempo (comportamiento original)
            bool min_time_reached = demo_timer_ >= logo_min_time_;
            bool max_time_reached = demo_timer_ >= logo_max_time_;
            bool convergence_ok = shape_convergence_ >= logo_convergence_threshold_;

            should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
        }
    } else {
        // DEMO/DEMO_LITE: Timer simple como antes
        should_trigger = demo_timer_ >= demo_next_action_time_;
    }

    // Si es hora de ejecutar acción
    if (should_trigger) {
        // MODO LOGO: Sistema de acciones variadas con gravedad dinámica
        if (state_manager_->getCurrentMode() == AppMode::LOGO) {
            // Elegir acción aleatoria ponderada
            int action = rand() % 100;

            if (current_mode_ == SimulationMode::SHAPE) {
                // Logo quieto (formado) → Decidir camino a seguir

                // DECISIÓN BIFURCADA: ¿Cambio inmediato o esperar flips?
                if (logo_waiting_for_flip_) {
                    // Ya estábamos esperando flips, y se disparó el trigger
                    // → Hacer el cambio SHAPE → PHYSICS ahora (durante el flip)
                    if (action < 50) {
                        toggleShapeModeInternal(true);   // Con gravedad ON
                    } else {
                        toggleShapeModeInternal(false);  // Con gravedad OFF
                    }

                    // Resetear variables de espera de flips
                    logo_waiting_for_flip_ = false;
                    logo_current_flip_count_ = 0;

                    // Resetear timer
                    demo_timer_ = 0.0f;
                    float interval_range = logo_max_time_ - logo_min_time_;
                    demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
                } else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
                    // CAMINO B (50%): Esperar a que ocurran 1-3 flips
                    logo_waiting_for_flip_ = true;
                    logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
                    logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
                    logo_current_flip_count_ = 0;

                    // Resetear contador de flips en PNGShape
                    if (active_shape_) {
                        PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
                        if (png_shape) {
                            png_shape->resetFlipCount();
                        }
                    }

                    // NO hacer nada más este frame - esperar a que ocurran los flips
                    // El trigger se ejecutará en futuras iteraciones cuando se cumplan las condiciones
                } else {
                    // CAMINO A (50%): Cambio inmediato
                    if (action < 50) {
                        // 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
                        toggleShapeModeInternal(true);
                    } else {
                        // 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
                        toggleShapeModeInternal(false);
                    }

                    // Resetear variables de espera de flips al cambiar a PHYSICS
                    logo_waiting_for_flip_ = false;
                    logo_current_flip_count_ = 0;

                    // Resetear timer con intervalos escalados
                    demo_timer_ = 0.0f;
                    float interval_range = logo_max_time_ - logo_min_time_;
                    demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
                }
            } else {
                // Logo animado (PHYSICS) → 4 opciones posibles
                if (action < 50) {
                    // 50%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
                    toggleShapeModeInternal(false);

                    // Resetear variables de espera de flips al volver a SHAPE
                    logo_waiting_for_flip_ = false;
                    logo_current_flip_count_ = 0;
                } else if (action < 68) {
                    // 18%: Forzar gravedad ON (empezar a caer mientras da vueltas)
                    scene_manager_->forceBallsGravityOn();
                } else if (action < 84) {
                    // 16%: Forzar gravedad OFF (flotar mientras da vueltas)
                    scene_manager_->forceBallsGravityOff();
                } else {
                    // 16%: Cambiar dirección de gravedad (nueva variación)
                    GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
                    scene_manager_->changeGravityDirection(new_direction);
                    // Si la gravedad está OFF, activarla para que el cambio sea visible
                    scene_manager_->forceBallsGravityOn();
                }

                // Resetear timer con intervalos escalados
                demo_timer_ = 0.0f;
                float interval_range = logo_max_time_ - logo_min_time_;
                demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
            }

            // Solo salir automáticamente si la entrada a LOGO fue automática (desde DEMO)
            // No salir si el usuario entró manualmente con tecla K
            // Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
            if (!state_manager_->getLogoEnteredManually() && rand() % 100 < 60) {
                state_manager_->exitLogoMode(true);  // Volver a DEMO/DEMO_LITE
            }
        }
        // MODO DEMO/DEMO_LITE: Acciones normales
        else {
            bool is_lite = (state_manager_->getCurrentMode() == AppMode::DEMO_LITE);
            executeDemoAction(is_lite);

            // Resetear timer y calcular próximo intervalo aleatorio
            demo_timer_ = 0.0f;

            // Usar intervalos diferentes según modo
            float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
            float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
            float interval_range = interval_max - interval_min;
            demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
        }
    }
}

// Callback para StateManager - Ejecutar acción DEMO
void Engine::executeDemoAction(bool is_lite) {
    // ============================================
    // SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
    // ============================================

    if (is_lite) {
        // DEMO LITE: Verificar condiciones para salto a Logo Mode
        if (static_cast<int>(scene_manager_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX] &&
            theme_manager_->getCurrentThemeIndex() == 5) {  // MONOCHROME
            // 10% probabilidad de saltar a Logo Mode
            if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
                state_manager_->enterLogoMode(true, current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
                return;
            }
        }
    } else {
        // DEMO COMPLETO: Verificar condiciones para salto a Logo Mode
        if (static_cast<int>(scene_manager_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX]) {
            // 15% probabilidad de saltar a Logo Mode
            if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
                state_manager_->enterLogoMode(true, current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
                return;
            }
        }
    }

    // ============================================
    // ACCIONES NORMALES DE DEMO/DEMO_LITE
    // ============================================

    int TOTAL_WEIGHT;
    int random_value;
    int accumulated_weight = 0;

    if (is_lite) {
        // DEMO LITE: Solo física/figuras
        TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
        random_value = rand() % TOTAL_WEIGHT;

        // Cambiar dirección gravedad (25%)
        accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
        if (random_value < accumulated_weight) {
            GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
            scene_manager_->changeGravityDirection(new_direction);
            return;
        }

        // Toggle gravedad ON/OFF (20%)
        accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
        if (random_value < accumulated_weight) {
            executeToggleGravityOnOff();
            return;
        }

        // Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode)
        accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
        if (random_value < accumulated_weight) {
            ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            int shape_index = rand() % 8;
            activateShapeInternal(shapes[shape_index]);
            return;
        }

        // Toggle física ↔ figura (20%)
        accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
        if (random_value < accumulated_weight) {
            toggleShapeModeInternal(false);  // NO forzar gravedad al salir
            return;
        }

        // Aplicar impulso (10%)
        accumulated_weight += DEMO_LITE_WEIGHT_IMPULSE;
        if (random_value < accumulated_weight) {
            pushBallsAwayFromGravity();
            return;
        }

    } else {
        // DEMO COMPLETO: Todas las acciones
        TOTAL_WEIGHT = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
        random_value = rand() % TOTAL_WEIGHT;

        // Cambiar dirección gravedad (10%)
        accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
        if (random_value < accumulated_weight) {
            GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
            scene_manager_->changeGravityDirection(new_direction);
            return;
        }

        // Toggle gravedad ON/OFF (8%)
        accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
        if (random_value < accumulated_weight) {
            executeToggleGravityOnOff();
            return;
        }

        // Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode)
        accumulated_weight += DEMO_WEIGHT_SHAPE;
        if (random_value < accumulated_weight) {
            ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            int shape_index = rand() % 8;
            activateShapeInternal(shapes[shape_index]);
            return;
        }

        // Toggle física ↔ figura (12%)
        accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
        if (random_value < accumulated_weight) {
            toggleShapeModeInternal(false);  // NO forzar gravedad al salir
            return;
        }

        // Re-generar misma figura (8%)
        accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE;
        if (random_value < accumulated_weight) {
            if (current_mode_ == SimulationMode::SHAPE && active_shape_) {
                generateShape();  // Re-generar sin cambiar tipo
            }
            return;
        }

        // Cambiar tema (15%)
        accumulated_weight += DEMO_WEIGHT_THEME;
        if (random_value < accumulated_weight) {
            // Elegir entre TODOS los 15 temas (9 estáticos + 6 dinámicos)
            int random_theme_index = rand() % 15;
            theme_manager_->switchToTheme(random_theme_index);
            return;
        }

        // Cambiar escenario (10%) - rango dinámico según benchmark de rendimiento
        accumulated_weight += DEMO_WEIGHT_SCENARIO;
        if (random_value < accumulated_weight) {
            int auto_max = std::min(max_auto_scenario_, DEMO_AUTO_MAX_SCENARIO);
            std::vector<int> candidates;
            for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i)
                candidates.push_back(i);
            if (custom_scenario_enabled_ && custom_auto_available_)
                candidates.push_back(CUSTOM_SCENARIO_IDX);
            int new_scenario = candidates[rand() % candidates.size()];
            scene_manager_->changeScenario(new_scenario, current_mode_);

            // Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
            if (current_mode_ == SimulationMode::SHAPE) {
                generateShape();

                // Activar atracción física en las bolas nuevas (crítico tras changeScenario)
                auto& balls = scene_manager_->getBallsMutable();
                for (auto& ball : balls) {
                    ball->enableShapeAttraction(true);
                }
            }

            return;
        }

        // Aplicar impulso (10%)
        accumulated_weight += DEMO_WEIGHT_IMPULSE;
        if (random_value < accumulated_weight) {
            pushBallsAwayFromGravity();
            return;
        }

        // Toggle profundidad (3%)
        accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM;
        if (random_value < accumulated_weight) {
            if (current_mode_ == SimulationMode::SHAPE) {
                depth_zoom_enabled_ = !depth_zoom_enabled_;
            }
            return;
        }

        // Cambiar escala de figura (2%)
        accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE;
        if (random_value < accumulated_weight) {
            if (current_mode_ == SimulationMode::SHAPE) {
                int scale_action = rand() % 3;
                if (scale_action == 0) {
                    shape_scale_factor_ += SHAPE_SCALE_STEP;
                } else if (scale_action == 1) {
                    shape_scale_factor_ -= SHAPE_SCALE_STEP;
                } else {
                    shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
                }
                clampShapeScale();
                generateShape();
            }
            return;
        }

        // Cambiar sprite (2%)
        accumulated_weight += DEMO_WEIGHT_SPRITE;
        if (random_value < accumulated_weight) {
            switchTextureInternal(false);  // Suprimir notificación en modo automático
            return;
        }
    }
}

// Callback para StateManager - Randomizar estado al iniciar DEMO
void Engine::executeRandomizeOnDemoStart(bool is_lite) {
    // Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
    // PNG_SHAPE es exclusivo del modo LOGO y no debe aparecer en DEMO/DEMO_LITE
    if (current_shape_type_ == ShapeType::PNG_SHAPE) {
        ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
                              ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
                              ShapeType::ICOSAHEDRON, ShapeType::ATOM};
        activateShapeInternal(shapes[rand() % 8]);
    }

    if (is_lite) {
        // DEMO LITE: Solo randomizar física/figura + gravedad
        // Elegir aleatoriamente entre modo física o figura
        if (rand() % 2 == 0) {
            // Modo física
            if (current_mode_ == SimulationMode::SHAPE) {
                toggleShapeModeInternal(false);  // Salir a física sin forzar gravedad
            }
        } else {
            // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
            ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            activateShapeInternal(shapes[rand() % 8]);
        }

        // Randomizar gravedad: dirección + ON/OFF
        GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
        scene_manager_->changeGravityDirection(new_direction);
        if (rand() % 2 == 0) {
            executeToggleGravityOnOff();  // 50% probabilidad de desactivar gravedad
        }

    } else {
        // DEMO COMPLETO: Randomizar TODO

        // 1. Física o Figura (decidir PRIMERO antes de cambiar escenario)
        if (rand() % 2 == 0) {
            // Modo física
            if (current_mode_ == SimulationMode::SHAPE) {
                toggleShapeModeInternal(false);
            }
        } else {
            // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
            ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            ShapeType selected_shape = shapes[rand() % 8];

            // Configurar figura SIN generar puntos (changeScenario lo hará después)
            last_shape_type_ = selected_shape;
            current_shape_type_ = selected_shape;
            current_mode_ = SimulationMode::SHAPE;

            // Crear instancia de la figura sin generar puntos todavía
            switch (selected_shape) {
                case ShapeType::SPHERE:
                    active_shape_ = std::make_unique<SphereShape>();
                    break;
                case ShapeType::CUBE:
                    active_shape_ = std::make_unique<CubeShape>();
                    break;
                case ShapeType::HELIX:
                    active_shape_ = std::make_unique<HelixShape>();
                    break;
                case ShapeType::TORUS:
                    active_shape_ = std::make_unique<TorusShape>();
                    break;
                case ShapeType::LISSAJOUS:
                    active_shape_ = std::make_unique<LissajousShape>();
                    break;
                case ShapeType::CYLINDER:
                    active_shape_ = std::make_unique<CylinderShape>();
                    break;
                case ShapeType::ICOSAHEDRON:
                    active_shape_ = std::make_unique<IcosahedronShape>();
                    break;
                case ShapeType::ATOM:
                    active_shape_ = std::make_unique<AtomShape>();
                    break;
                default:
                    active_shape_ = std::make_unique<SphereShape>();
                    break;
            }

            // Profundidad (solo si estamos en figura)
            if (rand() % 2 == 0) {
                depth_zoom_enabled_ = !depth_zoom_enabled_;
            }

            // Escala de figura (aleatoria entre 0.5x y 2.0x)
            shape_scale_factor_ = 0.5f + (rand() % 1500) / 1000.0f;
            clampShapeScale();

            // NOTA: NO llamar a generateShape() ni activar atracción aquí
            // changeScenario() creará las pelotas y luego llamará a generateShape()
        }

        // 2. Escenario - rango dinámico según benchmark de rendimiento
        int auto_max = std::min(max_auto_scenario_, DEMO_AUTO_MAX_SCENARIO);
        std::vector<int> candidates;
        for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i)
            candidates.push_back(i);
        if (custom_scenario_enabled_ && custom_auto_available_)
            candidates.push_back(CUSTOM_SCENARIO_IDX);
        int new_scenario = candidates[rand() % candidates.size()];
        scene_manager_->changeScenario(new_scenario, current_mode_);

        // Si estamos en modo SHAPE, generar la figura y activar atracción
        if (current_mode_ == SimulationMode::SHAPE) {
            generateShape();

            // Activar atracción física en las bolas nuevas
            auto& balls = scene_manager_->getBallsMutable();
            for (auto& ball : balls) {
                ball->enableShapeAttraction(true);
            }
        }

        // 3. Tema (elegir entre TODOS los 15 temas)
        int random_theme_index = rand() % 15;
        theme_manager_->switchToTheme(random_theme_index);

        // 4. Sprite
        if (rand() % 2 == 0) {
            switchTextureInternal(false);  // Suprimir notificación al activar modo DEMO
        }

        // 5. Gravedad: dirección + ON/OFF
        GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
        scene_manager_->changeGravityDirection(new_direction);
        if (rand() % 3 == 0) {  // 33% probabilidad de desactivar gravedad
            executeToggleGravityOnOff();
        }
    }
}

// Callback para StateManager - Toggle gravedad ON/OFF para todas las pelotas
void Engine::executeToggleGravityOnOff() {
    // Alternar entre activar/desactivar gravedad
    bool first_ball_gravity_enabled = (!scene_manager_->hasBalls() || scene_manager_->getFirstBall()->getGravityForce() > 0.0f);

    if (first_ball_gravity_enabled) {
        // Desactivar gravedad
        scene_manager_->forceBallsGravityOff();
    } else {
        // Activar gravedad
        scene_manager_->forceBallsGravityOn();
    }
}

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

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

    // Ocultar ventana y desactivar V-sync para medición limpia
    SDL_HideWindow(window_);
    SDL_SetRenderVSync(renderer_, 0);

    const int BENCH_DURATION_MS = 600;
    const int WARMUP_FRAMES = 5;

    SimulationMode original_mode = current_mode_;

    auto restore = [&]() {
        SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
        SDL_ShowWindow(window_);
        current_mode_ = original_mode;
        active_shape_.reset();
        scene_manager_->changeScenario(0, original_mode);
        last_frame_time_ = 0;
    };

    // Test escenario custom (independiente de max_auto_scenario_)
    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_FRAMES; ++w) {
            calculateDeltaTime();
            SDL_Event e; while (SDL_PollEvent(&e)) {}
            update();
            render();
        }
        int frame_count = 0;
        Uint64 start = SDL_GetTicks();
        while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
            calculateDeltaTime();
            SDL_Event e; while (SDL_PollEvent(&e)) {}
            update();
            render();
            ++frame_count;
        }
        float fps = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
        custom_auto_available_ = (fps >= monitor_hz);
    }

    // Probar de más pesado a más ligero
    for (int idx = DEMO_AUTO_MAX_SCENARIO; idx >= DEMO_AUTO_MIN_SCENARIO; --idx) {
        scene_manager_->changeScenario(idx, SimulationMode::SHAPE);
        activateShapeInternal(ShapeType::SPHERE);

        // Warmup: estabilizar física y pipeline GPU
        last_frame_time_ = 0;
        for (int w = 0; w < WARMUP_FRAMES; ++w) {
            calculateDeltaTime();
            SDL_Event e; while (SDL_PollEvent(&e)) {}
            update();
            render();
        }

        // Medición
        int frame_count = 0;
        Uint64 start = SDL_GetTicks();
        while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
            calculateDeltaTime();
            SDL_Event e;
            while (SDL_PollEvent(&e)) { /* descartar */ }
            update();
            render();
            ++frame_count;
        }

        float measured_fps = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
        if (measured_fps >= monitor_hz) {
            max_auto_scenario_ = idx;
            restore();
            return;
        }
    }
    // Fallback: escenario mínimo
    max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
    restore();
}

// ============================================================================
// CALLBACKS PARA STATEMANAGER - LOGO MODE
// ============================================================================

// Callback para StateManager - Configuración visual al entrar a LOGO MODE
void Engine::executeEnterLogoMode(size_t ball_count) {
    // Verificar mínimo de pelotas (LOGO_MIN_SCENARIO_IDX = índice 4 → 1000 bolas)
    if (scene_manager_->getCurrentScenario() < LOGO_MIN_SCENARIO_IDX) {
        scene_manager_->changeScenario(LOGO_MIN_SCENARIO_IDX, current_mode_);
    }

    // Guardar estado previo (para restaurar al salir)
    logo_previous_theme_ = theme_manager_->getCurrentThemeIndex();
    logo_previous_texture_index_ = current_texture_index_;
    logo_previous_shape_scale_ = shape_scale_factor_;

    // Buscar índice de textura "small"
    size_t small_index = current_texture_index_;  // Por defecto mantener actual
    for (size_t i = 0; i < texture_names_.size(); i++) {
        if (texture_names_[i] == "small") {
            small_index = i;
            break;
        }
    }

    // Aplicar configuración fija del Modo Logo
    if (small_index != current_texture_index_) {
        current_texture_index_ = small_index;
        texture_ = textures_[current_texture_index_];
        int new_size = texture_->getWidth();
        current_ball_size_ = new_size;
        scene_manager_->updateBallTexture(texture_, new_size);
    }

    // Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
    int logo_themes[] = {5, 6, 7, 8};  // MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
    int random_theme = logo_themes[rand() % 4];
    theme_manager_->switchToTheme(random_theme);

    // Establecer escala a 120%
    shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE;
    clampShapeScale();

    // Activar PNG_SHAPE (el logo)
    activateShapeInternal(ShapeType::PNG_SHAPE);

    // Configurar PNG_SHAPE en modo LOGO (flip intervals más largos)
    if (active_shape_) {
        PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
        if (png_shape) {
            png_shape->setLogoMode(true);
            png_shape->resetFlipCount();  // Resetear contador de flips
        }
    }
}

void Engine::executeExitLogoMode() {
    // Restaurar estado visual previo
    theme_manager_->switchToTheme(logo_previous_theme_);

    if (logo_previous_texture_index_ != current_texture_index_ &&
        logo_previous_texture_index_ < textures_.size()) {
        current_texture_index_ = logo_previous_texture_index_;
        texture_ = textures_[current_texture_index_];
        int new_size = texture_->getWidth();
        current_ball_size_ = new_size;
        scene_manager_->updateBallTexture(texture_, new_size);
    }

    shape_scale_factor_ = logo_previous_shape_scale_;
    clampShapeScale();
    generateShape();

    // Activar atracción física si estamos en modo SHAPE
    // (crítico para que las bolas se muevan hacia la figura restaurada)
    if (current_mode_ == SimulationMode::SHAPE) {
        auto& balls = scene_manager_->getBallsMutable();
        for (auto& ball : balls) {
            ball->enableShapeAttraction(true);
        }
    }

    // Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales)
    if (active_shape_) {
        PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
        if (png_shape) {
            png_shape->setLogoMode(false);
        }
    }

    // Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
    if (current_shape_type_ == ShapeType::PNG_SHAPE) {
        ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
                              ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
                              ShapeType::ICOSAHEDRON, ShapeType::ATOM};
        activateShapeInternal(shapes[rand() % 8]);
    }
}

// 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::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);
        showNotificationForAction("Sprite: " + texture_name);
    }
}

// ============================================================================
// Sistema de Figuras 3D - IMPLEMENTACIÓN PARA CALLBACKS DEMO/LOGO
// ============================================================================
// NOTA: Engine mantiene implementación de figuras usada por callbacks
// ShapeManager tiene implementación paralela para controles manuales del usuario
// Este enfoque permite que DEMO/LOGO manipulen figuras sin afectar el estado manual

// Alternar entre modo física y última figura (usado por performLogoAction)
void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) {
    if (current_mode_ == SimulationMode::PHYSICS) {
        // Cambiar a modo figura (usar última figura seleccionada)
        activateShapeInternal(last_shape_type_);

        // Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
        if (state_manager_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
            if (active_shape_) {
                PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
                if (png_shape) {
                    png_shape->setLogoMode(true);
                }
            }
        }

        // Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%)
        if (state_manager_->getCurrentMode() == AppMode::LOGO) {
            logo_convergence_threshold_ = LOGO_CONVERGENCE_MIN +
                (rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
            shape_convergence_ = 0.0f;  // Reset convergencia al entrar
        }
    } else {
        // Volver a modo física normal
        current_mode_ = SimulationMode::PHYSICS;

        // Desactivar atracción y resetear escala de profundidad
        auto& balls = scene_manager_->getBallsMutable();
        for (auto& ball : balls) {
            ball->enableShapeAttraction(false);
            ball->setDepthScale(1.0f);  // Reset escala a 100% (evita "pop" visual)
        }

        // Activar gravedad al salir (solo si se especifica)
        if (force_gravity_on_exit) {
            scene_manager_->forceBallsGravityOn();
        }

        // Mostrar notificación (solo si NO estamos en modo demo o logo)
        if (state_manager_->getCurrentMode() == AppMode::SANDBOX) {
            ui_manager_->showNotification("Modo Física");
        }
    }
}

// Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode)
void Engine::activateShapeInternal(ShapeType type) {
    // Guardar como última figura seleccionada
    last_shape_type_ = type;
    current_shape_type_ = type;

    // Cambiar a modo figura
    current_mode_ = SimulationMode::SHAPE;

    // Desactivar gravedad al entrar en modo figura
    scene_manager_->forceBallsGravityOff();

    // Crear instancia polimórfica de la figura correspondiente
    switch (type) {
        case ShapeType::SPHERE:
            active_shape_ = std::make_unique<SphereShape>();
            break;
        case ShapeType::CUBE:
            active_shape_ = std::make_unique<CubeShape>();
            break;
        case ShapeType::HELIX:
            active_shape_ = std::make_unique<HelixShape>();
            break;
        case ShapeType::TORUS:
            active_shape_ = std::make_unique<TorusShape>();
            break;
        case ShapeType::LISSAJOUS:
            active_shape_ = std::make_unique<LissajousShape>();
            break;
        case ShapeType::CYLINDER:
            active_shape_ = std::make_unique<CylinderShape>();
            break;
        case ShapeType::ICOSAHEDRON:
            active_shape_ = std::make_unique<IcosahedronShape>();
            break;
        case ShapeType::ATOM:
            active_shape_ = std::make_unique<AtomShape>();
            break;
        case ShapeType::PNG_SHAPE:
            active_shape_ = std::make_unique<PNGShape>("shapes/jailgames.png");
            break;
        default:
            active_shape_ = std::make_unique<SphereShape>();  // Fallback
            break;
    }

    // Generar puntos de la figura
    generateShape();

    // Activar atracción física en todas las pelotas
    auto& balls = scene_manager_->getBallsMutable();
    for (auto& ball : balls) {
        ball->enableShapeAttraction(true);
    }

    // Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo)
    if (active_shape_ && state_manager_->getCurrentMode() == AppMode::SANDBOX) {
        std::string notification = std::string("Modo ") + active_shape_->getName();
        ui_manager_->showNotification(notification);
    }
}

// Generar puntos de la figura activa
void Engine::generateShape() {
    if (!active_shape_) return;

    int num_points = static_cast<int>(scene_manager_->getBallCount());
    active_shape_->generatePoints(num_points, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
}

// Actualizar figura activa (rotación, animación, etc.)
void Engine::updateShape() {
    if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return;

    // Actualizar animación de la figura
    active_shape_->update(delta_time_, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));

    // Obtener factor de escala para física (base de figura + escala manual)
    float scale_factor = active_shape_->getScaleFactor(static_cast<float>(current_screen_height_)) * shape_scale_factor_;

    // Centro de la pantalla
    float center_x = current_screen_width_ / 2.0f;
    float center_y = current_screen_height_ / 2.0f;

    // Obtener referencia mutable a las bolas desde SceneManager
    auto& balls = scene_manager_->getBallsMutable();

    // Actualizar cada pelota con física de atracción
    for (size_t i = 0; i < balls.size(); i++) {
        // Obtener posición 3D rotada del punto i
        float x_3d, y_3d, z_3d;
        active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);

        // Aplicar escala manual a las coordenadas 3D
        x_3d *= shape_scale_factor_;
        y_3d *= shape_scale_factor_;
        z_3d *= shape_scale_factor_;

        // Proyección 2D ortográfica (punto objetivo móvil)
        float target_x = center_x + x_3d;
        float target_y = center_y + y_3d;

        // Actualizar target de la pelota para cálculo de convergencia
        balls[i]->setShapeTarget2D(target_x, target_y);

        // Aplicar fuerza de atracción física hacia el punto rotado
        // Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
        float shape_size = scale_factor * 80.0f;  // 80px = radio base
        balls[i]->applyShapeForce(target_x, target_y, shape_size, delta_time_, SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);

        // Calcular brillo según profundidad Z para renderizado
        // Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)
        float z_normalized = (z_3d + shape_size) / (2.0f * shape_size);
        z_normalized = std::max(0.0f, std::min(1.0f, z_normalized));
        balls[i]->setDepthBrightness(z_normalized);

        // Calcular escala según profundidad Z (perspectiva) - solo si está activado
        // 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
        float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f;
        balls[i]->setDepthScale(depth_scale);
    }

    // Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
    if (state_manager_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
        int balls_near = 0;
        float distance_threshold = LOGO_CONVERGENCE_DISTANCE;  // 20px fijo (más permisivo)

        for (const auto& ball : balls) {
            if (ball->getDistanceToTarget() < distance_threshold) {
                balls_near++;
            }
        }

        shape_convergence_ = static_cast<float>(balls_near) / scene_manager_->getBallCount();

        // Notificar a la figura sobre el porcentaje de convergencia
        // Esto permite que PNGShape decida cuándo empezar a contar para flips
        active_shape_->setConvergence(shape_convergence_);
    }
}

// Limitar escala de figura para evitar que se salga de pantalla
void Engine::clampShapeScale() {
    // Calcular tamaño máximo permitido según resolución actual
    // La figura más grande (esfera/cubo) usa ~33% de altura por defecto
    // Permitir hasta que la figura ocupe 90% de la dimensión más pequeña
    float max_dimension = std::min(current_screen_width_, current_screen_height_);
    float base_size_factor = 0.333f;  // ROTOBALL_RADIUS_FACTOR o similar
    float max_scale_for_screen = (max_dimension * 0.9f) / (max_dimension * base_size_factor);

    // Limitar entre SHAPE_SCALE_MIN y el mínimo de (SHAPE_SCALE_MAX, max_scale_for_screen)
    float max_allowed = std::min(SHAPE_SCALE_MAX, max_scale_for_screen);
    shape_scale_factor_ = std::max(SHAPE_SCALE_MIN, std::min(max_allowed, shape_scale_factor_));
}