/* @internal */
namespace ts.codefix {
    const fixId = "inferFromUsage";
    const errorCodes = [
        // Variable declarations
        Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code,

        // Variable uses
        Diagnostics.Variable_0_implicitly_has_an_1_type.code,

        // Parameter declarations
        Diagnostics.Parameter_0_implicitly_has_an_1_type.code,
        Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code,

        // Get Accessor declarations
        Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code,
        Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code,

        // Set Accessor declarations
        Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code,

        // Property declarations
        Diagnostics.Member_0_implicitly_has_an_1_type.code,

        //// Suggestions
        // Variable declarations
        Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_but_a_better_type_may_be_inferred_from_usage.code,

        // Variable uses
        Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code,

        // Parameter declarations
        Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code,
        Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code,

        // Get Accessor declarations
        Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_get_accessor_may_be_inferred_from_usage.code,
        Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage.code,

        // Set Accessor declarations
        Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_set_accessor_may_be_inferred_from_usage.code,

        // Property declarations
        Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code,

        // Function expressions and declarations
        Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code,
    ];
    registerCodeFix({
        errorCodes,
        getCodeActions(context) {
            const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, preferences } = context;

            const token = getTokenAtPosition(sourceFile, start);
            let declaration: Declaration | undefined;
            const changes = textChanges.ChangeTracker.with(context, changes => {
                declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, preferences);
            });
            const name = declaration && getNameOfDeclaration(declaration);
            return !name || changes.length === 0 ? undefined
                : [createCodeFixAction(fixId, changes, [getDiagnostic(errorCode, token), getTextOfNode(name)], fixId, Diagnostics.Infer_all_types_from_usage)];
        },
        fixIds: [fixId],
        getAllCodeActions(context) {
            const { sourceFile, program, cancellationToken, host, preferences } = context;
            const markSeen = nodeSeenTracker();
            return codeFixAll(context, errorCodes, (changes, err) => {
                doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, preferences);
            });
        },
    });

    function getDiagnostic(errorCode: number, token: Node): DiagnosticMessage {
        switch (errorCode) {
            case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
            case Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code:
                return isSetAccessorDeclaration(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217
            case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code:
            case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Infer_parameter_types_from_usage;
            case Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code:
                return Diagnostics.Infer_this_type_of_0_from_usage;
            default:
                return Diagnostics.Infer_type_of_0_from_usage;
        }
    }

    /** Map suggestion code to error code */
    function mapSuggestionDiagnostic(errorCode: number) {
        switch (errorCode) {
            case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code;
            case Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Variable_0_implicitly_has_an_1_type.code;
            case Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Parameter_0_implicitly_has_an_1_type.code;
            case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code;
            case Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_get_accessor_may_be_inferred_from_usage.code:
                return Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code;
            case Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code;
            case Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_set_accessor_may_be_inferred_from_usage.code:
                return Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code;
            case Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code:
                return Diagnostics.Member_0_implicitly_has_an_1_type.code;
        }
        return errorCode;
    }

    function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost, preferences: UserPreferences): Declaration | undefined {
        if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) {
            return undefined;
        }

        const { parent } = token;
        const importAdder = createImportAdder(sourceFile, program, preferences, host);
        errorCode = mapSuggestionDiagnostic(errorCode);
        switch (errorCode) {
            // Variable and Property declarations
            case Diagnostics.Member_0_implicitly_has_an_1_type.code:
            case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code:
                if ((isVariableDeclaration(parent) && markSeen(parent)) || isPropertyDeclaration(parent) || isPropertySignature(parent)) { // handle bad location
                    annotateVariableDeclaration(changes, importAdder, sourceFile, parent, program, host, cancellationToken);
                    importAdder.writeFixes(changes);
                    return parent;
                }
                if (isPropertyAccessExpression(parent)) {
                    const type = inferTypeForVariableFromUsage(parent.name, program, cancellationToken);
                    const typeNode = getTypeNodeIfAccessible(type, parent, program, host);
                    if (typeNode) {
                        // Note that the codefix will never fire with an existing `@type` tag, so there is no need to merge tags
                        const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode), /*comment*/ undefined);
                        changes.addJSDocTags(sourceFile, cast(parent.parent.parent, isExpressionStatement), [typeTag]);
                    }
                    importAdder.writeFixes(changes);
                    return parent;
                }
                return undefined;

            case Diagnostics.Variable_0_implicitly_has_an_1_type.code: {
                const symbol = program.getTypeChecker().getSymbolAtLocation(token);
                if (symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && markSeen(symbol.valueDeclaration)) {
                    annotateVariableDeclaration(changes, importAdder, getSourceFileOfNode(symbol.valueDeclaration), symbol.valueDeclaration, program, host, cancellationToken);
                    importAdder.writeFixes(changes);
                    return symbol.valueDeclaration;
                }
                return undefined;
            }
        }

        const containingFunction = getContainingFunction(token);
        if (containingFunction === undefined) {
            return undefined;
        }

        let declaration: Declaration | undefined;
        switch (errorCode) {
            // Parameter declarations
            case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
                if (isSetAccessorDeclaration(containingFunction)) {
                    annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken);
                    declaration = containingFunction;
                    break;
                }
                // falls through
            case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code:
                if (markSeen(containingFunction)) {
                    const param = cast(parent, isParameter);
                    annotateParameters(changes, importAdder, sourceFile, param, containingFunction, program, host, cancellationToken);
                    declaration = param;
                }
                break;

            // Get Accessor declarations
            case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code:
            case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code:
                if (isGetAccessorDeclaration(containingFunction) && isIdentifier(containingFunction.name)) {
                    annotate(changes, importAdder, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host);
                    declaration = containingFunction;
                }
                break;

            // Set Accessor declarations
            case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code:
                if (isSetAccessorDeclaration(containingFunction)) {
                    annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken);
                    declaration = containingFunction;
                }
                break;

            // Function 'this'
            case Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code:
                if (textChanges.isThisTypeAnnotatable(containingFunction) && markSeen(containingFunction)) {
                    annotateThis(changes, sourceFile, containingFunction, program, host, cancellationToken);
                    declaration = containingFunction;
                }
                break;

            default:
                return Debug.fail(String(errorCode));
        }

        importAdder.writeFixes(changes);
        return declaration;
    }

    function annotateVariableDeclaration(
        changes: textChanges.ChangeTracker,
        importAdder: ImportAdder,
        sourceFile: SourceFile,
        declaration: VariableDeclaration | PropertyDeclaration | PropertySignature,
        program: Program,
        host: LanguageServiceHost,
        cancellationToken: CancellationToken,
    ): void {
        if (isIdentifier(declaration.name)) {
            annotate(changes, importAdder, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host);
        }
    }

    function annotateParameters(
        changes: textChanges.ChangeTracker,
        importAdder: ImportAdder,
        sourceFile: SourceFile,
        parameterDeclaration: ParameterDeclaration,
        containingFunction: SignatureDeclaration,
        program: Program,
        host: LanguageServiceHost,
        cancellationToken: CancellationToken,
    ): void {
        if (!isIdentifier(parameterDeclaration.name)) {
            return;
        }

        const parameterInferences = inferTypeForParametersFromUsage(containingFunction, sourceFile, program, cancellationToken);
        Debug.assert(containingFunction.parameters.length === parameterInferences.length, "Parameter count and inference count should match");

        if (isInJSFile(containingFunction)) {
            annotateJSDocParameters(changes, sourceFile, parameterInferences, program, host);
        }
        else {
            const needParens = isArrowFunction(containingFunction) && !findChildOfKind(containingFunction, SyntaxKind.OpenParenToken, sourceFile);
            if (needParens) changes.insertNodeBefore(sourceFile, first(containingFunction.parameters), factory.createToken(SyntaxKind.OpenParenToken));
            for (const { declaration, type } of parameterInferences) {
                if (declaration && !declaration.type && !declaration.initializer) {
                    annotate(changes, importAdder, sourceFile, declaration, type, program, host);
                }
            }
            if (needParens) changes.insertNodeAfter(sourceFile, last(containingFunction.parameters), factory.createToken(SyntaxKind.CloseParenToken));
        }
    }

    function annotateThis(changes: textChanges.ChangeTracker, sourceFile: SourceFile, containingFunction: textChanges.ThisTypeAnnotatable, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken) {
        const references = getFunctionReferences(containingFunction, sourceFile, program, cancellationToken);
        if (!references || !references.length) {
            return;
        }
        const thisInference = inferTypeFromReferences(program, references, cancellationToken).thisParameter();
        const typeNode = getTypeNodeIfAccessible(thisInference, containingFunction, program, host);
        if (!typeNode) {
            return;
        }

        if (isInJSFile(containingFunction)) {
            annotateJSDocThis(changes, sourceFile, containingFunction, typeNode);
        }
        else {
            changes.tryInsertThisTypeAnnotation(sourceFile, containingFunction, typeNode);
        }
    }

    function annotateJSDocThis(changes: textChanges.ChangeTracker, sourceFile: SourceFile, containingFunction: SignatureDeclaration, typeNode: TypeNode) {
        changes.addJSDocTags(sourceFile, containingFunction, [
            factory.createJSDocThisTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode)),
        ]);
    }

    function annotateSetAccessor(
        changes: textChanges.ChangeTracker,
        importAdder: ImportAdder,
        sourceFile: SourceFile,
        setAccessorDeclaration: SetAccessorDeclaration,
        program: Program,
        host: LanguageServiceHost,
        cancellationToken: CancellationToken,

    ): void {
        const param = firstOrUndefined(setAccessorDeclaration.parameters);
        if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) {
            let type = inferTypeForVariableFromUsage(setAccessorDeclaration.name, program, cancellationToken);
            if (type === program.getTypeChecker().getAnyType()) {
                type = inferTypeForVariableFromUsage(param.name, program, cancellationToken);
            }
            if (isInJSFile(setAccessorDeclaration)) {
                annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host);
            }
            else {
                annotate(changes, importAdder, sourceFile, param, type, program, host);
            }
        }
    }

    function annotate(changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost): void {
        const typeNode = getTypeNodeIfAccessible(type, declaration, program, host);
        if (typeNode) {
            if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) {
                const parent = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : declaration;
                if (!parent) {
                    return;
                }
                const typeExpression = factory.createJSDocTypeExpression(typeNode);
                const typeTag = isGetAccessorDeclaration(declaration) ? factory.createJSDocReturnTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined) : factory.createJSDocTypeTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined);
                changes.addJSDocTags(sourceFile, parent, [typeTag]);
            }
            else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, declaration, sourceFile, changes, importAdder, getEmitScriptTarget(program.getCompilerOptions()))) {
                changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode);
            }
        }
    }

    function tryReplaceImportTypeNodeWithAutoImport(
        typeNode: TypeNode,
        declaration: textChanges.TypeAnnotatable,
        sourceFile: SourceFile,
        changes: textChanges.ChangeTracker,
        importAdder: ImportAdder,
        scriptTarget: ScriptTarget
    ): boolean {
        const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget);
        if (importableReference && changes.tryInsertTypeAnnotation(sourceFile, declaration, importableReference.typeNode)) {
            forEach(importableReference.symbols, s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true));
            return true;
        }
        return false;
    }

    function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: readonly ParameterInference[], program: Program, host: LanguageServiceHost): void {
        const signature = parameterInferences.length && parameterInferences[0].declaration.parent;
        if (!signature) {
            return;
        }

        const inferences = mapDefined(parameterInferences, inference => {
            const param = inference.declaration;
            // only infer parameters that have (1) no type and (2) an accessible inferred type
            if (param.initializer || getJSDocType(param) || !isIdentifier(param.name)) {
                return;
            }
            const typeNode = inference.type && getTypeNodeIfAccessible(inference.type, param, program, host);
            if (typeNode) {
                const name = factory.cloneNode(param.name);
                setEmitFlags(name, EmitFlags.NoComments | EmitFlags.NoNestedComments);
                return { name: factory.cloneNode(param.name), param, isOptional: !!inference.isOptional, typeNode };
            }
        });

        if (!inferences.length) {
            return;
        }

        if (isArrowFunction(signature) || isFunctionExpression(signature)) {
            const needParens = isArrowFunction(signature) && !findChildOfKind(signature, SyntaxKind.OpenParenToken, sourceFile);
            if (needParens) {
                changes.insertNodeBefore(sourceFile, first(signature.parameters), factory.createToken(SyntaxKind.OpenParenToken));
            }

            forEach(inferences, ({ typeNode, param }) => {
                const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode));
                const jsDoc = factory.createJSDocComment(/*comment*/ undefined, [typeTag]);
                changes.insertNodeAt(sourceFile, param.getStart(sourceFile), jsDoc, { suffix: " " });
            });

            if (needParens) {
                changes.insertNodeAfter(sourceFile, last(signature.parameters), factory.createToken(SyntaxKind.CloseParenToken));
            }
        }
        else {
            const paramTags = map(inferences, ({ name, typeNode, isOptional }) =>
                factory.createJSDocParameterTag(/*tagName*/ undefined, name, /*isBracketed*/ !!isOptional, factory.createJSDocTypeExpression(typeNode), /* isNameFirst */ false, /*comment*/ undefined));
            changes.addJSDocTags(sourceFile, signature, paramTags);
        }
    }

    function getReferences(token: PropertyName | Token<SyntaxKind.ConstructorKeyword>, program: Program, cancellationToken: CancellationToken): readonly Identifier[] {
        // Position shouldn't matter since token is not a SourceFile.
        return mapDefined(FindAllReferences.getReferenceEntriesForNode(-1, token, program, program.getSourceFiles(), cancellationToken), entry =>
            entry.kind !== FindAllReferences.EntryKind.Span ? tryCast(entry.node, isIdentifier) : undefined);
    }

    function inferTypeForVariableFromUsage(token: Identifier | PrivateIdentifier, program: Program, cancellationToken: CancellationToken): Type {
        const references = getReferences(token, program, cancellationToken);
        return inferTypeFromReferences(program, references, cancellationToken).single();
    }

    function inferTypeForParametersFromUsage(func: SignatureDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken) {
        const references = getFunctionReferences(func, sourceFile, program, cancellationToken);
        return references && inferTypeFromReferences(program, references, cancellationToken).parameters(func) ||
            func.parameters.map<ParameterInference>(p => ({
                declaration: p,
                type: isIdentifier(p.name) ? inferTypeForVariableFromUsage(p.name, program, cancellationToken) : program.getTypeChecker().getAnyType()
            }));
    }

    function getFunctionReferences(containingFunction: SignatureDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): readonly Identifier[] | undefined {
        let searchToken;
        switch (containingFunction.kind) {
            case SyntaxKind.Constructor:
                searchToken = findChildOfKind<Token<SyntaxKind.ConstructorKeyword>>(containingFunction, SyntaxKind.ConstructorKeyword, sourceFile);
                break;
            case SyntaxKind.ArrowFunction:
            case SyntaxKind.FunctionExpression:
                const parent = containingFunction.parent;
                searchToken = (isVariableDeclaration(parent) || isPropertyDeclaration(parent)) && isIdentifier(parent.name) ?
                    parent.name :
                    containingFunction.name;
                break;
            case SyntaxKind.FunctionDeclaration:
            case SyntaxKind.MethodDeclaration:
            case SyntaxKind.MethodSignature:
                searchToken = containingFunction.name;
                break;
        }

        if (!searchToken) {
            return undefined;
        }

        return getReferences(searchToken, program, cancellationToken);
    }

    interface ParameterInference {
        readonly declaration: ParameterDeclaration;
        readonly type: Type;
        readonly isOptional?: boolean;
    }

    function inferTypeFromReferences(program: Program, references: readonly Identifier[], cancellationToken: CancellationToken) {
        const checker = program.getTypeChecker();
        const builtinConstructors: { [s: string]: (t: Type) => Type } = {
            string: () => checker.getStringType(),
            number: () => checker.getNumberType(),
            Array: t => checker.createArrayType(t),
            Promise: t => checker.createPromiseType(t),
        };
        const builtins = [
            checker.getStringType(),
            checker.getNumberType(),
            checker.createArrayType(checker.getAnyType()),
            checker.createPromiseType(checker.getAnyType()),
        ];

        return {
            single,
            parameters,
            thisParameter,
        };

        interface CallUsage {
            argumentTypes: Type[];
            return_: Usage;
        }

        interface Usage {
            isNumber: boolean | undefined;
            isString: boolean | undefined;
            /** Used ambiguously, eg x + ___ or object[___]; results in string | number if no other evidence exists */
            isNumberOrString: boolean | undefined;

            candidateTypes: Type[] | undefined;
            properties: UnderscoreEscapedMap<Usage> | undefined;
            calls: CallUsage[] | undefined;
            constructs: CallUsage[] | undefined;
            numberIndex: Usage | undefined;
            stringIndex: Usage | undefined;
            candidateThisTypes: Type[] | undefined;
            inferredTypes: Type[] | undefined;
        }

        function createEmptyUsage(): Usage {
            return {
                isNumber: undefined,
                isString: undefined,
                isNumberOrString: undefined,
                candidateTypes: undefined,
                properties: undefined,
                calls: undefined,
                constructs: undefined,
                numberIndex: undefined,
                stringIndex: undefined,
                candidateThisTypes: undefined,
                inferredTypes: undefined,
            };
        }

        function combineUsages(usages: Usage[]): Usage {
            const combinedProperties = new Map<__String, Usage[]>();
            for (const u of usages) {
                if (u.properties) {
                    u.properties.forEach((p, name) => {
                        if (!combinedProperties.has(name)) {
                            combinedProperties.set(name, []);
                        }
                        combinedProperties.get(name)!.push(p);
                    });
                }
            }
            const properties = new Map<__String, Usage>();
            combinedProperties.forEach((ps, name) => {
                properties.set(name, combineUsages(ps));
            });
            return {
                isNumber: usages.some(u => u.isNumber),
                isString: usages.some(u => u.isString),
                isNumberOrString: usages.some(u => u.isNumberOrString),
                candidateTypes: flatMap(usages, u => u.candidateTypes) as Type[],
                properties,
                calls: flatMap(usages, u => u.calls) as CallUsage[],
                constructs: flatMap(usages, u => u.constructs) as CallUsage[],
                numberIndex: forEach(usages, u => u.numberIndex),
                stringIndex: forEach(usages, u => u.stringIndex),
                candidateThisTypes: flatMap(usages, u => u.candidateThisTypes) as Type[],
                inferredTypes: undefined, // clear type cache
            };
        }

        function single(): Type {
            return combineTypes(inferTypesFromReferencesSingle(references));
        }

        function parameters(declaration: SignatureDeclaration): ParameterInference[] | undefined {
            if (references.length === 0 || !declaration.parameters) {
                return undefined;
            }

            const usage = createEmptyUsage();
            for (const reference of references) {
                cancellationToken.throwIfCancellationRequested();
                calculateUsageOfNode(reference, usage);
            }
            const calls = [...usage.constructs || [], ...usage.calls || []];
            return declaration.parameters.map((parameter, parameterIndex): ParameterInference => {
                const types = [];
                const isRest = isRestParameter(parameter);
                let isOptional = false;
                for (const call of calls) {
                    if (call.argumentTypes.length <= parameterIndex) {
                        isOptional = isInJSFile(declaration);
                        types.push(checker.getUndefinedType());
                    }
                    else if (isRest) {
                        for (let i = parameterIndex; i < call.argumentTypes.length; i++) {
                            types.push(checker.getBaseTypeOfLiteralType(call.argumentTypes[i]));
                        }
                    }
                    else {
                        types.push(checker.getBaseTypeOfLiteralType(call.argumentTypes[parameterIndex]));
                    }
                }
                if (isIdentifier(parameter.name)) {
                    const inferred = inferTypesFromReferencesSingle(getReferences(parameter.name, program, cancellationToken));
                    types.push(...(isRest ? mapDefined(inferred, checker.getElementTypeOfArrayType) : inferred));
                }
                const type = combineTypes(types);
                return {
                    type: isRest ? checker.createArrayType(type) : type,
                    isOptional: isOptional && !isRest,
                    declaration: parameter
                };
            });
        }

        function thisParameter() {
            const usage = createEmptyUsage();
            for (const reference of references) {
                cancellationToken.throwIfCancellationRequested();
                calculateUsageOfNode(reference, usage);
            }

            return combineTypes(usage.candidateThisTypes || emptyArray);
        }

        function inferTypesFromReferencesSingle(references: readonly Identifier[]): Type[] {
            const usage: Usage = createEmptyUsage();
            for (const reference of references) {
                cancellationToken.throwIfCancellationRequested();
                calculateUsageOfNode(reference, usage);
            }
            return inferTypes(usage);
        }

        function calculateUsageOfNode(node: Expression, usage: Usage): void {
            while (isRightSideOfQualifiedNameOrPropertyAccess(node)) {
                node = node.parent as Expression;
            }

            switch (node.parent.kind) {
                case SyntaxKind.ExpressionStatement:
                    inferTypeFromExpressionStatement(node, usage);
                    break;
                case SyntaxKind.PostfixUnaryExpression:
                    usage.isNumber = true;
                    break;
                case SyntaxKind.PrefixUnaryExpression:
                    inferTypeFromPrefixUnaryExpression(node.parent as PrefixUnaryExpression, usage);
                    break;
                case SyntaxKind.BinaryExpression:
                    inferTypeFromBinaryExpression(node, node.parent as BinaryExpression, usage);
                    break;
                case SyntaxKind.CaseClause:
                case SyntaxKind.DefaultClause:
                    inferTypeFromSwitchStatementLabel(node.parent as CaseOrDefaultClause, usage);
                    break;
                case SyntaxKind.CallExpression:
                case SyntaxKind.NewExpression:
                    if ((node.parent as CallExpression | NewExpression).expression === node) {
                        inferTypeFromCallExpression(node.parent as CallExpression | NewExpression, usage);
                    }
                    else {
                        inferTypeFromContextualType(node, usage);
                    }
                    break;
                case SyntaxKind.PropertyAccessExpression:
                    inferTypeFromPropertyAccessExpression(node.parent as PropertyAccessExpression, usage);
                    break;
                case SyntaxKind.ElementAccessExpression:
                    inferTypeFromPropertyElementExpression(node.parent as ElementAccessExpression, node, usage);
                    break;
                case SyntaxKind.PropertyAssignment:
                case SyntaxKind.ShorthandPropertyAssignment:
                    inferTypeFromPropertyAssignment(node.parent as PropertyAssignment | ShorthandPropertyAssignment, usage);
                    break;
                case SyntaxKind.PropertyDeclaration:
                    inferTypeFromPropertyDeclaration(node.parent as PropertyDeclaration, usage);
                    break;
                case SyntaxKind.VariableDeclaration: {
                    const { name, initializer } = node.parent as VariableDeclaration;
                    if (node === name) {
                        if (initializer) { // This can happen for `let x = null;` which still has an implicit-any error.
                            addCandidateType(usage, checker.getTypeAtLocation(initializer));
                        }
                        break;
                    }
                }
                // falls through
                default:
                    return inferTypeFromContextualType(node, usage);
            }
        }

        function inferTypeFromContextualType(node: Expression, usage: Usage): void {
            if (isExpressionNode(node)) {
                addCandidateType(usage, checker.getContextualType(node));
            }
        }

        function inferTypeFromExpressionStatement(node: Expression, usage: Usage): void {
            addCandidateType(usage, isCallExpression(node) ? checker.getVoidType() : checker.getAnyType());
        }

        function inferTypeFromPrefixUnaryExpression(node: PrefixUnaryExpression, usage: Usage): void {
            switch (node.operator) {
                case SyntaxKind.PlusPlusToken:
                case SyntaxKind.MinusMinusToken:
                case SyntaxKind.MinusToken:
                case SyntaxKind.TildeToken:
                    usage.isNumber = true;
                    break;

                case SyntaxKind.PlusToken:
                    usage.isNumberOrString = true;
                    break;

                // case SyntaxKind.ExclamationToken:
                // no inferences here;
            }
        }

        function inferTypeFromBinaryExpression(node: Expression, parent: BinaryExpression, usage: Usage): void {
            switch (parent.operatorToken.kind) {
                // ExponentiationOperator
                case SyntaxKind.AsteriskAsteriskToken:

                // MultiplicativeOperator
                // falls through
                case SyntaxKind.AsteriskToken:
                case SyntaxKind.SlashToken:
                case SyntaxKind.PercentToken:

                // ShiftOperator
                // falls through
                case SyntaxKind.LessThanLessThanToken:
                case SyntaxKind.GreaterThanGreaterThanToken:
                case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:

                // BitwiseOperator
                // falls through
                case SyntaxKind.AmpersandToken:
                case SyntaxKind.BarToken:
                case SyntaxKind.CaretToken:

                // CompoundAssignmentOperator
                // falls through
                case SyntaxKind.MinusEqualsToken:
                case SyntaxKind.AsteriskAsteriskEqualsToken:
                case SyntaxKind.AsteriskEqualsToken:
                case SyntaxKind.SlashEqualsToken:
                case SyntaxKind.PercentEqualsToken:
                case SyntaxKind.AmpersandEqualsToken:
                case SyntaxKind.BarEqualsToken:
                case SyntaxKind.CaretEqualsToken:
                case SyntaxKind.LessThanLessThanEqualsToken:
                case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
                case SyntaxKind.GreaterThanGreaterThanEqualsToken:

                // AdditiveOperator
                // falls through
                case SyntaxKind.MinusToken:

                // RelationalOperator
                // falls through
                case SyntaxKind.LessThanToken:
                case SyntaxKind.LessThanEqualsToken:
                case SyntaxKind.GreaterThanToken:
                case SyntaxKind.GreaterThanEqualsToken:
                    const operandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left);
                    if (operandType.flags & TypeFlags.EnumLike) {
                        addCandidateType(usage, operandType);
                    }
                    else {
                        usage.isNumber = true;
                    }
                    break;

                case SyntaxKind.PlusEqualsToken:
                case SyntaxKind.PlusToken:
                    const otherOperandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left);
                    if (otherOperandType.flags & TypeFlags.EnumLike) {
                        addCandidateType(usage, otherOperandType);
                    }
                    else if (otherOperandType.flags & TypeFlags.NumberLike) {
                        usage.isNumber = true;
                    }
                    else if (otherOperandType.flags & TypeFlags.StringLike) {
                        usage.isString = true;
                    }
                    else if (otherOperandType.flags & TypeFlags.Any) {
                        // do nothing, maybe we'll learn something elsewhere
                    }
                    else {
                        usage.isNumberOrString = true;
                    }
                    break;

                //  AssignmentOperators
                case SyntaxKind.EqualsToken:
                case SyntaxKind.EqualsEqualsToken:
                case SyntaxKind.EqualsEqualsEqualsToken:
                case SyntaxKind.ExclamationEqualsEqualsToken:
                case SyntaxKind.ExclamationEqualsToken:
                    addCandidateType(usage, checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left));
                    break;

                case SyntaxKind.InKeyword:
                    if (node === parent.left) {
                        usage.isString = true;
                    }
                    break;

                // LogicalOperator Or NullishCoalescing
                case SyntaxKind.BarBarToken:
                case SyntaxKind.QuestionQuestionToken:
                    if (node === parent.left &&
                        (node.parent.parent.kind === SyntaxKind.VariableDeclaration || isAssignmentExpression(node.parent.parent, /*excludeCompoundAssignment*/ true))) {
                        // var x = x || {};
                        // TODO: use getFalsyflagsOfType
                        addCandidateType(usage, checker.getTypeAtLocation(parent.right));
                    }
                    break;

                case SyntaxKind.AmpersandAmpersandToken:
                case SyntaxKind.CommaToken:
                case SyntaxKind.InstanceOfKeyword:
                    // nothing to infer here
                    break;
            }
        }

        function inferTypeFromSwitchStatementLabel(parent: CaseOrDefaultClause, usage: Usage): void {
            addCandidateType(usage, checker.getTypeAtLocation(parent.parent.parent.expression));
        }

        function inferTypeFromCallExpression(parent: CallExpression | NewExpression, usage: Usage): void {
            const call: CallUsage = {
                argumentTypes: [],
                return_: createEmptyUsage()
            };

            if (parent.arguments) {
                for (const argument of parent.arguments) {
                    call.argumentTypes.push(checker.getTypeAtLocation(argument));
                }
            }

            calculateUsageOfNode(parent, call.return_);
            if (parent.kind === SyntaxKind.CallExpression) {
                (usage.calls || (usage.calls = [])).push(call);
            }
            else {
                (usage.constructs || (usage.constructs = [])).push(call);
            }
        }

        function inferTypeFromPropertyAccessExpression(parent: PropertyAccessExpression, usage: Usage): void {
            const name = escapeLeadingUnderscores(parent.name.text);
            if (!usage.properties) {
                usage.properties = new Map();
            }
            const propertyUsage = usage.properties.get(name) || createEmptyUsage();
            calculateUsageOfNode(parent, propertyUsage);
            usage.properties.set(name, propertyUsage);
        }

        function inferTypeFromPropertyElementExpression(parent: ElementAccessExpression, node: Expression, usage: Usage): void {
            if (node === parent.argumentExpression) {
                usage.isNumberOrString = true;
                return;
            }
            else {
                const indexType = checker.getTypeAtLocation(parent.argumentExpression);
                const indexUsage = createEmptyUsage();
                calculateUsageOfNode(parent, indexUsage);
                if (indexType.flags & TypeFlags.NumberLike) {
                    usage.numberIndex = indexUsage;
                }
                else {
                    usage.stringIndex = indexUsage;
                }
            }
        }

        function inferTypeFromPropertyAssignment(assignment: PropertyAssignment | ShorthandPropertyAssignment, usage: Usage) {
            const nodeWithRealType = isVariableDeclaration(assignment.parent.parent) ?
                assignment.parent.parent :
                assignment.parent;
            addCandidateThisType(usage, checker.getTypeAtLocation(nodeWithRealType));
        }

        function inferTypeFromPropertyDeclaration(declaration: PropertyDeclaration, usage: Usage) {
            addCandidateThisType(usage, checker.getTypeAtLocation(declaration.parent));
        }

        interface Priority {
            high: (t: Type) => boolean;
            low: (t: Type) => boolean;
        }

        function removeLowPriorityInferences(inferences: readonly Type[], priorities: Priority[]): Type[] {
            const toRemove: ((t: Type) => boolean)[] = [];
            for (const i of inferences) {
                for (const { high, low } of priorities) {
                    if (high(i)) {
                        Debug.assert(!low(i), "Priority can't have both low and high");
                        toRemove.push(low);
                    }
                }
            }
            return inferences.filter(i => toRemove.every(f => !f(i)));
        }

        function combineFromUsage(usage: Usage) {
            return combineTypes(inferTypes(usage));
        }

        function combineTypes(inferences: readonly Type[]): Type {
            if (!inferences.length) return checker.getAnyType();

            // 1. string or number individually override string | number
            // 2. non-any, non-void overrides any or void
            // 3. non-nullable, non-any, non-void, non-anonymous overrides anonymous types
            const stringNumber = checker.getUnionType([checker.getStringType(), checker.getNumberType()]);
            const priorities: Priority[] = [
                {
                    high: t => t === checker.getStringType() || t === checker.getNumberType(),
                    low: t => t === stringNumber
                },
                {
                    high: t => !(t.flags & (TypeFlags.Any | TypeFlags.Void)),
                    low: t => !!(t.flags & (TypeFlags.Any | TypeFlags.Void))
                },
                {
                    high: t => !(t.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(getObjectFlags(t) & ObjectFlags.Anonymous),
                    low: t => !!(getObjectFlags(t) & ObjectFlags.Anonymous)
                }];
            let good = removeLowPriorityInferences(inferences, priorities);
            const anons = good.filter(i => getObjectFlags(i) & ObjectFlags.Anonymous) as AnonymousType[];
            if (anons.length) {
                good = good.filter(i => !(getObjectFlags(i) & ObjectFlags.Anonymous));
                good.push(combineAnonymousTypes(anons));
            }
            return checker.getWidenedType(checker.getUnionType(good.map(checker.getBaseTypeOfLiteralType), UnionReduction.Subtype));
        }

        function combineAnonymousTypes(anons: AnonymousType[]) {
            if (anons.length === 1) {
                return anons[0];
            }
            const calls = [];
            const constructs = [];
            const stringIndices = [];
            const numberIndices = [];
            let stringIndexReadonly = false;
            let numberIndexReadonly = false;
            const props = createMultiMap<Type>();
            for (const anon of anons) {
                for (const p of checker.getPropertiesOfType(anon)) {
                    props.add(p.name, p.valueDeclaration ? checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration) : checker.getAnyType());
                }
                calls.push(...checker.getSignaturesOfType(anon, SignatureKind.Call));
                constructs.push(...checker.getSignaturesOfType(anon, SignatureKind.Construct));
                const stringIndexInfo = checker.getIndexInfoOfType(anon, IndexKind.String);
                if (stringIndexInfo) {
                    stringIndices.push(stringIndexInfo.type);
                    stringIndexReadonly = stringIndexReadonly || stringIndexInfo.isReadonly;
                }
                const numberIndexInfo = checker.getIndexInfoOfType(anon, IndexKind.Number);
                if (numberIndexInfo) {
                    numberIndices.push(numberIndexInfo.type);
                    numberIndexReadonly = numberIndexReadonly || numberIndexInfo.isReadonly;
                }
            }
            const members = mapEntries(props, (name, types) => {
                const isOptional = types.length < anons.length ? SymbolFlags.Optional : 0;
                const s = checker.createSymbol(SymbolFlags.Property | isOptional, name as __String);
                s.type = checker.getUnionType(types);
                return [name, s];
            });
            const indexInfos = [];
            if (stringIndices.length) indexInfos.push(checker.createIndexInfo(checker.getStringType(), checker.getUnionType(stringIndices), stringIndexReadonly));
            if (numberIndices.length) indexInfos.push(checker.createIndexInfo(checker.getNumberType(), checker.getUnionType(numberIndices), numberIndexReadonly));
            return checker.createAnonymousType(
                anons[0].symbol,
                members as UnderscoreEscapedMap<TransientSymbol>,
                calls,
                constructs,
                indexInfos);
        }

        function inferTypes(usage: Usage): Type[] {
            const types = [];

            if (usage.isNumber) {
                types.push(checker.getNumberType());
            }
            if (usage.isString) {
                types.push(checker.getStringType());
            }
            if (usage.isNumberOrString) {
                types.push(checker.getUnionType([checker.getStringType(), checker.getNumberType()]));
            }
            if (usage.numberIndex) {
                types.push(checker.createArrayType(combineFromUsage(usage.numberIndex)));
            }
            if (usage.properties?.size || usage.constructs?.length || usage.stringIndex) {
                types.push(inferStructuralType(usage));
            }

            const candidateTypes = (usage.candidateTypes || []).map(t => checker.getBaseTypeOfLiteralType(t));
            const callsType = usage.calls?.length ? inferStructuralType(usage) : undefined;
            if (callsType && candidateTypes) {
                types.push(checker.getUnionType([callsType, ...candidateTypes], UnionReduction.Subtype));
            }
            else {
                if (callsType) {
                    types.push(callsType);
                }
                if (length(candidateTypes)) {
                    types.push(...candidateTypes);
                }
            }

            types.push(...inferNamedTypesFromProperties(usage));
            return types;
        }

        function inferStructuralType(usage: Usage) {
            const members = new Map<__String, Symbol>();
            if (usage.properties) {
                usage.properties.forEach((u, name) => {
                    const symbol = checker.createSymbol(SymbolFlags.Property, name);
                    symbol.type = combineFromUsage(u);
                    members.set(name, symbol);
                });
            }
            const callSignatures: Signature[] = usage.calls ? [getSignatureFromCalls(usage.calls)] : [];
            const constructSignatures: Signature[] = usage.constructs ? [getSignatureFromCalls(usage.constructs)] : [];
            const indexInfos = usage.stringIndex ? [checker.createIndexInfo(checker.getStringType(), combineFromUsage(usage.stringIndex), /*isReadonly*/ false)] : [];
            return checker.createAnonymousType(/*symbol*/ undefined, members, callSignatures, constructSignatures, indexInfos);
        }

        function inferNamedTypesFromProperties(usage: Usage): Type[] {
            if (!usage.properties || !usage.properties.size) return [];
            const types = builtins.filter(t => allPropertiesAreAssignableToUsage(t, usage));
            if (0 < types.length && types.length < 3) {
                return types.map(t => inferInstantiationFromUsage(t, usage));
            }
            return [];
        }

        function allPropertiesAreAssignableToUsage(type: Type, usage: Usage) {
            if (!usage.properties) return false;
            return !forEachEntry(usage.properties, (propUsage, name) => {
                const source = checker.getTypeOfPropertyOfType(type, name as string);
                if (!source) {
                    return true;
                }
                if (propUsage.calls) {
                    const sigs = checker.getSignaturesOfType(source, SignatureKind.Call);
                    return !sigs.length || !checker.isTypeAssignableTo(source, getFunctionFromCalls(propUsage.calls));
                }
                else {
                    return !checker.isTypeAssignableTo(source, combineFromUsage(propUsage));
                }
            });
        }

        /**
         * inference is limited to
         * 1. generic types with a single parameter
         * 2. inference to/from calls with a single signature
         */
        function inferInstantiationFromUsage(type: Type, usage: Usage) {
            if (!(getObjectFlags(type) & ObjectFlags.Reference) || !usage.properties) {
                return type;
            }
            const generic = (type as TypeReference).target;
            const singleTypeParameter = singleOrUndefined(generic.typeParameters);
            if (!singleTypeParameter) return type;

            const types: Type[] = [];
            usage.properties.forEach((propUsage, name) => {
                const genericPropertyType = checker.getTypeOfPropertyOfType(generic, name as string);
                Debug.assert(!!genericPropertyType, "generic should have all the properties of its reference.");
                types.push(...inferTypeParameters(genericPropertyType, combineFromUsage(propUsage), singleTypeParameter));
            });
            return builtinConstructors[type.symbol.escapedName as string](combineTypes(types));
        }

        function inferTypeParameters(genericType: Type, usageType: Type, typeParameter: Type): readonly Type[] {
            if (genericType === typeParameter) {
                return [usageType];
            }
            else if (genericType.flags & TypeFlags.UnionOrIntersection) {
                return flatMap((genericType as UnionOrIntersectionType).types, t => inferTypeParameters(t, usageType, typeParameter));
            }
            else if (getObjectFlags(genericType) & ObjectFlags.Reference && getObjectFlags(usageType) & ObjectFlags.Reference) {
                // this is wrong because we need a reference to the targetType to, so we can check that it's also a reference
                const genericArgs = checker.getTypeArguments(genericType as TypeReference);
                const usageArgs = checker.getTypeArguments(usageType as TypeReference);
                const types = [];
                if (genericArgs && usageArgs) {
                    for (let i = 0; i < genericArgs.length; i++) {
                        if (usageArgs[i]) {
                            types.push(...inferTypeParameters(genericArgs[i], usageArgs[i], typeParameter));
                        }
                    }
                }
                return types;
            }
            const genericSigs = checker.getSignaturesOfType(genericType, SignatureKind.Call);
            const usageSigs = checker.getSignaturesOfType(usageType, SignatureKind.Call);
            if (genericSigs.length === 1 && usageSigs.length === 1) {
                return inferFromSignatures(genericSigs[0], usageSigs[0], typeParameter);
            }
            return [];
        }

        function inferFromSignatures(genericSig: Signature, usageSig: Signature, typeParameter: Type) {
            const types = [];
            for (let i = 0; i < genericSig.parameters.length; i++) {
                const genericParam = genericSig.parameters[i];
                const usageParam = usageSig.parameters[i];
                const isRest = genericSig.declaration && isRestParameter(genericSig.declaration.parameters[i]);
                if (!usageParam) {
                    break;
                }
                let genericParamType = genericParam.valueDeclaration ? checker.getTypeOfSymbolAtLocation(genericParam, genericParam.valueDeclaration) : checker.getAnyType();
                const elementType = isRest && checker.getElementTypeOfArrayType(genericParamType);
                if (elementType) {
                    genericParamType = elementType;
                }
                const targetType = (usageParam as SymbolLinks).type
                    || (usageParam.valueDeclaration ? checker.getTypeOfSymbolAtLocation(usageParam, usageParam.valueDeclaration) : checker.getAnyType());
                types.push(...inferTypeParameters(genericParamType, targetType, typeParameter));
            }
            const genericReturn = checker.getReturnTypeOfSignature(genericSig);
            const usageReturn = checker.getReturnTypeOfSignature(usageSig);
            types.push(...inferTypeParameters(genericReturn, usageReturn, typeParameter));
            return types;
        }

        function getFunctionFromCalls(calls: CallUsage[]) {
            return checker.createAnonymousType(/*symbol*/ undefined, createSymbolTable(), [getSignatureFromCalls(calls)], emptyArray, emptyArray);
        }

        function getSignatureFromCalls(calls: CallUsage[]): Signature {
            const parameters: Symbol[] = [];
            const length = Math.max(...calls.map(c => c.argumentTypes.length));
            for (let i = 0; i < length; i++) {
                const symbol = checker.createSymbol(SymbolFlags.FunctionScopedVariable, escapeLeadingUnderscores(`arg${i}`));
                symbol.type = combineTypes(calls.map(call => call.argumentTypes[i] || checker.getUndefinedType()));
                if (calls.some(call => call.argumentTypes[i] === undefined)) {
                    symbol.flags |= SymbolFlags.Optional;
                }
                parameters.push(symbol);
            }
            const returnType = combineFromUsage(combineUsages(calls.map(call => call.return_)));
            return checker.createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, parameters, returnType, /*typePredicate*/ undefined, length, SignatureFlags.None);
        }

        function addCandidateType(usage: Usage, type: Type | undefined) {
            if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) {
                (usage.candidateTypes || (usage.candidateTypes = [])).push(type);
            }
        }

        function addCandidateThisType(usage: Usage, type: Type | undefined) {
            if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) {
                (usage.candidateThisTypes || (usage.candidateThisTypes = [])).push(type);
            }
        }
    }
}
