/**
 * A modal, floating Component which may be shown above a specified {@link Ext.Component Component}
 * while loading data. When shown, the configured owning Component will be covered with a modality
 * mask, and the LoadMask's {@link #msg} will be displayed centered, accompanied by a spinner image.
 *
 * If the {@link #store} config option is specified, the masking will be automatically shown
 * and then hidden synchronized with the Store's loading process.
 *
 * Because this is a floating Component, its z-index will be managed by the global
 * {@link Ext.WindowManager ZIndexManager} object, and upon show, it will place itsef at the top
 * of the hierarchy.
 *
 * Example usage:
 *
 *     @example
 *     var myPanel = new Ext.panel.Panel({
 *         renderTo : document.body,
 *         height   : 100,
 *         width    : 200,
 *         title    : 'Foo'
 *     });
 *
 *     var myMask = new Ext.LoadMask({
 *         msg    : 'Please wait...',
 *         target : myPanel
 *     });
 *
 *     myMask.show();
 */
Ext.define('Ext.LoadMask', {
    extend: 'Ext.Component',
    alias: 'widget.loadmask',
 
    mixins: [
        'Ext.util.StoreHolder'
    ],
 
    uses: ['Ext.data.StoreManager'],
 
    /**
     * @property {Boolean} isLoadMask
     * `true` in this class to identify an object as an instantiated LoadMask, or subclass thereof.
     */
    isLoadMask: true,
 
    /**
     * @cfg {Ext.Component} target The Component you wish to mask. The the mask will be
     * automatically sized upon Component resize, and the message box will be kept centered.
     */
 
    /**
     * @cfg {Ext.data.Store} store
     * Optional Store to which the mask is bound. The mask is displayed when a load request
     * is issued, and hidden on either load success, or load fail.
     */
 
    /**
     * @cfg {String} [msg="Loading..."]
     * The text to display in a centered loading message box.
     * @locale
     */
    msg: 'Loading...',
 
    msgCls: Ext.baseCSSPrefix + 'mask-loading',
 
    msgWrapCls: Ext.baseCSSPrefix + 'mask-msg',
 
    /**
     * @cfg {Boolean} [useMsg=true]
     * Whether or not to use a loading message class or simply mask the bound element.
     */
    useMsg: true,
 
    /**
     * @cfg {Boolean} [useTargetEl=false]
     * True to mask the {@link Ext.Component#getTargetEl targetEl} of the bound Component.
     * By default, the {@link Ext.Component#getEl el} will be masked.
     */
    useTargetEl: false,
 
    /**
     * @cfg {Boolean} shim `true` to enable an iframe shim for this LoadMask to keep
     * windowed objects from showing through.
     */
 
    /**
     * @private
     */
    cls: Ext.baseCSSPrefix + 'mask',
    componentCls: Ext.baseCSSPrefix + 'border-box',
 
    ariaRole: "progressbar",
    focusable: true,
    tabIndex: 0,
 
    childEls: [
        'msgWrapEl',
        'msgEl',
        'msgTextEl'
    ],
 
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<div id="{id}-msgWrapEl" data-ref="msgWrapEl" class="{[values.$comp.msgWrapCls]}" role="presentation">',
            '<div id="{id}-msgEl" data-ref="msgEl" class="{[values.$comp.msgCls]} ',
                Ext.baseCSSPrefix, 'mask-msg-inner {childElCls}" role="presentation">',
                '<div id="{id}-msgTextEl" data-ref="msgTextEl" class="',
                    Ext.baseCSSPrefix, 'mask-msg-text',
                    '{childElCls}" role="presentation">{msg}</div>',
            '</div>',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
 
    maskOnDisable: false,
 
    /**
     * @private
     */
    skipLayout: true,
 
    /**
     * Creates new LoadMask.
     * @param {Object} [config] The config object.
     */
    constructor: function(config) {
        var me = this,
            comp;
 
        if (arguments.length === 2) {
            //<debug>
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn(
                    'Ext.LoadMask: LoadMask now uses a standard 1 arg constructor: ' +
                    'use the target config'
                );
            }
            //</debug>
 
            comp = me.target = config;
            config = arguments[1];
        }
        else {
            comp = config.target;
        }
 
        //<debug>
        if (config.maskCls) {
            Ext.log.warn('Ext.LoadMask property maskCls is deprecated, use msgWrapCls instead');
            config.msgWrapCls = config.msgWrapCls || config.maskCls;
        }
        //</debug>
 
        // Must apply configs early so that renderTo can be calculated correctly.
        me.callParent([config]);
 
        // Target is a Component
        if (comp.isComponent) {
            me.ownerCt = comp;
            me.hidden = true;
 
            // Ask the component which element should be masked.
            // Most will not have an answer, in which case this returns the document body
            // Ext.view.Table for example returns the el of its owning Panel.
            me.renderTo = me.getMaskTarget();
            me.external = me.renderTo === Ext.getBody();
            me.bindComponent(comp);
        }
        // Element support to be deprecated
        else {
            //<debug>
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn(
                    'Ext.LoadMask: LoadMask for elements has been deprecated, ' +
                    'use Ext.dom.Element.mask & Ext.dom.Element.unmask');
            }
            //</debug>
 
            comp = Ext.get(comp);
            me.isElement = true;
            me.renderTo = me.target;
        }
 
        me.render(me.renderTo);
 
        if (me.store) {
            me.bindStore(me.store, true);
        }
    },
 
    initRenderData: function() {
        var result = this.callParent(arguments);
 
        result.msg = this.msg || '';
 
        return result;
    },
 
    onRender: function() {
        this.callParent(arguments);
 
        // In versions prior to 5.1, maskEl was rendered outside of the
        // LoadMask's main el and had a reference to it; we keep this
        // reference for backwards compatibility.
        this.maskEl = this.el;
    },
 
    bindComponent: function(comp) {
        var me = this,
            listeners = {
                scope: this,
                resize: me.sizeMask
            };
 
        if (me.external) {
            listeners.added = me.onComponentAdded;
            listeners.removed = me.onComponentRemoved;
 
            if (comp.floating) {
                listeners.move = me.sizeMask;
                me.activeOwner = comp;
            }
            else if (comp.ownerCt) {
                me.onComponentAdded(comp.ownerCt);
            }
        }
 
        me.mon(comp, listeners);
 
        // Subscribe to the observer that manages the hierarchy
        // Only needed if we had to be rendered outside of the target
        if (me.external) {
            me.mon(Ext.GlobalEvents, {
                show: me.onContainerShow,
                hide: me.onContainerHide,
                expand: me.onContainerExpand,
                collapse: me.onContainerCollapse,
                scope: me
            });
        }
    },
 
    onComponentAdded: function(owner) {
        var me = this;
 
        delete me.activeOwner;
        me.floatParent = owner;
 
        if (!owner.floating) {
            owner = owner.up('[floating]');
        }
 
        if (owner) {
            me.activeOwner = owner;
            me.mon(owner, 'move', me.sizeMask, me);
            me.mon(owner, 'tofront', me.onOwnerToFront, me);
        }
        else {
            me.preventBringToFront = true;
        }
 
        owner = me.floatParent.ownerCt;
 
        if (me.rendered && me.isVisible() && owner) {
            me.floatOwner = owner;
            me.mon(owner, 'afterlayout', me.sizeMask, me, { single: true });
        }
    },
 
    onComponentRemoved: function(owner) {
        var me = this,
            activeOwner = me.activeOwner,
            floatOwner = me.floatOwner;
 
        if (activeOwner) {
            me.mun(activeOwner, 'move', me.sizeMask, me);
            me.mun(activeOwner, 'tofront', me.onOwnerToFront, me);
        }
 
        if (floatOwner) {
            me.mun(floatOwner, 'afterlayout', me.sizeMask, me);
        }
 
        delete me.activeOwner;
        delete me.floatOwner;
    },
 
    afterRender: function() {
        var me = this;
 
        me.callParent(arguments);
 
        // In IE8-11, clicking on an inner msgEl will focus it, despite
        // it having no tabindex attribute and thus being canonically
        // non-focusable. Placing unselectable="on" attribute will make
        // it unfocusable but will also prevent clicks from focusing
        // the parent element. We want clicks within the mask's main el
        // to focus it, hence the workaround.
        if (Ext.isIE) {
            me.el.on('mousedown', me.onMouseDown, me);
        }
 
        // This LoadMask shares the DOM and may be tipped out by the use of innerHTML
        // Ensure the element does not get garbage collected from under us.
        this.el.skipGarbageCollection = true;
    },
 
    onMouseDown: function(e) {
        var el = this.el;
 
        if (e.within(el)) {
            e.preventDefault();
            el.focus();
        }
    },
 
    onOwnerToFront: function(owner, zIndex) {
        this.el.setStyle('zIndex', zIndex + 1);
    },
 
    // Only called if we are rendered external to the target.
    // Best we can do is show.
    onContainerShow: function(container) {
        if (!this.isHierarchicallyHidden()) {
            this.onComponentShow();
        }
    },
 
    // Only called if we are rendered external to the target.
    // Best we can do is hide.
    onContainerHide: function(container) {
        if (this.isHierarchicallyHidden()) {
            this.onComponentHide();
        }
    },
 
    // Only called if we are rendered external to the target.
    // Best we can do is show.
    onContainerExpand: function(container) {
        if (!this.isHierarchicallyHidden()) {
            this.onComponentShow();
        }
    },
 
    // Only called if we are rendered external to the target.
    // Best we can do is hide.
    onContainerCollapse: function(container) {
        if (this.isHierarchicallyHidden()) {
            this.onComponentHide();
        }
    },
 
    onComponentHide: function() {
        var me = this;
 
        if (me.rendered && me.isVisible()) {
            me.hide();
            me.showNext = true;
        }
    },
 
    onComponentShow: function() {
        if (this.showNext) {
            this.show();
        }
 
        delete this.showNext;
    },
 
    /**
     * @private
     * Called when this LoadMask's Component is resized. The toFront method rebases and resizes
     * the modal mask.
     */
    sizeMask: function() {
        var me = this,
            // Need to use the closest floating component (if it exists) as the basis
            // for our z-index positioning
            target = me.activeOwner || me.target,
            boxTarget = me.external ? me.getOwner().el : me.getMaskTarget(),
            zIndex;
 
        if (me.rendered && me.isVisible()) {
            // Only need to move and size the message wrap if we are outside of
            // the masked element.
            // If we are inside, it will be left:0;top:0;width:100%;height:100% by default
            if (me.external) {
                if (!me.isElement && target.floating) {
                    zIndex = target.el.getZIndex();
 
                    if (!isNaN(zIndex)) {
                        me.onOwnerToFront(target, zIndex);
                    }
                }
 
                me.el.setSize(boxTarget.getSize()).alignTo(boxTarget, 'tl-tl');
            }
 
            // Always need to center the message wrap
            me.msgWrapEl.center(me.el);
        }
    },
 
    /**
     * Changes the data store bound to this LoadMask.
     * @param {Ext.data.Store} store The store to bind to this LoadMask
     * @param [initial]
     */
    bindStore: function(store, initial) {
        var me = this;
 
        // If the server returns a failure, and the proxy fires an exception instead of
        // loading the store, the mask must clear.
        Ext.destroy(me.proxyListeners);
 
        me.mixins.storeholder.bindStore.apply(me, arguments);
        store = me.store;
 
        if (store) {
            // Skip ChainedStores to the store that does the loading
            while (store.getSource) {
                store = store.getSource();
            }
 
            if (!store.loadsSynchronously()) {
                me.proxyListeners = store.getProxy().on({
                    exception: me.onLoad,
                    scope: me,
                    destroyable: true
                });
            }
 
            if (store.isLoading()) {
                me.onBeforeLoad();
            }
        }
    },
 
    getStoreListeners: function(store) {
        var onLoad = this.onLoad,
            beforeLoad = this.onBeforeLoad,
            result = {
                // Fired when a range is requested for rendering that is not in the cache
                cachemiss: beforeLoad,
 
                // Fired when a range for rendering which was previously missing from the cache
                // is loaded. buffer so that scrolling and store filling has settled,
                // and the results have been rendered.
                cachefilled: {
                    fn: onLoad,
                    buffer: 100
                }
            };
 
        // Only need to mask on load if the proxy is asynchronous - ie: Ajax/JsonP
        if (!store.loadsSynchronously()) {
            result.beforeload = beforeLoad;
            result.load = onLoad;
        }
 
        return result;
    },
 
    onDisable: function() {
        this.callParent(arguments);
 
        if (this.loading) {
            this.onLoad();
        }
    },
 
    getOwner: function() {
        return this.ownerCt || this.ownerCmp || this.floatParent;
    },
 
    getMaskTarget: function() {
        var owner = this.getOwner();
 
        if (this.isElement) {
            return this.target;
        }
 
        return this.useTargetEl ? owner.getTargetEl() : (owner.getMaskTarget() || Ext.getBody());
    },
 
    /**
     * @private
     */
    onBeforeLoad: function() {
        var me = this,
            owner = me.getOwner(),
            origin;
 
        if (!me.disabled) {
            me.loading = true;
 
            // If the owning Component has not been laid out, defer so that the ZIndexManager
            // gets to read its laid out size when sizing the modal mask
            if (owner.componentLayoutCounter) {
                me.maybeShow();
            }
            else {
                // The code below is a 'run-once' interceptor.
                origin = owner.afterComponentLayout;
 
                owner.afterComponentLayout = function() {
                    owner.afterComponentLayout = origin;
                    origin.apply(owner, arguments);
                    me.maybeShow();
                };
            }
        }
    },
 
    maybeShow: function() {
        var me = this,
            owner = me.getOwner(),
            ownerVisible;
 
        // Owner could be detached
        ownerVisible = owner.isVisible(true) && (!me.isComponent || owner.el.isVisible(true));
 
        if (!ownerVisible) {
            me.showNext = true;
        }
        else if (me.loading && owner.rendered) {
            me.show();
        }
    },
 
    hide: function() {
        var me = this,
            ownerCt = me.ownerCt;
 
        me.target.removeCls(Ext.baseCSSPrefix + "masked");
 
        // Element support to be deprecated
        if (me.isElement) {
            ownerCt.unmask();
            me.fireEvent('hide', this);
 
            return;
        }
 
        // Could be already nulled while destroying
        if (ownerCt) {
            ownerCt.updateMaskState(false, me);
        }
 
        delete me.showNext;
 
        return me.callParent(arguments);
    },
 
    show: function() {
        var me = this;
 
        me.target.addCls(Ext.baseCSSPrefix + "masked");
 
        // Element support to be deprecated
        if (me.isElement) {
            me.ownerCt.mask(this.useMsg ? this.msg : '', this.msgCls);
            me.fireEvent('show', this);
 
            return;
        }
 
        return me.callParent(arguments);
    },
 
    afterShow: function() {
        var me = this,
            ownerCt = me.ownerCt;
 
        me.loading = true;
        me.callParent(arguments);
 
        ownerCt.updateMaskState(true, me);
 
        // Owner's disabled tabbing will also make the mask
        // untabbable since it is rendered within the target
        me.el.restoreTabbableState();
 
        me.syncMaskState();
    },
 
    /**
     * Synchronizes the visible state of the mask with the configuration settings such
     * as {@link #msgWrapCls}{@link #msg}, sizes the mask to occlude the target element
     * or Component and focuses the mask.
     * @private
     */
    syncMaskState: function() {
        var me = this,
            ownerCt = me.ownerCt,
            el = me.el,
            ariaMsg,
            ariaMsgEl;
 
        if (me.isVisible()) {
            // Allow dynamic setting of msgWrapCls
            if (me.hasOwnProperty('msgWrapCls')) {
                el.dom.className = me.msgWrapCls;
            }
 
            if (me.useMsg) {
                me.msgTextEl.setHtml(me.msg);
 
                ariaMsg = me.msg;
                ariaMsgEl = me.ariaEl;
 
                if (Ext.isIE) {
                    ariaMsgEl = me.msgTextEl;
                    ariaMsgEl.dom.setAttribute("aria-live", 'polite');
                }
 
                ariaMsgEl.dom.removeAttribute("aria-valuetext");
                ariaMsgEl.dom.setAttribute("aria-valuetext", ariaMsg);
                ariaMsgEl.dom.setAttribute("aria-labelledBy", me.msgTextEl.id);
            }
            else {
                // Only the mask is visible if useMsg is false
                me.msgWrapEl.hide();
            }
 
            if (me.shim || Ext.useShims) {
                el.enableShim(null, true);
            }
            else {
                // Just in case me.shim was changed since last time we were shown (by
                // Component#setLoading())
                el.disableShim();
            }
 
            // If owner contains focus, focus this.
            // Component level onHide processing takes care of focus reversion on hide.
            // Also, focus if owner is configured with loadingText, so that
            // screen-reader will announce changes accordingly
            if (
                ownerCt.el.contains(Ext.Element.getActiveElement()) ||
                ownerCt.focusMaskWhileLoading
            ) {
                me.focus();
            }
 
            me.sizeMask();
        }
    },
 
    /**
     * @private
     */
    onLoad: function() {
        this.loading = false;
        this.hide();
    },
 
    doDestroy: function() {
        var me = this;
 
        // We don't have a real ownerCt, so clear it out here to prevent
        // spurious warnings when we are destroyed
        me.ownerCt = null;
        me.bindStore(null);
 
        if (me.isElement) {
            me.ownerCt.unmask();
        }
 
        me.callParent();
    }
});