import { convertChildren } from "./element.js";
import { getWithLoc, indexOf, lastIndexOf } from "./common.js";
import { getAlternateFromIfBlock, getBodyFromEachBlock, getCatchFromAwaitBlock, getChildren, getConsequentFromIfBlock, getFallbackFromEachBlock, getFragment, getPendingFromAwaitBlock, getTestFromIfBlock, getThenFromAwaitBlock, trimChildren, } from "../compat.js";
/** Get start index of block */
function startBlockIndex(code, endIndex, block) {
    return lastIndexOf(code, (c, index) => {
        if (c !== "{") {
            return false;
        }
        for (let next = index + 1; next < code.length; next++) {
            const nextC = code[next];
            if (!nextC.trim()) {
                continue;
            }
            return code.startsWith(block, next);
        }
        return false;
    }, endIndex);
}
function startIndexFromFragment(fragment, getBeforeEndIndex) {
    if (fragment.start != null) {
        return fragment.start;
    }
    const children = getChildren(fragment);
    return children.length ? children[0].start : getBeforeEndIndex();
}
function endIndexFromFragment(fragment, getBeforeEndIndex) {
    if (fragment.end != null) {
        return fragment.end;
    }
    const children = getChildren(fragment);
    return children.length
        ? children[children.length - 1].end
        : getBeforeEndIndex();
}
function endIndexFromBlock(fragment, lastExpression, ctx) {
    return endIndexFromFragment(fragment, () => {
        return ctx.code.indexOf("}", getWithLoc(lastExpression).end) + 1;
    });
}
/** Convert for IfBlock */
export function convertIfBlock(node, parent, ctx, elseifContext) {
    // {#if expr} {:else} {/if}
    // {:else if expr} {/if}
    const elseif = Boolean(elseifContext);
    const nodeStart = startBlockIndex(ctx.code, elseifContext?.start ?? node.start, elseif ? ":else" : "#if");
    const ifBlock = {
        type: "SvelteIfBlock",
        elseif: Boolean(elseif),
        expression: null,
        children: [],
        else: null,
        parent,
        ...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
    };
    const test = getTestFromIfBlock(node);
    ctx.scriptLet.nestIfBlock(test, ifBlock, (es) => {
        ifBlock.expression = es;
    });
    const consequent = getConsequentFromIfBlock(node);
    for (const child of convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(getChildren(consequent)),
    }, ifBlock, ctx)) {
        ifBlock.children.push(child);
    }
    ctx.scriptLet.closeScope();
    if (elseif) {
        const index = ctx.code.indexOf("if", nodeStart);
        ctx.addToken("MustacheKeyword", { start: index, end: index + 2 });
    }
    extractMustacheBlockTokens(ifBlock, ctx, { startOnly: elseif });
    const elseFragment = getAlternateFromIfBlock(node);
    if (!elseFragment) {
        return ifBlock;
    }
    const elseStart = startBlockIndexForElse(elseFragment, consequent, test, ctx);
    const elseChildren = getChildren(elseFragment);
    if (elseChildren.length === 1) {
        const c = elseChildren[0];
        if (c.type === "IfBlock" && c.elseif) {
            const elseBlock = {
                type: "SvelteElseBlock",
                elseif: true,
                children: [],
                parent: ifBlock,
                ...ctx.getConvertLocation({
                    start: elseStart,
                    end: c.end,
                }),
            };
            ifBlock.else = elseBlock;
            const elseIfBlock = convertIfBlock(c, elseBlock, ctx, {
                start: elseStart,
            });
            // adjust loc
            elseBlock.range[1] = elseIfBlock.range[1];
            elseBlock.loc.end = {
                line: elseIfBlock.loc.end.line,
                column: elseIfBlock.loc.end.column,
            };
            elseBlock.children = [elseIfBlock];
            return ifBlock;
        }
    }
    const elseBlock = {
        type: "SvelteElseBlock",
        elseif: false,
        children: [],
        parent: ifBlock,
        ...ctx.getConvertLocation({
            start: elseStart,
            end: endIndexFromFragment(elseFragment, () => ctx.code.indexOf("}", elseStart + 5) + 1),
        }),
    };
    ifBlock.else = elseBlock;
    ctx.scriptLet.nestBlock(elseBlock);
    for (const child of convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(elseChildren),
    }, elseBlock, ctx)) {
        elseBlock.children.push(child);
    }
    ctx.scriptLet.closeScope();
    extractMustacheBlockTokens(elseBlock, ctx, { startOnly: true });
    return ifBlock;
}
function startBlockIndexForElse(elseFragment, beforeFragment, lastExpression, ctx) {
    const elseChildren = getChildren(elseFragment);
    if (elseChildren.length > 0) {
        const c = elseChildren[0];
        if (c.type === "IfBlock" && c.elseif) {
            const contentStart = getWithLoc(getTestFromIfBlock(c)).start;
            if (contentStart <= c.start) {
                return startBlockIndex(ctx.code, contentStart - 1, ":else");
            }
        }
        return startBlockIndex(ctx.code, c.start, ":else");
    }
    const beforeEnd = endIndexFromFragment(beforeFragment, () => {
        return ctx.code.indexOf("}", getWithLoc(lastExpression).end) + 1;
    });
    return startBlockIndex(ctx.code, beforeEnd, ":else");
}
/** Convert for EachBlock */
export function convertEachBlock(node, parent, ctx) {
    // {#each expr as item, index (key)} {/each}
    const nodeStart = startBlockIndex(ctx.code, node.start, "#each");
    const eachBlock = {
        type: "SvelteEachBlock",
        expression: null,
        context: null,
        index: null,
        key: null,
        children: [],
        else: null,
        parent,
        ...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
    };
    let indexRange = null;
    if (node.index) {
        const start = ctx.code.indexOf(node.index, getWithLoc(node.context ?? node.expression).end);
        indexRange = {
            start,
            end: start + node.index.length,
        };
    }
    ctx.scriptLet.nestEachBlock(node.expression, node.context, indexRange, eachBlock, (expression, context, index) => {
        eachBlock.expression = expression;
        eachBlock.context = context;
        eachBlock.index = index;
    });
    if (node.context) {
        const asStart = ctx.code.indexOf("as", getWithLoc(node.expression).end);
        ctx.addToken("Keyword", {
            start: asStart,
            end: asStart + 2,
        });
    }
    if (node.key) {
        ctx.scriptLet.addExpression(node.key, eachBlock, null, (key) => {
            eachBlock.key = key;
        });
    }
    const body = getBodyFromEachBlock(node);
    eachBlock.children.push(...convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(getChildren(body)),
    }, eachBlock, ctx));
    ctx.scriptLet.closeScope();
    extractMustacheBlockTokens(eachBlock, ctx);
    const fallbackFragment = getFallbackFromEachBlock(node);
    if (!fallbackFragment) {
        return eachBlock;
    }
    const elseStart = startBlockIndexForElse(fallbackFragment, body, node.key || indexRange || node.context || node.expression, ctx);
    const elseBlock = {
        type: "SvelteElseBlock",
        elseif: false,
        children: [],
        parent: eachBlock,
        ...ctx.getConvertLocation({
            start: elseStart,
            end: endIndexFromFragment(fallbackFragment, () => elseStart),
        }),
    };
    eachBlock.else = elseBlock;
    ctx.scriptLet.nestBlock(elseBlock);
    elseBlock.children.push(...convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(getChildren(fallbackFragment)),
    }, elseBlock, ctx));
    ctx.scriptLet.closeScope();
    extractMustacheBlockTokens(elseBlock, ctx, { startOnly: true });
    return eachBlock;
}
/** Convert for AwaitBlock */
export function convertAwaitBlock(node, parent, ctx) {
    const nodeStart = startBlockIndex(ctx.code, node.start, "#await");
    const awaitBlock = {
        type: "SvelteAwaitBlock",
        expression: null,
        kind: "await",
        pending: null,
        then: null,
        catch: null,
        parent,
        ...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
    };
    ctx.scriptLet.addExpression(node.expression, awaitBlock, null, (expression) => {
        awaitBlock.expression = expression;
    });
    const pending = getPendingFromAwaitBlock(node);
    if (pending) {
        const pendingBlock = {
            type: "SvelteAwaitPendingBlock",
            children: [],
            parent: awaitBlock,
            ...ctx.getConvertLocation({
                start: awaitBlock.range[0],
                end: endIndexFromBlock(pending, node.expression, ctx),
            }),
        };
        ctx.scriptLet.nestBlock(pendingBlock);
        pendingBlock.children.push(...convertChildren(pending, pendingBlock, ctx));
        awaitBlock.pending = pendingBlock;
        ctx.scriptLet.closeScope();
    }
    const then = getThenFromAwaitBlock(node);
    if (then) {
        const awaitThen = !pending;
        if (awaitThen) {
            awaitBlock.kind = "await-then";
        }
        const thenStart = awaitBlock.pending
            ? startBlockIndex(ctx.code, node.value
                ? getWithLoc(node.value).start
                : startIndexFromFragment(then, () => {
                    return awaitBlock.pending.range[1];
                }), ":then")
            : nodeStart;
        const thenBlock = {
            type: "SvelteAwaitThenBlock",
            awaitThen,
            value: null,
            children: [],
            parent: awaitBlock,
            ...ctx.getConvertLocation({
                start: thenStart,
                end: endIndexFromFragment(then, () => {
                    return (ctx.code.indexOf("}", node.value ? getWithLoc(node.value).end : thenStart + 5) + 1);
                }),
            }),
        };
        if (node.value) {
            const baseParam = {
                node: node.value,
                parent: thenBlock,
                callback(value) {
                    thenBlock.value = value;
                },
                typing: "any",
            };
            ctx.scriptLet.nestBlock(thenBlock, (typeCtx) => {
                if (!typeCtx) {
                    return {
                        param: baseParam,
                    };
                }
                const expression = ctx.getText(node.expression);
                if (node.expression.type === "Literal") {
                    return {
                        param: {
                            ...baseParam,
                            typing: expression,
                        },
                    };
                }
                const idAwaitThenValue = typeCtx.generateUniqueId("AwaitThenValue");
                if (node.expression.type === "Identifier" &&
                    // We cannot use type annotations like `(x: Foo<x>)` if they have the same identifier name.
                    !hasIdentifierFor(node.expression.name, baseParam.node)) {
                    return {
                        preparationScript: [generateAwaitThenValueType(idAwaitThenValue)],
                        param: {
                            ...baseParam,
                            typing: `${idAwaitThenValue}<(typeof ${expression})>`,
                        },
                    };
                }
                const id = typeCtx.generateUniqueId(expression);
                return {
                    preparationScript: [
                        {
                            script: `const ${id} = ${expression};`,
                            nodeType: "VariableDeclaration",
                        },
                        generateAwaitThenValueType(idAwaitThenValue),
                    ],
                    param: {
                        ...baseParam,
                        typing: `${idAwaitThenValue}<(typeof ${id})>`,
                    },
                };
            });
        }
        else {
            ctx.scriptLet.nestBlock(thenBlock);
        }
        thenBlock.children.push(...convertChildren(then, thenBlock, ctx));
        if (awaitBlock.pending) {
            extractMustacheBlockTokens(thenBlock, ctx, { startOnly: true });
        }
        else {
            const thenIndex = ctx.code.indexOf("then", getWithLoc(node.expression).end);
            ctx.addToken("MustacheKeyword", {
                start: thenIndex,
                end: thenIndex + 4,
            });
        }
        awaitBlock.then = thenBlock;
        ctx.scriptLet.closeScope();
    }
    const catchFragment = getCatchFromAwaitBlock(node);
    if (catchFragment) {
        const awaitCatch = !pending && !then;
        if (awaitCatch) {
            awaitBlock.kind = "await-catch";
        }
        const catchStart = awaitBlock.then || awaitBlock.pending
            ? startBlockIndex(ctx.code, node.error
                ? getWithLoc(node.error).start
                : startIndexFromFragment(catchFragment, () => {
                    return (awaitBlock.then || awaitBlock.pending).range[1];
                }), ":catch")
            : nodeStart;
        const catchBlock = {
            type: "SvelteAwaitCatchBlock",
            awaitCatch,
            error: null,
            children: [],
            parent: awaitBlock,
            ...ctx.getConvertLocation({
                start: catchStart,
                end: endIndexFromFragment(catchFragment, () => {
                    return (ctx.code.indexOf("}", node.error ? getWithLoc(node.error).end : catchStart + 6) + 1);
                }),
            }),
        };
        if (node.error) {
            ctx.scriptLet.nestBlock(catchBlock, [
                {
                    node: node.error,
                    parent: catchBlock,
                    typing: "Error",
                    callback: (error) => {
                        catchBlock.error = error;
                    },
                },
            ]);
        }
        else {
            ctx.scriptLet.nestBlock(catchBlock);
        }
        catchBlock.children.push(...convertChildren(catchFragment, catchBlock, ctx));
        if (awaitBlock.pending || awaitBlock.then) {
            extractMustacheBlockTokens(catchBlock, ctx, { startOnly: true });
        }
        else {
            const catchIndex = ctx.code.indexOf("catch", getWithLoc(node.expression).end);
            ctx.addToken("MustacheKeyword", {
                start: catchIndex,
                end: catchIndex + 5,
            });
        }
        awaitBlock.catch = catchBlock;
        ctx.scriptLet.closeScope();
    }
    extractMustacheBlockTokens(awaitBlock, ctx);
    return awaitBlock;
}
/** Convert for KeyBlock */
export function convertKeyBlock(node, parent, ctx) {
    const nodeStart = startBlockIndex(ctx.code, node.start, "#key");
    const keyBlock = {
        type: "SvelteKeyBlock",
        expression: null,
        children: [],
        parent,
        ...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
    };
    ctx.scriptLet.addExpression(node.expression, keyBlock, null, (expression) => {
        keyBlock.expression = expression;
    });
    ctx.scriptLet.nestBlock(keyBlock);
    keyBlock.children.push(...convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(getChildren(getFragment(node))),
    }, keyBlock, ctx));
    ctx.scriptLet.closeScope();
    extractMustacheBlockTokens(keyBlock, ctx);
    return keyBlock;
}
/** Convert for SnippetBlock */
export function convertSnippetBlock(node, parent, ctx) {
    // {#snippet x(args)}...{/snippet}
    const nodeStart = startBlockIndex(ctx.code, node.start, "#snippet");
    const snippetBlock = {
        type: "SvelteSnippetBlock",
        id: null,
        params: [],
        children: [],
        parent,
        ...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
    };
    let beforeClosingParen;
    if (node.parameters.length > 0) {
        const lastParam = node.parameters[node.parameters.length - 1];
        beforeClosingParen = lastParam.typeAnnotation ?? lastParam;
    }
    else {
        beforeClosingParen = node.expression;
    }
    const closeParenIndex = ctx.code.indexOf(")", getWithLoc(beforeClosingParen).end);
    const scopeKind = parent.type === "Program"
        ? "snippet"
        : // use currentScriptScopeKind
            null;
    ctx.scriptLet.nestSnippetBlock(node.expression, closeParenIndex, snippetBlock, scopeKind, (id, params) => {
        snippetBlock.id = id;
        snippetBlock.params = params;
    });
    snippetBlock.children.push(...convertChildren({
        nodes: 
        // Adjust for Svelte v5
        trimChildren(node.body.nodes),
    }, snippetBlock, ctx));
    ctx.scriptLet.closeScope();
    extractMustacheBlockTokens(snippetBlock, ctx);
    ctx.snippets.push(snippetBlock);
    return snippetBlock;
}
/** Extract mustache block tokens */
function extractMustacheBlockTokens(node, ctx, option) {
    const startSectionNameStart = indexOf(ctx.code, (c) => Boolean(c.trim()), node.range[0] + 1);
    const startSectionNameEnd = indexOf(ctx.code, (c) => c === "}" || !c.trim(), startSectionNameStart + 1);
    ctx.addToken("MustacheKeyword", {
        start: startSectionNameStart,
        end: startSectionNameEnd,
    });
    if (option?.startOnly) {
        return;
    }
    const endSectionNameEnd = lastIndexOf(ctx.code, (c) => Boolean(c.trim()), node.range[1] - 2) + 1;
    const endSectionNameStart = lastIndexOf(ctx.code, (c) => c === "{" || c === "/" || !c.trim(), endSectionNameEnd - 1);
    ctx.addToken("MustacheKeyword", {
        start: endSectionNameStart,
        end: endSectionNameEnd,
    });
}
/** Generate Awaited like type code */
function generateAwaitThenValueType(id) {
    return {
        script: `type ${id}<T> = T extends null | undefined
    ? T
    : T extends { then(value: infer F): any }
    ? F extends (value: infer V, ...args: any) => any
        ? ${id}<V>
        : never
        : T;`,
        nodeType: "TSTypeAliasDeclaration",
    };
}
/** Checks whether the given name identifier is exists or not. */
function hasIdentifierFor(name, node) {
    if (node.type === "Identifier") {
        return node.name === name;
    }
    if (node.type === "ObjectPattern") {
        return node.properties.some((property) => property.type === "Property"
            ? hasIdentifierFor(name, property.value)
            : hasIdentifierFor(name, property));
    }
    if (node.type === "ArrayPattern") {
        return node.elements.some((element) => element && hasIdentifierFor(name, element));
    }
    if (node.type === "RestElement") {
        return hasIdentifierFor(name, node.argument);
    }
    if (node.type === "AssignmentPattern") {
        return hasIdentifierFor(name, node.left);
    }
    return false;
}
