/**
 * @private
 */
Ext.define('Ext.event.publisher.Focus', {
    extend: 'Ext.event.publisher.Dom',
    requires: [
        'Ext.dom.Element',
        'Ext.GlobalEvents'
    ],
 
    type: 'focus',
 
    handledEvents: ['focusenter', 'focusleave', 'focusmove'],
 
    // At this point only Firefox does not support focusin/focusout, see this bug:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=687787
    // TODO: Fix event order: https://github.com/jquery/jquery/issues/3123
    handledDomEvents: ['focusin', 'focusout'],
 
    publishDelegatedDomEvent: function(e) {
        var me = this,
            relatedTarget = e.relatedTarget;
 
        //<debug>
        if (me.$suppressEvents) {
            return;
        }
        //</debug>
 
        if (e.type === 'focusout') {
            // If focus is departing to the document, there will be no forthcoming focusin event
            // to trigger a focusleave, so fire a focusleave now.
            if (relatedTarget == null) {
                me.processFocusIn(e, e.target, document.body);
            }
        }
        else {
            // IE reports relatedTarget as either an inaccessible object which coercively
            // equates to null, or just a blank object in the case of focusing from nowhere.
            if (relatedTarget == null || !relatedTarget.tagName) {
                relatedTarget = document.body;
            }
 
            me.processFocusIn(e, relatedTarget, e.target);
        }
    },
 
    processFocusIn: function(e, fromElement, toElement) {
        var me = this,
            focusFly = me.focusFly,
            targets = [],
            commonAncestor, node, backwards, event, focusEnterEvent;
 
        // If we have suspended focus/blur processing due to framework needing to
        // silently manipulate focus position, then return early.
        if ((fromElement && focusFly.attach(fromElement).isFocusSuspended()) ||
            (toElement && focusFly.attach(toElement).isFocusSuspended())) {
            return;
        }
 
        if (toElement.compareDocumentPosition) {
            // Flag if the fromElement is DOCUMENT_POSITION_FOLLOWING toElement
            backwards = !!(toElement.compareDocumentPosition(fromElement) & 4);
        }
 
        // Gather targets for focusleave event from the fromElement to the parentNode
        // (not inclusive)
        // eslint-disable-next-line max-len
        for (node = fromElement, commonAncestor = Ext.dom.Element.getCommonAncestor(toElement, fromElement, true);
            node && node !== commonAncestor; node = node.parentNode) {
            targets.push(node);
        }
 
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            event = me.createSyntheticEvent('focusleave', e, fromElement, toElement, fromElement,
                                            toElement, backwards);
            me.publish(event, targets);
 
            if (event.stopped) {
                return;
            }
        }
 
        // Gather targets for focusenter event from the focus targetElement to the parentNode
        // (not inclusive)
        targets.length = 0;
 
        for (node = toElement; node && node !== commonAncestor; node = node.parentNode) {
            targets.push(node);
        }
 
        // We always need this event; this is what we pass to the global focus event
        focusEnterEvent = me.createSyntheticEvent('focusenter', e, toElement, fromElement,
                                                  fromElement, toElement, backwards);
 
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            me.publish(focusEnterEvent, targets);
 
            if (focusEnterEvent.stopped) {
                return;
            }
        }
 
        // When focus moves within an element, fire a bubbling focusmove event
        targets = me.getPropagatingTargets(commonAncestor);
 
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            event = me.createSyntheticEvent('focusmove', e, toElement, fromElement, fromElement,
                                            toElement, backwards);
            me.publish(event, targets);
 
            if (event.stopped) {
                return;
            }
        }
 
        if (Ext.GlobalEvents.hasListeners.focus) {
            Ext.GlobalEvents.fireEvent('focus', {
                event: focusEnterEvent,
                toElement: toElement,
                fromElement: fromElement,
                backwards: backwards
            });
        }
    },
 
    createSyntheticEvent: function(eventName, browserEvent, target, relatedTarget, fromElement,
        toElement, backwards) {
        var event = new Ext.event.Event(browserEvent);
 
        event.type = eventName;
        event.relatedTarget = relatedTarget;
        event.target = target;
        event.fromElement = fromElement;
        event.toElement = toElement;
        event.backwards = backwards;
 
        return event;
    }
}, function(Focus) {
    var focusTimeout;
 
    Focus.prototype.focusFly = new Ext.dom.Fly();
    Focus.instance = new Focus();
 
    // At this point only Firefox does not support focusin/focusout, see this bug:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=687787
    if (!Ext.supports.FocusinFocusoutEvents) {
        // When focusin/focusout are not available we capture focus event instead,
        // and fire both focusenter *and* focusleave in the focus handler.
        this.override({
            handledDomEvents: ['focus', 'blur'],
 
            publishDelegatedDomEvent: function(e) {
                var me = this,
                    targetIsElement;
 
                me.callSuper([e]);
 
                // We need to know if event target was an element or (window || document)
                targetIsElement = e.target !== window && e.target !== document;
 
                // There might be an upcoming focus event, but if none happens
                // within a minimal timeout, then we treat this as a focus of the body
                if (e.type === 'blur') {
                    if (!targetIsElement) {
                        // Apparently when focus goes outside of the document, Firefox
                        // will fire blur on the currently focused element, then on the document,
                        // then on the window. Interestingly enough, both follow-up blur events
                        // will have explicitOriginalTarget pointing at the previously focused
                        // element; when that happens we can be reasonably sure that focus
                        // indeed goes out the window.
                        if (e.explicitOriginalTarget === Focus.previousActiveElement) {
                            // But we want that to fire only once, so process window blur
                            // which happens last.
                            if (e.target === window) {
                                Ext.undefer(focusTimeout);
                                focusTimeout = 0;
                                me.processFocusIn(e, Focus.previousActiveElement, document.body);
                                Focus.previousActiveElement = null;
                            }
                        }
                    }
                    else {
                        // If event target is a valid element, blur could have been caused
                        // by removing previously focused element from the DOM, or some
                        // other happening that doesn't involve <strike>Elvis</strike>focus
                        // completely leaving the building.
                        focusTimeout = Ext.defer(function() {
                            focusTimeout = 0;
                            me.processFocusIn(e, e.target, document.body);
                            Focus.previousActiveElement = null;
                        }, 1);
 
                        // Store the timer in case the element gets destroyed before
                        // the function above has a chance to fire
                        if (targetIsElement && Ext.cache[e.target.id]) {
                            Ext.cache[e.target.id].focusinTimeout = focusTimeout;
                        }
                    }
 
                    Focus.previousActiveElement = targetIsElement ? e.target : null;
                }
                else {
                    Ext.undefer(focusTimeout);
                    focusTimeout = 0;
 
                    me.processFocusIn(
                        e,
                        Focus.previousActiveElement || document.body,
                        targetIsElement ? e.target : document.body
                    );
                }
            }
        });
 
        Ext.define(null, {
            override: 'Ext.dom.Element',
 
            destroy: function() {
                if (this.focusinTimeout) {
                    Ext.undefer(this.focusinTimeout);
                    this.focusinTimeout = null;
                }
 
                this.callParent();
            }
        });
    }
});