/**
 * A mixin for groups of Focusable things (Components, Widgets, etc) that
 * should respond to arrow keys to navigate among the peers, but keep only
 * one of the peers focusable by default (tabIndex=0)
 *
 * Some examples: Toolbars, Radio groups, Tab bars
 */

Ext.define('Ext.util.FocusableContainer', {
    extend: 'Ext.Mixin',
    
    requires: [
        'Ext.util.KeyNav'
    ],
    
    mixinConfig: {
        id: 'focusablecontainer',
        
        before: {
            onAdd: 'onFocusableChildAdd',
            onRemove: 'onFocusableChildRemove',
            destroy: 'destroyFocusableContainer',
            onFocusEnter: 'onFocusEnter'
        },
        
        after: {
            afterRender: 'initFocusableContainer',
            onFocusLeave: 'onFocusLeave'
        }
    },
    
    isFocusableContainer: true,
    
    /**
     * @cfg {Boolean} [enableFocusableContainer=true] Enable or disable
     * navigation with arrow keys for this FocusableContainer. This option may
     * be useful with nested FocusableContainers such as Grid column headers,
     * when only the root container should handle keyboard events.
     */
    enableFocusableContainer: true,
    
    /**
     * @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the
     * active Focusable child of this container when using the "Roaming tabindex"
     * technique. Set this value to > 0 to precisely control the tabbing order
     * of the components/containers on the page.
     */
    activeChildTabIndex: 0,
    
    /**
     * @cfg {Number} [inactiveChildTabIndex=-1] DOM tabIndex attribute to set on
     * inactive Focusable children of this container when using the "Roaming tabindex"
     * technique. This value rarely needs to be changed from its default.
     */
    inactiveChildTabIndex: -1,
    
    privates: {
        initFocusableContainer: function() {
            // Allow nested containers to optionally disable
            // children containers' behavior
            if (this.enableFocusableContainer) {
                this.doInitFocusableContainer();
            }
        },
        
        doInitFocusableContainer: function() {
            var me = this,
                el;
        
            el = me.getFocusableContainerEl();
        
            // We set tabIndex on the focusable container el so that the user
            // could tab into it; we catch its focus event and focus a child instead
            me.activateFocusableContainerEl(el);
        
            me.mon(el, 'mousedown', me.onFocusableContainerMousedown, me);
        
            me.focusableKeyNav = me.createFocusableContainerKeyNav(el);
        },
        
        createFocusableContainerKeyNav: function(el) {
            var me = this;
            
            return new Ext.util.KeyNav(el, {
                ignoreInputFields: true,
                scope: me,

                tab: me.onFocusableContainerTabKey,
                enter: me.onFocusableContainerEnterKey,
                space: me.onFocusableContainerSpaceKey,
                up: me.onFocusableContainerUpKey,
                down: me.onFocusableContainerDownKey,
                left: me.onFocusableContainerLeftKey,
                right: me.onFocusableContainerRightKey
            });
        },
        
        destroyFocusableContainer: function() {
            if (this.enableFocusableContainer) {
                this.doDestroyFocusableContainer();
            }
        },
    
        doDestroyFocusableContainer: function() {
            var keyNav = this.focusableKeyNav;
        
            if (keyNav) {
                keyNav.destroy();
                delete this.focusableKeyNav;
            }
        },
        
        // Default FocusableContainer implies a flat list of focusable children
        getFocusables: function() {
            return this.items.items;
        },

        initDefaultFocusable: function(beforeRender) {
            var me = this,
                activeIndex = me.activeChildTabIndex,
                haveFocusable = false,
                items, item, i, len, tabIdx;

            items = me.getFocusables();
            len   = items.length;

            if (!len) {
                return;
            }

            // Check if any child Focusable is already active.
            // Note that we're not determining *which* focusable child
            // to focus here, only that we have some focusables.
            for (i = 0; i < len; i++) {
                item = items[i];

                if (item.focusable) {
                    haveFocusable = true;
                    tabIdx = item.getTabIndex();

                    if (tabIdx != null && tabIdx >= activeIndex) {
                        return item;
                    }
                }
            }

            // No interactive children found, no point in going further
            if (!haveFocusable) {
                return;
            }

            // No child is focusable by default, so the first *interactive*
            // one gets initial childTabIndex. We are not looking for a focusable
            // child here because it may not be focusable yet if this happens
            // before rendering; we assume that an interactive child will become
            // focusable later and now activateFocusable() will just assign it
            // the respective tabIndex.
            item = me.findNextFocusableChild(null, true, items, beforeRender);

            if (item) {
                me.activateFocusable(item);
            }

            return item;
        },

        clearFocusables: function() {
            var me = this,
                items = me.getFocusables(),
                len = items.length,
                item, i;

            for (i = 0; i < len; i++) {
                item = items[i];

                if (item.focusable) {
                    me.deactivateFocusable(item);
                }
            }
        },

        activateFocusable: function(child, /* optional */ newTabIndex) {
            var activeIndex = newTabIndex != null ? newTabIndex : this.activeChildTabIndex;

            child.setTabIndex(activeIndex);
        },

        deactivateFocusable: function(child, /* optional */ newTabIndex) {
            var inactiveIndex = newTabIndex != null ? newTabIndex : this.inactiveChildTabIndex;

            child.setTabIndex(inactiveIndex);
        },

        onFocusableContainerTabKey: function() {
            return true;
        },

        onFocusableContainerEnterKey: function() {
            return true;
        },

        onFocusableContainerSpaceKey: function() {
            return true;
        },

        onFocusableContainerUpKey: function(e) {
            return this.moveChildFocus(e, false);
        },
        
        onFocusableContainerLeftKey: function(e) {
            return this.moveChildFocus(e, false);
        },
        
        onFocusableContainerRightKey: function(e) {
            return this.moveChildFocus(e, true);
        },
        
        onFocusableContainerDownKey: function(e) {
            return this.moveChildFocus(e, true);
        },
        
        getFocusableFromEvent: function(e) {
            var child = Ext.ComponentManager.byElement(e.getTarget());
        
            //<debug>
            if (!child) {
                Ext.Error.raise("No focusable child found for keyboard event!");
            }
            //</debug>
            
            return child;
        },
        
        moveChildFocus: function(e, forward) {
            var child = this.getFocusableFromEvent(e);
            
            return this.focusChild(child, forward, e);
        },
    
        focusChild: function(child, forward) {
            var nextChild = this.findNextFocusableChild(child, forward);
        
            if (nextChild) {
                nextChild.focus();
            }
        
            return nextChild;
        },
        
        findNextFocusableChild: function(child, step, items, beforeRender) {
            var item, idx, i, len;
        
            items = items || this.getFocusables();
            
            // If the child is null or undefined, idx will be -1.
            // The loop below will account for that, trying to find
            // the first focusable child from either end (depending on step)
            idx = Ext.Array.indexOf(items, child);
            
            // It's often easier to pass a boolean for 1/-1
            step = step === true ? 1 : step === false ? -1 : step;
        
            len = items.length;
            i   = step > 0 ? (idx < len ? idx + step : 0) : (idx > 0 ? idx + step : len - 1);
        
            for (;; i += step) {
                // We're looking for the first or last focusable child
                // and we've reached the end of the items, so punt
                if (idx < 0 && (i >= len || i < 0)) {
                    return null;
                }
                
                // Loop over forward
                else if (i >= len) {
                    i = -1; // Iterator will increase it once more
                    continue;
                }
                
                // Loop over backward
                else if (i < 0) {
                    i = len;
                    continue;
                }
                
                // Looped to the same item, give up
                else if (i === idx) {
                    return null;
                }
                
                item = items[i];
                
                if (!item || !item.focusable) {
                    continue;
                }
                
                // This loop can be run either at FocusableContainer init time,
                // or later when we need to navigate upon pressing an arrow key.
                // When we're navigating, we have to know exactly if the child is
                // focusable or not, hence only rendered children will make the cut.
                // At the init time item.isFocusable() may return false incorrectly
                // just because the item has not been rendered yet and its focusEl
                // is not defined, so we don't bother to call isFocus and return
                // the first potentially focusable child.
                if (beforeRender || item.isFocusable()) {
                    return item;
                }
            }
        
            return null;
        },
    
        getFocusableContainerEl: function() {
            return this.el;
        },
        
        onFocusableChildAdd: function(child) {
            return this.doFocusableChildAdd(child);
        },
        
        activateFocusableContainerEl: function(el) {
            el = el || this.getFocusableContainerEl();
        
            el.set({ tabindex: this.activeChildTabIndex });
        },
        
        deactivateFocusableContainerEl: function(el) {
            el = el || this.getFocusableContainerEl();
        
            el.set({ tabindex: this.inactiveChildTabIndex });
        },
        
        doFocusableChildAdd: function(child) {
            if (child.focusable) {
                child.focusableContainer = this;
                this.deactivateFocusable(child);
            }
        },
        
        onFocusableChildRemove: function(child) {
            return this.doFocusableChildRemove(child);
        },
    
        doFocusableChildRemove: function(child) {
            // If the focused child is being removed, we deactivate the FocusableContainer
            // So that it returns to the tabbing order.
            // For example, locking a grid column must return the owning HeaderContainer to tabbability
            if (child === this.lastFocusedChild) {
                this.lastFocusedChild = null;
                this.activateFocusableContainerEl();
            }
            delete child.focusableContainer;
        },
        
        onFocusableContainerMousedown: function(e, target) {
            var targetCmp = Ext.ComponentManager.byElement(target);
            
            // Capture the timestamp for the mousedown. If we're navigating into the container itself
            // via the mouse we don't want to default focus the first child like we would when using
            // the keyboard. By the time we get to the focusenter handling, we don't know what has caused
            // the focus to be triggered, so if the timestamp falls within some small epsilon, the focus enter
            // has been caused via the mouse and we can react accordingly.
            this.mousedownTimestamp = targetCmp === this ? Ext.Date.now() : 0;
        },

        onFocusEnter: function(e) {
            var me = this,
                target = e.toComponent,
                mousedownTimestamp = me.mousedownTimestamp,
                epsilon = 50,
                child;
            
            me.mousedownTimestamp = 0;
            if (target === me) {
                if (!mousedownTimestamp || Ext.Date.now() - mousedownTimestamp > epsilon) {
                    child = me.initDefaultFocusable();

                    if (child) {
                        me.deactivateFocusableContainerEl();
                        child.focus();
                    }
                }
            } else {
                me.deactivateFocusableContainerEl();
            }
            
            return target;
        },

        onFocusLeave: function(e) {
            var me = this,
                lastFocused = me.lastFocusedChild;

            if (!me.isDestroyed) {
                me.clearFocusables();

                if (lastFocused) {
                    me.activateFocusable(lastFocused);
                }
                else {
                    me.activateFocusableContainerEl();
                }
            }
        },
        
        beforeFocusableChildBlur: Ext.privateFn,
        afterFocusableChildBlur: Ext.privateFn,
    
        beforeFocusableChildFocus: function(child) {
            var me = this;
            
            me.clearFocusables();
            me.activateFocusable(child);
            
            if (child.needArrowKeys) {
                me.guardFocusableChild(child);
            }
        },
        
        guardFocusableChild: function(child) {
            var me = this,
                index = me.activeChildTabIndex,
                guard;
            
            guard = me.findNextFocusableChild(child, -1);
            
            if (guard) {
                guard.setTabIndex(index);
            }
            
            guard = me.findNextFocusableChild(child, 1);
            
            if (guard) {
                guard.setTabIndex(index);
            }
        },
    
        afterFocusableChildFocus: function(child) {
            this.lastFocusedChild = child;
        },
        
        // TODO
        onFocusableChildShow: Ext.privateFn,
        onFocusableChildHide: Ext.privateFn,
        onFocusableChildEnable: Ext.privateFn,
        onFocusableChildDisable: Ext.privateFn,
        onFocusableChildMasked: Ext.privateFn,
        onFocusableChildDestroy: Ext.privateFn,
        onFocusableChildUpdate: Ext.privateFn
    }
});
