/**
 * A class that manages a group of {@link Ext.Component#cfg-floating} Components and 
 * provides z-order management, and Component activation behavior, including masking 
 * below the active (topmost) Component.
 *
 * {@link Ext.Component#cfg-floating Floating} Components which are rendered directly 
 * into the document (such as {@link Ext.window.Window Window}s) which are 
 * {@link Ext.Component#method-show show}n are managed by a
 * {@link Ext.WindowManager global instance}.
 *
 * {@link Ext.Component#cfg-floating Floating} Components which are descendants of 
 * {@link Ext.Component#cfg-floating floating} *Containers* (for example a 
 * {@link Ext.view.BoundList BoundList} within an {@link Ext.window.Window Window},
 * or a {@link Ext.menu.Menu Menu}), are managed by a ZIndexManager owned by that floating Container. Therefore
 * ComboBox dropdowns within Windows will have managed z-indices guaranteed to be correct, relative to the Window.
 */
Ext.define('Ext.ZIndexManager', {
    alternateClassName: 'Ext.WindowGroup',
 
    requires: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.GlobalEvents'
    ],
 
    statics: {
        zBase : 9000,
        activeCounter: 0
    },
    
    reflowSuspended: 0,
 
    /**
     * @private
     */
    constructor: function(container) {
        var me = this;
 
        me.id = Ext.id(null, 'zindex-mgr-');
 
        // The stack is a collection sorted on the incrementing activeCounter ascending, so recently active components 
        // sort to the top. 
        // The component's alwaysOnTop flag takes priority in the sort order and 
        // cause the component to gravitate to the correct end of the stack. 
        me.zIndexStack = new Ext.util.Collection({
            sorters: {
                sorterFn: function(comp1, comp2) {
                    var ret = (comp1.alwaysOnTop || 0) - (comp2.alwaysOnTop || 0);
                    if (!ret) {
                       ret = comp1.getActiveCounter() - comp2.getActiveCounter();
                    }
                    return ret;
                }
            },
            filters: {
                filterFn: function(comp) {
                    return comp.isVisible();
                }
            }
        });
 
        // zIndexStack will call into this class on key lifecycle events if methods exist here. 
        // Specifically, we implement onCollectionSort which is called by Component's updaters for activeCounter and alwaysOnTop. 
        me.zIndexStack.addObserver(me);
        me.front = null;
        me.sortCount = 0;
 
        // Listen for global component hiding and showing. 
        // onComponentShowHide only reacts if we are managing the component. 
        me.globalListeners = Ext.GlobalEvents.on({
 
            // The 'beforehide' global event is a non-vetoable event fired before the component is hidden. 
            // We use this to sort the remaining visible components, and unmask if the hiding component is 
            // the sole modal. 
            beforehide: me.onComponentShowHide,
            show: me.onComponentShowHide,
            scope: me,
            destroyable: true
        });
 
        if (container) {
 
            // This is the ZIndexManager for an Ext.container.Container, base its zseed on the zIndex of the Container's element 
            if (container.isContainer) {
                me.resizeListeners = container.on({
                    resize: me.onContainerResize,
                    scope: me,
                    destroyable: true
                });
                
                me.zseed = Ext.Number.from(me.rendered ? container.getEl().getStyle('zIndex') : undefined, me.getNextZSeed());
                // The containing element we will be dealing with (eg masking) is the content target 
                me.targetEl = container.getTargetEl();
                me.container = container;
            }
            // This is the ZIndexManager for a DOM element 
            else {
                me.resizeListeners = Ext.on({
                    resize: me.onContainerResize,
                    scope: me,
                    destroyable: true
                });
                
                me.zseed = me.getNextZSeed();
                me.targetEl = Ext.get(container);
            }
        }
        // No container passed means we are the global WindowManager. Our target is the doc body. 
        // DOM must be ready to collect that ref. 
        else {
            me.zseed = me.getNextZSeed();
            Ext.onInternalReady(function() {
                // We need to use lowest possible priority here to give enough time 
                // for layouts to run and resize if we're masking a contained component 
                me.resizeListeners = Ext.on({
                    resize: me.scheduleContainerResize,
                    scope: me,
                    destroyable: true,
                    priority: -10000
                });
                
                me.targetEl = Ext.getBody();
            });
        }
    },
 
    // Required to be an Observer of a Collection 
    getId: function() {
        return this.id;
    },
 
    getNextZSeed: function() {
        return (Ext.ZIndexManager.zBase += 10000);
    },
 
    setBase: function(baseZIndex) {
        this.zseed = baseZIndex;
        return this.onCollectionSort();
    },
 
    onCollectionSort: function() {
        var me = this,
            oldFront = me.front,
            zIndex = me.zseed,
            a = me.zIndexStack.getRange(),
            len = a.length,
            i, comp, topModal, topFocusable, topMost,
            doFocus = !oldFront || oldFront.isVisible();
 
        me.sortCount++;
        for (= 0; i < len; i++) {
            comp = a[i];
 
            if (comp.destroying || comp.destroyed) {
                continue;
            }
 
            // Setting the zIndex of a Component returns the topmost zIndex consumed by 
            // that Component. 
            // If it's just a plain floating Component such as a BoundList, then the 
            // return value is the passed value plus 10, ready for the next item. 
            // If a floating *Container* has its zIndex set, it re-orders its managed 
            // floating children, starting from that new base, and returns a value 10000 above 
            // the highest zIndex which it allocates. 
            zIndex = comp.setZIndex(zIndex);
 
            // Only register a new topmost to activate if we find one that is visible 
            // Unfiltered panels with hidden:true can end up here during an animated hide process 
            // When the hidden flag is set, and the ghost show operation kicks the ZIndexManager's sort. 
            if (!comp.hidden) {
                topMost = comp;
 
                // Track topmost visible modal so we can place the modal mask just below it. 
                // Any prior focusable ones just became not focusable - they'll be below our mask. 
                if (comp.modal) {
                    topModal = comp;
                    topFocusable = null;
                }
 
                // Track topmost focusable floater which is above all modals. 
                // Unfocusable things like tooltips and toasts may be above it 
                // but they do not matter, the topmost *focusable* must be focused. 
                if (doFocus && (comp.isFocusable(true) &&
                        (comp.modal || comp.focusOnToFront))) {
                    topFocusable = comp;
                }
            }
        }
 
        // Sort resulted in a different topmost focusable. 
        if (topFocusable && topFocusable !== oldFront && !topFocusable.preventFocusOnActivate) {
            topFocusable.onFocusTopmost();
        }
 
        // If we encountered a modal in our reassigment, ensure our modal mask is just below it. 
        if (topModal) {
            // If it's the same topmost, then just ensure the 
            // correct z-index and size of mask. 
            if (topModal === me.topModal) {
                me.syncModalMask(topModal);
            }
            // If it's a new top, we must re-show the mask because of tabbability resets. 
            else {
                me.showModalMask(topModal);
            }
        } else {
            me.hideModalMask();
        }
 
        // Inform components of change in to of stack. 
        if (topMost !== me.topMost) {
            if (me.topMost) {
                // This one has been bumped from top. 
                me.topMost.onZIndexChange(false);
            }
            if (topMost) {
                // This one is now at the top. 
                topMost.onZIndexChange(true);
            }
        }
 
        // Cache the top of the stack 
        me.front = topFocusable;
        me.topModal = topModal;
        me.topMost = topMost;
    
        // Ensure the top-most component is the front 
        if (!me.front && me.topMost) {
            me.front = me.topMost;
        }
 
        return zIndex;
    },
 
    /**
     * @private
     * Called from {@link Ext.util.Floating} updater methods when a config which affects the stack order is
     * updated in a Component.
     *
     * eg {@link Ext.Component#alwaysOnTop alwaysOnTop} or {@link Ext.Component#activeCounter activeCounter}
     */
    onComponentUpdate: function(comp) {
        if (!this.reflowSuspended && this.zIndexStack.contains(comp)) {
            this.zIndexStack.sort();
        }
    },
    
    suspendReflow: function() {
        this.reflowSuspended++;
    },
 
    resumeReflow: function (flush) {
        if (this.reflowSuspended && ! --this.reflowSuspended) {
            if (flush) {
                this.zIndexStack.sort();
            }
        }
    },
 
    onAfterComponentRender: function(comp) {
        if (!this.reflowSuspended && comp.isVisible() && comp.toFrontOnShow) {
            this.zIndexStack.itemChanged(comp, 'hidden');
            this.zIndexStack.sort();
        }
    },
 
    /**
     * @private
     * Called when the global hide and show events are fired. If it is one of our components, we must re-sort.
     */
    onComponentShowHide: function(comp) {
        var me = this,
            zIndexStack = me.zIndexStack,
            sortCount = me.sortCount;
 
        // If component has hidden, it will be filtered out, so we have to look in Collection's source if it's there. 
        if (comp.isFloating() && !me.hidingAll && (zIndexStack.getSource() || zIndexStack).contains(comp)) {
            if (me.tempHidden) {
                Ext.Array.remove(me.tempHidden, comp);
            }
            zIndexStack.beginUpdate();
 
            // Showing. If it should go to front on show; nudge the active 
            // counter which will cause a stack sort. 
            if (comp.isVisible()) {
                if (comp.toFrontOnShow) {
                    zIndexStack.itemChanged(comp, 'hidden');
                    comp.setActiveCounter(++Ext.ZIndexManager.activeCounter);
                }
            }
            
            // Hiding 
            else {
                zIndexStack.itemChanged(comp, 'hidden');
            }
 
            // We must update the frontmost according to the new stack order 
            // even if there has been no sort (Collection will not autosort if only one member) 
            zIndexStack.endUpdate();
            if (me.sortCount === sortCount && !me.reflowSuspended) {
                me.onCollectionSort();
            }
        }
    },
 
    /**
     * Registers a floating {@link Ext.Component} with this ZIndexManager. This should not
     * need to be called under normal circumstances. Floating Components (such as Windows,
     * BoundLists and Menus) are automatically registered with a
     * {@link Ext.Component#zIndexManager zIndexManager} at render time.
     *
     * Where this may be useful is moving Windows between two ZIndexManagers. For example,
     * to bring the Ext.MessageBox dialog under the same manager as the Desktop's
     * ZIndexManager in the desktop sample app:
     *
     *     MyDesktop.getDesktop().getManager().register(Ext.MessageBox);
     *
     * @param {Ext.Component} comp The Component to register.
     */
    register: function(comp) {
        var me = this;
 
        if (comp.zIndexManager) {
            comp.zIndexManager.unregister(comp);
        }
        comp.zIndexManager = me;
        if (!comp.rendered) {
            // Checking for rendered as opposed to hide/show is important because 
            // it's still possible to render a floating component and have it be visible. 
            // Since rendered isn't a global event, we need to react individually on each 
            // component and update the state in the collection after render. 
            comp.on('afterrender', me.onAfterComponentRender, me, {single: true});
        }
        me.zIndexStack.add(comp);
    },
 
    /**
     * Unregisters a {@link Ext.Component} from this ZIndexManager. This should not
     * need to be called. Components are automatically unregistered upon destruction.
     * See {@link #register}.
     * @param {Ext.Component} comp The Component to unregister.
     */
    unregister: function(comp) {
        var me = this;
 
        delete comp.zIndexManager;
        comp.un('afterrender', me.onAfterComponentRender, me);
        me.zIndexStack.remove(comp);
        me.onCollectionSort();
    },
 
    /**
     * Gets a registered Component by id.
     * @param {String/Object} id The id of the Component or a {@link Ext.Component} instance
     * @return {Ext.Component}
     */
    get: function(id) {
        return id.isComponent ? id : this.zIndexStack.get(id);
    },
 
   /**
     * Brings the specified Component to the front of any other active Components in this ZIndexManager.
     * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance.
     * @param {Boolean} preventFocus Pass `true` to prevent the component being focused when moved to front.
     * @return {Boolean} True if the component was brought to the front, else false
     * if it was already in front, or another component remains at the front due to configuration (eg
     * {@link Ext.util.Floating#alwaysOnTop}, or if the component was not found.
     */
    bringToFront: function(comp, preventFocus) {
        var me = this,
            zIndexStack = me.zIndexStack,
            oldFront = zIndexStack.last(),
            newFront, preventFocusSetting;
            
        comp = me.get(comp);
 
        // Refuse to perform this operation if we do not own the passed component. 
        if (!comp) {
            return false;
        }
 
        preventFocusSetting = comp.preventFocusOnActivate;
 
        // The onCollectionSorted reaction to the setting of activeCounter will focus by default. 
        // Prevent it if requested. 
        comp.preventFocusOnActivate = preventFocus;
        comp.setActiveCounter(++Ext.ZIndexManager.activeCounter);
        comp.preventFocusOnActivate = preventFocusSetting;
        newFront = zIndexStack.last();
 
        // Return true if the passed component was moved to the front and was not already at the front 
        return (newFront === comp && newFront !== oldFront);
    },
 
    /**
     * Sends the specified Component to the back of other active Components in this ZIndexManager.
     * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance
     * @return {Ext.Component} The Component
     */
    sendToBack: function(comp) {
        comp = this.get(comp);
        if (comp) {
            comp.setActiveCounter(0);
        }
        return comp || null;
    },
 
    /**
     * Hides all Components managed by this ZIndexManager.
     */
    hideAll: function() {
        var me = this,
            all = me.zIndexStack.getRange(),
            len = all.length,
            i;
 
        me.hidingAll = true;
        for (= 0; i < len; i++) {
            all[i].hide();
        }
        me.hidingAll = false;
        me.hideModalMask();
        me.front = null;
    },
 
    /**
     * @private
     * Temporarily hides all currently visible managed Components. This is for when
     * dragging a Window which may manage a set of floating descendants in its ZIndexManager;
     * they should all be hidden just for the duration of the drag.
     */
    hide: function() {
        var me = this,
            activeElement = Ext.Element.getActiveElement(),
            all = me.zIndexStack.getRange(),
            len = all.length,
            i,
            comp;
 
        // If any of the components contained focus, we must restore it on show. 
        me.focusRestoreElement = null;
        (me.tempHidden || (me.tempHidden = [])).length = 0;
        for (= 0; i < len; i++) {
            comp = all[i];
            
            // Only hide currently visible floaters 
            if (comp.isVisible()) {
                if (comp.el.contains(activeElement)) {
                    me.focusRestoreElement = activeElement;
                }
                comp.el.hide();
                comp.pendingShow = comp.hidden = true;
                me.tempHidden.push(comp);
            }
        }
    },
 
    /**
     * @private
     * Restores temporarily hidden managed Components to visibility.
     */
    show: function() {
        var me = this,
            i,
            tempHidden = me.tempHidden,
            len = tempHidden ? tempHidden.length : 0,
            comp;
 
        for (= 0; i < len; i++) {
            comp = tempHidden[i];
            comp.hidden = false;
            if (comp.pendingShow) {
                comp.el.show();
                comp.pendingShow = false;
                comp.setPosition(comp.x, comp.y);
                comp.onFloatShow();
            }
            // An attempt at hiding while temp hidden 
            // has cleared the pendingShow flag. Hide it 
            // properly with full event flow now. 
            else {
                comp.hide();
            }
        }
        me.tempHidden = null;
        if (me.focusRestoreElement) {
            me.focusRestoreElement.focus();
        }
    },
 
    /**
     * Gets the currently-active Component in this ZIndexManager.
     * @return {Ext.Component} The active Component
     */
    getActive: function() {
        return this.zIndexStack.last();
    },
 
    /**
     * Returns zero or more Components in this ZIndexManager using the custom search function passed to this method.
     * The function should accept a single {@link Ext.Component} reference as its only argument and should
     * return true if the Component matches the search criteria, otherwise it should return false.
     * @param {Function} fn The search function
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the Component being tested. That gets passed to the function if not specified.
     * @return {Array} An array of zero or more matching floating components.
     */
    getBy: function(fn, scope) {
        return this.zIndexStack.filterBy(fn, scope).getRange();
    },
 
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * @param {Function} fn The function to execute for each item
     * @param {Object} [scope] The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    each: function(fn, scope) {
        this.zIndexStack.each(fn, scope);
    },
 
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * The components are passed to the function starting at the bottom and proceeding to the top.
     * @param {Function} fn The function to execute for each item
     * @param {Object} scope (optional) The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    eachBottomUp: function (fn, scope) {
        var stack = this.zIndexStack.getRange(),
            i,
            len = stack.length,
            comp;
 
        for (= 0; i < len; i++) {
            comp = stack[i];
            if (comp.isComponent && fn.call(scope || comp, comp) === false) {
                return;
            }
        }
    },
 
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * The components are passed to the function starting at the top and proceeding to the bottom.
     * @param {Function} fn The function to execute for each item
     * @param {Object} [scope] The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    eachTopDown: function (fn, scope) {
        var stack = this.zIndexStack.getRange(),
            i,
            comp;
 
        for (= stack.length; i-- > 0; ) {
            comp = stack[i];
            if (comp.isComponent && fn.call(scope || comp, comp) === false) {
                return;
            }
        }
    },
 
    destroy: function() {
        var me   = this,
            stack = me.zIndexStack.getRange(),
            len = stack.length,
            i;
 
        for (= 0; i < len; i++) {
            Ext.destroy(stack[i]);
        }
        
        Ext.destroy(me.mask, me.maskShim, me.zIndexStack, me.globalListeners, me.resizeListeners);
        
        me.callParent();
    },
 
    privates: {
        getMaskBox: function() {
            var maskTarget = this.mask.maskTarget;
 
            if (maskTarget.dom === document.body) {
                // If we're masking the body, subtract the border/padding so we don't cause scrollbar. 
                return {
                    height: Math.max(document.body.scrollHeight, Ext.dom.Element.getDocumentHeight()),
                    width: Math.max(document.body.scrollWidth, Ext.dom.Element.getDocumentWidth()),
                    x: 0,
                    y: 0
                };
            } else {
                return maskTarget.getBox();
            } 
        },
 
        scheduleContainerResize: function() {
            // The reason we're scheduling resize handler here is to allow Responsive mixin 
            // to fire events and run layouts that may affect the size of the modal mask. 
            // Responsive will request animation frame on browser window resize event, 
            // we do likewise here to minimize flicker. 
            if (!this.containerResizeTimer) {
                this.containerResizeTimer = Ext.raf(this.onContainerResize, this);
            }
        },
 
        onContainerResize: function() {
            var me = this,
                mask = me.mask,
                maskShim = me.maskShim,
                viewSize;
            
            me.containerResizeTimer = null;
 
            if (mask && mask.isVisible()) {
 
                // At the new container size, the mask might be *causing* the scrollbar, so to find the valid 
                // client size to mask, we must temporarily unmask the parent node. 
                mask.hide();
                if (maskShim) {
                    maskShim.hide();
                }
 
                viewSize = me.getMaskBox();
                if (maskShim) {
                    maskShim.setSize(viewSize);
                    maskShim.show();
                }
                mask.setSize(viewSize);
                mask.show();
            }
        },
 
        onMaskMousedown: function(e) {
            // Focus frontmost modal, do not allow mousedown to focus mask. 
            if (this.topModal) {
                this.topModal.focus();
                e.preventDefault();
            }
        },
 
        onMaskClick: function() {
            var front = this.topModal,
                methodName;
 
            if (front) {
                // Fire a maskclick event. Allow the onward processing by the maskClickAction method 
                // to be vetoed by a false return value. 
                if (!front.hasListeners.maskclick || front.fireEvent('maskclick', front) !== false) {
                    // We call whatever method 'maskClickAction' points to. 
                    // By default, Windows have 'focus'. If we encounter other 
                    // classes without that property, default to 'focus' 
                    methodName = front.maskClickAction || 'focus';
                    front[methodName]();
                }
            }
        },
 
        showModalMask: function(comp) {
            var me = this,
                compEl = comp.el,
                maskTarget = comp.floatParent ? comp.floatParent.getEl() : comp.container,
                mask = me.mask;
 
            if (!mask) {
                // Create the mask at zero size so that it does not affect upcoming target measurements. 
                me.mask = mask = Ext.getBody().createChild({
                    //<debug> 
                    // tell the spec runner to ignore this element when checking if the dom is clean  
                    'data-sticky': true,
                    //</debug> 
                    role: 'presentation',
                    cls: Ext.baseCSSPrefix + 'mask ' + Ext.baseCSSPrefix + 'border-box',
                    style: 'height:0;width:0'
                });
                mask.setVisibilityMode(Ext.Element.DISPLAY);
                mask.on({
                    mousedown: me.onMaskMousedown,
                    click: me.onMaskClick,
                    scope: me
                });
            }
            
            // If the mask is already shown, hide it before showing again 
            // to ensure underlying elements' tabbability is restored 
            else {
                me.hideModalMask();
            }
 
            mask.maskTarget = maskTarget;
            
            // Since there is no fast and reliable way to find elements above or below 
            // a given z-index, we just cheat and prevent tabbable elements within the 
            // topmost component from being made untabbable. 
            maskTarget.saveTabbableState({
                excludeRoot: compEl
            });
 
            // Size and zIndex stack the mask (and its shim) 
            me.syncModalMask(comp);
        },
 
        syncModalMask: function(comp) {
            var me = this,
                zIndex = comp.el.getZIndex() - 4,
                mask = me.mask,
                shim = me.maskShim,
                viewSize = me.getMaskBox();
 
            if (shim) {
                shim.setZIndex(zIndex);
                shim.show();
                shim.setBox(viewSize);
            }
            mask.setZIndex(zIndex);
            mask.show();
            mask.setBox(viewSize);
        },
        
        hideModalMask: function() {
            var mask = this.mask,
                maskShim = this.maskShim;
 
            if (mask && mask.isVisible()) {
                mask.maskTarget.restoreTabbableState();
                
                mask.maskTarget = undefined;
                mask.hide();
                
                if (maskShim) {
                    maskShim.hide();
                }
            }
        }
    }
}, function() {
    /**
     * @class Ext.WindowManager
     * @extends Ext.ZIndexManager
     *
     * The default global floating Component group that is available automatically.
     *
     * This manages instances of floating Components which were rendered programatically without
     * being added to a {@link Ext.container.Container Container}, and for floating Components
     * which were added into non-floating Containers.
     * 
     * *Floating* Containers create their own instance of ZIndexManager, and floating Components
     * added at any depth below there are managed by that ZIndexManager.
     *
     * @singleton
     */
    Ext.WindowManager = Ext.WindowMgr = new this();
});