/**
 * The FocusManager is responsible for globally:
 *
 * 1. Managing component focus
 * 2. Providing basic keyboard navigation
 * 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
 *
 * To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
 * deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();`.  The
 * FocusManager is disabled by default.
 *
 * To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #method-enable}.
 *
 * Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
 * that would like to have navigation between its child {@link Ext.Component}'s.
 *
 * @author Jarred Nicholls <[email protected]>
 * @docauthor Jarred Nicholls <[email protected]>
 */
Ext.define('Ext.FocusManager', {
    singleton: true,
    alternateClassName: ['Ext.FocusMgr' ],
 
    mixins: {
        observable: 'Ext.util.Observable'
    },
 
    requires: [
        'Ext.Component',
        'Ext.ComponentManager',
        'Ext.ComponentQuery',
        'Ext.util.HashMap',
        'Ext.util.KeyNav'
    ],
 
    /**
     * @property {Boolean} enabled 
     * Whether or not the FocusManager is currently enabled
     */
    enabled: false,
 
    /**
     * @property {Ext.Component} focusedCmp
     * The currently focused component.
     */
 
    focusElementCls: Ext.baseCSSPrefix + 'focus-element',
 
    focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
 
    /**
     * @property {String[]} whitelist
     * A list of xtypes that should ignore certain navigation input keys and
     * allow for the default browser event/behavior. These input keys include:
     *
     * 1. Backspace
     * 2. Delete
     * 3. Left
     * 4. Right
     * 5. Up
     * 6. Down
     *
     * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
     * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
     * the user to move the input cursor left and right, and to delete characters, etc.
     */
    whitelist: [
        'textfield'
    ],
 
    /**
     * @event beforecomponentfocus
     * Fires before a component becomes focused. Return `false` to prevent
     * the component from gaining focus.
     * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
     * @param {Ext.Component} cmp The component that is being focused
     * @param {Ext.Component} previousCmp The component that was previously focused,
     * or `undefined` if there was no previously focused component.
     */
 
    /**
     * @event componentfocus
     * Fires after a component becomes focused.
     * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
     * @param {Ext.Component} cmp The component that has been focused
     * @param {Ext.Component} previousCmp The component that was previously focused,
     * or `undefined` if there was no previously focused component.
     */
 
    /**
     * @event disable
     * Fires when the FocusManager is disabled
     * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
     */
 
    /**
     * @event enable
     * Fires when the FocusManager is enabled
     * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
     */
 
    constructor: function(config) {
        var me = this,
            CQ = Ext.ComponentQuery;
 
        me.mixins.observable.constructor.call(me, config);
 
        me.focusTask = new Ext.util.DelayedTask(me.handleComponentFocus, me);
 
        // Gain control on Component focus, blur, hide and destroy 
        Ext.override(Ext.Component, {
            onBlur: function () {
                this.callParent(arguments);
                if (me.enabled && !this.hasFocus) {
                    Array.prototype.unshift.call(arguments, this);
                    me.onComponentBlur.apply(me, arguments);
                }
            },
            onDestroy: function() {
                this.callParent(arguments);
                if (me.enabled) {
                    Array.prototype.unshift.call(arguments, this);
                    me.onComponentDestroy.apply(me, arguments);
                }
            },
            onFocus: function () {
                this.callParent(arguments);
                if (me.enabled && this.hasFocus) {
                    Array.prototype.unshift.call(arguments, this);
                    me.onComponentFocus.apply(me, arguments);
                }
            }
        });
        Ext.override(Ext.Component, {
            afterHide: function() {
                this.callParent(arguments);
                if (me.enabled) {
                    Array.prototype.unshift.call(arguments, this);
                    me.onComponentHide.apply(me, arguments);
                }
            }
        });
 
        me.focusData = {};
        me.subscribers = new Ext.util.HashMap();
        me.focusChain = {};
 
        // Setup some ComponentQuery pseudos 
        Ext.apply(CQ.pseudos, {
            // Return the single next focusable sibling from the current idx in either direction (step -1 or 1) 
            nextFocus: function(cmps, idx, step) {
                step = step || 1;
                idx = parseInt(idx, 10);
 
                var len = cmps.length,
                    i = idx, c;
 
                for (;;) {
                    // Increment index, and loop round if off either end 
                    if ((+= step) >= len) {
                        i = 0;
                    } else if (< 0) {
                        i = len - 1;
                    }
 
                    // As soon as we loop back to the starting index, give up, there are no focusable siblings. 
                    if (=== idx) {
                        return [];
                    }
 
                    // If we have found a focusable sibling, return it 
                    if ((= cmps[i]).isFocusable()) {
                        return [c];
                    }
                }
 
                return [];
            },
 
            prevFocus: function(cmps, idx) {
                return this.nextFocus(cmps, idx, -1);
            },
 
            root: function(cmps) {
                var len = cmps.length,
                    results = [],
                    i = 0,
                    c;
 
                for (; i < len; i++) {
                    c = cmps[i];
                    if (!c.ownerCt) {
                        results.push(c);
                    }
                }
 
                return results;
            }
        });
    },
 
    getKeyNav: function() {
        var me = this;
        me.keyNav = me.keyNav || new Ext.util.KeyNav(Ext.getDoc(), {
            disabled: true,
            scope: me,
 
            backspace: me.focusLast,
            enter: me.navigateIn,
            esc: me.navigateOut,
            tab: me.navigateSiblings,
            space: me.navigateIn,
            del: me.focusLast,
            left: me.navigateSiblings,
            right: me.navigateSiblings,
            down: me.navigateSiblings,
            up: me.navigateSiblings
        });
        return me.keyNav;
    },
 
    /**
     * Adds the specified xtype to the {@link #whitelist}.
     * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
     */
    addXTypeToWhitelist: function(xtype) {
        var me = this;
 
        if (Ext.isArray(xtype)) {
            Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
            return;
        }
 
        if (!Ext.Array.contains(me.whitelist, xtype)) {
            me.whitelist.push(xtype);
        }
    },
 
    clearComponent: function(cmp) {
        clearTimeout(this.cmpFocusDelay);
        if (!cmp.isDestroyed) {
            cmp.blur();
        }
    },
 
    /**
     * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
     */
    disable: function() {
        var me = this;
 
        if (!me.enabled) {
            return;
        }
 
        delete me.options;
        me.enabled = false;
 
        me.removeDOM();
 
        // Stop handling key navigation 
        me.getKeyNav().disable();
 
        me.fireEvent('disable', me);
    },
 
    /**
     * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
     * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object
     * with the following options:
     * @param {Boolean} [options.focusFrame=false] `true` to show the focus frame around a component when it is focused.
     */
    enable: function(options) {
        var me = this;
 
        if (options === true) {
            options = { focusFrame: true };
        }
        me.options = options = options || {};
 
        if (me.enabled) {
            return;
        }
 
        // When calling addFocusListener on Containers, the FocusManager must be enabled, otherwise it won't do it. 
        me.enabled = Ext.enableFocusManager = true;
        me.initDOM(options);
 
        // Start handling key navigation 
        me.getKeyNav().enable();
 
        // Finally, let's focus our global focus el so we start fresh 
        me.focusEl.focus();
        delete me.focusedCmp;
 
        me.fireEvent('enable', me);
    },
 
    focusLast: function(e) {
        var me = this;
 
        if (me.isWhitelisted(me.focusedCmp)) {
            return true;
        }
 
        // Go back to last focused item 
        if (me.previousFocusedCmp) {
            me.previousFocusedCmp.focus();
        }
    },
 
    getRootComponents: function() {
        var CQ = Ext.ComponentQuery,
            inline = CQ.query(':focusable:root:not([floating])'),
            floating = CQ.query(':focusable:root[floating]');
 
        // Floating items should go to the top of our root stack, and be ordered 
        // by their z-index (highest first) 
        floating.sort(function(a, b) {
            return a.el.getZIndex() > b.el.getZIndex();
        });
 
        return floating.concat(inline);
    },
 
    initDOM: function(options) {
        var me = this,
            cls = me.focusFrameCls,
            needListeners = Ext.ComponentQuery.query('{getFocusEl()}:not([focusListenerAdded])'),
            i = 0, len = needListeners.length;
 
        if (!Ext.isReady) {
            return Ext.onReady(me.initDOM, me);
        }
 
        // When we are enabled, we must ensure that all Components which return a focusEl that is *not naturally focusable* 
        // have focus/blur listeners enabled to then trigger onFocus/onBlur handling so that we get to know about their focus action. 
        // These listeners are not added at initialization unless the FocusManager is enabled at that time. 
        for (; i < len; i++) {
            needListeners[i].addFocusListener();
        }
 
        // Make the document body the global focus element 
        if (!me.focusEl) {
            me.focusEl = Ext.getBody();
            me.focusEl.dom.tabIndex = -1;
        }
 
        // Create global focus frame 
        if (!me.focusFrame && options.focusFrame) {
            me.focusFrame = Ext.getBody().createChild({
                cls: cls,
                children: [
                    { cls: cls + '-top' },
                    { cls: cls + '-bottom' },
                    { cls: cls + '-left' },
                    { cls: cls + '-right' }
                ],
                style: 'top: -100px; left: -100px;'
            });
            me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
            me.focusFrame.hide().setLocalXY(0, 0);
        }
    },
 
    isWhitelisted: function(cmp) {
        return cmp && Ext.Array.some(this.whitelist, function(x) {
            return cmp.isXType(x);
        });
    },
 
    navigateIn: function(e) {
        var me = this,
            focusedCmp = me.focusedCmp,
            defaultRoot,
            firstChild;
 
        if (me.isWhitelisted(focusedCmp)) {
            return true;
        }
 
        if (!focusedCmp) {
            // No focus yet, so focus the first root cmp on the page 
            defaultRoot = me.getRootComponents()[0];
            if (defaultRoot) {
                // If the default root is based upon the body, then it will already be focused, and will not fire a focus event to 
                // trigger its own onFocus processing, so we have to programatically blur it first. 
                if (defaultRoot.getFocusEl() === me.focusEl) {
                    me.focusEl.blur();
                }
                defaultRoot.focus();
            }
        } else {
            // Drill into child ref items of the focused cmp, if applicable. 
            // This works for any Component with a getRefItems implementation. 
            firstChild = focusedCmp.hasFocus ? Ext.ComponentQuery.query('>:focusable', focusedCmp)[0] : focusedCmp;
            if (firstChild) {
                firstChild.focus();
            } else {
                // Let's try to fire a click event, as if it came from the mouse 
                if (Ext.isFunction(focusedCmp.onClick)) {
                    e.button = 0;
                    focusedCmp.onClick(e);
                    if (focusedCmp.isVisible(true)) {
                        focusedCmp.focus();
                    } else {
                        me.navigateOut();
                    }
                }
            }
        }
    },
 
    navigateOut: function(e) {
        var me = this,
            parent;
 
        if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
            me.focusEl.focus();
        } else {
            parent.focus();
        }
 
        // In some browsers (Chrome) FocusManager can handle this before other 
        // handlers. Ext Windows have their own Esc key handling, so we need to 
        // return true here to allow the event to bubble. 
        return true;
    },
 
    navigateSiblings: function(e, source, parent) {
        var me = this,
            src = source || me,
            key = e.getKey(),
            goBack = e.shiftKey || key == e.LEFT || key == e.UP,
            checkWhitelist = key === e.LEFT || key === e.RIGHT || key === e.UP || key === e.DOWN,
            nextSelector = goBack ? 'prev' : 'next',
            idx, next, focusedCmp, siblings;
 
        focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
        if (!focusedCmp && !parent) {
            return true;
        }
 
        if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
            return true;
        }
 
        // If no focused Component, or a root level one was focused, then siblings are root components. 
        if (!focusedCmp || focusedCmp.is(':root')) {
            siblings = me.getRootComponents();
        } else {
            // Else if the focused component has a parent, get siblings from there 
            parent = parent || focusedCmp.up();
            if (parent) {
                siblings = parent.getRefItems();
            }
        }
 
 
        // Navigate if we have found siblings. 
        if (siblings) {
            idx = focusedCmp ? Ext.Array.indexOf(siblings, focusedCmp) : -1;
            next = Ext.ComponentQuery.query(':' + nextSelector + 'Focus(' + idx + ')', siblings)[0];
            if (next && focusedCmp !== next) {
                next.focus();
                return next;
            }
        }
    },
 
    onComponentBlur: function(cmp, e) {
        var me = this;
 
        if (me.focusedCmp === cmp) {
            me.previousFocusedCmp = cmp;
            delete me.focusedCmp;
        }
 
        if (me.focusFrame) {
            me.focusFrame.hide();
        }
    },
 
    onComponentFocus: function(cmp, e) {
        var me = this,
            chain = me.focusChain,
            parent;
 
        if (!cmp.isFocusable()) {
            me.clearComponent(cmp);
 
            // Check our focus chain, so we don't run into a never ending recursion 
            // If we've attempted (unsuccessfully) to focus this component before, 
            // then we're caught in a loop of child->parent->...->child and we 
            // need to cut the loop off rather than feed into it. 
            if (chain[cmp.id]) {
                return;
            }
 
            // Try to focus the parent instead 
            parent = cmp.up();
            if (parent) {
                // Add component to our focus chain to detect infinite focus loop 
                // before we fire off an attempt to focus our parent. 
                // See the comments above. 
                chain[cmp.id] = true;
                parent.focus();
            }
 
            return;
        }
        // Clear our focus chain when we have a focusable component 
        me.focusChain = {};
 
        // Capture the focusEl to frame now. 
        // Button returns its encapsulating element during the focus phase 
        // So that element gets styled and framed. 
        me.focusTask.delay(10, null, null, [cmp, cmp.getFocusEl()]);
    },
 
    handleComponentFocus: function(cmp, focusEl) {
        var me = this,
            cls,
            ff,
            box,
            bt,
            bl,
            bw,
            bh,
            ft,
            fb,
            fl,
            fr;
 
        if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
            me.clearComponent(cmp);
            return;
        }
 
        me.focusedCmp = cmp;
 
        // If we have a focus frame, show it around the focused component 
        if (me.shouldShowFocusFrame(cmp)) {
            cls = '.' + me.focusFrameCls + '-';
            ff = me.focusFrame;
            
            // focusEl may in fact be a descendant component to which to delegate focus 
            box = (focusEl.dom ? focusEl : focusEl.el).getBox();
 
            // Size the focus frame's t/b/l/r according to the box 
            // This leaves a hole in the middle of the frame so user 
            // interaction w/ the mouse can continue 
            bt = box.top;
            bl = box.left;
            bw = box.width;
            bh = box.height;
            ft = ff.child(cls + 'top');
            fb = ff.child(cls + 'bottom');
            fl = ff.child(cls + 'left');
            fr = ff.child(cls + 'right');
 
            ft.setWidth(bw).setLocalXY(bl, bt);
            fb.setWidth(bw).setLocalXY(bl, bt + bh - 2);
            fl.setHeight(bh - 2).setLocalXY(bl, bt + 2);
            fr.setHeight(bh - 2).setLocalXY(bl + bw - 2, bt + 2);
 
            ff.show();
        }
 
        me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
    },
 
    onComponentHide: function(cmp) {
        var me = this,
            cmpHadFocus = false,
            focusedCmp = me.focusedCmp,
            parent;
 
        if (focusedCmp) {
            // See if the Component being hidden was the focused Component, or owns the focused Component 
            // In these cases, focus needs to be removed from the focused Component to the nearest focusable ancestor 
            cmpHadFocus = cmp.hasFocus || (cmp.isContainer && cmp.isAncestor(me.focusedCmp));
        }
 
        me.clearComponent(cmp);
 
        // Move focus onto the nearest focusable ancestor, or this is there is none 
        if (cmpHadFocus && (parent = cmp.up(':focusable'))) {
            parent.focus();
        } else {
            me.focusEl.focus();
        }
    },
 
    onComponentDestroy: function() {
 
    },
 
    removeDOM: function() {
        var me = this;
 
        // If we are still enabled globally, or there are still subscribers 
        // then we will halt here, since our DOM stuff is still being used 
        if (me.enabled || me.subscribers.length) {
            return;
        }
 
        Ext.destroy(
            me.focusFrame
        );
        delete me.focusEl;
        delete me.focusFrame;
    },
 
    /**
     * Removes the specified xtype from the {@link #whitelist}.
     * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
     */
    removeXTypeFromWhitelist: function(xtype) {
        var me = this;
 
        if (Ext.isArray(xtype)) {
            Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
            return;
        }
 
        Ext.Array.remove(me.whitelist, xtype);
    },
 
    setupSubscriberKeys: function(container, keys) {
        var me = this,
            el = container.getFocusEl(),
            scope = keys.scope,
            handlers = {
                backspace: me.focusLast,
                enter: me.navigateIn,
                esc: me.navigateOut,
                scope: me
            },
 
            navSiblings = function(e) {
                if (me.focusedCmp === container) {
                    // Root the sibling navigation to this container, so that we 
                    // can automatically dive into the container, rather than forcing 
                    // the user to hit the enter key to dive in. 
                    return me.navigateSiblings(e, me, container);
                } else {
                    return me.navigateSiblings(e);
                }
            };
 
        Ext.iterate(keys, function(key, cb) {
            handlers[key] = function(e) {
                var ret = navSiblings(e);
 
                if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
                    return true;
                }
 
                return ret;
            };
        }, me);
 
        return new Ext.util.KeyNav(el, handlers);
    },
 
    shouldShowFocusFrame: function(cmp) {
        var me = this,
            opts = me.options || {};
 
        // Do not show a focus frame if 
        // 1. We are configured not to. 
        // 2. No Component was passed 
        if (!me.focusFrame || !cmp) {
            return false;
        }
 
        // Global trumps 
        if (opts.focusFrame) {
            return true;
        }
 
        if (me.focusData[cmp.id].focusFrame) {
            return true;
        }
 
        return false;
    }
});