/*eslint-env amd */
define([
	'i18n!javascript/nls/problems',
	'module'
], function(ProblemMessages, module) {
	/**
	 * @fileoverview Rule to forbid control charactes from regular expressions.
	 * @author Nicholas C. Zakas
	 */

	"use strict";

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

	module.exports = function(context) {
		var scopeInfo = null;

		/**
		 * Reports a given function node.
		 *
		 * @param {ASTNode} node - A node to report. This is a FunctionExpression or
		 *      an ArrowFunctionExpression.
		 * @returns {void}
		 */
		function report(node) {
			var nodeParent = node.parent; // memberexpression
			context.report(
				nodeParent.property,
				ProblemMessages.noExtraBind);
		}

		/**
		 * Gets the property name of a given node.
		 * If the property name is dynamic, this returns an empty string.
		 *
		 * @param {ASTNode} node - A node to check. This is a MemberExpression.
		 * @returns {string} The property name of the node.
		 */
		function getPropertyName(node) {
			if (node.computed) {
				switch (node.property.type) {
					case "Literal":
						return String(node.property.value);
					case "TemplateLiteral":
						if (node.property.expressions.length === 0) {
							return node.property.quasis[0].value.cooked;
						}
						// fallthrough
					default:
						return false;
				}
			}
			return node.property.name;
		}

		/**
		 * Checks whether or not a given function node is the callee of `.bind()`
		 * method.
		 *
		 * e.g. `(function() {}.bind(foo))`
		 *
		 * @param {ASTNode} node - A node to report. This is a FunctionExpression or
		 *      an ArrowFunctionExpression.
		 * @returns {boolean} `true` if the node is the callee of `.bind()` method.
		 */
		function isCalleeOfBindMethod(node) {
			var parent = node.parent;
			var grandparent = parent.parent;

			return grandparent &&
				grandparent.type === "CallExpression" &&
				grandparent.callee === parent &&
				grandparent.arguments.length === 1 &&
				parent.type === "MemberExpression" &&
				parent.object === node &&
				getPropertyName(parent) === "bind";
		}

		/**
		 * Adds a scope information object to the stack.
		 *
		 * @param {ASTNode} node - A node to add. This node is a FunctionExpression
		 *      or a FunctionDeclaration node.
		 * @returns {void}
		 */
		function enterFunction(node) {
			scopeInfo = {
				isBound: isCalleeOfBindMethod(node),
				thisFound: false,
				upper: scopeInfo
			};
		}

		/**
		 * Removes the scope information object from the top of the stack.
		 * At the same time, this reports the function node if the function has
		 * `.bind()` and the `this` keywords found.
		 *
		 * @param {ASTNode} node - A node to remove. This node is a
		 *      FunctionExpression or a FunctionDeclaration node.
		 * @returns {void}
		 */
		function exitFunction(node) {
			if (scopeInfo.isBound && !scopeInfo.thisFound) {
				report(node);
			}

			scopeInfo = scopeInfo.upper;
		}

		/**
		 * Reports a given arrow function if the function is callee of `.bind()`
		 * method.
		 *
		 * @param {ASTNode} node - A node to report. This node is an
		 *      ArrowFunctionExpression.
		 * @returns {void}
		 */
		function exitArrowFunction(node) {
			if (isCalleeOfBindMethod(node)) {
				report(node);
			}
		}

		/**
		 * Set the mark as the `this` keyword was found in this scope.
		 *
		 * @returns {void}
		 */
		function markAsThisFound() {
			if (scopeInfo) {
				scopeInfo.thisFound = true;
			}
		}

		return {
			"ArrowFunctionExpression:exit": exitArrowFunction,
			"FunctionDeclaration": enterFunction,
			"FunctionDeclaration:exit": exitFunction,
			"FunctionExpression": enterFunction,
			"FunctionExpression:exit": exitFunction,
			"ThisExpression": markAsThisFound
		};
	};

	module.exports.schema = [];

	return module.exports;
});