/**
 * @fileoverview Rule to flag when a function has too many parameters
 * @author Ilya Volodin
 */

"use strict";

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

const astUtils = require("./utils/ast-utils");
const { upperCaseFirst } = require("../shared/string-utils");

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

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

		docs: {
			description:
				"Enforce a maximum number of parameters in function definitions",
			dialects: ["JavaScript", "TypeScript"],
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/max-params",
		},

		schema: [
			{
				oneOf: [
					{
						type: "integer",
						minimum: 0,
					},
					{
						type: "object",
						properties: {
							maximum: {
								type: "integer",
								minimum: 0,
							},
							max: {
								type: "integer",
								minimum: 0,
							},
							countVoidThis: {
								type: "boolean",
								description:
									"Whether to count a `this` declaration when the type is `void`.",
							},
							countThis: {
								enum: ["never", "except-void", "always"],
								description:
									"Whether to count a `this` declaration.",
							},
						},
						additionalProperties: false,
						not: { required: ["countVoidThis", "countThis"] },
					},
				],
			},
		],

		defaultOptions: [3],

		messages: {
			exceed: "{{name}} has too many parameters ({{count}}). Maximum allowed is {{max}}.",
		},
	},

	create(context) {
		const sourceCode = context.sourceCode;
		const option = context.options[0];
		let numParams = 3;
		let countThis = "except-void";

		if (typeof option === "object") {
			if (
				Object.hasOwn(option, "maximum") ||
				Object.hasOwn(option, "max")
			) {
				numParams = option.maximum || option.max;
			}

			countThis = option.countThis;
			if (!countThis && Object.hasOwn(option, "countVoidThis")) {
				countThis = option.countVoidThis ? "always" : "except-void";
			}
			countThis ||= "except-void";
		}
		if (typeof option === "number") {
			numParams = option;
		}

		/**
		 * Checks a function to see if it has too many parameters.
		 * @param {ASTNode} node The node to check.
		 * @returns {void}
		 * @private
		 */
		function checkFunction(node) {
			const thisParam =
				node.params.length > 0 &&
				node.params[0].type === "Identifier" &&
				node.params[0].name === "this"
					? node.params[0]
					: null;

			let effectiveParamCount = node.params.length;
			if (thisParam) {
				if (countThis === "never") {
					effectiveParamCount = node.params.length - 1;
				} else if (
					countThis === "except-void" &&
					thisParam.typeAnnotation?.typeAnnotation.type ===
						"TSVoidKeyword"
				) {
					effectiveParamCount = node.params.length - 1;
				}
			}

			if (effectiveParamCount > numParams) {
				context.report({
					loc: astUtils.getFunctionHeadLoc(node, sourceCode),
					node,
					messageId: "exceed",
					data: {
						name: upperCaseFirst(
							astUtils.getFunctionNameWithKind(node),
						),
						count: effectiveParamCount,
						max: numParams,
					},
				});
			}
		}

		return {
			FunctionDeclaration: checkFunction,
			ArrowFunctionExpression: checkFunction,
			FunctionExpression: checkFunction,
			TSDeclareFunction: checkFunction,
			TSFunctionType: checkFunction,
		};
	},
};