/**
 * @fileoverview Rule to flag use of constructors without capital letters
 * @author Nicholas C. Zakas
 */

"use strict";

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

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

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

const CAPS_ALLOWED = [
	"Array",
	"Boolean",
	"Date",
	"Error",
	"Function",
	"Number",
	"Object",
	"RegExp",
	"String",
	"Symbol",
	"BigInt",
];

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

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

		docs: {
			description:
				"Require constructor names to begin with a capital letter",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/new-cap",
		},

		schema: [
			{
				type: "object",
				properties: {
					newIsCap: {
						type: "boolean",
					},
					capIsNew: {
						type: "boolean",
					},
					newIsCapExceptions: {
						type: "array",
						items: {
							type: "string",
						},
					},
					newIsCapExceptionPattern: {
						type: "string",
					},
					capIsNewExceptions: {
						type: "array",
						items: {
							type: "string",
						},
					},
					capIsNewExceptionPattern: {
						type: "string",
					},
					properties: {
						type: "boolean",
					},
				},
				additionalProperties: false,
			},
		],

		defaultOptions: [
			{
				capIsNew: true,
				capIsNewExceptions: CAPS_ALLOWED,
				newIsCap: true,
				newIsCapExceptions: [],
				properties: true,
			},
		],

		messages: {
			upper: "A function with a name starting with an uppercase letter should only be used as a constructor.",
			lower: "A constructor name should not start with a lowercase letter.",
		},
	},

	create(context) {
		const [config] = context.options;
		const skipProperties = !config.properties;

		const newIsCapExceptions = new Set(config.newIsCapExceptions);
		const newIsCapExceptionPattern = config.newIsCapExceptionPattern
			? new RegExp(config.newIsCapExceptionPattern, "u")
			: null;

		const capIsNewExceptions = new Set([
			...config.capIsNewExceptions,
			...CAPS_ALLOWED,
		]);
		const capIsNewExceptionPattern = config.capIsNewExceptionPattern
			? new RegExp(config.capIsNewExceptionPattern, "u")
			: null;

		const listeners = {};

		const sourceCode = context.sourceCode;

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

		/**
		 * Get exact callee name from expression
		 * @param {ASTNode} node CallExpression or NewExpression node
		 * @returns {string} name
		 */
		function extractNameFromExpression(node) {
			return node.callee.type === "Identifier"
				? node.callee.name
				: astUtils.getStaticPropertyName(node.callee) || "";
		}

		/**
		 * Returns the capitalization state of the string -
		 * Whether the first character is uppercase, lowercase, or non-alphabetic
		 * @param {string} str String
		 * @returns {string} capitalization state: "non-alpha", "lower", or "upper"
		 */
		function getCap(str) {
			const firstChar = str.charAt(0);

			const firstCharLower = firstChar.toLowerCase();
			const firstCharUpper = firstChar.toUpperCase();

			if (firstCharLower === firstCharUpper) {
				// char has no uppercase variant, so it's non-alphabetic
				return "non-alpha";
			}
			if (firstChar === firstCharLower) {
				return "lower";
			}
			return "upper";
		}

		/**
		 * Check if capitalization is allowed for a CallExpression
		 * @param {Set<string>} allowedNames Set of allowed callee names
		 * @param {ASTNode} node CallExpression node
		 * @param {string} calleeName Capitalized callee name from a CallExpression
		 * @param {Object} pattern RegExp object from options pattern
		 * @returns {boolean} Returns true if the callee may be capitalized
		 */
		function isCapAllowed(allowedNames, node, calleeName, pattern) {
			const sourceText = sourceCode.getText(node.callee);

			if (allowedNames.has(calleeName) || allowedNames.has(sourceText)) {
				return true;
			}

			if (pattern && pattern.test(sourceText)) {
				return true;
			}

			const callee = astUtils.skipChainExpression(node.callee);

			if (callee.type === "MemberExpression") {
				if (skipProperties) {
					return true;
				}

				if (calleeName === "UTC") {
					// allow if callee is Date.UTC
					return (
						callee.object.type === "Identifier" &&
						callee.object.name === "Date"
					);
				}
			}

			return false;
		}

		/**
		 * Reports the given messageId for the given node. The location will be the start of the property or the callee.
		 * @param {ASTNode} node CallExpression or NewExpression node.
		 * @param {string} messageId The messageId to report.
		 * @returns {void}
		 */
		function report(node, messageId) {
			let callee = astUtils.skipChainExpression(node.callee);

			if (callee.type === "MemberExpression") {
				callee = callee.property;
			}

			context.report({ node, loc: callee.loc, messageId });
		}

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

		if (config.newIsCap) {
			listeners.NewExpression = function (node) {
				const constructorName = extractNameFromExpression(node);

				if (constructorName) {
					const capitalization = getCap(constructorName);
					const isAllowed =
						capitalization !== "lower" ||
						isCapAllowed(
							newIsCapExceptions,
							node,
							constructorName,
							newIsCapExceptionPattern,
						);

					if (!isAllowed) {
						report(node, "lower");
					}
				}
			};
		}

		if (config.capIsNew) {
			listeners.CallExpression = function (node) {
				const calleeName = extractNameFromExpression(node);

				if (calleeName) {
					const capitalization = getCap(calleeName);
					const isAllowed =
						capitalization !== "upper" ||
						isCapAllowed(
							capIsNewExceptions,
							node,
							calleeName,
							capIsNewExceptionPattern,
						);

					if (!isAllowed) {
						report(node, "upper");
					}
				}
			};
		}

		return listeners;
	},
};
