// @tag core 
/**
 * Represents single event type that an Observable object listens to.
 * All actual listeners are tracked inside here.  When the event fires,
 * it calls all the registered listener functions.
 *
 * @private
 */
Ext.define('Ext.util.Event', function() {
  var arraySlice = Array.prototype.slice,
      arrayInsert = Ext.Array.insert,
      toArray = Ext.Array.toArray,
      fireArgs = {};
 
  return {
    requires: 'Ext.util.DelayedTask',
 
    /**
     * @property {Boolean} isEvent 
     * `true` in this class to identify an object as an instantiated Event, or subclass thereof.
     */
    isEvent: true,
    
    // Private. Event suspend count 
    suspended: 0,
 
    noOptions: {},
 
    constructor: function(observable, name) {
        this.name = name;
        this.observable = observable;
        this.listeners = [];
    },
 
    addListener: function(fn, scope, options, caller, manager) {
        var me = this,
            added = false,
            observable = me.observable,
            eventName = me.name,
            listeners, listener, priority, isNegativePriority, highestNegativePriorityIndex,
            hasNegativePriorityIndex, length, index, i, listenerPriority,
            managedListeners;
 
        //<debug> 
        if (scope && !Ext._namedScopes[scope] && (typeof fn === 'string') && (typeof scope[fn] !== 'function')) {
            Ext.raise("No method named '" + fn + "' found on scope object");
        }
        //</debug> 
 
        if (me.findListener(fn, scope) === -1) {
            listener = me.createListener(fn, scope, options, caller, manager);
            if (me.firing) {
                // if we are currently firing this event, don't disturb the listener loop 
                me.listeners = me.listeners.slice(0);
            }
            listeners = me.listeners;
            index = length = listeners.length;
            priority = options && options.priority;
            highestNegativePriorityIndex = me._highestNegativePriorityIndex;
            hasNegativePriorityIndex = highestNegativePriorityIndex !== undefined;
            if (priority) {
                // Find the index at which to insert the listener into the listeners array, 
                // sorted by priority highest to lowest. 
                isNegativePriority = (priority < 0);
                if (!isNegativePriority || hasNegativePriorityIndex) {
                    // If the priority is a positive number, or if it is a negative number 
                    // and there are other existing negative priority listenrs, then we 
                    // need to calcuate the listeners priority-order index. 
                    // If the priority is a negative number, begin the search for priority 
                    // order index at the index of the highest existing negative priority 
                    // listener, otherwise begin at 0 
                    for(= (isNegativePriority ? highestNegativePriorityIndex : 0); i < length; i++) {
                        // Listeners created without options will have no "o" property 
                        listenerPriority = listeners[i].o ? listeners[i].o.priority||0 : 0;
                        if (listenerPriority < priority) {
                            index = i;
                            break;
                        }
                    }
                } else {
                    // if the priority is a negative number, and there are no other negative 
                    // priority listeners, then no calculation is needed - the negative 
                    // priority listener gets appended to the end of the listeners array. 
                    me._highestNegativePriorityIndex = index;
                }
            } else if (hasNegativePriorityIndex) {
                // listeners with a priority of 0 or undefined are appended to the end of 
                // the listeners array unless there are negative priority listeners in the 
                // listeners array, then they are inserted before the highest negative 
                // priority listener. 
                index = highestNegativePriorityIndex;
            }
 
            if (!isNegativePriority && index <= highestNegativePriorityIndex) {
                me._highestNegativePriorityIndex ++;
            }
            if (index === length) {
                listeners[length] = listener;
            } else {
                arrayInsert(listeners, index, [listener]);
            }
 
            if (observable.isElement) {
                // It is the role of Ext.util.Event (vs Ext.Element) to handle subscribe/ 
                // unsubscribe because it is the lowest level place to intercept the 
                // listener before it is added/removed.  For addListener this could easily 
                // be done in Ext.Element's doAddListener override, but since there are 
                // multiple paths for listener removal (un, clearListeners), it is best 
                // to keep all subscribe/unsubscribe logic here. 
                observable._getPublisher(eventName, options.translate === false).subscribe(
                    observable,
                    eventName,
                    options.delegated !== false,
                    options.capture
                );
            }
 
            // If the listener was passed with a manager, add it to the manager's list. 
            if (manager) {
                // if scope is an observable, the listener will be automatically managed 
                // this eliminates the need to call mon() in a majority of cases 
                managedListeners = manager.managedListeners || (manager.managedListeners = []);
                managedListeners.push({
                    item: me.observable,
                    ename: (options && options.managedName) || me.name,
                    fn: fn,
                    scope: scope,
                    options: options
                });
            }
 
            added = true;
        }
 
        return added;
    },
 
    createListener: function(fn, scope, o, caller, manager) {
        var me = this,
            namedScope = Ext._namedScopes[scope],
            listener = {
                fn: fn,
                scope: scope,
                ev: me,
                caller: caller,
                manager: manager,
                namedScope: namedScope,
                defaultScope: namedScope ? (scope || me.observable) : undefined,
                lateBound: typeof fn === 'string'
            },
            handler = fn,
            wrapped = false,
            type;
 
        // The order is important. The 'single' wrapper must be wrapped by the 'buffer' and 'delayed' wrapper 
        // because the event removal that the single listener does destroys the listener's DelayedTask(s) 
        if (o) {
            listener.o = o;
            if (o.single) {
                handler = me.createSingle(handler, listener, o, scope);
                wrapped = true;
            }
            if (o.target) {
                handler = me.createTargeted(handler, listener, o, scope, wrapped);
                wrapped = true;
            }
            if (o.onFrame) {
                handler = me.createAnimFrame(handler, listener, o, scope, wrapped);
                wrapped = true;
            }
            if (o.delay) {
                handler = me.createDelayed(handler, listener, o, scope, wrapped);
                wrapped = true;
            }
            if (o.buffer) {
                handler = me.createBuffered(handler, listener, o, scope, wrapped);
                wrapped = true;
            }
 
            if (me.observable.isElement) {
                // If the event type was translated, e.g. mousedown -> touchstart, we need to save 
                // the original type in the listener object so that the Ext.event.Event object can 
                // reflect the correct type at firing time 
                type = o.type;
                if (type) {
                    listener.type = type;
                }
            }
        }
 
        listener.fireFn = handler;
        listener.wrapped = wrapped;
        return listener;
    },
 
    findListener: function(fn, scope) {
        var listeners = this.listeners,
            i = listeners.length,
            listener;
 
        while (i--) {
            listener = listeners[i];
            if (listener) {
                // use ==, not === for scope comparison, so that undefined and null are equal 
                if (listener.fn === fn && listener.scope == scope) {
                    return i;
                }
            }
        }
 
        return - 1;
    },
 
    removeListener: function(fn, scope, index) {
        var me = this,
            removed = false,
            observable = me.observable,
            eventName = me.name,
            listener, options, manager, managedListeners, managedListener, i;
 
        index = index != null ? index : me.findListener(fn, scope);
 
        if (index !== -1) {
            listener = me.listeners[index];
 
            if (me.firing) {
                me.listeners = me.listeners.slice(0);
            }
 
            // Remove this listener from the listeners array. We can use splice directly here. 
            // The IE8 bug which Ext.Array works around only affects *insertion* 
            // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/ 
            me.listeners.splice(index, 1);
 
            // if the listeners array contains negative priority listeners, adjust the 
            // internal index if needed. 
            if (me._highestNegativePriorityIndex) {
                if (index < me._highestNegativePriorityIndex) {
                    me._highestNegativePriorityIndex--;
                }
                else if (index === me._highestNegativePriorityIndex && index === me.listeners.length) {
                    delete me._highestNegativePriorityIndex;
                }
            }
 
            if (listener) {
                options = listener.o;
    
                // cancel and remove a buffered handler that hasn't fired yet. 
                // When the buffered listener is invoked, it must check whether 
                // it still has a task. 
                if (listener.task) {
                    listener.task.cancel();
                    delete listener.task;
                }
    
                // cancel and remove all delayed handlers that haven't fired yet 
                i = listener.tasks && listener.tasks.length;
 
                if (i) {
                    while (i--) {
                        listener.tasks[i].cancel();
                    }
 
                    delete listener.tasks;
                }
                
                // Cancel the timer that could have been set if the event has already fired 
                listener.fireFn.timerId = Ext.undefer(listener.fireFn.timerId);
    
                manager = listener.manager;
 
                if (manager) {
                    // If this is a managed listener we need to remove it from the manager's 
                    // managedListeners array.  This ensures that if we listen using mon 
                    // and then remove without using mun, the managedListeners array is updated 
                    // accordingly, for example 
                    // 
                    //     manager.on(target, 'foo', fn); 
                    // 
                    //     target.un('foo', fn); 
                    managedListeners = manager.managedListeners;
 
                    if (managedListeners) {
                        for (= managedListeners.length; i--;) {
                            managedListener = managedListeners[i];
 
                            if (managedListener.item === me.observable &&
                                managedListener.ename === eventName &&
                                managedListener.fn === fn && managedListener.scope === scope) {
                                managedListeners.splice(i, 1);
                            }
                        }
                    }
                }
    
                if (observable.isElement) {
                    observable._getPublisher(eventName, options.translate === false).unsubscribe(
                        observable,
                        eventName,
                        options.delegated !== false,
                        options.capture
                    );
                }
            }
            
            removed = true;
        }
 
        return removed;
    },
 
    // Iterate to stop any buffered/delayed events 
    clearListeners: function() {
        var listeners = this.listeners,
            i = listeners.length,
            listener;
 
        while (i--) {
            listener = listeners[i];
            this.removeListener(listener.fn, listener.scope);
        }
    },
 
    suspend: function() {
        ++this.suspended;
    },
 
    resume: function() {
        if (this.suspended) {
            --this.suspended;
        }
    },
    
    isSuspended: function() {
        return this.suspended > 0;
    },
 
    fireDelegated: function(firingObservable, args) {
        this.firingObservable = firingObservable;
        return this.fire.apply(this, args);
    },
 
    fire: function() {
        var me = this,
            CQ = Ext.ComponentQuery,
            listeners = me.listeners,
            count = listeners.length,
            observable = me.observable,
            isElement = observable.isElement,
            isComponent = observable.isComponent,
            firingObservable = me.firingObservable,
            options, delegate, fireInfo, i, args, listener, len, delegateEl, currentTarget,
            type, chained, firingArgs, e, fireFn, fireScope;
 
        if (!me.suspended && count > 0) {
            me.firing = true;
            args = arguments.length ? arraySlice.call(arguments, 0) : [];
            len = args.length;
            
            if (isElement) {
                e = args[0];
            }
 
            for (= 0; i < count; i++) {
                listener = listeners[i];
                
                // Listener may be undefined if one of the previous listeners 
                // destroyed the observable that was listening to these events. 
                // We'd be still in the middle of the loop here, unawares. 
                if (!listener) {
                    continue;
                }
                
                options = listener.o;
 
                if (isElement) {
                    if (currentTarget) {
                        // restore the previous currentTarget if we changed it last time 
                        // around the loop while processing the delegate option. 
                        e.setCurrentTarget(currentTarget);
                    }
 
                    // 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 
                        // Translated events are not gestures. They must appear to be 
                        // atomic events, so that they can be stopped. 
                        chained = e;
                        e = args[0] = chained.chain({ type: type, isGesture: false });
                    }
 
                    // 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 = e;
                }
 
                firingArgs = args;
 
                if (options) {
                    delegate = options.delegate;
                    if (delegate) {
                        if (isElement) {
                            // prepending the currentTarget.id to the delegate selector 
                            // allows us to match selectors such as "> div" 
                            delegateEl = e.getTarget(typeof delegate === 'function' ?
                                delegate : '#' + e.currentTarget.id + ' ' + delegate
                            );
                            if (delegateEl) {
                                args[1] = delegateEl;
                                // save the current target before changing it to the delegateEl 
                                // so that we can restore it next time around 
                                currentTarget = e.currentTarget;
                                e.setCurrentTarget(delegateEl);
                            } else {
                                continue;
                            }
                        } else if (isComponent && !CQ.is(firingObservable, delegate, observable)) {
                            continue;
                        }
                    }
                    
                    if (isElement) {
                        if (options.preventDefault) {
                            e.preventDefault();
                        }
    
                        if (options.stopPropagation) {
                            e.stopPropagation();
                        }
    
                        if (options.stopEvent) {
                            e.stopEvent();
                        }
                    }
 
                    args[len] = options;
 
                    if (options.args) {
                        firingArgs = options.args.concat(args);
                    }
                }
 
                fireInfo = me.getFireInfo(listener);
                fireFn = fireInfo.fn;
                fireScope = fireInfo.scope;
                
                // We don't want to keep closure and scope on the Event prototype! 
                fireInfo.fn = fireInfo.scope = null;
                
                // If the scope is already destroyed, we absolutely cannot deliver events to it. 
                // We also need to clean up the listener to avoid it hanging around forever 
                // like a zombie. Scope can be null/undefined, that's normal. 
                if (fireScope && fireScope.destroyed) {
                    me.removeListener(fireFn, fireScope, i);
                    fireFn = null;
                    
                    //<debug> 
                    // DON'T raise errors if the destroyed scope is an Ext.container.Monitor! 
                    // It is to be deprecated and removed shortly. 
                    if (fireScope.$className !== 'Ext.container.Monitor') {
                        Ext.raise({
                            msg: 'Attempting to fire "' + me.name + '" event on destroyed ' +
                                  (fireScope.$className || 'object') + ' instance with id: ' +
                                  (fireScope.id || 'unknown'),
                            instance: fireScope
                        });
                    }
                    //</debug> 
                }
                
                // N.B. This is where actual listener code is called. Step boldly into! 
                if (fireFn && fireFn.apply(fireScope, firingArgs) === false) {
                    Ext.EventObject = null;
                    
                    return (me.firing = false);
                }
 
                // We should remove the last item here to avoid future listeners 
                // in the Array to inherit these options by mistake 
                if (options) {
                    args.length--;
                }
 
                if (chained) {
                    // if we chained the event object for type translation we need to 
                    // un-chain it before proceeding to process the next listener, which 
                    // may not be a translated event. 
                    e = args[0] = chained;
                    chained = null;
                }
                
                // We don't guarantee Ext.EventObject existence outside of the immediate 
                // event propagation scope 
                Ext.EventObject = null;
            }
        }
        
        me.firing = false;
        
        return true;
    },
 
    getFireInfo: function(listener, fromWrapped) {
        var observable = this.observable,
            fireFn = listener.fireFn,
            scope = listener.scope,
            namedScope = listener.namedScope,
            fn;
 
        // If we are called with a wrapped listener, only attempt to do scope 
        // resolution if we are explicitly called by the last wrapped function 
        if (!fromWrapped && listener.wrapped) {
            fireArgs.fn = fireFn;
            return fireArgs;
        }
            
        fn = fromWrapped ? listener.fn : fireFn;
 
        //<debug> 
        var name = fn;
        //</debug> 
 
        if (listener.lateBound) {
            // handler is a function name - need to resolve it to a function reference 
            if (!scope || namedScope) {
                // Only invoke resolveListenerScope if the user did not specify a scope, 
                // or if the user specified a named scope.  Named function handlers that 
                // use an arbitrary object as the scope just skip this part, and just 
                // use the given scope object to resolve the method. 
                scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope);
            }
            //<debug> 
            if (!scope) {
                Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);
            }
 
            if (!Ext.isFunction(scope[fn])) {
                Ext.raise('No method named "' + fn + '" on ' +
                    (scope.$className || 'scope object.'));
            }
            //</debug> 
 
            fn = scope[fn];
        } else if (namedScope && namedScope.isController) {
            // If handler is a function reference and scope:'controller' was requested 
            // we'll do our best to look up a controller. 
            scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope);
            //<debug> 
            if (!scope) {
                Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);
            }
            //</debug> 
        } else if (!scope || namedScope) {
            // If handler is a function reference we use the observable instance as 
            // the default scope 
            scope = observable;
        }
 
        // We can only ever be firing one event at a time, so just keep 
        // overwriting the object we've got in our closure, otherwise we'll be 
        // creating a whole bunch of garbage objects 
        fireArgs.fn = fn;
        fireArgs.scope = scope;
        //<debug> 
        if (!fn) {
            Ext.raise('Unable to dynamically resolve method "' + name + '" on ' + this.observable.$className);
        }
        //</debug> 
        return fireArgs;
    },
 
    createAnimFrame: function (handler, listener, o, scope, wrapped) {
        var fireInfo;
 
        if (!wrapped) {
            fireInfo = listener.ev.getFireInfo(listener, true);
            handler = fireInfo.fn;
            scope = fireInfo.scope;
 
            // We don't want to keep closure and scope references on the Event prototype! 
            fireInfo.fn = fireInfo.scope = null;
        }
 
        return Ext.Function.createAnimationFrame(handler, scope, o.args);
    },
 
    createTargeted: function (handler, listener, o, scope, wrapped) {
        return function(){
            if (o.target === arguments[0]) {
                var fireInfo;
 
                if (!wrapped) {
                    fireInfo = listener.ev.getFireInfo(listener, true);
                    handler = fireInfo.fn;
                    scope = fireInfo.scope;
                    
                    // We don't want to keep closure and scope references on the Event prototype! 
                    fireInfo.fn = fireInfo.scope = null;
                }
 
                return handler.apply(scope, arguments);
            }
        };
    },
 
    createBuffered: function (handler, listener, o, scope, wrapped) {
        listener.task = new Ext.util.DelayedTask();
 
        //<debug> 
        if (Ext.Timer.track) {
            o.$delayedTask = listener.task;  // for unit test access 
        }
        //</debug> 
 
        return function() {
            // If the listener is removed during the event call, the listener stays in the 
            // list of listeners to be invoked in the fire method, but the task is deleted 
            // So if we get here with no task, it's because the listener has been removed. 
            if (listener.task) {
                var fireInfo;
 
                if (!wrapped) {
                    fireInfo = listener.ev.getFireInfo(listener, true);
                    handler = fireInfo.fn;
                    scope = fireInfo.scope;
 
                    // We don't want to keep closure and scope references on the Event prototype! 
                    fireInfo.fn = fireInfo.scope = null;
                }
 
                listener.task.delay(o.buffer, handler, scope, toArray(arguments));
            }
        };
    },
 
    createDelayed: function (handler, listener, o, scope, wrapped) {
        return function() {
            var task = new Ext.util.DelayedTask(),
                fireInfo;
 
            if (!wrapped) {
                fireInfo = listener.ev.getFireInfo(listener, true);
                handler = fireInfo.fn;
                scope = fireInfo.scope;
                
                // We don't want to keep closure and scope references on the Event prototype! 
                fireInfo.fn = fireInfo.scope = null;
            }
                
            if (!listener.tasks) {
                listener.tasks = [];
            }
            listener.tasks.push(task);
 
            //<debug> 
            if (Ext.Timer.track) {
                o.$delayedTask = task;  // for unit test access 
            }
            //</debug> 
 
            task.delay(o.delay || 10, handler, scope, toArray(arguments));
        };
    },
 
    createSingle: function (handler, listener, o, scope, wrapped) {
        return function() {
            var event = listener.ev,
                observable = event.observable,
                fn = listener.fn,
                fireInfo;
 
            // If we have an observable, use that to clean up because there 
            // can be special cases that need handling. For example element 
            // listeners may bind multiple events (mousemove+touchmove) and they 
            // need to act in tandem. 
            if (observable) {
                if (!observable.destroyed) {
                    observable.removeListener(event.name, fn, scope);
                }
            }
            else {
                event.removeListener(fn, scope);
            }
 
            if (!wrapped) {
                fireInfo = event.getFireInfo(listener, true);
                handler = fireInfo.fn;
                scope = fireInfo.scope;
                
                // We don't want to keep closure and scope references on the Event prototype! 
                fireInfo.fn = fireInfo.scope = null;
            }
            
            return handler.apply(scope, arguments);
        };
    }
  };
});