/**
 * @fileoverview A rule to disallow unnecessary assignments`.
 * @author Yosuke Ota
 */

"use strict";

const { findVariable } = require("@eslint-community/eslint-utils");

//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------

/** @typedef {import("estree").Node} ASTNode */
/** @typedef {import("estree").Pattern} Pattern */
/** @typedef {import("estree").Identifier} Identifier */
/** @typedef {import("estree").VariableDeclarator} VariableDeclarator */
/** @typedef {import("estree").AssignmentExpression} AssignmentExpression */
/** @typedef {import("estree").UpdateExpression} UpdateExpression */
/** @typedef {import("estree").Expression} Expression */
/** @typedef {import("eslint-scope").Scope} Scope */
/** @typedef {import("eslint-scope").Variable} Variable */
/** @typedef {import("../linter/code-path-analysis/code-path")} CodePath */
/** @typedef {import("../linter/code-path-analysis/code-path-segment")} CodePathSegment */

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Extract identifier from the given pattern node used on the left-hand side of the assignment.
 * @param {Pattern} pattern The pattern node to extract identifier
 * @returns {Iterable<Identifier>} The extracted identifier
 */
function *extractIdentifiersFromPattern(pattern) {
    switch (pattern.type) {
        case "Identifier":
            yield pattern;
            return;
        case "ObjectPattern":
            for (const property of pattern.properties) {
                yield* extractIdentifiersFromPattern(property.type === "Property" ? property.value : property);
            }
            return;
        case "ArrayPattern":
            for (const element of pattern.elements) {
                if (!element) {
                    continue;
                }
                yield* extractIdentifiersFromPattern(element);
            }
            return;
        case "RestElement":
            yield* extractIdentifiersFromPattern(pattern.argument);
            return;
        case "AssignmentPattern":
            yield* extractIdentifiersFromPattern(pattern.left);

        // no default
    }
}


/**
 * Checks whether the given identifier node is evaluated after the assignment identifier.
 * @param {AssignmentInfo} assignment The assignment info.
 * @param {Identifier} identifier The identifier to check.
 * @returns {boolean} `true` if the given identifier node is evaluated after the assignment identifier.
 */
function isIdentifierEvaluatedAfterAssignment(assignment, identifier) {
    if (identifier.range[0] < assignment.identifier.range[1]) {
        return false;
    }
    if (
        assignment.expression &&
        assignment.expression.range[0] <= identifier.range[0] &&
        identifier.range[1] <= assignment.expression.range[1]
    ) {

        /*
         * The identifier node is in an expression that is evaluated before the assignment.
         * e.g. x = id;
         *          ^^ identifier to check
         *      ^      assignment identifier
         */
        return false;
    }

    /*
     * e.g.
     *      x = 42; id;
     *              ^^ identifier to check
     *      ^          assignment identifier
     *      let { x, y = id } = obj;
     *                   ^^  identifier to check
     *            ^          assignment identifier
     */
    return true;
}

/**
 * Checks whether the given identifier node is used between the assigned identifier and the equal sign.
 *
 * e.g. let { x, y = x } = obj;
 *                   ^   identifier to check
 *            ^          assigned identifier
 * @param {AssignmentInfo} assignment The assignment info.
 * @param {Identifier} identifier The identifier to check.
 * @returns {boolean} `true` if the given identifier node is used between the assigned identifier and the equal sign.
 */
function isIdentifierUsedBetweenAssignedAndEqualSign(assignment, identifier) {
    if (!assignment.expression) {
        return false;
    }
    return (
        assignment.identifier.range[1] <= identifier.range[0] &&
        identifier.range[1] <= assignment.expression.range[0]
    );
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../shared/types').Rule} */
module.exports = {
    meta: {
        type: "problem",

        docs: {
            description: "Disallow variable assignments when the value is not used",
            recommended: false,
            url: "https://eslint.org/docs/latest/rules/no-useless-assignment"
        },

        schema: [],

        messages: {
            unnecessaryAssignment: "This assigned value is not used in subsequent statements."
        }
    },

    create(context) {
        const sourceCode = context.sourceCode;

        /**
         * @typedef {Object} ScopeStack
         * @property {CodePath} codePath The code path of this scope stack.
         * @property {Scope} scope The scope of this scope stack.
         * @property {ScopeStack} upper The upper scope stack.
         * @property {Record<string, ScopeStackSegmentInfo>} segments The map of ScopeStackSegmentInfo.
         * @property {Set<CodePathSegment>} currentSegments The current CodePathSegments.
         * @property {Map<Variable, AssignmentInfo[]>} assignments The map of list of AssignmentInfo for each variable.
         */
        /**
         * @typedef {Object} ScopeStackSegmentInfo
         * @property {CodePathSegment} segment The code path segment.
         * @property {Identifier|null} first The first identifier that appears within the segment.
         * @property {Identifier|null} last The last identifier that appears within the segment.
         * `first` and `last` are used to determine whether an identifier exists within the segment position range.
         * Since it is used as a range of segments, we should originally hold all nodes, not just identifiers,
         * but since the only nodes to be judged are identifiers, it is sufficient to have a range of identifiers.
         */
        /**
         * @typedef {Object} AssignmentInfo
         * @property {Variable} variable The variable that is assigned.
         * @property {Identifier} identifier The identifier that is assigned.
         * @property {VariableDeclarator|AssignmentExpression|UpdateExpression} node The node where the variable was updated.
         * @property {Expression|null} expression The expression that is evaluated before the assignment.
         * @property {CodePathSegment[]} segments The code path segments where the assignment was made.
         */

        /** @type {ScopeStack} */
        let scopeStack = null;

        /** @type {Set<Scope>} */
        const codePathStartScopes = new Set();

        /**
         * Gets the scope of code path start from given scope
         * @param {Scope} scope The initial scope
         * @returns {Scope} The scope of code path start
         * @throws {Error} Unexpected error
         */
        function getCodePathStartScope(scope) {
            let target = scope;

            while (target) {
                if (codePathStartScopes.has(target)) {
                    return target;
                }
                target = target.upper;
            }

            // Should be unreachable
            return null;
        }

        /**
         * Verify the given scope stack.
         * @param {ScopeStack} target The scope stack to verify.
         * @returns {void}
         */
        function verify(target) {

            /**
             * Checks whether the given identifier is used in the segment.
             * @param {CodePathSegment} segment The code path segment.
             * @param {Identifier} identifier The identifier to check.
             * @returns {boolean} `true` if the identifier is used in the segment.
             */
            function isIdentifierUsedInSegment(segment, identifier) {
                const segmentInfo = target.segments[segment.id];

                return (
                    segmentInfo.first &&
                    segmentInfo.last &&
                    segmentInfo.first.range[0] <= identifier.range[0] &&
                    identifier.range[1] <= segmentInfo.last.range[1]
                );
            }

            /**
             * Verifies whether the given assignment info is an used assignment.
             * Report if it is an unused assignment.
             * @param {AssignmentInfo} targetAssignment The assignment info to verify.
             * @param {AssignmentInfo[]} allAssignments The list of all assignment info for variables.
             * @returns {void}
             */
            function verifyAssignmentIsUsed(targetAssignment, allAssignments) {

                /**
                 * @typedef {Object} SubsequentSegmentData
                 * @property {CodePathSegment} segment The code path segment
                 * @property {AssignmentInfo} [assignment] The first occurrence of the assignment within the segment.
                 * There is no need to check if the variable is used after this assignment,
                 * as the value it was assigned will be used.
                 */

                /**
                 * Information used in `getSubsequentSegments()`.
                 * To avoid unnecessary iterations, cache information that has already been iterated over,
                 * and if additional iterations are needed, start iterating from the retained position.
                 */
                const subsequentSegmentData = {

                    /**
                     * Cache of subsequent segment information list that have already been iterated.
                     * @type {SubsequentSegmentData[]}
                     */
                    results: [],

                    /**
                     * Subsequent segments that have already been iterated on. Used to avoid infinite loops.
                     * @type {Set<CodePathSegment>}
                     */
                    subsequentSegments: new Set(),

                    /**
                     * Unexplored code path segment.
                     * If additional iterations are needed, consume this information and iterate.
                     * @type {CodePathSegment[]}
                     */
                    queueSegments: targetAssignment.segments.flatMap(segment => segment.nextSegments)
                };


                /**
                 * Gets the subsequent segments from the segment of
                 * the assignment currently being validated (targetAssignment).
                 * @returns {Iterable<SubsequentSegmentData>} the subsequent segments
                 */
                function *getSubsequentSegments() {
                    yield* subsequentSegmentData.results;

                    while (subsequentSegmentData.queueSegments.length > 0) {
                        const nextSegment = subsequentSegmentData.queueSegments.shift();

                        if (subsequentSegmentData.subsequentSegments.has(nextSegment)) {
                            continue;
                        }
                        subsequentSegmentData.subsequentSegments.add(nextSegment);

                        const assignmentInSegment = allAssignments
                            .find(otherAssignment => (
                                otherAssignment.segments.includes(nextSegment) &&
                                !isIdentifierUsedBetweenAssignedAndEqualSign(otherAssignment, targetAssignment.identifier)
                            ));

                        if (!assignmentInSegment) {

                            /*
                             * Stores the next segment to explore.
                             * If `assignmentInSegment` exists,
                             * we are guarding it because we don't need to explore the next segment.
                             */
                            subsequentSegmentData.queueSegments.push(...nextSegment.nextSegments);
                        }

                        /** @type {SubsequentSegmentData} */
                        const result = {
                            segment: nextSegment,
                            assignment: assignmentInSegment
                        };

                        subsequentSegmentData.results.push(result);
                        yield result;
                    }
                }


                const readReferences = targetAssignment.variable.references.filter(reference => reference.isRead());

                if (!readReferences.length) {

                    /*
                     * It is not just an unnecessary assignment, but an unnecessary (unused) variable
                     * and thus should not be reported by this rule because it is reported by `no-unused-vars`.
                     */
                    return;
                }

                /**
                 * Other assignment on the current segment and after current assignment.
                 */
                const otherAssignmentAfterTargetAssignment = allAssignments
                    .find(assignment => {
                        if (
                            assignment === targetAssignment ||
                            assignment.segments.length && assignment.segments.every(segment => !targetAssignment.segments.includes(segment))
                        ) {
                            return false;
                        }
                        if (isIdentifierEvaluatedAfterAssignment(targetAssignment, assignment.identifier)) {
                            return true;
                        }
                        if (
                            assignment.expression &&
                            assignment.expression.range[0] <= targetAssignment.identifier.range[0] &&
                            targetAssignment.identifier.range[1] <= assignment.expression.range[1]
                        ) {

                            /*
                             * The target assignment is in an expression that is evaluated before the assignment.
                             * e.g. x=(x=1);
                             *         ^^^ targetAssignment
                             *      ^^^^^^^ assignment
                             */
                            return true;
                        }

                        return false;
                    });

                for (const reference of readReferences) {

                    /*
                     * If the scope of the reference is outside the current code path scope,
                     * we cannot track whether this assignment is not used.
                     * For example, it can also be called asynchronously.
                     */
                    if (target.scope !== getCodePathStartScope(reference.from)) {
                        return;
                    }

                    // Checks if it is used in the same segment as the target assignment.
                    if (
                        isIdentifierEvaluatedAfterAssignment(targetAssignment, reference.identifier) &&
                        (
                            isIdentifierUsedBetweenAssignedAndEqualSign(targetAssignment, reference.identifier) ||
                            targetAssignment.segments.some(segment => isIdentifierUsedInSegment(segment, reference.identifier))
                        )
                    ) {

                        if (
                            otherAssignmentAfterTargetAssignment &&
                            isIdentifierEvaluatedAfterAssignment(otherAssignmentAfterTargetAssignment, reference.identifier)
                        ) {

                            // There was another assignment before the reference. Therefore, it has not been used yet.
                            continue;
                        }

                        // Uses in statements after the written identifier.
                        return;
                    }

                    if (otherAssignmentAfterTargetAssignment) {

                        /*
                         * The assignment was followed by another assignment in the same segment.
                         * Therefore, there is no need to check the next segment.
                         */
                        continue;
                    }

                    // Check subsequent segments.
                    for (const subsequentSegment of getSubsequentSegments()) {
                        if (isIdentifierUsedInSegment(subsequentSegment.segment, reference.identifier)) {
                            if (
                                subsequentSegment.assignment &&
                                isIdentifierEvaluatedAfterAssignment(subsequentSegment.assignment, reference.identifier)
                            ) {

                                // There was another assignment before the reference. Therefore, it has not been used yet.
                                continue;
                            }

                            // It is used
                            return;
                        }
                    }
                }
                context.report({
                    node: targetAssignment.identifier,
                    messageId: "unnecessaryAssignment"
                });
            }

            // Verify that each assignment in the code path is used.
            for (const assignments of target.assignments.values()) {
                assignments.sort((a, b) => a.identifier.range[0] - b.identifier.range[0]);
                for (const assignment of assignments) {
                    verifyAssignmentIsUsed(assignment, assignments);
                }
            }
        }

        return {
            onCodePathStart(codePath, node) {
                const scope = sourceCode.getScope(node);

                scopeStack = {
                    upper: scopeStack,
                    codePath,
                    scope,
                    segments: Object.create(null),
                    currentSegments: new Set(),
                    assignments: new Map()
                };
                codePathStartScopes.add(scopeStack.scope);
            },
            onCodePathEnd() {
                verify(scopeStack);

                scopeStack = scopeStack.upper;
            },
            onCodePathSegmentStart(segment) {
                const segmentInfo = { segment, first: null, last: null };

                scopeStack.segments[segment.id] = segmentInfo;
                scopeStack.currentSegments.add(segment);
            },
            onCodePathSegmentEnd(segment) {
                scopeStack.currentSegments.delete(segment);
            },
            Identifier(node) {
                for (const segment of scopeStack.currentSegments) {
                    const segmentInfo = scopeStack.segments[segment.id];

                    if (!segmentInfo.first) {
                        segmentInfo.first = node;
                    }
                    segmentInfo.last = node;
                }
            },
            ":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"(node) {
                if (scopeStack.currentSegments.size === 0) {

                    // Ignore unreachable segments
                    return;
                }

                const assignments = scopeStack.assignments;

                let pattern;
                let expression = null;

                if (node.type === "VariableDeclarator") {
                    pattern = node.id;
                    expression = node.init;
                } else if (node.type === "AssignmentExpression") {
                    pattern = node.left;
                    expression = node.right;
                } else { // UpdateExpression
                    pattern = node.argument;
                }

                for (const identifier of extractIdentifiersFromPattern(pattern)) {
                    const scope = sourceCode.getScope(identifier);

                    /** @type {Variable} */
                    const variable = findVariable(scope, identifier);

                    if (!variable) {
                        continue;
                    }

                    // We don't know where global variables are used.
                    if (variable.scope.type === "global" && variable.defs.length === 0) {
                        continue;
                    }

                    /*
                     * If the scope of the variable is outside the current code path scope,
                     * we cannot track whether this assignment is not used.
                     */
                    if (scopeStack.scope !== getCodePathStartScope(variable.scope)) {
                        continue;
                    }

                    // Variables marked by `markVariableAsUsed()` or
                    // exported by "exported" block comment.
                    if (variable.eslintUsed) {
                        continue;
                    }

                    // Variables exported by ESM export syntax
                    if (variable.scope.type === "module") {
                        if (
                            variable.defs
                                .some(def => (
                                    (def.type === "Variable" && def.parent.parent.type === "ExportNamedDeclaration") ||
                                    (
                                        def.type === "FunctionName" &&
                                        (
                                            def.node.parent.type === "ExportNamedDeclaration" ||
                                            def.node.parent.type === "ExportDefaultDeclaration"
                                        )
                                    ) ||
                                    (
                                        def.type === "ClassName" &&
                                        (
                                            def.node.parent.type === "ExportNamedDeclaration" ||
                                            def.node.parent.type === "ExportDefaultDeclaration"
                                        )
                                    )
                                ))
                        ) {
                            continue;
                        }
                        if (variable.references.some(reference => reference.identifier.parent.type === "ExportSpecifier")) {

                            // It have `export { ... }` reference.
                            continue;
                        }
                    }

                    let list = assignments.get(variable);

                    if (!list) {
                        list = [];
                        assignments.set(variable, list);
                    }
                    list.push({
                        variable,
                        identifier,
                        node,
                        expression,
                        segments: [...scopeStack.currentSegments]
                    });
                }
            }
        };
    }
};
