/**
 * @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;

/**
 * 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 &&
    astUtils.skipChainExpression(left).type === 'MemberExpression' &&
    astUtils.skipChainExpression(right).type === 'MemberExpression' &&
    astUtils.isSameReference(left, right)
  ) {
    report(right);
  }
}

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

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

    defaultOptions: [{ props: true }],

    docs: {
      description: 'Disallow assignments where both sides are exactly the same',
      recommended: true,
      url: 'https://eslint.org/docs/latest/rules/no-self-assign',
    },

    schema: [
      {
        type: 'object',
        properties: {
          props: {
            type: 'boolean',
          },
        },
        additionalProperties: false,
      },
    ],

    messages: {
      selfAssignment: "'{{name}}' is assigned to itself.",
    },
  },

  create(context) {
    const sourceCode = context.sourceCode;
    const [{ props }] = 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,
        messageId: 'selfAssignment',
        data: {
          name: sourceCode.getText(node).replace(SPACES, ''),
        },
      });
    }

    return {
      AssignmentExpression(node) {
        if (['=', '&&=', '||=', '??='].includes(node.operator)) {
          eachSelfAssignment(node.left, node.right, props, report);
        }
      },
    };
  },
};