/* @internal */
namespace ts.codefix {
    const fixId = "addMissingConst";
    const errorCodes = [
        Diagnostics.Cannot_find_name_0.code,
        Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.code
    ];

    registerCodeFix({
        errorCodes,
        getCodeActions: function getCodeActionsToAddMissingConst(context) {
            const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span.start, context.program));
            if (changes.length > 0) {
                return [createCodeFixAction(fixId, changes, Diagnostics.Add_const_to_unresolved_variable, fixId, Diagnostics.Add_const_to_all_unresolved_variables)];
            }
        },
        fixIds: [fixId],
        getAllCodeActions: context => {
            const fixedNodes = new Set<Node>();
            return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag.start, context.program, fixedNodes));
        },
    });

    function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, program: Program, fixedNodes?: Set<Node>) {
        const token = getTokenAtPosition(sourceFile, pos);
        const forInitializer = findAncestor(token, node =>
            isForInOrOfStatement(node.parent) ? node.parent.initializer === node :
            isPossiblyPartOfDestructuring(node) ? false : "quit"
        );
        if (forInitializer) return applyChange(changeTracker, forInitializer, sourceFile, fixedNodes);

        const parent = token.parent;
        if (isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken && isExpressionStatement(parent.parent)) {
            return applyChange(changeTracker, token, sourceFile, fixedNodes);
        }

        if (isArrayLiteralExpression(parent)) {
            const checker = program.getTypeChecker();
            if (!every(parent.elements, element => arrayElementCouldBeVariableDeclaration(element, checker))) {
                return;
            }

            return applyChange(changeTracker, parent, sourceFile, fixedNodes);
        }

        const commaExpression = findAncestor(token, node =>
            isExpressionStatement(node.parent) ? true :
            isPossiblyPartOfCommaSeperatedInitializer(node) ? false : "quit"
        );
        if (commaExpression) {
            const checker = program.getTypeChecker();
            if (!expressionCouldBeVariableDeclaration(commaExpression, checker)) {
                return;
            }

            return applyChange(changeTracker, commaExpression, sourceFile, fixedNodes);
        }
    }

    function applyChange(changeTracker: textChanges.ChangeTracker, initializer: Node, sourceFile: SourceFile, fixedNodes?: Set<Node>) {
        if (!fixedNodes || tryAddToSet(fixedNodes, initializer)) {
            changeTracker.insertModifierBefore(sourceFile, SyntaxKind.ConstKeyword, initializer);
        }
    }

    function isPossiblyPartOfDestructuring(node: Node): boolean {
        switch (node.kind) {
            case SyntaxKind.Identifier:
            case SyntaxKind.ArrayLiteralExpression:
            case SyntaxKind.ObjectLiteralExpression:
            case SyntaxKind.PropertyAssignment:
            case SyntaxKind.ShorthandPropertyAssignment:
                return true;
            default:
                return false;
        }
    }

    function arrayElementCouldBeVariableDeclaration(expression: Expression, checker: TypeChecker): boolean {
        const identifier =
            isIdentifier(expression) ? expression :
            isAssignmentExpression(expression, /*excludeCompoundAssignment*/ true) && isIdentifier(expression.left) ? expression.left :
            undefined;
        return !!identifier && !checker.getSymbolAtLocation(identifier);
    }

    function isPossiblyPartOfCommaSeperatedInitializer(node: Node): boolean {
        switch (node.kind) {
            case SyntaxKind.Identifier:
            case SyntaxKind.BinaryExpression:
            case SyntaxKind.CommaToken:
                return true;
            default:
                return false;
        }
    }

    function expressionCouldBeVariableDeclaration(expression: Node, checker: TypeChecker): boolean {
        if (!isBinaryExpression(expression)) {
            return false;
        }

        if (expression.operatorToken.kind === SyntaxKind.CommaToken) {
            return every([expression.left, expression.right], expression => expressionCouldBeVariableDeclaration(expression, checker));
        }

        return expression.operatorToken.kind === SyntaxKind.EqualsToken
            && isIdentifier(expression.left)
            && !checker.getSymbolAtLocation(expression.left);
    }
}
