/* eslint-env amd */
define([
	'./code-path',
	'./code-path-segment',
	'./id-generator',
	'../utils/util'
],
function(CodePath, CodePathSegment, IdGenerator, Utils) {
	/**
	 * @fileoverview A class of the code path analyzer.
	 * @author Toru Nagashima
	 */

	"use strict";
	//------------------------------------------------------------------------------
	// Helpers
	//------------------------------------------------------------------------------

	/**
	 * Checks whether or not a given node is a `case` node (not `default` node).
	 *
	 * @param {ASTNode} node - A `SwitchCase` node to check.
	 * @returns {boolean} `true` if the node is a `case` node (not `default` node).
	 */
	function isCaseNode(node) {
		return Boolean(node.test);
	}

	/**
	 * Checks whether or not a given logical expression node goes different path
	 * between the `true` case and the `false` case.
	 *
	 * @param {ASTNode} node - A node to check.
	 * @returns {boolean} `true` if the node is a test of a choice statement.
	 */
	function isForkingByTrueOrFalse(node) {
		var parent = node.parent;

		switch (parent.type) {
			case "ConditionalExpression":
			case "IfStatement":
			case "WhileStatement":
			case "DoWhileStatement":
			case "ForStatement":
				return parent.test === node;

			case "LogicalExpression":
				return true;

			default:
				return false;
		}
	}

	/**
	 * Gets the boolean value of a given literal node.
	 *
	 * This is used to detect infinity loops (e.g. `while (true) {}`).
	 * Statements preceded by an infinity loop are unreachable if the loop didn't
	 * have any `break` statement.
	 *
	 * @param {ASTNode} node - A node to get.
	 * @returns {boolean|} a boolean value if the node is a Literal node, otherwise undefined.
	 */
	function getBooleanValueIfSimpleConstant(node) {
		if (node.type === "Literal") {
			return Boolean(node.value);
		}
		return;
	}

	/**
	 * Checks that a given identifier node is a reference or not.
	 *
	 * This is used to detect the first throwable node in a `try` block.
	 *
	 * @param {ASTNode} node - An Identifier node to check.
	 * @returns {boolean} `true` if the node is a reference.
	 */
	function isIdentifierReference(node) {
		var parent = node.parent;

		switch (parent.type) {
			case "LabeledStatement":
			case "BreakStatement":
			case "ContinueStatement":
			case "ArrayPattern":
			case "RestElement":
			case "ImportSpecifier":
			case "ImportDefaultSpecifier":
			case "ImportNamespaceSpecifier":
			case "CatchClause":
				return false;

			case "FunctionDeclaration":
			case "FunctionExpression":
			case "ArrowFunctionExpression":
			case "ClassDeclaration":
			case "ClassExpression":
			case "VariableDeclarator":
				return parent.id !== node;

			case "Property":
			case "MethodDefinition":
				return parent.key !== node ||
					parent.computed ||
					parent.shorthand;

			case "AssignmentPattern":
				return parent.key !== node;

			default:
				return true;
		}
	}

	/**
	 * Updates the current segment with the head segment.
	 * This is similar to local branches and tracking branches of git.
	 *
	 * To separate the current and the head is in order to not make useless segments.
	 *
	 * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
	 * events are fired.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function forwardCurrentToHead(analyzer, node) {
		var codePath = analyzer.codePath;
		var state = CodePath.getState(codePath);
		var currentSegments = state.currentSegments;
		var headSegments = state.headSegments;
		var end = Math.max(currentSegments.length, headSegments.length);
		var i, currentSegment, headSegment;

		// Fires leaving events.
		for (i = 0; i < end; ++i) {
			currentSegment = currentSegments[i];
			headSegment = headSegments[i];

			if (currentSegment !== headSegment && currentSegment) {
				if (currentSegment.reachable) {
					analyzer.emitter.emit(
						"onCodePathSegmentEnd",
						currentSegment,
						node);
				}
			}
		}

		// Update state.
		state.currentSegments = headSegments;

		// Fires entering events.
		for (i = 0; i < end; ++i) {
			currentSegment = currentSegments[i];
			headSegment = headSegments[i];

			if (currentSegment !== headSegment && headSegment) {
				CodePathSegment.markUsed(headSegment);
				if (headSegment.reachable) {
					analyzer.emitter.emit(
						"onCodePathSegmentStart",
						headSegment,
						node);
				}
			}
		}

	}

	/**
	 * Updates the current segment with empty.
	 * This is called at the last of functions or the program.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function leaveFromCurrentSegment(analyzer, node) {
		var state = CodePath.getState(analyzer.codePath);
		var currentSegments = state.currentSegments;

		for (var i = 0; i < currentSegments.length; ++i) {
			var currentSegment = currentSegments[i];

			if (currentSegment.reachable) {
				analyzer.emitter.emit(
					"onCodePathSegmentEnd",
					currentSegment,
					node);
			}
		}

		state.currentSegments = [];
	}

	/**
	 * Updates the code path due to the position of a given node in the parent node
	 * thereof.
	 *
	 * For example, if the node is `parent.consequent`, this creates a fork from the
	 * current path.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function preprocess(analyzer, node) {
		var codePath = analyzer.codePath;
		var state = CodePath.getState(codePath);
		var parent = node.parent;

		switch (parent.type) {
			case "LogicalExpression":
				if (parent.right === node) {
					state.makeLogicalRight();
				}
				break;

			case "ConditionalExpression":
			case "IfStatement":

				/*
				 * Fork if this node is at `consequent`/`alternate`.
				 * `popForkContext()` exists at `IfStatement:exit` and
				 * `ConditionalExpression:exit`.
				 */
				if (parent.consequent === node) {
					state.makeIfConsequent();
				} else if (parent.alternate === node) {
					state.makeIfAlternate();
				}
				break;

			case "SwitchCase":
				if (parent.consequent[0] === node) {
					state.makeSwitchCaseBody(false, !parent.test);
				}
				break;

			case "TryStatement":
				if (parent.handler === node) {
					state.makeCatchBlock();
				} else if (parent.finalizer === node) {
					state.makeFinallyBlock();
				}
				break;

			case "WhileStatement":
				if (parent.test === node) {
					state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
				} else {
					state.makeWhileBody();
				}
				break;

			case "DoWhileStatement":
				if (parent.body === node) {
					state.makeDoWhileBody();
				} else {
					state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
				}
				break;

			case "ForStatement":
				if (parent.test === node) {
					state.makeForTest(getBooleanValueIfSimpleConstant(node));
				} else if (parent.update === node) {
					state.makeForUpdate();
				} else if (parent.body === node) {
					state.makeForBody();
				}
				break;

			case "ForInStatement":
			case "ForOfStatement":
				if (parent.left === node) {
					state.makeForInOfLeft();
				} else if (parent.right === node) {
					state.makeForInOfRight();
				} else {
					state.makeForInOfBody();
				}
				break;

			case "AssignmentPattern":

				/*
				 * Fork if this node is at `right`.
				 * `left` is executed always, so it uses the current path.
				 * `popForkContext()` exists at `AssignmentPattern:exit`.
				 */
				if (parent.right === node) {
					state.pushForkContext();
					state.forkBypassPath();
					state.forkPath();
				}
				break;

			default:
				break;
		}
	}

	/**
	 * Updates the code path due to the type of a given node in entering.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function processCodePathToEnter(analyzer, node) {
		var codePath = analyzer.codePath;
		var state = codePath && CodePath.getState(codePath);
		var parent = node.parent;

		switch (node.type) {
			case "Program":
			case "FunctionDeclaration":
			case "FunctionExpression":
			case "ArrowFunctionExpression":
				if (codePath) {

					// Emits onCodePathSegmentStart events if updated.
					forwardCurrentToHead(analyzer, node);
				}

				// Create the code path of this scope.
				codePath = analyzer.codePath = new CodePath(
					analyzer.idGenerator.next(),
					codePath,
					analyzer.onLooped
				);
				state = CodePath.getState(codePath);

				// Emits onCodePathStart events.
				analyzer.emitter.emit("onCodePathStart", codePath, node);
				break;

			case "LogicalExpression":
				state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
				break;

			case "ConditionalExpression":
			case "IfStatement":
				state.pushChoiceContext("test", false);
				break;

			case "SwitchStatement":
				state.pushSwitchContext(
					node.cases.some(isCaseNode),
					Utils.getLabel(node));
				break;

			case "TryStatement":
				state.pushTryContext(Boolean(node.finalizer));
				break;

			case "SwitchCase":

				/*
				 * Fork if this node is after the 2st node in `cases`.
				 * It's similar to `else` blocks.
				 * The next `test` node is processed in this path.
				 */
				if (parent.discriminant !== node && parent.cases[0] !== node) {
					state.forkPath();
				}
				break;

			case "WhileStatement":
			case "DoWhileStatement":
			case "ForStatement":
			case "ForInStatement":
			case "ForOfStatement":
				state.pushLoopContext(node.type, Utils.getLabel(node));
				break;

			case "LabeledStatement":
				if (!Utils.isBreakableStatement(node.body)) {
					state.pushBreakContext(false, node.label.name);
				}
				break;

			default:
				break;
		}

		// Emits onCodePathSegmentStart events if updated.
		forwardCurrentToHead(analyzer, node);
	}

	/**
	 * Updates the code path due to the type of a given node in leaving.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function processCodePathToExit(analyzer, node) {
		var codePath = analyzer.codePath;
		var state = CodePath.getState(codePath);
		var dontForward = false;

		switch (node.type) {
			case "IfStatement":
			case "ConditionalExpression":
			case "LogicalExpression":
				state.popChoiceContext();
				break;

			case "SwitchStatement":
				state.popSwitchContext();
				break;

			case "SwitchCase":

				/*
				 * This is the same as the process at the 1st `consequent` node in
				 * `preprocess` function.
				 * Must do if this `consequent` is empty.
				 */
				if (node.consequent.length === 0) {
					state.makeSwitchCaseBody(true, !node.test);
				}
				if (state.forkContext.reachable) {
					dontForward = true;
				}
				break;

			case "TryStatement":
				state.popTryContext();
				break;

			case "BreakStatement":
				forwardCurrentToHead(analyzer, node);
				state.makeBreak(node.label && node.label.name);
				dontForward = true;
				break;

			case "ContinueStatement":
				forwardCurrentToHead(analyzer, node);
				state.makeContinue(node.label && node.label.name);
				dontForward = true;
				break;

			case "ReturnStatement":
				forwardCurrentToHead(analyzer, node);
				state.makeReturn();
				dontForward = true;
				break;

			case "ThrowStatement":
				forwardCurrentToHead(analyzer, node);
				state.makeThrow();
				dontForward = true;
				break;

			case "Identifier":
				if (isIdentifierReference(node)) {
					state.makeFirstThrowablePathInTryBlock();
					dontForward = true;
				}
				break;

			case "CallExpression":
			case "MemberExpression":
			case "NewExpression":
				state.makeFirstThrowablePathInTryBlock();
				break;

			case "WhileStatement":
			case "DoWhileStatement":
			case "ForStatement":
			case "ForInStatement":
			case "ForOfStatement":
				state.popLoopContext();
				break;

			case "AssignmentPattern":
				state.popForkContext();
				break;

			case "LabeledStatement":
				if (!Utils.isBreakableStatement(node.body)) {
					state.popBreakContext();
				}
				break;

			default:
				break;
		}

		/*
		 * Skip updating the current segment to avoid creating useless segments if
		 * the node type is the same as the parent node type.
		 */
		if (!dontForward && (!node.parent || node.type !== node.parent.type)) {

			// Emits onCodePathSegmentStart events if updated.
			forwardCurrentToHead(analyzer, node);
		}
	}

	/**
	 * Updates the code path to finalize the current code path.
	 *
	 * @param {CodePathAnalyzer} analyzer - The instance.
	 * @param {ASTNode} node - The current AST node.
	 * @returns {void}
	 */
	function postprocess(analyzer, node) {
		switch (node.type) {
			case "Program":
			case "FunctionDeclaration":
			case "FunctionExpression":
			case "ArrowFunctionExpression":
				var codePath = analyzer.codePath;

				// Mark the current path as the final node.
				CodePath.getState(codePath).makeFinal();

				// Emits onCodePathSegmentEnd event of the current segments.
				leaveFromCurrentSegment(analyzer, node);

				// Emits onCodePathEnd event of this code path.
				analyzer.emitter.emit("onCodePathEnd", codePath, node);

				codePath = analyzer.codePath = analyzer.codePath.upper;
				break;

			default:
				break;
		}
	}

	//------------------------------------------------------------------------------
	// Public Interface
	//------------------------------------------------------------------------------

	/**
	 * The class to analyze code paths.
	 * This class implements the EventGenerator interface.
	 *
	 * @constructor
	 * @param {EventGenerator} eventGenerator - An event generator to wrap.
	 */
	function CodePathAnalyzer(eventGenerator) {
		this.original = eventGenerator;
		this.emitter = eventGenerator.emitter;
		this.codePath = null;
		this.idGenerator = new IdGenerator("s");
		this.currentNode = null;
		this.onLooped = this.onLooped.bind(this);
	}

	CodePathAnalyzer.prototype = {
		constructor: CodePathAnalyzer,

		/**
		 * Does the process to enter a given AST node.
		 * This updates state of analysis and calls `enterNode` of the wrapped.
		 *
		 * @param {ASTNode} node - A node which is entering.
		 * @returns {void}
		 */
		enterNode: function(node) {
			this.currentNode = node;

			// Updates the code path due to node's position in its parent node.
			if (node.parent) {
				preprocess(this, node);
			}

			// Updates the code path.
			// And emits onCodePathStart/onCodePathSegmentStart events.
			processCodePathToEnter(this, node);

			// Emits node events.
			this.original.enterNode(node);

			this.currentNode = null;
		},

		/**
		 * Does the process to leave a given AST node.
		 * This updates state of analysis and calls `leaveNode` of the wrapped.
		 *
		 * @param {ASTNode} node - A node which is leaving.
		 * @returns {void}
		 */
		leaveNode: function(node) {
			this.currentNode = node;// Updates the code path.
			// And emits onCodePathStart/onCodePathSegmentStart events.
			processCodePathToExit(this, node);

			// Emits node events.
			this.original.leaveNode(node);

			// Emits the last onCodePathStart/onCodePathSegmentStart events.
			postprocess(this, node);

			this.currentNode = null;
		},
		/**
		 * This is called on a code path looped.
		 * Then this raises a looped event.
		 *
		 * @param {CodePathSegment} fromSegment - A segment of prev.
		 * @param {CodePathSegment} toSegment - A segment of next.
		 * @returns {void}
		 */
		onLooped: function(fromSegment, toSegment) {
			if (fromSegment.reachable && toSegment.reachable) {
				this.emitter.emit(
					"onCodePathSegmentLoop",
					fromSegment,
					toSegment,
					this.currentNode
				);
			}
		}
	};

	return CodePathAnalyzer;
});