/**
 * Class that can manage the execution of route handlers. All {@link #befores} handlers
 * will be executed prior to the {@link #actions} handlers. If at any point this `Action`
 * class is stopped, no other handler (before or action) will be executed.
 */
Ext.define('Ext.route.Action', {
    config: {
        /**
         * @cfg {Function[]} actions
         * The action handlers to execute in response to the route executing.
         * The individual functions will be executed with the scope of the class
         * that connected the route and the arguments will be the configured URL
         * parameters in order they appear in the token.
         *
         * See {@link #befores} also.
         */
        actions: null,
 
        /**
         * @cfg {Function[]} befores
         * The before handlers to execute prior to the {@link #actions} handlers.
         * The individual functions will be executed with the scope of the class
         * that connected the route and the arguments will be the configured URL
         * parameters in the order they appear in the token plus this `Action` instance
         * as the last argument.
         *
         * **IMPORTANT** A before function must have a resolution. You can do this
         * by executing the {@link #resume} or {@link #stop} function or you can
         * return a promise and resolve/reject it.
         *
         *     var action = new Ext.route.Action({
         *         before: {
         *             fn: function (action) {
         *                 action.resume(); //or action.stop();
         *             }
         *         }
         *     });
         *     action.run();
         *
         *     var action = new Ext.route.Action({
         *         before: {
         *             fn: function () {
         *                 return new Ext.Promise(function (resolve, reject) {
         *                     resolve(); //or reject();
         *                 });
         *             }
         *         }
         *     });
         *     action.run();
         *
         * See {@link #actions} also.
         */
        befores: null,
 
        /**
         * @cfg {Array} urlParams
         * The URL parameters that were matched by the {@link Ext.route.Route}.
         */
        urlParams: []
    },
 
    /**
     * @property {Ext.Deferred} deferred
     * The deferral object that will resolve after all functions have executed
     * ({@link #befores} and {@link #actions}) or reject if any {@link #befores}
     * function stops this action.
     * @private
     */
 
    /**
     * @property {Boolean} [started=false]
     * Whether or not this class has started executing any {@link #befores} or {@link #actions}.
     * @readonly
     * @protected
     */
    started: false,
 
    /**
     * @property {Boolean} [stopped=false]
     * Whether or not this class was stopped by a {@link #befores} function.
     * @readonly
     * @protected
     */
    stopped: false,
 
    constructor: function(config) {
        var me = this;
 
        me.deferred = new Ext.Deferred();
 
        me.resume = me.resume.bind(me);
        me.stop = me.stop.bind(me);
 
        me.initConfig(config);
        me.callParent([config]);
    },
 
    applyActions: function(actions) {
        if (actions) {
            actions = Ext.Array.from(actions);
        }
 
        return actions;
    },
 
    applyBefores: function(befores) {
        if (befores) {
            befores = Ext.Array.from(befores);
        }
 
        return befores;
    },
 
    destroy: function() {
        this.deferred = null;
 
        this
            .setBefores(null)
            .setActions(null)
            .setUrlParams(null);
 
        this.callParent();
    },
 
    /**
     * Allow further function execution of other functions if any.
     *
     * @return {Ext.route.Action} this
     */
    resume: function() {
        return this.next();
    },
 
    /**
     * Prevent other functions from executing and resolve the {@link #deferred}.
     *
     * @return {Ext.route.Action} this
     */
    stop: function() {
        this.stopped = true;
 
        return this.done();
    },
 
    /**
     * Executes the next {@link #befores} or {@link #actions} function. If {@link #stopped}
     * is `true` or no functions are left to execute, the {@link #done} function will be called.
     *
     * @private
     * @return {Ext.route.Action} this
     */
    next: function() {
        var me = this,
            actions = me.getActions(),
            befores = me.getBefores(),
            urlParams = me.getUrlParams(),
            config, ret, args;
 
        if (Ext.isArray(urlParams)) {
            args = urlParams.slice();
        }
        else {
            args = [urlParams];
        }
 
        if (
            me.stopped ||
            (befores ? !befores.length : true) &&
            (actions ? !actions.length : true)
        ) {
            me.done();
        }
        else {
            if (befores && befores.length) {
                config = befores.shift();
 
                args.push(me);
 
                ret = Ext.callback(config.fn, config.scope, args);
 
                if (ret && ret.then) {
                    ret.then(function(arg) {
                        me.resume(arg);
                    }, function(arg) {
                        me.stop(arg);
                    });
                }
            }
            else if (actions && actions.length) {
                config = actions.shift();
 
                Ext.callback(config.fn, config.scope, args);
 
                me.next();
            }
            else {
                // needed?
                me.next();
            }
        }
 
        return me;
    },
 
    /**
     * Starts the execution of {@link #befores} and/or {@link #actions} functions.
     *
     * @return {Ext.promise.Promise} 
     */
    run: function() {
        var deferred = this.deferred;
 
        if (!this.started) {
            this.next();
 
            this.started = true;
        }
 
        return deferred.promise;
    },
 
    /**
     * When no {@link #befores} or {@link #actions} functions are left to execute
     * or {@link #stopped} is `true`, this function will be executed to resolve
     * or reject the {@link #deferred} object.
     *
     * @private
     * @return {Ext.route.Action} this
     */
    done: function() {
        var deferred = this.deferred;
 
        if (this.stopped) {
            deferred.reject();
        }
        else {
            deferred.resolve();
        }
 
        this.destroy();
 
        return this;
    },
 
    /**
     * Add a function to the {@link #befores} stack.
     *
     *     action.before(function() {}, this);
     *
     * By default, the function will be added to the end of the {@link #befores} stack. If
     * instead the function should be placed at the beginning of the stack, you can pass
     * `true` as the first argument:
     *
     *     action.before(true, function() {}, this);
     *
     * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
     * {@link #befores} stack instead of the end.
     * @param {Function/String} fn The function to add to the {@link #befores}.
     * @param {Object} [scope] The scope of the function to execute with. This is normally
     * the class that is adding the function to the before stack.
     * @return {Ext.route.Action} this
     */
    before: function(first, fn, scope) {
        if (!Ext.isBoolean(first)) {
            scope = fn;
            fn = first;
            first = false;
        }
 
        // eslint-disable-next-line vars-on-top
        var befores = this.getBefores(),
            config = {
                fn: fn,
                scope: scope
            };
 
        //<debug>
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' +
                      'execute this function.');
 
            return;
        }
        //</debug>
 
        if (befores) {
            if (first) {
                befores.unshift(config);
            }
            else {
                befores.push(config);
            }
        }
        else {
            this.setBefores(config);
        }
 
        return this;
    },
 
    /**
     * Add a function to the {@link #actions} stack.
     *
     *     action.action(function() {}, this);
     *
     * By default, the function will be added to the end of the {@link #actions} stack. If
     * instead the function should be placed at the beginning of the stack, you can pass
     * `true` as the first argument:
     *
     *     action.action(true, function() {}, this);
     *
     * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
     * {@link #befores} stack.
     * @param {Function/String} fn The function to add to the {@link #actions}.
     * @param {Object} [scope] The scope of the function to execute with. This is normally
     * the class that is adding the function to the action stack.
     * @return {Ext.route.Action} this
     */
    action: function(first, fn, scope) {
        if (!Ext.isBoolean(first)) {
            scope = fn;
            fn = first;
            first = false;
        }
 
        // eslint-disable-next-line vars-on-top
        var actions = this.getActions(),
            config = {
                fn: fn,
                scope: scope
            };
 
        //<debug>
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' +
                      'execute this function.');
 
            return;
        }
        //</debug>
 
        if (actions) {
            if (first) {
                actions.unshift(config);
            }
            else {
                actions.push(config);
            }
        }
        else {
            this.setActions(config);
        }
 
        return this;
    },
 
    /**
     * Execute functions when this action has been resolved or rejected.
     *
     * @param {Function} resolve The function to execute when this action has been resolved.
     * @param {Function} reject The function to execute when a before function stopped this action.
     * @return {Ext.Promise} 
     */
    then: function(resolve, reject) {
        //<debug>
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' +
                      'execute either function.');
 
            return;
        }
        //</debug>
 
        return this.deferred.then(resolve, reject);
    }
});