import { restorePhantomSpaces } from "../phantom.js";
import { readPosition } from "../wire-read.js";
import { MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "../op-stream.js";
import { decodeElementProp } from "./element-props.js";
import { NAME_TO_TYPE } from "./generated/node-types.js";
import { ARENA_MAGIC, KIND_HAST, FIELD, HEADER } from "../generated/arena-layout.js";
export const HAST_ROOT = NAME_TO_TYPE.root;
export const HAST_ELEMENT = NAME_TO_TYPE.element;
export const HAST_TEXT = NAME_TO_TYPE.text;
export const HAST_COMMENT = NAME_TO_TYPE.comment;
export const HAST_DOCTYPE = NAME_TO_TYPE.doctype;
export const HAST_RAW = NAME_TO_TYPE.raw;
export const HAST_MDX_JSX_ELEMENT = NAME_TO_TYPE.mdxJsxFlowElement;
export const HAST_MDX_JSX_TEXT_ELEMENT = NAME_TO_TYPE.mdxJsxTextElement;
export const HAST_MDX_FLOW_EXPRESSION = NAME_TO_TYPE.mdxFlowExpression;
export const HAST_MDX_ESM = NAME_TO_TYPE.mdxjsEsm;
export const HAST_MDX_TEXT_EXPRESSION = NAME_TO_TYPE.mdxTextExpression;
export class HastReader {
    #view;
    #header;
    #textDecoder;
    #stringPoolCache = null;
    constructor(buffer) {
        if (buffer instanceof Uint8Array) {
            this.#view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
        }
        else {
            this.#view = new DataView(buffer);
        }
        this.#textDecoder = new TextDecoder("utf-8");
        this.#header = this.#readHeader();
    }
    #readHeader() {
        const v = this.#view;
        const magic = v.getUint32(HEADER.magic, true);
        if (magic !== ARENA_MAGIC) {
            throw new Error(`Invalid HAST buffer: bad magic 0x${magic.toString(16)}`);
        }
        const kind = v.getUint32(HEADER.kind, true);
        if (kind !== KIND_HAST) {
            throw new Error(`HastReader was handed a buffer of kind ${kind} (expected ${KIND_HAST}). ` +
                `MDAST and HAST node types overlap; reading the wrong kind decodes garbage.`);
        }
        return {
            nodeStructSize: v.getUint32(HEADER.node_struct_size, true),
            nodeCount: v.getUint32(HEADER.node_count, true),
            nodesOffset: v.getUint32(HEADER.nodes_offset, true),
            childrenCount: v.getUint32(HEADER.children_count, true),
            childrenOffset: v.getUint32(HEADER.children_offset, true),
            typeDataLen: v.getUint32(HEADER.type_data_len, true),
            typeDataOffset: v.getUint32(HEADER.type_data_offset, true),
            stringPoolLen: v.getUint32(HEADER.string_pool_len, true),
            stringPoolOffset: v.getUint32(HEADER.string_pool_offset, true),
            nodeDataCount: v.getUint32(HEADER.node_data_count, true),
            nodeDataOffset: v.getUint32(HEADER.node_data_offset, true),
        };
    }
    #nodeDataTable = null;
    /**
     * Per-node JSON `data` blob (set via `Arena::set_node_data` on the Rust
     * side). Returns `null` when the node has no entry. Lazy-builds a
     * `Map<id, string>` on first call so a materialization pass on a
     * data-heavy tree stays O(nodes) rather than O(nodes × entries).
     */
    getNodeData(nodeId) {
        if (this.#header.nodeDataCount === 0)
            return null;
        if (this.#nodeDataTable === null) {
            this.#nodeDataTable = new Map();
            const v = this.#view;
            let pos = this.#header.nodeDataOffset;
            for (let i = 0; i < this.#header.nodeDataCount; i++) {
                const id = v.getUint32(pos, true);
                pos += 4;
                const len = v.getUint32(pos, true);
                pos += 4;
                const slice = new Uint8Array(this.#view.buffer, this.#view.byteOffset + pos, len);
                this.#nodeDataTable.set(id, this.#textDecoder.decode(slice));
                pos += len;
            }
        }
        return this.#nodeDataTable.get(nodeId) ?? null;
    }
    get nodeCount() {
        return this.#header.nodeCount;
    }
    get header() {
        return { ...this.#header };
    }
    /** The full string pool (original input + interning heap). Not the document
     * source as written; for that, read `ctx.source` from a plugin. */
    getStringPool() {
        if (this.#stringPoolCache === null) {
            const { stringPoolOffset, stringPoolLen } = this.#header;
            const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset, stringPoolLen);
            this.#stringPoolCache = this.#textDecoder.decode(bytes);
        }
        return this.#stringPoolCache;
    }
    /** Read a substring from the string pool by byte offset and length. */
    getString(offset, len) {
        if (len === 0)
            return "";
        const { stringPoolOffset } = this.#header;
        const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset + offset, len);
        return this.#textDecoder.decode(bytes);
    }
    /** Get position data for a node. */
    getPosition(nodeId) {
        const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
        return readPosition(this.#view, base + FIELD.start_offset);
    }
    /** Whether the node carries a source position (line 0 + offset 0 is the
     *  synthesized-node sentinel), without decoding the three position objects. */
    hasPosition(nodeId) {
        const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
        const v = this.#view;
        return (v.getUint32(base + FIELD.start_line, true) !== 0 ||
            v.getUint32(base + FIELD.start_offset, true) !== 0);
    }
    /** Get the node_type byte for a given node ID. */
    getNodeType(nodeId) {
        const { nodesOffset, nodeStructSize } = this.#header;
        return this.#view.getUint8(nodesOffset + nodeId * nodeStructSize + FIELD.node_type);
    }
    /** Get the parent id for a given node (0xffffffff at the root). */
    getParentId(nodeId) {
        const { nodesOffset, nodeStructSize } = this.#header;
        return this.#view.getUint32(nodesOffset + nodeId * nodeStructSize + FIELD.parent, true);
    }
    /** Get child node IDs for a given node. */
    getChildIds(nodeId) {
        const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
        const v = this.#view;
        const childrenStart = v.getUint32(base + FIELD.children_start, true);
        const childrenCount = v.getUint32(base + FIELD.children_count, true);
        if (childrenCount === 0)
            return [];
        const { childrenOffset } = this.#header;
        const ids = [];
        for (let i = 0; i < childrenCount; i++) {
            ids.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true));
        }
        return ids;
    }
    /** Push child node IDs directly onto a stack array (reverse order for depth-first). */
    pushChildIds(nodeId, stack) {
        const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
        const v = this.#view;
        const childrenStart = v.getUint32(base + FIELD.children_start, true);
        const childrenCount = v.getUint32(base + FIELD.children_count, true);
        if (childrenCount === 0)
            return;
        const { childrenOffset } = this.#header;
        for (let i = childrenCount - 1; i >= 0; i--) {
            stack.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true));
        }
    }
    /** Get the raw type_data bytes for a node. */
    getTypeData(nodeId) {
        const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
        const v = this.#view;
        const dataOffset = v.getUint32(base + FIELD.data_offset, true);
        const dataLen = v.getUint32(base + FIELD.data_len, true);
        if (dataLen === 0)
            return new Uint8Array(0);
        return new Uint8Array(this.#view.buffer, this.#view.byteOffset + this.#header.typeDataOffset + dataOffset, dataLen);
    }
    /** Read a StringRef (offset: u32 LE, len: u32 LE) from a byte array at byteOffset. */
    #readStringRef(data, byteOffset) {
        const view = new DataView(data.buffer, data.byteOffset + byteOffset);
        return {
            offset: view.getUint32(0, true),
            len: view.getUint32(4, true),
        };
    }
    /**
     * Get element data for a HAST_ELEMENT node.
     *
     * Element type_data layout:
     *   [tag_name: StringRef(8B)][prop_count: u32(4B)][_pad: u32(4B)] = 16-byte header
     *   then prop_count * 20 bytes:
     *     [name: StringRef(8B)][value_type: u8(1B)][_pad: [u8;3](3B)][value: StringRef(8B)]
     */
    getElementData(nodeId) {
        const data = this.getTypeData(nodeId);
        if (data.length < 16) {
            return { tagName: "", properties: [] };
        }
        const tagRef = this.#readStringRef(data, 0);
        const tagName = this.getString(tagRef.offset, tagRef.len);
        const view = new DataView(data.buffer, data.byteOffset + 8);
        const propCount = view.getUint32(0, true);
        const properties = [];
        for (let i = 0; i < propCount; i++) {
            const base = 16 + i * 20;
            const nameRef = this.#readStringRef(data, base);
            const name = this.getString(nameRef.offset, nameRef.len);
            const valueType = data[base + 8];
            const valueRef = this.#readStringRef(data, base + 12);
            const valueStr = this.getString(valueRef.offset, valueRef.len);
            properties.push({ name, value: decodeElementProp(valueType, valueStr) });
        }
        return { tagName, properties };
    }
    /**
     * Get MDX JSX element data: name and attributes.
     *
     * MDX JSX element type_data layout:
     *   [name: StringRef(8B)][attr_count: u32(4B)][_pad: u32(4B)] = 16-byte header
     *   then attr_count * 20 bytes:
     *     [kind: u8(1B)][_pad: [u8;3](3B)][name: StringRef(8B)][value: StringRef(8B)]
     */
    getMdxJsxElementData(nodeId) {
        const data = this.getTypeData(nodeId);
        if (data.length < 16) {
            return { name: null, attributes: [] };
        }
        const nameRef = this.#readStringRef(data, 0);
        const name = nameRef.len > 0 ? this.getString(nameRef.offset, nameRef.len) : null;
        const view = new DataView(data.buffer, data.byteOffset + 8);
        const attrCount = view.getUint32(0, true);
        const attributes = [];
        for (let i = 0; i < attrCount; i++) {
            const base = 16 + i * 20;
            const kind = data[base];
            const attrNameRef = this.#readStringRef(data, base + 4);
            const attrValueRef = this.#readStringRef(data, base + 12);
            switch (kind) {
                case MDX_ATTR_BOOLEAN_PROP:
                    attributes.push({
                        type: "mdxJsxAttribute",
                        name: this.getString(attrNameRef.offset, attrNameRef.len),
                        value: null,
                    });
                    break;
                case MDX_ATTR_LITERAL_PROP:
                    attributes.push({
                        type: "mdxJsxAttribute",
                        name: this.getString(attrNameRef.offset, attrNameRef.len),
                        value: this.getString(attrValueRef.offset, attrValueRef.len),
                    });
                    break;
                case MDX_ATTR_EXPRESSION_PROP:
                    attributes.push({
                        type: "mdxJsxAttribute",
                        name: this.getString(attrNameRef.offset, attrNameRef.len),
                        value: {
                            type: "mdxJsxAttributeValueExpression",
                            value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
                        },
                    });
                    break;
                case MDX_ATTR_SPREAD:
                    attributes.push({
                        type: "mdxJsxExpressionAttribute",
                        value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
                    });
                    break;
            }
        }
        return { name, attributes };
    }
    /**
     * Get the string value for HAST_TEXT, HAST_COMMENT, or HAST_RAW nodes.
     * These store a single StringRef (8 bytes) as their type_data.
     */
    getTextValue(nodeId) {
        const data = this.getTypeData(nodeId);
        if (data.length < 8)
            return "";
        const ref = this.#readStringRef(data, 0);
        return this.getString(ref.offset, ref.len);
    }
}
