/**
 * @private
 */
Ext.define('Ext.event.Controller', {
 
    isFiring: false,
 
    listenerStack: null,
 
    constructor: function(dispatcher) {
        this.firingListeners = [];
        this.firingArguments = [];
        this.dispatcher = dispatcher;
        return this;
    },
 
    setInfo: function(info) {
        this.info = info;
    },
 
    getInfo: function() {
        return this.info;
    },
 
    setListenerStacks: function(listenerStacks) {
        this.listenerStacks = listenerStacks;
    },
 
    fire: function(args, action) {
        var listenerStacks = this.listenerStacks,
            firingListeners = this.firingListeners,
            firingArguments = this.firingArguments,
            push = firingListeners.push,
            ln = listenerStacks.length,
            listeners, beforeListeners, currentListeners, afterListeners,
            isActionBefore = false,
            isActionAfter = false,
            i;
 
        firingListeners.length = 0;
 
        if (action) {
            if (action.order !== 'after') {
                isActionBefore = true;
            }
            else {
                isActionAfter = true;
            }
        }
 
        if (ln === 1) {
            listeners = listenerStacks[0].listeners;
            beforeListeners = listeners.before;
            currentListeners = listeners.current;
            afterListeners = listeners.after;
 
            if (beforeListeners.length > 0) {
                push.apply(firingListeners, beforeListeners);
            }
 
            if (isActionBefore) {
                push.call(firingListeners, action);
            }
 
            if (currentListeners.length > 0) {
                push.apply(firingListeners, currentListeners);
            }
 
            if (isActionAfter) {
                push.call(firingListeners, action);
            }
 
            if (afterListeners.length > 0) {
                push.apply(firingListeners, afterListeners);
            }
        }
        else {
            for (= 0; i < ln; i++) {
                beforeListeners = listenerStacks[i].listeners.before;
                if (beforeListeners.length > 0) {
                    push.apply(firingListeners, beforeListeners);
                }
            }
 
            if (isActionBefore) {
                push.call(firingListeners, action);
            }
 
            for (= 0; i < ln; i++) {
                currentListeners = listenerStacks[i].listeners.current;
                if (currentListeners.length > 0) {
                    push.apply(firingListeners, currentListeners);
                }
            }
 
            if (isActionAfter) {
                push.call(firingListeners, action);
            }
 
            for (= 0; i < ln; i++) {
                afterListeners = listenerStacks[i].listeners.after;
                if (afterListeners.length > 0) {
                    push.apply(firingListeners, afterListeners);
                }
            }
        }
 
        if (firingListeners.length === 0) {
            return this;
        }
 
        if (!args) {
            args = [];
        }
 
        firingArguments.length = 0;
        firingArguments.push.apply(firingArguments, args);
 
        // Backwards compatibility 
        firingArguments.push(null, this);
 
        this.doFire();
 
        return this;
    },
 
    doFire: function() {
        var me = this,
            firingListeners = me.firingListeners,
            firingArguments = me.firingArguments,
            arg1 = firingArguments[1],
            optionsArgumentIndex = firingArguments.length - 2,
            observable = firingListeners[0].observable,
            info = me.info,
            event, i, ln, listener, options, fn, firingFn,
            boundFn, isLateBinding, scope, args, result, type, beforeFn;
 
        if (observable && observable.isElement) {
            event = firingArguments[0];
 
            // this is to catch dom events that do have a underlying dom event to wrap (painted & resize) 
            // In this case you will get an 'Ext.Element' as the first parameter to the listener 
            if (event && !event.isEvent) {
                event = null;
            }
        }
 
        me.isPausing = me.isPaused = me.isStopped = false;
        me.isFiring = true;
 
        for (= 0,ln = firingListeners.length; i < ln; i++) {
            listener = firingListeners[i];
            options = listener.options;
            fn = listener.fn;
            firingFn = listener.firingFn;
            boundFn = listener.boundFn;
            isLateBinding = listener.isLateBinding;
            scope = listener.scope;
 
            // Re-bind the callback if it has changed since the last time it's bound (overridden) 
            if (isLateBinding && boundFn && boundFn !== scope[fn]) {
                boundFn = false;
                firingFn = false;
            }
 
            if (!boundFn) {
                if (isLateBinding) {
                    boundFn = scope[fn];
 
                    if (!boundFn) {
                        continue;
                    }
                }
                else {
                    boundFn = fn;
                }
 
                listener.boundFn = boundFn;
            }
 
            if (event) {
                // For events that have been translated to provide device compatibility, 
                // e.g. mousedown -> touchstart, we want the event object to reflect the 
                // type that was originally listened for, not the type of the actual event 
                // that fired. The listener's "type" property reflects the original type. 
                type = listener.type;
                if (type) {
                    // chain a new object to the event object before changing the type. 
                    // This is more efficient than creating a new event object, and we 
                    // don't want to change the type of the original event because it may 
                    // be used asynchronously by other handlers 
                    firingArguments[0] = event.chain({ type: type });
                }
 
                // In Ext4 Ext.EventObject was a singleton event object that was reused as events 
                // were fired.  Set Ext.EventObject to the last fired event for compatibility. 
                Ext.EventObject = firingArguments[0];
            }
 
            if (!firingFn) {
                firingFn = boundFn;
 
                if (options.delay) {
                    firingFn = Ext.Function.createDelayed(firingFn, options.delay, scope);
                }
 
                else if (options.buffer) {
                    firingFn = Ext.Function.createBuffered(firingFn, options.buffer, scope);
                }
 
                else if (options.onFrame) {
                    firingFn = Ext.Function.createAnimationFrame(firingFn, scope);
                }
 
                listener.firingFn = firingFn;
            }
 
            firingArguments[optionsArgumentIndex] = options;
 
            args = firingArguments;
 
            if (options.args) {
                args = options.args.concat(args);
            }
 
            // If using the delegate option, fix the target to be the delegate element, 
            // however we only want to do this for element delegates. 
            if (options.delegate && me.info.targetType === 'element') {
                firingArguments[1] = Ext.fly(arg1).findParent(info.target, arg1);
            }
 
            if (options.single) {
                // need to call the dispatcher, instead of just removing from the stack 
                // here because we need to also tell the publisher to unsubscribe 
                me.dispatcher.removeListener(info.targetType, info.target, info.eventName,
                    fn, scope, options, listener.order, listener.observable);
            }
 
            beforeFn = options.beforeFn;
            // Currently, the usage for the beforeFn is used for normalizing DOM events, for example 
            // mouseenter/mouseleave get transformed to mouseover/mouseout in browsers that don't support them. 
            // We give those functions a chance to jump in here and veto if necessary. We need to do this late 
            // because we have to do it just before resolving the real caller 
            if (beforeFn) {
                result = beforeFn.apply(Ext.global, args) !== false;
                if (result !== false) {
                    result = firingFn.apply(scope, args);
                }
            } else {
                result = firingFn.apply(scope, args);
            }
 
            // Restore the second argument if it wss set to the delegated target 
            if (options.delegate) {
                firingArguments[1] = arg1;
            }
 
            if (result === false) {
                me.stop();
            }
 
            if (me.isStopped) {
                break;
            }
 
            if (me.isPausing) {
                me.isPaused = true;
                firingListeners.splice(0, i + 1);
                return;
            }
 
            if (event) {
                // reset the arguments' event object to the original non-translated one. 
                firingArguments[0] = event;
            }
        }
 
        me.isFiring = false;
        me.listenerStacks = null;
        firingListeners.length = firingArguments.length = 0;
        me.connectingController = null;
    },
 
    connect: function(controller) {
        this.connectingController = controller;
    },
 
    resume: function() {
        var connectingController = this.connectingController;
 
        this.isPausing = false;
 
        if (this.isPaused && this.firingListeners.length > 0) {
            this.isPaused = false;
            this.doFire();
        }
 
        if (connectingController) {
            connectingController.resume();
        }
 
        return this;
    },
 
    isInterrupted: function() {
        return this.isStopped || this.isPaused;
    },
 
    stop: function() {
        var connectingController = this.connectingController;
 
        this.isStopped = true;
 
        if (connectingController) {
            this.connectingController = null;
            connectingController.stop();
        }
 
        this.isFiring = false;
 
        this.listenerStacks = null;
 
        return this;
    },
 
    pause: function() {
        var connectingController = this.connectingController;
 
        this.isPausing = true;
 
        if (connectingController) {
            connectingController.pause();
        }
 
        return this;
    }
});