import {checkVueTemplate} from './utils/rule.js';
import {
	isBooleanExpression,
	isControlFlowTest,
	getParenthesizedRange,
	isNodeValueNotFunction,
} from './utils/index.js';
import {removeMemberExpressionProperty} from './fix/index.js';
import {
	isLiteral,
	isNullLiteral,
	isUndefined,
	isMethodCall,
	isMemberExpression,
} from './ast/index.js';

const ERROR_ID_ARRAY_SOME = 'some';
const SUGGESTION_ID_ARRAY_SOME = 'some-suggestion';
const ERROR_ID_ARRAY_FILTER = 'filter';
const messages = {
	[ERROR_ID_ARRAY_SOME]: 'Prefer `.some(…)` over `.{{method}}(…)`.',
	[SUGGESTION_ID_ARRAY_SOME]: 'Replace `.{{method}}(…)` with `.some(…)`.',
	[ERROR_ID_ARRAY_FILTER]: 'Prefer `.some(…)` over non-zero length check from `.filter(…)`.',
};

const isCheckingUndefined = node =>
	node.parent.type === 'BinaryExpression'
	// Not checking yoda expression `null != foo.find()` and `undefined !== foo.find()
	&& node.parent.left === node
	&& (
		(
			(
				node.parent.operator === '!='
				|| node.parent.operator === '=='
				|| node.parent.operator === '==='
				|| node.parent.operator === '!=='
			)
			&& isUndefined(node.parent.right)
		)
		|| (
			(
				node.parent.operator === '!='
				|| node.parent.operator === '=='
			)
			&& isNullLiteral(node.parent.right)
		)
	);
const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
const isLiteralZero = node => isLiteral(node, 0);

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	// `.find(…)`
	// `.findLast(…)`
	context.on('CallExpression', callExpression => {
		if (!isMethodCall(callExpression, {
			methods: ['find', 'findLast'],
			minimumArguments: 1,
			maximumArguments: 2,
			optionalCall: false,
			optionalMember: false,
		})) {
			return;
		}

		const isCompare = isCheckingUndefined(callExpression);
		if (!isCompare && !(isBooleanExpression(callExpression) || isControlFlowTest(callExpression))) {
			return;
		}

		const methodNode = callExpression.callee.property;
		return {
			node: methodNode,
			messageId: ERROR_ID_ARRAY_SOME,
			data: {method: methodNode.name},
			suggest: [
				{
					messageId: SUGGESTION_ID_ARRAY_SOME,
					* fix(fixer) {
						yield fixer.replaceText(methodNode, 'some');

						if (!isCompare) {
							return;
						}

						const {sourceCode} = context;
						const parenthesizedRange = getParenthesizedRange(callExpression, context);
						yield fixer.removeRange([parenthesizedRange[1], sourceCode.getRange(callExpression.parent)[1]]);

						if (callExpression.parent.operator === '!=' || callExpression.parent.operator === '!==') {
							return;
						}

						yield fixer.insertTextBeforeRange(parenthesizedRange, '!');
					},
				},
			],
		};
	});

	// These operators also used in `prefer-includes`, try to reuse the code in future
	// `.{findIndex,findLastIndex}(…) !== -1`
	// `.{findIndex,findLastIndex}(…) != -1`
	// `.{findIndex,findLastIndex}(…) > -1`
	// `.{findIndex,findLastIndex}(…) === -1`
	// `.{findIndex,findLastIndex}(…) == -1`
	// `.{findIndex,findLastIndex}(…) >= 0`
	// `.{findIndex,findLastIndex}(…) < 0`
	context.on('BinaryExpression', binaryExpression => {
		const {left, right, operator} = binaryExpression;

		if (!(
			isMethodCall(left, {
				methods: ['findIndex', 'findLastIndex'],
				argumentsLength: 1,
				optionalCall: false,
				optionalMember: false,
			})
			&& (
				(['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right))
				|| (['>=', '<'].includes(operator) && isLiteralZero(right))
			)
		)) {
			return;
		}

		const methodNode = left.callee.property;
		return {
			node: methodNode,
			messageId: ERROR_ID_ARRAY_SOME,
			data: {method: methodNode.name},
			* fix(fixer) {
				if (['===', '==', '<'].includes(operator)) {
					yield fixer.insertTextBefore(binaryExpression, '!');
				}

				yield fixer.replaceText(methodNode, 'some');

				const {sourceCode} = context;
				const operatorToken = sourceCode.getTokenAfter(
					left,
					token => token.type === 'Punctuator' && token.value === operator,
				);
				const [start] = sourceCode.getRange(operatorToken);
				const [, end] = sourceCode.getRange(binaryExpression);

				yield fixer.removeRange([start, end]);
			},
		};
	});

	// `.filter(…).length > 0`
	// `.filter(…).length !== 0`
	context.on('BinaryExpression', binaryExpression => {
		if (!(
			// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
			(binaryExpression.operator === '>' || binaryExpression.operator === '!==')
			&& binaryExpression.right.type === 'Literal'
			&& binaryExpression.right.raw === '0'
			&& isMemberExpression(binaryExpression.left, {property: 'length', optional: false})
			&& isMethodCall(binaryExpression.left.object, {
				method: 'filter',
				optionalCall: false,
				optionalMember: false,
			})
		)) {
			return;
		}

		const filterCall = binaryExpression.left.object;
		const [firstArgument] = filterCall.arguments;
		if (!firstArgument || isNodeValueNotFunction(firstArgument)) {
			return;
		}

		const filterProperty = filterCall.callee.property;
		return {
			node: filterProperty,
			messageId: ERROR_ID_ARRAY_FILTER,
			* fix(fixer) {
				// `.filter` to `.some`
				yield fixer.replaceText(filterProperty, 'some');

				const {sourceCode} = context;
				const lengthNode = binaryExpression.left;
				/*
					Remove `.length`
					`(( (( array.filter() )).length )) > (( 0 ))`
					------------------------^^^^^^^
				*/
				yield removeMemberExpressionProperty(fixer, lengthNode, context);

				/*
					Remove `> 0`
					`(( (( array.filter() )).length )) > (( 0 ))`
					----------------------------------^^^^^^^^^^
				*/
				yield fixer.removeRange([
					getParenthesizedRange(lengthNode, context)[1],
					sourceCode.getRange(binaryExpression)[1],
				]);

				// The `BinaryExpression` always ends with a number or `)`, no need check for ASI
			},
		};
	});
};

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create: checkVueTemplate(create),
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`.',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		messages,
		hasSuggestions: true,
	},
};

export default config;
