import {isLiteral} from './ast/index.js';
import {
	addParenthesizesToReturnOrThrowExpression,
	removeSpacesAfter,
} from './fix/index.js';
import {
	needsSemicolon,
	isParenthesized,
	isOnSameLine,
	isUnresolvedVariable,
} from './utils/index.js';

const MESSAGE_ID_ERROR = 'no-typeof-undefined/error';
const MESSAGE_ID_SUGGESTION = 'no-typeof-undefined/suggestion';
const messages = {
	[MESSAGE_ID_ERROR]: 'Compare with `undefined` directly instead of using `typeof`.',
	[MESSAGE_ID_SUGGESTION]: 'Switch to `… {{operator}} undefined`.',
};

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

	context.on('BinaryExpression', binaryExpression => {
		if (!(
			(
				binaryExpression.operator === '==='
				|| binaryExpression.operator === '!=='
				|| binaryExpression.operator === '=='
				|| binaryExpression.operator === '!='
			)
			&& binaryExpression.left.type === 'UnaryExpression'
			&& binaryExpression.left.operator === 'typeof'
			&& binaryExpression.left.prefix
			&& isLiteral(binaryExpression.right, 'undefined')
		)) {
			return;
		}

		const {left: typeofNode, right: undefinedString, operator} = binaryExpression;
		const {sourceCode} = context;
		const valueNode = typeofNode.argument;
		const isGlobalVariable = valueNode.type === 'Identifier'
			&& (sourceCode.isGlobalReference(valueNode) || isUnresolvedVariable(valueNode, context));

		if (!checkGlobalVariables && isGlobalVariable) {
			return;
		}

		const [typeofToken, secondToken] = sourceCode.getFirstTokens(typeofNode, 2);

		const fix = function * (fixer) {
			// Change `==`/`!=` to `===`/`!==`
			if (operator === '==' || operator === '!=') {
				const operatorToken = sourceCode.getTokenAfter(
					typeofNode,
					token => token.type === 'Punctuator' && token.value === operator,
				);

				yield fixer.insertTextAfter(operatorToken, '=');
			}

			yield fixer.replaceText(undefinedString, 'undefined');

			yield fixer.remove(typeofToken);
			yield removeSpacesAfter(typeofToken, context, fixer);

			const {parent} = binaryExpression;
			if (
				(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
				&& parent.argument === binaryExpression
				&& !isOnSameLine(typeofToken, secondToken, context)
				&& !isParenthesized(binaryExpression, context)
				&& !isParenthesized(typeofNode, context)
			) {
				yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
				return;
			}

			const tokenBefore = sourceCode.getTokenBefore(binaryExpression);
			if (needsSemicolon(tokenBefore, context, secondToken.value)) {
				yield fixer.insertTextBefore(binaryExpression, ';');
			}
		};

		const problem = {
			node: binaryExpression,
			loc: sourceCode.getLoc(typeofToken),
			messageId: MESSAGE_ID_ERROR,
		};

		if (isGlobalVariable) {
			problem.suggest = [
				{
					messageId: MESSAGE_ID_SUGGESTION,
					data: {operator: operator.startsWith('!') ? '!==' : '==='},
					fix,
				},
			];
		} else {
			problem.fix = fix;
		}

		return problem;
	});
};

const schema = [
	{
		type: 'object',
		additionalProperties: false,
		properties: {
			checkGlobalVariables: {
				type: 'boolean',
			},
		},
	},
];

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Disallow comparing `undefined` using `typeof`.',
			recommended: 'unopinionated',
		},
		fixable: 'code',
		hasSuggestions: true,
		schema,
		defaultOptions: [{checkGlobalVariables: false}],
		messages,
	},
};

export default config;
