'use strict';
const {isParenthesized} = require('@eslint-community/eslint-utils');
const avoidCapture = require('./utils/avoid-capture.js');
const needsSemicolon = require('./utils/needs-semicolon.js');
const isSameReference = require('./utils/is-same-reference.js');
const getIndentString = require('./utils/get-indent-string.js');
const {getParenthesizedText} = require('./utils/parentheses.js');
const shouldAddParenthesesToConditionalExpressionChild = require('./utils/should-add-parentheses-to-conditional-expression-child.js');
const {extendFixRange} = require('./fix/index.js');
const getScopes = require('./utils/get-scopes.js');

const messageId = 'prefer-ternary';

const isTernary = node => node?.type === 'ConditionalExpression';

function getNodeBody(node) {
	/* c8 ignore next 3 */
	if (!node) {
		return;
	}

	if (node.type === 'ExpressionStatement') {
		return getNodeBody(node.expression);
	}

	if (node.type === 'BlockStatement') {
		const body = node.body.filter(({type}) => type !== 'EmptyStatement');
		if (body.length === 1) {
			return getNodeBody(body[0]);
		}
	}

	return node;
}

const isSingleLineNode = node => node.loc.start.line === node.loc.end.line;

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	const onlySingleLine = context.options[0] === 'only-single-line';
	const {sourceCode} = context;
	const scopeToNamesGeneratedByFixer = new WeakMap();
	const isSafeName = (name, scopes) => scopes.every(scope => {
		const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
		return !generatedNames || !generatedNames.has(name);
	});

	const getText = node => {
		let text = getParenthesizedText(node, sourceCode);
		if (
			!isParenthesized(node, sourceCode)
			&& shouldAddParenthesesToConditionalExpressionChild(node)
		) {
			text = `(${text})`;
		}

		return text;
	};

	function merge(options, mergeOptions) {
		const {
			before = '',
			after = ';',
			consequent,
			alternate,
			node,
		} = options;

		const {
			checkThrowStatement,
			returnFalseIfNotMergeable,
		} = {
			checkThrowStatement: false,
			returnFalseIfNotMergeable: false,
			...mergeOptions,
		};

		if (!consequent || !alternate || consequent.type !== alternate.type) {
			return returnFalseIfNotMergeable ? false : options;
		}

		const {type, argument, delegate, left, right, operator} = consequent;

		if (
			type === 'ReturnStatement'
			&& !isTernary(argument)
			&& !isTernary(alternate.argument)
		) {
			return merge({
				before: `${before}return `,
				after,
				consequent: argument === null ? 'undefined' : argument,
				alternate: alternate.argument === null ? 'undefined' : alternate.argument,
				node,
			});
		}

		if (
			type === 'YieldExpression'
			&& delegate === alternate.delegate
			&& !isTernary(argument)
			&& !isTernary(alternate.argument)
		) {
			return merge({
				before: `${before}yield${delegate ? '*' : ''} (`,
				after: `)${after}`,
				consequent: argument === null ? 'undefined' : argument,
				alternate: alternate.argument === null ? 'undefined' : alternate.argument,
				node,
			});
		}

		if (
			type === 'AwaitExpression'
			&& !isTernary(argument)
			&& !isTernary(alternate.argument)
		) {
			return merge({
				before: `${before}await (`,
				after: `)${after}`,
				consequent: argument,
				alternate: alternate.argument,
				node,
			});
		}

		if (
			checkThrowStatement
			&& type === 'ThrowStatement'
			&& !isTernary(argument)
			&& !isTernary(alternate.argument)
		) {
			// `ThrowStatement` don't check nested

			// If `IfStatement` is not a `BlockStatement`, need add `{}`
			const {parent} = node;
			const needBraces = parent && parent.type !== 'BlockStatement';
			return {
				type,
				before: `${before}${needBraces ? '{\n{{INDENT_STRING}}' : ''}const {{ERROR_NAME}} = `,
				after: `;\n{{INDENT_STRING}}throw {{ERROR_NAME}};${needBraces ? '\n}' : ''}`,
				consequent: argument,
				alternate: alternate.argument,
			};
		}

		if (
			type === 'AssignmentExpression'
			&& operator === alternate.operator
			&& !isTernary(left)
			&& !isTernary(alternate.left)
			&& !isTernary(right)
			&& !isTernary(alternate.right)
			&& isSameReference(left, alternate.left)
		) {
			return merge({
				before: `${before}${sourceCode.getText(left)} ${operator} `,
				after,
				consequent: right,
				alternate: alternate.right,
				node,
			});
		}

		return returnFalseIfNotMergeable ? false : options;
	}

	return {
		IfStatement(node) {
			if (
				(node.parent.type === 'IfStatement' && node.parent.alternate === node)
				|| node.test.type === 'ConditionalExpression'
				|| !node.consequent
				|| !node.alternate
			) {
				return;
			}

			const consequent = getNodeBody(node.consequent);
			const alternate = getNodeBody(node.alternate);

			if (
				onlySingleLine
				&& [consequent, alternate, node.test].some(node => !isSingleLineNode(node))
			) {
				return;
			}

			const result = merge({node, consequent, alternate}, {
				checkThrowStatement: true,
				returnFalseIfNotMergeable: true,
			});

			if (!result) {
				return;
			}

			const problem = {node, messageId};

			// Don't fix if there are comments
			if (sourceCode.getCommentsInside(node).length > 0) {
				return problem;
			}

			const scope = sourceCode.getScope(node);
			problem.fix = function * (fixer) {
				const testText = getText(node.test);
				const consequentText = typeof result.consequent === 'string'
					? result.consequent
					: getText(result.consequent);
				const alternateText = typeof result.alternate === 'string'
					? result.alternate
					: getText(result.alternate);

				let {type, before, after} = result;

				let generateNewVariables = false;
				if (type === 'ThrowStatement') {
					const scopes = getScopes(scope);
					const errorName = avoidCapture('error', scopes, isSafeName);

					for (const scope of scopes) {
						if (!scopeToNamesGeneratedByFixer.has(scope)) {
							scopeToNamesGeneratedByFixer.set(scope, new Set());
						}

						const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
						generatedNames.add(errorName);
					}

					const indentString = getIndentString(node, sourceCode);

					after = after
						.replace('{{INDENT_STRING}}', indentString)
						.replace('{{ERROR_NAME}}', errorName);
					before = before
						.replace('{{INDENT_STRING}}', indentString)
						.replace('{{ERROR_NAME}}', errorName);
					generateNewVariables = true;
				}

				let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`;
				const tokenBefore = sourceCode.getTokenBefore(node);
				const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed);
				if (shouldAddSemicolonBefore) {
					fixed = `;${fixed}`;
				}

				yield fixer.replaceText(node, fixed);

				if (generateNewVariables) {
					yield * extendFixRange(fixer, sourceCode.ast.range);
				}
			};

			return problem;
		},
	};
};

const schema = [
	{
		enum: ['always', 'only-single-line'],
		default: 'always',
	},
];

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Prefer ternary expressions over simple `if-else` statements.',
			recommended: true,
		},
		fixable: 'code',
		schema,
		messages: {
			[messageId]: 'This `if` statement can be replaced by a ternary expression.',
		},
	},
};
