/**
 * Pattern lowering pass.
 *
 * Walks the AST and transforms all destructuring/pattern-matching syntax into
 * existing Agency constructs (Assignments, IfElse, BinOpExpressions, ValueAccess,
 * FunctionCalls). After this pass, the AST contains NO pattern-specific nodes,
 * so the rest of the pipeline (typechecker, TypeScriptBuilder, preprocessor, LSP)
 * needs zero changes.
 *
 * Runs after parsing, before SymbolTable.build / typechecking.
 */
import { mapBodies } from "../utils/mapBodies.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function lowerPatterns(nodes) {
    return new PatternLowerer().lower(nodes);
}
class PatternLowerer {
    counter = 0;
    freshName(prefix) {
        return `__${prefix}_${++this.counter}`;
    }
    lower(nodes) {
        return nodes.flatMap((n) => this.lowerNode(n));
    }
    lowerNode(node) {
        switch (node.type) {
            case "assignment":
                return this.lowerAssignment(node);
            case "ifElse":
                return [this.lowerIfElse(node)];
            case "whileLoop":
                return [this.lowerWhileLoop(node)];
            case "matchBlock":
                return this.lowerMatchBlock(node);
            case "forLoop":
                return [this.lowerForLoop(node)];
            case "returnStatement": {
                const ret = node;
                return [
                    {
                        ...ret,
                        value: ret.value !== undefined ? this.lowerExpression(ret.value) : undefined,
                    },
                ];
            }
            default: {
                // Recurse into every body field on body-bearing nodes (function /
                // graphNode / handleBlock / parallelBlock / seqBlock / blockArgument /
                // functionCall.block / …). For nodes that are also Expressions,
                // additionally walk the expression tree so nested `isExpression` is
                // lowered.
                const withRecursedBodies = mapBodies(node, (b) => this.lower(b));
                if (isExpr(withRecursedBodies)) {
                    return [this.lowerExpression(withRecursedBodies)];
                }
                return [withRecursedBodies];
            }
        }
    }
    /**
     * Recursively lower an expression: any `isExpression` found in a pure-boolean
     * context (anywhere except as the direct condition of `if`/`while` or the
     * direct expression of `match`) is converted to its boolean condition form.
     * Binders inside such an `isExpression` are a compile-time error.
     */
    lowerExpression(expr) {
        if (expr.type === "isExpression") {
            assertNoBindersInBoolIs(expr.pattern);
            const inner = this.lowerExpression(expr.expression);
            return patternToCondition(expr.pattern, inner) ?? boolLit(true, expr.loc);
        }
        if (expr.type === "binOpExpression") {
            return {
                ...expr,
                left: this.lowerExpression(expr.left),
                right: this.lowerExpression(expr.right),
            };
        }
        if (expr.type === "functionCall") {
            const block = expr.block
                ? { ...expr.block, body: this.lower(expr.block.body) }
                : expr.block;
            return {
                ...expr,
                arguments: expr.arguments.map((a) => {
                    if ("type" in a && (a.type === "splat" || a.type === "namedArgument")) {
                        return { ...a, value: this.lowerExpression(a.value) };
                    }
                    return this.lowerExpression(a);
                }),
                block,
            };
        }
        if (expr.type === "valueAccess") {
            return {
                ...expr,
                chain: expr.chain.map((c) => {
                    if (c.kind === "index")
                        return { ...c, index: this.lowerExpression(c.index) };
                    if (c.kind === "slice") {
                        return {
                            ...c,
                            start: c.start ? this.lowerExpression(c.start) : c.start,
                            end: c.end ? this.lowerExpression(c.end) : c.end,
                        };
                    }
                    return c;
                }),
            };
        }
        if (expr.type === "agencyArray") {
            return {
                ...expr,
                items: expr.items.map((it) => "type" in it && it.type === "splat"
                    ? { ...it, value: this.lowerExpression(it.value) }
                    : this.lowerExpression(it)),
            };
        }
        if (expr.type === "agencyObject") {
            return {
                ...expr,
                entries: expr.entries.map((e) => ({
                    ...e,
                    value: this.lowerExpression(e.value),
                })),
            };
        }
        return expr;
    }
    // -------------------------------------------------------------------------
    // Assignment
    // -------------------------------------------------------------------------
    lowerAssignment(node) {
        // node.value can be Expression | MessageThread; only walk Expression cases.
        const loweredValue = isExpr(node.value)
            ? this.lowerExpression(node.value)
            : node.value;
        if (!node.pattern) {
            // Pure-boolean context: walk the value and replace any nested
            // `isExpression` with its boolean condition. Binders are a compile error.
            return [{ ...node, value: loweredValue }];
        }
        const loc = node.loc;
        const userDeclKind = node.declKind ?? "const";
        const tempName = this.freshName("tmp");
        // Temp is ALWAYS const — it's never reassigned.
        const tempAssign = {
            type: "assignment",
            variableName: tempName,
            declKind: "const",
            value: loweredValue,
            loc,
        };
        const tempRef = varRef(tempName, loc);
        const bindings = this.extractBindings(node.pattern, tempRef, userDeclKind, loc);
        return [tempAssign, ...bindings];
    }
    // -------------------------------------------------------------------------
    // If / While
    // -------------------------------------------------------------------------
    lowerIfElse(node) {
        if (node.condition.type === "isExpression") {
            const isExp = node.condition;
            const condition = patternToCondition(isExp.pattern, isExp.expression) ?? boolLit(true, node.loc);
            const bindings = this.extractBindings(isExp.pattern, isExp.expression, "const", node.loc);
            return {
                ...node,
                condition,
                thenBody: [...bindings, ...this.lower(node.thenBody)],
                elseBody: node.elseBody ? this.lower(node.elseBody) : undefined,
            };
        }
        return {
            ...node,
            condition: this.lowerExpression(node.condition),
            thenBody: this.lower(node.thenBody),
            elseBody: node.elseBody ? this.lower(node.elseBody) : undefined,
        };
    }
    lowerWhileLoop(node) {
        if (node.condition.type === "isExpression") {
            const isExp = node.condition;
            const condition = patternToCondition(isExp.pattern, isExp.expression) ?? boolLit(true, node.loc);
            const bindings = this.extractBindings(isExp.pattern, isExp.expression, "const", node.loc);
            return {
                ...node,
                condition,
                body: [...bindings, ...this.lower(node.body)],
            };
        }
        return {
            ...node,
            condition: this.lowerExpression(node.condition),
            body: this.lower(node.body),
        };
    }
    // -------------------------------------------------------------------------
    // Match block
    // -------------------------------------------------------------------------
    lowerMatchBlock(node) {
        if (node.expression.type === "isExpression") {
            return this.lowerMatchIsForm(node);
        }
        // Check if any arm uses patterns or has a guard.
        const hasPatternArms = node.cases.some((c) => c.type === "matchBlockCase" &&
            ((c.caseValue !== "_" &&
                (c.caseValue.type === "objectPattern" ||
                    c.caseValue.type === "arrayPattern" ||
                    c.caseValue.type === "resultPattern")) ||
                c.guard !== undefined));
        if (!hasPatternArms) {
            // Pure literal/identifier match — pass through, but still recurse into arm bodies.
            return [
                {
                    ...node,
                    cases: node.cases.map((c) => (c.type === "matchBlockCase" ? this.lowerMatchCase(c) : c)),
                },
            ];
        }
        // Bind scrutinee to a temp (evaluate exactly once even if expression has side effects).
        const scrutineeName = this.freshName("scrutinee");
        const scrutineeAssign = {
            type: "assignment",
            variableName: scrutineeName,
            declKind: "const",
            value: node.expression,
            loc: node.loc,
        };
        const scrutineeRef = varRef(scrutineeName, node.loc);
        const ifChain = this.buildIfChainFromArms(node.cases, scrutineeRef, node.loc);
        return ifChain ? [scrutineeAssign, ifChain] : [scrutineeAssign];
    }
    lowerMatchIsForm(node) {
        const isExpr = node.expression;
        const scrutineeName = this.freshName("scrutinee");
        const scrutineeAssign = {
            type: "assignment",
            variableName: scrutineeName,
            declKind: "const",
            value: isExpr.expression,
            loc: node.loc,
        };
        const scrutineeRef = varRef(scrutineeName, node.loc);
        const bindings = this.extractBindings(isExpr.pattern, scrutineeRef, "const", node.loc);
        // Each arm's caseValue is now a guard expression; build if/else chain over them.
        const ifChain = this.buildIfChainFromGuardArms(node.cases, node.loc);
        const inside = [...bindings];
        if (ifChain)
            inside.push(ifChain);
        // If the head pattern includes literal value checks (e.g. `match (x is { type: "a", v })`),
        // gate the bindings + arm chain on those checks. When the pattern doesn't
        // match at runtime the user has indicated a precondition, so we surface
        // the mismatch as a `failure` Result rather than silently no-oping.
        const patCond = patternToCondition(isExpr.pattern, scrutineeRef);
        if (patCond) {
            const failureMsg = "match(... is pattern) head pattern did not match";
            const failureCall = {
                type: "functionCall",
                functionName: "failure",
                arguments: [stringLit(failureMsg, node.loc)],
                loc: node.loc,
            };
            const failReturn = {
                type: "returnStatement",
                value: failureCall,
                loc: node.loc,
            };
            const guardedIf = {
                type: "ifElse",
                condition: patCond,
                thenBody: inside,
                elseBody: [failReturn],
                loc: node.loc,
            };
            return [scrutineeAssign, guardedIf];
        }
        return [scrutineeAssign, ...inside];
    }
    /** Recurse into the body of a single match arm (without lowering the arm itself). */
    lowerMatchCase(c) {
        const lowered = this.lower([c.body]);
        const body = lowered[0];
        return { ...c, body };
    }
    /** Build an if/else chain from arms with patterns/guards/literals. */
    buildIfChainFromArms(cases, scrutinee, loc) {
        const arms = cases.filter((c) => c.type === "matchBlockCase");
        return this.foldArms(arms, scrutinee, loc, false);
    }
    /** Build an if/else chain for `match(expr is pattern)` form (arms are guards). */
    buildIfChainFromGuardArms(cases, loc) {
        const arms = cases.filter((c) => c.type === "matchBlockCase");
        return this.foldArms(arms, null, loc, true);
    }
    /**
     * Build a nested if/else chain from match arms.
     *
     * @param scrutinee  The expression to test against. Null when in `match(... is ...)` form,
     *                   where each `caseValue` is itself the boolean guard.
     * @param guardOnly  When true, treat each `caseValue` as a boolean guard expression
     *                   (the bindings have already been extracted).
     */
    foldArms(arms, scrutinee, loc, guardOnly) {
        let result;
        for (let i = arms.length - 1; i >= 0; i--) {
            const arm = arms[i];
            const armBody = this.lower([arm.body]);
            if (arm.caseValue === "_") {
                // Default case: becomes an else-body. If we already have an `if`, attach as elseBody.
                if (result) {
                    result = { ...result, elseBody: armBody };
                }
                else {
                    // Default with no later arms — just inline body. Wrap in always-true if.
                    result = {
                        type: "ifElse",
                        condition: boolLit(true, loc),
                        thenBody: armBody,
                        loc,
                    };
                }
                continue;
            }
            // Build the condition for this arm
            let condition;
            let bindings = [];
            if (guardOnly) {
                // The caseValue IS the guard expression in match(... is ...) form
                condition = arm.caseValue;
            }
            else {
                const matchPat = arm.caseValue;
                const patCond = patternToCondition(matchPat, scrutinee);
                condition = patCond ?? boolLit(true, loc);
                bindings = this.extractBindings(matchPat, scrutinee, "const", loc);
            }
            // Apply guard if present (for non-guardOnly arms)
            if (!guardOnly && arm.guard) {
                // Guard may reference bindings, so we need bindings in scope first.
                // Strategy: emit `if (patCond) { let bindings; if (guard) { body } else <next-arm> }
                //                  else <next-arm>`
                // We fold guard into the condition with `&&` only when there are no
                // bindings AND the next arm chain doesn't depend on those bindings —
                // safe because there are no bindings to share. With bindings we must
                // build a nested if and replicate the next-arm chain in BOTH the
                // outer and inner else branches so guard failure falls through to the
                // next arm (it would otherwise exit the match entirely).
                if (bindings.length === 0) {
                    condition = makeBinOp(condition, "&&", arm.guard, loc);
                }
                else {
                    const innerIf = {
                        type: "ifElse",
                        condition: arm.guard,
                        thenBody: armBody,
                        elseBody: result ? [result] : undefined,
                        loc,
                    };
                    const thenBody = [...bindings, innerIf];
                    const next = {
                        type: "ifElse",
                        condition,
                        thenBody,
                        elseBody: result ? [result] : undefined,
                        loc,
                    };
                    result = next;
                    continue;
                }
            }
            const thenBody = [...bindings, ...armBody];
            const next = {
                type: "ifElse",
                condition,
                thenBody,
                elseBody: result ? [result] : undefined,
                loc,
            };
            result = next;
        }
        return result;
    }
    // -------------------------------------------------------------------------
    // For loop
    // -------------------------------------------------------------------------
    lowerForLoop(node) {
        if (typeof node.itemVar === "string") {
            return { ...node, body: this.lower(node.body) };
        }
        const tempItem = this.freshName("item");
        const tempRef = varRef(tempItem, node.loc);
        const bindings = this.extractBindings(node.itemVar, tempRef, "const", node.loc);
        return {
            ...node,
            itemVar: tempItem,
            body: [...bindings, ...this.lower(node.body)],
        };
    }
    // -------------------------------------------------------------------------
    // extractBindings — recursive: builds Assignment nodes from a pattern + source
    // -------------------------------------------------------------------------
    extractBindings(pattern, source, declKind, loc) {
        switch (pattern.type) {
            case "objectPattern": {
                // Compute named keys up-front for any rest pattern.
                const namedKeys = pattern.properties.flatMap((p) => p.type === "objectPatternShorthand"
                    ? [p.name]
                    : p.type === "objectPatternProperty"
                        ? [p.key]
                        : []);
                return pattern.properties.flatMap((prop) => {
                    if (prop.type === "objectPatternShorthand") {
                        return [makeAssign(prop.name, fieldAccess(source, prop.name, loc), declKind, loc)];
                    }
                    if (prop.type === "objectPatternProperty") {
                        return this.extractBindings(prop.value, fieldAccess(source, prop.key, loc), declKind, loc);
                    }
                    // restPattern
                    return [makeAssign(prop.identifier, makeObjectRestCall(source, namedKeys, loc), declKind, loc)];
                });
            }
            case "arrayPattern":
                return pattern.elements.flatMap((el, i) => {
                    if (el.type === "wildcardPattern")
                        return [];
                    if (el.type === "restPattern") {
                        return [makeAssign(el.identifier, sliceCall(source, i, loc), declKind, loc)];
                    }
                    return this.extractBindings(el, indexAccess(source, i, loc), declKind, loc);
                });
            case "variableName":
                return [makeAssign(pattern.value, source, declKind, loc)];
            case "wildcardPattern":
            case "restPattern":
                return [];
            case "resultPattern": {
                if (pattern.binding === null)
                    return [];
                const field = pattern.kind === "success" ? "value" : "error";
                return [
                    makeAssign(pattern.binding, fieldAccess(source, field, loc), declKind, loc),
                ];
            }
            default:
                // Literal — produces no binding
                return [];
        }
    }
}
// ---------------------------------------------------------------------------
// patternToCondition — boolean expression for `is` and match arms
// ---------------------------------------------------------------------------
function patternToCondition(pattern, source) {
    const checks = [];
    collectChecks(pattern, source, checks);
    if (checks.length === 0)
        return null;
    return checks.reduce((a, b) => makeBinOp(a, "&&", b, undefined));
}
function collectChecks(pattern, source, checks) {
    switch (pattern.type) {
        case "objectPattern":
            for (const prop of pattern.properties) {
                if (prop.type === "objectPatternProperty") {
                    collectChecks(prop.value, fieldAccess(source, prop.key, pattern.loc), checks);
                }
                // shorthand and rest do not produce checks (binders only)
            }
            break;
        case "arrayPattern": {
            // Length check — at least as many elements as named (excluding rest).
            const hasRest = pattern.elements.some((e) => e.type === "restPattern");
            const namedCount = pattern.elements.filter((e) => e.type !== "restPattern").length;
            const lenAccess = fieldAccess(source, "length", pattern.loc);
            const op = hasRest ? ">=" : ">=";
            checks.push(makeBinOp(lenAccess, op, numberLit(namedCount, pattern.loc), pattern.loc));
            pattern.elements.forEach((el, i) => {
                if (el.type !== "wildcardPattern" &&
                    el.type !== "restPattern" &&
                    el.type !== "variableName") {
                    collectChecks(el, indexAccess(source, i, pattern.loc), checks);
                }
            });
            break;
        }
        case "variableName":
        case "wildcardPattern":
        case "restPattern":
            // Always true (binders match anything)
            break;
        case "resultPattern":
            checks.push(resultCheckCall(pattern.kind, source, pattern.loc));
            break;
        default: {
            // Literal — equality check
            checks.push(makeBinOp(source, "==", pattern, pattern.loc));
            break;
        }
    }
}
function resultCheckCall(kind, source, loc) {
    return {
        type: "functionCall",
        functionName: kind === "success" ? "isSuccess" : "isFailure",
        arguments: [source],
        loc: loc,
    };
}
// ---------------------------------------------------------------------------
// assertNoBindersInBoolIs — compile error if binders appear in pure-boolean `is`
// ---------------------------------------------------------------------------
export class PatternLoweringError extends Error {
    loc;
    constructor(message, loc) {
        super(message);
        this.loc = loc;
        this.name = "PatternLoweringError";
    }
}
function assertNoBindersInBoolIs(pattern) {
    walkPattern(pattern, (p) => {
        const loc = "loc" in p ? p.loc : undefined;
        if (p.type === "objectPatternShorthand") {
            throw new PatternLoweringError("shorthand binder in pure-boolean `is` context has nowhere to bind; use `if (x is { ... })` to introduce variables", loc);
        }
        if (p.type === "restPattern") {
            throw new PatternLoweringError("rest binder in pure-boolean `is` context has nowhere to bind; use `if (x is { ... })` to introduce variables", loc);
        }
        if (p.type === "variableName") {
            throw new PatternLoweringError(`bare identifier binder \`${p.value}\` in pure-boolean \`is\` context has nowhere to bind; use \`if (x is pattern)\` to introduce variables`, loc);
        }
        if (p.type === "resultPattern" &&
            p.binding !== null) {
            throw new PatternLoweringError(`result pattern binder in pure-boolean \`is\` context has nowhere to bind; use \`if (x is ${p.kind}(...))\` to introduce variables`, loc);
        }
    });
}
function walkPattern(pattern, visit) {
    visit(pattern);
    if ("type" in pattern) {
        if (pattern.type === "objectPattern") {
            for (const prop of pattern.properties) {
                if (prop.type === "objectPatternProperty") {
                    walkPattern(prop, visit);
                    walkPattern(prop.value, visit);
                }
                else {
                    // shorthand or rest
                    walkPattern(prop, visit);
                }
            }
        }
        else if (pattern.type === "arrayPattern") {
            for (const el of pattern.elements) {
                walkPattern(el, visit);
            }
        }
        else if (pattern.type === "objectPatternProperty") {
            walkPattern(pattern.value, visit);
        }
    }
}
// ---------------------------------------------------------------------------
// AST factory helpers
// ---------------------------------------------------------------------------
function isExpr(v) {
    if (typeof v !== "object" || v === null || !("type" in v))
        return false;
    // Exclude non-Expression node types that may share the union with Expression
    // in some fields (e.g. Assignment.value is `Expression | MessageThread`).
    const t = v.type;
    return t !== "messageThread";
}
function varRef(name, loc) {
    return { type: "variableName", value: name, loc: loc };
}
function fieldAccess(source, key, loc) {
    return chainAccess(source, [{ kind: "property", name: key }], loc);
}
function indexAccess(source, i, loc) {
    return chainAccess(source, [{ kind: "index", index: numberLit(i, loc) }], loc);
}
function sliceCall(source, start, loc) {
    return chainAccess(source, [{ kind: "slice", start: numberLit(start, loc) }], loc);
}
function chainAccess(source, chain, loc) {
    // If source is already a ValueAccess, append to its chain.
    if (source.type === "valueAccess") {
        return { ...source, chain: [...source.chain, ...chain] };
    }
    return {
        type: "valueAccess",
        base: source,
        chain,
        loc: loc,
    };
}
/**
 * Emit a call to the synthetic `__objectRest` function. The TS builder
 * intercepts this name and emits an inline IIFE using native JS destructuring,
 * so no runtime helper is needed. The function is registered as a builtin so
 * the typechecker accepts the call.
 *
 * For `let { a, b, ...rest } = obj`, this emits:
 *   __objectRest(__tmp_1, ["a", "b"])
 * which the TS builder turns into:
 *   (({ a: __a, b: __b, ...__r }) => __r)(<resolved source>)
 */
function makeObjectRestCall(source, excludedKeys, loc) {
    const keysArray = {
        type: "agencyArray",
        items: excludedKeys.map((k) => stringLit(k, loc)),
        loc: loc,
    };
    return {
        type: "functionCall",
        functionName: "__objectRest",
        arguments: [source, keysArray],
        loc: loc,
    };
}
function stringLit(value, loc) {
    return {
        type: "string",
        segments: [{ type: "text", value }],
        loc: loc,
    };
}
function makeAssign(name, value, declKind, loc) {
    return {
        type: "assignment",
        variableName: name,
        declKind,
        value,
        loc: loc,
    };
}
function makeBinOp(left, op, right, loc) {
    return {
        type: "binOpExpression",
        operator: op,
        left,
        right,
        loc: loc,
    };
}
function numberLit(value, loc) {
    return { type: "number", value: String(value), loc: loc };
}
function boolLit(value, loc) {
    return { type: "boolean", value, loc: loc };
}
// Note: no explicit null/undefined checks are emitted. Native JS already throws
// `TypeError: Cannot read properties of null (reading 'foo')` on `__tmp.foo`,
// which is sufficient for users to diagnose destructuring failures.
