#include "engine.h"

#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

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

#include "ball.h"                      // for Ball
#include "external/dbgtxt.h"           // for dbg_init, dbg_print
#include "external/mouse.h"            // for Mouse namespace
#include "external/texture.h"          // for Texture
#include "shapes/atom_shape.h"         // for AtomShape
#include "shapes/cube_shape.h"         // for CubeShape
#include "shapes/cylinder_shape.h"     // for CylinderShape
#include "shapes/helix_shape.h"        // for HelixShape
#include "shapes/icosahedron_shape.h"  // for IcosahedronShape
#include "shapes/png_shape.h"          // for PNGShape
#include "shapes/sphere_shape.h"       // for SphereShape
#include "shapes/torus_shape.h"        // for TorusShape
#include "shapes/wave_grid_shape.h"    // for WaveGridShape

// 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) {
    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 (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;
    }

    // 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)
        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 {
            // 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 ResourcePack
            if (Texture::isPackLoaded()) {
                auto pack_resources = Texture::getPackResourceList();

                // 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)));
        dbg_init(renderer_);
        initializeThemes();
        initializeDynamicThemes();
        initBalls(scenario_);
    }

    return success;
}

void Engine::run() {
    while (!should_exit_) {
        calculateDeltaTime();
        update();
        handleEvents();
        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();

    // Calcular FPS
    fps_frame_count_++;
    Uint64 current_time = SDL_GetTicks();
    if (current_time - fps_last_time_ >= 1000)  // Actualizar cada segundo
    {
        fps_current_ = fps_frame_count_;
        fps_frame_count_ = 0;
        fps_last_time_ = current_time;
        fps_text_ = "FPS: " + std::to_string(fps_current_);
    }

    // Bifurcar actualización según modo activo
    if (current_mode_ == SimulationMode::PHYSICS) {
        // Modo física normal: actualizar física de cada pelota
        for (auto& ball : balls_) {
            ball->update(delta_time_);  // Pasar delta time a cada pelota
        }
    } else if (current_mode_ == SimulationMode::SHAPE) {
        // Modo Figura 3D: actualizar figura polimórfica
        updateShape();
    }

    // Actualizar texto (sin cambios en la lógica)
    if (show_text_) {
        show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION);
    }

    // Actualizar Modo DEMO (auto-play)
    updateDemoMode();

    // Actualizar transición de tema (LERP)
    if (transitioning_) {
        transition_progress_ += delta_time_ / transition_duration_;

        if (transition_progress_ >= 1.0f) {
            // Transición completa
            transition_progress_ = 1.0f;
            current_theme_ = target_theme_;
            transitioning_ = false;

            // Actualizar colores de las pelotas al tema final
            const ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
            for (size_t i = 0; i < balls_.size(); i++) {
                size_t color_index = i % theme.ball_colors.size();
                balls_[i]->setColor(theme.ball_colors[color_index]);
            }
        }
    }

    // Actualizar tema dinámico (animación de keyframes)
    updateDynamicTheme();
}

void Engine::handleEvents() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        // Procesar eventos de ratón (auto-ocultar cursor)
        Mouse::handleEvent(event);

        // Salir del bucle si se detecta una petición de cierre
        if (event.type == SDL_EVENT_QUIT) {
            should_exit_ = true;
            break;
        }

        // Procesar eventos de teclado
        if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) {
            switch (event.key.key) {
                case SDLK_ESCAPE:
                    should_exit_ = true;
                    break;

                case SDLK_SPACE:
                    pushBallsAwayFromGravity();
                    break;

                case SDLK_G:
                    // Si estamos en modo figura, salir a modo física SIN GRAVEDAD
                    if (current_mode_ == SimulationMode::SHAPE) {
                        toggleShapeMode(false);  // Desactivar figura sin forzar gravedad ON
                    } else {
                        switchBallsGravity();  // Toggle normal en modo física
                    }
                    break;

                // Controles de dirección de gravedad con teclas de cursor
                case SDLK_UP:
                    // Si estamos en modo figura, salir a modo física CON gravedad
                    if (current_mode_ == SimulationMode::SHAPE) {
                        toggleShapeMode();  // Desactivar figura (activa gravedad automáticamente)
                    } else {
                        enableBallsGravityIfDisabled();  // Reactivar gravedad si estaba OFF
                    }
                    changeGravityDirection(GravityDirection::UP);
                    break;

                case SDLK_DOWN:
                    // Si estamos en modo figura, salir a modo física CON gravedad
                    if (current_mode_ == SimulationMode::SHAPE) {
                        toggleShapeMode();  // Desactivar figura (activa gravedad automáticamente)
                    } else {
                        enableBallsGravityIfDisabled();  // Reactivar gravedad si estaba OFF
                    }
                    changeGravityDirection(GravityDirection::DOWN);
                    break;

                case SDLK_LEFT:
                    // Si estamos en modo figura, salir a modo física CON gravedad
                    if (current_mode_ == SimulationMode::SHAPE) {
                        toggleShapeMode();  // Desactivar figura (activa gravedad automáticamente)
                    } else {
                        enableBallsGravityIfDisabled();  // Reactivar gravedad si estaba OFF
                    }
                    changeGravityDirection(GravityDirection::LEFT);
                    break;

                case SDLK_RIGHT:
                    // Si estamos en modo figura, salir a modo física CON gravedad
                    if (current_mode_ == SimulationMode::SHAPE) {
                        toggleShapeMode();  // Desactivar figura (activa gravedad automáticamente)
                    } else {
                        enableBallsGravityIfDisabled();  // Reactivar gravedad si estaba OFF
                    }
                    changeGravityDirection(GravityDirection::RIGHT);
                    break;

                case SDLK_V:
                    toggleVSync();
                    break;

                case SDLK_H:
                    show_debug_ = !show_debug_;
                    break;

                // Toggle Física ↔ Última Figura (antes era C)
                case SDLK_F:
                    toggleShapeMode();
                    break;

                // Selección directa de figuras 3D
                case SDLK_Q:
                    activateShape(ShapeType::SPHERE);
                    break;

                case SDLK_W:
                    activateShape(ShapeType::WAVE_GRID);
                    break;

                case SDLK_E:
                    activateShape(ShapeType::HELIX);
                    break;

                case SDLK_R:
                    activateShape(ShapeType::TORUS);
                    break;

                case SDLK_T:
                    activateShape(ShapeType::CUBE);
                    break;

                case SDLK_Y:
                    activateShape(ShapeType::CYLINDER);
                    break;

                case SDLK_U:
                    activateShape(ShapeType::ICOSAHEDRON);
                    break;

                case SDLK_I:
                    activateShape(ShapeType::ATOM);
                    break;

                case SDLK_O:
                    activateShape(ShapeType::PNG_SHAPE);
                    break;

                // Ciclar temas de color (movido de T a B)
                case SDLK_B:
                    // Ciclar al siguiente tema con transición suave (LERP)
                    {
                        ColorTheme next_theme = static_cast<ColorTheme>((static_cast<int>(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0])));
                        startThemeTransition(next_theme);
                    }
                    break;

                // Temas de colores con teclado numérico (con transición suave)
                case SDLK_KP_1:
                    startThemeTransition(ColorTheme::SUNSET);
                    break;

                case SDLK_KP_2:
                    startThemeTransition(ColorTheme::OCEAN);
                    break;

                case SDLK_KP_3:
                    startThemeTransition(ColorTheme::NEON);
                    break;

                case SDLK_KP_4:
                    startThemeTransition(ColorTheme::FOREST);
                    break;

                case SDLK_KP_5:
                    startThemeTransition(ColorTheme::RGB);
                    break;

                case SDLK_KP_6:
                    startThemeTransition(ColorTheme::MONOCHROME);
                    break;

                case SDLK_KP_7:
                    startThemeTransition(ColorTheme::LAVENDER);
                    break;

                // Temas dinámicos (animados) - Solo Numpad 8/9/0 (teclas normales usadas para escenarios)
                case SDLK_KP_8:
                    activateDynamicTheme(0);  // SUNRISE
                    break;

                case SDLK_KP_9:
                    activateDynamicTheme(1);  // OCEAN WAVES
                    break;

                case SDLK_KP_0:
                    activateDynamicTheme(2);  // NEON PULSE
                    break;

                // Cambio de sprite/textura dinámico
                case SDLK_N:
                    switchTexture();
                    break;

                // Control de escala de figura (solo en modo SHAPE)
                case SDLK_KP_PLUS:
                    if (current_mode_ == SimulationMode::SHAPE) {
                        shape_scale_factor_ += SHAPE_SCALE_STEP;
                        clampShapeScale();
                        text_ = "SCALE " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
                        int text_width = static_cast<int>(text_.length() * 8);
                        text_pos_ = (current_screen_width_ - text_width) / 2;
                        text_init_time_ = SDL_GetTicks();
                        show_text_ = true;
                    }
                    break;

                case SDLK_KP_MINUS:
                    if (current_mode_ == SimulationMode::SHAPE) {
                        shape_scale_factor_ -= SHAPE_SCALE_STEP;
                        clampShapeScale();
                        text_ = "SCALE " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
                        int text_width = static_cast<int>(text_.length() * 8);
                        text_pos_ = (current_screen_width_ - text_width) / 2;
                        text_init_time_ = SDL_GetTicks();
                        show_text_ = true;
                    }
                    break;

                case SDLK_KP_MULTIPLY:
                    if (current_mode_ == SimulationMode::SHAPE) {
                        shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
                        text_ = "SCALE RESET (100%)";
                        int text_width = static_cast<int>(text_.length() * 8);
                        text_pos_ = (current_screen_width_ - text_width) / 2;
                        text_init_time_ = SDL_GetTicks();
                        show_text_ = true;
                    }
                    break;

                case SDLK_KP_DIVIDE:
                    if (current_mode_ == SimulationMode::SHAPE) {
                        depth_zoom_enabled_ = !depth_zoom_enabled_;
                        text_ = depth_zoom_enabled_ ? "DEPTH ZOOM ON" : "DEPTH ZOOM OFF";
                        int text_width = static_cast<int>(text_.length() * 8);
                        text_pos_ = (current_screen_width_ - text_width) / 2;
                        text_init_time_ = SDL_GetTicks();
                        show_text_ = true;
                    }
                    break;

                case SDLK_1:
                    scenario_ = 0;
                    initBalls(scenario_);
                    break;

                case SDLK_2:
                    scenario_ = 1;
                    initBalls(scenario_);
                    break;

                case SDLK_3:
                    scenario_ = 2;
                    initBalls(scenario_);
                    break;

                case SDLK_4:
                    scenario_ = 3;
                    initBalls(scenario_);
                    break;

                case SDLK_5:
                    scenario_ = 4;
                    initBalls(scenario_);
                    break;

                case SDLK_6:
                    scenario_ = 5;
                    initBalls(scenario_);
                    break;

                case SDLK_7:
                    scenario_ = 6;
                    initBalls(scenario_);
                    break;

                case SDLK_8:
                    scenario_ = 7;
                    initBalls(scenario_);
                    break;

                // Controles de zoom dinámico (solo si no estamos en fullscreen)
                case SDLK_F1:
                    if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
                        zoomOut();
                    }
                    break;

                case SDLK_F2:
                    if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
                        zoomIn();
                    }
                    break;

                // Control de pantalla completa
                case SDLK_F3:
                    toggleFullscreen();
                    break;

                // Modo real fullscreen (cambia resolución interna)
                case SDLK_F4:
                    toggleRealFullscreen();
                    break;

                // Toggle escalado entero/estirado (solo en fullscreen F3)
                case SDLK_F5:
                    toggleIntegerScaling();
                    break;

                // Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
                case SDLK_D:
                    // Shift+D = Pausar tema dinámico
                    if (event.key.mod & SDL_KMOD_SHIFT) {
                        pauseDynamicTheme();
                    } else {
                        // D sin Shift = Toggle modo DEMO
                        if (current_app_mode_ == AppMode::DEMO) {
                            // Desactivar DEMO → MANUAL
                            setState(AppMode::MANUAL);
                            text_ = "DEMO MODE OFF";
                            text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
                            show_text_ = true;
                            text_init_time_ = SDL_GetTicks();
                        } else {
                            // Activar DEMO (desde cualquier otro modo)
                            setState(AppMode::DEMO);
                            randomizeOnDemoStart(false);
                            text_ = "DEMO MODE ON";
                            text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
                            show_text_ = true;
                            text_init_time_ = SDL_GetTicks();
                        }
                    }
                    break;

                // Toggle Modo DEMO LITE (solo física/figuras)
                case SDLK_L:
                    if (current_app_mode_ == AppMode::DEMO_LITE) {
                        // Desactivar DEMO_LITE → MANUAL
                        setState(AppMode::MANUAL);
                        text_ = "DEMO LITE OFF";
                        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
                        show_text_ = true;
                        text_init_time_ = SDL_GetTicks();
                    } else {
                        // Activar DEMO_LITE (desde cualquier otro modo)
                        setState(AppMode::DEMO_LITE);
                        randomizeOnDemoStart(true);
                        text_ = "DEMO LITE ON";
                        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
                        show_text_ = true;
                        text_init_time_ = SDL_GetTicks();
                    }
                    break;

                // Toggle Modo LOGO (easter egg - marca de agua)
                case SDLK_K:
                    toggleLogoMode();
                    // Mostrar texto informativo
                    if (current_app_mode_ == AppMode::LOGO) {
                        text_ = "LOGO MODE ON";
                    } else {
                        text_ = "LOGO MODE OFF";
                    }
                    text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
                    show_text_ = true;
                    text_init_time_ = SDL_GetTicks();
                    break;
            }
        }
    }
}

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 en lugar de color sólido
    renderGradientBackground();

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

    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

        // Crear vector de índices para ordenamiento
        std::vector<size_t> render_order;
        render_order.reserve(balls_.size());
        for (size_t i = 0; i < balls_.size(); i++) {
            render_order.push_back(i);
        }

        // Ordenar índices por profundidad Z (menor primero = fondo primero)
        std::sort(render_order.begin(), render_order.end(), [this](size_t a, size_t b) {
            return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness();
        });

        // Renderizar en orden de profundidad (fondo → frente)
        for (size_t idx : render_order) {
            SDL_FRect pos = balls_[idx]->getPosition();
            Color color = 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);
        }
    } else {
        // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
        size_t idx = 0;
        for (auto& ball : balls_) {
            SDL_FRect pos = ball->getPosition();
            Color color = 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()));
    }

    if (show_text_) {
        // Obtener datos del tema actual (estático o dinámico)
        int text_color_r, text_color_g, text_color_b;
        const char* theme_name_es = nullptr;

        if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
            // Tema dinámico activo
            const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
            text_color_r = dyn_theme.text_color_r;
            text_color_g = dyn_theme.text_color_g;
            text_color_b = dyn_theme.text_color_b;
            theme_name_es = dyn_theme.name_es;
        } else {
            // Tema estático
            int theme_idx = static_cast<int>(current_theme_);
            const ThemeColors& current = themes_[theme_idx];
            text_color_r = current.text_color_r;
            text_color_g = current.text_color_g;
            text_color_b = current.text_color_b;
            theme_name_es = current.name_es;
        }

        // Texto del número de pelotas con color del tema
        dbg_print(text_pos_, 8, text_.c_str(), text_color_r, text_color_g, text_color_b);

        // 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 = static_cast<int>(strlen(theme_name_es) * 8);  // 8 píxeles por carácter
            int theme_x = (current_screen_width_ - theme_text_width) / 2;        // Centrar horizontalmente

            // Texto del nombre del tema con el mismo color
            dbg_print(theme_x, 24, theme_name_es, text_color_r, text_color_g, text_color_b);
        }
    }

    // Debug display (solo si está activado con tecla H)
    if (show_debug_) {
        // Mostrar contador de FPS en esquina superior derecha
        int fps_text_width = static_cast<int>(fps_text_.length() * 8);  // 8 píxeles por carácter
        int fps_x = current_screen_width_ - fps_text_width - 8;         // 8 píxeles de margen
        dbg_print(fps_x, 8, fps_text_.c_str(), 255, 255, 0);            // Amarillo para distinguir

        // Mostrar estado V-Sync en esquina superior izquierda
        dbg_print(8, 8, vsync_text_.c_str(), 0, 255, 255);  // Cian para distinguir

        // Debug: Mostrar valores de la primera pelota (si existe)
        if (!balls_.empty()) {
            // Línea 1: Gravedad (solo números enteros)
            int grav_int = static_cast<int>(balls_[0]->getGravityForce());
            std::string grav_text = "GRAV " + std::to_string(grav_int);
            dbg_print(8, 24, grav_text.c_str(), 255, 0, 255);  // Magenta para debug

            // Línea 2: Velocidad Y (solo números enteros)
            int vy_int = static_cast<int>(balls_[0]->getVelocityY());
            std::string vy_text = "VY " + std::to_string(vy_int);
            dbg_print(8, 32, vy_text.c_str(), 255, 0, 255);  // Magenta para debug

            // Línea 3: Estado superficie
            std::string surface_text = balls_[0]->isOnSurface() ? "SURFACE YES" : "SURFACE NO";
            dbg_print(8, 40, surface_text.c_str(), 255, 0, 255);  // Magenta para debug

            // Línea 4: Coeficiente de rebote (loss)
            float loss_val = balls_[0]->getLossCoefficient();
            std::string loss_text = "LOSS " + std::to_string(loss_val).substr(0, 4);  // Solo 2 decimales
            dbg_print(8, 48, loss_text.c_str(), 255, 0, 255);                         // Magenta para debug

            // Línea 5: Dirección de gravedad
            std::string gravity_dir_text = "GRAVITY " + gravityDirectionToString(current_gravity_);
            dbg_print(8, 56, gravity_dir_text.c_str(), 255, 255, 0);  // Amarillo para dirección
        }

        // Debug: Mostrar tema actual
        std::string theme_text;
        if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
            theme_text = std::string("THEME ") + dynamic_themes_[current_dynamic_theme_index_].name_en;
        } else {
            theme_text = std::string("THEME ") + themes_[static_cast<int>(current_theme_)].name_en;
        }
        dbg_print(8, 64, theme_text.c_str(), 255, 255, 128);  // Amarillo claro para tema

        // Debug: Mostrar modo de simulación actual
        std::string mode_text;
        if (current_mode_ == SimulationMode::PHYSICS) {
            mode_text = "MODE PHYSICS";
        } else if (active_shape_) {
            mode_text = std::string("MODE ") + active_shape_->getName();
        } else {
            mode_text = "MODE SHAPE";
        }
        dbg_print(8, 72, mode_text.c_str(), 0, 255, 128);  // Verde claro para modo

        // Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON)
        if (current_app_mode_ == AppMode::LOGO) {
            int logo_text_width = 9 * 8;  // "LOGO MODE" = 9 caracteres × 8 píxeles
            int logo_x = (current_screen_width_ - logo_text_width) / 2;
            dbg_print(logo_x, 80, "LOGO MODE", 255, 128, 0);  // Naranja para Logo Mode
        } else if (current_app_mode_ == AppMode::DEMO) {
            int demo_text_width = 9 * 8;  // "DEMO MODE" = 9 caracteres × 8 píxeles
            int demo_x = (current_screen_width_ - demo_text_width) / 2;
            dbg_print(demo_x, 80, "DEMO MODE", 255, 165, 0);  // Naranja para DEMO
        } else if (current_app_mode_ == AppMode::DEMO_LITE) {
            int lite_text_width = 14 * 8;  // "DEMO LITE MODE" = 14 caracteres × 8 píxeles
            int lite_x = (current_screen_width_ - lite_text_width) / 2;
            dbg_print(lite_x, 80, "DEMO LITE MODE", 255, 200, 0);  // Amarillo-naranja para DEMO LITE
        }
    }

    SDL_RenderPresent(renderer_);
}

void Engine::initBalls(int value) {
    // Si estamos en modo figura 3D, desactivarlo antes de regenerar pelotas
    if (current_mode_ == SimulationMode::SHAPE) {
        current_mode_ = SimulationMode::PHYSICS;
        active_shape_.reset();  // Liberar figura actual
    }

    // Limpiar las bolas actuales
    balls_.clear();

    // Resetear gravedad al estado por defecto (DOWN) al cambiar escenario
    changeGravityDirection(GravityDirection::DOWN);

    // Crear las bolas según el escenario
    for (int i = 0; i < BALL_COUNT_SCENARIOS[value]; ++i) {
        const int SIGN = ((rand() % 2) * 2) - 1;  // Genera un signo aleatorio (+ o -)
        // Calcular spawn zone: margen a cada lado, zona central para spawn
        const int margin = static_cast<int>(current_screen_width_ * BALL_SPAWN_MARGIN);
        const int spawn_zone_width = current_screen_width_ - (2 * margin);
        const float X = (rand() % spawn_zone_width) + margin;   // Posición inicial en X
        const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN;  // Velocidad en X
        const float VY = ((rand() % 60) - 30) * 0.1f;           // Velocidad en Y

        // Seleccionar color de la paleta del tema actual (estático o dinámico)
        Color COLOR;
        if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
            // Tema dinámico: usar colores del keyframe actual
            const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
            const DynamicThemeKeyframe& current_kf = dyn_theme.keyframes[current_keyframe_index_];
            int color_index = rand() % current_kf.ball_colors.size();
            COLOR = current_kf.ball_colors[color_index];
        } else {
            // Tema estático
            ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
            int color_index = rand() % theme.ball_colors.size();
            COLOR = theme.ball_colors[color_index];
        }
        // Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
        float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN);
        balls_.emplace_back(std::make_unique<Ball>(X, VX, VY, COLOR, texture_, current_screen_width_, current_screen_height_, current_ball_size_, current_gravity_, mass_factor));
    }
    setText();  // Actualiza el texto
}

void Engine::setText() {
    // Suprimir textos durante modos demo
    if (current_app_mode_ != AppMode::MANUAL) return;

    int num_balls = BALL_COUNT_SCENARIOS[scenario_];
    if (num_balls == 1) {
        text_ = "1 PELOTA";
    } else {
        text_ = std::to_string(num_balls) + " PELOTAS";
    }
    text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;  // Centrar texto
    show_text_ = true;
    text_init_time_ = SDL_GetTicks();
}

void Engine::pushBallsAwayFromGravity() {
    for (auto& ball : balls_) {
        const int SIGNO = ((rand() % 2) * 2) - 1;
        const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
        const float MAIN = ((rand() % 40) * 0.1f) + 5;

        float vx = 0, vy = 0;
        switch (current_gravity_) {
            case GravityDirection::DOWN:  // Impulsar ARRIBA
                vx = LATERAL;
                vy = -MAIN;
                break;
            case GravityDirection::UP:  // Impulsar ABAJO
                vx = LATERAL;
                vy = MAIN;
                break;
            case GravityDirection::LEFT:  // Impulsar DERECHA
                vx = MAIN;
                vy = LATERAL;
                break;
            case GravityDirection::RIGHT:  // Impulsar IZQUIERDA
                vx = -MAIN;
                vy = LATERAL;
                break;
        }
        ball->modVel(vx, vy);  // Modifica la velocidad según dirección de gravedad
    }
}

void Engine::switchBallsGravity() {
    for (auto& ball : balls_) {
        ball->switchGravity();
    }
}

void Engine::enableBallsGravityIfDisabled() {
    for (auto& ball : balls_) {
        ball->enableGravityIfDisabled();
    }
}

void Engine::forceBallsGravityOn() {
    for (auto& ball : balls_) {
        ball->forceGravityOn();
    }
}

void Engine::forceBallsGravityOff() {
    // Contar cuántas pelotas están en superficie (suelo/techo/pared)
    int balls_on_surface = 0;
    for (const auto& ball : balls_) {
        if (ball->isOnSurface()) {
            balls_on_surface++;
        }
    }

    // Si la mayoría (>50%) están en superficie, aplicar impulso para que se vea el efecto
    float surface_ratio = static_cast<float>(balls_on_surface) / static_cast<float>(balls_.size());
    if (surface_ratio > 0.5f) {
        pushBallsAwayFromGravity();  // Dar impulso contrario a gravedad
    }

    // Desactivar gravedad
    for (auto& ball : balls_) {
        ball->forceGravityOff();
    }
}

void Engine::changeGravityDirection(GravityDirection direction) {
    current_gravity_ = direction;
    for (auto& ball : balls_) {
        ball->setGravityDirection(direction);
        ball->applyRandomLateralPush();  // Aplicar empuje lateral aleatorio
    }
}

void Engine::toggleVSync() {
    vsync_enabled_ = !vsync_enabled_;
    vsync_text_ = vsync_enabled_ ? "VSYNC ON" : "VSYNC OFF";

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

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

                // Reinicar la escena con nueva resolución
                initBalls(scenario_);
            }
            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
        SDL_SetWindowFullscreen(window_, false);
        SDL_SetWindowSize(window_, base_screen_width_ * DEFAULT_WINDOW_ZOOM, base_screen_height_ * DEFAULT_WINDOW_ZOOM);

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

        // Reinicar la escena con resolución original
        initBalls(scenario_);
    }
}

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 texto informativo
    text_ = "SCALING: ";
    text_ += mode_name;
    text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
    show_text_ = true;
    text_init_time_ = SDL_GetTicks();
}

std::string Engine::gravityDirectionToString(GravityDirection direction) const {
    switch (direction) {
        case GravityDirection::DOWN:
            return "DOWN";
        case GravityDirection::UP:
            return "UP";
        case GravityDirection::LEFT:
            return "LEFT";
        case GravityDirection::RIGHT:
            return "RIGHT";
        default:
            return "UNKNOWN";
    }
}

void Engine::renderGradientBackground() {
    // Crear quad de pantalla completa con degradado
    SDL_Vertex bg_vertices[4];

    // Obtener colores (con LERP si estamos en transición)
    float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;

    if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
        // Tema dinámico activo: interpolar entre keyframes
        DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
        const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
        const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];

        top_r = lerp(current_kf.bg_top_r, target_kf.bg_top_r, dynamic_transition_progress_);
        top_g = lerp(current_kf.bg_top_g, target_kf.bg_top_g, dynamic_transition_progress_);
        top_b = lerp(current_kf.bg_top_b, target_kf.bg_top_b, dynamic_transition_progress_);

        bottom_r = lerp(current_kf.bg_bottom_r, target_kf.bg_bottom_r, dynamic_transition_progress_);
        bottom_g = lerp(current_kf.bg_bottom_g, target_kf.bg_bottom_g, dynamic_transition_progress_);
        bottom_b = lerp(current_kf.bg_bottom_b, target_kf.bg_bottom_b, dynamic_transition_progress_);
    } else if (transitioning_) {
        // Transición estática: interpolar entre tema actual y tema destino
        ThemeColors& current = themes_[static_cast<int>(current_theme_)];
        ThemeColors& target = themes_[static_cast<int>(target_theme_)];

        top_r = lerp(current.bg_top_r, target.bg_top_r, transition_progress_);
        top_g = lerp(current.bg_top_g, target.bg_top_g, transition_progress_);
        top_b = lerp(current.bg_top_b, target.bg_top_b, transition_progress_);

        bottom_r = lerp(current.bg_bottom_r, target.bg_bottom_r, transition_progress_);
        bottom_g = lerp(current.bg_bottom_g, target.bg_bottom_g, transition_progress_);
        bottom_b = lerp(current.bg_bottom_b, target.bg_bottom_b, transition_progress_);
    } else {
        // Sin transición: usar tema estático actual directamente
        ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
        top_r = theme.bg_top_r;
        top_g = theme.bg_top_g;
        top_b = theme.bg_top_b;

        bottom_r = theme.bg_bottom_r;
        bottom_g = theme.bg_bottom_g;
        bottom_b = theme.bg_bottom_b;
    }

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

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

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

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

void Engine::initializeThemes() {
    // SUNSET: Naranjas, rojos, amarillos, rosas (8 colores)
    themes_[0] = {
        "SUNSET",
        "ATARDECER",  // Nombres (inglés, español)
        255,
        140,
        60,  // Color texto: naranja cálido
        180.0f / 255.0f,
        140.0f / 255.0f,
        100.0f / 255.0f,  // Fondo superior (naranja suave)
        40.0f / 255.0f,
        20.0f / 255.0f,
        60.0f / 255.0f,  // Fondo inferior (púrpura oscuro)
        {{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}}};

    // OCEAN: Azules, turquesas, blancos (8 colores)
    themes_[1] = {
        "OCEAN",
        "OCEANO",  // Nombres (inglés, español)
        80,
        200,
        255,  // Color texto: azul océano
        100.0f / 255.0f,
        150.0f / 255.0f,
        200.0f / 255.0f,  // Fondo superior (azul cielo)
        20.0f / 255.0f,
        40.0f / 255.0f,
        80.0f / 255.0f,  // Fondo inferior (azul marino)
        {{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}}};

    // NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores)
    themes_[2] = {
        "NEON",
        "NEON",  // Nombres (inglés, español)
        255,
        60,
        255,  // Color texto: magenta brillante
        20.0f / 255.0f,
        20.0f / 255.0f,
        40.0f / 255.0f,  // Fondo superior (negro azulado)
        0.0f / 255.0f,
        0.0f / 255.0f,
        0.0f / 255.0f,  // Fondo inferior (negro)
        {{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}}};

    // FOREST: Verdes, marrones, amarillos otoño (8 colores)
    themes_[3] = {
        "FOREST",
        "BOSQUE",  // Nombres (inglés, español)
        100,
        255,
        100,  // Color texto: verde natural
        144.0f / 255.0f,
        238.0f / 255.0f,
        144.0f / 255.0f,  // Fondo superior (verde claro)
        101.0f / 255.0f,
        67.0f / 255.0f,
        33.0f / 255.0f,  // Fondo inferior (marrón tierra)
        {{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}}};

    // RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática
    themes_[4] = {
        "RGB",
        "RGB",  // Nombres (inglés, español)
        100,
        100,
        100,  // Color texto: gris oscuro (contraste con fondo blanco)
        1.0f,
        1.0f,
        1.0f,  // Fondo superior (blanco puro)
        1.0f,
        1.0f,
        1.0f,  // Fondo inferior (blanco puro) - sin degradado
        {
            {255, 0, 0},    // 0° - Rojo puro
            {255, 64, 0},   // 15° - Rojo-Naranja
            {255, 128, 0},  // 30° - Naranja
            {255, 191, 0},  // 45° - Naranja-Amarillo
            {255, 255, 0},  // 60° - Amarillo puro
            {191, 255, 0},  // 75° - Amarillo-Verde claro
            {128, 255, 0},  // 90° - Verde-Amarillo
            {64, 255, 0},   // 105° - Verde claro-Amarillo
            {0, 255, 0},    // 120° - Verde puro
            {0, 255, 64},   // 135° - Verde-Cian claro
            {0, 255, 128},  // 150° - Verde-Cian
            {0, 255, 191},  // 165° - Verde claro-Cian
            {0, 255, 255},  // 180° - Cian puro
            {0, 191, 255},  // 195° - Cian-Azul claro
            {0, 128, 255},  // 210° - Azul-Cian
            {0, 64, 255},   // 225° - Azul claro-Cian
            {0, 0, 255},    // 240° - Azul puro
            {64, 0, 255},   // 255° - Azul-Magenta claro
            {128, 0, 255},  // 270° - Azul-Magenta
            {191, 0, 255},  // 285° - Azul claro-Magenta
            {255, 0, 255},  // 300° - Magenta puro
            {255, 0, 191},  // 315° - Magenta-Rojo claro
            {255, 0, 128},  // 330° - Magenta-Rojo
            {255, 0, 64}    // 345° - Magenta claro-Rojo
        }};

    // MONOCHROME: Fondo negro degradado, sprites blancos monocromáticos (8 tonos grises)
    themes_[5] = {
        "MONOCHROME",
        "MONOCROMO",  // Nombres (inglés, español)
        200,
        200,
        200,  // Color texto: gris claro
        20.0f / 255.0f,
        20.0f / 255.0f,
        20.0f / 255.0f,  // Fondo superior (gris muy oscuro)
        0.0f / 255.0f,
        0.0f / 255.0f,
        0.0f / 255.0f,  // Fondo inferior (negro)
        {
            {255, 255, 255},  // Blanco puro - todas las pelotas del mismo color
            {255, 255, 255},
            {255, 255, 255},
            {255, 255, 255},
            {255, 255, 255},
            {255, 255, 255},
            {255, 255, 255},
            {255, 255, 255}}};

    // LAVENDER: Degradado violeta oscuro → azul medianoche, pelotas amarillo dorado monocromático
    themes_[6] = {
        "LAVENDER",
        "LAVANDA",  // Nombres (inglés, español)
        255,
        200,
        100,  // Color texto: amarillo cálido
        120.0f / 255.0f,
        80.0f / 255.0f,
        140.0f / 255.0f,  // Fondo superior (violeta oscuro)
        25.0f / 255.0f,
        30.0f / 255.0f,
        60.0f / 255.0f,  // Fondo inferior (azul medianoche)
        {
            {255, 215, 0},  // Amarillo dorado - todas las pelotas del mismo color
            {255, 215, 0},
            {255, 215, 0},
            {255, 215, 0},
            {255, 215, 0},
            {255, 215, 0},
            {255, 215, 0},
            {255, 215, 0}}};
}

void Engine::initializeDynamicThemes() {
    // ========================================================================
    // DYNAMIC_1: "SUNRISE" (Amanecer) - Noche → Alba → Día → Loop
    // ========================================================================
    dynamic_themes_[0] = {
        "SUNRISE",
        "AMANECER",
        255, 200, 100,  // Color texto: amarillo cálido
        {
            // Keyframe 0: Noche oscura (estado inicial)
            {
                20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f,    // Fondo superior: azul medianoche
                10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f,    // Fondo inferior: azul muy oscuro
                {{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
                 {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}},  // Pelotas azules tenues
                0.0f  // Sin transición (estado inicial)
            },
            // Keyframe 1: Alba naranja-rosa
            {
                180.0f / 255.0f, 100.0f / 255.0f, 120.0f / 255.0f,  // Fondo superior: naranja-rosa
                255.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f,  // Fondo inferior: naranja cálido
                {{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90},
                 {255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}},  // Pelotas naranjas
                4.0f  // 4 segundos para llegar aquí
            },
            // Keyframe 2: Día brillante amarillo
            {
                255.0f / 255.0f, 240.0f / 255.0f, 180.0f / 255.0f,  // Fondo superior: amarillo claro
                255.0f / 255.0f, 255.0f / 255.0f, 220.0f / 255.0f,  // Fondo inferior: amarillo muy claro
                {{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190},
                 {255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}},  // Pelotas amarillas brillantes
                3.0f  // 3 segundos para llegar aquí
            },
            // Keyframe 3: Vuelta a noche (para loop)
            {
                20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f,    // Fondo superior: azul medianoche
                10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f,    // Fondo inferior: azul muy oscuro
                {{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
                 {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}},  // Pelotas azules tenues
                5.0f  // 5 segundos para volver a noche
            }
        },
        true  // Loop = true
    };

    // ========================================================================
    // DYNAMIC_2: "OCEAN WAVES" (Olas Oceánicas) - Azul oscuro ↔ Turquesa
    // ========================================================================
    dynamic_themes_[1] = {
        "OCEAN WAVES",
        "OLAS OCEANICAS",
        100, 220, 255,  // Color texto: cian claro
        {
            // Keyframe 0: Profundidad oceánica (azul oscuro)
            {
                20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f,   // Fondo superior: azul marino
                10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f,    // Fondo inferior: azul muy oscuro
                {{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
                 {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}},  // Pelotas azul oscuro
                0.0f  // Estado inicial
            },
            // Keyframe 1: Aguas poco profundas (turquesa brillante)
            {
                100.0f / 255.0f, 200.0f / 255.0f, 230.0f / 255.0f,  // Fondo superior: turquesa claro
                50.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f,   // Fondo inferior: turquesa medio
                {{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250},
                 {105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}},  // Pelotas turquesa brillante
                4.0f  // 4 segundos para llegar
            },
            // Keyframe 2: Vuelta a profundidad (para loop)
            {
                20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f,   // Fondo superior: azul marino
                10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f,    // Fondo inferior: azul muy oscuro
                {{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
                 {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}},  // Pelotas azul oscuro
                4.0f  // 4 segundos para volver
            }
        },
        true  // Loop = true
    };

    // ========================================================================
    // DYNAMIC_3: "NEON PULSE" (Pulso Neón) - Negro → Neón brillante (rápido)
    // ========================================================================
    dynamic_themes_[2] = {
        "NEON PULSE",
        "PULSO NEON",
        255, 60, 255,  // Color texto: magenta brillante
        {
            // Keyframe 0: Apagado (negro)
            {
                0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f,      // Fondo superior: negro
                0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f,      // Fondo inferior: negro
                {{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
                 {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}},  // Pelotas grises muy oscuras
                0.0f  // Estado inicial
            },
            // Keyframe 1: Encendido (neón cian-magenta)
            {
                20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f,   // Fondo superior: azul oscuro
                0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f,      // Fondo inferior: negro
                {{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255},
                 {50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}},  // Pelotas neón vibrante
                1.5f  // 1.5 segundos para encender (rápido)
            },
            // Keyframe 2: Vuelta a apagado (para loop ping-pong)
            {
                0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f,      // Fondo superior: negro
                0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f,      // Fondo inferior: negro
                {{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
                 {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}},  // Pelotas grises muy oscuras
                1.5f  // 1.5 segundos para apagar
            }
        },
        true  // Loop = true
    };
}

void Engine::startThemeTransition(ColorTheme new_theme) {
    if (new_theme == current_theme_) return;  // Ya estamos en ese tema

    // Si venimos de tema dinámico, hacer transición instantánea (sin LERP)
    // porque current_theme_ apunta a DYNAMIC_X (índice >= 7) y no existe en themes_[]
    if (dynamic_theme_active_) {
        dynamic_theme_active_ = false;
        current_dynamic_theme_index_ = -1;

        // Cambio instantáneo: sin transición LERP
        current_theme_ = new_theme;
        transitioning_ = false;

        // Actualizar colores de pelotas al tema final
        const ThemeColors& theme = themes_[static_cast<int>(new_theme)];
        for (size_t i = 0; i < balls_.size(); i++) {
            size_t color_index = i % theme.ball_colors.size();
            balls_[i]->setColor(theme.ball_colors[color_index]);
        }

        // Mostrar nombre del tema
        if (current_app_mode_ == AppMode::MANUAL) {
            text_ = theme.name_es;
            text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
            show_text_ = true;
            text_init_time_ = SDL_GetTicks();
        }
        return;
    }

    // Transición normal entre temas estáticos
    target_theme_ = new_theme;
    transitioning_ = true;
    transition_progress_ = 0.0f;

    // Mostrar nombre del tema (solo si NO estamos en modo demo)
    if (current_app_mode_ == AppMode::MANUAL) {
        ThemeColors& theme = themes_[static_cast<int>(new_theme)];
        text_ = theme.name_es;
        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
        show_text_ = true;
        text_init_time_ = SDL_GetTicks();
    }
}

void Engine::updateDynamicTheme() {
    if (!dynamic_theme_active_ || current_dynamic_theme_index_ < 0) return;
    if (dynamic_theme_paused_) return;  // Pausado con Shift+D

    DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];

    // Obtener keyframe destino para calcular duración
    const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];

    // Avanzar progreso de transición
    dynamic_transition_progress_ += delta_time_ / target_kf.duration;

    if (dynamic_transition_progress_ >= 1.0f) {
        // Transición completa: avanzar al siguiente keyframe
        dynamic_transition_progress_ = 0.0f;
        current_keyframe_index_ = target_keyframe_index_;

        // Calcular siguiente keyframe destino
        target_keyframe_index_++;
        if (target_keyframe_index_ >= theme.keyframes.size()) {
            if (theme.loop) {
                target_keyframe_index_ = 0;  // Volver al inicio
            } else {
                target_keyframe_index_ = theme.keyframes.size() - 1;  // Quedarse en el último
                dynamic_theme_active_ = false;  // Detener animación
            }
        }

        // NOTA: No se actualiza Ball::color_ aquí porque getInterpolatedColor()
        // calcula el color directamente desde los keyframes cada frame.
        // Cuando progress=1.0, getInterpolatedColor() devuelve exactamente el color destino.
    }
}

void Engine::activateDynamicTheme(int index) {
    if (index < 0 || index >= 3) return;  // Validar índice

    // Desactivar transición estática si estaba activa
    if (transitioning_) {
        transitioning_ = false;
        current_theme_ = target_theme_;
    }

    // Activar tema dinámico
    dynamic_theme_active_ = true;
    current_dynamic_theme_index_ = index;
    current_keyframe_index_ = 0;
    target_keyframe_index_ = 1;
    dynamic_transition_progress_ = 0.0f;
    dynamic_theme_paused_ = false;

    // NOTA: No actualizamos current_theme_ porque cuando dynamic_theme_active_=true,
    // todo el código usa current_dynamic_theme_index_ en su lugar

    // Establecer colores iniciales del keyframe 0
    DynamicTheme& theme = dynamic_themes_[index];
    const DynamicThemeKeyframe& initial_kf = theme.keyframes[0];

    for (size_t i = 0; i < balls_.size(); i++) {
        size_t color_index = i % initial_kf.ball_colors.size();
        balls_[i]->setColor(initial_kf.ball_colors[color_index]);
    }

    // Mostrar nombre del tema (solo si NO estamos en modo demo)
    if (current_app_mode_ == AppMode::MANUAL) {
        text_ = theme.name_es;
        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
        show_text_ = true;
        text_init_time_ = SDL_GetTicks();
    }
}

void Engine::pauseDynamicTheme() {
    if (!dynamic_theme_active_) return;  // Solo funciona si hay tema dinámico activo

    dynamic_theme_paused_ = !dynamic_theme_paused_;

    // Mostrar estado de pausa (solo si NO estamos en modo demo)
    if (current_app_mode_ == AppMode::MANUAL) {
        text_ = dynamic_theme_paused_ ? "PAUSADO" : "REANUDADO";
        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
        show_text_ = true;
        text_init_time_ = SDL_GetTicks();
    }
}

Color Engine::getInterpolatedColor(size_t ball_index) const {
    if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
        // Tema dinámico activo: interpolar entre keyframes
        const DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
        const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
        const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];

        // Obtener colores desde keyframes (NO desde Ball::color_)
        size_t color_index = ball_index % current_kf.ball_colors.size();
        Color current_color = current_kf.ball_colors[color_index];  // Color del keyframe actual
        Color target_color = target_kf.ball_colors[color_index];    // Color del keyframe destino

        // Interpolar RGB entre keyframes
        return {
            static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), dynamic_transition_progress_)),
            static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), dynamic_transition_progress_)),
            static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), dynamic_transition_progress_))};
    } else if (transitioning_) {
        // Transición estática: interpolar entre tema actual y tema destino
        Color current_color = balls_[ball_index]->getColor();

        // Obtener el color destino (mismo índice de color en el tema destino)
        const ThemeColors& target_theme = themes_[static_cast<int>(target_theme_)];
        size_t color_index = ball_index % target_theme.ball_colors.size();
        Color target_color = target_theme.ball_colors[color_index];

        // Interpolar RGB
        return {
            static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
            static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
            static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))};
    } else {
        // Sin transición: devolver color actual
        return balls_[ball_index]->getColor();
    }
}

// ============================================================================
// Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO)
// ============================================================================

void Engine::setState(AppMode new_mode) {
    // Si ya estamos en ese modo, no hacer nada
    if (current_app_mode_ == new_mode) return;

    // Al salir de LOGO, guardar en previous_app_mode_ (para volver al modo correcto)
    if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
        previous_app_mode_ = new_mode;
    }

    // Al entrar a LOGO, guardar el modo previo
    if (new_mode == AppMode::LOGO) {
        previous_app_mode_ = current_app_mode_;
    }

    // Aplicar el nuevo modo
    current_app_mode_ = new_mode;

    // Configurar timer de demo según el modo
    if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
        demo_timer_ = 0.0f;
        float min_interval, max_interval;

        if (new_mode == AppMode::LOGO) {
            // Escalar tiempos con resolución (720p como base)
            float resolution_scale = current_screen_height_ / 720.0f;
            logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
            logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;

            min_interval = logo_min_time_;
            max_interval = logo_max_time_;
        } else {
            bool is_lite = (new_mode == AppMode::DEMO_LITE);
            min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
            max_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
        }

        demo_next_action_time_ = min_interval + (rand() % 1000) / 1000.0f * (max_interval - min_interval);
    }
}

// ============================================================================
// Sistema de Modo DEMO (auto-play)
// ============================================================================

void Engine::updateDemoMode() {
    // Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
    if (current_app_mode_ == AppMode::MANUAL) return;

    // Actualizar timer
    demo_timer_ += delta_time_;

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

    if (current_app_mode_ == AppMode::LOGO) {
        // LOGO MODE: Esperar convergencia + tiempo mínimo (o timeout máximo)
        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 (current_app_mode_ == AppMode::LOGO) {
            // Elegir acción aleatoria ponderada
            int action = rand() % 100;

            if (current_mode_ == SimulationMode::SHAPE) {
                // Logo quieto (formado) → 2 opciones posibles
                if (action < 50) {
                    // 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
                    toggleShapeMode(true);
                } else {
                    // 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
                    toggleShapeMode(false);
                }
            } else {
                // Logo animado (PHYSICS) → 3 opciones posibles
                if (action < 60) {
                    // 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
                    toggleShapeMode(false);
                } else if (action < 80) {
                    // 20%: Forzar gravedad ON (empezar a caer mientras da vueltas)
                    forceBallsGravityOn();
                } else {
                    // 20%: Forzar gravedad OFF (flotar mientras da vueltas)
                    forceBallsGravityOff();
                }
            }

            // Resetear timer con intervalos escalados (logo_min_time_ y logo_max_time_)
            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 NO llegamos desde MANUAL
            // Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
            if (previous_app_mode_ != AppMode::MANUAL && rand() % 100 < 60) {
                exitLogoMode(true);  // Volver a DEMO/DEMO_LITE
            }
        }
        // MODO DEMO/DEMO_LITE: Acciones normales
        else {
            bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE);
            performDemoAction(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;
        }
    }
}

void Engine::performDemoAction(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>(balls_.size()) >= LOGO_MODE_MIN_BALLS &&
            current_theme_ == ColorTheme::MONOCHROME) {
            // 10% probabilidad de saltar a Logo Mode
            if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
                enterLogoMode(true);  // Entrar desde DEMO
                return;
            }
        }
    } else {
        // DEMO COMPLETO: Verificar condiciones para salto a Logo Mode
        if (static_cast<int>(balls_.size()) >= LOGO_MODE_MIN_BALLS) {
            // 15% probabilidad de saltar a Logo Mode
            if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
                enterLogoMode(true);  // Entrar desde DEMO
                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);
            changeGravityDirection(new_direction);
            return;
        }

        // Toggle gravedad ON/OFF (20%)
        accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
        if (random_value < accumulated_weight) {
            toggleGravityOnOff();
            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::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            int shape_index = rand() % 8;
            activateShape(shapes[shape_index]);
            return;
        }

        // Toggle física ↔ figura (20%)
        accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
        if (random_value < accumulated_weight) {
            toggleShapeMode(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);
            changeGravityDirection(new_direction);
            return;
        }

        // Toggle gravedad ON/OFF (8%)
        accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
        if (random_value < accumulated_weight) {
            toggleGravityOnOff();
            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::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            int shape_index = rand() % 8;
            activateShape(shapes[shape_index]);
            return;
        }

        // Toggle física ↔ figura (12%)
        accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
        if (random_value < accumulated_weight) {
            toggleShapeMode(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) {
            ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
            startThemeTransition(new_theme);
            return;
        }

        // Cambiar escenario (10%) - EXCLUIR índices 0, 6, 7 (1, 50K, 100K pelotas)
        accumulated_weight += DEMO_WEIGHT_SCENARIO;
        if (random_value < accumulated_weight) {
            // Escenarios válidos: índices 1, 2, 3, 4, 5 (10, 100, 500, 1000, 10000 pelotas)
            int valid_scenarios[] = {1, 2, 3, 4, 5};
            scenario_ = valid_scenarios[rand() % 5];
            initBalls(scenario_);
            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) {
            switchTexture();
            return;
        }
    }
}

// Randomizar todo al iniciar modo DEMO
void Engine::randomizeOnDemoStart(bool is_lite) {
    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) {
                toggleShapeMode(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::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            activateShape(shapes[rand() % 8]);
        }

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

    } else {
        // DEMO COMPLETO: Randomizar TODO

        // 1. Escenario (excluir índices 0, 6, 7)
        int valid_scenarios[] = {1, 2, 3, 4, 5};
        scenario_ = valid_scenarios[rand() % 5];
        initBalls(scenario_);

        // 2. Tema
        ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
        startThemeTransition(new_theme);

        // 3. Sprite
        if (rand() % 2 == 0) {
            switchTexture();
        }

        // 4. Física o Figura
        if (rand() % 2 == 0) {
            // Modo física
            if (current_mode_ == SimulationMode::SHAPE) {
                toggleShapeMode(false);
            }
        } else {
            // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
            ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
            activateShape(shapes[rand() % 8]);

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

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

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

// Toggle gravedad ON/OFF para todas las pelotas
void Engine::toggleGravityOnOff() {
    // Alternar entre activar/desactivar gravedad
    bool first_ball_gravity_enabled = (balls_.empty() || balls_[0]->getGravityForce() > 0.0f);

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

// ============================================================================
// SISTEMA DE MODO LOGO (Easter Egg - "Marca de Agua")
// ============================================================================

// Entrar al Modo Logo (manual con tecla K o automático desde DEMO)
void Engine::enterLogoMode(bool from_demo) {
    // Verificar mínimo de pelotas
    if (static_cast<int>(balls_.size()) < LOGO_MODE_MIN_BALLS) {
        // Ajustar a 5000 pelotas automáticamente
        scenario_ = 5;  // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS)
        initBalls(scenario_);
    }

    // Guardar estado previo (para restaurar al salir)
    logo_previous_theme_ = current_theme_;
    logo_previous_texture_index_ = current_texture_index_;
    logo_previous_shape_scale_ = shape_scale_factor_;

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

    // Aplicar configuración fija del Modo Logo
    if (tiny_index != current_texture_index_) {
        current_texture_index_ = tiny_index;
        int old_size = current_ball_size_;
        current_ball_size_ = textures_[current_texture_index_]->getWidth();
        updateBallSizes(old_size, current_ball_size_);

        // Actualizar textura global y en cada pelota
        texture_ = textures_[current_texture_index_];
        for (auto& ball : balls_) {
            ball->setTexture(texture_);
        }
    }

    // Cambiar a tema MONOCHROME
    startThemeTransition(ColorTheme::MONOCHROME);

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

    // Activar PNG_SHAPE (el logo)
    activateShape(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);
        }
    }

    // Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
    setState(AppMode::LOGO);
}

// Salir del Modo Logo (volver a estado anterior o salir de DEMO)
void Engine::exitLogoMode(bool return_to_demo) {
    if (current_app_mode_ != AppMode::LOGO) return;

    // Restaurar estado previo
    startThemeTransition(logo_previous_theme_);

    if (logo_previous_texture_index_ != current_texture_index_ &&
        logo_previous_texture_index_ < textures_.size()) {
        current_texture_index_ = logo_previous_texture_index_;
        int old_size = current_ball_size_;
        current_ball_size_ = textures_[current_texture_index_]->getWidth();
        updateBallSizes(old_size, current_ball_size_);

        // Actualizar textura global y en cada pelota
        texture_ = textures_[current_texture_index_];
        for (auto& ball : balls_) {
            ball->setTexture(texture_);
        }
    }

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

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

    if (!return_to_demo) {
        // Salida manual (tecla K): volver a MANUAL
        setState(AppMode::MANUAL);
    } else {
        // Volver al modo previo (DEMO o DEMO_LITE)
        setState(previous_app_mode_);
    }
}

// Toggle manual del Modo Logo (tecla K)
void Engine::toggleLogoMode() {
    if (current_app_mode_ == AppMode::LOGO) {
        exitLogoMode(false);  // Salir y volver a MANUAL
    } else {
        enterLogoMode(false);  // Entrar manualmente
    }
}

// Sistema de cambio de sprites dinámico
void Engine::updateBallSizes(int old_size, int new_size) {
    float delta_size = static_cast<float>(new_size - old_size);

    for (auto& ball : balls_) {
        SDL_FRect pos = ball->getPosition();

        // Solo ajustar posición si la pelota está en superficie
        if (ball->isOnSurface()) {
            GravityDirection grav_dir = ball->getGravityDirection();

            switch (grav_dir) {
                case GravityDirection::DOWN:
                    // Superficie inferior: ajustar Y hacia abajo si crece
                    pos.y += delta_size;
                    break;

                case GravityDirection::UP:
                    // Superficie superior: ajustar Y hacia arriba si crece
                    pos.y -= delta_size;
                    break;

                case GravityDirection::LEFT:
                    // Superficie izquierda: ajustar X hacia izquierda si crece
                    pos.x -= delta_size;
                    break;

                case GravityDirection::RIGHT:
                    // Superficie derecha: ajustar X hacia derecha si crece
                    pos.x += delta_size;
                    break;
            }
        }

        // Actualizar tamaño del hitbox
        ball->updateSize(new_size);

        // Si ajustamos posición, aplicarla ahora
        if (ball->isOnSurface()) {
            ball->setRotoBallScreenPosition(pos.x, pos.y);
        }
    }
}

void Engine::switchTexture() {
    if (textures_.empty()) return;

    // Guardar tamaño antiguo
    int old_size = current_ball_size_;

    // 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
    for (auto& ball : balls_) {
        ball->setTexture(texture_);
    }

    // Ajustar posiciones según el cambio de tamaño
    updateBallSizes(old_size, new_size);

    // Mostrar texto informativo (solo si NO estamos en modo demo)
    if (current_app_mode_ == AppMode::MANUAL) {
        // Obtener nombre de textura (uppercase)
        std::string texture_name = texture_names_[current_texture_index_];
        std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);

        text_ = "SPRITE: " + texture_name;
        text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
        show_text_ = true;
        text_init_time_ = SDL_GetTicks();
    }
}

// Sistema de Figuras 3D - Alternar entre modo física y última figura (Toggle con tecla F)
void Engine::toggleShapeMode(bool force_gravity_on_exit) {
    if (current_mode_ == SimulationMode::PHYSICS) {
        // Cambiar a modo figura (usar última figura seleccionada)
        activateShape(last_shape_type_);

        // Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
        if (current_app_mode_ == 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 (current_app_mode_ == 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
        for (auto& ball : balls_) {
            ball->enableRotoBallAttraction(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) {
            forceBallsGravityOn();
        }

        // Mostrar texto informativo (solo si NO estamos en modo demo o logo)
        if (current_app_mode_ == AppMode::MANUAL) {
            text_ = "MODO FISICA";
            int text_width = static_cast<int>(text_.length() * 8);
            text_pos_ = (current_screen_width_ - text_width) / 2;
            text_init_time_ = SDL_GetTicks();
            show_text_ = true;
        }
    }
}

// Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode)
void Engine::activateShape(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
    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::WAVE_GRID:
            active_shape_ = std::make_unique<WaveGridShape>();
            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>("data/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
    for (auto& ball : balls_) {
        ball->enableRotoBallAttraction(true);
    }

    // Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo)
    if (active_shape_ && current_app_mode_ == AppMode::MANUAL) {
        text_ = std::string("MODO ") + active_shape_->getName();
        int text_width = static_cast<int>(text_.length() * 8);
        text_pos_ = (current_screen_width_ - text_width) / 2;
        text_init_time_ = SDL_GetTicks();
        show_text_ = true;
    }
}

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

    int num_points = static_cast<int>(balls_.size());
    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;

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

        // 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]->applyRotoBallForce(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 (current_app_mode_ == 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) / balls_.size();
    }
}

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