import {isMethodCall} from './ast/index.js';
import {isNodeValueNotFunction, isArrayPrototypeProperty} from './utils/index.js';

const MESSAGE_ID_REDUCE = 'reduce';
const MESSAGE_ID_REDUCE_RIGHT = 'reduceRight';
const messages = {
	[MESSAGE_ID_REDUCE]: '`Array#reduce()` is not allowed. Prefer other types of loop for readability.',
	[MESSAGE_ID_REDUCE_RIGHT]: '`Array#reduceRight()` is not allowed. Prefer other types of loop for readability. You may want to call `Array#toReversed()` before looping it.',
};

const cases = [
	// `array.{reduce,reduceRight}()`
	{
		test: callExpression =>
			isMethodCall(callExpression, {
				methods: ['reduce', 'reduceRight'],
				minimumArguments: 1,
				maximumArguments: 2,
				optionalCall: false,
			})
			&& !isNodeValueNotFunction(callExpression.arguments[0]),
		getMethodNode: callExpression => callExpression.callee.property,
		isSimpleOperation(callExpression) {
			const [callback] = callExpression.arguments;

			return (
				callback
				&& (
					// `array.reduce((accumulator, element) => accumulator + element)`
					(callback.type === 'ArrowFunctionExpression' && callback.body.type === 'BinaryExpression')
					// `array.reduce((accumulator, element) => {return accumulator + element;})`
					// `array.reduce(function (accumulator, element){return accumulator + element;})`
					|| (
						(callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
						&& callback.body.type === 'BlockStatement'
						&& callback.body.body.length === 1
						&& callback.body.body[0].type === 'ReturnStatement'
						&& callback.body.body[0].argument.type === 'BinaryExpression'
					)
				)
			);
		},
	},
	// `[].{reduce,reduceRight}.call()` and `Array.{reduce,reduceRight}.call()`
	{
		test: callExpression =>
			isMethodCall(callExpression, {
				method: 'call',
				optionalCall: false,
				optionalMember: false,
			})
			&& isArrayPrototypeProperty(callExpression.callee.object, {
				properties: ['reduce', 'reduceRight'],
			})
			&& (
				!callExpression.arguments[1]
				|| !isNodeValueNotFunction(callExpression.arguments[1])
			),
		getMethodNode: callExpression => callExpression.callee.object.property,
	},
	// `[].{reduce,reduceRight}.apply()` and `Array.{reduce,reduceRight}.apply()`
	{
		test: callExpression =>
			isMethodCall(callExpression, {
				method: 'apply',
				optionalCall: false,
				optionalMember: false,
			})
			&& isArrayPrototypeProperty(callExpression.callee.object, {
				properties: ['reduce', 'reduceRight'],
			}),
		getMethodNode: callExpression => callExpression.callee.object.property,
	},
];

const schema = [
	{
		type: 'object',
		additionalProperties: false,
		properties: {
			allowSimpleOperations: {
				type: 'boolean',
				description: 'Whether to allow simple reduce operations whose callback body is a single binary expression.',
			},
		},
	},
];

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

	context.on('CallExpression', function * (callExpression) {
		for (const {test, getMethodNode, isSimpleOperation} of cases) {
			if (!test(callExpression)) {
				continue;
			}

			if (allowSimpleOperations && isSimpleOperation?.(callExpression)) {
				continue;
			}

			const methodNode = getMethodNode(callExpression);
			yield {
				node: methodNode,
				messageId: methodNode.name,
			};
		}
	});
};

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.',
			recommended: true,
		},
		schema,
		defaultOptions: [{allowSimpleOperations: true}],
		messages,
	},
};

export default config;
