/** @license MIT License (c) copyright 2010-2014 original author or authors */
/** @author Brian Cavalier */
/** @author John Hann */

(function(define) { 'use strict';
define(function() {

	return function array(Promise) {

		var arrayReduce = Array.prototype.reduce;
		var arrayReduceRight = Array.prototype.reduceRight;

		var toPromise = Promise.resolve;
		var all = Promise.all;

		// Additional array combinators

		Promise.any = any;
		Promise.some = some;
		Promise.settle = settle;

		Promise.map = map;
		Promise.filter = filter;
		Promise.reduce = reduce;
		Promise.reduceRight = reduceRight;

		/**
		 * When this promise fulfills with an array, do
		 * onFulfilled.apply(void 0, array)
		 * @param {function} onFulfilled function to apply
		 * @returns {Promise} promise for the result of applying onFulfilled
		 */
		Promise.prototype.spread = function(onFulfilled) {
			return this.then(all).then(function(array) {
				return onFulfilled.apply(void 0, array);
			});
		};

		return Promise;

		/**
		 * One-winner competitive race.
		 * Return a promise that will fulfill when one of the promises
		 * in the input array fulfills, or will reject when all promises
		 * have rejected.
		 * @param {array} promises
		 * @returns {Promise} promise for the first fulfilled value
		 */
		function any(promises) {
			return new Promise(function(resolve, reject) {
				var errors = [];
				var pending = initRace(promises, resolve, handleReject);

				if(pending === 0) {
					reject(new RangeError('any() input must not be empty'));
				}

				function handleReject(e) {
					errors.push(e);
					if(--pending === 0) {
						reject(errors);
					}
				}
			});
		}

		/**
		 * N-winner competitive race
		 * Return a promise that will fulfill when n input promises have
		 * fulfilled, or will reject when it becomes impossible for n
		 * input promises to fulfill (ie when promises.length - n + 1
		 * have rejected)
		 * @param {array} promises
		 * @param {number} n
		 * @returns {Promise} promise for the earliest n fulfillment values
		 *
		 * @deprecated
		 */
		function some(promises, n) {
			return new Promise(function(resolve, reject, notify) {
				var results = [];
				var errors = [];
				var nReject;
				var nFulfill = initRace(promises, handleResolve, handleReject, notify);

				n = Math.max(n, 0);
				nReject = (nFulfill - n + 1);
				nFulfill = Math.min(n, nFulfill);

				if(n > nFulfill) {
					reject(new RangeError('some() input must contain at least '
						+ n + ' element(s), but had ' + nFulfill));
				} else if(nFulfill === 0) {
					resolve(results);
				}

				function handleResolve(x) {
					if(nFulfill > 0) {
						--nFulfill;
						results.push(x);

						if(nFulfill === 0) {
							resolve(results);
						}
					}
				}

				function handleReject(e) {
					if(nReject > 0) {
						--nReject;
						errors.push(e);

						if(nReject === 0) {
							reject(errors);
						}
					}
				}
			});
		}

		/**
		 * Initialize a race observing each promise in the input promises
		 * @param {Array} promises
		 * @param {function} resolve
		 * @param {function} reject
		 * @param {?function=} notify
		 * @returns {Number} actual count of items being raced
		 */
		function initRace(promises, resolve, reject, notify) {
			return arrayReduce.call(promises, function(pending, p) {
				toPromise(p).then(resolve, reject, notify);
				return pending + 1;
			}, 0);
		}

		/**
		 * Apply f to the value of each promise in a list of promises
		 * and return a new list containing the results.
		 * @param {array} promises
		 * @param {function(x:*, index:Number):*} f mapping function
		 * @returns {Promise}
		 */
		function map(promises, f) {
			if(typeof promises !== 'object') {
				return toPromise([]);
			}

			return all(mapArray(function(x, i) {
				return toPromise(x).fold(mapWithIndex, i);
			}, promises));

			function mapWithIndex(k, x) {
				return f(x, k);
			}
		}

		/**
		 * Filter the provided array of promises using the provided predicate.  Input may
		 * contain promises and values
		 * @param {Array} promises array of promises and values
		 * @param {function(x:*, index:Number):boolean} predicate filtering predicate.
		 *  Must return truthy (or promise for truthy) for items to retain.
		 * @returns {Promise} promise that will fulfill with an array containing all items
		 *  for which predicate returned truthy.
		 */
		function filter(promises, predicate) {
			return all(promises).then(function(values) {
				return all(mapArray(predicate, values)).then(function(results) {
					var len = results.length;
					var filtered = new Array(len);
					for(var i=0, j= 0, x; i<len; ++i) {
						x = results[i];
						if(x === void 0 && !(i in results)) {
							continue;
						}
						if(results[i]) {
							filtered[j++] = values[i];
						}
					}
					filtered.length = j;
					return filtered;
				});
			});
		}

		/**
		 * Return a promise that will always fulfill with an array containing
		 * the outcome states of all input promises.  The returned promise
		 * will never reject.
		 * @param {array} promises
		 * @returns {Promise} promise for array of settled state descriptors
		 */
		function settle(promises) {
			return all(mapArray(function(p) {
				p = toPromise(p);
				return p.then(inspect, inspect);

				function inspect() {
					return p.inspect();
				}
			}, promises));
		}

		/**
		 * Reduce an array of promises and values
		 * @param {Array} promises
		 * @param {function(accumulated:*, x:*, index:Number):*} f reduce function
		 * @returns {Promise} promise for reduced value
		 */
		function reduce(promises, f) {
			var reducer = makeReducer(f);
			return arguments.length > 2
				? arrayReduce.call(promises, reducer, arguments[2])
				: arrayReduce.call(promises, reducer);
		}

		/**
		 * Reduce an array of promises and values from the right
		 * @param {Array} promises
		 * @param {function(accumulated:*, x:*, index:Number):*} f reduce function
		 * @returns {Promise} promise for reduced value
		 */
		function reduceRight(promises, f) {
			var reducer = makeReducer(f);
			return arguments.length > 2
				? arrayReduceRight.call(promises, reducer, arguments[2])
				: arrayReduceRight.call(promises, reducer);
		}

		function makeReducer(f) {
			return function reducer(result, x, i) {
				return toPromise(result).then(function(r) {
					return toPromise(x).then(function(x) {
						return f(r, x, i);
					});
				});
			};
		}

		function mapArray(f, a) {
			var l = a.length;
			var b = new Array(l);
			for(var i=0, x; i<l; ++i) {
				x = a[i];
				if(x === void 0 && !(i in a)) {
					continue;
				}
				b[i] = f(a[i], i);
			}
			return b;
		}
	};



});
}(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }));
