/**
 * @typedef {import('./WorkspaceNavigator.js').WorkspaceNavigator} WorkspaceNavigator
 * @typedef {import('./NodeRenderer.js').NodeRenderer} NodeRenderer
 */

/**
 * Handles pointer-driven drag movement and sway animation for world-space items.
 *
 * Ported directly from Picograph's WorkspaceDragManager + WorkspaceGeometry:
 * - Anchor-ratio drag (cursor grabs exactly where it hits the element)
 * - Ghost placeholder for nodes while dragging
 * - movementX-preferred velocity → rotation blend (±7° clamp, grab-point pivot)
 * - 0.85×/frame rotation decay RAF loop
 * - RAF-coalesced connection refresh during drag, immediate flush on drop
 * - Grid snapping on ghost preview and drop (dragging remains smooth)
 */
export class ItemDragger {
    /** Grid size for snapping (matches CSS --grid-size) */
    static GRID_SIZE = 20;
    /** @type {HTMLElement} */
    #canvas;

    /** @type {WorkspaceNavigator} */
    #nav;

    /** @type {NodeRenderer} */
    #nodeRenderer;

    /** @type {boolean} */
    #enabled = false;

    /** @type {{ id: string, el: HTMLElement, pos: { x: number, y: number } }[]} */
    #images = [];

    /**
     * Per-item apply-transform callbacks.
     *
     * @type {Map<string, (x: number, y: number, rotation: number) => void>}
     */
    #applyTransform = new Map();

    /** @type {Map<string, { x: number, y: number }>} */
    #itemPositions = new Map();

    // ─── Rotation (matches workspace.nodeDragRotation in Picograph) ────────────

    /** @type {Map<string, number>} */
    #dragRotation = new Map();

    /**
     * Stores the grab-ratio pivot point per item, used to set transform-origin during rotation.
     *
     * @type {Map<string, { ratioX: number, ratioY: number }>}
     */
    #dragOrigin = new Map();

    /** @type {number | null} */
    #decayFrame = null;

    // ─── Ghost (node drag placeholder) ────────────────────────────────────────

    /** @type {{ el: HTMLElement, nodeId: string, width: number, height: number } | null} */
    #ghost = null;

    // ─── Connection refresh coalescing ─────────────────────────────────────────

    /** @type {number | null} */
    #connRefreshFrame = null;

    // ─── Edge-pan (auto-scroll while dragging near canvas edges) ──────────────

    /**
     * @type {{ clientX: number, clientY: number,
     *          anchors: Map<string, {ratioX: number, ratioY: number, width: number, height: number}>,
     *          primaryId: string, useGhost: boolean } | null}
     */
    #edgePanState = null;

    /** @type {number | null} */
    #edgePanFrame = null;

    /** @type {boolean} */
    #dragging = false;

    /** @type {(() => void) | null} */
    #cancelActiveDrag = null;

    // ─── Active item (debug panel) ─────────────────────────────────────────────

    /** @type {string | null} */
    #activeId = null;

    /** @type {{ x: number, y: number } | null} */
    #activePos = null;

    /** @type {((id: string | null, pos: { x: number, y: number } | null) => void) | null} */
    #onActiveItemChange = null;

    /** @type {(() => void) | null} */
    #onItemMoved = null;

    /**
     * @param {HTMLElement} canvas - The workspace canvas element.
     * @param {WorkspaceNavigator} nav
     * @param {NodeRenderer} nodeRenderer
     * @param {((id: string | null, pos: { x: number, y: number } | null) => void) | null} [onActiveItemChange]
     * @param {(() => void) | null} [onItemMoved]
     */
    constructor(canvas, nav, nodeRenderer, onActiveItemChange = null, onItemMoved = null) {
        this.#canvas             = canvas;
        this.#nav                = nav;
        this.#nodeRenderer       = nodeRenderer;
        this.#onActiveItemChange = onActiveItemChange;
        this.#onItemMoved        = onItemMoved;
    }

    // ─── Public ───────────────────────────────────────────────────────────────

    /** @returns {string | null} */
    get activeId() { return this.#activeId; }

    /** @returns {{ x: number, y: number } | null} */
    get activePos() { return this.#activePos; }

    /** True while a drag gesture is actively in progress. */
    get isDragging() { return this.#dragging; }

    /**
     * Immediately drops any active drag, snapping the item to grid at its current position.
     * Call this when a pinch-to-zoom starts so the two gestures don't fight each other.
     */
    cancelDrag() {
        this.#cancelActiveDrag?.();
    }

    /** @param {boolean} v */
    set enabled(v) {
        this.#enabled = v;
        this.#images.forEach(({ el }) => {
            el.style.pointerEvents = v ? "auto" : "none";
            el.style.cursor = v ? "grab" : "";
        });
        this.#nodeRenderer.getNodeElements().forEach((el) => {
            el.dataset.draggable = v ? "true" : "false";
        });
        if (!v) {
            this.#removeGhost();
            this.#stopDecay();
        }
    }

    /** @returns {boolean} */
    get enabled() { return this.#enabled; }

    /**
     * Registers an image element as draggable.
     *
     * @param {string} id
     * @param {HTMLElement} el
     * @param {{ x: number, y: number }} initialPos
     */
    registerImage(id, el, initialPos) {
        const entry = { id, el, pos: { ...initialPos } };
        this.#images.push(entry);
        this.#itemPositions.set(id, { ...initialPos });
        this.#applyTransform.set(id, (x, y, rot) => {
            entry.pos.x = x;
            entry.pos.y = y;
            el.style.transform = this.#buildTransform(x, y, rot);
        });

        el.addEventListener("pointerdown", (e) => {
            if (!this.#enabled) return;
            e.stopPropagation();
            const pointerWorld = this.#clientToWorkspace(e.clientX, e.clientY);
            const zoom   = this.#nav.zoomLevel || 1;
            const rect   = el.getBoundingClientRect();
            const width  = rect.width  / zoom || 512;
            const height = rect.height / zoom || 512;
            const ratioX = width  > 0 ? (pointerWorld.x - entry.pos.x) / width  : 0;
            const ratioY = height > 0 ? (pointerWorld.y - entry.pos.y) / height : 0;
            this.#beginDrag(id, e, el, new Map([[id, { ratioX, ratioY, width, height }]]), false);
        });
    }

    /**
     * Registers a node element as draggable. Call after the node has been added to NodeRenderer.
     *
     * @param {string} nodeId
     */
    registerNode(nodeId) {
        const el   = this.#nodeRenderer.getNodeElements().get(nodeId);
        const node = this.#nodeRenderer.getNodes().find(n => n.id === nodeId);
        if (!el || !node) return;

        this.#itemPositions.set(nodeId, { ...node.position });
        this.#applyTransform.set(nodeId, (x, y, rot) => {
            this.#nodeRenderer.setNodeTransform(nodeId, x, y, rot);
        });

        el.addEventListener("pointerdown", (e) => {
            if (!this.#enabled) return;
            const target = /** @type {HTMLElement} */ (e.target);
            if (target.closest(".pin-handle")) return;
            
            // Allow text selection in blog/info node bodies
            const nodeBody = target.closest(".node-body");
            if (nodeBody && (node.type === 'info' || node.type === 'blog_post')) {
                return; // Don't drag, allow text selection
            }
            
            e.stopPropagation();

            const pointerWorld = this.#clientToWorkspace(e.clientX, e.clientY);
            const zoom   = this.#nav.zoomLevel || 1;
            const rect   = el.getBoundingClientRect();
            const width  = Math.max(1, rect.width  / zoom);
            const height = Math.max(1, rect.height / zoom);
            const ratioX = width  > 0 ? (pointerWorld.x - node.position.x) / width  : 0;
            const ratioY = height > 0 ? (pointerWorld.y - node.position.y) / height : 0;
            this.#beginDrag(nodeId, e, el, new Map([[nodeId, { ratioX, ratioY, width, height }]]), true);
        });
    }

    // ─── Drag ─────────────────────────────────────────────────────────────────

    /**
     * @param {string} primaryId
     * @param {PointerEvent} event
     * @param {HTMLElement} primaryEl
     * @param {Map<string, { ratioX: number, ratioY: number, width: number, height: number }>} anchors
     * @param {boolean} useGhost
     */
    #beginDrag(primaryId, event, primaryEl, anchors, useGhost) {
        // Don't start a drag while a pinch-to-zoom is already running.
        // This can happen when both fingers land simultaneously: touchstart (pinch)
        // fires before pointerdown, so the guard here catches the race.
        if (this.#nav.isPinching) return;
        anchors.forEach((_, id) => this.#dragRotation.delete(id));

        // Stop any previous edge-pan and mark drag as active
        this.#stopEdgePan();
        this.#dragging = true;

        // Store grab-point ratios and apply them as transform-origin for pivot rotation
        anchors.forEach((anchor, id) => {
            this.#dragOrigin.set(id, { ratioX: anchor.ratioX, ratioY: anchor.ratioY });
            const el = this.#getItemElement(id);
            if (el) el.style.transformOrigin = `${anchor.ratioX * 100}% ${anchor.ratioY * 100}%`;
        });

        primaryEl.style.transition = "";

        let prevClientX = event.clientX;
        let prevClientY = event.clientY;
        let prevMoveTime = performance.now();

        // Ghost placeholder (nodes only)
        if (useGhost) {
            const anchor = anchors.get(primaryId);
            if (anchor) {
                const initialPointer = this.#clientToWorkspace(event.clientX, event.clientY);
                const ghostPos = {
                    x: initialPointer.x - anchor.ratioX * anchor.width,
                    y: initialPointer.y - anchor.ratioY * anchor.height,
                };
                this.#ensureGhost(primaryId, primaryEl, anchor.width, anchor.height, ghostPos);
            }
        }

        this.#setActive(primaryId, this.#itemPositions.get(primaryId) ?? null);

        // Initialise edge-pan state so #tickEdgePan can reference anchors without closure mutation
        this.#edgePanState = { clientX: event.clientX, clientY: event.clientY, anchors, primaryId, useGhost };

        const handlePointerMove = (/** @type {PointerEvent} */ moveE) => {
            if (moveE.pointerId !== event.pointerId) return;

            const pointer = this.#clientToWorkspace(moveE.clientX, moveE.clientY);

            anchors.forEach((anchor, id) => {
                const newX = pointer.x - anchor.ratioX * anchor.width;
                const newY = pointer.y - anchor.ratioY * anchor.height;
                this.#itemPositions.set(id, { x: newX, y: newY });
                const rot = this.#dragRotation.get(id) ?? 0;
                this.#applyTransform.get(id)?.(newX, newY, rot);

                if (useGhost && id === primaryId) {
                    this.#updateGhost(newX, newY);
                }
            });

            this.#setActive(primaryId, this.#itemPositions.get(primaryId) ?? null);

            // Update edge-pan pointer so the RAF loop knows where the pointer is
            this.#edgePanState.clientX = moveE.clientX;
            this.#edgePanState.clientY = moveE.clientY;
            if (this.#edgePanFrame === null) {
                this.#edgePanFrame = requestAnimationFrame(() => this.#tickEdgePan());
            }

            // Compute time-normalised velocity so touch (coalesced, less frequent events) and
            // mouse (per-frame events) produce the same rotation magnitude at the same finger/cursor speed.
            // Target frame budget: 16 ms. Clamp dt to 8–60 ms to avoid spikes on tab-switch etc.
            const now = performance.now();
            const dt = Math.max(8, Math.min(60, now - prevMoveTime));
            prevMoveTime = now;
            const rawDelta = moveE.clientX - prevClientX;
            const velocityX = (rawDelta / dt) * 16;
            anchors.forEach((_, id) => this.#nudgeRotation(id, velocityX));

            prevClientX = moveE.clientX;
            prevClientY = moveE.clientY;

            this.#scheduleConnectionRefresh();
        };

        const handlePointerFinish = (/** @type {PointerEvent} */ finishE) => {
            if (finishE.pointerId !== event.pointerId) return;
            window.removeEventListener("pointermove",   handlePointerMove);
            window.removeEventListener("pointerup",     handlePointerFinish);
            window.removeEventListener("pointercancel", handlePointerFinish);

            this.#cancelActiveDrag = null;
            this.#stopEdgePan();
            this.#dragging = false;

            // Commit position from final pointer coords, snapped to grid
            if (Number.isFinite(finishE.clientX)) {
                const pointer = this.#clientToWorkspace(finishE.clientX, finishE.clientY);
                anchors.forEach((anchor, id) => {
                    const rawX = pointer.x - anchor.ratioX * anchor.width;
                    const rawY = pointer.y - anchor.ratioY * anchor.height;
                    const snapped = this.#snapToGrid(rawX, rawY);
                    this.#itemPositions.set(id, { x: snapped.x, y: snapped.y });
                });
            }

            // Zero out rotations with snap-back transition, then decay
            anchors.forEach((_, id) => this.#setDragRotation(id, 0));

            if (useGhost) {
                const pos = this.#itemPositions.get(primaryId);
                if (pos) this.#updateGhost(pos.x, pos.y);
                window.setTimeout(() => this.#removeGhost(), 160);
            }

            // Keep selection active after drag ends
            this.#flushConnectionRefresh();
        };

        // Exposed so external callers (e.g. pinch-to-zoom start) can drop the drag cleanly
        this.#cancelActiveDrag = () => {
            window.removeEventListener("pointermove",   handlePointerMove);
            window.removeEventListener("pointerup",     handlePointerFinish);
            window.removeEventListener("pointercancel", handlePointerFinish);

            this.#cancelActiveDrag = null;
            this.#stopEdgePan();
            this.#dragging = false;

            // Snap current live positions to grid
            anchors.forEach((_, id) => {
                const pos = this.#itemPositions.get(id);
                if (!pos) return;
                const snapped = this.#snapToGrid(pos.x, pos.y);
                this.#itemPositions.set(id, snapped);
                const rot = this.#dragRotation.get(id) ?? 0;
                this.#applyTransform.get(id)?.(snapped.x, snapped.y, rot);
            });

            anchors.forEach((_, id) => this.#setDragRotation(id, 0));

            if (useGhost) {
                const pos = this.#itemPositions.get(primaryId);
                if (pos) this.#updateGhost(pos.x, pos.y);
                window.setTimeout(() => this.#removeGhost(), 160);
            }

            this.#flushConnectionRefresh();
        };

        window.addEventListener("pointermove",   handlePointerMove);
        window.addEventListener("pointerup",     handlePointerFinish);
        window.addEventListener("pointercancel", handlePointerFinish);
    }

    // ─── Ghost ────────────────────────────────────────────────────────────────

    /**
     * @param {string} nodeId
     * @param {HTMLElement} nodeEl
     * @param {number} width
     * @param {number} height
     * @param {{ x: number, y: number }} pos
     */
    #ensureGhost(nodeId, nodeEl, width, height, pos) {
        this.#removeGhost();
        const el = document.createElement("div");
        el.className = "node-drag-ghost";
        el.style.width  = `${width}px`;
        el.style.height = `${height}px`;
        el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
        el.setAttribute("role", "presentation");
        if (nodeEl.parentElement) {
            nodeEl.parentElement.insertBefore(el, nodeEl);
        } else {
            nodeEl.appendChild(el);
        }
        this.#ghost = { el, nodeId, width, height };
    }

    /**
     * @param {number} x
     * @param {number} y
     */
    #updateGhost(x, y) {
        if (!this.#ghost) return;
        const snapped = this.#snapToGrid(x, y);
        this.#ghost.el.style.transform = `translate3d(${snapped.x}px, ${snapped.y}px, 0)`;
    }

    #removeGhost() {
        if (!this.#ghost) return;
        if (this.#ghost.el.isConnected) this.#ghost.el.remove();
        this.#ghost = null;
    }

    // ─── Edge-pan (auto-scroll while dragging near canvas edges) ─────────────

    /**
     * Stops the edge-pan RAF loop and clears its state.
     */
    #stopEdgePan() {
        if (this.#edgePanFrame !== null) {
            cancelAnimationFrame(this.#edgePanFrame);
            this.#edgePanFrame = null;
        }
        this.#edgePanState = null;
    }

    /**
     * RAF loop that scrolls the viewport when the drag pointer is within 10% of any canvas edge,
     * then re-positions the dragged item so it stays under the pointer.
     * Runs continuously while the pointer stays in the edge zone; stops when it leaves.
     */
    #tickEdgePan() {
        this.#edgePanFrame = null;
        const state = this.#edgePanState;
        if (!state) return;

        const rect = this.#canvas.getBoundingClientRect();
        const { clientX, clientY, anchors, primaryId, useGhost } = state;

        const edgeW = rect.width  * 0.10;
        const edgeH = rect.height * 0.10;
        /** Max pan speed in screen pixels per frame */
        const MAX_SPEED = 14;

        const cx = clientX - rect.left;
        const cy = clientY - rect.top;

        let screenDx = 0;
        let screenDy = 0;

        if (cx < edgeW)                  screenDx =  MAX_SPEED * (1 - cx / edgeW);
        if (cx > rect.width  - edgeW)    screenDx = -MAX_SPEED * (1 - (rect.width  - cx) / edgeW);
        if (cy < edgeH)                  screenDy =  MAX_SPEED * (1 - cy / edgeH);
        if (cy > rect.height - edgeH)    screenDy = -MAX_SPEED * (1 - (rect.height - cy) / edgeH);

        if (screenDx !== 0 || screenDy !== 0) {
            const zoom    = this.#nav.zoomLevel || 1;
            this.#nav.panBy(screenDx / zoom, screenDy / zoom);

            // Re-derive world position from the (now-updated) viewport so node follows pointer
            const pointer = this.#clientToWorkspace(clientX, clientY);
            anchors.forEach((anchor, id) => {
                const newX = pointer.x - anchor.ratioX * anchor.width;
                const newY = pointer.y - anchor.ratioY * anchor.height;
                this.#itemPositions.set(id, { x: newX, y: newY });
                const rot = this.#dragRotation.get(id) ?? 0;
                this.#applyTransform.get(id)?.(newX, newY, rot);
                if (useGhost && id === primaryId) {
                    this.#updateGhost(newX, newY);
                }
            });

            this.#scheduleConnectionRefresh();
            this.#edgePanFrame = requestAnimationFrame(() => this.#tickEdgePan());
        }
        // Pointer outside the edge zone — loop pauses; next pointermove will restart it if needed
    }

    // ─── Rotation ─────────────────────────────────────────────────────────────

    /**
     * Blends horizontal velocity into an item's rotation state.
     * Exact port of nudgeNodeDragRotation from WorkspaceDragManager.
     *
     * @param {string} id
     * @param {number} velocityX
     */
    #nudgeRotation(id, velocityX) {
        if (!Number.isFinite(velocityX)) return;
        const existing = this.#dragRotation.get(id) ?? 0;
        const blended  = existing * 0.92 + velocityX * 0.18;
        this.#setDragRotation(id, blended);
    }

    /**
     * Applies or clears a rotation and updates the item's transform.
     * Exact port of setNodeDragRotation from WorkspaceDragManager.
     *
     * @param {string} id
     * @param {number} angle
     */
    #setDragRotation(id, angle) {
        const sanitized = Number.isFinite(angle) ? angle : 0;
        const clamped   = Math.max(-7, Math.min(7, sanitized));
        if (Math.abs(clamped) < 0.01) {
            this.#dragRotation.delete(id);
            this.#dragOrigin.delete(id);
            const el = this.#getItemElement(id);
            if (el) el.style.transformOrigin = "";
        } else {
            this.#dragRotation.set(id, clamped);
        }

        const pos = this.#itemPositions.get(id);
        if (pos) {
            this.#applyTransform.get(id)?.(pos.x, pos.y, clamped);
        }

        if (this.#dragRotation.size > 0) {
            this.#ensureDecay();
        } else {
            this.#stopDecay();
        }
    }

    /**
     * Returns the DOM element for a registered item (image or node).
     *
     * @param {string} id
     * @returns {HTMLElement | null}
     */
    #getItemElement(id) {
        const img = this.#images.find(i => i.id === id);
        if (img) return img.el;
        return this.#nodeRenderer.getNodeElements().get(id) ?? null;
    }

    // ─── Decay ────────────────────────────────────────────────────────────────

    /**
     * Ensures the rotation decay RAF loop is running.
     * Exact port of ensureNodeRotationDecay from WorkspaceDragManager.
     */
    #ensureDecay() {
        if (this.#decayFrame !== null) return;

        const step = () => {
            let hasActive = false;
            this.#dragRotation.forEach((angle, id) => {
                const pos = this.#itemPositions.get(id);
                if (!pos) { this.#dragRotation.delete(id); return; }

                const damped = angle * 0.85;
                if (Math.abs(damped) < 0.05) {
                    this.#dragRotation.delete(id);
                    this.#dragOrigin.delete(id);
                    const el = this.#getItemElement(id);
                    if (el) el.style.transformOrigin = "";
                    this.#applyTransform.get(id)?.(pos.x, pos.y, 0);
                } else {
                    this.#dragRotation.set(id, damped);
                    this.#applyTransform.get(id)?.(pos.x, pos.y, damped);
                    hasActive = true;
                }
            });

            this.#scheduleConnectionRefresh();

            if (hasActive) {
                this.#decayFrame = requestAnimationFrame(step);
            } else {
                this.#decayFrame = null;
                this.#flushConnectionRefresh();
            }
        };

        this.#decayFrame = requestAnimationFrame(step);
    }

    /**
     * Stops the decay loop if running.
     * Exact port of stopNodeRotationDecay from WorkspaceDragManager.
     */
    #stopDecay() {
        if (this.#decayFrame === null) return;
        cancelAnimationFrame(this.#decayFrame);
        this.#decayFrame = null;
    }

    // ─── Connection refresh coalescing ────────────────────────────────────────

    /**
     * Queues a single RAF to refresh connections.
     * Matches scheduleConnectionRefresh in BlueprintWorkspace.
     */
    #scheduleConnectionRefresh() {
        if (this.#connRefreshFrame !== null) return;
        this.#connRefreshFrame = requestAnimationFrame(() => {
            this.#connRefreshFrame = null;
            this.#onItemMoved?.();
        });
    }

    /**
     * Immediately refreshes connections, cancelling any pending RAF.
     * Matches flushConnectionRefresh in BlueprintWorkspace.
     */
    #flushConnectionRefresh() {
        if (this.#connRefreshFrame !== null) {
            cancelAnimationFrame(this.#connRefreshFrame);
            this.#connRefreshFrame = null;
        }
        this.#onItemMoved?.();
    }

    // ─── Helpers ──────────────────────────────────────────────────────────────

    /**
     * Snaps a world-space position to the grid.
     *
     * @param {number} x
     * @param {number} y
     * @returns {{ x: number, y: number }}
     */
    #snapToGrid(x, y) {
        const gridSize = ItemDragger.GRID_SIZE;
        return {
            x: Math.round(x / gridSize) * gridSize,
            y: Math.round(y / gridSize) * gridSize,
        };
    }

    /**
     * Converts client coordinates to world space.
     * Exact port of clientToWorkspace from WorkspaceDragManager.
     *
     * @param {number} clientX
     * @param {number} clientY
     * @returns {{ x: number, y: number }}
     */
    #clientToWorkspace(clientX, clientY) {
        const rect   = this.#canvas.getBoundingClientRect();
        const zoom   = Math.max(0.01, this.#nav.zoomLevel || 1);
        const offset = this.#nav.getEffectiveOffset();
        return {
            x: (clientX - rect.left) / zoom - offset.x,
            y: (clientY - rect.top)  / zoom - offset.y,
        };
    }

    /**
     * Builds a CSS transform matching WorkspaceGeometry.positionToTransform in Picograph.
     *
     * @param {number} x
     * @param {number} y
     * @param {number} rotation
     * @returns {string}
     */
    #buildTransform(x, y, rotation) {
        const rotZ = Number.isFinite(rotation) ? rotation : 0;
        if (!rotZ) return `translate3d(${x}px, ${y}px, 0)`;
        return `translate3d(${x}px, ${y}px, 0) rotate(${rotZ}deg)`;
    }

    /**
     * @param {string | null} id
     * @param {{ x: number, y: number } | null} pos
     */
    #setActive(id, pos) {
        this.#activeId  = id;
        this.#activePos = pos;
        this.#onActiveItemChange?.(id, pos);
    }

    /**
     * Unregisters a node that's being deleted.
     *
     * @param {string} nodeId
     */
    unregisterNode(nodeId) {
        this.#itemPositions.delete(nodeId);
        this.#applyTransform.delete(nodeId);
        this.#dragRotation.delete(nodeId);
        this.#dragOrigin.delete(nodeId);
        
        // Clear active if it's the deleted node
        if (this.#activeId === nodeId) {
            this.clearSelection();
        }
    }

    /**
     * Clear the current selection
     */
    clearSelection() {
        this.#setActive(null, null);
    }

    /**
     * Get the currently active item ID
     * @returns {string | null}
     */
    getActiveId() {
        return this.#activeId;
    }
}

