/**
 * @fileoverview Rule to disallow unnecessary labels
 * @author Toru Nagashima
 */

'use strict';

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

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

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

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

    docs: {
      description: 'Disallow unnecessary labels',
      recommended: false,
      frozen: true,
      url: 'https://eslint.org/docs/latest/rules/no-extra-label',
    },

    schema: [],
    fixable: 'code',

    messages: {
      unexpected: "This label '{{name}}' is unnecessary.",
    },
  },

  create(context) {
    const sourceCode = context.sourceCode;
    let scopeInfo = null;

    /**
     * Creates a new scope with a breakable statement.
     * @param {ASTNode} node A node to create. This is a BreakableStatement.
     * @returns {void}
     */
    function enterBreakableStatement(node) {
      scopeInfo = {
        label:
          node.parent.type === 'LabeledStatement' ? node.parent.label : null,
        breakable: true,
        upper: scopeInfo,
      };
    }

    /**
     * Removes the top scope of the stack.
     * @returns {void}
     */
    function exitBreakableStatement() {
      scopeInfo = scopeInfo.upper;
    }

    /**
     * Creates a new scope with a labeled statement.
     *
     * This ignores it if the body is a breakable statement.
     * In this case it's handled in the `enterBreakableStatement` function.
     * @param {ASTNode} node A node to create. This is a LabeledStatement.
     * @returns {void}
     */
    function enterLabeledStatement(node) {
      if (!astUtils.isBreakableStatement(node.body)) {
        scopeInfo = {
          label: node.label,
          breakable: false,
          upper: scopeInfo,
        };
      }
    }

    /**
     * Removes the top scope of the stack.
     *
     * This ignores it if the body is a breakable statement.
     * In this case it's handled in the `exitBreakableStatement` function.
     * @param {ASTNode} node A node. This is a LabeledStatement.
     * @returns {void}
     */
    function exitLabeledStatement(node) {
      if (!astUtils.isBreakableStatement(node.body)) {
        scopeInfo = scopeInfo.upper;
      }
    }

    /**
     * Reports a given control node if it's unnecessary.
     * @param {ASTNode} node A node. This is a BreakStatement or a
     *      ContinueStatement.
     * @returns {void}
     */
    function reportIfUnnecessary(node) {
      if (!node.label) {
        return;
      }

      const labelNode = node.label;

      for (let info = scopeInfo; info !== null; info = info.upper) {
        if (
          info.breakable ||
          (info.label && info.label.name === labelNode.name)
        ) {
          if (
            info.breakable &&
            info.label &&
            info.label.name === labelNode.name
          ) {
            context.report({
              node: labelNode,
              messageId: 'unexpected',
              data: labelNode,
              fix(fixer) {
                const breakOrContinueToken = sourceCode.getFirstToken(node);

                if (
                  sourceCode.commentsExistBetween(
                    breakOrContinueToken,
                    labelNode
                  )
                ) {
                  return null;
                }

                return fixer.removeRange([
                  breakOrContinueToken.range[1],
                  labelNode.range[1],
                ]);
              },
            });
          }
          return;
        }
      }
    }

    return {
      WhileStatement: enterBreakableStatement,
      'WhileStatement:exit': exitBreakableStatement,
      DoWhileStatement: enterBreakableStatement,
      'DoWhileStatement:exit': exitBreakableStatement,
      ForStatement: enterBreakableStatement,
      'ForStatement:exit': exitBreakableStatement,
      ForInStatement: enterBreakableStatement,
      'ForInStatement:exit': exitBreakableStatement,
      ForOfStatement: enterBreakableStatement,
      'ForOfStatement:exit': exitBreakableStatement,
      SwitchStatement: enterBreakableStatement,
      'SwitchStatement:exit': exitBreakableStatement,
      LabeledStatement: enterLabeledStatement,
      'LabeledStatement:exit': exitLabeledStatement,
      BreakStatement: reportIfUnnecessary,
      ContinueStatement: reportIfUnnecessary,
    };
  },
};