#include "core/rendering/menu.hpp"

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <memory>
#include <string>
#include <vector>

#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp"
#include "core/system/director.hpp"
#include "game/defines.hpp"
#include "game/options.hpp"
#include "utils/easing.hpp"
#include "version.h"

namespace Menu {

    // --- Constants visuals ---
    static constexpr int SCREEN_W = 320;
    static constexpr int SCREEN_H = 200;

    static constexpr int BOX_W = 220;

    static constexpr Uint32 BG_COLOR = 0xFF1A0E0E;      // fons marró fosc (ABGR)
    static constexpr Uint8 BG_ALPHA = 220;              // semi-transparent
    static constexpr Uint32 BORDER_COLOR = 0xFFFFFF00;  // cyan
    static constexpr Uint32 TITLE_COLOR = 0xFFFFFFFF;   // blanc
    static constexpr Uint32 LABEL_COLOR = 0xFFCCCCCC;   // gris clar
    static constexpr Uint32 VALUE_COLOR = 0xFFFFFF00;   // cyan
    static constexpr Uint32 CURSOR_COLOR = 0xFF00FFFF;  // groc
    static constexpr Uint32 EMPTY_COLOR = 0xFF888888;   // gris

    static constexpr int TITLE_PAD_Y = 4;
    static constexpr int ITEM_PAD_X = 10;
    static constexpr int ITEM_SPACING = 11;
    static constexpr int BOTTOM_PAD = 6;
    static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4;  // títol + línia + gap
    static constexpr int SUBTITLE_H = 8 + 3;                            // línia de subtítol + gap

    // --- Animació ---
    static constexpr float OPEN_SPEED = 8.0F;    // 1.0 / 0.125s
    static constexpr float CLOSE_SPEED = 10.0F;  // 1.0 / 0.1s (una mica més ràpida que l'obertura)
    static constexpr float HEIGHT_RATE = 12.0F;  // smoothing exponencial de l'alçada (~150 ms al 90%)

    // --- Items ---
    enum class ItemKind : std::uint8_t { TOGGLE,
        CYCLE,
        INT_RANGE,
        SUBMENU,
        KEY_BIND,
        ACTION };

    struct Item {
        const char* label;
        ItemKind kind;
        std::function<std::string()> get_value;  // opcional
        std::function<void(int dir)> change;     // per TOGGLE/CYCLE/INT_RANGE
        std::function<void()> enter;             // per SUBMENU i ACTION
        SDL_Scancode* scancode{nullptr};         // per KEY_BIND
        std::function<bool()> visible;           // nullptr ⇒ sempre visible
    };

    struct Page {
        const char* title;
        std::vector<Item> items;
        int cursor{0};
        std::string subtitle;  // opcional — si no buit, es dibuixa sota el títol
    };

    static auto isVisible(const Item& it) -> bool { return !it.visible || it.visible(); }

    // Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
    // Si cap és visible retorna `from`.
    static auto nextVisibleCursor(const Page& p, int from, int dir) -> int {
        const int N = static_cast<int>(p.items.size());
        if (N <= 0) {
            return from;
        }
        for (int i = 1; i <= N; ++i) {
            int idx = ((from + dir * i) % N + N) % N;
            if (isVisible(p.items[idx])) {
                return idx;
            }
        }
        return from;
    }

    // --- Estat ---
    static std::vector<Page> stack;
    static std::unique_ptr<Text> font;
    static float open_anim{0.0F};   // 0 = tancat, 1 = obert
    static float animated_h{0.0F};  // alçada actual animada (smoothing cap al target visible)
    static Uint32 last_ticks{0};
    static SDL_Scancode* capturing{nullptr};  // != null → esperant tecla per assignar
    static bool closing{false};               // true mentre l'animació de tancament és en curs

    // --- Transició entre pàgines ---
    static constexpr float TRANSITION_SPEED = 5.5F;  // ~180 ms
    static Page transition_outgoing{.title = "", .items = {}, .cursor = 0};
    static bool transition_active{false};
    static float transition_progress{1.0F};
    static int transition_dir{+1};  // +1 endavant, -1 enrere

    // Helpers per triggerar transicions
    static void pushPage(Page new_page) {
        transition_outgoing = stack.back();
        stack.push_back(std::move(new_page));
        transition_active = true;
        transition_progress = 0.0F;
        transition_dir = +1;
    }
    static void popPage() {
        transition_outgoing = stack.back();
        stack.pop_back();
        transition_active = true;
        transition_progress = 0.0F;
        transition_dir = -1;
    }

    // --- Helpers ---

    static auto yesNo(bool b) -> std::string { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); }
    static auto onOff(bool b) -> std::string { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }

    // --- Builders de pàgines ---

    static auto buildVideo() -> Page;
    static auto buildAudio() -> Page;
    static auto buildControls() -> Page;
    static auto buildGame() -> Page;
    static auto buildSystem() -> Page;

    static auto buildRoot() -> Page {
        Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0};
        p.items.push_back({Locale::get("menu.items.video"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
        p.items.push_back({Locale::get("menu.items.audio"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
        p.items.push_back({Locale::get("menu.items.controls"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
        p.items.push_back({Locale::get("menu.items.game"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
        p.items.push_back({Locale::get("menu.items.system"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
        return p;
    }

    static auto buildVideo() -> Page {
        Page p{.title = Locale::get("menu.titles.video"), .items = {}, .cursor = 0};

        // Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
#ifndef __EMSCRIPTEN__
        p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::INT_RANGE, [] {
                char buf[16];
                std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
                return std::string(buf); }, [](int dir) {
                if (dir < 0) { Screen::get()->decZoom();
                } else if (dir > 0) { Screen::get()->incZoom(); 
} }, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.screen"), ItemKind::TOGGLE, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
#endif

        // Opcions visuals generals (sempre visibles)
        p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::TOGGLE, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::TOGGLE, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::CYCLE, [] {
                switch (Options::video.scaling_mode) {
                    case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
                    case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
                    case Options::ScalingMode::LETTERBOX: return std::string(Locale::get("menu.values.scaling_letterbox"));
                    case Options::ScalingMode::OVERSCAN: return std::string(Locale::get("menu.values.scaling_overscan"));
                    case Options::ScalingMode::INTEGER: return std::string(Locale::get("menu.values.scaling_integer"));
                }
                return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::CYCLE, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
                                                                                                   ? Locale::get("menu.values.linear")
                                                                                                   : Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::INT_RANGE, [] {
                char buf[16];
                std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
                return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});

        // Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
#ifndef __EMSCRIPTEN__
        p.items.push_back({Locale::get("menu.items.shader"), ItemKind::TOGGLE, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
                if (dir < 0) { Screen::get()->prevShaderType();
                } else { Screen::get()->nextShaderType(); 
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});

        p.items.push_back({Locale::get("menu.items.preset"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
                if (dir < 0) { Screen::get()->prevPreset();
                } else { Screen::get()->nextPreset(); 
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});

        p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::TOGGLE, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
                if (!Options::video.shader_enabled) { return false;
}
                const char* name = Screen::get()->getActiveShaderName();
                return name && std::string(name) == "POSTFX"; }});
#endif

        // Informació de render
        p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::CYCLE, [] {
                switch (Options::render_info.position) {
                    case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
                    case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
                    case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
                }
                return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});

        p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::TOGGLE, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});

        return p;
    }

    // Converteix volum 0..1 a percentatge i ho formata com "50%"
    static auto volPct(float v) -> std::string {
        int pct = static_cast<int>(std::lround(v * 100.0F));
        pct = std::max(pct, 0);
        pct = std::min(pct, 100);
        char buf[8];
        std::snprintf(buf, sizeof(buf), "%d%%", pct);
        return {buf};
    }

    // Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping
    static void stepVolume(float& v, int dir) {
        v += (dir >= 0 ? 0.05F : -0.05F);
        v = std::max(v, 0.0F);
        v = std::min(v, 1.0F);
        Options::applyAudio();
    }

    static auto buildControls() -> Page {
        Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0};
        p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.up});
        p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.down});
        p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.left});
        p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.right});
        p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
        return p;
    }

    static auto buildAudio() -> Page {
        Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0};

        p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::TOGGLE, [] { return onOff(Options::audio.enabled); }, [](int) {
                Options::audio.enabled = !Options::audio.enabled;
                Options::applyAudio(); }, nullptr});

        p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});

        p.items.push_back({Locale::get("menu.items.music"), ItemKind::TOGGLE, [] { return onOff(Options::audio.music.enabled); }, [](int) {
                Options::audio.music.enabled = !Options::audio.music.enabled;
                Options::applyAudio(); }, nullptr});

        p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});

        p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::TOGGLE, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
                Options::audio.sound.enabled = !Options::audio.sound.enabled;
                Options::applyAudio(); }, nullptr});

        p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});

        return p;
    }

    static auto buildGame() -> Page {
        Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0};

        p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::TOGGLE, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});

        p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});

        p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});

        return p;
    }

    static auto buildSystem() -> Page {
        Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0};
        p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";

        p.items.push_back({Locale::get("menu.items.restart"), ItemKind::ACTION, nullptr, nullptr, [] {
                               if (Director::get()) {
                                   Director::get()->requestRestart();
                               }
                           },
            nullptr,
            nullptr});

#ifndef __EMSCRIPTEN__
        p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::ACTION, nullptr, nullptr, [] {
                               if (Director::get()) {
                                   Director::get()->requestQuit();
                               }
                           },
            nullptr,
            nullptr});
#endif

        return p;
    }

    // --- Dibuix ---

    // Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
    static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
        const Uint8 SA = src_alpha;
        const Uint8 SR = src_argb & 0xFF;
        const Uint8 SG = (src_argb >> 8) & 0xFF;
        const Uint8 SB = (src_argb >> 16) & 0xFF;
        const Uint8 INV = 255 - SA;
        for (int row = y; row < y + h; row++) {
            if (row < 0 || row >= SCREEN_H) {
                continue;
            }
            for (int col = x; col < x + w; col++) {
                if (col < 0 || col >= SCREEN_W) {
                    continue;
                }
                Uint32* p = &buf[col + (row * SCREEN_W)];
                Uint32 dst = *p;
                Uint8 dr = dst & 0xFF;
                Uint8 dg = (dst >> 8) & 0xFF;
                Uint8 db = (dst >> 16) & 0xFF;
                Uint8 r = (SR * SA + dr * INV) / 255;
                Uint8 g = (SG * SA + dg * INV) / 255;
                Uint8 b = (SB * SA + db * INV) / 255;
                *p = 0xFF000000U | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
            }
        }
    }

    static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
        for (int row = y; row < y + h; row++) {
            if (row < 0 || row >= SCREEN_H) {
                continue;
            }
            for (int col = x; col < x + w; col++) {
                if (col < 0 || col >= SCREEN_W) {
                    continue;
                }
                buf[col + (row * SCREEN_W)] = color;
            }
        }
    }

    static void drawBorder(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
        fillRect(buf, x, y, w, 1, color);
        fillRect(buf, x, y + h - 1, w, 1, color);
        fillRect(buf, x, y, 1, h, color);
        fillRect(buf, x + w - 1, y, 1, h, color);
    }

    // Mida final de la caixa segons el nombre d'items *visibles*.
    // body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
    // sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
    static auto boxHeight(const Page& page) -> int {
        const int N = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
        int body = (N == 0) ? 8 : ((N - 1) * ITEM_SPACING) + 8;
        int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
        return header + body + BOTTOM_PAD;
    }

    // --- API pública ---

    void init() {
        font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
        stack.clear();
        open_anim = 0.0F;
        closing = false;
        last_ticks = SDL_GetTicks();
    }

    void destroy() {
        font.reset();
        stack.clear();
        closing = false;
    }

    // "Actiu": accepta input. Durant l'animació de tancament la pila encara
    // té pàgines però ja no ha de processar tecles.
    auto isOpen() -> bool {
        return !stack.empty() && !closing;
    }

    // "Visible": encara hi ha caixa per pintar (incloent close animation).
    auto isVisible() -> bool {
        return !stack.empty();
    }

    void toggle() {
        if (closing && !stack.empty()) {
            // Cancel·la el tancament en curs — continua l'animació cap a "obert"
            // des del valor actual d'open_anim.
            closing = false;
            last_ticks = SDL_GetTicks();
            return;
        }
        if (isOpen()) {
            close();
        } else {
            stack.push_back(buildRoot());
            open_anim = 0.0F;
            closing = false;
            animated_h = static_cast<float>(boxHeight(stack.back()));
            last_ticks = SDL_GetTicks();
        }
    }

    // close() no buida la pila immediatament: marca closing i deixa que
    // render() faça decréixer open_anim fins a 0. En aquell moment es neteja
    // l'estat. Si es crida estant ja tancat o tancant-se, no-op.
    void close() {
        if (stack.empty() || closing) {
            return;
        }
        closing = true;
        capturing = nullptr;
        transition_active = false;
        transition_progress = 1.0F;
        last_ticks = SDL_GetTicks();
    }

    auto isCapturing() -> bool {
        return capturing != nullptr;
    }

    void captureKey(SDL_Scancode sc) {
        if (capturing == nullptr) {
            return;
        }
        if (sc == SDL_SCANCODE_ESCAPE) {
            // Cancel·la
            capturing = nullptr;
            return;
        }
        *capturing = sc;
        capturing = nullptr;
    }

    static void backOrClose() {
        if (stack.size() > 1) {
            popPage();
        } else {
            close();
        }
    }

    // Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
    // KEY_BIND inicia captura, la resta avança change(+1).
    static void activateItem(Item& item) {
        if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
            if (item.enter) {
                item.enter();
            }
        } else if (item.kind == ItemKind::KEY_BIND) {
            capturing = item.scancode;
        } else if (item.change) {
            item.change(+1);
        }
    }

    static void applyKeyToItem(Page& page, SDL_Scancode sc) {
        Item& item = page.items[page.cursor];
        switch (sc) {
            case SDL_SCANCODE_UP:
                page.cursor = nextVisibleCursor(page, page.cursor, -1);
                break;
            case SDL_SCANCODE_DOWN:
                page.cursor = nextVisibleCursor(page, page.cursor, +1);
                break;
            case SDL_SCANCODE_LEFT:
                if (item.kind != ItemKind::SUBMENU && item.change) {
                    item.change(-1);
                }
                break;
            case SDL_SCANCODE_RIGHT:
                if (item.kind != ItemKind::SUBMENU && item.change) {
                    item.change(+1);
                }
                break;
            case SDL_SCANCODE_RETURN:
            case SDL_SCANCODE_KP_ENTER:
                activateItem(item);
                break;
            case SDL_SCANCODE_BACKSPACE:
                backOrClose();
                break;
            default:
                break;
        }
    }

    void handleKey(SDL_Scancode sc) {
        if (!isOpen()) {
            return;
        }
        Page& page = stack.back();
        if (page.items.empty()) {
            if (sc == SDL_SCANCODE_BACKSPACE) {
                backOrClose();
            }
            return;
        }
        if (!isVisible(page.items[page.cursor])) {
            page.cursor = nextVisibleCursor(page, page.cursor, +1);
        }
        applyKeyToItem(page, sc);
        // Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
        // saltem al pròxim visible.
        if (!stack.empty()) {
            Page& top = stack.back();
            if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
                top.cursor = nextVisibleCursor(top, top.cursor, +1);
            }
        }
    }

    // Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
    static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);

    // Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
    // box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
    // clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
    static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
        // Títol
        int title_w = font->width(page.title);
        int title_x = box_x + ((BOX_W - title_w) / 2) + x_offset;
        font->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);

        // Línia sota el títol (també lliscada) — clippada manualment
        int title_line_y = box_y + TITLE_PAD_Y + font->charHeight() + 2;
        if (title_line_y >= clip_y_min && title_line_y < clip_y_max) {
            int line_x = box_x + 4 + x_offset;
            int line_w = BOX_W - 8;
            int line_start = line_x < clip_x_min ? clip_x_min : line_x;
            int line_end = (line_x + line_w) > clip_x_max ? clip_x_max : (line_x + line_w);
            if (line_end > line_start) {
                fillRect(pixel_data, line_start, title_line_y, line_end - line_start, 1, BORDER_COLOR);
            }
        }

        // Subtítol opcional (sota la línia del títol, abans dels items)
        int items_y = title_line_y + 4;
        if (!page.subtitle.empty()) {
            int sub_w = font->width(page.subtitle.c_str());
            int sub_x = box_x + ((BOX_W - sub_w) / 2) + x_offset;
            font->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            items_y += SUBTITLE_H;
        }
        // Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
        const int VISIBLE_COUNT = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
        if (VISIBLE_COUNT == 0) {
            const char* empty_text = Locale::get("menu.values.empty");
            int ew = font->width(empty_text);
            font->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            return;
        }

        int y_slot = 0;
        for (size_t i = 0; i < page.items.size(); i++) {
            const Item& item = page.items[i];
            if (!isVisible(item)) {
                continue;
            }
            const int Y = items_y + (y_slot * ITEM_SPACING);
            ++y_slot;
            const bool SELECTED = (static_cast<int>(i) == page.cursor);
            renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
        }
    }

    static auto keybindText(const Item& item, bool this_capturing) -> const char* {
        const char* text = nullptr;
        if (this_capturing) {
            text = Locale::get("menu.values.press_key");
        } else if (item.scancode != nullptr) {
            text = SDL_GetScancodeName(*item.scancode);
        } else {
            text = "";
        }
        if ((text == nullptr) || (*text == 0)) {
            text = Locale::get("menu.values.unknown");
        }
        return text;
    }

    static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
        const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
        const int LW = font->width(item.label);
        const int LX = box_x + ((BOX_W - LW) / 2) + x_offset;
        if (selected) {
            font->drawClipped(pixel_data, LX - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
        }
        font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
    }

    static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
        if (this_capturing) {
            return 0xFF00FFFF;
        }
        return selected ? CURSOR_COLOR : VALUE_COLOR;
    }

    static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
        if (item.kind == ItemKind::SUBMENU) {
            const char* arrow = ">>";
            const int AW = font->width(arrow);
            const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
            font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - AW + x_offset, y, arrow, AC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            return;
        }
        if (item.kind == ItemKind::KEY_BIND) {
            const bool THIS_CAPTURING = (capturing == item.scancode);
            const char* text = keybindText(item, THIS_CAPTURING);
            const int TW = font->width(text);
            const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
            font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - TW + x_offset, y, text, TC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            return;
        }
        if (item.get_value) {
            const std::string VALUE = item.get_value();
            const int VALUE_W = font->width(VALUE.c_str());
            const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
            font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
        }
    }

    static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
        if (item.kind == ItemKind::ACTION) {
            renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            return;
        }
        const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
        if (selected) {
            font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
        }
        font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
        renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
    }

    void render(Uint32* pixel_data) {
        if (!isVisible() || !font || (pixel_data == nullptr)) {
            return;
        }

        // Delta time
        Uint32 now = SDL_GetTicks();
        float dt = static_cast<float>(now - last_ticks) / 1000.0F;
        last_ticks = now;
        if (closing) {
            open_anim -= CLOSE_SPEED * dt;
            if (open_anim <= 0.0F) {
                // Animació de tancament completada — buida l'estat de veritat.
                open_anim = 0.0F;
                stack.clear();
                animated_h = 0.0F;
                closing = false;
                return;
            }
        } else if (open_anim < 1.0F) {
            open_anim += OPEN_SPEED * dt;
            open_anim = std::min(open_anim, 1.0F);
        }

        // Avança transició
        if (transition_active) {
            transition_progress += TRANSITION_SPEED * dt;
            if (transition_progress >= 1.0F) {
                transition_progress = 1.0F;
                transition_active = false;
            }
        }

        const Page& page = stack.back();
        const int CURRENT_H = boxHeight(page);

        // Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
        // Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
        // d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
        if (animated_h <= 0.0F) {
            animated_h = static_cast<float>(CURRENT_H);
        } else {
            float diff = static_cast<float>(CURRENT_H) - animated_h;
            if (std::fabs(diff) < 0.5F) {
                animated_h = static_cast<float>(CURRENT_H);
            } else {
                float t = HEIGHT_RATE * dt;
                t = std::min(t, 1.0F);
                animated_h += diff * t;
            }
        }

        float eased = Easing::outQuad(open_anim);

        // Calcula alçada (amb transició si escau)
        int target_h = static_cast<int>(animated_h);
        if (transition_active) {
            int outgoing_h = boxHeight(transition_outgoing);
            float tp = Easing::outQuad(transition_progress);
            target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h), tp);
        }

        // Caixa creix verticalment durant l'obertura
        int box_h = static_cast<int>(target_h * eased);
        box_h = std::max(box_h, 2);
        int box_x = (SCREEN_W - BOX_W) / 2;
        int box_y = (SCREEN_H - box_h) / 2;

        // Fons semi-transparent (alpha escalat per l'animació d'obertura)
        auto alpha = static_cast<Uint8>(BG_ALPHA * eased);
        blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha);

        // El contingut només apareix quan la caixa és prou gran
        if (open_anim >= 0.9F) {
            int clip_x_min = box_x + 1;
            int clip_x_max = box_x + BOX_W - 1;
            int clip_y_min = box_y + 1;
            int clip_y_max = box_y + box_h - 1;

            if (transition_active) {
                float tp = Easing::outQuad(transition_progress);
                int out_offset = static_cast<int>(-transition_dir * BOX_W * tp);
                int new_offset = static_cast<int>(transition_dir * BOX_W * (1.0F - tp));
                renderPageContent(pixel_data, transition_outgoing, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
                renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            } else {
                renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
            }
        }

        // Vora per damunt de tot
        drawBorder(pixel_data, box_x, box_y, BOX_W, box_h, BORDER_COLOR);
    }

}  // namespace Menu
