/**
 * @fileoverview Rule to require braces in arrow function body.
 * @author Alberto Rodríguez
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
	meta: {
		type: "suggestion",

		defaultOptions: ["as-needed"],

		docs: {
			description: "Require braces around arrow function bodies",
			recommended: false,
			frozen: true,
			url: "https://eslint.org/docs/latest/rules/arrow-body-style",
		},

		schema: {
			anyOf: [
				{
					type: "array",
					items: [
						{
							enum: ["always", "never"],
						},
					],
					minItems: 0,
					maxItems: 1,
				},
				{
					type: "array",
					items: [
						{
							enum: ["as-needed"],
						},
						{
							type: "object",
							properties: {
								requireReturnForObjectLiteral: {
									type: "boolean",
								},
							},
							additionalProperties: false,
						},
					],
					minItems: 0,
					maxItems: 2,
				},
			],
		},

		fixable: "code",

		messages: {
			unexpectedOtherBlock:
				"Unexpected block statement surrounding arrow body.",
			unexpectedEmptyBlock:
				"Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
			unexpectedObjectBlock:
				"Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
			unexpectedSingleBlock:
				"Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
			expectedBlock: "Expected block statement surrounding arrow body.",
		},
	},

	create(context) {
		const options = context.options;
		const always = options[0] === "always";
		const asNeeded = options[0] === "as-needed";
		const never = options[0] === "never";
		const requireReturnForObjectLiteral =
			options[1] && options[1].requireReturnForObjectLiteral;
		const sourceCode = context.sourceCode;
		let funcInfo = null;

		/**
		 * Checks whether the given node has ASI problem or not.
		 * @param {Token} token The token to check.
		 * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
		 */
		function hasASIProblem(token) {
			return (
				token &&
				token.type === "Punctuator" &&
				/^[([/`+-]/u.test(token.value)
			);
		}

		/**
		 * Gets the closing parenthesis by the given node.
		 * @param {ASTNode} node first node after an opening parenthesis.
		 * @returns {Token} The found closing parenthesis token.
		 */
		function findClosingParen(node) {
			let nodeToCheck = node;

			while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
				nodeToCheck = nodeToCheck.parent;
			}
			return sourceCode.getTokenAfter(nodeToCheck);
		}

		/**
		 * Check whether the node is inside of a for loop's init
		 * @param {ASTNode} node node is inside for loop
		 * @returns {boolean} `true` if the node is inside of a for loop, else `false`
		 */
		function isInsideForLoopInitializer(node) {
			if (node && node.parent) {
				if (
					node.parent.type === "ForStatement" &&
					node.parent.init === node
				) {
					return true;
				}
				return isInsideForLoopInitializer(node.parent);
			}
			return false;
		}

		/**
		 * Determines whether a arrow function body needs braces
		 * @param {ASTNode} node The arrow function node.
		 * @returns {void}
		 */
		function validate(node) {
			const arrowBody = node.body;

			if (arrowBody.type === "BlockStatement") {
				const blockBody = arrowBody.body;

				if (blockBody.length !== 1 && !never) {
					return;
				}

				if (
					asNeeded &&
					requireReturnForObjectLiteral &&
					blockBody[0].type === "ReturnStatement" &&
					blockBody[0].argument &&
					blockBody[0].argument.type === "ObjectExpression"
				) {
					return;
				}

				if (
					never ||
					(asNeeded && blockBody[0].type === "ReturnStatement")
				) {
					let messageId;

					if (blockBody.length === 0) {
						messageId = "unexpectedEmptyBlock";
					} else if (
						blockBody.length > 1 ||
						blockBody[0].type !== "ReturnStatement"
					) {
						messageId = "unexpectedOtherBlock";
					} else if (blockBody[0].argument === null) {
						messageId = "unexpectedSingleBlock";
					} else if (
						astUtils.isOpeningBraceToken(
							sourceCode.getFirstToken(blockBody[0], { skip: 1 }),
						)
					) {
						messageId = "unexpectedObjectBlock";
					} else {
						messageId = "unexpectedSingleBlock";
					}

					context.report({
						node,
						loc: arrowBody.loc,
						messageId,
						fix(fixer) {
							const fixes = [];

							if (
								blockBody.length !== 1 ||
								blockBody[0].type !== "ReturnStatement" ||
								!blockBody[0].argument ||
								hasASIProblem(
									sourceCode.getTokenAfter(arrowBody),
								)
							) {
								return fixes;
							}

							const openingBrace =
								sourceCode.getFirstToken(arrowBody);
							const closingBrace =
								sourceCode.getLastToken(arrowBody);
							const firstValueToken = sourceCode.getFirstToken(
								blockBody[0],
								1,
							);
							const lastValueToken = sourceCode.getLastToken(
								blockBody[0],
							);
							const commentsExist =
								sourceCode.commentsExistBetween(
									openingBrace,
									firstValueToken,
								) ||
								sourceCode.commentsExistBetween(
									lastValueToken,
									closingBrace,
								);

							/*
							 * Remove tokens around the return value.
							 * If comments don't exist, remove extra spaces as well.
							 */
							if (commentsExist) {
								fixes.push(
									fixer.remove(openingBrace),
									fixer.remove(closingBrace),
									fixer.remove(
										sourceCode.getTokenAfter(openingBrace),
									), // return keyword
								);
							} else {
								fixes.push(
									fixer.removeRange([
										openingBrace.range[0],
										firstValueToken.range[0],
									]),
									fixer.removeRange([
										lastValueToken.range[1],
										closingBrace.range[1],
									]),
								);
							}

							/*
							 * If the first token of the return value is `{` or the return value is a sequence expression,
							 * enclose the return value by parentheses to avoid syntax error.
							 */
							if (
								astUtils.isOpeningBraceToken(firstValueToken) ||
								blockBody[0].argument.type ===
									"SequenceExpression" ||
								(funcInfo.hasInOperator &&
									isInsideForLoopInitializer(node))
							) {
								if (
									!astUtils.isParenthesised(
										sourceCode,
										blockBody[0].argument,
									)
								) {
									fixes.push(
										fixer.insertTextBefore(
											firstValueToken,
											"(",
										),
										fixer.insertTextAfter(
											lastValueToken,
											")",
										),
									);
								}
							}

							/*
							 * If the last token of the return statement is semicolon, remove it.
							 * Non-block arrow body is an expression, not a statement.
							 */
							if (astUtils.isSemicolonToken(lastValueToken)) {
								fixes.push(fixer.remove(lastValueToken));
							}

							return fixes;
						},
					});
				}
			} else {
				if (
					always ||
					(asNeeded &&
						requireReturnForObjectLiteral &&
						arrowBody.type === "ObjectExpression")
				) {
					context.report({
						node,
						loc: arrowBody.loc,
						messageId: "expectedBlock",
						fix(fixer) {
							const fixes = [];
							const arrowToken = sourceCode.getTokenBefore(
								arrowBody,
								astUtils.isArrowToken,
							);
							const [
								firstTokenAfterArrow,
								secondTokenAfterArrow,
							] = sourceCode.getTokensAfter(arrowToken, {
								count: 2,
							});
							const lastToken = sourceCode.getLastToken(node);

							let parenthesisedObjectLiteral = null;

							if (
								astUtils.isOpeningParenToken(
									firstTokenAfterArrow,
								) &&
								astUtils.isOpeningBraceToken(
									secondTokenAfterArrow,
								)
							) {
								const braceNode =
									sourceCode.getNodeByRangeIndex(
										secondTokenAfterArrow.range[0],
									);

								if (braceNode.type === "ObjectExpression") {
									parenthesisedObjectLiteral = braceNode;
								}
							}

							// If the value is object literal, remove parentheses which were forced by syntax.
							if (parenthesisedObjectLiteral) {
								const openingParenToken = firstTokenAfterArrow;
								const openingBraceToken = secondTokenAfterArrow;

								if (
									astUtils.isTokenOnSameLine(
										openingParenToken,
										openingBraceToken,
									)
								) {
									fixes.push(
										fixer.replaceText(
											openingParenToken,
											"{return ",
										),
									);
								} else {
									// Avoid ASI
									fixes.push(
										fixer.replaceText(
											openingParenToken,
											"{",
										),
										fixer.insertTextBefore(
											openingBraceToken,
											"return ",
										),
									);
								}

								// Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
								fixes.push(
									fixer.remove(
										findClosingParen(
											parenthesisedObjectLiteral,
										),
									),
								);
								fixes.push(
									fixer.insertTextAfter(lastToken, "}"),
								);
							} else {
								fixes.push(
									fixer.insertTextBefore(
										firstTokenAfterArrow,
										"{return ",
									),
								);
								fixes.push(
									fixer.insertTextAfter(lastToken, "}"),
								);
							}

							return fixes;
						},
					});
				}
			}
		}

		return {
			"BinaryExpression[operator='in']"() {
				let info = funcInfo;

				while (info) {
					info.hasInOperator = true;
					info = info.upper;
				}
			},
			ArrowFunctionExpression() {
				funcInfo = {
					upper: funcInfo,
					hasInOperator: false,
				};
			},
			"ArrowFunctionExpression:exit"(node) {
				validate(node);
				funcInfo = funcInfo.upper;
			},
		};
	},
};
