/**
 * @fileoverview A rule to disallow using `this`/`super` before `super()`.
 * @author Toru Nagashima
 */

"use strict";

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

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

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Checks whether or not a given node is a constructor.
 * @param {ASTNode} node A node to check. This node type is one of
 *   `Program`, `FunctionDeclaration`, `FunctionExpression`, and
 *   `ArrowFunctionExpression`.
 * @returns {boolean} `true` if the node is a constructor.
 */
function isConstructorFunction(node) {
	return (
		node.type === "FunctionExpression" &&
		node.parent.type === "MethodDefinition" &&
		node.parent.kind === "constructor"
	);
}

/*
 * Information for each code path segment.
 * - superCalled:  The flag which shows `super()` called in all code paths.
 * - invalidNodes: The array of invalid ThisExpression and Super nodes.
 */
/**
 *
 */
class SegmentInfo {
	/**
	 * Indicates whether `super()` is called in all code paths.
	 * @type {boolean}
	 */
	superCalled = false;

	/**
	 * The array of invalid ThisExpression and Super nodes.
	 * @type {ASTNode[]}
	 */
	invalidNodes = [];
}

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

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

		docs: {
			description:
				"Disallow `this`/`super` before calling `super()` in constructors",
			recommended: true,
			url: "https://eslint.org/docs/latest/rules/no-this-before-super",
		},

		schema: [],

		messages: {
			noBeforeSuper: "'{{kind}}' is not allowed before 'super()'.",
		},
	},

	create(context) {
		/*
		 * Information for each constructor.
		 * - upper:      Information of the upper constructor.
		 * - hasExtends: A flag which shows whether the owner class has a valid
		 *   `extends` part.
		 * - scope:      The scope of the owner class.
		 * - codePath:   The code path of this constructor.
		 */
		let funcInfo = null;

		/** @type {Record<string, SegmentInfo>} */
		let segInfoMap = Object.create(null);

		/**
		 * Gets whether or not `super()` is called in a given code path segment.
		 * @param {CodePathSegment} segment A code path segment to get.
		 * @returns {boolean} `true` if `super()` is called.
		 */
		function isCalled(segment) {
			return !segment.reachable || segInfoMap[segment.id]?.superCalled;
		}

		/**
		 * Checks whether or not this is in a constructor.
		 * @returns {boolean} `true` if this is in a constructor.
		 */
		function isInConstructorOfDerivedClass() {
			return Boolean(
				funcInfo && funcInfo.isConstructor && funcInfo.hasExtends,
			);
		}

		/**
		 * Determines if every segment in a set has been called.
		 * @param {Set<CodePathSegment>} segments The segments to search.
		 * @returns {boolean} True if every segment has been called; false otherwise.
		 */
		function isEverySegmentCalled(segments) {
			for (const segment of segments) {
				if (!isCalled(segment)) {
					return false;
				}
			}

			return true;
		}

		/**
		 * Checks whether or not this is before `super()` is called.
		 * @returns {boolean} `true` if this is before `super()` is called.
		 */
		function isBeforeCallOfSuper() {
			return (
				isInConstructorOfDerivedClass() &&
				!isEverySegmentCalled(funcInfo.currentSegments)
			);
		}

		/**
		 * Sets a given node as invalid.
		 * @param {ASTNode} node A node to set as invalid. This is one of
		 *      a ThisExpression and a Super.
		 * @returns {void}
		 */
		function setInvalid(node) {
			const segments = funcInfo.currentSegments;

			for (const segment of segments) {
				if (segment.reachable) {
					segInfoMap[segment.id].invalidNodes.push(node);
				}
			}
		}

		/**
		 * Sets the current segment as `super` was called.
		 * @returns {void}
		 */
		function setSuperCalled() {
			const segments = funcInfo.currentSegments;

			for (const segment of segments) {
				if (segment.reachable) {
					segInfoMap[segment.id].superCalled = true;
				}
			}
		}

		return {
			/**
			 * Adds information of a constructor into the stack.
			 * @param {CodePath} codePath A code path which was started.
			 * @param {ASTNode} node The current node.
			 * @returns {void}
			 */
			onCodePathStart(codePath, node) {
				if (isConstructorFunction(node)) {
					// Class > ClassBody > MethodDefinition > FunctionExpression
					const classNode = node.parent.parent.parent;

					funcInfo = {
						upper: funcInfo,
						isConstructor: true,
						hasExtends: Boolean(
							classNode.superClass &&
							!astUtils.isNullOrUndefined(classNode.superClass),
						),
						codePath,
						currentSegments: new Set(),
					};
				} else {
					funcInfo = {
						upper: funcInfo,
						isConstructor: false,
						hasExtends: false,
						codePath,
						currentSegments: new Set(),
					};
				}
			},

			/**
			 * Removes the top of stack item.
			 *
			 * And this traverses all segments of this code path then reports every
			 * invalid node.
			 * @param {CodePath} codePath A code path which was ended.
			 * @returns {void}
			 */
			onCodePathEnd(codePath) {
				const isDerivedClass = funcInfo.hasExtends;

				funcInfo = funcInfo.upper;
				if (!isDerivedClass) {
					return;
				}

				/**
				 * A collection of nodes to avoid duplicate reports.
				 * @type {Set<ASTNode>}
				 */
				const reported = new Set();

				codePath.traverseSegments((segment, controller) => {
					const info = segInfoMap[segment.id];
					const invalidNodes = info.invalidNodes.filter(
						/*
						 * Avoid duplicate reports.
						 * When there is a `finally`, invalidNodes may contain already reported node.
						 */
						node => !reported.has(node),
					);

					for (const invalidNode of invalidNodes) {
						reported.add(invalidNode);

						context.report({
							messageId: "noBeforeSuper",
							node: invalidNode,
							data: {
								kind:
									invalidNode.type === "Super"
										? "super"
										: "this",
							},
						});
					}

					if (info.superCalled) {
						controller.skip();
					}
				});
			},

			/**
			 * Initialize information of a given code path segment.
			 * @param {CodePathSegment} segment A code path segment to initialize.
			 * @returns {void}
			 */
			onCodePathSegmentStart(segment) {
				funcInfo.currentSegments.add(segment);

				if (!isInConstructorOfDerivedClass()) {
					return;
				}

				// Initialize info.
				segInfoMap[segment.id] = {
					superCalled:
						segment.prevSegments.length > 0 &&
						segment.prevSegments.every(isCalled),
					invalidNodes: [],
				};
			},

			onUnreachableCodePathSegmentStart(segment) {
				funcInfo.currentSegments.add(segment);
			},

			onUnreachableCodePathSegmentEnd(segment) {
				funcInfo.currentSegments.delete(segment);
			},

			onCodePathSegmentEnd(segment) {
				funcInfo.currentSegments.delete(segment);
			},

			/**
			 * Update information of the code path segment when a code path was
			 * looped.
			 * @param {CodePathSegment} fromSegment The code path segment of the
			 *      end of a loop.
			 * @param {CodePathSegment} toSegment A code path segment of the head
			 *      of a loop.
			 * @returns {void}
			 */
			onCodePathSegmentLoop(fromSegment, toSegment) {
				if (!isInConstructorOfDerivedClass()) {
					return;
				}

				// Update information inside of the loop.
				funcInfo.codePath.traverseSegments(
					{ first: toSegment, last: fromSegment },
					(segment, controller) => {
						const info =
							segInfoMap[segment.id] ?? new SegmentInfo();

						if (info.superCalled) {
							controller.skip();
						} else if (
							segment.prevSegments.length > 0 &&
							segment.prevSegments.every(isCalled)
						) {
							info.superCalled = true;
						}

						segInfoMap[segment.id] = info;
					},
				);
			},

			/**
			 * Reports if this is before `super()`.
			 * @param {ASTNode} node A target node.
			 * @returns {void}
			 */
			ThisExpression(node) {
				if (isBeforeCallOfSuper()) {
					setInvalid(node);
				}
			},

			/**
			 * Reports if this is before `super()`.
			 * @param {ASTNode} node A target node.
			 * @returns {void}
			 */
			Super(node) {
				if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) {
					setInvalid(node);
				}
			},

			/**
			 * Marks `super()` called.
			 * @param {ASTNode} node A target node.
			 * @returns {void}
			 */
			"CallExpression:exit"(node) {
				if (node.callee.type === "Super" && isBeforeCallOfSuper()) {
					setSuperCalled();
				}
			},

			/**
			 * Resets state.
			 * @returns {void}
			 */
			"Program:exit"() {
				segInfoMap = Object.create(null);
			},
		};
	},
};
