/**
 * @class Ext.grid.NavigationModel
 * @private
 * This class listens for events fired from a {@link Ext.grid.Panel GridPanel}, and
 * tracks the currently focused cell.
 */
Ext.define('Ext.grid.NavigationModel', {
    extend: 'Ext.dataview.NavigationModel',
    alias: 'navmodel.grid',
 
    requires: [
        'Ext.grid.Location'
    ],
 
    locationClass: 'Ext.grid.Location',
    
    statics: {
        /**
         * We should ignore keydown events for certain keys pressed in an input field
         * e.g. while editing, to allow for native arrow key navigation, Home/End keys,
         * etc. However we can't ignore all keydown events in input fields wholesale,
         * that breaks Enter/Esc/Tab key processing.
         * This map defines what key names should be ignored if target is an input field.
         * @private
         * @since 6.5.1
         */
        ignoreInputFieldKeys: {
            PAGE_UP: true,
            PAGE_DOWN: true,
            END: true,
            HOME: true,
            LEFT: true,
            UP: true,
            RIGHT: true,
            DOWN: true
        }
    },
 
    /**
     * Focuses the passed position, and optionally selects that position.
     * @param {Ext.grid.Location/Ext.data.Model/Number} location The location to focus.
     * This may be
     *     - A `{@link Ext.grid.Location grid Location}` object;
     *     - A `[column, row]` array; column is `X` coordinate, record is `Y` coordinate.
     *     - An element which is used to locate the referenced cell.
     *     - A number to locate column zero on the passed row
     *     - A record to indicate column zero on the passed record's row.
     * @param {Object} [options] 
     * @param {Object} [options.event] The UI event which caused the navigation if any.
     * @param {Object} [options.select] Pass as `true` to also select the location.
     */
    setLocation: function(location, options) {
        var me = this,
            view = me.getView(),
            event = options && options.event;
 
        me.columnIndex = -1;
 
        // Massage the incoming location into a form the base class can work with
        if (location != null && !location.isGridLocation) {
            if (Ext.isArray(location)) {
                location = {
                    column: location[0],
                    record: location[1]
                };
            } else if (typeof location === 'number') {
                location = view.store.getAt(location);
            }
            location = me.createLocation(location);
            if (event) {
                location.event = event;
            }
        }
        return me.callParent([location, options]);
    },
 
    clearLocation: function() {
        var me = this,
            item;
 
        if (me.location) {
            me.previousLocation = me.location;
            item = me.location.sourceElement;
            if (item) {
                Ext.fly(item).removeCls(me.focusedCls);
            }
            me.location = null;
        }
    },
 
    registerActionable: function(actionable) {
        var me = this,
            view = me.getView(),
            actionables = me.actionables || (me.actionables = []),
            triggerEvent, listeners;
 
        if (!Ext.Array.contains(actionables, actionable)) {
            actionables.push(actionable);
            triggerEvent = actionable.getTriggerEvent();
            if (triggerEvent) {
 
                // create {click: triggerActionable, scope: me, args:[theActionable]}
                listeners = {
                    scope: me,
                    args: [actionable]
                };
                listeners[triggerEvent] = 'triggerActionable';
 
                // Trigger the actionable on the requested event.
                // Also, per ARIA standards:
                // "Enter or F2 pressed while focus is on a cell containing
                // an actionable item enters Actionable Mode"
                actionable.triggerEventListener = view.bodyElement.on(listeners);
            }
        }
    },
 
    unregisterActionable: function(actionable) {
        var actionables = this.actionables;
 
        if (actionables) {
            Ext.Array.remove(actionables, actionable);
        }
    },
 
    privates: {
        // In the case of a focus move invoked by assistive technologies,
        // we have to react to that and maintain correct state.
        onFocusMove: function(e) {
            var me = this,
                view = me.getView(),
                location = me.getLocation();
 
            // Focus moved to the view from an inner location.
            // This can happen on mousedown on non-focusable parts of the grid
            // such as row lines (which are not part of a cell) and non-focusable
            // non-data items. We revert to the current location.
            if (e.toElement === view.el.dom && location) {
                me.clearLocation();
                return me.setLocation(location);
            }
 
            location = me.createLocation(e);
 
            // If a setLocation call has been called with the synchronous option
            // handleLocationChange will already have been called.
            if (!location.equals(me.location)) {
                me.handleLocationChange(location, {
                    event: e,
                    navigate: false // We just navigated
                });
            }
        },
 
        processViewEvent: function(e) {
            var me = this,
                view = me.getView(),
                cell = view.mapToCell(e);
            
            if (Ext.fly(e.target).isInputField() && me.self.ignoreInputFieldKeys[e.getKeyName()]) {
                return false;
            }
 
            // We found our grid cell which contained the event.
            if (cell && cell.row.grid === view) {
                return e;
            }
        },
 
        /**
         * Enters actionable mode at the passed location
         */
        activateCell: function(location) {
            // Attempt to activate a Location based on the current location.
            location.clone().activate();
        },
 
        triggerActionable: function(actionable, e) {
            var actionLocation;
 
            // Request the Actionable to activate a *cell* Location based on the event location.
            actionLocation = actionable.activateCell(this.createLocation(e));
 
            // If we successfully started actionable mode, set our location.
            // Note that this may pass the already active location if the trigger
            // event was inside the actionable, such as clicking in a cell editor.
            // This will be a no-op, so harmless.
            if (actionLocation) {
                this.setLocation(actionLocation);
            }
        },
 
        onChildTouchStart: function(view, location) {
            var e = location.event;
 
            // Not a navigable child - do not disturb focus
            if (location.header || location.footer) {
                e.preventDefault();
            } else {
                // Only react if we already have a location that is elsewhere in the grid,
                // so that we can move.
                //
                // If we do not, this means that there will be an impending
                // onFocusEnter call, and that event will be decoded to focus
                // the targeted location.
                if (this.location && !this.location.equalCell(location)) {
                    this.setLocation(location, {
                        event: location.event,
                        navigate: this.getView().getTriggerEvent() === 'childtouchstart'
                    });
                }
            }
        },
 
        onKeyUp: function(e) {
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                if (this.location) {
                    this.moveUp(e);
                } else {
                    this.setLocation(0);
                }
            }
        },
 
        onKeyDown: function(e) {
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                if (this.location) {
                    this.moveDown(e);
                } else {
                    this.setLocation(0);
                }
            }
        },
 
        onKeyLeft: function(e) {
            var location = this.location,
                isSimpleTree = location.isLastColumn() && location.isFirstColumn();
 
            if (!location.actionable) {
                // Do not scroll
                e.preventDefault();
 
                // On an expanded non-leaf tree cell.
                if (location.isTreeLocation && !location.record.isLeaf() && location.record.isExpanded()) {
 
                    // If a simple Tree (just one column), or its a ctrl+RIGHT
                    // expand the node.
                    if (isSimpleTree === !e.ctrlKey) {
                        return location.cell.collapse();
                    }
                }
 
                // Do not allow SHIFT+(left|right) to wrap.
                if (!(e.shiftKey && location.isFirstColumn())) {
                    this.movePrevious({
                        event: e
                    });
                }
            }
            // From an input field - return true to *not* stop the event.
            else if (Ext.fly(e.target).isInputField()) {
                return true;
            }
        },
 
        onKeyRight: function(e) {
            var location = this.location,
                isSimpleTree = location.isLastColumn() && location.isFirstColumn();
 
            if (!location.actionable) {
                // Do not scroll
                e.preventDefault();
 
                // On a collapsed non-leaf tree cell.
                if (location.isTreeLocation && !location.record.isLeaf() && !location.record.isExpanded()) {
 
                    // If a simple Tree (just one column), or its a ctrl+RIGHT
                    // expand the node.
                    if (isSimpleTree === !e.ctrlKey) {
                        return location.cell.expand();
                    }
                }
 
                // Do not allow SHIFT+(left|right) to wrap.
                if (!(e.shiftKey && location.isLastColumn())) {
                    this.moveNext({
                        event: e
                    });
                }
            }
            // From an input field - return true to *not* stop the event.
            else if (Ext.fly(e.target).isInputField()) {
                return true;
            }
        },
 
        onKeyF2: function(e) {
            // Events are tagged with information about the event target.
            // If the target is *within* the cell, it's actionable mode.
            // If the target *is* the cell, it's navigation mode.
            if (this.location.actionable) {
                this.onKeyEsc();
            } else {
                this.activateCell(this.location);
            }
        },
 
        onKeyEsc: function(e) {
            // Focus the item which the current location encpsulates, regardless of whether it
            // also encpsulates an inner, focusable targetElement..
            if (this.location.actionable) {
                this.location.get().el.focus();
            }
        },
 
        onKeyTab: function(e) {
            var me = this,
                view = me.getView(),
                location = me.location,
                navigate;
 
            if (location.actionable) {
                navigate = function() {
                    me.location = e.shiftKey ? location.previous() : location.next();
                };
                // Now ensure that item is visible beore tabbing.
                view.ensureVisible(location.record).then(function() {
                    // TODO: ensureVisible does not ensure the item is present - it just scrolls
                    // and does not wait for the resulting List adjustment.
                    // TODO: workaround is a 100ms delay. Remove this when ensureVisible guarantees item presence.
                    if (view.mapToItem(location.record)) {
                        navigate();
                    } else {
                        Ext.defer(navigate, 100);
                    }
                });
            }
            // Navigation mode - return true to *not* stop the event
            else {
                return true;
            }
        },
 
        onKeyPageDown: function(e) {
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                var me = this,
                    view = me.getView(),
                    y = (view.infinite ? view.getItemTop(me.location.child) : me.location.child.el.dom.offsetTop) + view.getVisibleHeight(),
                    candidate = view.getRecordIndexFromPoint(0, y);
 
                view.ensureVisible(candidate).then(function() {
                    candidate = new Ext.grid.Location(view, {
                        record: candidate,
                        column: me.location.column
                    });
 
                    // Might have landed on a non-focusable item.
                    // The up method moves to a focusable location.
                    if (!(candidate.sourceElement && Ext.fly(candidate.sourceElement).isFocusable())) {
                        candidate = candidate.up();
                    }
                    // Go down by the visible page size
                    me.setLocation(candidate, {
                        event: e
                    });
                });
            }
        },
 
        onKeyPageUp: function(e) {
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                var me = this,
                    view = me.getView(),
                    y = (view.infinite ? view.getItemTop(me.location.child) : me.location.child.el.dom.offsetTop) - view.getVisibleHeight(),
                    candidate = view.getRecordIndexFromPoint(0, y);
 
                view.ensureVisible(candidate).then(function() {
                    candidate = new Ext.grid.Location(view, {
                        record: candidate,
                        column: me.location.column
                    });
 
                    // Might have landed on a non-focusable item.
                    // The down method advances to a focusable location.
                    if (!(candidate.sourceElement && Ext.fly(candidate.sourceElement).isFocusable())) {
                        candidate = candidate.down();
                    }
                    // Go up by the visible page size
                    me.setLocation(candidate, {
                        event: e
                    });
                });
            }
        },
 
        onKeyHome: function(e) {
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                // Go to first cell in current column
                if (e.ctrlKey) {
                    this.setLocation({
                        record: this.getView().getStore().first(),
                        column: this.location.column
                    }, {
                        event: e
                    });
                }
                // Go to first cell in row
                else {
                    this.setLocation({
                        record: this.location.record,
                        column: this.getView().getFirstVisibleColumn()
                    }, {
                        event: e
                    });
                }
            }
        },
 
        onKeyEnd: function(e) {
 
            // Do not scroll
            e.preventDefault();
 
            if (!this.location.actionable) {
                // Go to last cell in current column
                if (e.ctrlKey) {
                    this.setLocation({
                        record: this.getView().getStore().last(),
                        column: this.location.column
                    }, {
                        event: e
                    });
                }
                // Go to last cell in row
                else {
                    this.setLocation({
                        record: this.location.record,
                        column: this.getView().getLastVisibleColumn()
                    }, {
                        event: e
                    });
                }
            }
        },
 
        onKeySpace: function(e) {
            var target = Ext.fly(e.target),
                events, focusables, result;
 
            // SPACE hits up the Selection Model.
            // But also click the first focusable inner el.
            this.onNavigate(e);
 
            if (!this.location.actionable) {
                focusables = this.location.getFocusables();
                if (focusables.length) {
                    events = Ext.get(focusables[0]).events;
                }
            }
            // In actionable mode, SPACE clicks the focused el
            // if it is not an input field.
            else {
                if (target.isInputField()) {
                    result = true;
                } else {
                    events = target.events;
                }
            }
 
            // Fire any click or tap handlers on the discovered action element.
            if (events) {
                if (events.tap) {
                    events.tap.fire(e);
                }
                if (events.click) {
                    events.click.fire(e);
                }
            }
            return result;
        },
 
        // ENTER emulates an childtap event at the View level
        onKeyEnter: function(e) {
            var l = this.location;
 
            // 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.
            e.stopEvent();
 
            // Navigation mode drops into actionable mode
            if (!l.actionable) {
                // Enter on a CheckNode, toggles it.
                if (l.isTreeLocation && l.record.data.checked != null) {
                    l.record.set('checked', !l.record.data.checked);
                } else {
                    this.activateCell(l);
                }
            }
            // Actionable mode clicks the target, same as SPACE
            else {
                this.onKeySpace(e);
            }
        },
 
        onSelectAllKeyPress: function(e) {
            // Return true to not stop the event if it's in an input field
            if (Ext.fly(e.target).isInputField()) {
                return true;
            }
            // Superclass selects all
            else {
                return this.callParent([e]);
            }
        },
 
        moveUp: function(e) {
            var location = this.getLocation();
 
            if (location) {
                location = location.up();
                if (location) {
                    this.setLocation(location, {
                        event: e
                    });
                }
            }
        },
 
        moveDown: function(e) {
            var location = this.getLocation();
 
            if (location) {
                location = location.down();
                if (location) {
                    this.setLocation(location, {
                        event: e
                    });
                }
            }
        }
    }
});