'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const isLiteralValue = require('./utils/is-literal-value');

const methods = new Map([
	[
		'slice',
		{
			argumentsIndexes: [0, 1],
			supportObjects: new Set([
				'Array',
				'String',
				'ArrayBuffer',
				'Int8Array',
				'Uint8Array',
				'Uint8ClampedArray',
				'Int16Array',
				'Uint16Array',
				'Int32Array',
				'Uint32Array',
				'Float32Array',
				'Float64Array',
				'BigInt64Array',
				'BigUint64Array'
				// `{Blob,File}#slice()` are not generally used
				// 'Blob'
				// 'File'
			])
		}
	],
	[
		'splice',
		{
			argumentsIndexes: [0],
			supportObjects: new Set([
				'Array'
			])
		}
	]
]);

const OPERATOR_MINUS = '-';

const isPropertiesEqual = (node1, node2) => properties => {
	return properties.every(property => isEqual(node1[property], node2[property]));
};

const isTemplateElementEqual = (node1, node2) => {
	return node1.value &&
		node2.value &&
		node1.tail === node2.tail &&
		isPropertiesEqual(node1.value, node2.value)(['cooked', 'raw']);
};

const isTemplateLiteralEqual = (node1, node2) => {
	const {quasis: quasis1} = node1;
	const {quasis: quasis2} = node2;

	return quasis1.length === quasis2.length &&
		quasis1.every((templateElement, index) => isEqual(templateElement, quasis2[index]));
};

const isEqual = (node1, node2) => {
	if (node1 === node2) {
		return true;
	}

	const compare = isPropertiesEqual(node1, node2);

	if (!compare(['type'])) {
		return false;
	}

	const {type} = node1;

	switch (type) {
		case 'Identifier':
			return compare(['name', 'computed']);
		case 'Literal':
			return compare(['value', 'raw']);
		case 'TemplateLiteral':
			return isTemplateLiteralEqual(node1, node2);
		case 'TemplateElement':
			return isTemplateElementEqual(node1, node2);
		case 'BinaryExpression':
			return compare(['operator', 'left', 'right']);
		case 'MemberExpression':
			return compare(['object', 'property']);
		default:
			return false;
	}
};

const isLengthMemberExpression = node => node &&
	node.type === 'MemberExpression' &&
	node.property &&
	node.property.type === 'Identifier' &&
	node.property.name === 'length' &&
	node.object;

const isLiteralPositiveValue = node =>
	node &&
	node.type === 'Literal' &&
	typeof node.value === 'number' &&
	node.value > 0;

const getLengthMemberExpression = node => {
	if (!node) {
		return;
	}

	const {type, operator, left, right} = node;

	if (
		type !== 'BinaryExpression' ||
		operator !== OPERATOR_MINUS ||
		!left ||
		!isLiteralPositiveValue(right)
	) {
		return;
	}

	if (isLengthMemberExpression(left)) {
		return left;
	}

	// Nested BinaryExpression
	return getLengthMemberExpression(left);
};

const getRemoveAbleNode = (target, argument) => {
	const lengthMemberExpression = getLengthMemberExpression(argument);

	if (lengthMemberExpression && isEqual(target, lengthMemberExpression.object)) {
		return lengthMemberExpression;
	}
};

const getRemovalRange = (node, sourceCode) => {
	let before = sourceCode.getTokenBefore(node);
	let after = sourceCode.getTokenAfter(node);

	let [start, end] = node.range;

	let hasParentheses = true;

	while (hasParentheses) {
		hasParentheses =
			(before.type === 'Punctuator' && before.value === '(') &&
			(after.type === 'Punctuator' && after.value === ')');
		if (hasParentheses) {
			before = sourceCode.getTokenBefore(before);
			after = sourceCode.getTokenAfter(after);
			start = before.range[1];
			end = after.range[0];
		}
	}

	const [nextStart] = after.range;
	const textBetween = sourceCode.text.slice(end, nextStart);

	end += textBetween.match(/\S|$/).index;

	return [start, end];
};

const getMemberName = node => {
	const {type, property} = node;

	if (
		type === 'MemberExpression' &&
		property &&
		property.type === 'Identifier'
	) {
		return property.name;
	}
};

function parse(node) {
	const {callee, arguments: originalArguments} = node;

	let method = callee.property.name;
	let target = callee.object;
	let argumentsNodes = originalArguments;

	if (methods.has(method)) {
		return {
			method,
			target,
			argumentsNodes
		};
	}

	if (method !== 'call' && method !== 'apply') {
		return;
	}

	const isApply = method === 'apply';

	method = getMemberName(callee.object);

	if (!methods.has(method)) {
		return;
	}

	const {
		supportObjects
	} = methods.get(method);

	const parentCallee = callee.object.object;

	if (
		// [].{slice,splice}
		(
			parentCallee.type === 'ArrayExpression' &&
			parentCallee.elements.length === 0
		) ||
		// ''.slice
		(
			method === 'slice' &&
			isLiteralValue(parentCallee, '')
		) ||
		// {Array,String...}.prototype.slice
		// Array.prototype.splice
		(
			getMemberName(parentCallee) === 'prototype' &&
			parentCallee.object.type === 'Identifier' &&
			supportObjects.has(parentCallee.object.name)
		)
	) {
		[target] = originalArguments;

		if (isApply) {
			const [, secondArgument] = originalArguments;
			if (secondArgument.type !== 'ArrayExpression') {
				return;
			}

			argumentsNodes = secondArgument.elements;
		} else {
			argumentsNodes = originalArguments.slice(1);
		}

		return {
			method,
			target,
			argumentsNodes
		};
	}
}

const create = context => ({
	CallExpression: node => {
		if (node.callee.type !== 'MemberExpression') {
			return;
		}

		const parsed = parse(node);

		if (!parsed) {
			return;
		}

		const {
			method,
			target,
			argumentsNodes
		} = parsed;

		const {argumentsIndexes} = methods.get(method);
		const removableNodes = argumentsIndexes
			.map(index => getRemoveAbleNode(target, argumentsNodes[index]))
			.filter(Boolean);

		if (removableNodes.length === 0) {
			return;
		}

		context.report({
			node,
			message: `Prefer negative index over length minus index for \`${method}\`.`,
			fix(fixer) {
				const sourceCode = context.getSourceCode();
				return removableNodes.map(
					node => fixer.removeRange(
						getRemovalRange(node, sourceCode)
					)
				);
			}
		});
	}
});

module.exports = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			url: getDocumentationUrl(__filename)
		},
		fixable: 'code'
	}
};
