/**
 * Truthy/falsy narrowing resolution and sequential narrowing collection.
 *
 * Contains tryResolveTruthyNarrowing, tryResolveFalsyNarrowing,
 * collectTypeNarrowingsInTruthyExpr, collectTypeNarrowingsInFalsyExpr,
 * and collectSequentialNarrowings.
 */
import * as ts from "typescript";
import { normalizedUnionType } from "../types/type-ops.js";
import { narrowTypeByArrayShape } from "./array-type-guards.js";
import { narrowTypeByAssignableTarget } from "./reference-type-guards.js";
import { getAccessPathTarget, getCurrentTypeForAccessPath, } from "./access-paths.js";
import { collectNarrowingCandidateLeaves } from "./narrowing-candidates.js";
import { unwrapExpr, getStringLiteralText, makeTypeNarrowing, extractArrayIsArrayTarget, tryResolveCallPredicateNarrowing, extractIdentifierPropertyAccess, narrowTypeByTypeofTag, tryResolveTypeofNarrowing, tryResolveEqualityLiteralNarrowing, resolveInstanceofTargetType, } from "./narrowing-resolvers.js";
import { withAppliedNarrowings } from "./narrowing-environment.js";
import { getCurrentTypeForDecl, narrowTypeByPropertyPresence, narrowTypeByPropertyTruthiness, } from "./narrowing-property-helpers.js";
const KNOWN_VALUE_LIKE_REFERENCE_NAMES = new Set([
    "sbyte",
    "byte",
    "short",
    "ushort",
    "int",
    "uint",
    "long",
    "ulong",
    "nint",
    "nuint",
    "half",
    "float",
    "double",
    "decimal",
    "bool",
    "boolean",
    "char",
    "Int128",
    "UInt128",
    "SByte",
    "Byte",
    "Int16",
    "UInt16",
    "Int32",
    "UInt32",
    "Int64",
    "UInt64",
    "Half",
    "Single",
    "Double",
    "Decimal",
    "Boolean",
    "Char",
]);
const isNullishCandidate = (type) => type.kind === "primitiveType" &&
    (type.name === "null" || type.name === "undefined");
const isDeterministicallyTruthyCandidate = (type) => {
    switch (type.kind) {
        case "objectType":
        case "arrayType":
        case "tupleType":
        case "dictionaryType":
        case "functionType":
            return true;
        case "referenceType":
            return !KNOWN_VALUE_LIKE_REFERENCE_NAMES.has(type.name);
        case "literalType":
            if (typeof type.value === "string") {
                return type.value.length > 0;
            }
            if (typeof type.value === "boolean") {
                return type.value;
            }
            if (typeof type.value === "number") {
                return type.value !== 0 && !Number.isNaN(type.value);
            }
            return false;
        default:
            return false;
    }
};
const buildCandidateUnion = (candidates) => {
    if (candidates.length === 0) {
        return undefined;
    }
    if (candidates.length === 1) {
        return candidates[0];
    }
    return normalizedUnionType(candidates);
};
const narrowTypeByDirectTruthiness = (currentType, wantTruthy, ctx) => {
    if (!currentType) {
        return undefined;
    }
    const candidates = collectNarrowingCandidateLeaves(ctx.typeSystem, currentType).filter((candidate) => candidate !== undefined);
    if (candidates.length === 0) {
        return undefined;
    }
    const nullishCandidates = candidates.filter((candidate) => isNullishCandidate(candidate));
    if (nullishCandidates.length === 0) {
        return undefined;
    }
    const nonNullishCandidates = candidates.filter((candidate) => !isNullishCandidate(candidate));
    if (nonNullishCandidates.length === 0 ||
        nonNullishCandidates.some((candidate) => !isDeterministicallyTruthyCandidate(candidate))) {
        return undefined;
    }
    return wantTruthy
        ? buildCandidateUnion(nonNullishCandidates)
        : buildCandidateUnion(nullishCandidates);
};
const tryResolveTruthyNarrowing = (expr, ctx) => {
    const unwrapped = unwrapExpr(expr);
    const directTarget = getAccessPathTarget(unwrapped, ctx);
    if (directTarget) {
        const targetType = narrowTypeByDirectTruthiness(getCurrentTypeForAccessPath(directTarget, ctx), true, ctx);
        if (targetType) {
            return makeTypeNarrowing(directTarget, targetType);
        }
    }
    const arrayTarget = extractArrayIsArrayTarget(unwrapped, ctx);
    if (arrayTarget) {
        const targetType = narrowTypeByArrayShape(ctx.typeSystem, getCurrentTypeForAccessPath(arrayTarget, ctx), true);
        if (targetType) {
            return makeTypeNarrowing(arrayTarget, targetType);
        }
    }
    const propertyAccess = extractIdentifierPropertyAccess(unwrapped, ctx);
    if (propertyAccess) {
        const currentType = getCurrentTypeForDecl(propertyAccess.declId, ctx);
        const targetType = narrowTypeByPropertyTruthiness(currentType, propertyAccess.propertyName, true, ctx);
        if (targetType) {
            return {
                kind: "decl",
                declId: propertyAccess.declId.id,
                targetType,
            };
        }
    }
    if (ts.isBinaryExpression(unwrapped) &&
        unwrapped.operatorToken.kind === ts.SyntaxKind.InKeyword) {
        const propertyName = getStringLiteralText(unwrapped.left);
        const object = unwrapExpr(unwrapped.right);
        if (propertyName && ts.isIdentifier(object)) {
            const declId = ctx.binding.resolveIdentifier(object);
            if (declId) {
                const targetType = narrowTypeByPropertyPresence(getCurrentTypeForDecl(declId, ctx), propertyName, true, ctx);
                if (targetType) {
                    return { kind: "decl", declId: declId.id, targetType };
                }
            }
        }
    }
    // istype<T>(x)
    if (ts.isCallExpression(unwrapped) &&
        ts.isIdentifier(unwrapped.expression) &&
        unwrapped.expression.text === "istype" &&
        unwrapped.typeArguments &&
        unwrapped.typeArguments.length === 1 &&
        unwrapped.arguments.length === 1) {
        const typeArg = unwrapped.typeArguments[0];
        const rawArg = unwrapped.arguments[0];
        if (!typeArg || !rawArg)
            return undefined;
        const valueArg = unwrapExpr(rawArg);
        if (!ts.isIdentifier(valueArg))
            return undefined;
        const targetType = ctx.typeSystem.typeFromSyntax(ctx.binding.captureTypeSyntax(typeArg));
        if (targetType.kind === "unknownType")
            return undefined;
        const narrowedDeclId = ctx.binding.resolveIdentifier(valueArg);
        if (!narrowedDeclId)
            return undefined;
        return { kind: "decl", declId: narrowedDeclId.id, targetType };
    }
    // typeof x === "string" / "number" / "boolean" / "undefined"
    if (ts.isBinaryExpression(unwrapped) &&
        (unwrapped.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
            unwrapped.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken)) {
        const left = unwrapExpr(unwrapped.left);
        const right = unwrapExpr(unwrapped.right);
        const leftLiteral = getStringLiteralText(left);
        const rightLiteral = getStringLiteralText(right);
        const extractTypeofTarget = (expr) => {
            if (!ts.isTypeOfExpression(expr))
                return undefined;
            return getAccessPathTarget(expr.expression, ctx);
        };
        const leftTypeofTarget = extractTypeofTarget(left);
        const rightTypeofTarget = extractTypeofTarget(right);
        const tag = leftTypeofTarget && rightLiteral
            ? rightLiteral
            : rightTypeofTarget && leftLiteral
                ? leftLiteral
                : undefined;
        const narrowedTarget = leftTypeofTarget ?? rightTypeofTarget;
        if (!tag || !narrowedTarget)
            return undefined;
        const currentType = getCurrentTypeForAccessPath(narrowedTarget, ctx);
        const targetType = narrowTypeByTypeofTag(currentType, tag, ctx);
        if (!targetType)
            return undefined;
        return makeTypeNarrowing(narrowedTarget, targetType);
    }
    // x instanceof T
    if (!ts.isBinaryExpression(unwrapped))
        return undefined;
    if (unwrapped.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword)
        return undefined;
    const narrowedTarget = getAccessPathTarget(unwrapped.left, ctx);
    if (!narrowedTarget)
        return undefined;
    const targetType = resolveInstanceofTargetType(unwrapped.right, ctx);
    if (!targetType)
        return undefined;
    const narrowedType = narrowTypeByAssignableTarget({
        collectNarrowingCandidates: (type) => ctx.typeSystem.collectNarrowingCandidates(type),
        isAssignableTo: (source, target) => ctx.typeSystem.matchesInstanceofTarget(source, target),
    }, getCurrentTypeForAccessPath(narrowedTarget, ctx), targetType, true);
    if (!narrowedType)
        return undefined;
    return makeTypeNarrowing(narrowedTarget, narrowedType);
};
/**
 * Collect `instanceof` narrowings that are guaranteed to hold when `expr` is truthy.
 *
 * We only collect from conjunctions (&&) because truthiness of `A && B` implies both
 * `A` and `B` are truthy; for `A || B` it does not guarantee either side.
 */
export const collectTypeNarrowingsInTruthyExpr = (expr, ctx) => {
    const unwrapped = unwrapExpr(expr);
    if (ts.isPrefixUnaryExpression(unwrapped) &&
        unwrapped.operator === ts.SyntaxKind.ExclamationToken) {
        return collectTypeNarrowingsInFalsyExpr(unwrapped.operand, ctx);
    }
    const direct = tryResolveTruthyNarrowing(unwrapped, ctx);
    if (direct)
        return [direct];
    const predicateNarrowing = tryResolveCallPredicateNarrowing(unwrapped, ctx, true);
    if (predicateNarrowing)
        return [predicateNarrowing];
    const typeofNarrowing = tryResolveTypeofNarrowing(unwrapped, ctx, true);
    if (typeofNarrowing)
        return [typeofNarrowing];
    const equalityNarrowing = tryResolveEqualityLiteralNarrowing(unwrapped, ctx, true);
    if (equalityNarrowing)
        return [equalityNarrowing];
    if (ts.isBinaryExpression(unwrapped)) {
        if (unwrapped.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
            return collectSequentialNarrowings(ctx, [
                (phaseCtx) => collectTypeNarrowingsInTruthyExpr(unwrapped.left, phaseCtx),
                (phaseCtx) => collectTypeNarrowingsInTruthyExpr(unwrapped.right, phaseCtx),
            ]);
        }
    }
    return [];
};
const tryResolveFalsyNarrowing = (expr, ctx) => {
    const unwrapped = unwrapExpr(expr);
    const directTarget = getAccessPathTarget(unwrapped, ctx);
    if (directTarget) {
        const targetType = narrowTypeByDirectTruthiness(getCurrentTypeForAccessPath(directTarget, ctx), false, ctx);
        if (targetType) {
            return makeTypeNarrowing(directTarget, targetType);
        }
    }
    const arrayTarget = extractArrayIsArrayTarget(unwrapped, ctx);
    if (arrayTarget) {
        const targetType = narrowTypeByArrayShape(ctx.typeSystem, getCurrentTypeForAccessPath(arrayTarget, ctx), false);
        if (targetType) {
            return makeTypeNarrowing(arrayTarget, targetType);
        }
    }
    const propertyAccess = extractIdentifierPropertyAccess(unwrapped, ctx);
    if (propertyAccess) {
        const currentType = getCurrentTypeForDecl(propertyAccess.declId, ctx);
        const targetType = narrowTypeByPropertyTruthiness(currentType, propertyAccess.propertyName, false, ctx);
        if (targetType) {
            return {
                kind: "decl",
                declId: propertyAccess.declId.id,
                targetType,
            };
        }
    }
    if (ts.isBinaryExpression(unwrapped) &&
        unwrapped.operatorToken.kind === ts.SyntaxKind.InKeyword) {
        const propertyName = getStringLiteralText(unwrapped.left);
        const object = unwrapExpr(unwrapped.right);
        if (propertyName && ts.isIdentifier(object)) {
            const declId = ctx.binding.resolveIdentifier(object);
            if (declId) {
                const targetType = narrowTypeByPropertyPresence(getCurrentTypeForDecl(declId, ctx), propertyName, false, ctx);
                if (targetType) {
                    return { kind: "decl", declId: declId.id, targetType };
                }
            }
        }
    }
    if (ts.isBinaryExpression(unwrapped) &&
        unwrapped.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
        const narrowedTarget = getAccessPathTarget(unwrapped.left, ctx);
        if (!narrowedTarget)
            return undefined;
        const targetType = resolveInstanceofTargetType(unwrapped.right, ctx);
        if (!targetType)
            return undefined;
        const narrowedType = narrowTypeByAssignableTarget({
            collectNarrowingCandidates: (type) => ctx.typeSystem.collectNarrowingCandidates(type),
            isAssignableTo: (source, target) => ctx.typeSystem.matchesInstanceofTarget(source, target),
        }, getCurrentTypeForAccessPath(narrowedTarget, ctx), targetType, false);
        if (!narrowedType)
            return undefined;
        return makeTypeNarrowing(narrowedTarget, narrowedType);
    }
    return undefined;
};
export const collectTypeNarrowingsInFalsyExpr = (expr, ctx) => {
    const unwrapped = unwrapExpr(expr);
    if (ts.isPrefixUnaryExpression(unwrapped) &&
        unwrapped.operator === ts.SyntaxKind.ExclamationToken) {
        return collectTypeNarrowingsInTruthyExpr(unwrapped.operand, ctx);
    }
    if (ts.isBinaryExpression(unwrapped) &&
        unwrapped.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
        return collectSequentialNarrowings(ctx, [
            (phaseCtx) => collectTypeNarrowingsInFalsyExpr(unwrapped.left, phaseCtx),
            (phaseCtx) => collectTypeNarrowingsInFalsyExpr(unwrapped.right, phaseCtx),
        ]);
    }
    const direct = tryResolveFalsyNarrowing(unwrapped, ctx);
    if (direct)
        return [direct];
    const predicateNarrowing = tryResolveCallPredicateNarrowing(unwrapped, ctx, false);
    if (predicateNarrowing)
        return [predicateNarrowing];
    const typeofNarrowing = tryResolveTypeofNarrowing(unwrapped, ctx, false);
    if (typeofNarrowing)
        return [typeofNarrowing];
    const equalityNarrowing = tryResolveEqualityLiteralNarrowing(unwrapped, ctx, false);
    if (equalityNarrowing)
        return [equalityNarrowing];
    return [];
};
const collectSequentialNarrowings = (ctx, phases) => {
    const combined = [];
    let currentCtx = ctx;
    for (const collect of phases) {
        const next = collect(currentCtx);
        if (next.length === 0) {
            continue;
        }
        combined.push(...next);
        currentCtx = withAppliedNarrowings(currentCtx, next);
    }
    return combined;
};
//# sourceMappingURL=narrowing-truthy.js.map