/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/
module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <libudev.h>
#include <libevdev/libevdev.h>
#include <linux/input.h>
#include <linux/input-event-codes.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <cstring>
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#define COBJMACROS
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <windows.gaming.input.h>
#include <roapi.h>
#include <initguid.h>
#endif
module Crafter.Graphics;
import std;

using namespace Crafter;

// ────────────────────────────────────────────────────────────────────
// Linux backend (libudev + libevdev)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
namespace {
    // Per-device runtime state. The public `Gamepad::Device` carries the
    // identity + polled state + events; this struct holds the libevdev /
    // fd / mapping tables we need to drain events from.
    struct LinuxBackend {
        libevdev*     evdev = nullptr;
        int           fd    = -1;
        // Per-axis calibration (libevdev's min/max so we can normalize).
        // Indexed by Gamepad::Axis. -1 in `code` means "this axis isn't
        // exposed by this device".
        struct AxisInfo {
            int   code = -1;       // kernel ABS_* code, or -1
            int   min  = 0;
            int   max  = 0;
            float flat = 0.0f;     // deadzone hint (raw units)
            bool  invertY = false; // negate output (stick Y by convention)
        };
        AxisInfo axisInfo[(std::size_t)Crafter::Gamepad::Axis::Max];
        // FF effect id from EVIOCSFF, -1 if no rumble support / not active.
        int   ffEffectId = -1;
        std::chrono::steady_clock::time_point rumbleExpiry{};
    };

    std::unordered_map<Crafter::Gamepad::Device*, std::unique_ptr<LinuxBackend>> g_backends;
    udev*         g_udev    = nullptr;
    udev_monitor* g_monitor = nullptr;
    bool          g_initialized = false;

    std::uint32_t SyspathToId(const char* syspath) {
        // FNV-1a 32-bit, stable across runs for the same physical device.
        std::uint32_t h = 0x811C9DC5u;
        for (const char* p = syspath; *p; ++p) {
            h ^= static_cast<std::uint8_t>(*p);
            h *= 0x01000193u;
        }
        // Reserve 0 as "unmapped".
        return h == 0 ? 1 : h;
    }

    // BTN_* → Gamepad::Button. -1 = not a button we care about.
    int ButtonFromKernel(int code) {
        using B = Crafter::Gamepad::Button;
        switch (code) {
            case BTN_SOUTH:        return (int)B::South;
            case BTN_EAST:         return (int)B::East;
            case BTN_NORTH:        return (int)B::North;
            case BTN_WEST:         return (int)B::West;
            case BTN_TL:           return (int)B::LeftBumper;
            case BTN_TR:           return (int)B::RightBumper;
            case BTN_TL2:          return (int)B::LeftTrigger;
            case BTN_TR2:          return (int)B::RightTrigger;
            case BTN_SELECT:       return (int)B::Select;
            case BTN_START:        return (int)B::Start;
            case BTN_MODE:         return (int)B::Home;
            case BTN_THUMBL:       return (int)B::LeftStickClick;
            case BTN_THUMBR:       return (int)B::RightStickClick;
            case BTN_DPAD_UP:      return (int)B::DPadUp;
            case BTN_DPAD_DOWN:    return (int)B::DPadDown;
            case BTN_DPAD_LEFT:    return (int)B::DPadLeft;
            case BTN_DPAD_RIGHT:   return (int)B::DPadRight;
            default: return -1;
        }
    }

    // Populate `bp->axisInfo` from libevdev. Sticks use ABS_X/Y/RX/RY;
    // triggers prefer ABS_Z/RZ but fall back to ABS_BRAKE/GAS.
    void DiscoverAxes(LinuxBackend* bp) {
        using A = Crafter::Gamepad::Axis;
        auto setup = [&](A which, int code, bool invertY = false) {
            if (!libevdev_has_event_code(bp->evdev, EV_ABS, code)) return;
            auto* info = libevdev_get_abs_info(bp->evdev, code);
            if (!info) return;
            auto& slot = bp->axisInfo[(std::size_t)which];
            slot.code    = code;
            slot.min     = info->minimum;
            slot.max     = info->maximum;
            slot.flat    = (float)info->flat;
            slot.invertY = invertY;
        };
        setup(A::LeftStickX,  ABS_X);
        setup(A::LeftStickY,  ABS_Y,  /*invertY=*/true);
        setup(A::RightStickX, ABS_RX);
        setup(A::RightStickY, ABS_RY, /*invertY=*/true);
        // Trigger axis: Z/RZ on Xbox, BRAKE/GAS on some others.
        if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_Z)) {
            setup(A::LeftTrigger, ABS_Z);
        } else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_BRAKE)) {
            setup(A::LeftTrigger, ABS_BRAKE);
        }
        if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_RZ)) {
            setup(A::RightTrigger, ABS_RZ);
        } else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_GAS)) {
            setup(A::RightTrigger, ABS_GAS);
        }
    }

    // Normalize a raw axis value to -1..1 (sticks) or 0..1 (triggers).
    float NormalizeAxis(const LinuxBackend::AxisInfo& info, int raw, bool isTrigger) {
        if (info.code < 0 || info.max == info.min) return 0.0f;
        float v;
        if (isTrigger) {
            v = float(raw - info.min) / float(info.max - info.min);
            if (v < 0.0f) v = 0.0f;
            if (v > 1.0f) v = 1.0f;
        } else {
            float mid   = 0.5f * (info.max + info.min);
            float halfR = 0.5f * (info.max - info.min);
            v = (raw - mid) / halfR;
            if (v < -1.0f) v = -1.0f;
            if (v >  1.0f) v =  1.0f;
            if (info.invertY) v = -v;
        }
        return v;
    }

    bool IsGamepadDevice(libevdev* evdev) {
        // A real gamepad reports BTN_GAMEPAD (a kernel alias for BTN_SOUTH
        // when the device is classified as a gamepad). This filters out
        // keyboards/mice/touchpads/tablets that share the evdev interface.
        return libevdev_has_event_type(evdev, EV_KEY)
            && libevdev_has_event_code(evdev, EV_KEY, BTN_GAMEPAD);
    }

    Crafter::Gamepad::Device* OpenDevice(const char* devnode, const char* syspath, const char* name) {
        int fd = ::open(devnode, O_RDWR | O_NONBLOCK);
        if (fd < 0) {
            // Read-only is fine if we just can't rumble.
            fd = ::open(devnode, O_RDONLY | O_NONBLOCK);
            if (fd < 0) return nullptr;
        }
        libevdev* evdev = nullptr;
        if (libevdev_new_from_fd(fd, &evdev) < 0) {
            ::close(fd);
            return nullptr;
        }
        if (!IsGamepadDevice(evdev)) {
            libevdev_free(evdev);
            ::close(fd);
            return nullptr;
        }

        auto dev = std::make_unique<Crafter::Gamepad::Device>();
        dev->id   = SyspathToId(syspath);
        dev->name = name ? name : libevdev_get_name(evdev);

        auto bp = std::make_unique<LinuxBackend>();
        bp->evdev = evdev;
        bp->fd    = fd;
        DiscoverAxes(bp.get());

        auto* raw = dev.get();
        g_backends[raw] = std::move(bp);
        Crafter::Gamepad::connected.push_back(std::move(dev));
        return raw;
    }

    void CloseDevice(Crafter::Gamepad::Device* dev) {
        auto it = g_backends.find(dev);
        if (it != g_backends.end()) {
            if (it->second->evdev) libevdev_free(it->second->evdev);
            if (it->second->fd >= 0) ::close(it->second->fd);
            g_backends.erase(it);
        }
        auto& v = Crafter::Gamepad::connected;
        for (auto vi = v.begin(); vi != v.end(); ++vi) {
            if (vi->get() == dev) { v.erase(vi); break; }
        }
    }

    void EnumerateExisting() {
        udev_enumerate* en = udev_enumerate_new(g_udev);
        if (!en) return;
        udev_enumerate_add_match_subsystem(en, "input");
        udev_enumerate_scan_devices(en);
        udev_list_entry* list = udev_enumerate_get_list_entry(en);
        udev_list_entry* entry;
        udev_list_entry_foreach(entry, list) {
            const char* syspath = udev_list_entry_get_name(entry);
            udev_device* d = udev_device_new_from_syspath(g_udev, syspath);
            if (!d) continue;
            const char* devnode = udev_device_get_devnode(d);
            const char* sysname = udev_device_get_sysname(d);
            if (devnode && sysname && std::strncmp(sysname, "event", 5) == 0) {
                const char* name = udev_device_get_sysattr_value(
                    udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
                    "name");
                auto* dev = OpenDevice(devnode, syspath, name);
                if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
            }
            udev_device_unref(d);
        }
        udev_enumerate_unref(en);
    }

    void HandleHotplugEvent(udev_device* d) {
        const char* action  = udev_device_get_action(d);
        const char* devnode = udev_device_get_devnode(d);
        const char* sysname = udev_device_get_sysname(d);
        const char* syspath = udev_device_get_syspath(d);
        if (!action || !devnode || !sysname || !syspath) return;
        if (std::strncmp(sysname, "event", 5) != 0) return;

        if (std::strcmp(action, "add") == 0) {
            const char* name = udev_device_get_sysattr_value(
                udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
                "name");
            auto* dev = OpenDevice(devnode, syspath, name);
            if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
        } else if (std::strcmp(action, "remove") == 0) {
            std::uint32_t id = SyspathToId(syspath);
            for (auto& up : Crafter::Gamepad::connected) {
                if (up->id == id) {
                    Crafter::Gamepad::Device* dev = up.get();
                    Crafter::Gamepad::onDisconnected.Invoke(dev);
                    CloseDevice(dev);
                    return;
                }
            }
        }
    }

    void DrainHotplug() {
        if (!g_monitor) return;
        for (;;) {
            udev_device* d = udev_monitor_receive_device(g_monitor);
            if (!d) break;
            HandleHotplugEvent(d);
            udev_device_unref(d);
        }
    }

    void DrainDeviceEvents(Crafter::Gamepad::Device* dev, LinuxBackend* bp) {
        using A = Crafter::Gamepad::Axis;
        using B = Crafter::Gamepad::Button;
        // Walk libevdev's internal queue; SYNC means we missed events and
        // need to resync the state. We treat the synced events the same
        // way as normal events — net effect is the polled state ends up
        // correct and the event stream reports the deltas.
        input_event ev;
        int rc;
        while ((rc = libevdev_next_event(bp->evdev,
                    LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC,
                    &ev)) >= 0)
        {
            if (rc == LIBEVDEV_READ_STATUS_SYNC) continue; // handled in next iter
            if (ev.type == EV_KEY) {
                int b = ButtonFromKernel(ev.code);
                if (b < 0) continue;
                bool pressed = (ev.value != 0);
                if (dev->buttons[b] == pressed) continue;
                dev->buttons[b] = pressed;
                if (pressed) dev->onButtonDown.Invoke((B)b);
                else         dev->onButtonUp  .Invoke((B)b);
            } else if (ev.type == EV_ABS) {
                // ABS_HAT0X / ABS_HAT0Y emulate the D-pad on some
                // controllers. Translate to the four DPad buttons so
                // bindings can be uniform.
                if (ev.code == ABS_HAT0X) {
                    bool left  = ev.value < 0;
                    bool right = ev.value > 0;
                    auto edge = [&](int idx, bool now) {
                        if (dev->buttons[idx] == now) return;
                        dev->buttons[idx] = now;
                        if (now) dev->onButtonDown.Invoke((B)idx);
                        else     dev->onButtonUp  .Invoke((B)idx);
                    };
                    edge((int)B::DPadLeft,  left);
                    edge((int)B::DPadRight, right);
                    continue;
                }
                if (ev.code == ABS_HAT0Y) {
                    bool up   = ev.value < 0;
                    bool down = ev.value > 0;
                    auto edge = [&](int idx, bool now) {
                        if (dev->buttons[idx] == now) return;
                        dev->buttons[idx] = now;
                        if (now) dev->onButtonDown.Invoke((B)idx);
                        else     dev->onButtonUp  .Invoke((B)idx);
                    };
                    edge((int)B::DPadUp,   up);
                    edge((int)B::DPadDown, down);
                    continue;
                }
                // Look up which Gamepad::Axis this kernel code maps to
                // (by scanning the calibration table — sticks first,
                // triggers second).
                for (std::size_t i = 0; i < (std::size_t)A::Max; ++i) {
                    if (bp->axisInfo[i].code != ev.code) continue;
                    bool isTrigger = (i == (std::size_t)A::LeftTrigger
                                    || i == (std::size_t)A::RightTrigger);
                    float v = NormalizeAxis(bp->axisInfo[i], ev.value, isTrigger);
                    if (dev->axes[i] == v) break;
                    dev->axes[i] = v;
                    dev->onAxisChanged.Invoke((A)i);
                    break;
                }
            }
        }
    }

    void ExpireRumbles() {
        auto now = std::chrono::steady_clock::now();
        for (auto& [dev, bp] : g_backends) {
            if (bp->ffEffectId >= 0 && now >= bp->rumbleExpiry) {
                input_event stop{};
                stop.type  = EV_FF;
                stop.code  = bp->ffEffectId;
                stop.value = 0;
                if (bp->fd >= 0) ::write(bp->fd, &stop, sizeof(stop));
                ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
                bp->ffEffectId = -1;
            }
        }
    }

    void EnsureInit() {
        if (g_initialized) return;
        g_initialized = true;
        g_udev = udev_new();
        if (!g_udev) return;
        g_monitor = udev_monitor_new_from_netlink(g_udev, "udev");
        if (g_monitor) {
            udev_monitor_filter_add_match_subsystem_devtype(g_monitor, "input", nullptr);
            udev_monitor_enable_receiving(g_monitor);
        }
        EnumerateExisting();
    }
}

void Crafter::Gamepad::Tick() {
    EnsureInit();
    DrainHotplug();
    for (auto& up : Gamepad::connected) {
        Device* dev = up.get();
        auto it = g_backends.find(dev);
        if (it == g_backends.end()) continue;
        DrainDeviceEvents(dev, it->second.get());
    }
    ExpireRumbles();
}

void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
                              std::chrono::milliseconds duration)
{
    auto it = g_backends.find(&dev);
    if (it == g_backends.end()) return;
    LinuxBackend* bp = it->second.get();
    if (bp->fd < 0) return;
    // Cancel any active effect first.
    if (bp->ffEffectId >= 0) {
        input_event stop{};
        stop.type = EV_FF; stop.code = bp->ffEffectId; stop.value = 0;
        ::write(bp->fd, &stop, sizeof(stop));
        ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
        bp->ffEffectId = -1;
    }
    if (duration.count() <= 0 || (low <= 0.0f && high <= 0.0f)) return;
    auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
    ff_effect effect{};
    effect.type = FF_RUMBLE;
    effect.id   = -1;
    effect.u.rumble.strong_magnitude = (std::uint16_t)(clamp01(low)  * 0xFFFFu);
    effect.u.rumble.weak_magnitude   = (std::uint16_t)(clamp01(high) * 0xFFFFu);
    effect.replay.length = (std::uint16_t)std::min<std::int64_t>(duration.count(), 0xFFFF);
    if (ioctl(bp->fd, EVIOCSFF, &effect) < 0) return;
    bp->ffEffectId = effect.id;
    bp->rumbleExpiry = std::chrono::steady_clock::now() + duration;
    input_event play{};
    play.type  = EV_FF;
    play.code  = effect.id;
    play.value = 1;
    ::write(bp->fd, &play, sizeof(play));
}
#endif

// ────────────────────────────────────────────────────────────────────
// Win32 backend (Windows.Gaming.Input via C ABI)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
namespace {
    using IGamepad        = __x_ABI_CWindows_CGaming_CInput_CIGamepad;
    using IGamepadStatics = __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics;
    using GamepadAddedHandler   = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
    using GamepadRemovedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
    using GamepadReading        = __x_ABI_CWindows_CGaming_CInput_CGamepadReading;
    using GamepadVibration      = __x_ABI_CWindows_CGaming_CInput_CGamepadVibration;
    using GamepadButtons        = __x_ABI_CWindows_CGaming_CInput_CGamepadButtons;

    IGamepadStatics* g_statics = nullptr;
    EventRegistrationToken g_addedToken   {};
    EventRegistrationToken g_removedToken {};
    bool g_initialized = false;

    // Cross-thread queue. WGI fires Added/Removed on a thread pool; the
    // event loop drains this on the main thread inside Tick().
    struct QueueEntry {
        bool      added;
        IGamepad* gamepad;  // AddRef'd
    };
    std::mutex             g_queueMutex;
    std::vector<QueueEntry> g_queue;

    // Maps a live IGamepad* to its Crafter::Gamepad::Device. Pointer is
    // stable from WGI's perspective for the lifetime of the connection.
    std::unordered_map<IGamepad*, Crafter::Gamepad::Device*> g_byGamepad;

    std::uint32_t PointerToId(IGamepad* g) {
        // Hash the pointer for a stable-while-connected id. (NonRoamableId
        // would be more durable across reconnects but adds another COM
        // round-trip; pointer hash is fine for v1.)
        std::uintptr_t v = reinterpret_cast<std::uintptr_t>(g);
        v = (v ^ (v >> 16)) * 0x85EBCA6Bu;
        v = (v ^ (v >> 13)) * 0xC2B2AE35u;
        v =  v ^ (v >> 16);
        std::uint32_t out = (std::uint32_t)v;
        return out == 0 ? 1 : out;
    }

    // ── Delegate vtable for GamepadAdded / GamepadRemoved ───────────
    struct AddedDelegate {
        // The first member matches the C ABI vtable layout. Cast yields
        // a valid GamepadAddedHandler*.
        void* vtbl;
        LONG  refCount;
        bool  isAdded;
    };

    extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl;
    extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl;

    HRESULT STDMETHODCALLTYPE DelegateQI(GamepadAddedHandler* This, REFIID riid, void** out) {
        if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IAgileObject) ||
            IsEqualIID(riid, &IID___FIEventHandler_1_Windows__CGaming__CInput__CGamepad))
        {
            *out = This;
            This->lpVtbl->AddRef(This);
            return S_OK;
        }
        *out = nullptr;
        return E_NOINTERFACE;
    }
    ULONG STDMETHODCALLTYPE DelegateAddRef(GamepadAddedHandler* This) {
        AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
        return (ULONG)InterlockedIncrement(&d->refCount);
    }
    ULONG STDMETHODCALLTYPE DelegateRelease(GamepadAddedHandler* This) {
        AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
        LONG r = InterlockedDecrement(&d->refCount);
        return (ULONG)r;
    }
    HRESULT STDMETHODCALLTYPE DelegateInvoke(GamepadAddedHandler* This, IInspectable* sender, IGamepad* gamepad) {
        AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
        if (gamepad) IGamepad_AddRef(gamepad);
        {
            std::lock_guard<std::mutex> lock(g_queueMutex);
            g_queue.push_back({ d->isAdded, gamepad });
        }
        return S_OK;
    }

    GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl = {
        DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
    };
    GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl = {
        DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
    };

    AddedDelegate g_addedDelegate   { &g_addedVtbl,   1, true  };
    AddedDelegate g_removedDelegate { &g_removedVtbl, 1, false };

    // Translate GamepadReading.Buttons bitfield to our Button enum.
    bool ReadingHas(const GamepadReading& r, GamepadButtons b) {
        return (r.Buttons & b) != 0;
    }

    void ApplyReading(Crafter::Gamepad::Device* dev, const GamepadReading& r) {
        using B = Crafter::Gamepad::Button;
        using A = Crafter::Gamepad::Axis;

        struct Map { B which; GamepadButtons mask; };
        static const Map buttonMap[] = {
            { B::DPadUp,         GamepadButtons_DPadUp },
            { B::DPadDown,       GamepadButtons_DPadDown },
            { B::DPadLeft,       GamepadButtons_DPadLeft },
            { B::DPadRight,      GamepadButtons_DPadRight },
            { B::Start,          GamepadButtons_Menu },
            { B::Select,         GamepadButtons_View },
            { B::LeftStickClick, GamepadButtons_LeftThumbstick },
            { B::RightStickClick,GamepadButtons_RightThumbstick },
            { B::LeftBumper,     GamepadButtons_LeftShoulder },
            { B::RightBumper,    GamepadButtons_RightShoulder },
            { B::South,          GamepadButtons_A },
            { B::East,           GamepadButtons_B },
            { B::West,           GamepadButtons_X },
            { B::North,          GamepadButtons_Y },
        };
        for (const Map& m : buttonMap) {
            bool now = ReadingHas(r, m.mask);
            bool& cur = dev->buttons[(std::size_t)m.which];
            if (cur == now) continue;
            cur = now;
            if (now) dev->onButtonDown.Invoke(m.which);
            else     dev->onButtonUp  .Invoke(m.which);
        }
        // WGI doesn't have a Home/Guide button in the standard Gamepad
        // interface (it's reserved for the OS). Leave dev->buttons[Home]
        // at false.

        auto axis = [&](A which, float v) {
            float& cur = dev->axes[(std::size_t)which];
            if (cur == v) return;
            cur = v;
            dev->onAxisChanged.Invoke(which);
        };
        axis(A::LeftStickX,   (float)r.LeftThumbstickX);
        axis(A::LeftStickY,   (float)r.LeftThumbstickY);
        axis(A::RightStickX,  (float)r.RightThumbstickX);
        axis(A::RightStickY,  (float)r.RightThumbstickY);
        axis(A::LeftTrigger,  (float)r.LeftTrigger);
        axis(A::RightTrigger, (float)r.RightTrigger);

        // Triggers also expose digital "pressed" buttons at a threshold.
        constexpr float kTriggerThreshold = 0.1f;
        auto trigBtn = [&](B which, double value) {
            bool now = value >= kTriggerThreshold;
            bool& cur = dev->buttons[(std::size_t)which];
            if (cur == now) return;
            cur = now;
            if (now) dev->onButtonDown.Invoke(which);
            else     dev->onButtonUp  .Invoke(which);
        };
        trigBtn(B::LeftTrigger,  r.LeftTrigger);
        trigBtn(B::RightTrigger, r.RightTrigger);
    }

    void EnsureInit() {
        if (g_initialized) return;
        g_initialized = true;
        // RoInitialize is required once per thread; ignore RPC_E_CHANGED_MODE
        // (some hosts initialize as STA, that's fine for WGI).
        RoInitialize(RO_INIT_MULTITHREADED);

        HSTRING_HEADER header;
        HSTRING className;
        const wchar_t* name = RuntimeClass_Windows_Gaming_Input_Gamepad;
        if (FAILED(WindowsCreateStringReference(name,
                (UINT32)wcslen(name), &header, &className))) return;

        if (FAILED(RoGetActivationFactory(className,
                &IID___x_ABI_CWindows_CGaming_CInput_CIGamepadStatics,
                (void**)&g_statics))) return;

        __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadAdded(
            g_statics,
            reinterpret_cast<GamepadAddedHandler*>(&g_addedDelegate),
            &g_addedToken);
        __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadRemoved(
            g_statics,
            reinterpret_cast<GamepadAddedHandler*>(&g_removedDelegate),
            &g_removedToken);
    }
}

void Crafter::Gamepad::Tick() {
    EnsureInit();
    if (!g_statics) return;

    // Drain the cross-thread connect/disconnect queue.
    std::vector<QueueEntry> pending;
    {
        std::lock_guard<std::mutex> lock(g_queueMutex);
        pending.swap(g_queue);
    }
    for (QueueEntry& e : pending) {
        if (!e.gamepad) continue;
        if (e.added) {
            if (g_byGamepad.find(e.gamepad) != g_byGamepad.end()) {
                IGamepad_Release(e.gamepad);
                continue;
            }
            auto dev = std::make_unique<Crafter::Gamepad::Device>();
            dev->id   = PointerToId(e.gamepad);
            dev->name = "Gamepad";
            auto* raw = dev.get();
            g_byGamepad[e.gamepad] = raw;
            connected.push_back(std::move(dev));
            onConnected.Invoke(raw);
            // keep AddRef'd; released on disconnect
        } else {
            auto it = g_byGamepad.find(e.gamepad);
            if (it != g_byGamepad.end()) {
                Crafter::Gamepad::Device* raw = it->second;
                onDisconnected.Invoke(raw);
                for (auto vi = connected.begin(); vi != connected.end(); ++vi) {
                    if (vi->get() == raw) { connected.erase(vi); break; }
                }
                g_byGamepad.erase(it);
            }
            IGamepad_Release(e.gamepad);
        }
    }

    // Poll each connected gamepad.
    for (auto& [gp, dev] : g_byGamepad) {
        GamepadReading reading{};
        if (FAILED(__x_ABI_CWindows_CGaming_CInput_CIGamepad_GetCurrentReading(gp, &reading))) continue;
        ApplyReading(dev, reading);
    }
}

void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
                              std::chrono::milliseconds /*duration*/)
{
    // WGI vibration is "set and forget" — no duration. Caller can stop
    // by re-calling with zero magnitudes. We honor 0 duration as stop.
    IGamepad* gp = nullptr;
    for (auto& [g, d] : g_byGamepad) {
        if (d == &dev) { gp = g; break; }
    }
    if (!gp) return;
    auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
    GamepadVibration vib{};
    vib.LeftMotor  = clamp01(low);
    vib.RightMotor = clamp01(high);
    __x_ABI_CWindows_CGaming_CInput_CIGamepad_put_Vibration(gp, vib);
}
#endif

// ────────────────────────────────────────────────────────────────────
// DOM backend (browser Gamepad API via env.js polling helpers)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
namespace Crafter::DomEnv {
    // External linkage required for the import_module attribute to bind
    // against the env.js function. An anonymous namespace would drop the
    // attribute at link time.
    __attribute__((import_module("env"), import_name("gamepadPollConnected")))
    bool gamepadPollConnected();
    __attribute__((import_module("env"), import_name("gamepadPollDisconnected")))
    bool gamepadPollDisconnected();
    __attribute__((import_module("env"), import_name("gamepadCount")))
    std::int32_t gamepadCount();
    __attribute__((import_module("env"), import_name("gamepadGetButton")))
    std::int32_t gamepadGetButton(std::int32_t idx, std::int32_t buttonIdx);
    __attribute__((import_module("env"), import_name("gamepadGetAxis")))
    double gamepadGetAxis(std::int32_t idx, std::int32_t axisIdx);
}
namespace {
    using Crafter::DomEnv::gamepadPollConnected;
    using Crafter::DomEnv::gamepadPollDisconnected;
    using Crafter::DomEnv::gamepadCount;
    using Crafter::DomEnv::gamepadGetButton;
    using Crafter::DomEnv::gamepadGetAxis;

    // Standard W3C Gamepad mapping: indices match the "standard" layout
    // every browser exposes for Xbox / DualShock / DualSense controllers.
    // Mismatched / non-standard pads fall through with zeroed buttons.
    constexpr int kStdBtnSouth          = 0;
    constexpr int kStdBtnEast           = 1;
    constexpr int kStdBtnWest           = 2;
    constexpr int kStdBtnNorth          = 3;
    constexpr int kStdBtnLB             = 4;
    constexpr int kStdBtnRB             = 5;
    constexpr int kStdBtnLT             = 6;
    constexpr int kStdBtnRT             = 7;
    constexpr int kStdBtnSelect         = 8;
    constexpr int kStdBtnStart          = 9;
    constexpr int kStdBtnLStickClick    = 10;
    constexpr int kStdBtnRStickClick    = 11;
    constexpr int kStdBtnDPadUp         = 12;
    constexpr int kStdBtnDPadDown       = 13;
    constexpr int kStdBtnDPadLeft       = 14;
    constexpr int kStdBtnDPadRight      = 15;
    constexpr int kStdBtnHome           = 16;

    constexpr int kStdAxisLX = 0, kStdAxisLY = 1, kStdAxisRX = 2, kStdAxisRY = 3;

    // Map our `Gamepad::Button` ordinal to the W3C standard button index.
    int StdButtonIdx(Crafter::Gamepad::Button b) {
        using B = Crafter::Gamepad::Button;
        switch (b) {
            case B::South:           return kStdBtnSouth;
            case B::East:            return kStdBtnEast;
            case B::West:            return kStdBtnWest;
            case B::North:           return kStdBtnNorth;
            case B::Select:          return kStdBtnSelect;
            case B::Start:           return kStdBtnStart;
            case B::Home:            return kStdBtnHome;
            case B::LeftStickClick:  return kStdBtnLStickClick;
            case B::RightStickClick: return kStdBtnRStickClick;
            case B::LeftBumper:      return kStdBtnLB;
            case B::RightBumper:     return kStdBtnRB;
            case B::DPadUp:          return kStdBtnDPadUp;
            case B::DPadDown:        return kStdBtnDPadDown;
            case B::DPadLeft:        return kStdBtnDPadLeft;
            case B::DPadRight:       return kStdBtnDPadRight;
            case B::LeftTrigger:     return kStdBtnLT;
            case B::RightTrigger:    return kStdBtnRT;
            default:                 return -1;
        }
    }
}

void Crafter::Gamepad::Tick() {
    // Handle connect/disconnect by rebuilding `connected` from scratch
    // when the JS flags say something changed. Cheap — only fires on
    // actual hot-plug, not every frame.
    if (gamepadPollConnected() || gamepadPollDisconnected() || connected.empty()) {
        // Snapshot previously-known ids so we can fire onDisconnected
        // for ones that went away in this rebuild.
        std::vector<std::uint32_t> previousIds;
        previousIds.reserve(connected.size());
        for (auto& d : connected) previousIds.push_back(d->id);

        std::int32_t n = gamepadCount();
        connected.clear();
        for (std::int32_t i = 0; i < n; ++i) {
            auto dev = std::make_unique<Device>();
            dev->id = static_cast<std::uint32_t>(i);  // index-based; stable for the page
            dev->name = "Gamepad";
            connected.push_back(std::move(dev));
        }
        // Fire connected events for ids that weren't in the previous set.
        for (auto& d : connected) {
            bool wasKnown = false;
            for (std::uint32_t prev : previousIds) if (prev == d->id) { wasKnown = true; break; }
            if (!wasKnown) onConnected.Invoke(d.get());
        }
        // No way to call onDisconnected with the right Device* once it's
        // gone — fire with nullptr so subscribers know SOMETHING changed.
        for (std::uint32_t prev : previousIds) {
            bool stillThere = false;
            for (auto& d : connected) if (d->id == prev) { stillThere = true; break; }
            if (!stillThere) onDisconnected.Invoke(nullptr);
        }
    }

    // Poll buttons + axes on every Tick — same shape as the native paths.
    for (auto& devUp : connected) {
        Device& dev = *devUp;
        std::int32_t idx = static_cast<std::int32_t>(dev.id);

        for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) {
            int stdIdx = StdButtonIdx(static_cast<Button>(b));
            bool newState = stdIdx >= 0 && gamepadGetButton(idx, stdIdx) != 0;
            if (newState != dev.buttons[b]) {
                dev.buttons[b] = newState;
                if (newState) dev.onButtonDown.Invoke(static_cast<Button>(b));
                else          dev.onButtonUp  .Invoke(static_cast<Button>(b));
            }
        }
        // Axes — sticks come straight through, triggers come from button
        // analog values (the standard mapping exposes those too, with
        // buttons[6/7].value, but the polling shim returns binary
        // pressed-state. For V1, triggers report 0 / 1 only).
        float lx = static_cast<float>(gamepadGetAxis(idx, kStdAxisLX));
        float ly = static_cast<float>(gamepadGetAxis(idx, kStdAxisLY));
        float rx = static_cast<float>(gamepadGetAxis(idx, kStdAxisRX));
        float ry = static_cast<float>(gamepadGetAxis(idx, kStdAxisRY));
        auto setAxis = [&](Axis a, float v) {
            std::size_t i = (std::size_t)a;
            if (dev.axes[i] != v) {
                dev.axes[i] = v;
                dev.onAxisChanged.Invoke(a);
            }
        };
        setAxis(Axis::LeftStickX,   lx);
        setAxis(Axis::LeftStickY,   ly);
        setAxis(Axis::RightStickX,  rx);
        setAxis(Axis::RightStickY,  ry);
        setAxis(Axis::LeftTrigger,  gamepadGetButton(idx, kStdBtnLT) ? 1.0f : 0.0f);
        setAxis(Axis::RightTrigger, gamepadGetButton(idx, kStdBtnRT) ? 1.0f : 0.0f);
    }
}

void Crafter::Gamepad::Rumble(Device& /*dev*/, float /*low*/, float /*high*/,
                              std::chrono::milliseconds /*duration*/) {
    // Browser Gamepad rumble (HapticActuator API) is patchy across
    // engines. V1: no-op. Pads that don't support it would silently
    // fall through anyway.
}
#endif

Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
    for (auto& up : connected) {
        if (up->id == id) return up.get();
    }
    return nullptr;
}
