/**
 * @fileoverview Rule to enforce the use of `u` or `v` flag on regular expressions.
 * @author Toru Nagashima
 */

"use strict";

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

const {
	CALL,
	CONSTRUCT,
	ReferenceTracker,
	getStringIfConstant,
} = require("@eslint-community/eslint-utils");
const astUtils = require("./utils/ast-utils.js");
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");

/**
 * Checks whether the flag configuration should be treated as a missing flag.
 * @param {"u"|"v"|undefined} requireFlag A particular flag to require
 * @param {string} flags The regex flags
 * @returns {boolean} Whether the flag configuration results in a missing flag.
 */
function checkFlags(requireFlag, flags) {
	let missingFlag;

	if (requireFlag === "v") {
		missingFlag = !flags.includes("v");
	} else if (requireFlag === "u") {
		missingFlag = !flags.includes("u");
	} else {
		missingFlag = !flags.includes("u") && !flags.includes("v");
	}

	return missingFlag;
}

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

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

		docs: {
			description:
				"Enforce the use of `u` or `v` flag on regular expressions",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/require-unicode-regexp",
		},

		hasSuggestions: true,

		messages: {
			addUFlag: "Add the 'u' flag.",
			addVFlag: "Add the 'v' flag.",
			requireUFlag: "Use the 'u' flag.",
			requireVFlag: "Use the 'v' flag.",
		},

		schema: [
			{
				type: "object",
				properties: {
					requireFlag: {
						enum: ["u", "v"],
					},
				},
				additionalProperties: false,
			},
		],
	},

	create(context) {
		const sourceCode = context.sourceCode;

		const { requireFlag } = context.options[0] ?? {};

		return {
			"Literal[regex]"(node) {
				const flags = node.regex.flags || "";

				const missingFlag = checkFlags(requireFlag, flags);

				if (missingFlag) {
					context.report({
						messageId:
							requireFlag === "v"
								? "requireVFlag"
								: "requireUFlag",
						node,
						suggest: isValidWithUnicodeFlag(
							context.languageOptions.ecmaVersion,
							node.regex.pattern,
							requireFlag,
						)
							? [
									{
										fix(fixer) {
											const replaceFlag =
												requireFlag ?? "u";
											const regex =
												sourceCode.getText(node);
											const slashPos =
												regex.lastIndexOf("/");

											if (requireFlag) {
												const flag =
													requireFlag === "u"
														? "v"
														: "u";

												if (
													regex.includes(
														flag,
														slashPos,
													)
												) {
													return fixer.replaceText(
														node,
														regex.slice(
															0,
															slashPos,
														) +
															regex
																.slice(slashPos)
																.replace(
																	flag,
																	requireFlag,
																),
													);
												}
											}

											return fixer.insertTextAfter(
												node,
												replaceFlag,
											);
										},
										messageId:
											requireFlag === "v"
												? "addVFlag"
												: "addUFlag",
									},
								]
							: null,
					});
				}
			},

			Program(node) {
				const scope = sourceCode.getScope(node);
				const tracker = new ReferenceTracker(scope);
				const trackMap = {
					RegExp: { [CALL]: true, [CONSTRUCT]: true },
				};

				for (const { node: refNode } of tracker.iterateGlobalReferences(
					trackMap,
				)) {
					const [patternNode, flagsNode] = refNode.arguments;

					if (patternNode && patternNode.type === "SpreadElement") {
						continue;
					}
					const pattern = getStringIfConstant(patternNode, scope);
					const flags = getStringIfConstant(flagsNode, scope);

					let missingFlag = !flagsNode;

					if (typeof flags === "string") {
						missingFlag = checkFlags(requireFlag, flags);
					}

					if (missingFlag) {
						context.report({
							messageId:
								requireFlag === "v"
									? "requireVFlag"
									: "requireUFlag",
							node: refNode,
							suggest:
								typeof pattern === "string" &&
								isValidWithUnicodeFlag(
									context.languageOptions.ecmaVersion,
									pattern,
									requireFlag,
								)
									? [
											{
												fix(fixer) {
													const replaceFlag =
														requireFlag ?? "u";

													if (flagsNode) {
														if (
															(flagsNode.type ===
																"Literal" &&
																typeof flagsNode.value ===
																	"string") ||
															flagsNode.type ===
																"TemplateLiteral"
														) {
															const flagsNodeText =
																sourceCode.getText(
																	flagsNode,
																);
															const flag =
																requireFlag ===
																"u"
																	? "v"
																	: "u";

															if (
																flags.includes(
																	flag,
																)
															) {
																// Avoid replacing "u" in escapes like `\uXXXX`
																if (
																	flagsNode.type ===
																		"Literal" &&
																	flagsNode.raw.includes(
																		"\\",
																	)
																) {
																	return null;
																}

																// Avoid replacing "u" in expressions like "`${regularFlags}g`"
																if (
																	flagsNode.type ===
																		"TemplateLiteral" &&
																	(flagsNode
																		.expressions
																		.length ||
																		flagsNode.quasis.some(
																			({
																				value: {
																					raw,
																				},
																			}) =>
																				raw.includes(
																					"\\",
																				),
																		))
																) {
																	return null;
																}

																return fixer.replaceText(
																	flagsNode,
																	flagsNodeText.replace(
																		flag,
																		replaceFlag,
																	),
																);
															}

															return fixer.replaceText(
																flagsNode,
																[
																	flagsNodeText.slice(
																		0,
																		flagsNodeText.length -
																			1,
																	),
																	flagsNodeText.slice(
																		flagsNodeText.length -
																			1,
																	),
																].join(
																	replaceFlag,
																),
															);
														}

														// We intentionally don't suggest concatenating + "u" to non-literals
														return null;
													}

													const penultimateToken =
														sourceCode.getLastToken(
															refNode,
															{ skip: 1 },
														); // skip closing parenthesis

													return fixer.insertTextAfter(
														penultimateToken,
														astUtils.isCommaToken(
															penultimateToken,
														)
															? ` "${replaceFlag}",`
															: `, "${replaceFlag}"`,
													);
												},
												messageId:
													requireFlag === "v"
														? "addVFlag"
														: "addUFlag",
											},
										]
									: null,
						});
					}
				}
			},
		};
	},
};
