/*eslint-env amd */
define([
	'i18n!javascript/nls/problems',
	'module'
], function(ProblemMessages, module) {
/**
 * @fileoverview Rule to flag comparisons to null without a type-checking
 * operator.
 * @author Ian Christian Myers
 */

"use strict";

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

module.exports = function(context) {
	var INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/;

	/**
	 * Parses and normalizes an option object.
	 * @param {object} options - An option object to parse.
	 * @returns {object} The parsed and normalized option object.
	 */
	function parseOptions(options) {
		options = options || {};
		return {
			boolean: "boolean" in options ? Boolean(options.boolean) : true,
			number: "number" in options ? Boolean(options.number) : true,
			string: "string" in options ? Boolean(options.string) : true
		};
	}

	/**
	 * Checks whether or not a node is a double logical negating.
	 * @param {ASTNode} node - An UnaryExpression node to check.
	 * @returns {boolean} Whether or not the node is a double logical negating.
	 */
	function isDoubleLogicalNegating(node) {
		return node.operator === "!" &&
			node.argument.type === "UnaryExpression" &&
			node.argument.operator === "!";
	}

	/**
	 * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
	 * @param {ASTNode} node - An UnaryExpression node to check.
	 * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
	 */
	function isBinaryNegatingOfIndexOf(node) {
		return node.operator === "~" &&
			node.argument.type === "CallExpression" &&
			node.argument.callee.type === "MemberExpression" &&
			node.argument.callee.property.type === "Identifier" &&
			INDEX_OF_PATTERN.test(node.argument.callee.property.name);
	}

	/**
	 * Checks whether or not a node is a multiplying by one.
	 * @param {BinaryExpression} node - A BinaryExpression node to check.
	 * @returns {boolean} Whether or not the node is a multiplying by one.
	 */
	function isMultiplyByOne(node) {
		return node.operator === "*" && (
			node.left.type === "Literal" && node.left.value === 1 ||
			node.right.type === "Literal" && node.right.value === 1
		);
	}

	/**
	 * Checks whether the result of a node is numeric or not
	 * @param {ASTNode} node The node to test
	 * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
	 */
	function isNumeric(node) {
		return node.type === "Literal"
			&& typeof node.value === "number"
			|| node.type === "CallExpression"
			&& (node.callee.name === "Number" ||
				node.callee.name === "parseInt" ||
				node.callee.name === "parseFloat");
	}

	/**
	 * Returns the first non-numeric operand in a BinaryExpression. Designed to be
	 * used from bottom to up since it walks up the BinaryExpression trees using
	 * node.parent to find the result.
	 * @param {BinaryExpression} node The BinaryExpression node to be walked up on
	 * @returns {ASTNode|
	} The first non-numeric item in the BinaryExpression tree or 

	 */
	function getNonNumericOperand(node) {
		var left = node.left,
			right = node.right;

		if (right.type !== "BinaryExpression" && !isNumeric(right)) {
			return right;
		}

		if (left.type !== "BinaryExpression" && !isNumeric(left)) {
			return left;
		}
	}

	/**
	 * Checks whether or not a node is a concatenating with an empty string.
	 * @param {ASTNode} node - A BinaryExpression node to check.
	 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
	 */
	function isConcatWithEmptyString(node) {
		return node.operator === "+" && (
			(node.left.type === "Literal" && node.left.value === "") ||
			(node.right.type === "Literal" && node.right.value === "")
		);
	}

	/**
	 * Checks whether or not a node is appended with an empty string.
	 * @param {ASTNode} node - An AssignmentExpression node to check.
	 * @returns {boolean} Whether or not the node is appended with an empty string.
	 */
	function isAppendEmptyString(node) {
		return node.operator === "+=" && node.right.type === "Literal" && node.right.value === "";
	}

	/**
	 * Gets a node that is the left or right operand of a node, is not the specified literal.
	 * @param {ASTNode} node - A BinaryExpression node to get.
	 * @param {any} value - A literal value to check.
	 * @returns {ASTNode} A node that is the left or right operand of the node, is not the specified literal.
	 */
	function getOtherOperand(node, value) {
		if (node.left.type === "Literal" && node.left.value === value) {
			return node.right;
		}
		return node.left;
	}
	var options = parseOptions(context.options[0]);

	return {
		"UnaryExpression": function(node) {
			// !!foo
			if (options.boolean && isDoubleLogicalNegating(node)) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionBoolean,
					{
						code: context.getSource(node.argument.argument)
					});
			}

			// ~foo.indexOf(bar)
			if (options.boolean && isBinaryNegatingOfIndexOf(node)) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionIndexOf,
					{
						code: context.getSource(node.argument)
					});
			}

			// +foo
			if (options.number && node.operator === "+" && !isNumeric(node.argument)) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionNumber,
					{
						code: context.getSource(node.argument)
					});
			}
		},

		// Use `:exit` to prevent double reporting
		"BinaryExpression:exit": function(node) {
			// 1 * foo
			var nonNumericOperand = options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
			if (nonNumericOperand) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionNumber,
					{
						code: context.getSource(nonNumericOperand)
					});
			}

			// "" + foo
			if (options.string && isConcatWithEmptyString(node)) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionString,
					{
						code: context.getSource(getOtherOperand(node, ""))
					});
			}
		},
		AssignmentExpression: function(node) {
			// foo += ""
			if (options.string && isAppendEmptyString(node)) {
				context.report(
					node,
					ProblemMessages.noImplicitCoercionString2,
					{
						code: context.getSource(getOtherOperand(node, ""))
					});
			}
		}
	};
};

module.exports.schema = [{
	"type": "object",
	"properties": {
		"boolean": {
			"type": "boolean"
		},
		"number": {
			"type": "boolean"
		},
		"string": {
			"type": "boolean"
		}
	},
	"additionalProperties": false
}];

return module.exports;
});
