/*
 Ext.Deferred adapted from:
 [DeftJS](https://github.com/deftjs/deftjs5)
 Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org)
 Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).

 when(), all(), any(), some(), map(), reduce(), delay() and timeout()
 sequence(), parallel(), pipeline()
 methods adapted from: [when.js](https://github.com/cujojs/when)
 Copyright (c) B Cavalier & J Hann
 Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
 */

/**
 * Deferreds are the mechanism used to create new Promises. A Deferred has a single
 * associated Promise that can be safely returned to external consumers to ensure they do
 * not interfere with the resolution or rejection of the deferred operation.
 *
 * This implementation of Promises is an extension of the ECMAScript 6 Promises API as
 * detailed [here][1]. For a compatible, though less full featured, API see `{@link Ext.Promise}`.
 *
 * A Deferred is typically used within the body of a function that performs an asynchronous
 * operation. When that operation succeeds, the Deferred should be resolved; if that
 * operation fails, the Deferred should be rejected.
 *
 * Each Deferred has an associated Promise. A Promise delegates `then` calls to its
 * Deferred's `then` method. In this way, access to Deferred operations are divided between
 * producer (Deferred) and consumer (Promise) roles.
 *
 * ## Basic Usage
 *
 * In it's most common form, a method will create and return a Promise like this:
 *
 *      // A method in a service class which uses a Store and returns a Promise
 *      //
 *      loadCompanies: function () {
 *          var deferred = new Ext.Deferred(); // create the Ext.Deferred object
 *
 *          this.companyStore.load({
 *              callback: function (records, operation, success) {
 *                  if (success) {
 *                      // Use "deferred" to drive the promise:
 *                      deferred.resolve(records);
 *                  }
 *                  else {
 *                      // Use "deferred" to drive the promise:
 *                      deferred.reject("Error loading Companies.");
 *                  }
 *              }
 *          });
 *
 *          return deferred.promise;  // return the Promise to the caller
 *      }
 *
 * You can see this method first creates a `{@link Ext.Deferred Deferred}` object. It then
 * returns its `Promise` object for use by the caller. Finally, in the asynchronous
 * callback, it resolves the `deferred` object if the call was successful, and rejects the
 * `deferred` if the call failed.
 *
 * When a Deferred's `resolve` method is called, it fulfills with the optionally specified
 * value. If `resolve` is called with a then-able (i.e.a Function or Object with a `then`
 * function, such as another Promise) it assimilates the then-able's result; the Deferred
 * provides its own `resolve` and `reject` methods as the onFulfilled or onRejected
 * arguments in a call to that then-able's `then` function. If an error is thrown while
 * calling the then-able's `then` function (prior to any call back to the specified
 * `resolve` or `reject` methods), the Deferred rejects with that error. If a Deferred's
 * `resolve` method is called with its own Promise, it rejects with a TypeError.
 *
 * When a Deferred's `reject` method is called, it rejects with the optionally specified
 * reason.
 *
 * Each time a Deferred's `then` method is called, it captures a pair of optional
 * onFulfilled and onRejected callbacks and returns a Promise of the Deferred's future
 * value as transformed by those callbacks.
 *
 * See `{@link Ext.promise.Promise}` for an example of using the returned Promise.
 *
 * @since 6.0.0
 */
Ext.define('Ext.Deferred', function (Deferred) {
    var ExtPromise,
        when;

return {
    extend: 'Ext.promise.Deferred',

    requires: [
        'Ext.Promise'
    ],

    statics: {
        _ready: function () {
            // Our requires are met, so we can cache Ext.promise.Deferred
            ExtPromise = Ext.promise.Promise;
            when = Ext.Promise.resolve;
        },

        /**
         * Returns a new Promise that will only resolve once all the specified
         * `promisesOrValues` have resolved.
         *
         * The resolution value will be an Array containing the resolution value of each
         * of the `promisesOrValues`.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @return {Ext.promise.Promise} A Promise of an Array of the resolved values.
         * @static
         */
        all: function () {
            return ExtPromise.all.apply(ExtPromise, arguments);
        },

        /**
         * Initiates a competitive race, returning a new Promise that will resolve when
         * any one of the specified `promisesOrValues` have resolved, or will reject when
         * all `promisesOrValues` have rejected or cancelled.
         *
         * The resolution value will the first value of `promisesOrValues` to resolve.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @return {Ext.promise.Promise} A Promise of the first resolved value.
         * @static
         */
        any: function (promisesOrValues) {
            //<debug>
            if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
            }
            //</debug>

            return Deferred.some(promisesOrValues, 1).then(function (array) {
                return array[0];
            }, function (error) {
                if (error instanceof Error &&
                    error.message === 'Too few Promises were resolved.') {
                    Ext.raise('No Promises were resolved.');
                }
                else {
                    throw error;
                }
            });
        },

        /**
         * Returns a new Promise that will automatically resolve with the specified
         * Promise or value after the specified delay (in milliseconds).
         *
         * @param {Mixed} promiseOrValue A Promise or value.
         * @param {Number} milliseconds A delay duration (in milliseconds).
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value that
         * will resolve after the specified delay.
         * @static
         */
        delay: function (promiseOrValue, milliseconds) {
            var deferred;

            if (arguments.length === 1) {
                milliseconds = promiseOrValue;
                promiseOrValue = undefined;
            }

            milliseconds = Math.max(milliseconds, 0);

            deferred = new Deferred();

            setTimeout(function () {
                deferred.resolve(promiseOrValue);
            }, milliseconds);

            return deferred.promise;
        },

        /**
         * Traditional map function, similar to `Array.prototype.map()`, that allows
         * input to contain promises and/or values.
         *
         * The specified map function may return either a value or a promise.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Function} mapFn A Function to call to transform each resolved value in
         * the Array.
         * @return {Ext.promise.Promise} A Promise of an Array of the mapped resolved
         * values.
         * @static
         */
        map: function (promisesOrValues, mapFn) {
            //<debug>
            if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
            }

            if (!Ext.isFunction(mapFn)) {
                Ext.raise('Invalid parameter: expected a function.');
            }
            //</debug>

            return Deferred.resolved(promisesOrValues).then(function (promisesOrValues) {
                var deferred, index, promiseOrValue, remainingToResolve, resolve, results, i, len;

                remainingToResolve = promisesOrValues.length;
                results = new Array(promisesOrValues.length);
                deferred = new Deferred();

                if (!remainingToResolve) {
                    deferred.resolve(results);
                }
                else {
                    resolve = function (item, index) {
                        return Deferred.resolved(item).then(function (value) {
                            return mapFn(value, index, results);
                        }).then(function (value) {
                            results[index] = value;
                            if (!--remainingToResolve) {
                                deferred.resolve(results);
                            }
                            return value;
                        }, function (reason) {
                            return deferred.reject(reason);
                        });
                    };
                    for (index = i = 0, len = promisesOrValues.length; i < len; index = ++i) {
                        promiseOrValue = promisesOrValues[index];

                        if (index in promisesOrValues) {
                            resolve(promiseOrValue, index);
                        }
                        else {
                            remainingToResolve--;
                        }
                    }
                }

                return deferred.promise;
            });
        },

        /**
         * Returns a new function that wraps the specified function and caches the
         * results for previously processed inputs.
         *
         * Similar to {@link Ext.Function#memoize Ext.Function.memoize()}, except it
         * allows for parameters that are Promises and/or values.
         *
         * @param {Function} fn A Function to wrap.
         * @param {Object} scope An optional scope in which to execute the wrapped function.
         * @param {Function} hashFn An optional function used to compute a hash key for
         * storing the result, based on the arguments to the original function.
         * @return {Function} The new wrapper function.
         * @static
         */
        memoize: function (fn, scope, hashFn) {
            var memoizedFn = Ext.Function.memoize(fn, scope, hashFn);

            return function () {
                return Deferred.all(Ext.Array.slice(arguments)).then(function (values) {
                    return memoizedFn.apply(scope, values);
                });
            };
        },

        /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions in parallel.
         *
         * The specified functions may optionally return their results as
         * {@link Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of an Array of results for each function
         * call (in the same order).
         * @static
         */
        parallel: function (fns, scope) {
            if (scope == null) {
                scope = null;
            }

            var args = Ext.Array.slice(arguments, 2);

            return Deferred.map(fns, function (fn) {
                if (!Ext.isFunction(fn)) {
                    throw new Error('Invalid parameter: expected a function.');
                }

                return fn.apply(scope, args);
            });
        },

        /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions as a pipeline, where each function's result is passed to the
         * subsequent function as input.
         *
         * The specified functions may optionally return their results as
         * {@link Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} initialValue Initial value to be passed to the first function
         * in the pipeline.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of the result value for the final
         * function in the pipeline.
         * @static
         */
        pipeline: function (fns, initialValue, scope) {
            if (scope == null) {
                scope = null;
            }

            return Deferred.reduce(fns, function (value, fn) {
                if (!Ext.isFunction(fn)) {
                    throw new Error('Invalid parameter: expected a function.');
                }

                return fn.call(scope, value);
            }, initialValue);
        },

        /**
         * Traditional reduce function, similar to `Array.reduce()`, that allows input to
         * contain promises and/or values.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} values An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Function} reduceFn A Function to call to transform each successive
         * item in the Array into the final reduced value.
         * @param {Mixed} initialValue An initial Promise or value.
         * @return {Ext.promise.Promise} A Promise of the reduced value.
         * @static
         */
        reduce: function (values, reduceFn, initialValue) {
            //<debug>
            if (!(Ext.isArray(values) || ExtPromise.is(values))) {
                Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
            }

            if (!Ext.isFunction(reduceFn)) {
                Ext.raise('Invalid parameter: expected a function.');
            }
            //</debug>

            var initialValueSpecified = arguments.length === 3;

            return Deferred.resolved(values).then(function (promisesOrValues) {
                var reduceArguments = [
                    promisesOrValues,
                    function (previousValueOrPromise, currentValueOrPromise, currentIndex) {
                        return Deferred.resolved(previousValueOrPromise).then(function (previousValue) {
                            return Deferred.resolved(currentValueOrPromise).then(function (currentValue) {
                                return reduceFn(previousValue, currentValue, currentIndex, promisesOrValues);
                            });
                        });
                    }
                ];

                if (initialValueSpecified) {
                    reduceArguments.push(initialValue);
                }

                return Ext.Array.reduce.apply(Ext.Array, reduceArguments);
            });
        },

        /**
         * Convenience method that returns a new Promise rejected with the specified
         * reason.
         *
         * @param {Error} reason Rejection reason.
         * @return {Ext.promise.Promise} The rejected Promise.
         * @static
         */
        rejected: function (reason) {
            var deferred = new Ext.Deferred();

            deferred.reject(reason);

            return deferred.promise;
        },

        /**
         * Returns a new Promise that either
         *
         *  * Resolves immediately for the specified value, or
         *  * Resolves or rejects when the specified promise (or third-party Promise or
         *    then()-able) is resolved or rejected.
         *
         * @param {Mixed} promiseOrValue A Promise (or third-party Promise or then()-able)
         * or value.
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value.
         * @static
         */
        resolved: function (value) {
            var deferred = new Ext.Deferred();

            deferred.resolve(value);

            return deferred.promise;
        },

        /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions sequentially.
         *
         * The specified functions may optionally return their results as {@link
         * Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of an Array of results for each function
         * call (in the same order).
         * @static
         */
        sequence: function (fns, scope) {
            if (scope == null) {
                scope = null;
            }

            var args = Ext.Array.slice(arguments, 2);

            return Deferred.reduce(fns, function (results, fn) {
                if (!Ext.isFunction(fn)) {
                    throw new Error('Invalid parameter: expected a function.');
                }

                return Deferred.resolved(fn.apply(scope, args)).then(function (result) {
                    results.push(result);

                    return results;
                });
            }, []);
        },

        /**
         * Initiates a competitive race, returning a new Promise that will resolve when
         * `howMany` of the specified `promisesOrValues` have resolved, or will reject
         * when it becomes impossible for `howMany` to resolve.
         *
         * The resolution value will be an Array of the first `howMany` values of
         * `promisesOrValues` to resolve.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Number} howMany The expected number of resolved values.
         * @return {Ext.promise.Promise} A Promise of the expected number of resolved
         * values.
         * @static
         */
        some: function (promisesOrValues, howMany) {
            //<debug>
            if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
            }

            if (!Ext.isNumeric(howMany) || howMany <= 0) {
                Ext.raise('Invalid parameter: expected a positive integer.');
            }
            //</debug>

            return Deferred.resolved(promisesOrValues).then(function (promisesOrValues) {
                var deferred, index, onReject, onResolve, promiseOrValue,
                    remainingToReject, remainingToResolve, values, i, len;

                values = [];
                remainingToResolve = howMany;
                remainingToReject = (promisesOrValues.length - remainingToResolve) + 1;
                deferred = new Deferred();

                if (promisesOrValues.length < howMany) {
                    deferred.reject(new Error('Too few Promises were resolved.'));
                }
                else {
                    onResolve = function (value) {
                        if (remainingToResolve > 0) {
                            values.push(value);
                        }

                        remainingToResolve--;

                        if (remainingToResolve === 0) {
                            deferred.resolve(values);
                        }

                        return value;
                    };

                    onReject = function (reason) {
                        remainingToReject--;

                        if (remainingToReject === 0) {
                            deferred.reject(new Error('Too few Promises were resolved.'));
                        }

                        return reason;
                    };

                    for (index = i = 0, len = promisesOrValues.length; i < len; index = ++i) {
                        promiseOrValue = promisesOrValues[index];

                        if (index in promisesOrValues) {
                            Deferred.resolved(promiseOrValue).then(onResolve, onReject);
                        }
                    }
                }

                return deferred.promise;
            });
        },

        /**
         * Returns a new Promise that will automatically reject after the specified
         * timeout (in milliseconds) if the specified promise has not resolved or
         * rejected.
         *
         * @param {Mixed} promiseOrValue A Promise or value.
         * @param {Number} milliseconds A timeout duration (in milliseconds).
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value that
         * enforces the specified timeout.
         * @static
         */
        timeout: function (promiseOrValue, milliseconds) {
            var deferred = new Deferred(),
                timeoutId;

            timeoutId = setTimeout(function () {
                if (timeoutId) {
                    deferred.reject(new Error('Promise timed out.'));
                }
            }, milliseconds);

            Deferred.resolved(promiseOrValue).then(function (value) {
                clearTimeout(timeoutId);
                timeoutId = null;
                deferred.resolve(value);
            }, function (reason) {
                clearTimeout(timeoutId);
                timeoutId = null;
                deferred.reject(reason);
            });

            return deferred.promise;
        }
    }
}},
function (Deferred) {
    Deferred._ready();
});
