import toLocation from './utils/to-location.js';
import {isMethodCall, isNegativeOne, isNumericLiteral} from './ast/index.js';

const MESSAGE_ID = 'consistent-existence-index-check';
const messages = {
	[MESSAGE_ID]: 'Prefer `{{replacementOperator}} {{replacementValue}}` over `{{originalOperator}} {{originalValue}}` to check {{existenceOrNonExistence}}.',
};

const isZero = node => isNumericLiteral(node) && node.value === 0;

/**
@param {parent: import('estree').BinaryExpression} binaryExpression
@returns {{
	replacementOperator: string,
	replacementValue: string,
	originalOperator: string,
	originalValue: string,
} | undefined}
*/
function getReplacement(binaryExpression) {
	const {operator, right} = binaryExpression;

	if (operator === '<' && isZero(right)) {
		return {
			replacementOperator: '===',
			replacementValue: '-1',
			originalOperator: operator,
			originalValue: '0',
		};
	}

	if (operator === '>' && isNegativeOne(right)) {
		return {
			replacementOperator: '!==',
			replacementValue: '-1',
			originalOperator: operator,
			originalValue: '-1',
		};
	}

	if (operator === '>=' && isZero(right)) {
		return {
			replacementOperator: '!==',
			replacementValue: '-1',
			originalOperator: operator,
			originalValue: '0',
		};
	}
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	context.on('VariableDeclarator', /** @param {import('estree').VariableDeclarator} variableDeclarator */ function * (variableDeclarator) {
		if (!(
			variableDeclarator.parent.type === 'VariableDeclaration'
			&& variableDeclarator.parent.kind === 'const'
			&& variableDeclarator.id.type === 'Identifier'
			&& isMethodCall(variableDeclarator.init, {methods: ['indexOf', 'lastIndexOf', 'findIndex', 'findLastIndex']})
		)) {
			return;
		}

		const variableIdentifier = variableDeclarator.id;
		const variables = context.sourceCode.getDeclaredVariables(variableDeclarator);
		const [variable] = variables;

		// Just for safety
		if (
			variables.length !== 1
			|| variable.identifiers.length !== 1
			|| variable.identifiers[0] !== variableIdentifier
		) {
			return;
		}

		for (const {identifier} of variable.references) {
			/** @type {{parent: import('estree').BinaryExpression}} */
			const binaryExpression = identifier.parent;

			if (binaryExpression.type !== 'BinaryExpression' || binaryExpression.left !== identifier) {
				continue;
			}

			const replacement = getReplacement(binaryExpression);

			if (!replacement) {
				return;
			}

			const {left, operator, right} = binaryExpression;
			const {sourceCode} = context;

			const operatorToken = sourceCode.getTokenAfter(
				left,
				token => token.type === 'Punctuator' && token.value === operator,
			);

			const [start] = sourceCode.getRange(operatorToken);
			const [, end] = sourceCode.getRange(right);

			yield {
				node: binaryExpression,
				loc: toLocation([start, end], context),
				messageId: MESSAGE_ID,
				data: {
					...replacement,
					existenceOrNonExistence: `${replacement.replacementOperator === '===' ? 'non-' : ''}existence`,
				},
				* fix(fixer) {
					yield fixer.replaceText(operatorToken, replacement.replacementOperator);

					if (replacement.replacementValue !== replacement.originalValue) {
						yield fixer.replaceText(right, replacement.replacementValue);
					}
				},
			};
		}
	});
};

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'problem',
		docs: {
			description:
				'Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`.',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		messages,
	},
};

export default config;
