// @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
                        // eslint-disable-next-line max-len
                        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,
                namedScopes = Ext._namedScopes,
                namedScope = 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;
 
            if (listener.lateBound && fn[2] === '.') {
                //<debug>
                if (fn.substr(0, 2) !== 'up') {
                    Ext.raise('Invalid listener method: ' + fn);
                }
                //</debug>
 
                listener.defaultScope = null;
                listener.namedScope = namedScopes[listener.scope = scope = 'up'];
                listener.fn = handler = fn.substr(3);
            }
 
            // 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
                    // eslint-disable-next-line eqeqeq
                    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) {
                        // eslint-disable-next-line max-len
                        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"
                                // eslint-disable-next-line max-len
                                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;
                                }
                            }
                            // eslint-disable-next-line max-len
                            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>
                        // Skip warnings for Ext.container.Monitor
                        // It is to be deprecated and removed shortly.
                        if (fireScope.$className !== 'Ext.container.Monitor') {
                            (Ext.raiseOnDestroyed ? Ext.raise : Ext.log.warn)({
                                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, origin;
 
            // 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; // eslint-disable-line vars-on-top
            //</debug>
 
            if (listener.lateBound) {
                // handler is a function name - need to resolve it to a function reference
                origin = listener.caller || observable;
 
                if (namedScope && namedScope.isUp) {
                    scope = Ext.lookUpFn(origin, fn);
                }
                else 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 = origin.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() {
                var fireInfo;
 
                if (o.target === arguments[0]) {
                    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();
 
            return function() {
                var fireInfo;
 
                // 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) {
                    //<debug>
                    if (Ext._unitTesting) {
                        o.$delayedTask = listener.task;  // for unit test access
                    }
                    //</debug>
 
                    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._unitTesting) {
                    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);
            };
        }
    };
});