/**
 * @fileoverview Disallow use of multiple spaces.
 * @author Nicholas C. Zakas
 * @deprecated in ESLint v8.53.0
 */

"use strict";

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

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

/** @type {import('../types').Rule.RuleModule} */
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: "no-multi-spaces",
						url: "https://eslint.style/rules/js/no-multi-spaces",
					},
				},
			],
		},
		type: "layout",

		docs: {
			description: "Disallow multiple spaces",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/no-multi-spaces",
		},

		fixable: "whitespace",

		schema: [
			{
				type: "object",
				properties: {
					exceptions: {
						type: "object",
						patternProperties: {
							"^([A-Z][a-z]*)+$": {
								type: "boolean",
							},
						},
						additionalProperties: false,
					},
					ignoreEOLComments: {
						type: "boolean",
						default: false,
					},
				},
				additionalProperties: false,
			},
		],

		messages: {
			multipleSpaces: "Multiple spaces found before '{{displayValue}}'.",
		},
	},

	create(context) {
		const sourceCode = context.sourceCode;
		const options = context.options[0] || {};
		const ignoreEOLComments = options.ignoreEOLComments;
		const exceptions = Object.assign(
			{ Property: true },
			options.exceptions,
		);
		const hasExceptions = Object.keys(exceptions).some(
			key => exceptions[key],
		);

		/**
		 * Formats value of given comment token for error message by truncating its length.
		 * @param {Token} token comment token
		 * @returns {string} formatted value
		 * @private
		 */
		function formatReportedCommentValue(token) {
			const valueLines = token.value.split("\n");
			const value = valueLines[0];
			const formattedValue = `${value.slice(0, 12)}...`;

			return valueLines.length === 1 && value.length <= 12
				? value
				: formattedValue;
		}

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

		return {
			Program() {
				sourceCode.tokensAndComments.forEach(
					(leftToken, leftIndex, tokensAndComments) => {
						if (leftIndex === tokensAndComments.length - 1) {
							return;
						}
						const rightToken = tokensAndComments[leftIndex + 1];

						// Ignore tokens that don't have 2 spaces between them or are on different lines
						if (
							!sourceCode.text
								.slice(leftToken.range[1], rightToken.range[0])
								.includes("  ") ||
							leftToken.loc.end.line < rightToken.loc.start.line
						) {
							return;
						}

						// Ignore comments that are the last token on their line if `ignoreEOLComments` is active.
						if (
							ignoreEOLComments &&
							astUtils.isCommentToken(rightToken) &&
							(leftIndex === tokensAndComments.length - 2 ||
								rightToken.loc.end.line <
									tokensAndComments[leftIndex + 2].loc.start
										.line)
						) {
							return;
						}

						// Ignore tokens that are in a node in the "exceptions" object
						if (hasExceptions) {
							const parentNode = sourceCode.getNodeByRangeIndex(
								rightToken.range[0] - 1,
							);

							if (parentNode && exceptions[parentNode.type]) {
								return;
							}
						}

						let displayValue;

						if (rightToken.type === "Block") {
							displayValue = `/*${formatReportedCommentValue(rightToken)}*/`;
						} else if (rightToken.type === "Line") {
							displayValue = `//${formatReportedCommentValue(rightToken)}`;
						} else {
							displayValue = rightToken.value;
						}

						context.report({
							node: rightToken,
							loc: {
								start: leftToken.loc.end,
								end: rightToken.loc.start,
							},
							messageId: "multipleSpaces",
							data: { displayValue },
							fix: fixer =>
								fixer.replaceTextRange(
									[leftToken.range[1], rightToken.range[0]],
									" ",
								),
						});
					},
				);
			},
		};
	},
};
