import ts from "typescript";
import assert from "assert";
import { DeclarationReflection, IntrinsicType, ParameterReflection, PredicateType, ReferenceType, ReflectionFlag, ReflectionKind, SignatureReflection, TypeParameterReflection, VarianceModifier, } from "../../models/index.js";
import { ConverterEvents } from "../converter-events.js";
import { convertDefaultValue } from "../convert-expression.js";
import { removeUndefined } from "../utils/reflections.js";
import { createSymbolId } from "./symbol-id.js";
export function createSignature(context, kind, signature, symbol, declaration) {
    assert(context.scope instanceof DeclarationReflection);
    declaration ||= signature.getDeclaration();
    const sigRef = new SignatureReflection(kind == ReflectionKind.ConstructorSignature
        ? context.scope.parent.name
        : context.scope.name, kind, context.scope);
    // This feels awful, but we need some way to tell if callable signatures on classes
    // are "static" (e.g. `Foo()`) or not (e.g. `(new Foo())()`)
    if (context.shouldBeStatic) {
        sigRef.setFlag(ReflectionFlag.Static);
    }
    if (symbol && declaration) {
        context.project.registerSymbolId(sigRef, createSymbolId(symbol, declaration));
    }
    let parentReflection = context.scope;
    if (parentReflection.kindOf(ReflectionKind.TypeLiteral) &&
        parentReflection.parent instanceof DeclarationReflection) {
        parentReflection = parentReflection.parent;
    }
    if (declaration) {
        const sigComment = context.getSignatureComment(declaration);
        if (parentReflection.comment?.discoveryId !== sigComment?.discoveryId) {
            sigRef.comment = sigComment;
            if (parentReflection.kindOf(ReflectionKind.MayContainDocuments)) {
                context.converter.processDocumentTags(sigRef, parentReflection);
            }
        }
    }
    const sigRefCtx = context.withScope(sigRef);
    sigRef.typeParameters = convertTypeParameters(sigRefCtx, sigRef, signature.typeParameters);
    const parameterSymbols = signature.thisParameter
        ? [signature.thisParameter, ...signature.parameters]
        : signature.parameters;
    sigRef.parameters = convertParameters(sigRefCtx, sigRef, parameterSymbols, declaration?.parameters);
    const predicate = context.checker.getTypePredicateOfSignature(signature);
    if (predicate) {
        sigRef.type = convertPredicate(predicate, sigRefCtx);
    }
    else if (kind == ReflectionKind.SetSignature) {
        sigRef.type = new IntrinsicType("void");
    }
    else if (declaration?.type?.kind === ts.SyntaxKind.ThisType) {
        sigRef.type = new IntrinsicType("this");
    }
    else {
        let typeNode = declaration?.type;
        if (typeNode && ts.isJSDocReturnTag(typeNode)) {
            typeNode = typeNode.typeExpression?.type;
        }
        sigRef.type = context.converter.convertType(sigRefCtx, signature.getReturnType(), typeNode);
    }
    context.registerReflection(sigRef, undefined);
    switch (kind) {
        case ReflectionKind.GetSignature:
            context.scope.getSignature = sigRef;
            break;
        case ReflectionKind.SetSignature:
            context.scope.setSignature = sigRef;
            break;
        case ReflectionKind.CallSignature:
        case ReflectionKind.ConstructorSignature:
            context.scope.signatures ??= [];
            context.scope.signatures.push(sigRef);
            break;
    }
    context.converter.trigger(ConverterEvents.CREATE_SIGNATURE, context, sigRef, declaration, signature);
}
/**
 * Special cased constructor factory for functions tagged with `@class`
 */
export function createConstructSignatureWithType(context, signature, classType) {
    assert(context.scope instanceof DeclarationReflection);
    const declaration = signature.getDeclaration();
    const sigRef = new SignatureReflection(context.scope.parent.name, ReflectionKind.ConstructorSignature, context.scope);
    const sigRefCtx = context.withScope(sigRef);
    if (declaration) {
        sigRef.comment = context.getSignatureComment(declaration);
        if (sigRef.comment?.discoveryId === context.scope.parent?.comment?.discoveryId) {
            delete sigRef.comment;
        }
    }
    const parameterSymbols = signature.thisParameter
        ? [signature.thisParameter, ...signature.parameters]
        : [...signature.parameters];
    // Prevent a `this` parameter from appearing on constructor signature
    // as TS disallows them on regular classes.
    if (parameterSymbols[0]?.name === "this") {
        parameterSymbols.shift();
    }
    sigRef.parameters = convertParameters(sigRefCtx, sigRef, parameterSymbols, declaration?.parameters);
    sigRef.parameters = convertParameters(sigRefCtx, sigRef, parameterSymbols, undefined);
    // Do NOT convert type parameters here, they go on the class itself in the @class case
    // See #2914.
    sigRef.type = ReferenceType.createResolvedReference(context.scope.parent.name, classType, context.project);
    context.registerReflection(sigRef, undefined);
    context.scope.signatures ??= [];
    context.scope.signatures.push(sigRef);
    context.converter.trigger(ConverterEvents.CREATE_SIGNATURE, context, sigRef, declaration, signature);
}
function convertParameters(context, sigRef, parameters, parameterNodes) {
    // #2698 if `satisfies` is used to imply a this parameter, we might have
    // more parameters than parameter nodes and need to shift the parameterNode
    // access index. Very ugly, but it does the job.
    const parameterNodeOffset = parameterNodes?.length !== parameters.length ? -1 : 0;
    return parameters.map((param, i) => {
        const declaration = param.valueDeclaration;
        assert(!declaration ||
            ts.isParameter(declaration) ||
            ts.isJSDocParameterTag(declaration));
        const paramRefl = new ParameterReflection(/__\d+/.test(param.name) ? "__namedParameters" : param.name, ReflectionKind.Parameter, sigRef);
        if (declaration && ts.isJSDocParameterTag(declaration)) {
            paramRefl.comment = context.getJsDocComment(declaration);
        }
        paramRefl.comment ||= context.getComment(param, paramRefl.kind);
        context.registerReflection(paramRefl, param);
        context.converter.trigger(ConverterEvents.CREATE_PARAMETER, context, paramRefl);
        let type;
        let typeNode;
        if (declaration) {
            type = context.checker.getTypeOfSymbolAtLocation(param, declaration);
            if (ts.isParameter(declaration)) {
                typeNode = declaration.type;
            }
            else {
                typeNode = declaration.typeExpression?.type;
            }
        }
        else {
            type = param.type;
        }
        if (declaration &&
            ts.isParameter(declaration) &&
            declaration.type?.kind === ts.SyntaxKind.ThisType) {
            paramRefl.type = new IntrinsicType("this");
        }
        else if (!type) {
            paramRefl.type = new IntrinsicType("any");
        }
        else {
            paramRefl.type = context.converter.convertType(context.withScope(paramRefl), type, typeNode);
        }
        let isOptional = false;
        if (declaration) {
            isOptional = ts.isParameter(declaration)
                ? !!declaration.questionToken ||
                    ts
                        .getJSDocParameterTags(declaration)
                        .some((tag) => tag.isBracketed)
                : declaration.isBracketed;
        }
        if (isOptional) {
            paramRefl.type = removeUndefined(paramRefl.type);
        }
        paramRefl.defaultValue = convertDefaultValue(parameterNodes?.[i + parameterNodeOffset]);
        paramRefl.setFlag(ReflectionFlag.Optional, isOptional);
        // If we have no declaration, then this is an implicitly defined parameter in JS land
        // because the method body uses `arguments`... which is always a rest argument
        let isRest = true;
        if (declaration) {
            isRest = ts.isParameter(declaration)
                ? !!declaration.dotDotDotToken
                : !!declaration.typeExpression &&
                    ts.isJSDocVariadicType(declaration.typeExpression.type);
        }
        paramRefl.setFlag(ReflectionFlag.Rest, isRest);
        checkForDestructuredParameterDefaults(paramRefl, parameterNodes?.[i + parameterNodeOffset]);
        return paramRefl;
    });
}
export function convertParameterNodes(context, sigRef, parameters) {
    return parameters.map((param) => {
        const paramRefl = new ParameterReflection(/__\d+/.test(param.name.getText())
            ? "__namedParameters"
            : param.name.getText(), ReflectionKind.Parameter, sigRef);
        if (ts.isJSDocParameterTag(param)) {
            paramRefl.comment = context.getJsDocComment(param);
        }
        context.registerReflection(paramRefl, context.getSymbolAtLocation(param));
        context.converter.trigger(ConverterEvents.CREATE_PARAMETER, context, paramRefl);
        paramRefl.type = context.converter.convertType(context.withScope(paramRefl), ts.isParameter(param) ? param.type : param.typeExpression?.type);
        const isOptional = ts.isParameter(param)
            ? !!param.questionToken
            : param.isBracketed;
        if (isOptional) {
            paramRefl.type = removeUndefined(paramRefl.type);
        }
        paramRefl.defaultValue = convertDefaultValue(param);
        paramRefl.setFlag(ReflectionFlag.Optional, isOptional);
        paramRefl.setFlag(ReflectionFlag.Rest, ts.isParameter(param)
            ? !!param.dotDotDotToken
            : !!param.typeExpression &&
                ts.isJSDocVariadicType(param.typeExpression.type));
        checkForDestructuredParameterDefaults(paramRefl, param);
        return paramRefl;
    });
}
function checkForDestructuredParameterDefaults(param, decl) {
    if (!decl || !ts.isParameter(decl))
        return;
    if (param.name !== "__namedParameters")
        return;
    if (!ts.isObjectBindingPattern(decl.name))
        return;
    if (param.type?.type !== "reflection")
        return;
    for (const child of param.type.declaration.children || []) {
        const tsChild = decl.name.elements.find((el) => (el.propertyName || el.name).getText() === child.name);
        if (tsChild) {
            child.defaultValue = convertDefaultValue(tsChild);
        }
    }
}
export function convertTypeParameters(context, parent, parameters) {
    return parameters?.map((param) => {
        const constraintT = param.getConstraint();
        const defaultT = param.getDefault();
        // There's no way to determine directly from a ts.TypeParameter what it's variance modifiers are
        // so unfortunately we have to go back to the node for this...
        const declaration = param
            .getSymbol()
            ?.declarations?.find(ts.isTypeParameterDeclaration);
        const variance = getVariance(declaration?.modifiers);
        const paramRefl = new TypeParameterReflection(param.symbol.name, parent, variance);
        const paramCtx = context.withScope(paramRefl);
        paramRefl.type = constraintT
            ? context.converter.convertType(paramCtx, constraintT)
            : void 0;
        paramRefl.default = defaultT
            ? context.converter.convertType(paramCtx, defaultT)
            : void 0;
        // No way to determine this from the type parameter itself, need to go back to the declaration
        if (declaration?.modifiers?.some((m) => m.kind === ts.SyntaxKind.ConstKeyword)) {
            paramRefl.flags.setFlag(ReflectionFlag.Const, true);
        }
        context.registerReflection(paramRefl, param.getSymbol());
        context.converter.trigger(ConverterEvents.CREATE_TYPE_PARAMETER, context, paramRefl);
        return paramRefl;
    });
}
export function convertTypeParameterNodes(context, parameters) {
    return parameters?.map((param) => createTypeParamReflection(param, context));
}
export function createTypeParamReflection(param, context) {
    const paramRefl = new TypeParameterReflection(param.name.text, context.scope, getVariance(param.modifiers));
    const paramScope = context.withScope(paramRefl);
    if (ts.isJSDocTemplateTag(param.parent)) {
        // With a @template tag, the constraint applies only to the
        // first type parameter declared.
        if (param.parent.typeParameters[0].name.text === param.name.text && param.parent.constraint) {
            paramRefl.type = context.converter.convertType(paramScope, param.parent.constraint);
        }
    }
    else {
        paramRefl.type = param.constraint
            ? context.converter.convertType(paramScope, param.constraint)
            : void 0;
    }
    paramRefl.default = param.default
        ? context.converter.convertType(paramScope, param.default)
        : void 0;
    if (param.modifiers?.some((m) => m.kind === ts.SyntaxKind.ConstKeyword)) {
        paramRefl.flags.setFlag(ReflectionFlag.Const, true);
    }
    context.registerReflection(paramRefl, param.symbol);
    if (ts.isJSDocTemplateTag(param.parent)) {
        paramRefl.comment = context.getJsDocComment(param.parent);
    }
    context.converter.trigger(ConverterEvents.CREATE_TYPE_PARAMETER, context, paramRefl, param);
    return paramRefl;
}
export function convertTemplateParameterNodes(context, nodes) {
    return nodes?.flatMap((node) => {
        return node.typeParameters.map((param, index) => {
            const paramRefl = new TypeParameterReflection(param.name.text, context.scope, getVariance(param.modifiers));
            const paramScope = context.withScope(paramRefl);
            paramRefl.type = index || !node.constraint
                ? void 0
                : context.converter.convertType(paramScope, node.constraint.type);
            paramRefl.default = param.default
                ? context.converter.convertType(paramScope, param.default)
                : void 0;
            if (param.modifiers?.some((m) => m.kind === ts.SyntaxKind.ConstKeyword)) {
                paramRefl.flags.setFlag(ReflectionFlag.Const, true);
            }
            context.registerReflection(paramRefl, param.symbol);
            if (ts.isJSDocTemplateTag(param.parent)) {
                paramRefl.comment = context.getJsDocComment(param.parent);
            }
            context.converter.trigger(ConverterEvents.CREATE_TYPE_PARAMETER, context, paramRefl, param);
            return paramRefl;
        });
    });
}
function getVariance(modifiers) {
    const hasIn = modifiers?.some((mod) => mod.kind === ts.SyntaxKind.InKeyword);
    const hasOut = modifiers?.some((mod) => mod.kind === ts.SyntaxKind.OutKeyword);
    if (hasIn && hasOut) {
        return VarianceModifier.inOut;
    }
    if (hasIn) {
        return VarianceModifier.in;
    }
    if (hasOut) {
        return VarianceModifier.out;
    }
}
function convertPredicate(predicate, context) {
    let name;
    switch (predicate.kind) {
        case ts.TypePredicateKind.This:
        case ts.TypePredicateKind.AssertsThis:
            name = "this";
            break;
        case ts.TypePredicateKind.Identifier:
        case ts.TypePredicateKind.AssertsIdentifier:
            name = predicate.parameterName;
            break;
    }
    let asserts;
    switch (predicate.kind) {
        case ts.TypePredicateKind.This:
        case ts.TypePredicateKind.Identifier:
            asserts = false;
            break;
        case ts.TypePredicateKind.AssertsThis:
        case ts.TypePredicateKind.AssertsIdentifier:
            asserts = true;
            break;
    }
    return new PredicateType(name, asserts, predicate.type
        ? context.converter.convertType(context, predicate.type)
        : void 0);
}
