/**
 * @fileoverview Rule to flag blocks with no reason to exist
 * @author Brandon Mills
 */

"use strict";

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

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

		docs: {
			description: "Disallow unnecessary nested blocks",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/no-lone-blocks",
		},

		schema: [],

		messages: {
			redundantBlock: "Block is redundant.",
			redundantNestedBlock: "Nested block is redundant.",
		},
	},

	create(context) {
		// A stack of lone blocks to be checked for block-level bindings
		const loneBlocks = [];
		let ruleDef;
		const sourceCode = context.sourceCode;

		/**
		 * Reports a node as invalid.
		 * @param {ASTNode} node The node to be reported.
		 * @returns {void}
		 */
		function report(node) {
			const messageId =
				node.parent.type === "BlockStatement" ||
				node.parent.type === "StaticBlock"
					? "redundantNestedBlock"
					: "redundantBlock";

			context.report({
				node,
				messageId,
			});
		}

		/**
		 * Checks for any occurrence of a BlockStatement in a place where lists of statements can appear
		 * @param {ASTNode} node The node to check
		 * @returns {boolean} True if the node is a lone block.
		 */
		function isLoneBlock(node) {
			return (
				node.parent.type === "BlockStatement" ||
				node.parent.type === "StaticBlock" ||
				node.parent.type === "Program" ||
				// Don't report blocks in switch cases if the block is the only statement of the case.
				(node.parent.type === "SwitchCase" &&
					!(
						node.parent.consequent[0] === node &&
						node.parent.consequent.length === 1
					))
			);
		}

		/**
		 * Checks the enclosing block of the current node for block-level bindings,
		 * and "marks it" as valid if any.
		 * @param {ASTNode} node The current node to check.
		 * @returns {void}
		 */
		function markLoneBlock(node) {
			if (loneBlocks.length === 0) {
				return;
			}

			const block = node.parent;

			if (loneBlocks.at(-1) === block) {
				loneBlocks.pop();
			}
		}

		// Default rule definition: report all lone blocks
		ruleDef = {
			BlockStatement(node) {
				if (isLoneBlock(node)) {
					report(node);
				}
			},
		};

		// ES6: report blocks without block-level bindings, or that's only child of another block
		if (context.languageOptions.ecmaVersion >= 2015) {
			ruleDef = {
				BlockStatement(node) {
					if (isLoneBlock(node)) {
						loneBlocks.push(node);
					}
				},
				"BlockStatement:exit"(node) {
					if (loneBlocks.length > 0 && loneBlocks.at(-1) === node) {
						loneBlocks.pop();
						report(node);
					} else if (
						(node.parent.type === "BlockStatement" ||
							node.parent.type === "StaticBlock") &&
						node.parent.body.length === 1
					) {
						report(node);
					}
				},
			};

			ruleDef.VariableDeclaration = function (node) {
				if (node.kind !== "var") {
					markLoneBlock(node);
				}
			};

			ruleDef.FunctionDeclaration = function (node) {
				if (sourceCode.getScope(node).isStrict) {
					markLoneBlock(node);
				}
			};

			ruleDef.ClassDeclaration = markLoneBlock;
		}

		return ruleDef;
	},
};
