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