import {getStaticValue} from '@eslint-community/eslint-utils';
import regjsparser from 'regjsparser';
import {isRegexLiteral, isNewExpression, isMethodCall} from './ast/index.js';

const {parse: parseRegExp} = regjsparser;
const MESSAGE_ID_USE_REPLACE_ALL = 'method';
const MESSAGE_ID_USE_STRING = 'pattern';
const messages = {
	[MESSAGE_ID_USE_REPLACE_ALL]: 'Prefer `String#replaceAll()` over `String#replace()`.',
	[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
};

const QUOTE = '\'';

function getPatternReplacement(node) {
	if (!isRegexLiteral(node)) {
		return;
	}

	const {pattern, flags} = node.regex;
	if (flags.replace('u', '').replace('v', '') !== 'g') {
		return;
	}

	let tree;

	try {
		tree = parseRegExp(pattern, flags, {
			unicodePropertyEscape: flags.includes('u'),
			unicodeSet: flags.includes('v'),
			namedGroups: true,
			lookbehind: true,
		});
	} catch {
		return;
	}

	const parts = tree.type === 'alternative' ? tree.body : [tree];
	if (parts.some(part => part.type !== 'value')) {
		return;
	}

	return QUOTE
		+ parts.map(part => {
			const {kind, codePoint, raw} = part;

			if (kind === 'controlLetter') {
				if (codePoint === 13) {
					return String.raw`\r`;
				}

				if (codePoint === 10) {
					return String.raw`\n`;
				}

				if (codePoint === 9) {
					return String.raw`\t`;
				}

				return String.raw`\u{${codePoint.toString(16)}}`;
			}

			if (kind === 'octal') {
				return String.raw`\u{${codePoint.toString(16)}}`;
			}

			let character = raw;
			if (kind === 'identifier') {
				character = character.slice(1);
			}

			if (character === QUOTE || character === '\\') {
				return `\\${character}`;
			}

			return character;
		}).join('')
		+ QUOTE;
}

const isRegExpWithGlobalFlag = (node, scope) => {
	if (isRegexLiteral(node)) {
		return node.regex.flags.includes('g');
	}

	if (
		isNewExpression(node, {name: 'RegExp'})
		&& node.arguments[0]?.type !== 'SpreadElement'
		&& node.arguments[1]?.type === 'Literal'
		&& typeof node.arguments[1].value === 'string'
	) {
		return node.arguments[1].value.includes('g');
	}

	const staticResult = getStaticValue(node, scope);

	// Don't know if there is `g` flag
	if (!staticResult) {
		return false;
	}

	const {value} = staticResult;
	return (
		Object.prototype.toString.call(value) === '[object RegExp]'
		&& value.global
	);
};

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	context.on('CallExpression', node => {
		if (!isMethodCall(node, {
			methods: ['replace', 'replaceAll'],
			argumentsLength: 2,
			optionalCall: false,
		})) {
			return;
		}

		const {
			arguments: [pattern],
			callee: {property},
		} = node;

		if (!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))) {
			return;
		}

		const methodName = property.name;
		const patternReplacement = getPatternReplacement(pattern);

		if (methodName === 'replaceAll') {
			if (!patternReplacement) {
				return;
			}

			return {
				node: pattern,
				messageId: MESSAGE_ID_USE_STRING,
				data: {
					// Show `This pattern can be replaced with a string literal.` for long strings
					replacement: patternReplacement.length < 20 ? patternReplacement : 'a string literal',
				},
				/** @param {import('eslint').Rule.RuleFixer} fixer */
				fix: fixer => fixer.replaceText(pattern, patternReplacement),
			};
		}

		return {
			node: property,
			messageId: MESSAGE_ID_USE_REPLACE_ALL,
			/** @param {import('eslint').Rule.RuleFixer} fixer */
			* fix(fixer) {
				yield fixer.insertTextAfter(property, 'All');

				if (!patternReplacement) {
					return;
				}

				yield fixer.replaceText(pattern, patternReplacement);
			},
		};
	});
};

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Prefer `String#replaceAll()` over regex searches with the global flag.',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		messages,
	},
};

export default config;
