/**
 * @fileoverview A rule to ensure whitespace before blocks.
 * @author Mathias Schreck <https://github.com/lo1tuma>
 * @deprecated in ESLint v8.53.0
 */

"use strict";

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

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

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

/**
 * Checks whether the given node represents the body of a function.
 * @param {ASTNode} node the node to check.
 * @returns {boolean} `true` if the node is function body.
 */
function isFunctionBody(node) {
	const parent = node.parent;

	return (
		node.type === "BlockStatement" &&
		astUtils.isFunction(parent) &&
		parent.body === node
	);
}

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

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
	meta: {
		deprecated: {
			message: "Formatting rules are being moved out of ESLint core.",
			url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
			deprecatedSince: "8.53.0",
			availableUntil: "11.0.0",
			replacedBy: [
				{
					message:
						"ESLint Stylistic now maintains deprecated stylistic core rules.",
					url: "https://eslint.style/guide/migration",
					plugin: {
						name: "@stylistic/eslint-plugin",
						url: "https://eslint.style",
					},
					rule: {
						name: "space-before-blocks",
						url: "https://eslint.style/rules/space-before-blocks",
					},
				},
			],
		},
		type: "layout",

		docs: {
			description: "Enforce consistent spacing before blocks",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/space-before-blocks",
		},

		fixable: "whitespace",

		schema: [
			{
				oneOf: [
					{
						enum: ["always", "never"],
					},
					{
						type: "object",
						properties: {
							keywords: {
								enum: ["always", "never", "off"],
							},
							functions: {
								enum: ["always", "never", "off"],
							},
							classes: {
								enum: ["always", "never", "off"],
							},
						},
						additionalProperties: false,
					},
				],
			},
		],

		messages: {
			unexpectedSpace: "Unexpected space before opening brace.",
			missingSpace: "Missing space before opening brace.",
		},
	},

	create(context) {
		const config = context.options[0],
			sourceCode = context.sourceCode;
		let alwaysFunctions = true,
			alwaysKeywords = true,
			alwaysClasses = true,
			neverFunctions = false,
			neverKeywords = false,
			neverClasses = false;

		if (typeof config === "object") {
			alwaysFunctions = config.functions === "always";
			alwaysKeywords = config.keywords === "always";
			alwaysClasses = config.classes === "always";
			neverFunctions = config.functions === "never";
			neverKeywords = config.keywords === "never";
			neverClasses = config.classes === "never";
		} else if (config === "never") {
			alwaysFunctions = false;
			alwaysKeywords = false;
			alwaysClasses = false;
			neverFunctions = true;
			neverKeywords = true;
			neverClasses = true;
		}

		/**
		 * Checks whether the spacing before the given block is already controlled by another rule:
		 * - `arrow-spacing` checks spaces after `=>`.
		 * - `keyword-spacing` checks spaces after keywords in certain contexts.
		 * - `switch-colon-spacing` checks spaces after `:` of switch cases.
		 * @param {Token} precedingToken first token before the block.
		 * @param {ASTNode|Token} node `BlockStatement` node or `{` token of a `SwitchStatement` node.
		 * @returns {boolean} `true` if requiring or disallowing spaces before the given block could produce conflicts with other rules.
		 */
		function isConflicted(precedingToken, node) {
			return (
				astUtils.isArrowToken(precedingToken) ||
				(astUtils.isKeywordToken(precedingToken) &&
					!isFunctionBody(node)) ||
				(astUtils.isColonToken(precedingToken) &&
					node.parent &&
					node.parent.type === "SwitchCase" &&
					precedingToken ===
						astUtils.getSwitchCaseColonToken(
							node.parent,
							sourceCode,
						))
			);
		}

		/**
		 * Checks the given BlockStatement node has a preceding space if it doesn’t start on a new line.
		 * @param {ASTNode|Token} node The AST node of a BlockStatement.
		 * @returns {void} undefined.
		 */
		function checkPrecedingSpace(node) {
			const precedingToken = sourceCode.getTokenBefore(node);

			if (
				precedingToken &&
				!isConflicted(precedingToken, node) &&
				astUtils.isTokenOnSameLine(precedingToken, node)
			) {
				const hasSpace = sourceCode.isSpaceBetween(
					precedingToken,
					node,
				);
				let requireSpace;
				let requireNoSpace;

				if (isFunctionBody(node)) {
					requireSpace = alwaysFunctions;
					requireNoSpace = neverFunctions;
				} else if (node.type === "ClassBody") {
					requireSpace = alwaysClasses;
					requireNoSpace = neverClasses;
				} else {
					requireSpace = alwaysKeywords;
					requireNoSpace = neverKeywords;
				}

				if (requireSpace && !hasSpace) {
					context.report({
						node,
						messageId: "missingSpace",
						fix(fixer) {
							return fixer.insertTextBefore(node, " ");
						},
					});
				} else if (requireNoSpace && hasSpace) {
					context.report({
						node,
						messageId: "unexpectedSpace",
						fix(fixer) {
							return fixer.removeRange([
								precedingToken.range[1],
								node.range[0],
							]);
						},
					});
				}
			}
		}

		/**
		 * Checks if the CaseBlock of an given SwitchStatement node has a preceding space.
		 * @param {ASTNode} node The node of a SwitchStatement.
		 * @returns {void} undefined.
		 */
		function checkSpaceBeforeCaseBlock(node) {
			const cases = node.cases;
			let openingBrace;

			if (cases.length > 0) {
				openingBrace = sourceCode.getTokenBefore(cases[0]);
			} else {
				openingBrace = sourceCode.getLastToken(node, 1);
			}

			checkPrecedingSpace(openingBrace);
		}

		return {
			BlockStatement: checkPrecedingSpace,
			ClassBody: checkPrecedingSpace,
			SwitchStatement: checkSpaceBeforeCaseBlock,
		};
	},
};
