/**
 * @fileoverview enforce consistent line breaks inside function parentheses
 * @author Teddy Katz
 * @deprecated in ESLint v8.53.0
 */
"use strict";

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

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

//------------------------------------------------------------------------------
// 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: "10.0.0",
			replacedBy: [
				{
					message:
						"ESLint Stylistic now maintains deprecated stylistic core rules.",
					url: "https://eslint.style/guide/migration",
					plugin: {
						name: "@stylistic/eslint-plugin-js",
						url: "https://eslint.style/packages/js",
					},
					rule: {
						name: "function-paren-newline",
						url: "https://eslint.style/rules/js/function-paren-newline",
					},
				},
			],
		},
		type: "layout",

		docs: {
			description:
				"Enforce consistent line breaks inside function parentheses",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/function-paren-newline",
		},

		fixable: "whitespace",

		schema: [
			{
				oneOf: [
					{
						enum: [
							"always",
							"never",
							"consistent",
							"multiline",
							"multiline-arguments",
						],
					},
					{
						type: "object",
						properties: {
							minItems: {
								type: "integer",
								minimum: 0,
							},
						},
						additionalProperties: false,
					},
				],
			},
		],

		messages: {
			expectedBefore: "Expected newline before ')'.",
			expectedAfter: "Expected newline after '('.",
			expectedBetween: "Expected newline between arguments/params.",
			unexpectedBefore: "Unexpected newline before ')'.",
			unexpectedAfter: "Unexpected newline after '('.",
		},
	},

	create(context) {
		const sourceCode = context.sourceCode;
		const rawOption = context.options[0] || "multiline";
		const multilineOption = rawOption === "multiline";
		const multilineArgumentsOption = rawOption === "multiline-arguments";
		const consistentOption = rawOption === "consistent";
		let minItems;

		if (typeof rawOption === "object") {
			minItems = rawOption.minItems;
		} else if (rawOption === "always") {
			minItems = 0;
		} else if (rawOption === "never") {
			minItems = Infinity;
		} else {
			minItems = null;
		}

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

		/**
		 * Determines whether there should be newlines inside function parens
		 * @param {ASTNode[]} elements The arguments or parameters in the list
		 * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
		 * @returns {boolean} `true` if there should be newlines inside the function parens
		 */
		function shouldHaveNewlines(elements, hasLeftNewline) {
			if (multilineArgumentsOption && elements.length === 1) {
				return hasLeftNewline;
			}
			if (multilineOption || multilineArgumentsOption) {
				return elements.some(
					(element, index) =>
						index !== elements.length - 1 &&
						element.loc.end.line !==
							elements[index + 1].loc.start.line,
				);
			}
			if (consistentOption) {
				return hasLeftNewline;
			}
			return elements.length >= minItems;
		}

		/**
		 * Validates parens
		 * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
		 * @param {ASTNode[]} elements The arguments or parameters in the list
		 * @returns {void}
		 */
		function validateParens(parens, elements) {
			const leftParen = parens.leftParen;
			const rightParen = parens.rightParen;
			const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
			const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
			const hasLeftNewline = !astUtils.isTokenOnSameLine(
				leftParen,
				tokenAfterLeftParen,
			);
			const hasRightNewline = !astUtils.isTokenOnSameLine(
				tokenBeforeRightParen,
				rightParen,
			);
			const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);

			if (hasLeftNewline && !needsNewlines) {
				context.report({
					node: leftParen,
					messageId: "unexpectedAfter",
					fix(fixer) {
						return sourceCode
							.getText()
							.slice(
								leftParen.range[1],
								tokenAfterLeftParen.range[0],
							)
							.trim()
							? // If there is a comment between the ( and the first element, don't do a fix.
								null
							: fixer.removeRange([
									leftParen.range[1],
									tokenAfterLeftParen.range[0],
								]);
					},
				});
			} else if (!hasLeftNewline && needsNewlines) {
				context.report({
					node: leftParen,
					messageId: "expectedAfter",
					fix: fixer => fixer.insertTextAfter(leftParen, "\n"),
				});
			}

			if (hasRightNewline && !needsNewlines) {
				context.report({
					node: rightParen,
					messageId: "unexpectedBefore",
					fix(fixer) {
						return sourceCode
							.getText()
							.slice(
								tokenBeforeRightParen.range[1],
								rightParen.range[0],
							)
							.trim()
							? // If there is a comment between the last element and the ), don't do a fix.
								null
							: fixer.removeRange([
									tokenBeforeRightParen.range[1],
									rightParen.range[0],
								]);
					},
				});
			} else if (!hasRightNewline && needsNewlines) {
				context.report({
					node: rightParen,
					messageId: "expectedBefore",
					fix: fixer => fixer.insertTextBefore(rightParen, "\n"),
				});
			}
		}

		/**
		 * Validates a list of arguments or parameters
		 * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
		 * @param {ASTNode[]} elements The arguments or parameters in the list
		 * @returns {void}
		 */
		function validateArguments(parens, elements) {
			const leftParen = parens.leftParen;
			const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
			const hasLeftNewline = !astUtils.isTokenOnSameLine(
				leftParen,
				tokenAfterLeftParen,
			);
			const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);

			for (let i = 0; i <= elements.length - 2; i++) {
				const currentElement = elements[i];
				const nextElement = elements[i + 1];
				const hasNewLine =
					currentElement.loc.end.line !== nextElement.loc.start.line;

				if (!hasNewLine && needsNewlines) {
					context.report({
						node: currentElement,
						messageId: "expectedBetween",
						fix: fixer => fixer.insertTextBefore(nextElement, "\n"),
					});
				}
			}
		}

		/**
		 * Gets the left paren and right paren tokens of a node.
		 * @param {ASTNode} node The node with parens
		 * @throws {TypeError} Unexpected node type.
		 * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
		 * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
		 * with a single parameter)
		 */
		function getParenTokens(node) {
			switch (node.type) {
				case "NewExpression":
					if (
						!node.arguments.length &&
						!(
							astUtils.isOpeningParenToken(
								sourceCode.getLastToken(node, { skip: 1 }),
							) &&
							astUtils.isClosingParenToken(
								sourceCode.getLastToken(node),
							) &&
							node.callee.range[1] < node.range[1]
						)
					) {
						// If the NewExpression does not have parens (e.g. `new Foo`), return null.
						return null;
					}

				// falls through

				case "CallExpression":
					return {
						leftParen: sourceCode.getTokenAfter(
							node.callee,
							astUtils.isOpeningParenToken,
						),
						rightParen: sourceCode.getLastToken(node),
					};

				case "FunctionDeclaration":
				case "FunctionExpression": {
					const leftParen = sourceCode.getFirstToken(
						node,
						astUtils.isOpeningParenToken,
					);
					const rightParen = node.params.length
						? sourceCode.getTokenAfter(
								node.params.at(-1),
								astUtils.isClosingParenToken,
							)
						: sourceCode.getTokenAfter(leftParen);

					return { leftParen, rightParen };
				}

				case "ArrowFunctionExpression": {
					const firstToken = sourceCode.getFirstToken(node, {
						skip: node.async ? 1 : 0,
					});

					if (!astUtils.isOpeningParenToken(firstToken)) {
						// If the ArrowFunctionExpression has a single param without parens, return null.
						return null;
					}

					const rightParen = node.params.length
						? sourceCode.getTokenAfter(
								node.params.at(-1),
								astUtils.isClosingParenToken,
							)
						: sourceCode.getTokenAfter(firstToken);

					return {
						leftParen: firstToken,
						rightParen,
					};
				}

				case "ImportExpression": {
					const leftParen = sourceCode.getFirstToken(node, 1);
					const rightParen = sourceCode.getLastToken(node);

					return { leftParen, rightParen };
				}

				default:
					throw new TypeError(
						`unexpected node with type ${node.type}`,
					);
			}
		}

		//----------------------------------------------------------------------
		// Public
		//----------------------------------------------------------------------

		return {
			[[
				"ArrowFunctionExpression",
				"CallExpression",
				"FunctionDeclaration",
				"FunctionExpression",
				"ImportExpression",
				"NewExpression",
			]](node) {
				const parens = getParenTokens(node);
				let params;

				if (node.type === "ImportExpression") {
					params = [node.source];
				} else if (astUtils.isFunction(node)) {
					params = node.params;
				} else {
					params = node.arguments;
				}

				if (parens) {
					validateParens(parens, params);

					if (multilineArgumentsOption) {
						validateArguments(parens, params);
					}
				}
			},
		};
	},
};
