/**
 * @fileoverview Rule to enforce a particular function style
 * @author Nicholas C. Zakas
 */
"use strict";

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

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

		defaultOptions: [
			"expression",
			{
				allowArrowFunctions: false,
				overrides: {},
			},
		],

		docs: {
			description:
				"Enforce the consistent use of either `function` declarations or expressions assigned to variables",
			recommended: false,
			frozen: true,
			url: "https://eslint.org/docs/latest/rules/func-style",
		},

		schema: [
			{
				enum: ["declaration", "expression"],
			},
			{
				type: "object",
				properties: {
					allowArrowFunctions: {
						type: "boolean",
					},
					overrides: {
						type: "object",
						properties: {
							namedExports: {
								enum: ["declaration", "expression", "ignore"],
							},
						},
						additionalProperties: false,
					},
				},
				additionalProperties: false,
			},
		],

		messages: {
			expression: "Expected a function expression.",
			declaration: "Expected a function declaration.",
		},
	},

	create(context) {
		const [style, { allowArrowFunctions, overrides }] = context.options;
		const enforceDeclarations = style === "declaration";
		const { namedExports: exportFunctionStyle } = overrides;
		const stack = [];

		const nodesToCheck = {
			FunctionDeclaration(node) {
				stack.push(false);

				if (
					!enforceDeclarations &&
					node.parent.type !== "ExportDefaultDeclaration" &&
					(typeof exportFunctionStyle === "undefined" ||
						node.parent.type !== "ExportNamedDeclaration")
				) {
					context.report({ node, messageId: "expression" });
				}

				if (
					node.parent.type === "ExportNamedDeclaration" &&
					exportFunctionStyle === "expression"
				) {
					context.report({ node, messageId: "expression" });
				}
			},
			"FunctionDeclaration:exit"() {
				stack.pop();
			},

			FunctionExpression(node) {
				stack.push(false);

				if (
					enforceDeclarations &&
					node.parent.type === "VariableDeclarator" &&
					(typeof exportFunctionStyle === "undefined" ||
						node.parent.parent.parent.type !==
							"ExportNamedDeclaration")
				) {
					context.report({
						node: node.parent,
						messageId: "declaration",
					});
				}

				if (
					node.parent.type === "VariableDeclarator" &&
					node.parent.parent.parent.type ===
						"ExportNamedDeclaration" &&
					exportFunctionStyle === "declaration"
				) {
					context.report({
						node: node.parent,
						messageId: "declaration",
					});
				}
			},
			"FunctionExpression:exit"() {
				stack.pop();
			},

			"ThisExpression, Super"() {
				if (stack.length > 0) {
					stack[stack.length - 1] = true;
				}
			},
		};

		if (!allowArrowFunctions) {
			nodesToCheck.ArrowFunctionExpression = function () {
				stack.push(false);
			};

			nodesToCheck["ArrowFunctionExpression:exit"] = function (node) {
				const hasThisOrSuperExpr = stack.pop();

				if (
					!hasThisOrSuperExpr &&
					node.parent.type === "VariableDeclarator"
				) {
					if (
						enforceDeclarations &&
						(typeof exportFunctionStyle === "undefined" ||
							node.parent.parent.parent.type !==
								"ExportNamedDeclaration")
					) {
						context.report({
							node: node.parent,
							messageId: "declaration",
						});
					}

					if (
						node.parent.parent.parent.type ===
							"ExportNamedDeclaration" &&
						exportFunctionStyle === "declaration"
					) {
						context.report({
							node: node.parent,
							messageId: "declaration",
						});
					}
				}
			};
		}

		return nodesToCheck;
	},
};
