import {
	checkVueTemplate,
	getParenthesizedRange,
	getTokenStore,
} from './utils/index.js';
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
import builtinErrors from './shared/builtin-errors.js';
import typedArray from './shared/typed-array.js';

const isInstanceofToken = token => token.value === 'instanceof' && token.type === 'Keyword';

const MESSAGE_ID = 'no-instanceof-builtins';
const MESSAGE_ID_SWITCH_TO_TYPE_OF = 'switch-to-type-of';
const messages = {
	[MESSAGE_ID]: 'Avoid using `instanceof` for type checking as it can lead to unreliable results.',
	[MESSAGE_ID_SWITCH_TO_TYPE_OF]: 'Switch to `typeof … === \'{{type}}\'`.',
};

const primitiveWrappers = new Set([
	'String',
	'Number',
	'Boolean',
	'BigInt',
	'Symbol',
]);

const strictStrategyConstructors = [
	// Error types
	...builtinErrors,

	// Collection types
	'Map',
	'Set',
	'WeakMap',
	'WeakRef',
	'WeakSet',

	// Arrays and Typed Arrays
	'ArrayBuffer',
	...typedArray,

	// Data types
	'Object',

	// Regular Expressions
	'RegExp',

	// Async and functions
	'Promise',
	'Proxy',

	// Other
	'DataView',
	'Date',
	'SharedArrayBuffer',
	'FinalizationRegistry',
];

const replaceWithFunctionCall = (node, context, functionName) => function * (fixer) {
	const {left, right} = node;
	const tokenStore = getTokenStore(context, node);
	const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);

	yield fixSpaceAroundKeyword(fixer, node, context);

	const range = getParenthesizedRange(left, {sourceCode: tokenStore});
	yield fixer.insertTextBeforeRange(range, functionName + '(');
	yield fixer.insertTextAfterRange(range, ')');

	yield replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, context, tokenStore);
	yield replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
};

const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
	const {left, right} = node;
	const tokenStore = getTokenStore(context, node);
	const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
	const {sourceCode} = context;

	// Check if the node is in a Vue template expression
	const vueExpressionContainer = sourceCode.getAncestors(node).findLast(ancestor => ancestor.type === 'VExpressionContainer');

	// Get safe quote
	const safeQuote = vueExpressionContainer ? (sourceCode.getText(vueExpressionContainer)[0] === '"' ? '\'' : '"') : '\'';

	yield fixSpaceAroundKeyword(fixer, node, context);

	const leftRange = getParenthesizedRange(left, {sourceCode: tokenStore});
	yield fixer.insertTextBeforeRange(leftRange, 'typeof ');

	yield fixer.replaceText(instanceofToken, '===');

	const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});

	yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
};

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	const {
		useErrorIsError = false,
		strategy = 'loose',
		include = [],
		exclude = [],
	} = context.options[0] ?? {};

	const forbiddenConstructors = new Set(strategy === 'strict'
		? [...strictStrategyConstructors, ...include]
		: include);

	context.on('BinaryExpression', /** @param {import('estree').BinaryExpression} node */ node => {
		const {right, operator} = node;

		if ((operator !== 'instanceof') || (right.type !== 'Identifier') || exclude.includes(right.name)) {
			return;
		}

		const constructorName = right.name;

		/** @type {import('eslint').Rule.ReportDescriptor} */
		const problem = {
			node,
			messageId: MESSAGE_ID,
		};

		if (
			constructorName === 'Array'
			|| (constructorName === 'Error' && useErrorIsError)
		) {
			const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
			problem.fix = replaceWithFunctionCall(node, context, functionName);
			return problem;
		}

		if (constructorName === 'Function') {
			problem.fix = replaceWithTypeOfExpression(node, context);
			return problem;
		}

		if (primitiveWrappers.has(constructorName)) {
			problem.suggest = [
				{
					messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
					data: {type: constructorName.toLowerCase()},
					fix: replaceWithTypeOfExpression(node, context),
				},
			];
			return problem;
		}

		if (!forbiddenConstructors.has(constructorName)) {
			return;
		}

		return problem;
	});
};

const schema = [
	{
		type: 'object',
		properties: {
			useErrorIsError: {
				type: 'boolean',
			},
			strategy: {
				enum: [
					'loose',
					'strict',
				],
			},
			include: {
				type: 'array',
				items: {
					type: 'string',
				},
			},
			exclude: {
				type: 'array',
				items: {
					type: 'string',
				},
			},
		},
		additionalProperties: false,
	},
];

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create: checkVueTemplate(create),
	meta: {
		type: 'problem',
		docs: {
			description: 'Disallow `instanceof` with built-in objects',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		schema,
		defaultOptions: [{
			useErrorIsError: false,
			strategy: 'loose',
			include: [],
			exclude: [],
		}],
		hasSuggestions: true,
		messages,
	},
};

export default config;
