import { materializeHastNode } from "./hast-materializer.js";
import { HastReader, HAST_ELEMENT, HAST_TEXT, HAST_COMMENT, HAST_RAW, HAST_MDX_JSX_ELEMENT, HAST_MDX_JSX_TEXT_ELEMENT, HAST_MDX_FLOW_EXPRESSION, HAST_MDX_TEXT_EXPRESSION, HAST_MDX_ESM, } from "./hast-reader.js";
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, HAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
import { CommandBuffer } from "../command-buffer.js";
import { OpWriter, OF_VALUE, OF_TAGNAME, OF_NAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, emitMdxAttr, } from "../op-stream.js";
import { restorePhantomSpaces } from "../phantom.js";
import { decodeMdxJsxAttr } from "../mdx-attr.js";
import { decodeElementProp } from "./element-props.js";
import { readPosition, rstr } from "../wire-read.js";
import { walkHandle, applyCommandsToHandle, textContentHandle, parseExpression as napiParseExpression, parseEsm as napiParseEsm, } from "#binding";
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
import { LazyChildResolver, markHandleMutated } from "../lazy-child-resolver.js";
import { HastChildStub } from "./child-stub.js";
/** Maps HastNode objects to their arena node IDs without Object.defineProperty overhead. */
const nodeIdMap = new WeakMap();
/** Attach `parseExpression()` to an MDX expression or ESM node. */
function attachParseExpression(node, parseFn) {
    Object.defineProperty(node, "parseExpression", {
        value() {
            const value = this.value;
            if (typeof value !== "string")
                return null;
            const json = parseFn(value);
            if (json == null)
                return null;
            return JSON.parse(json);
        },
        writable: false,
        enumerable: false,
        configurable: true,
    });
}
/**
 * Arena identity of a node, rejecting impostors — the one place the
 * spread/identity invariant is enforced. A spread copy of a matched node or
 * stub must read as NEW content: trusting a copied id would splice the
 * original in as a ref and drop the copy's edits. Walk elements carry their
 * id in a private field behind `instanceof` (spread copies fail the check);
 * other walk-built nodes are keyed in the WeakMap (invisible to spread);
 * `HastChildStub`s (enumerable `_id`, but that key is ignored on plain
 * objects) are recognized by `instanceof`. Plain objects are trusted only via
 * the WeakMap or a NON-enumerable `_nodeId` (the materializers' convention,
 * which spread cannot copy).
 */
function nid(node) {
    if (node instanceof WalkElement)
        return node._nid;
    if (node instanceof HastChildStub)
        return node._id;
    const id = nodeIdMap.get(node);
    if (id !== undefined)
        return id;
    const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
    return d !== undefined && !d.enumerable ? d.value : undefined;
}
const requireNid = makeRequireNid(nid);
function hastReusedId(node) {
    if (node === null || typeof node !== "object")
        return undefined;
    const id = nid(node);
    return typeof id === "number" ? id : undefined;
}
// Reused across replacements in a pass — see the note on `mdastWriter`.
const hastWriter = new OpWriter();
/** Compile a set-children payload: a root-wrapped child list, the shape
 *  `Patch::SetChildren` splices in. Reused children become refs. */
function compileHastChildrenToOpstream(children) {
    if (!Array.isArray(children))
        return null;
    hastWriter.begin();
    try {
        hastWriter.open(NAME_TO_TYPE.root);
        for (const c of children) {
            if (!emitHastOp(hastWriter, c, false))
                return null;
        }
        hastWriter.close();
        return hastWriter.take();
    }
    finally {
        hastWriter.end();
    }
}
/** Encode `node` as the `op` structural command. HAST content is always a
 *  declarative node (no raw escape hatch), so it compiles to the op-stream or
 *  it's a hard error — the op-stream is the only structural encoding. The
 *  switch stays inline so the buffer calls are monomorphic (computed method
 *  names defeat inline caches on this warm path). */
function emitHastTree(buffer, op, id, node) {
    const ops = compileHastToOpstream(node);
    if (ops === null)
        throw unencodableContentError(node);
    switch (op) {
        case "replace":
            return buffer.replaceOpstream(id, ops);
        case "insertBefore":
            return buffer.insertBeforeOpstream(id, ops);
        case "insertAfter":
            return buffer.insertAfterOpstream(id, ops);
        case "prependChild":
            return buffer.prependChildOpstream(id, ops);
        case "appendChild":
            return buffer.appendChildOpstream(id, ops);
        case "wrapNode":
            return buffer.wrapNodeOpstream(id, ops);
    }
}
/**
 * Compile a declarative HAST replacement tree to the op-stream — the only
 * structural encoding. Reused nodes become `ref`s (transparent passthrough).
 * Returns null when the tree holds a type the replay can't reproduce
 * identically; the caller throws.
 */
function compileHastToOpstream(root) {
    hastWriter.begin();
    try {
        if (!emitHastOp(hastWriter, root, true))
            return null;
        return hastWriter.take();
    }
    finally {
        hastWriter.end();
    }
}
function emitHastOp(w, node, isRoot) {
    if (node === null || typeof node !== "object")
        return false;
    if (!isRoot) {
        const id = hastReusedId(node);
        if (id !== undefined) {
            w.ref(id);
            return true;
        }
    }
    const n = node;
    const type = HAST_OPSTREAM_TYPES[n.type];
    if (type === undefined)
        return false;
    w.open(type);
    if (type === HAST_ELEMENT) {
        w.str(OF_TAGNAME, typeof n.tagName === "string" ? n.tagName : "div");
        const props = n.properties;
        if (props !== null && typeof props === "object") {
            for (const key in props) {
                emitHastProp(w, key, props[key]);
            }
        }
    }
    else if (type === HAST_MDX_JSX_ELEMENT || type === HAST_MDX_JSX_TEXT_ELEMENT) {
        // Name falls back to tagName, matching `encode_hast_js_node_data`.
        const name = typeof n.name === "string" ? n.name : typeof n.tagName === "string" ? n.tagName : "";
        if (name !== "")
            w.str(OF_NAME, name);
        if (Array.isArray(n.attributes)) {
            for (const a of n.attributes)
                emitMdxAttr(w, a);
        }
        if (n.data?._mdxExplicitJsx === true) {
            w.bool(OF_EXPLICIT, true);
        }
    }
    else {
        w.str(OF_VALUE, typeof n.value === "string" ? n.value : "");
    }
    if (n.data != null)
        w.data(n.data);
    const children = n.children;
    if (Array.isArray(children)) {
        for (const c of children)
            if (!emitHastOp(w, c, false))
                return false;
    }
    w.close();
    return true;
}
/** Emit one element property, mirroring `encode_hast_js_node_data` exactly:
 *  bool/string/number/array → kind; null/object → skip. */
function emitHastProp(w, name, value) {
    if (value === true)
        w.prop(name, PROP_BOOL_TRUE, "");
    else if (value === false)
        w.prop(name, PROP_BOOL_FALSE, "");
    else if (typeof value === "string")
        w.prop(name, PROP_STRING, value);
    else if (typeof value === "number")
        w.prop(name, PROP_INT, String(value));
    else if (Array.isArray(value))
        w.prop(name, PROP_SPACE_SEP, value.filter((v) => typeof v === "string").join(" "));
}
class HastVisitorContextImpl {
    #commandBuffer = new CommandBuffer();
    #diagnostics = [];
    /** Track accumulated node state for multiple setProperty calls on the same node. */
    #pendingNodes = new Map();
    #handle;
    #getSource;
    #resolver;
    /** One canonical object per parent id, so visitors can dedupe by identity.
     *  Null until the first `parent()` call; most passes never make one. */
    #parentsById = null;
    fileURL;
    data;
    constructor(handle, getSource, fileURL, resolver, data) {
        this.#handle = handle;
        this.#getSource = getSource;
        this.fileURL = fileURL;
        this.#resolver = resolver;
        this.data = data;
    }
    get source() {
        const value = this.#getSource();
        Object.defineProperty(this, "source", { value, writable: false, enumerable: true });
        return value;
    }
    removeNode(node) {
        this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
    }
    replaceNode(node, newNode) {
        const id = requireNid(node, "replaceNode");
        emitHastTree(this.#commandBuffer, "replace", id, newNode);
        // Track the replacement so a later mdxJsx setProperty can fold into it.
        this.#pendingNodes.set(id, newNode);
    }
    insertBefore(node, newNode) {
        const id = requireNid(node, "insertBefore");
        for (const n of asArray(newNode))
            emitHastTree(this.#commandBuffer, "insertBefore", id, n);
    }
    insertAfter(node, newNode) {
        const id = requireNid(node, "insertAfter");
        for (const n of asArray(newNode))
            emitHastTree(this.#commandBuffer, "insertAfter", id, n);
    }
    wrapNode(node, parentNode) {
        const id = requireNid(node, "wrapNode");
        emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode);
    }
    prependChild(node, childNode) {
        const id = requireNid(node, "prependChild");
        for (const n of asArray(childNode))
            emitHastTree(this.#commandBuffer, "prependChild", id, n);
    }
    appendChild(node, childNode) {
        const id = requireNid(node, "appendChild");
        for (const n of asArray(childNode))
            emitHastTree(this.#commandBuffer, "appendChild", id, n);
    }
    insertChildAt(node, index, childNode) {
        const children = "children" in node ? node.children : [];
        if (index <= 0 || children.length === 0) {
            this.prependChild(node, childNode);
        }
        else if (index >= children.length) {
            this.appendChild(node, childNode);
        }
        else {
            this.insertBefore(children[index], childNode);
        }
    }
    removeChildAt(node, index) {
        const child = "children" in node ? node.children[index] : undefined;
        if (child)
            this.removeNode(child);
    }
    setProperty(node, key, value) {
        const id = requireNid(node, "setProperty");
        if (key === "children") {
            // children is structural: set-children keeps the node and swaps only its
            // child list (reused children keep their id).
            const ops = compileHastChildrenToOpstream(value);
            if (!ops)
                throw unencodableContentError(value);
            this.#commandBuffer.setChildrenOpstream(id, ops);
            return;
        }
        if (key === "data") {
            this.#commandBuffer.setProperty(id, key, value != null ? JSON.stringify(value) : null);
            return;
        }
        if (node.type === "element") {
            this.#commandBuffer.setProperty(id, key, value);
            return;
        }
        if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
            // MDX JSX nodes carry `attributes`, not `properties`. If a replacement is
            // already queued for this node, fold the attribute into it so the change
            // survives the rebuild. This spreads the queued replacement object, not
            // the matched node, so it never forces the matched node's children to
            // materialize.
            const pending = this.#pendingNodes.get(id);
            if (pending !== undefined) {
                const updated = { ...pending };
                const attrs = [...(updated.attributes ?? [])];
                const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
                if (idx !== -1)
                    attrs.splice(idx, 1);
                // Arrays space-join, matching the binary path's PROP_SPACE_SEP encoding
                // (hast convention for list-valued properties like className).
                const attrValue = value === true || value === null || value === undefined
                    ? null
                    : typeof value === "string"
                        ? value
                        : Array.isArray(value)
                            ? value.join(" ")
                            : String(value);
                attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
                updated.attributes = attrs;
                this.replaceNode(node, updated);
                return;
            }
            // Binary attribute upsert in the arena's type_data — no child
            // materialization. Rust maps the value-type to a boolean (true/null) or
            // literal (string/number/false) attribute, mirroring the fold path above.
            this.#commandBuffer.setProperty(id, key, value);
            return;
        }
        // Text-like nodes (text, comment, raw, expressions, esm): Rust handles
        // `value` directly on these types.
        this.#commandBuffer.setProperty(id, key, value);
    }
    textContent(node) {
        return textContentHandle(this.#handle, requireNid(node, "textContent"));
    }
    parent(node) {
        const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
        if (parentId === undefined)
            return undefined;
        const byId = (this.#parentsById ??= new Map());
        let parent = byId.get(parentId);
        if (parent === undefined) {
            parent = this.#resolver.materializeOne(parentId);
            byId.set(parentId, parent);
        }
        return parent;
    }
    indexOf(node) {
        return this.#resolver.indexInParent(requireNid(node, "indexOf"));
    }
    report({ message, node, severity = "error", }) {
        this.#diagnostics.push({ message, nodeId: node ? nid(node) : undefined, severity });
    }
    getCommandBuffer() {
        return this.#commandBuffer;
    }
    getDiagnostics() {
        return this.#diagnostics;
    }
}
/** Node types that use filtered visitors (have tag/component names). */
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
export function resolveSubscriptions(plugin) {
    const subs = [];
    for (const [methodName, nodeType] of Object.entries(METHOD_TO_TYPE)) {
        const value = plugin[methodName];
        if (value === undefined)
            continue;
        if (FILTERED_METHODS.has(methodName)) {
            const items = Array.isArray(value) ? value : [value];
            for (const fv of items) {
                subs.push({
                    nodeType,
                    tagFilter: fv.filter,
                    visitFn: fv.visit,
                });
            }
        }
        else {
            // Bare function, empty filter matches all nodes of this type
            subs.push({ nodeType, tagFilter: [], visitFn: value });
        }
    }
    return subs;
}
/** Visitor method name → node-type tag (method names are the subscribable AST names). */
const METHOD_TO_TYPE = Object.fromEntries([...VISITOR_KEYS].map((name) => [name, NAME_TO_TYPE[name]]));
function decodeProperties(view, buf, pos) {
    const propCount = view.getUint16(pos, true);
    pos += 2;
    const properties = {};
    for (let i = 0; i < propCount; i++) {
        const nameLen = view.getUint16(pos, true);
        pos += 2;
        const name = rstr(buf, pos, nameLen);
        pos += nameLen;
        const kind = buf[pos];
        pos += 1;
        const valLen = view.getUint16(pos, true);
        pos += 2;
        const valStr = rstr(buf, pos, valLen);
        pos += valLen;
        properties[name] = decodeElementProp(kind, valStr);
    }
    return properties;
}
/** Build the child-stub list for a matched node from the wire's `[child_ids]
 *  [child_types]` blocks — no arena snapshot. The seal check still applies:
 *  post-pass ids are stale, and a stub built from them could later splice the
 *  wrong node as a ref. */
function readChildStubs(view, buf, idsPos, typesPos, count, resolver) {
    resolver.assertUnsealed();
    const stubs = new Array(count);
    for (let i = 0; i < count; i++) {
        stubs[i] = new HastChildStub(resolver, view.getUint32(idsPos + i * 4, true), buf[typesPos + i]);
    }
    return stubs;
}
// Shared own-getter descriptors for WalkElement's lazy fields, populated in
// its static block so the getters can read the private wire fields.
let WALK_PROPS_DESC;
let WALK_CHILDREN_DESC;
/**
 * Walk-path element. Spread-correctness requires `properties`/`children` as
 * own enumerable keys (`{ ...node }` copies nothing else), but construction
 * runs per matched element, so everything stays off the expensive paths:
 * wire state in private fields (plain stores, invisible to spread — a WeakMap
 * entry per element caused major-GC ephemeron stalls at this volume), shared
 * getter functions instead of per-node closures, at most one define per lazy
 * field, and `instanceof` gating identity so copies read as new content.
 */
class WalkElement {
    type = "element";
    tagName;
    #nodeId;
    #view;
    #buf;
    #propsPos;
    #childIdsPos;
    #childTypesPos;
    #childCount;
    #resolver;
    constructor(tagName, nodeId, view, buf, propsPos, propCount, childIdsPos, childTypesPos, childCount, resolver) {
        this.tagName = tagName;
        this.#nodeId = nodeId;
        this.#view = view;
        this.#buf = buf;
        this.#propsPos = propsPos;
        this.#childIdsPos = childIdsPos;
        this.#childTypesPos = childTypesPos;
        this.#childCount = childCount;
        this.#resolver = resolver;
        if (propCount === 0) {
            this.properties = {};
        }
        else {
            Object.defineProperty(this, "properties", WALK_PROPS_DESC);
        }
        if (childCount === 0) {
            this.children = [];
        }
        else {
            Object.defineProperty(this, "children", WALK_CHILDREN_DESC);
        }
    }
    /** @internal */
    get _nid() {
        return this.#nodeId;
    }
    static {
        WALK_PROPS_DESC = {
            enumerable: true,
            configurable: true,
            get() {
                const val = decodeProperties(this.#view, this.#buf, this.#propsPos);
                Object.defineProperty(this, "properties", {
                    value: val,
                    writable: true,
                    enumerable: true,
                    configurable: true,
                });
                return val;
            },
        };
        WALK_CHILDREN_DESC = {
            enumerable: true,
            configurable: true,
            get() {
                const val = readChildStubs(this.#view, this.#buf, this.#childIdsPos, this.#childTypesPos, this.#childCount, this.#resolver);
                Object.defineProperty(this, "children", {
                    value: val,
                    writable: true,
                    enumerable: true,
                    configurable: true,
                });
                return val;
            },
        };
    }
}
/** Read the tail of a matched element node (tag + properties).
 *  Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data) {
    let pos = offset;
    // Eager: tagName (almost always accessed by visitors)
    const tagLen = view.getUint16(pos, true);
    pos += 2;
    const tagName = rstr(buf, pos, tagLen);
    pos += tagLen;
    const propCount = view.getUint16(pos, true);
    const node = new WalkElement(tagName, nodeId, view, buf, pos, propCount, childIdsPos, childTypesPos, childCount, resolver);
    if (position !== undefined)
        node.position = position;
    if (data !== null)
        node.data = data;
    return node;
}
/** Value-carrying types read by `readTextFromBinary` (tag → AST name). */
const TEXT_NODE_TYPES = Object.fromEntries(["text", "comment", "raw", "mdxFlowExpression", "mdxTextExpression", "mdxjsEsm"].map((name) => [NAME_TO_TYPE[name], name]));
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data) {
    const valLen = view.getUint32(offset, true);
    const rawValue = rstr(buf, offset + 4, valLen);
    // MDX flow/text expressions store phantom-space sentinels; restore them so
    // the value matches the reader path. ESM and plain text keep their value.
    const value = nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION
        ? restorePhantomSpaces(rawValue)
        : rawValue;
    const base = { type: TEXT_NODE_TYPES[nodeType], value };
    if (position !== undefined)
        base.position = position;
    if (data !== null)
        base.data = data;
    const node = base;
    nodeIdMap.set(node, nodeId);
    if (nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION) {
        attachParseExpression(node, napiParseExpression);
    }
    else if (nodeType === HAST_MDX_ESM) {
        attachParseExpression(node, napiParseEsm);
    }
    return node;
}
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data) {
    let pos = offset;
    const nameLen = view.getUint16(pos, true);
    pos += 2;
    const name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
    pos += nameLen;
    // Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]
    const attrCount = view.getUint16(pos, true);
    pos += 2;
    const attributes = [];
    for (let i = 0; i < attrCount; i++) {
        const kind = buf[pos];
        pos += 1;
        const attrNameLen = view.getUint16(pos, true);
        pos += 2;
        const attrName = rstr(buf, pos, attrNameLen);
        pos += attrNameLen;
        const attrValLen = view.getUint32(pos, true);
        pos += 4;
        const attrVal = rstr(buf, pos, attrValLen);
        pos += attrValLen;
        attributes.push(decodeMdxJsxAttr(kind, attrName, attrVal));
    }
    const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
    const base = { type: typeName, name, attributes };
    if (position !== undefined)
        base.position = position;
    if (data !== null)
        base.data = data;
    nodeIdMap.set(base, nodeId);
    makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
    return base;
}
function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
    let pos = offset;
    // Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
    //   [data_len: u32][data_bytes][position: 24B][child_count: u32][child_ids: N×u32][child_types: N×u8]
    const dataLen = view.getUint32(pos, true);
    pos += 4;
    let data = null;
    if (dataLen > 0) {
        const jsonStr = rstr(buf, pos, dataLen);
        try {
            data = JSON.parse(jsonStr);
        }
        catch (err) {
            if (process.env.NODE_ENV !== "production") {
                console.warn(`readMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
            }
        }
        pos += dataLen;
    }
    const position = readPosition(view, pos);
    pos += 24;
    const childCount = view.getUint32(pos, true);
    pos += 4;
    // Ids/types decode lazily with `.children` — most matched nodes never read them.
    const childIdsPos = pos;
    pos += childCount * 4;
    const childTypesPos = pos;
    pos += childCount;
    // Dispatch to type-specific tail (pos now sits at the type-specific section)
    if (nodeType === HAST_ELEMENT) {
        return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data);
    }
    else if (nodeType === HAST_TEXT ||
        nodeType === HAST_COMMENT ||
        nodeType === HAST_RAW ||
        nodeType === HAST_MDX_FLOW_EXPRESSION ||
        nodeType === HAST_MDX_TEXT_EXPRESSION ||
        nodeType === HAST_MDX_ESM) {
        return readTextFromBinary(view, buf, pos, nodeId, nodeType, position, data);
    }
    else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
        return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data);
    }
    // Fallback (e.g. doctype): minimal node carrying whatever prelude data we found
    const base = { type: TYPE_NAMES[nodeType] ?? `unknown(${nodeType})` };
    if (position !== undefined)
        base.position = position;
    if (data !== null)
        base.data = data;
    const node = base;
    nodeIdMap.set(node, nodeId);
    return node;
}
class HastLazyChildResolver extends LazyChildResolver {
    createReader(wire) {
        return new HastReader(wire);
    }
    materializeNode(reader, nodeId) {
        return materializeHastNode(reader, nodeId);
    }
    readParentId(reader, nodeId) {
        return reader.getParentId(nodeId);
    }
    readChildIds(reader, nodeId) {
        return reader.getChildIds(nodeId);
    }
}
/** Install `children` as an own enumerable getter (spread must carry it),
 *  self-replacing with the one stable stub array on first read. One closure
 *  and one define per node — installing the wire locals as hidden slots
 *  instead measurably regressed every matching pipeline. */
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
    Object.defineProperty(node, "children", {
        get() {
            const val = readChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
            Object.defineProperty(this, "children", {
                value: val,
                writable: true,
                enumerable: true,
                configurable: true,
            });
            return val;
        },
        enumerable: true,
        configurable: true,
    });
}
/** A result that is the same object as the input node is a no-op, so context
 *  mutations (e.g. setProperty) are not clobbered. */
function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode) {
    if (result == null)
        return deferred;
    if (result === originalNode)
        return deferred;
    if (result instanceof Promise) {
        const list = deferred ?? [];
        list.push({ nodeId, promise: result, originalNode });
        return list;
    }
    emitHastTree(returnBuffer, "replace", nodeId, result);
    return deferred;
}
/**
 * Dispatch matched nodes from a binary match buffer to visitor functions.
 * Returns null if all sync, or an array of deferred promises if any visitor was async.
 */
function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
    const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
    const matchCount = matchView.getUint32(0, true);
    let deferred = null;
    for (let i = 0; i < matchCount; i++) {
        const indexBase = 4 + i * 10;
        const nodeId = matchView.getUint32(indexBase, true);
        const subIndex = matchBuf[indexBase + 4];
        const dataOffset = matchView.getUint32(indexBase + 6, true);
        const sub = subs[subIndex];
        const node = readMatchedNode(matchView, matchBuf, dataOffset, nodeId, sub.nodeType, resolver);
        const result = sub.visitFn(node, ctx);
        deferred = handleVisitResult(result, nodeId, returnBuffer, deferred, node);
    }
    return deferred;
}
/**
 * Walk a handle's arena in Rust, dispatch matched nodes to JS visitor functions,
 * and apply mutations back to the handle. No arena buffers cross NAPI.
 *
 * Returns the number of patches dropped because their target was removed or
 * replaced earlier in the same pass (the caller warns when non-zero), or a
 * Promise of that count if any visitor is async.
 */
export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}) {
    const getSource = typeof source === "function" ? source : () => source;
    const resolver = new HastLazyChildResolver(handle);
    const ctx = new HastVisitorContextImpl(handle, getSource, fileURL, resolver, data);
    const returnBuffer = new CommandBuffer();
    const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
    const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);
    if (deferred) {
        return Promise.all(deferred.map((d) => d.promise.then((result) => ({ nodeId: d.nodeId, result, originalNode: d.originalNode })))).then((results) => {
            for (const { nodeId, result, originalNode } of results) {
                if (result != null && result !== originalNode) {
                    emitHastTree(returnBuffer, "replace", nodeId, result);
                }
            }
            // Mutations land next, renumbering the arena: snapshots taken after
            // this point would resolve match-time child ids against wrong nodes.
            resolver.seal();
            return applyMutations(handle, returnBuffer, ctx);
        });
    }
    resolver.seal();
    return applyMutations(handle, returnBuffer, ctx);
}
/** Returns the number of patches dropped as stranded (0 when none). */
function applyMutations(handle, returnBuffer, ctx) {
    const { merged, hasMutations } = mergeAndReset(returnBuffer, ctx);
    if (hasMutations) {
        markHandleMutated(handle);
        return applyCommandsToHandle(handle, merged);
    }
    return 0;
}
