import {isMethodCall, isMemberExpression} from './ast/index.js';
import {getParenthesizedRange, isSameReference, isLogicalExpression} from './utils/index.js';

const messages = {
	'non-zero': 'The non-empty check is useless as `Array#some()` returns `false` for an empty array.',
	zero: 'The empty check is useless as `Array#every()` returns `true` for an empty array.',
};

// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
const isLengthCompareZero = node =>
	node.type === 'BinaryExpression'
	&& node.right.type === 'Literal'
	&& node.right.raw === '0'
	&& isMemberExpression(node.left, {property: 'length', optional: false})
	&& isLogicalExpression(node.parent);

function flatLogicalExpression(node) {
	return [node.left, node.right].flatMap(child =>
		child.type === 'LogicalExpression' && child.operator === node.operator
			? flatLogicalExpression(child)
			: [child],
	);
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	const logicalExpressions = [];
	const zeroLengthChecks = new Set();
	const nonZeroLengthChecks = new Set();
	const arraySomeCalls = new Set();
	const arrayEveryCalls = new Set();

	function isUselessLengthCheckNode({node, operator, siblings}) {
		return (
			(
				operator === '||'
				&& zeroLengthChecks.has(node)
				&& siblings.some(condition =>
					arrayEveryCalls.has(condition)
					&& isSameReference(node.left.object, condition.callee.object),
				)
			)
			|| (
				operator === '&&'
				&& nonZeroLengthChecks.has(node)
				&& siblings.some(condition =>
					arraySomeCalls.has(condition)
					&& isSameReference(node.left.object, condition.callee.object),
				)
			)
		);
	}

	function getUselessLengthCheckNode(logicalExpression) {
		const {operator} = logicalExpression;
		return flatLogicalExpression(logicalExpression)
			.filter((node, index, conditions) => isUselessLengthCheckNode({
				node,
				operator,
				siblings: [
					conditions[index - 1],
					conditions[index + 1],
				].filter(Boolean),
			}));
	}

	context.on('BinaryExpression', node => {
		if (isLengthCompareZero(node)) {
			const {operator} = node;
			if (operator === '===') {
				zeroLengthChecks.add(node);
			} else if (operator === '>' || operator === '!==') {
				nonZeroLengthChecks.add(node);
			}
		}
	});

	context.on('CallExpression', node => {
		if (
			isMethodCall(node, {
				optionalCall: false,
				optionalMember: false,
				computed: false,
			})
			&& node.callee.property.type === 'Identifier'
		) {
			if (node.callee.property.name === 'some') {
				arraySomeCalls.add(node);
			} else if (node.callee.property.name === 'every') {
				arrayEveryCalls.add(node);
			}
		}
	});

	context.on('LogicalExpression', node => {
		if (isLogicalExpression(node)) {
			logicalExpressions.push(node);
		}
	});

	context.on('Program:exit', function * () {
		const nodes = new Set(
			logicalExpressions.flatMap(logicalExpression =>
				getUselessLengthCheckNode(logicalExpression),
			),
		);
		const {sourceCode} = context;

		for (const node of nodes) {
			yield {
				loc: {
					start: sourceCode.getLoc(node.left.property).start,
					end: sourceCode.getLoc(node).end,
				},
				messageId: zeroLengthChecks.has(node) ? 'zero' : 'non-zero',
				/** @param {import('eslint').Rule.RuleFixer} fixer */
				fix(fixer) {
					const {left, right} = node.parent;
					const leftRange = getParenthesizedRange(left, context);
					const rightRange = getParenthesizedRange(right, context);
					const range = [];
					if (left === node) {
						range[0] = leftRange[0];
						range[1] = rightRange[0];
					} else {
						range[0] = leftRange[1];
						range[1] = rightRange[1];
					}

					return fixer.removeRange(range);
				},
			};
		}
	});
};

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Disallow useless array length check.',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		messages,
	},
};

export default config;
