#include "core/input/gamepad.hpp"

#include <cstdio>
#include <string>

#include "core/input/key_config.hpp"
#include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp"

namespace Gamepad {

    static SDL_Gamepad* pad = nullptr;
    static SDL_JoystickID pad_id = 0;

    // Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
    // de Chrome/Android no porta Vendor/Product, el parser extreu valors
    // escombraries, el GUID no està a gamecontrollerdb i el gamepad queda
    // obert amb un mapping incorrecte). Com el W3C Gamepad API garanteix
    // layout estàndard quan mapping=="standard", injectem un mapping SDL
    // amb eixe layout per al GUID del joystick abans d'obrir-lo com gamepad.
    // Fora d'Emscripten és un no-op.
    static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
        SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
        char guidStr[33];
        SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
        const char* name = SDL_GetJoystickNameForID(jid);
        if (!name || !*name) name = "Standard Gamepad";

        char mapping[512];
        SDL_snprintf(mapping, sizeof(mapping),
            "%s,%s,"
            "a:b0,b:b1,x:b2,y:b3,"
            "leftshoulder:b4,rightshoulder:b5,"
            "lefttrigger:b6,righttrigger:b7,"
            "back:b8,start:b9,"
            "leftstick:b10,rightstick:b11,"
            "dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
            "guide:b16,"
            "leftx:a0,lefty:a1,rightx:a2,righty:a3,"
            "platform:Emscripten",
            guidStr,
            name);
        SDL_AddGamepadMapping(mapping);
#else
        (void)jid;
#endif
    }

    // Recorta el nom visible del mando: trim des del primer '(' o '['
    // (per a evitar coses com "Retroid Controller (vendor: 1001) ..."),
    // elimina espais finals i talla a 25 caràcters.
    static auto prettyName(const char* raw) -> std::string {
        std::string name = ((raw != nullptr) && (*raw != 0)) ? raw : "Gamepad";
        const auto POS = name.find_first_of("([");
        if (POS != std::string::npos) {
            name.erase(POS);
        }
        while (!name.empty() && name.back() == ' ') {
            name.pop_back();
        }
        if (name.size() > 25) {
            name.resize(25);
        }
        if (name.empty()) {
            name = "Gamepad";
        }
        return name;
    }

    // Dead-zone del stick esquerre (rang Sint16: -32768..32767)
    static constexpr Sint16 STICK_DEADZONE = 12000;

    // Estat previ per a detecció de flanc (edge-triggered)
    static bool prev_up = false;
    static bool prev_down = false;
    static bool prev_left = false;
    static bool prev_right = false;
    static bool prev_south = false;
    static bool prev_east = false;
    static bool prev_west = false;
    static bool prev_north = false;
    static bool prev_start = false;
    static bool prev_back = false;

    static void notify(const std::string& name, const char* status_key) {
        std::string msg = name.empty() ? "Gamepad" : name;
        msg += ' ';
        msg += Locale::get(status_key);
        Overlay::showNotification(msg.c_str(), 2.5F);
    }

    static void notifyConnected(const std::string& name) { notify(name, "notifications.gamepad_connected"); }
    static void notifyDisconnected(const std::string& name) { notify(name, "notifications.gamepad_disconnected"); }

    // Obri el primer joystick disponible que siga reconegut com a gamepad
    // (o que ho esdevinga després d'injectar el mapping web estàndard).
    static void openFirstGamepad() {
        int count = 0;
        SDL_JoystickID* ids = SDL_GetJoysticks(&count);
        if (ids != nullptr) {
            for (int i = 0; i < count; ++i) {
                installWebStandardMapping(ids[i]);
                if (!SDL_IsGamepad(ids[i])) {
                    continue;
                }
                pad = SDL_OpenGamepad(ids[i]);
                if (pad != nullptr) {
                    pad_id = ids[i];
                    SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad));
                    break;
                }
            }
            SDL_free(ids);
        }
    }

    void init() {
        if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
            SDL_Log("No s'ha pogut inicialitzar SDL_INIT_GAMEPAD: %s", SDL_GetError());
            return;
        }
        int added = SDL_AddGamepadMappingsFromFile("gamecontrollerdb.txt");
        if (added < 0) {
            SDL_Log("No s'ha pogut carregar gamecontrollerdb.txt: %s", SDL_GetError());
        } else {
            SDL_Log("Carregats %d mappings de gamepad", added);
        }
        openFirstGamepad();
    }

    void destroy() {
        if (pad != nullptr) {
            SDL_CloseGamepad(pad);
            pad = nullptr;
            pad_id = 0;
        }
        SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
    }

    auto isConnected() -> bool {
        return pad != nullptr;
    }

    void handleEvent(const SDL_Event& event) {
        // A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no
        // GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
        // injectem el mapping estàndard abans d'obrir el mando.
        if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
            if (pad == nullptr) {
                SDL_JoystickID jid = event.jdevice.which;
                installWebStandardMapping(jid);
                if (!SDL_IsGamepad(jid)) {
                    return;
                }
                pad = SDL_OpenGamepad(jid);
                if (pad != nullptr) {
                    pad_id = jid;
                    std::string name = prettyName(SDL_GetGamepadName(pad));
                    SDL_Log("Gamepad connectat: %s", name.c_str());
                    notifyConnected(name);
                }
            }
        } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
            if ((pad != nullptr) && event.jdevice.which == pad_id) {
                std::string saved_name = prettyName(SDL_GetGamepadName(pad));
                SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
                SDL_CloseGamepad(pad);
                pad = nullptr;
                pad_id = 0;
                // Neteja qualsevol tecla virtual que poguera estar premuda
                Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
                Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
                Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
                Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
                notifyDisconnected(saved_name);
            }
        }
    }

    // Emet un parell de KEY_DOWN + KEY_UP sintètics que passaran pel handleEvents
    // del Director (menú, ESC, F12, etc.)
    static void pushKey(SDL_Scancode sc) {
        SDL_Event e;
        SDL_zero(e);
        e.type = SDL_EVENT_KEY_DOWN;
        e.key.scancode = sc;
        e.key.repeat = false;
        e.key.down = true;
        SDL_PushEvent(&e);
        e.type = SDL_EVENT_KEY_UP;
        e.key.down = false;
        SDL_PushEvent(&e);
    }

    // Estat agregat d'un frame: D-pad i stick combinats, més botons frontals.
    struct PadState {
        bool up;
        bool down;
        bool left;
        bool right;
        bool south;
        bool east;
        bool west;
        bool north;
        bool start;
        bool back;
    };

    static auto readPadState() -> PadState {
        const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
        const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
        return PadState{
            .up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE,
            .down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE,
            .left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE,
            .right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE,
            .south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH),
            .east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST),
            .west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST),
            .north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH),
            .start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START),
            .back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK),
        };
    }

    static void handleMenuNavigation(const PadState& s) {
        if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); }
        if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); }
        if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); }
        if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); }
        if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); }
        if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); }
        // Mentre el menú està obert, el joc no ha de rebre moviment.
        Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
        Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
        Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
        Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
    }

    static void handleGameInput(const PadState& s) {
        Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up);
        Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down);
        Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left);
        Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right);
        const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) ||
            (s.west && !prev_west) || (s.north && !prev_north);
        if (ANY_FRONT_EDGE) {
            pushKey(SDL_SCANCODE_RETURN);
        }
    }

    void update() {
        if (pad == nullptr) {
            return;
        }
        const PadState S = readPadState();
        // Flancs globals: Select i Start sempre operen.
        if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); }
        if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); }
        if (Menu::isOpen()) {
            handleMenuNavigation(S);
        } else {
            handleGameInput(S);
        }
        prev_up = S.up;
        prev_down = S.down;
        prev_left = S.left;
        prev_right = S.right;
        prev_south = S.south;
        prev_east = S.east;
        prev_west = S.west;
        prev_north = S.north;
        prev_start = S.start;
        prev_back = S.back;
    }

}  // namespace Gamepad
