/**
 * A mixin for individually focusable things (Components, Widgets, etc)
 */
Ext.define('Ext.util.Focusable', {
    mixinId: 'focusable',
    
    /**
     * @property {Boolean} focusable 
     * `true` for interactive Components, `false` for static Components
     */
    // Plain Components are static, so not focusable 
    focusable: false,
    
    /**
     * @cfg {Number} [tabIndex] DOM tabIndex attribute for this Focusable
     */
    
    /**
     * @cfg {String} [focusCls='x-focus'] CSS class that will be added to focused
     * Component, and removed when Component blurs.
     */
    focusCls: 'focus',
    
    /**
     * @event focus
     * Fires when this Component receives focus.
     * @param {Ext.Component} this
     * @param {Ext.event.Event} event The focus event.
     */
 
    /**
     * @event blur
     * Fires when this Component loses focus.
     * @param {Ext.Component} this
     * @param {Ext.event.Event} event The blur event.
     */
    
    /**
     * Template method to do any Focusable related initialization that
     * does not involve event listeners creation.
     * @protected
     */
    initFocusable: Ext.emptyFn,
    
    /**
     * Template method to do any event listener initialization for a Focusable.
     * This generally happens after the focusEl is available.
     * @protected
     */
    initFocusableEvents: function() {
        // This will add focus/blur listeners to the getFocusEl() element if that is naturally 
        // focusable. If *not* naturally focusable, then we look for the tabIndex property 
        // to be defined which indicates that the element should be made focusable. 
        this.addFocusListener();
    },
    
    destroyFocusable: function() {
        Ext.destroy(this.focusListeners);
        delete this.focusTask;
    },
 
    enableFocusable: Ext.emptyFn,
    
    disableFocusable: function() {
        var me = this,
            focusCls = me.focusCls,
            focusEl = me.getFocusEl();
        
        if (focusCls && focusEl) {
            focusEl.removeCls(me.removeClsWithUI(focusCls, true));
        }
    },
    
    isFocusable: function() {
        var me = this,
            focusEl;
        
        if (!me.focusable) {
            return false;
        }
        
        focusEl = me.getFocusEl();
        
        if (focusEl && me.canFocus()) {
 
            // getFocusEl might return a Component if a Container wishes to 
            // delegate focus to a descendant. Window can do this via its 
            // defaultFocus configuration which can reference a Button. 
            // Both Component and Element implement isFocusable, so always 
            // ask that. 
            return focusEl.isFocusable(true);
        }
    },
    
    canFocus: function() {
        var me = this;
        
        return me.focusable && me.rendered && !me.destroying &&
              !me.isDestroyed && !me.disabled && me.isVisible(true);
    },
    
    /**
     * Sets up the focus listener on this Component's {@link #getFocusEl focusEl} if it has one.
     *
     * Form Components which must implicitly participate in tabbing order usually have a naturally
     * focusable element as their {@link #getFocusEl focusEl}, and it is the DOM event of that
     * receiving focus which drives the Component's `onFocus` handling, and the DOM event of it
     * being blurred which drives the `onBlur` handling.
     * @private
     */
    addFocusListener: function() {
        var me = this,
            tabIndex = me.tabIndex,
            focusEl = me.getFocusEl(),
            needsTabIndex;
 
        if (focusEl) {
            // getFocusEl might return a Component if a Container wishes to delegate focus to a 
            // descendant. Window can do this via its defaultFocus configuration which can 
            // reference a Button. 
            if (focusEl.isComponent) {
                return focusEl.addFocusListener();
            }
 
            // If the focusEl is naturally focusable, then we always need a focus listener to 
            // drive the Component's onFocus handling. 
            // If *not* naturally focusable, then we only need the focus listener if the 
            // tabIndex property is defined for a Component 
            needsTabIndex = focusEl.needsTabIndex() && !focusEl.dom.hasAttribute('tabindex');
            
            if (!me.focusListenerAdded && (!needsTabIndex || tabIndex != null)) {
                if (needsTabIndex && !focusEl.dom.getAttribute('tabIndex')) {
                    focusEl.dom.setAttribute('tabIndex', tabIndex);
                }
                
                me.focusListeners = focusEl.on({
                    focus: me.onFocus,
                    blur: me.onBlur,
                    scope: me,
                    destroyable: true
                });
 
                // This attribute is a shortcut to look up a Component by its Elements 
                // It only makes sense on focusable elements, so we set it here 
                focusEl.dom.setAttribute(Ext.Component.componentIdAttribute, me.id);
 
                me.focusListenerAdded = true;
            }
        }
    },
    
    /**
     * Try to focus this component.
     * @param {Mixed} [selectText] If applicable, `true` to also select all the text in this component, or an array consisting of start and end (defaults to start) position of selection.
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
     * @param {Function} [callback] Only needed if the `delay` parameter is used. A function to call upon focus.
     * @param {Function} [scope] Only needed if the `delay` parameter is used. The scope (`this` reference) in which to execute the callback.
     * @return {Ext.Component} The focused Component. Usually <code>this</code> Component. Some Containers may
     * delegate focus to a descendant Component ({@link Ext.window.Window Window}s can do this through their
     * {@link Ext.window.Window#defaultFocus defaultFocus} config option.
     */
    focus: function(selectText, delay, callback, scope) {
        var me = this,
            focusEl,
            focusElDom,
            containerScrollTop;
 
        if (!me.focusable || me.isDestroyed || me.destroying) {
            return;
        }
 
        // If delay is wanted, queue a call to this function. 
        if (delay) {
            me.getFocusTask().delay(Ext.isNumber(delay) ? delay : 10, me.focus, me, [selectText, false, callback, scope]);
            return me;
        }
 
        // An immediate focus call must cancel any outstanding delayed focus calls. 
        me.cancelFocus();
        
        // Assignment in conditional here to avoid calling getFocusEl() 
        // if me.canFocus() returns false 
        if (me.canFocus() && (focusEl = me.getFocusEl())) {
 
            // getFocusEl might return a Component if a Container wishes to delegate focus to a 
            // descendant. 
            // Window can do this via its defaultFocus configuration which can reference a Button. 
            if (focusEl.isComponent) {
                return focusEl.focus(selectText, delay, callback, scope);
            }
            
            focusElDom = focusEl.dom;
 
            // If it was an Element with a dom property 
            if (focusElDom) {
 
                // Not a natural focus holding element, add a tab index to make it programatically 
                // focusable (unless it has tabindex already) 
                if (focusEl.needsTabIndex() && !focusElDom.hasAttribute('tabindex')) {
                    focusElDom.tabIndex = -1;
                }
 
                if (me.floating) {
                    containerScrollTop = me.container.dom.scrollTop;
                }
 
                // Focus the element. 
                // The focusEl has a DOM focus listener on it which invokes the Component's onFocus 
                // method to perform Component-specific focus processing 
                focusEl.focus();
                
                if (selectText) {
                    if (Ext.isArray(selectText)) {
                        if (me.selectText) {
                            me.selectText.apply(me, selectText);
                        }
                    }
                    else {
                        focusElDom.select();
                    }
                }
 
                // Call the callback when focus is done 
                Ext.callback(callback, scope);
            }
 
            // Focusing a floating Component brings it to the front of its stack. 
            // this is performed by its zIndexManager. Pass preventFocus true to avoid recursion. 
            if (me.floating) {
                // Every component that doesn't have preventFocus set gets a delayed call to focus(). 
                // Only bring to front if the current component isn't the manager's topmost component. 
                if (me !== me.zIndexManager.getActive()) {
                    me.toFront(true);
                }
 
                if (containerScrollTop !== undefined) {
                    me.container.dom.scrollTop = containerScrollTop;
                }
            }
        }
        
        return me;
    },
 
    /**
     * Cancel any deferred focus on this component
     * @protected
     */
    cancelFocus: function() {
        var task = this.getFocusTask();
        
        if (task) {
            task.cancel();
        }
    },
 
    /**
     * Template method to do any pre-blur processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    beforeBlur: Ext.emptyFn,
    
    // private 
    onBlur: function(e) {
        var me = this,
            container = me.focusableContainer,
            focusCls = me.focusCls,
            focusClsEl;
        
        if (!me.focusable || me.destroying) {
            return;
        }
 
        me.beforeBlur(e);
        
        if (container) {
            container.beforeFocusableChildBlur(me, e);
        }
        
        focusClsEl = me.getFocusClsEl();
        
        if (focusCls && focusClsEl) {
            focusClsEl.removeCls(me.removeClsWithUI(focusCls, true));
        }
        
        if (me.validateOnBlur) {
            me.validate();
        }
        
        me.hasFocus = false;
        me.fireEvent('blur', me, e);
        me.postBlur(e);
        
        if (container) {
            container.afterFocusableChildBlur(me, e);
        }
    },
 
    /**
     * Template method to do any post-blur processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    postBlur: Ext.emptyFn,
 
    /**
     * Template method to do any pre-focus processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    beforeFocus: Ext.emptyFn,
    
    // private 
    onFocus: function(e) {
        var me = this,
            container = me.focusableContainer,
            focusCls = me.focusCls,
            focusClsEl;
        
        if (!me.focusable) {
            return;
        }
 
        if (me.canFocus()) {
            me.beforeFocus(e);
            
            if (container) {
                container.beforeFocusableChildFocus(me, e);
            }
            
            focusClsEl = me.getFocusClsEl();
            
            if (focusCls && focusClsEl) {
                focusClsEl.addCls(me.addClsWithUI(focusCls, true));
            }
            
            if (!me.hasFocus) {
                me.hasFocus = true;
                me.fireEvent('focus', me, e);
            }
            
            me.postFocus(e);
            
            if (container) {
                container.afterFocusableChildFocus(me, e);
            }
        }
    },
    
    /**
     * Template method to do any post-focus processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    postFocus: Ext.emptyFn,
    
    /**
     * Return the actual tabIndex for this Focusable.
     *
     * @return {Number} tabIndex attribute value
     */
    getTabIndex: function() {
        var me = this,
            el, index;
        
        if (!me.focusable) {
            return;
        }
        
        el = me.getFocusEl();
        
        if (el) {
            // getFocusEl may return a child Component 
            index = el.isComponent ? el.getTabIndex() : el.getAttribute('tabindex');
            
            me.tabIndex = index;
        }
        else {
            index = me.tabIndex;
        }
        
        return index;
    },
    
    /**
     * Set the tabIndex property for this Focusable. If the focusEl
     * is avalable, set tabIndex attribute on it, too.
     *
     * @param {Number} newTabIndex new tabIndex to set
     */
    setTabIndex: function(newTabIndex) {
        var me = this,
            el;
        
        if (!me.focusable) {
            return;
        }
        
        me.tabIndex = newTabIndex;
        el = me.getFocusEl();
        
        if (el) {
            // getFocusEl may return a child Component 
            if (el.isComponent) {
                el.setTabIndex(newTabIndex);
            }
            else {
                el.set({ tabindex: newTabIndex });
            }
        }
    },
    
    privates: {
        privacy: 'framework',
        
        /**
         * Returns the focus holder element associated with this Focusable.
         * At the Focusable base mixin level, this function returns `undefined`.
         *
         * Subclasses which use embedded focusable elements (such as Window,
         * Field and Button) should override this for use by the
         * {@link Ext.util.Focusable#method-focus focus} method.
         *
         * @return {Ext.Element} `undefined` because raw Focusables cannot by
         * default hold focus.
         * @method
         * @private
         */
        getFocusEl: Ext.privateFn,
        
        /**
         * Returns the focus styling holder element associated with this Focusable.
         * By default it is the same element as {@link #getFocusEl getFocusEl}.
         *
         * @return {Ext.Element} The focus styling element.
         * @private
         */
        getFocusClsEl: function() {
            return this.getFocusEl();
        },
 
        // private 
        getFocusTask: function() {
            if (!this.focusTask) {
                this.focusTask = Ext.focusTask;
            }
            
            return this.focusTask;
        },
 
        // private 
        blur: function() {
            var me = this,
                focusEl;
            
            if (!me.focusable || !me.rendered) {
                return;
            }
            
            focusEl = me.getFocusEl();
 
            if (focusEl) {
                me.blurring = true;
                focusEl.blur();
                delete me.blurring;
            }
            
            return me;
        },
        
        disableTabbing: function() {
            var me = this,
                el = me.el,
                focusEl;
            
            if (!me.focusable) {
                return;
            }
            
            focusEl = me.getFocusEl();
            
            if (el) {
                el.saveChildrenTabbableState();
            }
            
            if (focusEl) {
                focusEl.saveTabbableState();
            }
        },
        
        enableTabbing: function() {
            var me = this,
                el = me.el,
                focusEl;
            
            if (!me.focusable) {
                return;
            }
            
            focusEl = me.getFocusEl();
            
            if (focusEl) {
                focusEl.restoreTabbableState();
            }
            
            if (el) {
                el.restoreChildrenTabbableState();
            }
        }
    }
},
 
function() {
    // One global DelayedTask to assign focus 
    // So that the last focus call wins. 
    if (!Ext.focusTask) {
        Ext.focusTask = new Ext.util.DelayedTask();
    }
});