/**
 * @fileoverview Rule to disallow assignments where both sides are exactly the same
 * @author Toru Nagashima
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");

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

const SPACES = /\s+/gu;

/**
 * Checks whether the property of 2 given member expression nodes are the same
 * property or not.
 * @param {ASTNode} left A member expression node to check.
 * @param {ASTNode} right Another member expression node to check.
 * @returns {boolean} `true` if the member expressions have the same property.
 */
function isSameProperty(left, right) {
    if (left.property.type === "Identifier" &&
        left.property.type === right.property.type &&
        left.property.name === right.property.name &&
        left.computed === right.computed
    ) {
        return true;
    }

    const lname = astUtils.getStaticPropertyName(left);
    const rname = astUtils.getStaticPropertyName(right);

    return lname !== null && lname === rname;
}

/**
 * Checks whether 2 given member expression nodes are the reference to the same
 * property or not.
 * @param {ASTNode} left A member expression node to check.
 * @param {ASTNode} right Another member expression node to check.
 * @returns {boolean} `true` if the member expressions are the reference to the
 *  same property or not.
 */
function isSameMember(left, right) {
    if (!isSameProperty(left, right)) {
        return false;
    }

    const lobj = left.object;
    const robj = right.object;

    if (lobj.type !== robj.type) {
        return false;
    }
    if (lobj.type === "MemberExpression") {
        return isSameMember(lobj, robj);
    }
    if (lobj.type === "ThisExpression") {
        return true;
    }
    return lobj.type === "Identifier" && lobj.name === robj.name;
}

/**
 * Traverses 2 Pattern nodes in parallel, then reports self-assignments.
 * @param {ASTNode|null} left A left node to traverse. This is a Pattern or
 *      a Property.
 * @param {ASTNode|null} right A right node to traverse. This is a Pattern or
 *      a Property.
 * @param {boolean} props The flag to check member expressions as well.
 * @param {Function} report A callback function to report.
 * @returns {void}
 */
function eachSelfAssignment(left, right, props, report) {
    if (!left || !right) {

        // do nothing
    } else if (
        left.type === "Identifier" &&
        right.type === "Identifier" &&
        left.name === right.name
    ) {
        report(right);
    } else if (
        left.type === "ArrayPattern" &&
        right.type === "ArrayExpression"
    ) {
        const end = Math.min(left.elements.length, right.elements.length);

        for (let i = 0; i < end; ++i) {
            const leftElement = left.elements[i];
            const rightElement = right.elements[i];

            // Avoid cases such as [...a] = [...a, 1]
            if (
                leftElement &&
                leftElement.type === "RestElement" &&
                i < right.elements.length - 1
            ) {
                break;
            }

            eachSelfAssignment(leftElement, rightElement, props, report);

            // After a spread element, those indices are unknown.
            if (rightElement && rightElement.type === "SpreadElement") {
                break;
            }
        }
    } else if (
        left.type === "RestElement" &&
        right.type === "SpreadElement"
    ) {
        eachSelfAssignment(left.argument, right.argument, props, report);
    } else if (
        left.type === "ObjectPattern" &&
        right.type === "ObjectExpression" &&
        right.properties.length >= 1
    ) {

        /*
         * Gets the index of the last spread property.
         * It's possible to overwrite properties followed by it.
         */
        let startJ = 0;

        for (let i = right.properties.length - 1; i >= 0; --i) {
            const propType = right.properties[i].type;

            if (propType === "SpreadElement" || propType === "ExperimentalSpreadProperty") {
                startJ = i + 1;
                break;
            }
        }

        for (let i = 0; i < left.properties.length; ++i) {
            for (let j = startJ; j < right.properties.length; ++j) {
                eachSelfAssignment(
                    left.properties[i],
                    right.properties[j],
                    props,
                    report
                );
            }
        }
    } else if (
        left.type === "Property" &&
        right.type === "Property" &&
        right.kind === "init" &&
        !right.method
    ) {
        const leftName = astUtils.getStaticPropertyName(left);

        if (leftName !== null && leftName === astUtils.getStaticPropertyName(right)) {
            eachSelfAssignment(left.value, right.value, props, report);
        }
    } else if (
        props &&
        left.type === "MemberExpression" &&
        right.type === "MemberExpression" &&
        isSameMember(left, right)
    ) {
        report(right);
    }
}

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

module.exports = {
    meta: {
        type: "problem",

        docs: {
            description: "disallow assignments where both sides are exactly the same",
            category: "Best Practices",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-self-assign"
        },

        schema: [
            {
                type: "object",
                properties: {
                    props: {
                        type: "boolean",
                        default: true
                    }
                },
                additionalProperties: false
            }
        ]
    },

    create(context) {
        const sourceCode = context.getSourceCode();
        const [{ props = true } = {}] = context.options;

        /**
         * Reports a given node as self assignments.
         * @param {ASTNode} node A node to report. This is an Identifier node.
         * @returns {void}
         */
        function report(node) {
            context.report({
                node,
                message: "'{{name}}' is assigned to itself.",
                data: {
                    name: sourceCode.getText(node).replace(SPACES, "")
                }
            });
        }

        return {
            AssignmentExpression(node) {
                if (node.operator === "=") {
                    eachSelfAssignment(node.left, node.right, props, report);
                }
            }
        };
    }
};
