// service_menu.cpp - Implementacio del menu de servei
// © 2026 JailDesigner

#include "core/system/service_menu.hpp"

#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <format>
#include <utility>

#include "core/audio/audio.hpp"
#include "core/config/engine_config.hpp"
#include "core/defaults/audio.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/service_menu.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/debug_overlay.hpp"
#include "core/system/notifier.hpp"
#include "core/system/relaunch.hpp"
#include "core/types.hpp"
#include "core/utils/string_utils.hpp"
#include "game/config_yaml.hpp"
#include "project.h"

namespace {

    // Easing ease-out quadratic per a l'obertura/tancament. Identic a
    // aee_arcade service_menu.cpp:114-120.
    auto easeOutQuad(float t) -> float {
        t = std::clamp(t, 0.0F, 1.0F);
        const float INV = 1.0F - t;
        return 1.0F - (INV * INV);
    }

    // Canvas logic del joc (constants compartides amb la resta del renderer).
    constexpr float CANVAS_W = 1280.0F;
    constexpr float CANVAS_H = 720.0F;

    // Crida pushRect amb un SDL_Color (els components s'escalen a [0..1]).
    void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
        renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
    }

    void playSelectSound() {
        if (auto* audio = Audio::get(); audio != nullptr) {
            audio->playSound(Defaults::ServiceMenu::SELECT_SOUND, Audio::Group::INTERFACE);
        }
    }

    void playAcceptSound() {
        if (auto* audio = Audio::get(); audio != nullptr) {
            audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
        }
    }

    // Resol el text del label d'un item: prioritza label_text (literal) sobre
    // label_key (locale). Retorna cadena buida si tots dos son buits.
    auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string {
        if (!item.label_text.empty()) {
            return item.label_text;
        }
        if (item.label_key.empty()) {
            return {};
        }
        return Locale::get().text(item.label_key);
    }

    // ---- Helpers de la pagina CONTROLS ----

    auto padDisplayName(int player_index) -> std::string {
        const auto* input = Input::get();
        if (input == nullptr) {
            return Locale::get().text("service_menu.controls_no_pad");
        }
        auto pad = input->getPlayerGamepad(player_index);
        if (!pad) {
            return Locale::get().text("service_menu.controls_no_pad");
        }
        return Utils::toUpperAscii(pad->name);
    }

    // Index del pad assignat dins de la llista. Retorna pads.size() per
    // representar el slot virtual SENSE MANDO (al final del cycle).
    auto findAssignedIndex(const std::vector<std::shared_ptr<Input::Gamepad>>& pads,
        const Config::PlayerBindings& pcfg) -> std::size_t {
        for (std::size_t i = 0; i < pads.size(); ++i) {
            if (pads[i] && !pcfg.gamepad_path.empty() && pads[i]->path == pcfg.gamepad_path) {
                return i;
            }
        }
        for (std::size_t i = 0; i < pads.size(); ++i) {
            if (pads[i] && !pcfg.gamepad_name.empty() && pads[i]->name == pcfg.gamepad_name) {
                return i;
            }
        }
        return pads.size();  // Slot virtual "sense mando"
    }

    // Aplica les noves assignacions a Input. Si ha hagut swap, refresca els
    // dos jugadors; en cas contrari nomes el que ha canviat.
    void reapplyBindings(int player_index, bool swap_other) {
        auto* input = Input::get();
        if (input == nullptr) {
            return;
        }
        if (player_index == 0) {
            input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
            if (swap_other) {
                input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
            }
        } else {
            input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
            if (swap_other) {
                input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
            }
        }
    }

    // Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
    // Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
    // l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
    // new_path buits.
    void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
        auto& pcfg = (player_index == 0)
            ? ConfigYaml::engine_config.player1
            : ConfigYaml::engine_config.player2;
        auto& other = (player_index == 0)
            ? ConfigYaml::engine_config.player2
            : ConfigYaml::engine_config.player1;

        // Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
        // path (mateix criteri que resolvePlayerGamepad); si nomes tenim
        // nom (mando reconegut sense path), comparem per nom.
        const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
        const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
            other.gamepad_name == new_name;
        const bool DO_SWAP = CONFLICT || CONFLICT_BY_NAME;

        const std::string PREV_NAME = pcfg.gamepad_name;
        const std::string PREV_PATH = pcfg.gamepad_path;

        pcfg.gamepad_name = new_name;
        pcfg.gamepad_path = new_path;
        if (DO_SWAP) {
            other.gamepad_name = PREV_NAME;
            other.gamepad_path = PREV_PATH;
        }

        reapplyBindings(player_index, DO_SWAP);
        ConfigYaml::saveToFile();
    }

    // Arranca un rebind o avisa si el jugador no te pad. Retorna true si el
    // rebind ha començat (el caller ha de tancar el menu).
    auto startDefine(System::DefineInputs::Mode mode, System::DefineInputs::Player pl) -> bool {
        auto* di = System::DefineInputs::get();
        if (di == nullptr) {
            return false;
        }
        if (!di->begin(mode, pl)) {
            if (auto* n = System::Notifier::get(); n != nullptr) {
                n->notifyWarn(Locale::get().text("define.no_gamepad"));
            }
            return false;
        }
        return true;
    }

}  // namespace

namespace System {

    std::unique_ptr<ServiceMenu> ServiceMenu::instance;

    void ServiceMenu::init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay) {
        instance.reset(new ServiceMenu(renderer, sdl, debug_overlay));
    }

    void ServiceMenu::destroy() {
        instance.reset();
    }

    auto ServiceMenu::get() -> ServiceMenu* {
        return instance.get();
    }

    ServiceMenu::ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay)
        : renderer_(renderer),
          sdl_(sdl),
          debug_overlay_(debug_overlay),
          text_(renderer) {}

    auto ServiceMenu::isOpen() const -> bool {
        return open_;
    }

    void ServiceMenu::toggle() {
        if (!open_) {
            open_ = true;
            closing_ = false;
            open_anim_ = 0.0F;
            animated_h_ = 0.0F;
            highlight_snap_ = true;  // primera frame: enganxar el highlight al cursor
            buildRootPage();
            // L'ample comença ja al valor objectiu (la caixa surt amb l'amplada
            // final i nomes anima l'alçada). L'ample s'animarà despres entre
            // pagines (push/pop).
            animated_w_ = computeTargetWidth();
            playAcceptSound();
            return;
        }
        // Ja obert: iniciem tancament. open_ es mante a true fins que l'animacio
        // arriba a 0, per a permetre que update() segueixi avançant open_anim_.
        closing_ = true;
        playAcceptSound();
    }

    namespace {

        // Helper local: construeix un item de tipus SUBMENU amb el callback
        // d'entrada. Es manté local a aquesta TU per a poder construir la
        // pagina arrel a buildRootPage sense designed-initializers parcials
        // (clang-tidy es queixa quan en falten).
        auto makeSubmenu(const std::string& label_key, std::function<void()> on_activate) -> ServiceMenu::Item {
            return ServiceMenu::Item{
                .kind = ServiceMenu::Kind::SUBMENU,
                .label_key = label_key,
                .label_text = {},
                .selectable = true,
                .on_activate = std::move(on_activate),
                .get_value_text = {},
                .on_change = {},
            };
        }

    }  // namespace

    void ServiceMenu::buildRootPage() {
        Page root;
        root.title_key = "service_menu.title";
        root.items = {
            makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
            makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
            makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
            makeSubmenu("service_menu.controls", [this] { pushPage(buildControlsPage()); }),
            makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
        };
        stack_.clear();
        stack_.push_back(std::move(root));
    }

    auto ServiceMenu::buildVideoPage() -> Page {
        // Helper: localitza ON/OFF per a TOGGLE items.
        auto on_off_text = [](bool v) -> std::string {
            return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
        };

        SDLManager* sdl = sdl_;

        Page page;
        page.title_key = "service_menu.video";
        page.items = {
            // ZOOM (INT_RANGE-style: ± delega a sdl.increase/decreaseWindowSize).
            Item{
                .kind = Kind::INT_RANGE,
                .label_key = "service_menu.video_zoom",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [sdl] { return std::format("{:.1f}X", sdl->getScaleFactor()); },
                .on_change = [sdl](int dir) {
                    if (dir > 0) {
                        sdl->increaseWindowSize();
                    } else {
                        sdl->decreaseWindowSize();
                    } },
            },
            // FULLSCREEN
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.video_fullscreen",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [sdl, on_off_text] { return on_off_text(sdl->isFullscreen()); },
                .on_change = [sdl](int) { sdl->toggleFullscreen(); },
            },
            // VSYNC
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.video_vsync",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [on_off_text] { return on_off_text(ConfigYaml::engine_config.rendering.vsync != 0); },
                .on_change = [sdl](int) { sdl->toggleVSync(); },
            },
            // RESOLUCIO (sub-submenu amb els 5 presets; mostra l'actual com a valor)
            Item{
                .kind = Kind::SUBMENU,
                .label_key = "service_menu.video_resolution",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] { pushPage(buildResolutionPage()); },
                .get_value_text = [] { return std::format("{}X{}",
                                           ConfigYaml::engine_config.rendering.render_width,
                                           ConfigYaml::engine_config.rendering.render_height); },
                .on_change = {},
            },
            // ANTIALIAS
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.video_aa",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [on_off_text] { return on_off_text(ConfigYaml::engine_config.rendering.antialias != 0); },
                .on_change = [sdl](int) { sdl->toggleAntialias(); },
            },
            // POSTPROCESSAT
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.video_postfx",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [sdl, on_off_text] { return on_off_text(sdl->isPostFxEnabled()); },
                .on_change = [sdl](int) { sdl->togglePostFx(); },
            },
        };
        return page;
    }

    auto ServiceMenu::buildResolutionPage() const -> Page {
        Page page;
        page.title_key = "service_menu.video_resolution";
        // El cursor arrenca sobre el preset actual perquè l'usuari vegi quin
        // esta seleccionat sense buscar-lo.
        const int CURR_W = ConfigYaml::engine_config.rendering.render_width;
        const int CURR_H = ConfigYaml::engine_config.rendering.render_height;
        std::size_t cursor = 0;
        SDLManager* sdl = sdl_;
        for (std::size_t i = 0; i < Defaults::Rendering::RESOLUTION_PRESETS.size(); ++i) {
            const auto& preset = Defaults::Rendering::RESOLUTION_PRESETS[i];
            if (preset.w == CURR_W && preset.h == CURR_H) {
                cursor = i;
            }
            const int PW = preset.w;
            const int PH = preset.h;
            page.items.push_back(Item{
                .kind = Kind::ACTION,
                .label_key = {},
                .label_text = std::format("{}X{}", PW, PH),
                .selectable = true,
                .on_activate = [sdl, PW, PH] { sdl->setRenderResolution(PW, PH); },
                .get_value_text = {},
                .on_change = {},
            });
        }
        page.cursor = cursor;
        return page;
    }

    auto ServiceMenu::buildAudioPage() -> Page {
        auto on_off_text = [](bool v) -> std::string {
            return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
        };

        // Aplica un step de volum (±VOLUME_STEP) a un valor 0..1 i retorna el
        // resultat clampat. El motor s'encarrega d'aplicar-lo amb el getter.
        auto step_volume = [](float current, int dir) -> float {
            const float STEP = Defaults::Audio::VOLUME_STEP;
            return std::clamp(current + (static_cast<float>(dir) * STEP), 0.0F, 1.0F);
        };

        Page page;
        page.title_key = "service_menu.audio";
        page.items = {
            // AUDIO (master ON/OFF)
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.audio_master",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [on_off_text] {
                    const Audio* a = Audio::get();
                    return on_off_text(a != nullptr && a->isEnabled()); },
                .on_change = [](int) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->toggleEnabled();
                        ConfigYaml::engine_config.audio.enabled = a->isEnabled();
                        ConfigYaml::saveToFile();
                    } },
            },
            // VOLUM GENERAL (master)
            Item{
                .kind = Kind::INT_RANGE,
                .label_key = "service_menu.audio_master_volume",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [] {
                    const Audio* a = Audio::get();
                    const float V = (a != nullptr) ? a->getMasterVolume() : 0.0F;
                    return std::to_string(Audio::toPercent(V)); },
                .on_change = [step_volume](int dir) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->setMasterVolume(step_volume(a->getMasterVolume(), dir));
                        ConfigYaml::engine_config.audio.volume = a->getMasterVolume();
                        ConfigYaml::saveToFile();
                    } },
            },
            // MUSICA ON/OFF
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.audio_music",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [on_off_text] {
                    const Audio* a = Audio::get();
                    return on_off_text(a != nullptr && a->isMusicEnabled()); },
                .on_change = [](int) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->toggleMusic();
                        ConfigYaml::engine_config.audio.music_enabled = a->isMusicEnabled();
                        ConfigYaml::saveToFile();
                    } },
            },
            // VOLUM MUSICA
            Item{
                .kind = Kind::INT_RANGE,
                .label_key = "service_menu.audio_music_volume",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [] {
                    const Audio* a = Audio::get();
                    const float V = (a != nullptr) ? a->getMusicVolume() : 0.0F;
                    return std::to_string(Audio::toPercent(V)); },
                .on_change = [step_volume](int dir) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->setMusicVolume(step_volume(a->getMusicVolume(), dir));
                        ConfigYaml::engine_config.audio.music_volume = a->getMusicVolume();
                        ConfigYaml::saveToFile();
                    } },
            },
            // SONS ON/OFF
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.audio_sound",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [on_off_text] {
                    const Audio* a = Audio::get();
                    return on_off_text(a != nullptr && a->isSoundEnabled()); },
                .on_change = [](int) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->toggleSound();
                        ConfigYaml::engine_config.audio.sound_enabled = a->isSoundEnabled();
                        ConfigYaml::saveToFile();
                    } },
            },
            // VOLUM SONS
            Item{
                .kind = Kind::INT_RANGE,
                .label_key = "service_menu.audio_sound_volume",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [] {
                    const Audio* a = Audio::get();
                    const float V = (a != nullptr) ? a->getSoundVolume() : 0.0F;
                    return std::to_string(Audio::toPercent(V)); },
                .on_change = [step_volume](int dir) {
                    if (auto* a = Audio::get(); a != nullptr) {
                        a->setSoundVolume(step_volume(a->getSoundVolume(), dir));
                        ConfigYaml::engine_config.audio.sound_volume = a->getSoundVolume();
                        ConfigYaml::saveToFile();
                    } },
            },
        };
        return page;
    }

    auto ServiceMenu::buildOptionsPage() const -> Page {
        auto on_off_text = [](bool v) -> std::string {
            return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
        };

        DebugOverlay* debug = debug_overlay_;

        Page page;
        page.title_key = "service_menu.options";
        page.items = {
            // IDIOMA (cycle entre ca i en, mateix codi que F7).
            Item{
                .kind = Kind::CYCLE,
                .label_key = "service_menu.options_language",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [] { return Locale::get().text("language." + ConfigYaml::engine_config.locale); },
                .on_change = [](int) {
                    const std::string NEW_LANG = (ConfigYaml::engine_config.locale == "ca") ? "en" : "ca";
                    if (Locale::get().switchTo(NEW_LANG)) {
                        ConfigYaml::engine_config.locale = NEW_LANG;
                        ConfigYaml::saveToFile();
                    } },
            },
            // MOSTRAR INFO (debug overlay, equivalent a F11).
            Item{
                .kind = Kind::TOGGLE,
                .label_key = "service_menu.options_show_info",
                .label_text = {},
                .selectable = true,
                .on_activate = {},
                .get_value_text = [debug, on_off_text] { return on_off_text(debug != nullptr && debug->isVisible()); },
                .on_change = [debug](int) {
                    if (debug != nullptr) {
                        debug->toggle();
                    } },
            },
        };
        return page;
    }

    namespace {

        auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item {
            return ServiceMenu::Item{
                .kind = ServiceMenu::Kind::ACTION,
                .label_key = label_key,
                .label_text = {},
                .selectable = true,
                // El menu de servei NO es tanca: queda obert per sota de
                // l'overlay i absorbira qualsevol event que arribi un cop
                // l'overlay s'haja auto-cancel·lat.
                .on_activate = [mode, pl] { startDefine(mode, pl); },
                .get_value_text = {},
                .on_change = {},
            };
        }

    }  // namespace

    auto ServiceMenu::buildControlsPage() -> Page {
        Page page;
        page.title_key = "service_menu.controls";
        page.items = {
            Item{
                .kind = Kind::SUBMENU,
                .label_key = "service_menu.controls_pad_p1",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] { pushPage(buildPadPickerPage(0)); },
                .get_value_text = [] { return padDisplayName(0); },
                .on_change = {},
            },
            Item{
                .kind = Kind::SUBMENU,
                .label_key = "service_menu.controls_pad_p2",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] { pushPage(buildPadPickerPage(1)); },
                .get_value_text = [] { return padDisplayName(1); },
                .on_change = {},
            },
            makeDefineItem("service_menu.controls_define_keyboard_p1",
                DefineInputs::Mode::KEYBOARD,
                DefineInputs::Player::P1),
            makeDefineItem("service_menu.controls_define_keyboard_p2",
                DefineInputs::Mode::KEYBOARD,
                DefineInputs::Player::P2),
            makeDefineItem("service_menu.controls_define_gamepad_p1",
                DefineInputs::Mode::GAMEPAD,
                DefineInputs::Player::P1),
            makeDefineItem("service_menu.controls_define_gamepad_p2",
                DefineInputs::Mode::GAMEPAD,
                DefineInputs::Player::P2),
        };
        return page;
    }

    auto ServiceMenu::buildPadPickerPage(int player_index) -> Page {
        Page page;
        page.title_key = (player_index == 0)
            ? "service_menu.controls_pad_p1"
            : "service_menu.controls_pad_p2";

        const auto* input = Input::get();
        if (input == nullptr) {
            return page;
        }
        const auto& pads = input->getGamepads();
        const auto& pcfg = (player_index == 0)
            ? ConfigYaml::engine_config.player1
            : ConfigYaml::engine_config.player2;
        const auto& other = (player_index == 0)
            ? ConfigYaml::engine_config.player2
            : ConfigYaml::engine_config.player1;

        // Cursor inicial sobre el pad assignat, o sobre "SENSE MANDO"
        // (ultim item) si el jugador no en te cap.
        page.cursor = findAssignedIndex(pads, pcfg);

        for (const auto& pad : pads) {
            if (!pad) {
                continue;
            }
            std::string label = Utils::toUpperAscii(pad->name);
            // Sufix (PX) nomes si el mando el te l'altre jugador, perque
            // l'usuari sapiga que assignar-lo li'l "robarà".
            const bool OTHER_HAS_BY_PATH = !other.gamepad_path.empty() && other.gamepad_path == pad->path;
            const bool OTHER_HAS_BY_NAME = other.gamepad_path.empty() && !other.gamepad_name.empty() && other.gamepad_name == pad->name;
            if (OTHER_HAS_BY_PATH || OTHER_HAS_BY_NAME) {
                label += (player_index == 0) ? " (P2)" : " (P1)";
            }
            const std::string PAD_NAME = pad->name;
            const std::string PAD_PATH = pad->path;
            const int PI = player_index;
            page.items.push_back(Item{
                .kind = Kind::ACTION,
                .label_key = {},
                .label_text = std::move(label),
                .selectable = true,
                .on_activate = [this, PI, PAD_NAME, PAD_PATH] {
                    assignPadToPlayer(PI, PAD_NAME, PAD_PATH);
                    popPage();
                },
                .get_value_text = {},
                .on_change = {},
            });
        }

        // Item final: desasignar.
        const int PI = player_index;
        page.items.push_back(Item{
            .kind = Kind::ACTION,
            .label_key = "service_menu.controls_no_pad",
            .label_text = {},
            .selectable = true,
            .on_activate = [this, PI] {
                assignPadToPlayer(PI, {}, {});
                popPage();
            },
            .get_value_text = {},
            .on_change = {},
        });
        return page;
    }

    auto ServiceMenu::buildSystemPage() -> Page {
        Page page;
        page.title_key = "service_menu.system";
        // Versio + hash com a subtitol sota el titol (apagat, mes petit).
        // Uppercase del hash perque VectorText nomes admet majuscules.
        page.subtitle_provider = [] {
            return std::format("V{} - {}", Project::VERSION, Utils::toUpperAscii(Project::GIT_HASH));
        };
        page.items = {
            // REINICIAR (amb confirmacio).
            Item{
                .kind = Kind::ACTION,
                .label_key = "service_menu.system_restart",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] {
                    pushConfirmPage("service_menu.confirm_restart", [] {
                        System::Relaunch::request();
                        SDL_Event quit_event{};
                        quit_event.type = SDL_EVENT_QUIT;
                        SDL_PushEvent(&quit_event);
                    });
                },
                .get_value_text = {},
                .on_change = {},
            },
            // EIXIR DEL JOC (amb confirmacio).
            Item{
                .kind = Kind::ACTION,
                .label_key = "service_menu.exit",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] {
                    pushConfirmPage("service_menu.confirm_exit", [] {
                        SDL_Event quit_event{};
                        quit_event.type = SDL_EVENT_QUIT;
                        SDL_PushEvent(&quit_event);
                    });
                },
                .get_value_text = {},
                .on_change = {},
            },
        };
        return page;
    }

    void ServiceMenu::pushConfirmPage(const std::string& title_key, std::function<void()> on_yes) {
        auto yes_callback = std::move(on_yes);
        Page page;
        page.title_key = title_key;
        page.cursor = 0;  // per defecte sobre NO (segur)
        page.items = {
            Item{
                .kind = Kind::ACTION,
                .label_key = "service_menu.confirm_no",
                .label_text = {},
                .selectable = true,
                .on_activate = [this] { popPage(); },
                .get_value_text = {},
                .on_change = {},
            },
            Item{
                .kind = Kind::ACTION,
                .label_key = "service_menu.confirm_yes",
                .label_text = {},
                .selectable = true,
                .on_activate = std::move(yes_callback),
                .get_value_text = {},
                .on_change = {},
            },
        };
        pushPage(std::move(page));
    }

    void ServiceMenu::pushPage(Page page) {
        stack_.push_back(std::move(page));
        // El cursor salta a una pagina nova: enganxem el highlight per a
        // evitar que vagi lliscant des de la posicio anterior.
        highlight_snap_ = true;
    }

    void ServiceMenu::popPage() {
        if (stack_.size() <= 1) {
            // Estem a la pagina arrel: BACKSPACE tanca el menu.
            closing_ = true;
            playAcceptSound();
            return;
        }
        stack_.pop_back();
        highlight_snap_ = true;
        playAcceptSound();
    }

    void ServiceMenu::moveCursor(int direction) {
        if (stack_.empty()) {
            return;
        }
        Page& page = stack_.back();
        const std::size_t N = page.items.size();
        if (N == 0) {
            return;
        }
        // Cerca el seguent item seleccionable amb wrap-around.
        std::size_t idx = page.cursor;
        for (std::size_t step = 0; step < N; ++step) {
            idx = (idx + static_cast<std::size_t>(direction + static_cast<int>(N))) % N;
            if (page.items[idx].selectable) {
                if (idx != page.cursor) {
                    page.cursor = idx;
                    playSelectSound();
                }
                return;
            }
        }
    }

    void ServiceMenu::activateCurrent() {
        // ENTER = canvi de valor cap endavant (equivalent a RIGHT). Per a
        // SUBMENU/ACTION entra/activa; per a TOGGLE/CYCLE/INT_RANGE incrementa.
        changeValue(+1);
    }

    void ServiceMenu::changeValue(int direction) {
        if (stack_.empty()) {
            return;
        }
        const Page& page = stack_.back();
        if (page.cursor >= page.items.size()) {
            return;
        }
        const Item& item = page.items[page.cursor];
        if (!item.selectable) {
            return;
        }
        switch (item.kind) {
            case Kind::TOGGLE:
            case Kind::CYCLE:
            case Kind::INT_RANGE:
                if (item.on_change) {
                    item.on_change(direction);
                    playAcceptSound();
                }
                break;
            case Kind::SUBMENU:
            case Kind::ACTION:
                // Nomes +1 entra/activa: LEFT no fa res (BACKSPACE per a sortir).
                if (direction > 0 && item.on_activate) {
                    item.on_activate();
                    playAcceptSound();
                }
                break;
            case Kind::LABEL:
                break;
        }
    }

    namespace {

        // Llindar de stick per a navegacio de menu (mig camp del rang ±32767).
        // Mes baix que el del joc (30000) per a una resposta mes agil al menu.
        constexpr Sint16 MENU_STICK_THRESHOLD = 16384;
        // Llindar de trigger (mateix valor que MENU_TRIGGER_THRESHOLD, que
        // és private). Edge a partir del 50% del rang.
        constexpr Sint16 MENU_TRIGGER_THRESHOLD = 16384;

        // Retorna true si el codi de boto SDL coincideix amb l'accio
        // configurada per algun dels dos jugadors (es a dir, el boto te el
        // mateix codi al binding de FIRE o ACCELERATE del pad emissor).
        auto buttonMatchesAction(SDL_JoystickID which, int button, InputAction action) -> bool {
            const auto* input = Input::get();
            if (input == nullptr) {
                return false;
            }
            for (int i = 0; i < 2; ++i) {
                auto pad = input->getPlayerGamepad(i);
                if (!pad || pad->instance_id != which) {
                    continue;
                }
                auto it = pad->bindings.find(action);
                if (it != pad->bindings.end() && it->second.button == button) {
                    return true;
                }
            }
            return false;
        }

    }  // namespace

    auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool {
        if (!open_ || stack_.empty()) {
            return false;
        }
        if (event.type == SDL_EVENT_KEY_DOWN) {
            return handleKeyDown(event);
        }
        if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
            return handleGamepadButton(event);
        }
        if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
            return handleGamepadAxis(event);
        }
        return false;
    }

    auto ServiceMenu::handleKeyDown(const SDL_Event& event) -> bool {
        switch (event.key.scancode) {
            case SDL_SCANCODE_UP:
                moveCursor(-1);
                return true;
            case SDL_SCANCODE_DOWN:
                moveCursor(+1);
                return true;
            case SDL_SCANCODE_RETURN:
            case SDL_SCANCODE_KP_ENTER:
                activateCurrent();
                return true;
            case SDL_SCANCODE_RIGHT:
                changeValue(+1);
                return true;
            case SDL_SCANCODE_LEFT:
                changeValue(-1);
                return true;
            case SDL_SCANCODE_BACKSPACE:
                popPage();
                return true;
            default:
                return false;
        }
    }

    auto ServiceMenu::handleGamepadButton(const SDL_Event& event) -> bool {
        const int BTN = static_cast<int>(event.gbutton.button);
        if (BTN == SDL_GAMEPAD_BUTTON_DPAD_UP) {
            moveCursor(-1);
            return true;
        }
        if (BTN == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
            moveCursor(+1);
            return true;
        }
        if (BTN == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
            changeValue(-1);
            return true;
        }
        if (BTN == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
            changeValue(+1);
            return true;
        }
        // Botons d'accio per al pad emissor: FIRE = ENTER, ACCELERATE = BACK.
        if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::SHOOT)) {
            activateCurrent();
            return true;
        }
        if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::THRUST)) {
            popPage();
            return true;
        }
        return false;
    }

    void ServiceMenu::processStickX(Sint16 val) {
        const bool LEFT_NOW = val < -MENU_STICK_THRESHOLD;
        const bool RIGHT_NOW = val > MENU_STICK_THRESHOLD;
        if (LEFT_NOW && !stick_left_held_) {
            changeValue(-1);
        }
        if (RIGHT_NOW && !stick_right_held_) {
            changeValue(+1);
        }
        stick_left_held_ = LEFT_NOW;
        stick_right_held_ = RIGHT_NOW;
    }

    void ServiceMenu::processStickY(Sint16 val) {
        const bool UP_NOW = val < -MENU_STICK_THRESHOLD;
        const bool DOWN_NOW = val > MENU_STICK_THRESHOLD;
        if (UP_NOW && !stick_up_held_) {
            moveCursor(-1);
        }
        if (DOWN_NOW && !stick_down_held_) {
            moveCursor(+1);
        }
        stick_up_held_ = UP_NOW;
        stick_down_held_ = DOWN_NOW;
    }

    // Edge-detect d'un trigger: si creua el llindar amunt, despatxa
    // ENTER/BACK segons el binding (FIRE/ACCEL) que apunta al codi
    // virtual del trigger (100 = L2, 101 = R2).
    void ServiceMenu::processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held) {
        const bool NOW = val > MENU_TRIGGER_THRESHOLD;
        if (NOW && !held) {
            if (buttonMatchesAction(which, virtual_button, InputAction::SHOOT)) {
                activateCurrent();
            } else if (buttonMatchesAction(which, virtual_button, InputAction::THRUST)) {
                popPage();
            }
        }
        held = NOW;
    }

    auto ServiceMenu::handleGamepadAxis(const SDL_Event& event) -> bool {
        const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
        const Sint16 VAL = event.gaxis.value;
        switch (AXIS) {
            case SDL_GAMEPAD_AXIS_LEFTX:
                processStickX(VAL);
                return true;
            case SDL_GAMEPAD_AXIS_LEFTY:
                processStickY(VAL);
                return true;
            // Triggers L2/R2: SDL3 nomes emet AXIS_MOTION, no button events.
            // Per poder rebindar FIRE/ACCEL als triggers, sintetitzem aqui
            // la pulsacio amb edge-detect i la passem pel mateix flux.
            case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
                processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_L2_AS_BUTTON, trigger_l2_held_);
                return true;
            case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
                processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_R2_AS_BUTTON, trigger_r2_held_);
                return true;
            default:
                return false;
        }
    }

    auto ServiceMenu::computeTargetHeight() const -> float {
        if (stack_.empty()) {
            return 0.0F;
        }
        using namespace Defaults::ServiceMenu;
        const Page& page = stack_.back();
        int h = GAP_Y;      // padding superior
        h += TITLE_HEIGHT;  // titol
        if (page.subtitle_provider) {
            h += GAP_Y / 2 + SUBTITLE_HEIGHT;  // subtitol amb mig gap
        }
        h += GAP_Y;                     // gap abans del separador
        h += SEPARATOR_HEIGHT + GAP_Y;  // separador + gap
        const auto N = static_cast<int>(page.items.size());
        if (N > 0) {
            h += (N * ITEM_HEIGHT) + ((N - 1) * ITEM_GAP_Y) + GAP_Y;
        }
        return static_cast<float>(h);
    }

    auto ServiceMenu::computeTargetWidth() const -> float {
        using namespace Defaults::ServiceMenu;
        if (stack_.empty()) {
            return static_cast<float>(BOX_WIDTH_MIN);
        }
        const Page& page = stack_.back();
        // Comencem amb l'ample del titol.
        float content_w = Graphics::VectorText::getTextWidth(
            Locale::get().text(page.title_key),
            TITLE_SCALE,
            TEXT_SPACING);
        if (page.subtitle_provider) {
            content_w = std::max(content_w, Graphics::VectorText::getTextWidth(page.subtitle_provider(), SUBTITLE_SCALE, TEXT_SPACING));
        }
        for (const Item& item : page.items) {
            const std::string LABEL = resolveLabel(item);
            if (LABEL.empty() && item.get_value_text) {
                content_w = std::max(content_w, Graphics::VectorText::getTextWidth(item.get_value_text(), ITEM_SCALE, TEXT_SPACING));
            } else if (item.get_value_text) {
                const float LABEL_W = Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING);
                const float VALUE_W = Graphics::VectorText::getTextWidth(
                    item.get_value_text(),
                    ITEM_SCALE,
                    TEXT_SPACING);
                content_w = std::max(content_w,
                    LABEL_W + static_cast<float>(MIN_LABEL_VALUE_GAP) + VALUE_W);
            } else {
                content_w = std::max(content_w,
                    Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING));
            }
        }
        // Padding total: highlight pad als dos costats + inset del text.
        const float REQUIRED = content_w +
            (2.0F * static_cast<float>(HIGHLIGHT_PAD_X)) +
            (2.0F * static_cast<float>(TEXT_INSET_X));
        return std::max(static_cast<float>(BOX_WIDTH_MIN), REQUIRED);
    }

    auto ServiceMenu::computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float {
        using namespace Defaults::ServiceMenu;
        float items_y0 = box_y +
            static_cast<float>(GAP_Y) +
            static_cast<float>(TITLE_HEIGHT);
        if (has_subtitle) {
            items_y0 += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
        }
        items_y0 += static_cast<float>(GAP_Y) +
            static_cast<float>(SEPARATOR_HEIGHT) +
            static_cast<float>(GAP_Y);
        return items_y0 + (static_cast<float>(index) * static_cast<float>(ITEM_HEIGHT + ITEM_GAP_Y));
    }

    void ServiceMenu::update(float delta_time) {
        if (!open_) {
            return;
        }
        using namespace Defaults::ServiceMenu;

        if (closing_) {
            open_anim_ -= CLOSE_SPEED * delta_time;
            if (open_anim_ <= 0.0F) {
                open_anim_ = 0.0F;
                animated_h_ = 0.0F;
                open_ = false;
                closing_ = false;
                stack_.clear();
                return;
            }
        } else {
            open_anim_ = std::min(1.0F, open_anim_ + (OPEN_SPEED * delta_time));
        }

        // Smoothing exponencial cap a l'alçada i ample objectius de la pagina.
        const float TARGET_H_BOX = closing_ ? 0.0F : computeTargetHeight();
        const float TARGET_W_BOX = closing_ ? animated_w_ : computeTargetWidth();
        const float ALPHA_H = 1.0F - std::exp(-HEIGHT_RATE * delta_time);
        const float ALPHA_W = 1.0F - std::exp(-WIDTH_RATE * delta_time);
        animated_h_ += (TARGET_H_BOX - animated_h_) * ALPHA_H;
        animated_w_ += (TARGET_W_BOX - animated_w_) * ALPHA_W;

        // Highlight: lerp cap a la Y de l'item del cursor. Snapping nomes en
        // obrir o canviar de pagina; en moure el cursor amb UP/DOWN, el rect
        // llisca suaument cap a la nova posicio.
        if (stack_.empty()) {
            return;
        }
        const Page& page = stack_.back();
        if (page.items.empty()) {
            highlight_snap_ = true;
            return;
        }
        const float BOX_H_TARGET = computeTargetHeight();
        const float BOX_Y_TARGET = (CANVAS_H - BOX_H_TARGET) * 0.5F;
        const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);
        const float ITEM_TOP = computeItemTopY(BOX_Y_TARGET, page.cursor, HAS_SUBTITLE);
        const float TARGET_Y = ITEM_TOP - static_cast<float>(HIGHLIGHT_PAD_Y);
        const float TARGET_H = static_cast<float>(ITEM_HEIGHT) + (2.0F * static_cast<float>(HIGHLIGHT_PAD_Y));
        if (highlight_snap_) {
            highlight_y_ = TARGET_Y;
            highlight_h_ = TARGET_H;
            highlight_snap_ = false;
        } else {
            const float HL_ALPHA = 1.0F - std::exp(-HIGHLIGHT_RATE * delta_time);
            highlight_y_ += (TARGET_Y - highlight_y_) * HL_ALPHA;
            highlight_h_ += (TARGET_H - highlight_h_) * HL_ALPHA;
        }
    }

    namespace {

        // Dibuixa un rect (BG sombrejat + 4 ticks L als cantons), simulant
        // un visor sci-fi al voltant de l'item sel·leccionat.
        void drawHighlightRect(Rendering::Renderer* renderer, float x, float y, float w, float h) {
            using namespace Defaults::ServiceMenu;
            if (w <= 0.0F || h <= 0.0F) {
                return;
            }
            // Wash de fons translucid.
            fillRect(renderer, x, y, w, h, HIGHLIGHT_FILL);

            const auto T = static_cast<float>(HIGHLIGHT_THICKNESS);
            const auto L = static_cast<float>(HIGHLIGHT_TICK_LEN);

            // Top-left
            fillRect(renderer, x, y, L, T, HIGHLIGHT_OUTLINE);
            fillRect(renderer, x, y, T, L, HIGHLIGHT_OUTLINE);
            // Top-right
            fillRect(renderer, x + w - L, y, L, T, HIGHLIGHT_OUTLINE);
            fillRect(renderer, x + w - T, y, T, L, HIGHLIGHT_OUTLINE);
            // Bottom-left
            fillRect(renderer, x, y + h - T, L, T, HIGHLIGHT_OUTLINE);
            fillRect(renderer, x, y + h - L, T, L, HIGHLIGHT_OUTLINE);
            // Bottom-right
            fillRect(renderer, x + w - L, y + h - T, L, T, HIGHLIGHT_OUTLINE);
            fillRect(renderer, x + w - T, y + h - L, T, L, HIGHLIGHT_OUTLINE);
        }

        // Brackets als 4 cantons de la caixa (sci-fi HUD). Substitueix la vora
        // completa per un marc obert.
        void drawCornerBrackets(Rendering::Renderer* renderer, float x, float y, float w, float h) {
            using namespace Defaults::ServiceMenu;
            const auto T = static_cast<float>(CORNER_THICKNESS);
            const auto AH = static_cast<float>(CORNER_ARM_H);
            const auto AV = static_cast<float>(CORNER_ARM_V);

            // Top-left
            fillRect(renderer, x, y, AH, T, CORNER_COLOR);
            fillRect(renderer, x, y, T, AV, CORNER_COLOR);
            // Top-right
            fillRect(renderer, x + w - AH, y, AH, T, CORNER_COLOR);
            fillRect(renderer, x + w - T, y, T, AV, CORNER_COLOR);
            // Bottom-left
            fillRect(renderer, x, y + h - T, AH, T, CORNER_COLOR);
            fillRect(renderer, x, y + h - AV, T, AV, CORNER_COLOR);
            // Bottom-right
            fillRect(renderer, x + w - AH, y + h - T, AH, T, CORNER_COLOR);
            fillRect(renderer, x + w - T, y + h - AV, T, AV, CORNER_COLOR);
        }

    }  // namespace

    void ServiceMenu::draw() const {
        if (!open_ || stack_.empty() || renderer_ == nullptr) {
            return;
        }
        using namespace Defaults::ServiceMenu;

        // Alçada final: smoothing × easing. easeOutQuad afegeix la sensacio
        // de "snap" al final de l'obertura i l'inici del tancament.
        const float EASED = easeOutQuad(open_anim_);
        const float BOX_H = animated_h_ * EASED;
        if (BOX_H < 1.0F) {
            return;
        }

        const float BOX_W = animated_w_;
        const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
        const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
        const float CENTER_X = BOX_X + (BOX_W * 0.5F);

        // Fons semi-transparent.
        fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);

        // Brackets als cantons (substitueixen la vora completa).
        drawCornerBrackets(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H);

        // Clip interior per a tallar text que sortiria del cuadre durant
        // l'animacio open/close. Marge generos perquè no es mengi els brackets.
        const int CLIP_X = static_cast<int>(BOX_X + static_cast<float>(CORNER_THICKNESS));
        const int CLIP_Y = static_cast<int>(BOX_Y + static_cast<float>(CORNER_THICKNESS));
        const int CLIP_W = static_cast<int>(BOX_W - (2.0F * static_cast<float>(CORNER_THICKNESS)));
        const int CLIP_H = std::max(0, static_cast<int>(BOX_H - (2.0F * static_cast<float>(CORNER_THICKNESS))));
        renderer_->pushClip(CLIP_X, CLIP_Y, CLIP_W, CLIP_H);

        const Page& page = stack_.back();
        const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);

        // Titol centrat al cim de la caixa.
        const std::string TITLE = Locale::get().text(page.title_key);
        const float TITLE_CY = BOX_Y + static_cast<float>(GAP_Y) + (static_cast<float>(TITLE_HEIGHT) * 0.5F);
        text_.renderCentered(TITLE,
            Vec2{.x = CENTER_X, .y = TITLE_CY},
            TITLE_SCALE,
            TEXT_SPACING,
            1.0F,
            TITLE_COLOR);

        // Subtitol opcional: sota el titol, mes petit i apagat.
        if (HAS_SUBTITLE) {
            const float SUBTITLE_CY = BOX_Y + static_cast<float>(GAP_Y) +
                static_cast<float>(TITLE_HEIGHT) +
                (static_cast<float>(GAP_Y) / 4.0F) +
                (static_cast<float>(SUBTITLE_HEIGHT) * 0.5F);
            text_.renderCentered(page.subtitle_provider(),
                Vec2{.x = CENTER_X, .y = SUBTITLE_CY},
                SUBTITLE_SCALE,
                TEXT_SPACING,
                1.0F,
                SUBTITLE_COLOR);
        }

        // Separador horitzontal sota el titol (o subtitol si n'hi ha).
        float sep_y = BOX_Y + static_cast<float>(GAP_Y) + static_cast<float>(TITLE_HEIGHT);
        if (HAS_SUBTITLE) {
            sep_y += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
        }
        sep_y += static_cast<float>(GAP_Y) * 0.5F;
        const float SEP_Y = sep_y;
        fillRect(renderer_,
            BOX_X + static_cast<float>(GAP_Y),
            SEP_Y,
            BOX_W - (2.0F * static_cast<float>(GAP_Y)),
            static_cast<float>(SEPARATOR_HEIGHT),
            SEPARATOR_COLOR);

        // Highlight rect: nomes si la pagina te items i el rect te alçada.
        if (!page.items.empty() && highlight_h_ > 0.0F) {
            const float HL_X = BOX_X + static_cast<float>(HIGHLIGHT_PAD_X);
            const float HL_W = BOX_W - (2.0F * static_cast<float>(HIGHLIGHT_PAD_X));
            drawHighlightRect(renderer_, HL_X, highlight_y_, HL_W, highlight_h_);
        }

        // Llista d'items.
        // - Items amb valor (TOGGLE/CYCLE/INT_RANGE): label esquerra + valor dreta dins del highlight.
        // - Items sense valor (SUBMENU/ACTION/LABEL): label centrat.
        const float HL_LEFT = BOX_X + static_cast<float>(HIGHLIGHT_PAD_X);
        const float HL_RIGHT = BOX_X + BOX_W - static_cast<float>(HIGHLIGHT_PAD_X);
        const float TEXT_TOP_OFFSET = Graphics::VectorText::getTextHeight(ITEM_SCALE) * 0.5F;
        for (std::size_t i = 0; i < page.items.size(); ++i) {
            const Item& item = page.items[i];
            const SDL_Color COL = (i == page.cursor) ? CURSOR_COLOR : LABEL_COLOR;
            // resolveLabel prioritza label_text (literal) sobre label_key (locale).
            const std::string LABEL = resolveLabel(item);
            const float ITEM_TOP = computeItemTopY(BOX_Y, i, HAS_SUBTITLE);
            const float ITEM_CY = ITEM_TOP + (static_cast<float>(ITEM_HEIGHT) * 0.5F);

            if (LABEL.empty() && item.get_value_text) {
                // Item nomes-valor (sense label): el text del valor es
                // renderitza centrat com a label decoratiu. Util per a items
                // d'informacio com la versio/hash a SISTEMA.
                text_.renderCentered(item.get_value_text(),
                    Vec2{.x = CENTER_X, .y = ITEM_CY},
                    ITEM_SCALE,
                    TEXT_SPACING,
                    1.0F,
                    COL);
            } else if (item.get_value_text) {
                // Layout dues columnes: label esquerra, valor dreta.
                const std::string VALUE = item.get_value_text();
                const float TEXT_TOP_Y = ITEM_CY - TEXT_TOP_OFFSET;
                const float VALUE_W = Graphics::VectorText::getTextWidth(VALUE, ITEM_SCALE, TEXT_SPACING);
                text_.render(LABEL,
                    Vec2{.x = HL_LEFT + static_cast<float>(TEXT_INSET_X), .y = TEXT_TOP_Y},
                    ITEM_SCALE,
                    TEXT_SPACING,
                    1.0F,
                    COL);
                text_.render(VALUE,
                    Vec2{.x = HL_RIGHT - static_cast<float>(TEXT_INSET_X) - VALUE_W, .y = TEXT_TOP_Y},
                    ITEM_SCALE,
                    TEXT_SPACING,
                    1.0F,
                    COL);
            } else {
                // Layout simple: label centrat.
                text_.renderCentered(LABEL,
                    Vec2{.x = CENTER_X, .y = ITEM_CY},
                    ITEM_SCALE,
                    TEXT_SPACING,
                    1.0F,
                    COL);
            }
        }

        renderer_->popClip();
    }

}  // namespace System
