import cleanRegexp from 'clean-regexp';
import regexpTree from 'regexp-tree';
import escapeString from './utils/escape-string.js';
import {isStringLiteral, isNewExpression, isRegexLiteral} from './ast/index.js';

const MESSAGE_ID = 'better-regex';
const MESSAGE_ID_PARSE_ERROR = 'better-regex/parse-error';
const messages = {
	[MESSAGE_ID]: '{{original}} can be optimized to {{optimized}}.',
	[MESSAGE_ID_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}',
};

// `regexp-tree` can optimize `/|/` into `//`, which is not valid JavaScript syntax.
// Normalize to an explicit empty alternative so the autofix always stays parseable.
const normalizeOptimizedRegexLiteral = optimizedRegexLiteral => (
	optimizedRegexLiteral.startsWith('//')
		? `/(?:)/${optimizedRegexLiteral.slice(2)}`
		: optimizedRegexLiteral
);

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	const {sortCharacterClasses} = context.options[0];

	const ignoreList = [];

	if (sortCharacterClasses === false) {
		ignoreList.push('charClassClassrangesMerge');
	}

	context.on('Literal', node => {
		if (!isRegexLiteral(node)) {
			return;
		}

		const {raw: original, regex} = node;
		// Regular Expressions with `u` and `v` flag are not well handled by `regexp-tree`
		// https://github.com/DmitrySoshnikov/regexp-tree/issues/162
		if (regex.flags.includes('u') || regex.flags.includes('v')) {
			return;
		}

		let optimized = original;

		try {
			optimized = regexpTree.optimize(original, undefined, {blacklist: ignoreList}).toString();
			optimized = normalizeOptimizedRegexLiteral(optimized);
		} catch (error) {
			return {
				node,
				messageId: MESSAGE_ID_PARSE_ERROR,
				data: {
					original,
					error: error.message,
				},
			};
		}

		if (original === optimized) {
			return;
		}

		const problem = {
			node,
			messageId: MESSAGE_ID,
			data: {
				original,
				optimized,
			},
		};

		if (
			node.parent.type === 'MemberExpression'
			&& node.parent.object === node
			&& !node.parent.optional
			&& !node.parent.computed
			&& node.parent.property.type === 'Identifier'
			&& (
				node.parent.property.name === 'toString'
				|| node.parent.property.name === 'source'
			)
		) {
			return problem;
		}

		return Object.assign(problem, {
			fix: fixer => fixer.replaceText(node, optimized),
		});
	});

	context.on('NewExpression', node => {
		if (!isNewExpression(node, {name: 'RegExp', minimumArguments: 1})) {
			return;
		}

		const [patternNode, flagsNode] = node.arguments;

		if (!isStringLiteral(patternNode)) {
			return;
		}

		const oldPattern = patternNode.value;
		const flags = isStringLiteral(flagsNode)
			? flagsNode.value
			: '';

		const newPattern = cleanRegexp(oldPattern, flags);

		if (oldPattern !== newPattern) {
			return {
				node,
				messageId: MESSAGE_ID,
				data: {
					original: oldPattern,
					optimized: newPattern,
				},
				fix: fixer => fixer.replaceText(
					patternNode,
					escapeString(newPattern, patternNode.raw.charAt(0)),
				),
			};
		}
	});
};

const schema = [
	{
		type: 'object',
		additionalProperties: false,
		properties: {
			sortCharacterClasses: {
				type: 'boolean',
				description: 'Whether to sort character classes.',
			},
		},
	},
];

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Improve regexes by making them shorter, consistent, and safer.',
			recommended: false,
		},
		fixable: 'code',
		schema,
		defaultOptions: [{sortCharacterClasses: true}],
		messages,
	},
};

export default config;
