import { visitHastHandle, resolveSubscriptions } from "./hast/hast-visitor.js";
import { visitMdastHandle, resolveMdastSubscriptions, } from "./mdast/mdast-visitor.js";
import { createHastHandle, createMdxHastHandle, renderHandle, compileHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, getHandleSource, getMdastFrontmatter, serializeHandle, } from "#binding";
import { MdastReader } from "./mdast/mdast-reader.js";
import { materializeMdastTree } from "./mdast/mdast-materializer.js";
import { markHandleMutated } from "./lazy-child-resolver.js";
import { HastReader } from "./hast/hast-reader.js";
import { materializeHastTree } from "./hast/hast-materializer.js";
/**
 * Split the user-facing `Features` (with nested unions) into the flat napi
 * `JsFeatures` shape plus the conversion-side `JsConvertOptions` carrying
 * the footnote i18n strings. The public API only exposes `features`; the
 * footnote strings are routed to napi internally.
 */
function featuresToNative(features) {
    if (!features)
        return { features: undefined, convertOptions: undefined };
    const result = {};
    let convertOptions;
    if (features.gfm !== undefined) {
        if (typeof features.gfm === "object") {
            const g = features.gfm;
            const gfmOpts = {};
            if (g.footnotes !== undefined) {
                if (typeof g.footnotes === "object") {
                    gfmOpts.footnotes = true;
                    convertOptions = convertOptions ?? {};
                    if (g.footnotes.label !== undefined)
                        convertOptions.footnoteLabel = g.footnotes.label;
                    if (g.footnotes.backContent !== undefined)
                        convertOptions.footnoteBackContent = g.footnotes.backContent;
                    if (g.footnotes.backLabel !== undefined)
                        convertOptions.footnoteBackLabel = g.footnotes.backLabel;
                }
                else {
                    gfmOpts.footnotes = g.footnotes;
                }
            }
            result.gfmOptions = gfmOpts;
        }
        else {
            result.gfm = features.gfm;
        }
    }
    if (features.frontmatter !== undefined)
        result.frontmatter = features.frontmatter;
    if (features.math !== undefined) {
        if (typeof features.math === "object") {
            const mathOpts = {};
            if (features.math.singleDollarTextMath !== undefined)
                mathOpts.singleDollarTextMath = features.math.singleDollarTextMath;
            result.mathOptions = mathOpts;
        }
        else {
            result.math = features.math;
        }
    }
    if (features.headingAttributes !== undefined)
        result.headingAttributes = features.headingAttributes;
    if (features.directive !== undefined)
        result.directive = features.directive;
    if (features.superscript !== undefined)
        result.superscript = features.superscript;
    if (features.subscript !== undefined)
        result.subscript = features.subscript;
    if (features.wikilinks !== undefined)
        result.wikilinks = features.wikilinks;
    if (features.smartPunctuation !== undefined) {
        if (typeof features.smartPunctuation === "object") {
            result.smartPunctuationOptions = features.smartPunctuation;
        }
        else {
            result.smartPunctuation = features.smartPunctuation;
        }
    }
    return { features: result, convertOptions };
}
/** Free a handle's arena. A plugin's visitor can hand child stubs to user code;
 *  pass `invalidateStubs` so the epoch bump makes any stub retained past this
 *  point hit the designed retention error instead of snapshotting the freed arena. */
function releaseHandle(handle, invalidateStubs) {
    if (invalidateStubs)
        markHandleMutated(handle);
    dropHandle(handle);
}
function warnDroppedTransforms(plugin, dropped, kind) {
    const name = plugin.name ?? "<anonymous>";
    const noun = dropped === 1 ? "transform" : "transforms";
    console.warn(`satteri: plugin "${name}" queued ${dropped} ${kind} ${noun} on node(s) that were removed or ` +
        `replaced earlier in the same pass; ${dropped === 1 ? "it was" : "they were"} dropped.`);
}
function runMdastPluginsOnHandle(handle, plugins, fileURL, data) {
    // Each plugin runs once over the tree. A transform that passes a child
    // through (returning it inside the replacement) keeps that child's identity,
    // so a patch the same pass queued on it still applies — nesting composes in
    // one pass. A plugin's own freshly-built nodes are not re-walked; transform
    // them up front, or hand off to a later plugin that sees the materialized tree.
    const runPlugin = (plugin) => {
        const subs = resolveMdastSubscriptions(plugin);
        const result = visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data);
        const apply = (r) => {
            if (!r.hasMutations)
                return;
            markHandleMutated(handle);
            const dropped = applyCommandsToMdastHandle(handle, r.commandBuffer);
            if (dropped)
                warnDroppedTransforms(plugin, dropped, "mdast");
        };
        return result instanceof Promise ? result.then(apply) : apply(result);
    };
    let i = 0;
    const runNext = () => {
        while (i < plugins.length) {
            const raw = plugins[i++];
            const plugin = typeof raw === "function" ? raw() : raw;
            const r = runPlugin(plugin);
            if (r instanceof Promise)
                return r.then(runNext);
        }
        return { handle };
    };
    return runNext();
}
function runHastPluginsOnHandle(handle, plugins, source, fileURL, data) {
    if (plugins.length === 0)
        return;
    let i = 0;
    const runNext = () => {
        while (i < plugins.length) {
            const raw = plugins[i];
            i++;
            const plugin = typeof raw === "function" ? raw() : raw;
            const subs = resolveSubscriptions(plugin);
            const result = visitHastHandle(handle, plugin, subs, source, fileURL, data);
            const warnIfDropped = (dropped) => {
                if (dropped)
                    warnDroppedTransforms(plugin, dropped, "hast");
            };
            if (result instanceof Promise) {
                return result.then((dropped) => {
                    warnIfDropped(dropped);
                    return runNext();
                });
            }
            warnIfDropped(result);
        }
    };
    return runNext();
}
// Public API
function mdxOptionsToNative(opts) {
    const hasAny = opts.optimizeStatic ||
        opts.jsxImportSource !== undefined ||
        opts.jsx !== undefined ||
        opts.jsxRuntime !== undefined ||
        opts.development !== undefined ||
        opts.providerImportSource !== undefined ||
        opts.pragma !== undefined ||
        opts.pragmaFrag !== undefined ||
        opts.pragmaImportSource !== undefined ||
        opts.outputFormat !== undefined ||
        opts.elementAttributeNameCase !== undefined ||
        opts.stylePropertyNameCase !== undefined;
    if (!hasAny)
        return undefined;
    const result = {};
    if (opts.optimizeStatic)
        result.optimizeStatic = opts.optimizeStatic;
    if (opts.jsxImportSource !== undefined)
        result.jsxImportSource = opts.jsxImportSource;
    if (opts.jsx !== undefined)
        result.jsx = opts.jsx;
    if (opts.jsxRuntime !== undefined)
        result.jsxRuntime = opts.jsxRuntime;
    if (opts.development !== undefined)
        result.development = opts.development;
    if (opts.providerImportSource !== undefined)
        result.providerImportSource = opts.providerImportSource;
    if (opts.pragma !== undefined)
        result.pragma = opts.pragma;
    if (opts.pragmaFrag !== undefined)
        result.pragmaFrag = opts.pragmaFrag;
    if (opts.pragmaImportSource !== undefined)
        result.pragmaImportSource = opts.pragmaImportSource;
    if (opts.outputFormat !== undefined)
        result.outputFormat = opts.outputFormat;
    if (opts.elementAttributeNameCase !== undefined)
        result.elementAttributeNameCase = opts.elementAttributeNameCase;
    if (opts.stylePropertyNameCase !== undefined)
        result.stylePropertyNameCase = opts.stylePropertyNameCase;
    return result;
}
export function markdownToHtml(source, options = {}) {
    const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {} } = options;
    const hastMayHaveStubs = hastPlugins.length > 0;
    const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
    const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions, data);
    const renderAndDrop = (h, frontmatter) => {
        try {
            const html = renderHandle(h);
            return { html, frontmatter, data };
        }
        finally {
            releaseHandle(h, hastMayHaveStubs);
        }
    };
    const runHastThenRender = (r) => {
        let hastResult;
        try {
            hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
        }
        catch (err) {
            releaseHandle(r.hastHandle, hastMayHaveStubs);
            throw err;
        }
        if (hastResult instanceof Promise) {
            return hastResult.then(() => renderAndDrop(r.hastHandle, r.frontmatter), (err) => {
                releaseHandle(r.hastHandle, hastMayHaveStubs);
                throw err;
            });
        }
        return renderAndDrop(r.hastHandle, r.frontmatter);
    };
    if (result instanceof Promise)
        return result.then(runHastThenRender);
    return runHastThenRender(result);
}
export function mdxToJs(source, options = {}) {
    const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {}, ...mdxFields } = options;
    const hastMayHaveStubs = hastPlugins.length > 0;
    const mdxOptions = mdxOptionsToNative(mdxFields);
    const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
    const result = createHastHandleFromMdast(source, mdastPlugins, true, fileURL, nativeFeatures, nativeConvertOptions, data);
    const compileAndDrop = (h, frontmatter) => {
        try {
            const code = compileHandle(h, mdxOptions);
            return { code, frontmatter, data };
        }
        finally {
            releaseHandle(h, hastMayHaveStubs);
        }
    };
    const runHastThenCompile = (r) => {
        let hastResult;
        try {
            hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
        }
        catch (err) {
            releaseHandle(r.hastHandle, hastMayHaveStubs);
            throw err;
        }
        if (hastResult instanceof Promise) {
            return hastResult.then(() => compileAndDrop(r.hastHandle, r.frontmatter), (err) => {
                releaseHandle(r.hastHandle, hastMayHaveStubs);
                throw err;
            });
        }
        return compileAndDrop(r.hastHandle, r.frontmatter);
    };
    if (result instanceof Promise)
        return result.then(runHastThenCompile);
    return runHastThenCompile(result);
}
/**
 * Compile and evaluate MDX in one step.
 *
 * Returns the module's exports, including `default` (the MDX component).
 * Returns a Promise when async plugins are used, otherwise returns synchronously.
 *
 * ```ts
 * import * as runtime from "react/jsx-runtime";
 * const { default: Content } = evaluate("# Hello", { ...runtime });
 * ```
 */
export function evaluate(source, options) {
    const { Fragment, jsx, jsxs, jsxDEV, useMDXComponents, ...compileOpts } = options;
    const runtime = { Fragment, jsx, jsxs, jsxDEV, useMDXComponents };
    const result = mdxToJs(source, { ...compileOpts, outputFormat: "function-body" });
    if (result instanceof Promise) {
        return result.then((resolved) => new Function(resolved.code)(runtime));
    }
    return new Function(result.code)(runtime);
}
function readFrontmatter(handle) {
    const raw = getMdastFrontmatter(handle);
    return raw ? { kind: raw.kind === "toml" ? "toml" : "yaml", value: raw.value } : null;
}
/** Parse, run mdast plugins, capture frontmatter, then convert to HAST.
 *  Frontmatter is read from the post-plugin MDAST so visitor mutations to
 *  the yaml/toml node are reflected in the returned value. */
function createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL, 
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nativeFeatures, 
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nativeConvertOptions, data) {
    const mdastHandle = mdx
        ? createMdxMdastHandle(source, nativeFeatures)
        : createMdastHandle(source, nativeFeatures);
    const mdastMayHaveStubs = mdastPlugins.length > 0;
    // finally{release} is intentional: convertMdastToHastHandle empties the arena
    // on success, but if any step here throws the handle would otherwise leak.
    const finalize = (r) => {
        try {
            const frontmatter = readFrontmatter(r.handle);
            // convert empties the mdast arena, so invalidate any stub held past this point.
            if (mdastMayHaveStubs)
                markHandleMutated(r.handle);
            const hastHandle = convertMdastToHastHandle(r.handle, nativeConvertOptions);
            return { hastHandle, frontmatter };
        }
        finally {
            releaseHandle(r.handle, mdastMayHaveStubs);
        }
    };
    try {
        if (mdastPlugins.length === 0) {
            return finalize({ handle: mdastHandle });
        }
        const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data);
        if (mdastResult instanceof Promise) {
            return mdastResult.then(finalize, (err) => {
                releaseHandle(mdastHandle, mdastMayHaveStubs);
                throw err;
            });
        }
        return finalize(mdastResult);
    }
    catch (err) {
        releaseHandle(mdastHandle, mdastMayHaveStubs);
        throw err;
    }
}
// Step-by-step API: individual pipeline stages with materialized trees
/** Parse Markdown source into a materialized mdast tree. */
export function markdownToMdast(source, options = {}) {
    const handle = createMdastHandle(source, featuresToNative(options.features).features);
    try {
        return materializeMdastTree(new MdastReader(serializeHandle(handle)));
    }
    finally {
        releaseHandle(handle, true);
    }
}
/** Parse MDX source into a materialized mdast tree. */
export function mdxToMdast(source, options = {}) {
    const handle = createMdxMdastHandle(source, featuresToNative(options.features).features);
    try {
        return materializeMdastTree(new MdastReader(serializeHandle(handle)));
    }
    finally {
        releaseHandle(handle, true);
    }
}
/** Convert Markdown source to a materialized hast tree. */
export function markdownToHast(source, options = {}) {
    const { features: nativeFeatures, convertOptions } = featuresToNative(options.features);
    const handle = createHastHandle(source, nativeFeatures, convertOptions);
    try {
        return materializeHastTree(new HastReader(serializeHandle(handle)));
    }
    finally {
        releaseHandle(handle, true);
    }
}
/** Convert MDX source to a materialized hast tree. */
export function mdxToHast(source, options = {}) {
    const { features: nativeFeatures, convertOptions } = featuresToNative(options.features);
    const handle = createMdxHastHandle(source, nativeFeatures, convertOptions);
    try {
        return materializeHastTree(new HastReader(serializeHandle(handle)));
    }
    finally {
        releaseHandle(handle, true);
    }
}
