/**
 * @class Ext.view.NavigationModel
 * @private
 * This class listens for key events fired from a {@link Ext.view.View DataView}, and moves
 * the currently focused item by adding the class {@link #focusCls}.
 */
Ext.define('Ext.view.NavigationModel', {
    alias: 'view.navigation.default',
 
    mixins: [
        'Ext.util.Observable',
        'Ext.mixin.Factoryable',
        'Ext.util.StoreHolder'
    ],
 
    config: {
        store: null
    },
 
    /**
     * @event navigate Fired when a key has been used to navigate around the view.
     * @param {Object} event 
     * @param {Ext.event.Event} keyEvent The key event which caused the navigation.
     * @param {Number} event.previousRecordIndex The previously focused record index.
     * @param {Ext.data.Model} event.previousRecord The previously focused record.
     * @param {HTMLElement} event.previousItem The previously focused view item.
     * @param {Number} event.recordIndex The newly focused record index.
     * @param {Ext.data.Model} event.record the newly focused record.
     * @param {HTMLElement} event.item the newly focused view item.
     */
 
    /**
     * @private
     */
    focusCls: Ext.baseCSSPrefix + 'view-item-focused',
 
    constructor: function() {
        this.mixins.observable.constructor.call(this);
    },
 
    bindComponent: function(view) {
        if (this.view !== view) {
            this.view = view;
            this.bindView(view);
        }
    },
 
    bindView: function(view) {
        var me = this,
            dataSource = view.dataSource,
            listeners;
 
        me.initKeyNav(view);
 
        if (!dataSource.isEmptyStore) {
            me.setStore(dataSource);
        }
 
        listeners = me.getViewListeners();
        listeners.destroyable = true;
 
        me.viewListeners = me.viewListeners || [];
        me.viewListeners.push(view.on(listeners));
    },
 
    updateStore: function(store) {
        this.mixins.storeholder.bindStore.apply(this, [store]);
    },
 
    getViewListeners: function() {
        var me = this;
 
        return {
            containermousedown: me.onContainerMouseDown,
            itemmousedown: me.onItemMouseDown,
 
            // We focus on click if the mousedown handler did not focus
            // because it was a translated "touchstart" event.
            itemclick: me.onItemClick,
            itemcontextmenu: me.onItemMouseDown,
            scope: me
        };
    },
 
    initKeyNav: function(view) {
        var me = this;
 
        // Drive the KeyNav off the View's itemkeydown event so that beforeitemkeydown listeners
        // may veto. By default KeyNav uses defaultEventAction: 'stopEvent', and this is required
        // for movement keys which by default affect scrolling.
        me.keyNav = new Ext.util.KeyNav({
            target: view,
            ignoreInputFields: true,
            eventName: 'itemkeydown',
            defaultEventAction: 'stopEvent',
            processEvent: me.processViewEvent,
            up: me.onKeyUp,
            down: me.onKeyDown,
            right: me.onKeyRight,
            left: me.onKeyLeft,
            pageDown: me.onKeyPageDown,
            pageUp: me.onKeyPageUp,
            home: me.onKeyHome,
            end: me.onKeyEnd,
            space: me.onKeySpace,
            enter: me.onKeyEnter,
            A: {
                ctrl: true,
                // Need a separate function because we don't want the key
                // events passed on to selectAll (causes event suppression).
                handler: me.onSelectAllKeyPress
            },
            scope: me
        });
    },
 
    processViewEvent: function(view, record, node, index, event) {
        if (event.target === node) {
            return event;
        }
 
        return null;
    },
 
    addKeyBindings: function(binding) {
        this.keyNav.addBindings(binding);
    },
 
    enable: function() {
        this.keyNav.enable();
        this.disabled = false;
    },
 
    disable: function() {
        this.keyNav.disable();
        this.disabled = true;
    },
 
    onContainerMouseDown: function(view, mousedownEvent) {
        // If the mousedown in the view element is NOT inside the client region,
        // that is, it was on a scrollbar, then prevent default.
        //
        // Mousedowning on a scrollbar will focus the View.
        // If they have scrolled to the bottom, then onFocusEnter will
        // try to focus the lastFocused or first item. This is undesirable.
        // So on mousedown outside of view client area, prevent the default focus behaviour.
        // See Ext.view.Table#onFocusEnter for this being acted upon.
        if (Ext.scrollbar.width()) {
            if (!view.el.getClientRegion().contains(mousedownEvent.getPoint())) {
                mousedownEvent.preventDefault();
                view.lastFocused = 'scrollbar';
            }
        }
    },
 
    onItemMouseDown: function(view, record, item, index, mousedownEvent) {
        // If the event is a touchstart, leave it until the click to focus.
        if (mousedownEvent.pointerType !== 'touch') {
            this.setPosition(index);
        }
    },
 
    onItemClick: function(view, record, item, index, clickEvent) {
        // If the mousedown that initiated the click has navigated us to the correct spot,
        // just fire the event
        if (this.record === record) {
            this.fireNavigateEvent(clickEvent);
        }
        else {
            this.setPosition(index, clickEvent);
        }
    },
 
    setPosition: function(recordIndex, keyEvent, suppressEvent, preventNavigation, preventFocus) {
        var me = this,
            view = me.view,
            selModel = view.getSelectionModel(),
            dataSource = view.dataSource,
            newRecord,
            newRecordIndex;
 
        if (recordIndex == null || !view.all.getCount()) {
            me.record = me.recordIndex = null;
        }
        else {
            if (typeof recordIndex === 'number') {
                newRecordIndex = Math.max(Math.min(recordIndex, dataSource.getCount() - 1), 0);
                newRecord = dataSource.getAt(recordIndex);
            }
            // row is a Record
            else if (recordIndex.isEntity) {
                newRecord = dataSource.getById(recordIndex.id);
                newRecordIndex = dataSource.indexOf(newRecord);
 
                // Previous record is no longer present; revert to first.
                if (newRecordIndex === -1) {
                    newRecord = dataSource.getAt(0);
                    newRecordIndex = 0;
                }
            }
            // row is a view item
            else if (recordIndex.tagName) {
                newRecord = view.getRecord(recordIndex);
                newRecordIndex = dataSource.indexOf(newRecord);
            }
            else {
                newRecord = newRecordIndex = null;
            }
        }
 
        // No change; just ensure the correct item is focused and return early.
        // Do not push current position into previous position, do not fire events.
        // We must check record instances, not indices because of store reloads
        // (combobox remote filtering).
        // If there's a new record, focus it. Note that the index may be different even though
        // the record is the same (filtering, sorting)
        if (newRecord === me.record) {
            me.recordIndex = newRecordIndex;
 
            return me.focusPosition(newRecordIndex);
        }
 
        if (me.item) {
            me.item.removeCls(me.focusCls);
        }
 
        // Track the last position.
        // Used by SelectionModels as the navigation "from" position.
        me.previousRecordIndex = me.recordIndex;
        me.previousRecord = me.record;
        me.previousItem = me.item;
 
        // Update our position
        me.recordIndex = newRecordIndex;
        me.record = newRecord;
 
        // Prevent navigation if focus has not moved
        preventNavigation = preventNavigation || me.record === me.lastFocused;
 
        // Maintain lastFocused, so that on non-specific focus of the View,
        // we can focus the correct descendant.
        if (newRecord) {
            me.focusPosition(me.recordIndex);
        }
        else if (!preventFocus) {
            me.item = null;
        }
 
        if (!suppressEvent) {
            selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record);
        }
 
        // If we have moved, fire an event
        if (!preventNavigation && keyEvent) {
            me.fireNavigateEvent(keyEvent);
        }
    },
 
    /**
     * @private
     * Focuses the currently active position.
     * This is used on view refresh and on replace.
     */
    focusPosition: function(recordIndex) {
        var me = this,
            lastFocusedItem;
 
        if (recordIndex != null && recordIndex !== -1) {
            if (recordIndex.isEntity) {
                recordIndex = me.view.dataSource.indexOf(recordIndex);
            }
 
            me.item = me.view.all.item(recordIndex);
 
            if (me.item) {
 
                if (me.view.tabInnerItems) {
                    lastFocusedItem = Ext.fly(me.view.getNodeByRecord(me.lastFocused));
 
                    if (lastFocusedItem && lastFocusedItem.el !== me.item.el) {
 
                        // Save the tabbable state of the current component, 
                        // including itself and excluding any previously saved state.
                        // - `skipSelf: false` includes the current component itself, 
                        // only capturing the tabbable state of its children.
                        // - `includeSaved: false` ensures that any previously saved 
                        // tabbable state is not included in this update.
                        lastFocusedItem.saveTabbableState({
                            skipSelf: false,
                            includeSaved: false
                        });
                    }
 
                    // When we focus the dataitem for the first time, tab-index is not saved, hence
                    // explicitly setting it.
                    me.item.setTabIndex(0);
 
                    // Restore the previously saved tabbable state.
                    // - skipSelf: true because we need to make currently focused item, 
                    // to be tabbable, for Shift + Tab
                    me.item.restoreTabbableState({
                        skipSelf: false
                    });
                }
 
                me.lastFocused = me.record;
                me.lastFocusedIndex = me.recordIndex;
                me.focusItem(me.item);
            }
            else {
                me.record = null;
            }
        }
        else {
            me.item = null;
        }
    },
 
    /**
     * @template
     * @protected
     * Called to focus an item in the client {@link Ext.view.View DataView}.
     * The default implementation adds the {@link #focusCls} to the passed item focuses it.
     * Subclasses may choose to keep focus in another target.
     *
     * For example {@link Ext.view.BoundListKeyNav} maintains focus in the input field.
     * @param {Ext.dom.Element} item 
     * @return {undefined} 
     */
    focusItem: function(item) {
        item.addCls(this.focusCls);
        item.focus();
    },
 
    getPosition: function() {
        return this.record ? this.recordIndex : null;
    },
 
    getRecordIndex: function() {
        return this.recordIndex;
    },
 
    getItem: function() {
        return this.item;
    },
 
    getRecord: function() {
        return this.record;
    },
 
    getLastFocused: function() {
        // No longer there. The caller must fall back to a default.
        if (this.view.dataSource.indexOf(this.lastFocused) === -1) {
            return null;
        }
 
        return this.lastFocused;
    },
 
    onKeyUp: function(keyEvent) {
        var newPosition = this.recordIndex - 1;
 
        if (newPosition < 0) {
            newPosition = this.view.all.getCount() - 1;
        }
 
        this.setPosition(newPosition, keyEvent);
    },
 
    onKeyDown: function(keyEvent) {
        var newPosition = this.recordIndex + 1;
 
        if (newPosition > this.view.all.getCount() - 1) {
            newPosition = 0;
        }
 
        this.setPosition(newPosition, keyEvent);
    },
 
    onKeyRight: function(keyEvent) {
        var newPosition = this.recordIndex + 1;
 
        if (newPosition > this.view.all.getCount() - 1) {
            newPosition = 0;
        }
 
        this.setPosition(newPosition, keyEvent);
    },
 
    onKeyLeft: function(keyEvent) {
        var newPosition = this.recordIndex - 1;
 
        if (newPosition < 0) {
            newPosition = this.view.all.getCount() - 1;
        }
 
        this.setPosition(newPosition, keyEvent);
    },
 
    onKeyPageDown: Ext.emptyFn,
 
    onKeyPageUp: Ext.emptyFn,
 
    onKeyHome: function(keyEvent) {
        this.setPosition(0, keyEvent);
    },
 
    onKeyEnd: function(keyEvent) {
        this.setPosition(this.view.all.getCount() - 1, keyEvent);
    },
 
    onKeySpace: function(keyEvent) {
        this.fireNavigateEvent(keyEvent);
    },
 
    // ENTER emulates an itemclick event at the View level
    onKeyEnter: function(keyEvent) {
        // Stop the keydown event so that an ENTER keyup does not get delivered to
        // any element which focus is transferred to in a click handler.
        keyEvent.stopEvent();
        keyEvent.view.fireEvent('itemclick', keyEvent.view, keyEvent.record, keyEvent.item,
                                keyEvent.recordIndex, keyEvent);
    },
 
    onSelectAllKeyPress: function(keyEvent) {
        this.fireNavigateEvent(keyEvent);
    },
 
    fireNavigateEvent: function(keyEvent) {
        var me = this;
 
        me.fireEvent('navigate', {
            navigationModel: me,
            keyEvent: keyEvent,
            previousRecordIndex: me.previousRecordIndex,
            previousRecord: me.previousRecord,
            previousItem: me.previousItem,
            recordIndex: me.recordIndex,
            record: me.record,
            item: me.item
        });
    },
 
    destroy: function() {
        this.setStore(null);
        Ext.destroy(this.viewListeners, this.keyNav);
 
        this.callParent();
    }
});