import OrderedMap from 'orderedmap';
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from 'prosemirror-keymap';
var Priority;
(function (Priority) {
    Priority[Priority["Highest"] = 1000000] = "Highest";
    Priority[Priority["VeryHigh"] = 100000] = "VeryHigh";
    Priority[Priority["High"] = 10000] = "High";
    Priority[Priority["Medium"] = 1000] = "Medium";
    Priority[Priority["Low"] = 100] = "Low";
    Priority[Priority["VeryLow"] = 10] = "VeryLow";
    Priority[Priority["Lowest"] = 0] = "Lowest";
})(Priority || (Priority = {}));
const DEFAULT_PRIORITY = Priority.Medium;
function resolveParserPipeline(pipeline, initialPrimaryTokens, initialParsers) {
    const primaryParserToken = { ...initialPrimaryTokens };
    const parsers = { ...initialParsers };
    const extraTokenNames = [];
    const overriddenTokens = new Set();
    for (const entry of pipeline) {
        if (entry.type === 'addParserSpec') {
            const tokenSpec = entry.cb();
            const entityName = tokenSpec.name;
            if (primaryParserToken[entityName] === entry.tokenName) {
                // Same-name token for addNode/addMark entity — skip, already covered
                continue;
            }
            if (entry.tokenName in initialParsers) {
                throw new Error(`Parser token "${entry.tokenName}" is already owned by an entity registered via addNode/addMark. ` +
                    `Use overrideMarkdownTokenParserSpec() to modify it, or choose a different tokenName.`);
            }
            if (primaryParserToken[entityName]) {
                // Extra parser-only token targeting an existing entity
                extraTokenNames.push(entry.tokenName);
            }
            else {
                // Primary parser for a granular entity
                primaryParserToken[entityName] = entry.tokenName;
            }
            parsers[entry.tokenName] = { tokenName: entry.tokenName, tokenSpec };
        }
        else {
            // overrideParserSpec
            const existing = parsers[entry.tokenName];
            if (existing) {
                parsers[entry.tokenName] = {
                    tokenName: existing.tokenName,
                    tokenSpec: entry.cb(existing.tokenSpec),
                };
                overriddenTokens.add(entry.tokenName);
            }
        }
    }
    return { parsers, primaryParserToken, extraTokenNames, overriddenTokens };
}
function resolveSerializerPipeline(pipeline, initialSerializers) {
    const serializers = { ...initialSerializers };
    const overriddenNames = new Set();
    for (const entry of pipeline) {
        if (entry.type === 'addSerializer') {
            serializers[entry.name] = entry.cb();
        }
        else {
            serializers[entry.name] = entry.cb(serializers[entry.name]);
            overriddenNames.add(entry.name);
        }
    }
    return { serializers, overriddenNames };
}
function validateGranularSpecs(granularNames, entityType, primaryParserToken, serializers) {
    for (const name of granularNames) {
        if (!primaryParserToken[name]) {
            throw new Error(`Incomplete ${entityType} spec for "${name}": missing parser spec. ` +
                `Use addMarkdownTokenParserSpec() to register a parser for this ${entityType}.`);
        }
        if (!serializers[name]) {
            throw new Error(`Incomplete ${entityType} spec for "${name}": missing serializer. ` +
                `Use add${entityType === 'node' ? 'Node' : 'Mark'}SerializerSpec() to register a serializer for this ${entityType}.`);
        }
    }
}
function buildParserOnlyNodeEntry(resolved) {
    return {
        spec: {},
        fromMd: { tokenName: resolved.tokenName, tokenSpec: resolved.tokenSpec },
        toMd: () => {
            throw new Error(`Unexpected toMd() call on parser-only node "${resolved.tokenName}"`);
        },
    };
}
function buildParserOnlyMarkEntry(resolved) {
    return {
        spec: {},
        fromMd: { tokenName: resolved.tokenName, tokenSpec: resolved.tokenSpec },
        toMd: { open: '', close: '' },
    };
}
function processEntityPipeline(entityType, entityPipeline, parserPipeline, serializerPipeline, buildParserOnlyEntry) {
    const order = [];
    const specs = {};
    const initParsers = {};
    const initSerializers = {};
    const views = {};
    const granularEntities = new Set();
    const initPrimaryTokens = {};
    const priorityByEntity = {};
    // Originals from addEntity — preserved as-is in assembly when no overrides were applied.
    // This keeps reference identity (and any extra fields) for unmodified entries.
    const originals = {};
    const modified = new Set();
    // Pass 1: entityPipeline → specs, order, raw parsers/serializers from addEntity
    for (const entry of entityPipeline) {
        const { name } = entry;
        if (entry.type === 'addEntity') {
            const base = entry.cb();
            const tokenName = base.fromMd.tokenName ?? name;
            order.push({ name, priority: entry.priority });
            specs[name] = base.spec;
            initParsers[tokenName] = { tokenName, tokenSpec: base.fromMd.tokenSpec };
            initPrimaryTokens[name] = tokenName;
            priorityByEntity[name] = entry.priority;
            initSerializers[name] = base.toMd;
            views[name] = base.view;
            originals[name] = base;
        }
        else if (entry.type === 'addEntitySpec') {
            order.push({ name, priority: entry.priority });
            specs[name] = entry.cb();
            priorityByEntity[name] = entry.priority;
            granularEntities.add(name);
        }
        else {
            // overrideEntitySpec
            specs[name] = entry.cb(specs[name]);
            modified.add(name);
        }
    }
    // Pass 2: parserPipeline → fill/override parsers, add extra parser-only entries
    const { parsers, primaryParserToken, extraTokenNames, overriddenTokens } = resolveParserPipeline(parserPipeline, initPrimaryTokens, initParsers);
    for (const tokenName of extraTokenNames) {
        // Inherit priority from the entity this token targets
        const entityName = parsers[tokenName]?.tokenSpec.name;
        const priority = entityName ? (priorityByEntity[entityName] ?? 0) : 0;
        order.push({ name: tokenName, priority });
    }
    // Map primary-token overrides back to their owning addEntity names
    for (const entityName of Object.keys(originals)) {
        if (overriddenTokens.has(initPrimaryTokens[entityName])) {
            modified.add(entityName);
        }
    }
    // Pass 3: serializerPipeline → fill/override serializers
    const { serializers, overriddenNames: overriddenSerializers } = resolveSerializerPipeline(serializerPipeline, initSerializers);
    for (const name of overriddenSerializers) {
        modified.add(name);
    }
    validateGranularSpecs(granularEntities, entityType, primaryParserToken, serializers);
    if (entityType === 'mark') {
        // The order of marks in schema is important when serializing pm-document to DOM or markup
        // https://discuss.prosemirror.net/t/marks-priority/4463
        order.sort((a, b) => b.priority - a.priority);
    }
    // Assemble
    let map = OrderedMap.from({});
    for (const { name } of order) {
        // Fast path: unmodified addEntity registrations keep their original object reference.
        if (originals[name] && !modified.has(name)) {
            map = map.addToEnd(name, originals[name]);
            continue;
        }
        const parserKey = primaryParserToken[name] ?? name;
        map = map.addToEnd(name, serializers[name]
            ? {
                spec: specs[name] ?? {},
                fromMd: parsers[parserKey],
                toMd: serializers[name],
                ...(views[name] !== undefined && { view: views[name] }),
            }
            : buildParserOnlyEntry(parsers[name]));
    }
    return map;
}
export class ExtensionBuilder {
    static createContext() {
        return new Map();
    }
    /* eslint-disable @typescript-eslint/member-ordering */
    static Priority = Priority;
    Priority = ExtensionBuilder.Priority;
    /* eslint-enable @typescript-eslint/member-ordering */
    #logger;
    #confMdCbs = [];
    #plugins = [];
    #actions = [];
    // Unified pipelines — preserve registration order across legacy and granular APIs
    #nodePipeline = [];
    #nodeIndex = {};
    #markPipeline = [];
    #markIndex = {};
    // Parser and serializer pipelines
    #parserPipeline = [];
    #parserIndex = new Set();
    #nodeSerializerPipeline = [];
    #nodeSerializerIndex = new Set();
    #markSerializerPipeline = [];
    #markSerializerIndex = new Set();
    context;
    constructor(logger, context) {
        this.#logger = logger;
        this.context = context ?? ExtensionBuilder.createContext();
    }
    get logger() {
        return this.#logger;
    }
    use(extension, ...params) {
        extension(this, ...params);
        return this;
    }
    configureMd(cb, params = {}) {
        this.#confMdCbs.push({
            cb,
            params: {
                text: params.text ?? true,
                markup: params.markup ?? true,
            },
        });
        return this;
    }
    hasNodeSpec(name) {
        return Boolean(this.#nodeIndex[name]);
    }
    hasMarkSpec(name) {
        return Boolean(this.#markIndex[name]);
    }
    /**
     * @deprecated Will be removed in the next major version.
     * Use addNodeSpec() + addMarkdownTokenParserSpec() + addNodeSerializerSpec() instead.
     */
    addNode(name, cb) {
        if (this.#nodeIndex[name]?.source === 'addNode') {
            throw new Error(`ProseMirror node with this name "${name}" already exist`);
        }
        if (this.#nodeIndex[name]?.source === 'addNodeSpec') {
            throw new Error(`Node with name "${name}" already registered via addNodeSpec. ` +
                `Cannot use addNode for a node that already has granular registrations.`);
        }
        if (this.#nodeSerializerIndex.has(name)) {
            throw new Error(`Node serializer for "${name}" already registered via addNodeSerializerSpec. ` +
                `Cannot use addNode for a node that already has granular registrations.`);
        }
        this.#nodePipeline.push({ type: 'addEntity', name, cb, priority: 0 });
        this.#nodeIndex[name] = { source: 'addNode' };
        return this;
    }
    /**
     * @deprecated Will be removed in the next major version.
     * Use addMarkSpec() + addMarkdownTokenParserSpec() + addMarkSerializerSpec() instead.
     */
    addMark(name, cb, priority = DEFAULT_PRIORITY) {
        if (this.#markIndex[name]?.source === 'addMark') {
            throw new Error(`ProseMirror mark with this name "${name}" already exist`);
        }
        if (this.#markIndex[name]?.source === 'addMarkSpec') {
            throw new Error(`Mark with name "${name}" already registered via addMarkSpec. ` +
                `Cannot use addMark for a mark that already has granular registrations.`);
        }
        if (this.#markSerializerIndex.has(name)) {
            throw new Error(`Mark serializer for "${name}" already registered via addMarkSerializerSpec. ` +
                `Cannot use addMark for a mark that already has granular registrations.`);
        }
        this.#markPipeline.push({ type: 'addEntity', name, cb, priority });
        this.#markIndex[name] = { source: 'addMark' };
        return this;
    }
    addPlugin(cb, priority = DEFAULT_PRIORITY) {
        this.#plugins.push({ cb, priority });
        return this;
    }
    addKeymap(cb, priority = DEFAULT_PRIORITY) {
        this.#plugins.push({ cb: (...args) => keymap(cb(...args)), priority });
        return this;
    }
    addInputRules(cb, priority = DEFAULT_PRIORITY) {
        this.#plugins.push({ cb: (...args) => inputRules(cb(...args)), priority });
        return this;
    }
    addAction(name, cb) {
        if (this.#actions.some(([actionName]) => actionName === name)) {
            throw new Error(`[Markdown Wysiwyg Editor] action with this name "${name}" already exist`);
        }
        this.#actions.push([name, cb]);
        return this;
    }
    addNodeSpec(name, cb) {
        if (this.#nodeIndex[name]?.source === 'addNodeSpec') {
            throw new Error(`Node spec with name "${name}" already registered via addNodeSpec`);
        }
        if (this.#nodeIndex[name]?.source === 'addNode') {
            throw new Error(`Node with name "${name}" already registered via addNode. Use overrideNodeSpec to modify it.`);
        }
        this.#nodePipeline.push({ type: 'addEntitySpec', name, cb, priority: 0 });
        this.#nodeIndex[name] = { source: 'addNodeSpec' };
        return this;
    }
    addMarkSpec(name, cb, priority = DEFAULT_PRIORITY) {
        if (this.#markIndex[name]?.source === 'addMarkSpec') {
            throw new Error(`Mark spec with name "${name}" already registered via addMarkSpec`);
        }
        if (this.#markIndex[name]?.source === 'addMark') {
            throw new Error(`Mark with name "${name}" already registered via addMark. Use overrideMarkSpec to modify it.`);
        }
        this.#markPipeline.push({ type: 'addEntitySpec', name, cb, priority });
        this.#markIndex[name] = { source: 'addMarkSpec' };
        return this;
    }
    addMarkdownTokenParserSpec(tokenName, cb) {
        if (this.#parserIndex.has(tokenName)) {
            throw new Error(`Parser spec for token "${tokenName}" already registered via addMarkdownTokenParserSpec`);
        }
        this.#parserPipeline.push({ type: 'addParserSpec', tokenName, cb });
        this.#parserIndex.add(tokenName);
        return this;
    }
    addNodeSerializerSpec(name, cb) {
        if (this.#nodeSerializerIndex.has(name)) {
            throw new Error(`Node serializer for "${name}" already registered via addNodeSerializerSpec`);
        }
        if (this.#nodeIndex[name]?.source === 'addNode') {
            throw new Error(`Node with name "${name}" already registered via addNode. Use overrideNodeSerializerSpec to modify it.`);
        }
        this.#nodeSerializerPipeline.push({ type: 'addSerializer', name, cb });
        this.#nodeSerializerIndex.add(name);
        return this;
    }
    addMarkSerializerSpec(name, cb) {
        if (this.#markSerializerIndex.has(name)) {
            throw new Error(`Mark serializer for "${name}" already registered via addMarkSerializerSpec`);
        }
        if (this.#markIndex[name]?.source === 'addMark') {
            throw new Error(`Mark with name "${name}" already registered via addMark. Use overrideMarkSerializerSpec to modify it.`);
        }
        this.#markSerializerPipeline.push({ type: 'addSerializer', name, cb });
        this.#markSerializerIndex.add(name);
        return this;
    }
    overrideNodeSpec(name, cb) {
        if (!this.#nodeIndex[name]) {
            throw new Error(`Cannot override node spec "${name}": not registered. Use addNode() or addNodeSpec() first.`);
        }
        this.#nodePipeline.push({ type: 'overrideEntitySpec', name, cb });
        return this;
    }
    overrideMarkSpec(name, cb) {
        if (!this.#markIndex[name]) {
            throw new Error(`Cannot override mark spec "${name}": not registered. Use addMark() or addMarkSpec() first.`);
        }
        this.#markPipeline.push({ type: 'overrideEntitySpec', name, cb });
        return this;
    }
    overrideMarkdownTokenParserSpec(tokenName, cb) {
        if (!this.#parserIndex.has(tokenName) &&
            !this.#nodeIndex[tokenName] &&
            !this.#markIndex[tokenName]) {
            throw new Error(`Cannot override parser spec for token "${tokenName}": not registered. ` +
                `Use addMarkdownTokenParserSpec(), addNode(), or addMark() first.`);
        }
        this.#parserPipeline.push({ type: 'overrideParserSpec', tokenName, cb });
        return this;
    }
    overrideNodeSerializerSpec(name, cb) {
        if (this.#nodeIndex[name]?.source !== 'addNode' && !this.#nodeSerializerIndex.has(name)) {
            throw new Error(`Cannot override node serializer "${name}": not registered. Use addNode() or addNodeSerializerSpec() first.`);
        }
        this.#nodeSerializerPipeline.push({ type: 'overrideSerializer', name, cb });
        return this;
    }
    overrideMarkSerializerSpec(name, cb) {
        if (this.#markIndex[name]?.source !== 'addMark' && !this.#markSerializerIndex.has(name)) {
            throw new Error(`Cannot override mark serializer "${name}": not registered. Use addMark() or addMarkSerializerSpec() first.`);
        }
        this.#markSerializerPipeline.push({ type: 'overrideSerializer', name, cb });
        return this;
    }
    build() {
        const confMd = this.#confMdCbs.slice();
        const nodePipeline = this.#nodePipeline.slice();
        const markPipeline = this.#markPipeline.slice();
        const parserPipeline = this.#parserPipeline.slice();
        const nodeSerializerPipeline = this.#nodeSerializerPipeline.slice();
        const markSerializerPipeline = this.#markSerializerPipeline.slice();
        const plugins = this.#plugins.slice();
        const actions = this.#actions.slice();
        return {
            configureMd: (md, parserType) => confMd.reduce((pMd, { cb, params }) => {
                if (parserType === 'text' && params.text) {
                    return cb(pMd);
                }
                if (parserType === 'markup' && params.markup) {
                    return cb(pMd);
                }
                return pMd;
            }, md),
            nodes: () => processEntityPipeline('node', nodePipeline, parserPipeline, nodeSerializerPipeline, buildParserOnlyNodeEntry),
            marks: () => processEntityPipeline('mark', markPipeline, parserPipeline, markSerializerPipeline, buildParserOnlyMarkEntry),
            plugins: (deps) => {
                return plugins
                    .sort((a, b) => b.priority - a.priority)
                    .reduce((acc, { cb }) => {
                    const res = cb(deps);
                    if (Array.isArray(res))
                        acc.push(...res);
                    else
                        acc.push(res);
                    return acc;
                }, []);
            },
            actions: (deps) => actions.reduce((obj, [name, cb]) => {
                obj[name] = cb(deps);
                return obj;
            }, {}),
        };
    }
}
//# sourceMappingURL=ExtensionBuilder.js.map
