define(['Guard', 'Arrays', 'Term', 'Dimensions'], function(Guard, Arrays, Term, Dimensions) {
	/**
	 * @classdesc
	 * Represents an expression made of composed Units.
	 * This structure is the basis of DerivedUnit definitions.
	 * 
	 * Logically, a UnitExpression represents a list of Terms multiplied
	 * together.  Each Term has a base "unit" and a power.
	 * 
	 * For instance, a UnitExpression that represents "meter per second squared,"
	 * would consist of one Term that represents "meter" (to the first power) and another
	 * Term that represents "second squared."
	 * @class
	 * @alias UnitExpression
	 * @hideconstructor
	 * @param {array<Term>}} terms 
	 */
	function UnitExpression(terms) {
		Guard(terms, "terms").isArrayOf(Term);
		this._terms = Arrays.frozenClone(terms);
		var that = this;
		var dimensions = null;
		this._dimensions = function() {
			if (dimensions === null) {
				dimensions = getDimensions.call(that);
			}
			return dimensions;
		};
		Object.freeze(this);
	}

	function getDimensions() {
		var dim = this._terms
			.map(function(t) {
				return t.unit()
					.dimensions()
					.mult(t.power());
			})
			.reduce(function(acc, item) {
				return acc.add(item);
			}, Dimensions.empty());
		return dim;
	}

	UnitExpression.prototype = 
	/** @lends UnitExpression# */
	{
		/**
		 * Returns an immutable array of [Terms]{@link Term} that make up the UnitExpression.
		 * @method
		 * @type {array<Term>}
		 */
		terms: function() {
			return this._terms;
		},
		/**
		 * Returns a Dimensions object that represents the unit dimensions of
		 * this UnitExpression.
		 * @method
		 * @type {Dimensions}
		 */
		dimensions: function() {
			return this._dimensions();
		},
		/**
		 * Multiplies this UnitExpression by another UnitExpression
		 * and returns the result as another UnitExpression.
		 * 
		 * Logically, this operation is implemented by concatenating
		 * all of the terms together and combining like bases.
		 * @method
		 * @param {UnitExpression} rhs
		 * @type {UnitExpression}
		 */
		mult: function(rhs) {
			var terms = flatten([this.terms(), rhs.terms()]);
			terms = combineLikeTerms(terms);
			return new UnitExpression(terms);
		},
		/**
		 * Divides this UnitExpression by another UnitExpression
		 * and returns the result as another UnitExpression.
		 * 
		 * Logically, this operation is implemented by inverting the
		 * divisor `rhs` and performing a multiplication operation.
		 * @method
		 * @param {UnitExpression} rhs 
		 * @type {UnitExpression}
		 */
		div: function(rhs) {
			var inverseRhs = new UnitExpression(rhs.terms().map(invert));
			return this.mult(inverseRhs);
		},
		/**
		 * Exponentiates this UnitExpression by the given
		 * power and returns the result as another UnitExpression.
		 * @method
		 * @param {number} power
		 * @type {UnitExpression}
		 */
		pow: function(power) {
			var exponentiatedTerms = this.terms().map(termExponent(power));
			return new UnitExpression(exponentiatedTerms);
		},
		/**
		 * Recursively breaks down this UnitExpression into its constituent
		 * Terms and each [Term]{@link Term} into its constituent [BaseUnits]{@link BaseUnit},
		 * until only BaseUnits are left.
		 * @method
		 * @type {UnitExpression}
		 */
		toBaseUnits: function() {
			var baseTerms = getEquivalentBaseTermsForList(this.terms());
			return new UnitExpression(baseTerms);
		},
		/**
		 * Simplifies the UnitExpression by combining like [Terms]{@link Term}.
		 * @method
		 * @type {UnitExpression}
		 */
		simplify: function() {
			return new UnitExpression(combineLikeTerms(this.terms()));
		},
		/**
		 * Projects this UnitExpression onto an equivalent map,
		 * where each unit type becomes a key, and the value
		 * is the sum of all the powers of the Terms of that type.
		 * @method
		 * @type {object}
		 */
		toMap: function() {
			var terms = combineLikeTerms(this.terms());

			return terms.reduce(
				function(map, term) {
					map[term.unit().name()] = term.power();
					return map;
				}, 
				Object.create(null)
			);
		},
		/**
		 * Returns a string representation of the given UnitExpression.
		 * 
		 * For example, the string representation of a UnitExpression 
		 * that represents "meters per second squared" will be "m/s^2".
		 * @method
		 * @type {string}
		 */
		toString: function() {
			var terms = this._terms;
			 var reduced = terms
			 	.map(function(term) {
					var unitString = term.unit().toString();
					var opString = getOperatorString(term);
					var powerString = getPowerString(term);
					if (term.power() === 0) {
						return "";
					}
					return opString + unitString + powerString;
				})
				.reduce(function(acc, termString) {
					if (acc === "") {
						if (termString.startsWith(" ")) {
							termString = termString.substring(1);
						}
					}
					return acc + termString;
				}, "");

			return reduced;
		}
	};

	Object.defineProperty(UnitExpression.prototype, 'constructor', {
		value: UnitExpression,
		enumerable: false,
		writable: true
	});
	
	function termExponent(power) {
		return function(t) {
			return new Term(t.unit(), t.power() * power); 	
		};
	}
	
	function unitProperty(term) {
		return term.unit();
	}
	
	function combineLikeTerms(terms) {
		Guard(terms, "terms").isArrayOf(Term);
		var groupedTerms = groupBy(terms, unitProperty);
		var outputTerms = Object.keys(groupedTerms)
			.map(function(groupName) {
				var group = groupedTerms[groupName];
				return new Term(group.key, sumPowersOf(group.items));
			});
		return outputTerms;
	}
	
	function invert(term) {
		return new Term(term.unit(), -term.power());
	}
	
	function flatten(arrays) {
		var flattened = [];
		arrays.forEach(function(a) { 
			Array.prototype.push.apply(flattened, a);
		});
		return flattened;
	}
	
	function groupBy(arr, keySelector) {
		var groups = Object.create(null);
		for (var i = 0; i < arr.length; i++) {
			var item = arr[i];
			var key = keySelector(item);
			var group = groups[key] || { key: key, items: [] };
			group.items.push(item);
			groups[key] = group;
		}
		return groups;
	}
	
	function sumPowersOf(terms) {
		var sum = terms.reduce(function(acc, t) { return acc + t.power(); }, 0);
		return sum;
	}
	
	function emptyUnitExpression() {
		return new UnitExpression([]);
	}

	/**
	 * Given an array of Terms (which could be in any type of units), returns an
	 * array of equivalent BaseUnit terms.
	 * @param {Term[]} terms 
	 */
	function getEquivalentBaseTermsForList(terms) {
		return terms.map(function(term) {
			return new UnitExpression(getEquivalentBaseTerms(term));
		}).reduce(function(acc, expr) {
			return acc.mult(expr);
		}, emptyUnitExpression()
		).terms();
	}

	/**
	 * Given a Term (which could be in any Units) returns an equivalent array of 
	 * Terms expressed in BaseUnits.
	 * @param {Term} term 
	 */
	function getEquivalentBaseTerms(term) {
		Guard(term, "term").instanceOf(Term);
		var unit = term.unit();
		if (unit.isBaseUnit()) {
			return [ term ];
		}
		else {
			var subTermsExpr = unit.expression().pow(term.power());
			return getEquivalentBaseTermsForList(subTermsExpr.terms());			
		}
	}

	function getOperatorString(term) {
		var power = term.power();
		if (power === 0) {
			return "";
		}
		else if (power < 0) {
			return "/";
		}
		else if (power > 0) {
			return " ";
		}
		else {
			throw new Error("'power' had the unexpected value '" + power + "'.");
		}
	}

	function getPowerString(term) {
		var power = term.power();
		var absPower = Math.abs(power);
		if (absPower === 0 || absPower === 1) {
			return "";
		}
		return "^" + absPower;
	}
	
	return UnitExpression;
});