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

/**
 * Promises represent a future value; i.e., a value that may not yet be available.
 *
 * Users should **not** create instances of this class directly. Instead user code should
 * use `new {@link Ext.Promise}()` or `new {@link Ext.Deferred}()` to create and manage
 * promises. If the browser supports the standard `Promise` constructor, this class will
 * not be used by `Ext.Promise`. This class will always be used by `Ext.Deferred` in order
 * to provide enhanced capabilities beyond standard promises.
 *
 * A Promise's `{@link #then then()}` method is used to specify onFulfilled and onRejected
 * callbacks that will be notified when the future value becomes available. Those callbacks
 * can subsequently transform the value that was resolved or the reason that was rejected.
 * Each call to `then` returns a new Promise of that transformed value; i.e., a Promise
 * that is resolved with the callback return value or rejected with any error thrown by
 * the callback.
 *
 * ## Basic Usage
 *
 *      this.companyService.loadCompanies().then(
 *          function (records) {
 *              // Do something with result.
 *          },
 *          function (error) {
 *              // Do something on failure.
 *          }).
 *      always(function () {
 *          // Do something whether call succeeded or failed
 *      });
 *
 * The above code uses the `Promise` returned from the `companyService.loadCompanies()`
 * method and uses `then()` to attach success and failure handlers. Finally, an `always()`
 * method call is chained onto the returned promise. This specifies a callback function
 * that will run whether the underlying call succeeded or failed.
 *
 * See `{@link Ext.Deferred}` for an example of using the returned Promise.
 *
 * [1]: http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts#april_14_2015_rev_38_final_draft
 *
 * @since 6.0.0
 */
Ext.define('Ext.promise.Promise', function (ExtPromise) {
    var Deferred;

return {
    requires: [
        'Ext.promise.Deferred'
    ],

    statics: {
        /**
         * @property CancellationError
         * @static
         * The type of `Error` propagated by the `{@link #method-cancel}` method. If
         * the browser provides a native `CancellationError` then that type is used. If
         * not, a basic `Error` type is used.
         */
        CancellationError: Ext.global.CancellationError || Error,

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

        /**
         * 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`.
         *
         * The public API's to use instead of this method are `{@link Ext.Promise#all}`
         * and `{@link Ext.Deferred#all}`.
         *
         * @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
         * @private
         */
        all: function (promisesOrValues) {
            //<debug>
            if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
            }
            //</debug>

            return ExtPromise.when(promisesOrValues).then(function (promisesOrValues) {
                var deferred = new Deferred(),
                    remainingToResolve = promisesOrValues.length,
                    results = new Array(remainingToResolve),
                    index, promiseOrValue, resolve, i, len;

                if (!remainingToResolve) {
                    deferred.resolve(results);
                }
                else {
                    resolve = function (item, index) {
                        return ExtPromise.when(item).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;
            });
        },

        /**
         * Determines whether the specified value is a Promise (including third-party
         * untrusted Promises or then()-ables), based on the Promises/A specification
         * feature test.
         *
         * @param {Mixed} value A potential Promise.
         * @return {Boolean} `true` if the given value is a Promise, otherwise `false`.
         * @static
         * @private
         */
        is: function (value) {
            return (Ext.isObject(value) || Ext.isFunction(value)) &&
                Ext.isFunction(value.then);
        },

        /**
         * Rethrows the specified Error on the next turn of the event loop.
         * @static
         * @private
         */
        rethrowError: function (error) {
            Ext.asap(function () {
                throw error;
            });
        },

        /**
         * 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.
         *
         * The public API's to use instead of this method are `{@link Ext.Promise#resolve}`
         * and `{@link Ext.Deferred#resolved}`.
         *
         * @param {Mixed} promiseOrValue A Promise (or third-party Promise or then()-able)
         * or value.
         * @return {Ext.Promise} A Promise of the specified Promise or value.
         *
         * @static
         * @private
         */
        when: function (value) {
            var deferred = new Ext.promise.Deferred();

            deferred.resolve(value);

            return deferred.promise;
        }
    },

    /**
     * @property {Ext.promise.Deferred} Reference to this promise's
     * `{@link Ext.promise.Deferred Deferred}` instance.
     *
     * @readonly
     * @private
     */
    owner: null,

    /**
     * NOTE: {@link Ext.promise.Deferred Deferreds} are the mechanism used to create new
     * Promises.
     * @param {Ext.promise.Deferred} owner The owning `Deferred` instance.
     *
     * @private
     */
    constructor: function (owner) {
        this.owner = owner;
    },

    /**
     * Attaches onFulfilled and onRejected callbacks that will be notified when the future
     * value becomes available.
     *
     * Those callbacks can subsequently transform the value that was fulfilled or the error
     * that was rejected. Each call to `then` returns a new Promise of that transformed
     * value; i.e., a Promise that is fulfilled with the callback return value or rejected
     * with any error thrown by the callback.
     *
     * @param {Function} onFulfilled Optional callback to execute to transform a
     * fulfillment value.
     * @param {Function} onRejected Optional callback to execute to transform a rejection
     * reason.
     * @param {Function} onProgress Optional callback function to be called with progress
     * updates.
     * @param {Object} scope Optional scope for the callback(s).
     * @return {Ext.promise.Promise} Promise that is fulfilled with the callback return
     * value or rejected with any error thrown by the callback.
     */
    then: function (onFulfilled, onRejected, onProgress, scope) {
        var ref;

        if (arguments.length === 1 && Ext.isObject(arguments[0])) {
            ref = arguments[0];
            onFulfilled = ref.success;
            onRejected = ref.failure;
            onProgress = ref.progress;
            scope = ref.scope;
        }

        if (scope) {
            if (onFulfilled) {
                onFulfilled = Ext.Function.bind(onFulfilled, scope);
            }
            
            if (onRejected) {
                onRejected = Ext.Function.bind(onRejected, scope);
            }
            
            if (onProgress) {
                onProgress = Ext.Function.bind(onProgress, scope);
            }
        }

        return this.owner.then(onFulfilled, onRejected, onProgress);
    },

    /**
     * Attaches an onRejected callback that will be notified if this Promise is rejected.
     *
     * The callback can subsequently transform the reason that was rejected. Each call to
     * `otherwise` returns a new Promise of that transformed value; i.e., a Promise that
     * is resolved with the original resolved value, or resolved with the callback return
     * value or rejected with any error thrown by the callback.
     *
     * @param {Function} onRejected Callback to execute to transform a rejection reason.
     * @param {Object} scope Optional scope for the callback.
     * @return {Ext.promise.Promise} Promise of the transformed future value.
     */
    otherwise: function (onRejected, scope) {
        var ref;

        if (arguments.length === 1 && Ext.isObject(arguments[0])) {
            ref = arguments[0];
            onRejected = ref.fn;
            scope = ref.scope;
        }

        if (scope != null) {
            onRejected = Ext.Function.bind(onRejected, scope);
        }

        return this.owner.then(null, onRejected);
    },

    /**
     * Attaches an onCompleted callback that will be notified when this Promise is completed.
     *
     * Similar to `finally` in `try... catch... finally`.
     *
     * NOTE: The specified callback does not affect the resulting Promise's outcome; any
     * return value is ignored and any Error is rethrown.
     *
     * @param {Function} onCompleted Callback to execute when the Promise is resolved or
     * rejected.
     * @param {Object} scope Optional scope for the callback.
     * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with
     * the original value or rejected with the original reason.
     */
    always: function (onCompleted, scope) {
        var ref;

        if (arguments.length === 1 && Ext.isObject(arguments[0])) {
            ref = arguments[0];
            onCompleted = ref.fn;
            scope = ref.scope;
        }

        if (scope != null) {
            onCompleted = Ext.Function.bind(onCompleted, scope);
        }

        return this.owner.then(function (value) {
            try {
                onCompleted();
            }
            catch (e) {
                ExtPromise.rethrowError(e);
            }
            
            return value;
        }, function (reason) {
            try {
                onCompleted();
            }
            catch (e) {
                ExtPromise.rethrowError(e);
            }
            
            throw reason;
        });
    },

    /**
     * Terminates a Promise chain, ensuring that unhandled rejections will be rethrown as
     * Errors.
     *
     * One of the pitfalls of interacting with Promise-based APIs is the tendency for
     * important errors to be silently swallowed unless an explicit rejection handler is
     * specified.
     *
     * For example:
     *
     *      promise.then(function () {
     *          // logic in your callback throws an error and it is interpreted as a
     *          // rejection. throw new Error("Boom!");
     *      });
     *
     *      // The Error was not handled by the Promise chain and is silently swallowed.
     *
     * This problem can be addressed by terminating the Promise chain with the done()
     * method:
     *
     *      promise.then(function () {
     *          // logic in your callback throws an error and it is interpreted as a
     *          // rejection. throw new Error("Boom!");
     *      }).done();
     *
     *     // The Error was not handled by the Promise chain and is rethrown by done() on
     *     // the next tick.
     *
     * The `done()` method ensures that any unhandled rejections are rethrown as Errors.
     */
    done: function () {
        this.owner.then(null, ExtPromise.rethrowError);
    },

    /**
     * Cancels this Promise if it is still pending, triggering a rejection with a
     * `{@link #CancellationError}` that will propagate to any Promises originating from
     * this Promise.
     *
     * NOTE: Cancellation only propagates to Promises that branch from the target Promise.
     * It does not traverse back up to parent branches, as this would reject nodes from
     * which other Promises may have branched, causing unintended side-effects.
     *
     * @param {Error} reason Cancellation reason.
     */
    cancel: function (reason) {
        if (reason == null) {
            reason = null;
        }
        
        this.owner.reject(new this.self.CancellationError(reason));
    },

    /**
     * Logs the resolution or rejection of this Promise with the specified category and
     * optional identifier. Messages are logged via all registered custom logger functions.
     *
     * @param {String} identifier An optional identifier to incorporate into the
     * resulting log entry.
     *
     * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with
     * the original value or rejected with the original reason.
     */
    log: function (identifier) {
        if (identifier == null) {
            identifier = '';
        }
        
        return this._owner.then(function (value) {
            Ext.log("" + (identifier || 'Promise') + " resolved with value: " + value);
            
            return value;
        }, function (reason) {
            Ext.log("" + (identifier || 'Promise') + " rejected with reason: " + reason);
            
            throw reason;
        });
    }
}},
function (ExtPromise) {
    ExtPromise._ready();
});
