/**
 * @fileoverview Source code for spaced-comments rule
 * @author Gyandeep Singh
 * @deprecated in ESLint v8.53.0
 */
"use strict";

const escapeRegExp = require("escape-string-regexp");
const astUtils = require("./utils/ast-utils");

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

/**
 * Escapes the control characters of a given string.
 * @param {string} s A string to escape.
 * @returns {string} An escaped string.
 */
function escape(s) {
	return `(?:${escapeRegExp(s)})`;
}

/**
 * Escapes the control characters of a given string.
 * And adds a repeat flag.
 * @param {string} s A string to escape.
 * @returns {string} An escaped string.
 */
function escapeAndRepeat(s) {
	return `${escape(s)}+`;
}

/**
 * Parses `markers` option.
 * If markers don't include `"*"`, this adds `"*"` to allow JSDoc comments.
 * @param {string[]} [markers] A marker list.
 * @returns {string[]} A marker list.
 */
function parseMarkersOption(markers) {
	// `*` is a marker for JSDoc comments.
	if (!markers.includes("*")) {
		return markers.concat("*");
	}

	return markers;
}

/**
 * Creates string pattern for exceptions.
 * Generated pattern:
 *
 * 1. A space or an exception pattern sequence.
 * @param {string[]} exceptions An exception pattern list.
 * @returns {string} A regular expression string for exceptions.
 */
function createExceptionsPattern(exceptions) {
	let pattern = "";

	/*
	 * A space or an exception pattern sequence.
	 * []                 ==> "\s"
	 * ["-"]              ==> "(?:\s|\-+$)"
	 * ["-", "="]         ==> "(?:\s|(?:\-+|=+)$)"
	 * ["-", "=", "--=="] ==> "(?:\s|(?:\-+|=+|(?:\-\-==)+)$)" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5Cs%7C(%3F%3A%5C-%2B%7C%3D%2B%7C(%3F%3A%5C-%5C-%3D%3D)%2B)%24)
	 */
	if (exceptions.length === 0) {
		// a space.
		pattern += "\\s";
	} else {
		// a space or...
		pattern += "(?:\\s|";

		if (exceptions.length === 1) {
			// a sequence of the exception pattern.
			pattern += escapeAndRepeat(exceptions[0]);
		} else {
			// a sequence of one of the exception patterns.
			pattern += "(?:";
			pattern += exceptions.map(escapeAndRepeat).join("|");
			pattern += ")";
		}
		pattern += `(?:$|[${Array.from(astUtils.LINEBREAKS).join("")}]))`;
	}

	return pattern;
}

/**
 * Creates RegExp object for `always` mode.
 * Generated pattern for beginning of comment:
 *
 * 1. First, a marker or nothing.
 * 2. Next, a space or an exception pattern sequence.
 * @param {string[]} markers A marker list.
 * @param {string[]} exceptions An exception pattern list.
 * @returns {RegExp} A RegExp object for the beginning of a comment in `always` mode.
 */
function createAlwaysStylePattern(markers, exceptions) {
	let pattern = "^";

	/*
	 * A marker or nothing.
	 * ["*"]            ==> "\*?"
	 * ["*", "!"]       ==> "(?:\*|!)?"
	 * ["*", "/", "!<"] ==> "(?:\*|\/|(?:!<))?" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5C*%7C%5C%2F%7C(%3F%3A!%3C))%3F
	 */
	if (markers.length === 1) {
		// the marker.
		pattern += escape(markers[0]);
	} else {
		// one of markers.
		pattern += "(?:";
		pattern += markers.map(escape).join("|");
		pattern += ")";
	}

	pattern += "?"; // or nothing.
	pattern += createExceptionsPattern(exceptions);

	return new RegExp(pattern, "u");
}

/**
 * Creates RegExp object for `never` mode.
 * Generated pattern for beginning of comment:
 *
 * 1. First, a marker or nothing (captured).
 * 2. Next, a space or a tab.
 * @param {string[]} markers A marker list.
 * @returns {RegExp} A RegExp object for `never` mode.
 */
function createNeverStylePattern(markers) {
	const pattern = `^(${markers.map(escape).join("|")})?[ \t]+`;

	return new RegExp(pattern, "u");
}

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

/** @type {import('../shared/types').Rule} */
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: "spaced-comment",
						url: "https://eslint.style/rules/js/spaced-comment",
					},
				},
			],
		},
		type: "suggestion",

		docs: {
			description:
				"Enforce consistent spacing after the `//` or `/*` in a comment",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/spaced-comment",
		},

		fixable: "whitespace",

		schema: [
			{
				enum: ["always", "never"],
			},
			{
				type: "object",
				properties: {
					exceptions: {
						type: "array",
						items: {
							type: "string",
						},
					},
					markers: {
						type: "array",
						items: {
							type: "string",
						},
					},
					line: {
						type: "object",
						properties: {
							exceptions: {
								type: "array",
								items: {
									type: "string",
								},
							},
							markers: {
								type: "array",
								items: {
									type: "string",
								},
							},
						},
						additionalProperties: false,
					},
					block: {
						type: "object",
						properties: {
							exceptions: {
								type: "array",
								items: {
									type: "string",
								},
							},
							markers: {
								type: "array",
								items: {
									type: "string",
								},
							},
							balanced: {
								type: "boolean",
								default: false,
							},
						},
						additionalProperties: false,
					},
				},
				additionalProperties: false,
			},
		],

		messages: {
			unexpectedSpaceAfterMarker:
				"Unexpected space or tab after marker ({{refChar}}) in comment.",
			expectedExceptionAfter:
				"Expected exception block, space or tab after '{{refChar}}' in comment.",
			unexpectedSpaceBefore:
				"Unexpected space or tab before '*/' in comment.",
			unexpectedSpaceAfter:
				"Unexpected space or tab after '{{refChar}}' in comment.",
			expectedSpaceBefore:
				"Expected space or tab before '*/' in comment.",
			expectedSpaceAfter:
				"Expected space or tab after '{{refChar}}' in comment.",
		},
	},

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

		// Unless the first option is never, require a space
		const requireSpace = context.options[0] !== "never";

		/*
		 * Parse the second options.
		 * If markers don't include `"*"`, it's added automatically for JSDoc
		 * comments.
		 */
		const config = context.options[1] || {};
		const balanced = config.block && config.block.balanced;

		const styleRules = ["block", "line"].reduce((rule, type) => {
			const markers = parseMarkersOption(
				(config[type] && config[type].markers) || config.markers || [],
			);
			const exceptions =
				(config[type] && config[type].exceptions) ||
				config.exceptions ||
				[];
			const endNeverPattern = "[ \t]+$";

			// Create RegExp object for valid patterns.
			rule[type] = {
				beginRegex: requireSpace
					? createAlwaysStylePattern(markers, exceptions)
					: createNeverStylePattern(markers),
				endRegex:
					balanced && requireSpace
						? new RegExp(
								`${createExceptionsPattern(exceptions)}$`,
								"u",
							)
						: new RegExp(endNeverPattern, "u"),
				hasExceptions: exceptions.length > 0,
				captureMarker: new RegExp(
					`^(${markers.map(escape).join("|")})`,
					"u",
				),
				markers: new Set(markers),
			};

			return rule;
		}, {});

		/**
		 * Reports a beginning spacing error with an appropriate message.
		 * @param {ASTNode} node A comment node to check.
		 * @param {string} messageId An error message to report.
		 * @param {Array} match An array of match results for markers.
		 * @param {string} refChar Character used for reference in the error message.
		 * @returns {void}
		 */
		function reportBegin(node, messageId, match, refChar) {
			const type = node.type.toLowerCase(),
				commentIdentifier = type === "block" ? "/*" : "//";

			context.report({
				node,
				fix(fixer) {
					const start = node.range[0];
					let end = start + 2;

					if (requireSpace) {
						if (match) {
							end += match[0].length;
						}
						return fixer.insertTextAfterRange([start, end], " ");
					}
					end += match[0].length;
					return fixer.replaceTextRange(
						[start, end],
						commentIdentifier + (match[1] ? match[1] : ""),
					);
				},
				messageId,
				data: { refChar },
			});
		}

		/**
		 * Reports an ending spacing error with an appropriate message.
		 * @param {ASTNode} node A comment node to check.
		 * @param {string} messageId An error message to report.
		 * @param {string} match An array of the matched whitespace characters.
		 * @returns {void}
		 */
		function reportEnd(node, messageId, match) {
			context.report({
				node,
				fix(fixer) {
					if (requireSpace) {
						return fixer.insertTextAfterRange(
							[node.range[0], node.range[1] - 2],
							" ",
						);
					}
					const end = node.range[1] - 2,
						start = end - match[0].length;

					return fixer.replaceTextRange([start, end], "");
				},
				messageId,
			});
		}

		/**
		 * Reports a given comment if it's invalid.
		 * @param {ASTNode} node a comment node to check.
		 * @returns {void}
		 */
		function checkCommentForSpace(node) {
			const type = node.type.toLowerCase(),
				rule = styleRules[type],
				commentIdentifier = type === "block" ? "/*" : "//";

			// Ignores empty comments and comments that consist only of a marker.
			if (node.value.length === 0 || rule.markers.has(node.value)) {
				return;
			}

			const beginMatch = rule.beginRegex.exec(node.value);
			const endMatch = rule.endRegex.exec(node.value);

			// Checks.
			if (requireSpace) {
				if (!beginMatch) {
					const hasMarker = rule.captureMarker.exec(node.value);
					const marker = hasMarker
						? commentIdentifier + hasMarker[0]
						: commentIdentifier;

					if (rule.hasExceptions) {
						reportBegin(
							node,
							"expectedExceptionAfter",
							hasMarker,
							marker,
						);
					} else {
						reportBegin(
							node,
							"expectedSpaceAfter",
							hasMarker,
							marker,
						);
					}
				}

				if (balanced && type === "block" && !endMatch) {
					reportEnd(node, "expectedSpaceBefore");
				}
			} else {
				if (beginMatch) {
					if (!beginMatch[1]) {
						reportBegin(
							node,
							"unexpectedSpaceAfter",
							beginMatch,
							commentIdentifier,
						);
					} else {
						reportBegin(
							node,
							"unexpectedSpaceAfterMarker",
							beginMatch,
							beginMatch[1],
						);
					}
				}

				if (balanced && type === "block" && endMatch) {
					reportEnd(node, "unexpectedSpaceBefore", endMatch);
				}
			}
		}

		return {
			Program() {
				const comments = sourceCode.getAllComments();

				comments
					.filter(token => token.type !== "Shebang")
					.forEach(checkCommentForSpace);
			},
		};
	},
};
