/*
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 version 3.0 as published by the Free Software Foundation;

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

// Implementation of the :Dom partition. Only ever built when
// CRAFTER_GRAPHICS_WINDOW_DOM is defined (project.cpp's DOM branch
// is what brings this file into the source list). All WASM imports
// resolve against the `env` module produced by additional/dom-env.js;
// all exports become C entry points the JS bridge calls back through.

module;
module Crafter.Graphics;
import std;

using namespace Crafter::Dom;

// ─── WASM env imports + per-event-kind handler maps ───────────────────
//
// External linkage required for the import_module/import_name attributes
// to actually wire up to a WASM env symbol — anonymous namespace would
// hide them. The namespace also gates access from the extern "C"
// dispatcher block below.
namespace Crafter::DomBindings {
    // DOM ops (string args are pointer/length pairs — the JS side decodes
    // UTF-8 directly out of WASM linear memory; no copying on the C++
    // side).
    __attribute__((import_module("env"), import_name("freeJs")))
    void FreeJs(std::int32_t ptr);
    __attribute__((import_module("env"), import_name("getElementById")))
    std::int32_t GetElementById(const char* id, std::int32_t idLength);
    __attribute__((import_module("env"), import_name("createElement")))
    std::int32_t CreateElement(std::int32_t parentPtr,
                               const char* tagName, std::int32_t tagNameLength,
                               const char* id, std::int32_t idLength);
    __attribute__((import_module("env"), import_name("getBody")))
    std::int32_t GetBody();
    __attribute__((import_module("env"), import_name("setInnerHTML")))
    void SetInnerHTML(std::int32_t ptr, const char* html, std::int32_t htmlLength);
    __attribute__((import_module("env"), import_name("setStyle")))
    void SetStyle(std::int32_t ptr, const char* style, std::int32_t styleLength);
    __attribute__((import_module("env"), import_name("setProperty")))
    void SetProperty(std::int32_t ptr,
                     const char* property, std::int32_t propertyLength,
                     const char* value, std::int32_t valueLength);
    __attribute__((import_module("env"), import_name("addClass")))
    void AddClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
    __attribute__((import_module("env"), import_name("removeClass")))
    void RemoveClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
    __attribute__((import_module("env"), import_name("toggleClass")))
    void ToggleClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
    __attribute__((import_module("env"), import_name("hasClass")))
    bool HasClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
    __attribute__((import_module("env"), import_name("deleteElement")))
    void DeleteElement(std::int32_t ptr);
    __attribute__((import_module("env"), import_name("getValue")))
    const char* GetValue(std::int32_t ptr);
    __attribute__((import_module("env"), import_name("setValue")))
    void SetValue(std::int32_t ptr, const char* value, std::int32_t valueLength);

    // Per-event-kind listener register / unregister imports.
    #define CG_DOM_LISTENER_IMPORT(addName, removeName)                                          \
        __attribute__((import_module("env"), import_name(#addName)))                             \
        void addName(std::int32_t ptr, std::int32_t id);                                         \
        __attribute__((import_module("env"), import_name(#removeName)))                          \
        void removeName(std::int32_t ptr, std::int32_t id);

    CG_DOM_LISTENER_IMPORT(addClickListener,       removeClickListener)
    CG_DOM_LISTENER_IMPORT(addMouseOverListener,   removeMouseOverListener)
    CG_DOM_LISTENER_IMPORT(addMouseOutListener,    removeMouseOutListener)
    CG_DOM_LISTENER_IMPORT(addMouseMoveListener,   removeMouseMoveListener)
    CG_DOM_LISTENER_IMPORT(addMouseDownListener,   removeMouseDownListener)
    CG_DOM_LISTENER_IMPORT(addMouseUpListener,     removeMouseUpListener)
    CG_DOM_LISTENER_IMPORT(addFocusListener,       removeFocusListener)
    CG_DOM_LISTENER_IMPORT(addBlurListener,        removeBlurListener)
    CG_DOM_LISTENER_IMPORT(addKeyDownListener,     removeKeyDownListener)
    CG_DOM_LISTENER_IMPORT(addKeyUpListener,       removeKeyUpListener)
    CG_DOM_LISTENER_IMPORT(addKeyPressListener,    removeKeyPressListener)
    CG_DOM_LISTENER_IMPORT(addChangeListener,      removeChangeListener)
    CG_DOM_LISTENER_IMPORT(addSubmitListener,      removeSubmitListener)
    CG_DOM_LISTENER_IMPORT(addInputListener,       removeInputListener)
    CG_DOM_LISTENER_IMPORT(addResizeListener,      removeResizeListener)
    CG_DOM_LISTENER_IMPORT(addScrollListener,      removeScrollListener)
    CG_DOM_LISTENER_IMPORT(addContextMenuListener, removeContextMenuListener)
    CG_DOM_LISTENER_IMPORT(addDragStartListener,   removeDragStartListener)
    CG_DOM_LISTENER_IMPORT(addDragEndListener,     removeDragEndListener)
    CG_DOM_LISTENER_IMPORT(addDropListener,        removeDropListener)
    CG_DOM_LISTENER_IMPORT(addDragOverListener,    removeDragOverListener)
    CG_DOM_LISTENER_IMPORT(addDragEnterListener,   removeDragEnterListener)
    CG_DOM_LISTENER_IMPORT(addDragLeaveListener,   removeDragLeaveListener)
    CG_DOM_LISTENER_IMPORT(addWheelListener,       removeWheelListener)
    #undef CG_DOM_LISTENER_IMPORT

    // Per-event-kind callback maps. Counters are per-kind so two
    // different event kinds can share an id without aliasing — the JS
    // bridge keys its own handler table by (ptr, id, kind) for the same
    // reason. `maxId` is bumped without wrap-around handling: 2 billion
    // listeners over the lifetime of a single page is enough.
    template <typename EvT>
    struct HandlerTable {
        std::int32_t maxId = 0;
        std::unordered_map<std::int32_t, std::function<void(EvT)>> map;
    };
    template <>
    struct HandlerTable<void> {
        std::int32_t maxId = 0;
        std::unordered_map<std::int32_t, std::function<void()>> map;
    };

    inline HandlerTable<MouseEvent>    clickT, mouseOverT, mouseOutT, mouseMoveT,
                                       mouseDownT, mouseUpT, contextMenuT,
                                       dragStartT, dragEndT, dropT,
                                       dragOverT, dragEnterT, dragLeaveT;
    inline HandlerTable<WheelEvent>    wheelT;
    inline HandlerTable<FocusEvent>    focusT, blurT;
    inline HandlerTable<KeyboardEvent> keyDownT, keyUpT, keyPressT;
    inline HandlerTable<ChangeEvent>   changeT;
    inline HandlerTable<InputEvent>    inputT;
    inline HandlerTable<ResizeEvent>   resizeT;
    inline HandlerTable<ScrollEvent>   scrollT;
    inline HandlerTable<void>          submitT;
    inline HandlerTable<void>          popStateT;
    inline HandlerTable<std::string>   fetchT;  // Reserved for future use.

    // Opaque cross-TU helpers for the popState table. Router.cpp lives
    // in a different translation unit and can't touch HandlerTable
    // (re-defining the specialization there would be an ODR violation);
    // it goes through these instead. Defined below the namespace.
    std::int32_t PopStateRegister(std::function<void()> cb);
    void         PopStateUnregister(std::int32_t id);

    // The 24 stable indices used by HtmlElementPtr::handlerIds_[] to
    // map the per-kind id list back to the right `remove*Listener`
    // import on destruction. Order MUST match the cppm comment.
    enum class Kind : std::uint8_t {
        Click = 0, MouseOver, MouseOut, MouseMove, MouseDown, MouseUp,
        Focus, Blur, KeyDown, KeyUp, KeyPress, Change, Submit, Input,
        Resize, Scroll, ContextMenu, DragStart, DragEnd, Drop,
        DragOver, DragEnter, DragLeave, Wheel,
    };
}

// ─── WASM exports the JS bridge calls back through ────────────────────
//
// One per event kind plus the popstate / fetch dispatchers. Each takes
// the handler id + the marshaled event fields, looks up the table, and
// invokes the std::function. find()->second is intentional: a callback
// firing for an id we never registered is a bridge bug worth crashing
// on (the env.js side stores its own handler table keyed by id and
// only calls Execute*Handler for ids it minted).

extern "C" {
    __attribute__((export_name("WasmAlloc")))
    void* WasmAlloc(std::int32_t size) { return std::malloc(size); }

    __attribute__((export_name("WasmFree")))
    void WasmFree(void* ptr) { std::free(ptr); }

    #define CG_DOM_EXEC_MOUSE(exportName, table)                                                 \
        __attribute__((export_name(#exportName)))                                                \
        void exportName(std::int32_t handlerID,                                                  \
                        double clientX, double clientY,                                          \
                        double screenX, double screenY,                                          \
                        std::int32_t button, std::int32_t buttons,                               \
                        bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) {                \
            Crafter::DomBindings::table.map.find(handlerID)->second(                             \
                MouseEvent(clientX, clientY, screenX, screenY,                                   \
                           button, buttons, altKey, ctrlKey, shiftKey, metaKey));                \
        }

    CG_DOM_EXEC_MOUSE(ExecuteClickHandler,       clickT)
    CG_DOM_EXEC_MOUSE(ExecuteMouseOverHandler,   mouseOverT)
    CG_DOM_EXEC_MOUSE(ExecuteMouseOutHandler,    mouseOutT)
    CG_DOM_EXEC_MOUSE(ExecuteMouseMoveHandler,   mouseMoveT)
    CG_DOM_EXEC_MOUSE(ExecuteMouseDownHandler,   mouseDownT)
    CG_DOM_EXEC_MOUSE(ExecuteMouseUpHandler,     mouseUpT)
    CG_DOM_EXEC_MOUSE(ExecuteContextMenuHandler, contextMenuT)
    CG_DOM_EXEC_MOUSE(ExecuteDragStartHandler,   dragStartT)
    CG_DOM_EXEC_MOUSE(ExecuteDragEndHandler,     dragEndT)
    CG_DOM_EXEC_MOUSE(ExecuteDropHandler,        dropT)
    CG_DOM_EXEC_MOUSE(ExecuteDragOverHandler,    dragOverT)
    CG_DOM_EXEC_MOUSE(ExecuteDragEnterHandler,   dragEnterT)
    CG_DOM_EXEC_MOUSE(ExecuteDragLeaveHandler,   dragLeaveT)
    #undef CG_DOM_EXEC_MOUSE

    __attribute__((export_name("ExecuteWheelHandler")))
    void ExecuteWheelHandler(std::int32_t handlerID,
                             double deltaX, double deltaY, double deltaZ, std::int32_t /*deltaMode*/,
                             double clientX, double clientY, double screenX, double screenY,
                             std::int32_t button, std::int32_t buttons,
                             bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) {
        Crafter::DomBindings::wheelT.map.find(handlerID)->second(
            WheelEvent(clientX, clientY, screenX, screenY,
                       button, buttons, altKey, ctrlKey, shiftKey, metaKey,
                       deltaX, deltaY, deltaZ));
    }

    __attribute__((export_name("ExecuteFocusHandler")))
    void ExecuteFocusHandler(std::int32_t handlerID, void* target, void* relatedTarget) {
        Crafter::DomBindings::focusT.map.find(handlerID)->second(
            FocusEvent(target, relatedTarget));
    }

    __attribute__((export_name("ExecuteBlurHandler")))
    void ExecuteBlurHandler(std::int32_t handlerID, void* target, void* relatedTarget) {
        Crafter::DomBindings::blurT.map.find(handlerID)->second(
            FocusEvent(target, relatedTarget));
    }

    #define CG_DOM_EXEC_KEY(exportName, table)                                                   \
        __attribute__((export_name(#exportName)))                                                \
        void exportName(std::int32_t handlerID,                                                  \
                        const char* key, std::int32_t keyCode,                                   \
                        bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) {                \
            Crafter::DomBindings::table.map.find(handlerID)->second(                             \
                KeyboardEvent(key, keyCode, altKey, ctrlKey, shiftKey, metaKey));                \
        }

    CG_DOM_EXEC_KEY(ExecuteKeyDownHandler,  keyDownT)
    CG_DOM_EXEC_KEY(ExecuteKeyUpHandler,    keyUpT)
    CG_DOM_EXEC_KEY(ExecuteKeyPressHandler, keyPressT)
    #undef CG_DOM_EXEC_KEY

    __attribute__((export_name("ExecuteChangeHandler")))
    void ExecuteChangeHandler(std::int32_t handlerID, const char* value) {
        Crafter::DomBindings::changeT.map.find(handlerID)->second(ChangeEvent(value));
    }

    __attribute__((export_name("ExecuteSubmitHandler")))
    void ExecuteSubmitHandler(std::int32_t handlerID) {
        Crafter::DomBindings::submitT.map.find(handlerID)->second();
    }

    __attribute__((export_name("ExecuteInputHandler")))
    void ExecuteInputHandler(std::int32_t handlerID, const char* data, bool isComposing) {
        Crafter::DomBindings::inputT.map.find(handlerID)->second(InputEvent(data, isComposing));
    }

    __attribute__((export_name("ExecuteResizeHandler")))
    void ExecuteResizeHandler(std::int32_t handlerID, std::int32_t width, std::int32_t height) {
        Crafter::DomBindings::resizeT.map.find(handlerID)->second(ResizeEvent(width, height));
    }

    __attribute__((export_name("ExecuteScrollHandler")))
    void ExecuteScrollHandler(std::int32_t handlerID, double scrollX, double scrollY) {
        Crafter::DomBindings::scrollT.map.find(handlerID)->second(ScrollEvent(scrollX, scrollY));
    }

    __attribute__((export_name("ExecutePopStateHandler")))
    void ExecutePopStateHandler(std::int32_t handlerID) {
        Crafter::DomBindings::popStateT.map.find(handlerID)->second();
    }
}

// ─── Listener add/remove helpers ──────────────────────────────────────
//
// Each Add*Listener follows the same five-step pattern; capturing it in
// a single function template removes ~120 lines of near-duplicate body
// code. AddImpl returns the freshly minted id (already pushed onto the
// per-element tracking list). RemoveImpl performs the inverse for an
// id known to belong to the right table.

namespace {
    template <typename EvT, typename JsAdd>
    std::int32_t AddImpl(Crafter::DomBindings::HandlerTable<EvT>& table,
                         std::int32_t ptr,
                         std::vector<std::int32_t>& trackingList,
                         JsAdd jsAdd,
                         std::function<void(EvT)> callback) {
        if (ptr == 0) return 0;
        std::int32_t id = table.maxId++;
        table.map.insert({id, std::move(callback)});
        jsAdd(ptr, id);
        trackingList.push_back(id);
        return id;
    }
    template <typename JsAdd>
    std::int32_t AddVoidImpl(Crafter::DomBindings::HandlerTable<void>& table,
                             std::int32_t ptr,
                             std::vector<std::int32_t>& trackingList,
                             JsAdd jsAdd,
                             std::function<void()> callback) {
        if (ptr == 0) return 0;
        std::int32_t id = table.maxId++;
        table.map.insert({id, std::move(callback)});
        jsAdd(ptr, id);
        trackingList.push_back(id);
        return id;
    }

    template <typename Table, typename JsRemove>
    void RemoveImpl(Table& table,
                    std::int32_t ptr,
                    std::vector<std::int32_t>& trackingList,
                    JsRemove jsRemove,
                    std::int32_t id) {
        if (ptr == 0) return;
        table.map.erase(id);
        trackingList.erase(std::remove(trackingList.begin(), trackingList.end(), id),
                           trackingList.end());
        jsRemove(ptr, id);
    }
}

// ─── HtmlElementPtr ───────────────────────────────────────────────────

namespace Crafter::Dom {

    HtmlElementPtr::HtmlElementPtr(const std::string_view id)
        : ptr(Crafter::DomBindings::GetElementById(id.data(), id.size())) {}

    HtmlElementPtr::HtmlElementPtr(const std::string_view id, const std::string_view html)
        : ptr(Crafter::DomBindings::GetElementById(id.data(), id.size())) {
        if (ptr != 0) {
            Crafter::DomBindings::SetInnerHTML(ptr, html.data(), html.size());
        }
    }

    HtmlElementPtr::HtmlElementPtr(HtmlElementPtr&& other) noexcept
        : ptr(other.ptr) {
        other.ptr = 0;
        for (std::size_t i = 0; i < 24; ++i) handlerIds_[i] = std::move(other.handlerIds_[i]);
    }

    HtmlElementPtr& HtmlElementPtr::operator=(HtmlElementPtr&& other) noexcept {
        if (this == &other) return *this;
        RemoveAllHandlersAndFree();
        ptr = other.ptr;
        other.ptr = 0;
        for (std::size_t i = 0; i < 24; ++i) handlerIds_[i] = std::move(other.handlerIds_[i]);
        return *this;
    }

    HtmlElementPtr::~HtmlElementPtr() {
        RemoveAllHandlersAndFree();
    }

    void HtmlElementPtr::RemoveAllHandlersAndFree() {
        if (ptr == 0) return;
        // Unregister every still-live handler the user forgot about.
        // The per-kind index matches the cppm comment ordering and the
        // Kind enum above; keep them in sync.
        namespace D = Crafter::DomBindings;
        auto sweep = [this](auto& table, auto jsRemove, std::size_t kind) {
            for (std::int32_t id : handlerIds_[kind]) {
                table.map.erase(id);
                jsRemove(ptr, id);
            }
            handlerIds_[kind].clear();
        };
        sweep(D::clickT,       D::removeClickListener,       (std::size_t)D::Kind::Click);
        sweep(D::mouseOverT,   D::removeMouseOverListener,   (std::size_t)D::Kind::MouseOver);
        sweep(D::mouseOutT,    D::removeMouseOutListener,    (std::size_t)D::Kind::MouseOut);
        sweep(D::mouseMoveT,   D::removeMouseMoveListener,   (std::size_t)D::Kind::MouseMove);
        sweep(D::mouseDownT,   D::removeMouseDownListener,   (std::size_t)D::Kind::MouseDown);
        sweep(D::mouseUpT,     D::removeMouseUpListener,     (std::size_t)D::Kind::MouseUp);
        sweep(D::focusT,       D::removeFocusListener,       (std::size_t)D::Kind::Focus);
        sweep(D::blurT,        D::removeBlurListener,        (std::size_t)D::Kind::Blur);
        sweep(D::keyDownT,     D::removeKeyDownListener,     (std::size_t)D::Kind::KeyDown);
        sweep(D::keyUpT,       D::removeKeyUpListener,       (std::size_t)D::Kind::KeyUp);
        sweep(D::keyPressT,    D::removeKeyPressListener,    (std::size_t)D::Kind::KeyPress);
        sweep(D::changeT,      D::removeChangeListener,      (std::size_t)D::Kind::Change);
        sweep(D::submitT,      D::removeSubmitListener,      (std::size_t)D::Kind::Submit);
        sweep(D::inputT,       D::removeInputListener,       (std::size_t)D::Kind::Input);
        sweep(D::resizeT,      D::removeResizeListener,      (std::size_t)D::Kind::Resize);
        sweep(D::scrollT,      D::removeScrollListener,      (std::size_t)D::Kind::Scroll);
        sweep(D::contextMenuT, D::removeContextMenuListener, (std::size_t)D::Kind::ContextMenu);
        sweep(D::dragStartT,   D::removeDragStartListener,   (std::size_t)D::Kind::DragStart);
        sweep(D::dragEndT,     D::removeDragEndListener,     (std::size_t)D::Kind::DragEnd);
        sweep(D::dropT,        D::removeDropListener,        (std::size_t)D::Kind::Drop);
        sweep(D::dragOverT,    D::removeDragOverListener,    (std::size_t)D::Kind::DragOver);
        sweep(D::dragEnterT,   D::removeDragEnterListener,   (std::size_t)D::Kind::DragEnter);
        sweep(D::dragLeaveT,   D::removeDragLeaveListener,   (std::size_t)D::Kind::DragLeave);
        sweep(D::wheelT,       D::removeWheelListener,       (std::size_t)D::Kind::Wheel);
        D::FreeJs(ptr);
        ptr = 0;
    }

    // DOM ops are thin pass-throughs; the JS side does the work.
    void HtmlElementPtr::SetInnerHTML(const std::string_view html) {
        if (ptr) Crafter::DomBindings::SetInnerHTML(ptr, html.data(), html.size());
    }
    void HtmlElementPtr::SetStyle(const std::string_view style) {
        if (ptr) Crafter::DomBindings::SetStyle(ptr, style.data(), style.size());
    }
    void HtmlElementPtr::SetProperty(const std::string_view property, const std::string_view value) {
        if (ptr) Crafter::DomBindings::SetProperty(ptr,
            property.data(), property.size(),
            value.data(),    value.size());
    }
    void HtmlElementPtr::AddClass(const std::string_view className) {
        if (ptr) Crafter::DomBindings::AddClass(ptr, className.data(), className.size());
    }
    void HtmlElementPtr::RemoveClass(const std::string_view className) {
        if (ptr) Crafter::DomBindings::RemoveClass(ptr, className.data(), className.size());
    }
    void HtmlElementPtr::ToggleClass(const std::string_view className) {
        if (ptr) Crafter::DomBindings::ToggleClass(ptr, className.data(), className.size());
    }
    bool HtmlElementPtr::HasClass(const std::string_view className) {
        return ptr && Crafter::DomBindings::HasClass(ptr, className.data(), className.size());
    }
    std::string HtmlElementPtr::GetValue() {
        if (!ptr) return {};
        // The JS side WasmAlloc's a NUL-terminated buffer. We own it
        // after the call — copy into std::string and free.
        const char* raw = Crafter::DomBindings::GetValue(ptr);
        if (!raw) return {};
        std::string out(raw);
        std::free(const_cast<char*>(raw));
        return out;
    }
    void HtmlElementPtr::SetValue(const std::string_view value) {
        if (ptr) Crafter::DomBindings::SetValue(ptr, value.data(), value.size());
    }

    // Listener wrappers. Each Add/Remove pair just plugs the right
    // table, kind index, and js import into the helper templates. The
    // 23 (+1 for popstate, lived in :Router) listener kinds previously
    // expanded to ~250 lines of body; here they're ~60.

    #define CG_DOM_ADD(MethodName, JsAdd, TableName, EvType, KindEnum)                           \
        std::int32_t HtmlElementPtr::MethodName(std::function<void(EvType)> cb) {                \
            return AddImpl<EvType>(Crafter::DomBindings::TableName, ptr,                         \
                handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum],                  \
                Crafter::DomBindings::JsAdd, std::move(cb));                                     \
        }
    #define CG_DOM_REMOVE(MethodName, JsRemove, TableName, KindEnum)                             \
        void HtmlElementPtr::MethodName(std::int32_t id) {                                       \
            RemoveImpl(Crafter::DomBindings::TableName, ptr,                                     \
                handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum],                  \
                Crafter::DomBindings::JsRemove, id);                                             \
        }

    CG_DOM_ADD   (AddClickListener,        addClickListener,        clickT,       MouseEvent,    Click)
    CG_DOM_REMOVE(RemoveClickListener,     removeClickListener,     clickT,                      Click)
    CG_DOM_ADD   (AddMouseOverListener,    addMouseOverListener,    mouseOverT,   MouseEvent,    MouseOver)
    CG_DOM_REMOVE(RemoveMouseOverListener, removeMouseOverListener, mouseOverT,                  MouseOver)
    CG_DOM_ADD   (AddMouseOutListener,     addMouseOutListener,     mouseOutT,    MouseEvent,    MouseOut)
    CG_DOM_REMOVE(RemoveMouseOutListener,  removeMouseOutListener,  mouseOutT,                   MouseOut)
    CG_DOM_ADD   (AddMouseMoveListener,    addMouseMoveListener,    mouseMoveT,   MouseEvent,    MouseMove)
    CG_DOM_REMOVE(RemoveMouseMoveListener, removeMouseMoveListener, mouseMoveT,                  MouseMove)
    CG_DOM_ADD   (AddMouseDownListener,    addMouseDownListener,    mouseDownT,   MouseEvent,    MouseDown)
    CG_DOM_REMOVE(RemoveMouseDownListener, removeMouseDownListener, mouseDownT,                  MouseDown)
    CG_DOM_ADD   (AddMouseUpListener,      addMouseUpListener,      mouseUpT,     MouseEvent,    MouseUp)
    CG_DOM_REMOVE(RemoveMouseUpListener,   removeMouseUpListener,   mouseUpT,                    MouseUp)
    CG_DOM_ADD   (AddFocusListener,        addFocusListener,        focusT,       FocusEvent,    Focus)
    CG_DOM_REMOVE(RemoveFocusListener,     removeFocusListener,     focusT,                      Focus)
    CG_DOM_ADD   (AddBlurListener,         addBlurListener,         blurT,        FocusEvent,    Blur)
    CG_DOM_REMOVE(RemoveBlurListener,      removeBlurListener,      blurT,                       Blur)
    CG_DOM_ADD   (AddKeyDownListener,      addKeyDownListener,      keyDownT,     KeyboardEvent, KeyDown)
    CG_DOM_REMOVE(RemoveKeyDownListener,   removeKeyDownListener,   keyDownT,                    KeyDown)
    CG_DOM_ADD   (AddKeyUpListener,        addKeyUpListener,        keyUpT,       KeyboardEvent, KeyUp)
    CG_DOM_REMOVE(RemoveKeyUpListener,     removeKeyUpListener,     keyUpT,                      KeyUp)
    CG_DOM_ADD   (AddKeyPressListener,     addKeyPressListener,     keyPressT,    KeyboardEvent, KeyPress)
    CG_DOM_REMOVE(RemoveKeyPressListener,  removeKeyPressListener,  keyPressT,                   KeyPress)
    CG_DOM_ADD   (AddChangeListener,       addChangeListener,       changeT,      ChangeEvent,   Change)
    CG_DOM_REMOVE(RemoveChangeListener,    removeChangeListener,    changeT,                     Change)
    CG_DOM_ADD   (AddInputListener,        addInputListener,        inputT,       InputEvent,    Input)
    CG_DOM_REMOVE(RemoveInputListener,     removeInputListener,     inputT,                      Input)
    CG_DOM_ADD   (AddResizeListener,       addResizeListener,       resizeT,      ResizeEvent,   Resize)
    CG_DOM_REMOVE(RemoveResizeListener,    removeResizeListener,    resizeT,                     Resize)
    CG_DOM_ADD   (AddScrollListener,       addScrollListener,       scrollT,      ScrollEvent,   Scroll)
    CG_DOM_REMOVE(RemoveScrollListener,    removeScrollListener,    scrollT,                     Scroll)
    CG_DOM_ADD   (AddContextMenuListener,  addContextMenuListener,  contextMenuT, MouseEvent,    ContextMenu)
    CG_DOM_REMOVE(RemoveContextMenuListener,removeContextMenuListener,contextMenuT,              ContextMenu)
    CG_DOM_ADD   (AddDragStartListener,    addDragStartListener,    dragStartT,   MouseEvent,    DragStart)
    CG_DOM_REMOVE(RemoveDragStartListener, removeDragStartListener, dragStartT,                  DragStart)
    CG_DOM_ADD   (AddDragEndListener,      addDragEndListener,      dragEndT,     MouseEvent,    DragEnd)
    CG_DOM_REMOVE(RemoveDragEndListener,   removeDragEndListener,   dragEndT,                    DragEnd)
    CG_DOM_ADD   (AddDropListener,         addDropListener,         dropT,        MouseEvent,    Drop)
    CG_DOM_REMOVE(RemoveDropListener,      removeDropListener,      dropT,                       Drop)
    CG_DOM_ADD   (AddDragOverListener,     addDragOverListener,     dragOverT,    MouseEvent,    DragOver)
    CG_DOM_REMOVE(RemoveDragOverListener,  removeDragOverListener,  dragOverT,                   DragOver)
    CG_DOM_ADD   (AddDragEnterListener,    addDragEnterListener,    dragEnterT,   MouseEvent,    DragEnter)
    CG_DOM_REMOVE(RemoveDragEnterListener, removeDragEnterListener, dragEnterT,                  DragEnter)
    CG_DOM_ADD   (AddDragLeaveListener,    addDragLeaveListener,    dragLeaveT,   MouseEvent,    DragLeave)
    CG_DOM_REMOVE(RemoveDragLeaveListener, removeDragLeaveListener, dragLeaveT,                  DragLeave)
    CG_DOM_ADD   (AddWheelListener,        addWheelListener,        wheelT,       WheelEvent,    Wheel)
    CG_DOM_REMOVE(RemoveWheelListener,     removeWheelListener,     wheelT,                      Wheel)
    #undef CG_DOM_ADD
    #undef CG_DOM_REMOVE

    // Submit takes std::function<void()> — needs the void specialization.
    std::int32_t HtmlElementPtr::AddSubmitListener(std::function<void()> cb) {
        return AddVoidImpl(Crafter::DomBindings::submitT, ptr,
            handlerIds_[(std::size_t)Crafter::DomBindings::Kind::Submit],
            Crafter::DomBindings::addSubmitListener, std::move(cb));
    }
    void HtmlElementPtr::RemoveSubmitListener(std::int32_t id) {
        RemoveImpl(Crafter::DomBindings::submitT, ptr,
            handlerIds_[(std::size_t)Crafter::DomBindings::Kind::Submit],
            Crafter::DomBindings::removeSubmitListener, id);
    }

    // ─── HtmlElement ──────────────────────────────────────────────────

    HtmlElement::HtmlElement(const std::string_view id)
        : HtmlElementPtr(id) {}

    HtmlElement::HtmlElement(const std::string_view id, const std::string_view html)
        : HtmlElementPtr(id, html) {}

    HtmlElement::HtmlElement(HtmlElement&& other) noexcept
        : HtmlElementPtr(std::move(other)) {}

    HtmlElement& HtmlElement::operator=(HtmlElement&& other) noexcept {
        if (this == &other) return *this;
        HtmlElementPtr::operator=(std::move(other));
        return *this;
    }

    HtmlElement::~HtmlElement() {
        // Remove the element from the DOM first. The base destructor
        // runs next (implicit) and will sweep handler maps + call
        // FreeJs on the still-valid JS cookie. DeleteElement only
        // removes the node from its parent; the cookie itself stays
        // alive in jsmemory until FreeJs.
        if (ptr != 0) {
            Crafter::DomBindings::DeleteElement(ptr);
        }
    }

    HtmlElement HtmlElement::Create(const HtmlElementPtr& parent,
                                    std::string_view tagName,
                                    std::string_view id) {
        std::int32_t handle = Crafter::DomBindings::CreateElement(
            parent.ptr,
            tagName.data(), static_cast<std::int32_t>(tagName.size()),
            id.data(),      static_cast<std::int32_t>(id.size()));
        return HtmlElement(HtmlElementPtr::FromHandle{handle});
    }

    HtmlElement HtmlElement::CreateInBody(std::string_view tagName,
                                          std::string_view id) {
        std::int32_t body = Crafter::DomBindings::GetBody();
        std::int32_t handle = Crafter::DomBindings::CreateElement(
            body,
            tagName.data(), static_cast<std::int32_t>(tagName.size()),
            id.data(),      static_cast<std::int32_t>(id.size()));
        Crafter::DomBindings::FreeJs(body);
        return HtmlElement(HtmlElementPtr::FromHandle{handle});
    }
}

// Sibling-TU helpers exposed for Router.cpp. Keep the popStateT table
// private to this TU and let Router go through these.
namespace Crafter::DomBindings {
    std::int32_t PopStateRegister(std::function<void()> cb) {
        std::int32_t id = popStateT.maxId++;
        popStateT.map.insert({id, std::move(cb)});
        return id;
    }
    void PopStateUnregister(std::int32_t id) {
        popStateT.map.erase(id);
    }
}
