/*
 Ext.promise.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).
 */

/**
 * 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.
 *
 * 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.
 *
 * 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.
 *
 * @private
 * @since 6.0.0
 */
Ext.define('Ext.promise.Deferred', {
    requires: [
        'Ext.promise.Consequence'
    ],

    /**
     * @property {Ext.promise.Promise} promise Promise of the future value of this Deferred.
     */
    promise: null,

    /**
     * @property {Ext.promise.Consequence[]} consequences Pending Consequences chained to this Deferred.
     *
     * @private
     */
    consequences: [],

    /**
     * @property {Boolean} completed Indicates whether this Deferred has been completed.
     *
     * @private
     */
    completed: false,

    /**
     * @property {String} completeAction The completion action (i.e. 'fulfill' or 'reject').
     *
     * @private
     */
    completionAction: null,

    /**
     * @property {Mixed} completionValue The completion value (i.e. resolution value or rejection error).
     *
     * @private
     */
    completionValue: null,

    constructor: function() {
        var me = this;

        me.promise = new Ext.promise.Promise(me);
        me.consequences = [];
        me.completed = false;
        me.completionAction = null;
        me.completionValue = null;
    },

    /**
     * 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
     * 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] Callback to execute to transform a fulfillment value.
     * @param {Function} [onRejected] Callback to execute to transform a rejection reason.
     * @param {Function} [onProgress] Callback to execute to transform a progress value.
     *
     * @return Promise that is fulfilled with the callback return value or rejected with
     * any error thrown by the callback.
     */
    then: function(onFulfilled, onRejected, onProgress) {
        var me = this,
            consequence = new Ext.promise.Consequence(onFulfilled, onRejected, onProgress);

        if (me.completed) {
            consequence.trigger(me.completionAction, me.completionValue);
        }
        else {
            me.consequences.push(consequence);
        }

        return consequence.promise;
    },

    /**
     * Resolve this Deferred with the (optional) specified value.
     *
     * If 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.
     *
     * Once a Deferred has been fulfilled or rejected, it is considered to be complete
     * and subsequent calls to `resolve` or `reject` are ignored.
     *
     * @param {Mixed} value Value to resolve as either a fulfillment value or rejection
     * reason.
     */
    resolve: function(value) {
        var me = this,
            isHandled, thenFn;

        if (me.completed) {
            return;
        }

        try {
            if (value === me.promise) {
                throw new TypeError('A Promise cannot be resolved with itself.');
            }

            if ((Ext.isObject(value) || Ext.isFunction(value)) &&
                        Ext.isFunction(thenFn = value.then)) {
                isHandled = false;

                try {
                    thenFn.call(value, function(value) {
                        if (!isHandled) {
                            isHandled = true;
                            me.resolve(value);
                        }
                    }, function(error) {
                        if (!isHandled) {
                            isHandled = true;
                            me.reject(error);
                        }
                    });
                }
                catch (e) {
                    if (!isHandled) {
                        me.reject(e);
                    }
                }
            }
            else {
                me.complete('fulfill', value);
            }
        }
        catch (e) {
            me.reject(e);
        }
    },

    /**
     * Reject this Deferred with the specified reason.
     *
     * Once a Deferred has been rejected, it is considered to be complete
     * and subsequent calls to `resolve` or `reject` are ignored.
     *
     * @param {Error} reason Rejection reason.
     */
    reject: function(reason) {
        if (this.completed) {
            return;
        }
        
        this.complete('reject', reason);
    },

    /**
     * Updates progress for this Deferred, if it is still pending, triggering it to
     * execute the `onProgress` callback and propagate the resulting transformed progress
     * value to Deferreds that originate from this Deferred.
     *
     * @param {Mixed} progress The progress value.
     */
    update: function(progress) {
        var consequences = this.consequences,
            consequence, i, len;
        
        if (this.completed) {
            return;
        }
        
        for (i = 0, len = consequences.length; i < len; i++) {
            consequence = consequences[i];
            consequence.update(progress);
        }
    },

    /**
     * Complete this Deferred with the specified action and value.
     *
     * @param {String} action Completion action (i.e. 'fufill' or 'reject').
     * @param {Mixed} value Fulfillment value or rejection reason.
     *
     * @private
     */
    complete: function(action, value) {
        var me = this,
            consequences = me.consequences,
            consequence, i, len;

        me.completionAction = action;
        me.completionValue = value;
        me.completed = true;

        for (i = 0, len = consequences.length; i < len; i++) {
            consequence = consequences[i];
            consequence.trigger(me.completionAction, me.completionValue);
        }

        me.consequences = null;
    }
});
