/**
 * A selection model for {@link Ext.grid.Grid grids} which allows you to select data in
 * a spreadsheet-like manner.
 *
 * Supported features:
 *
 *  - Single / Range / Multiple individual row selection.
 *  - Single / Range cell selection.
 *  - Column selection by click selecting column headers.
 *  - Select / deselect all by clicking in the top-left, header.
 *  - Adds row number column to enable row selection.
 *  - Optionally you can enable row selection using checkboxes
 *
 * # Example usage
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: '[email protected]', phone: '555-111-1224' },
 *             { name: 'Bart', email: '[email protected]', phone: '555-222-1234' },
 *             { name: 'Homer', email: '[email protected]', phone: '555-222-1244' },
 *             { name: 'Marge', email: '[email protected]', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: store,
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         columns: [
 *             { text: 'Name', dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' }
 *         ],
 *         selectable: {
 *             columns: false, // Can select cells and rows, but not columns
 *             extensible: true // Uses the draggable selection extender
 *         }
 *     });
 *
 * # Using {@link Ext.data.virtual.Store}s
 * It is very important to remember that a {@link Ext.data.virtual.Store} does *not* contain the
 * full dataset. The purpose of a VirtualStore is to only hold in the client, a range of
 * pages from the dataset that corresponds with what is currently visible in the grid
 * (plus a few pages above and below the visible range to allow fast scrolling).
 *
 * When using "select all" rows and a VirtualStore, an `allSelected` flag is set, and so all
 * records which are read into the client side cache will thenceforth be selected, and will
 * be rendered as selected in the grid.
 *
 * *But records which have not been read into the cache will obviously not be available
 * when interrogating selected records. What is stored as being selected is row index ranges.*
 *
 */
Ext.define('Ext.grid.selection.Model', {
    extend: 'Ext.dataview.selection.Model',
    requires: [
        'Ext.grid.Location',
        'Ext.grid.selection.*'
    ],
 
    alias: 'selmodel.grid',
 
    isGridSelectionModel: true,
 
    config: {
        /**
         * @cfg {Boolean} [columns=false]
         * Set to `true` to enable selection of columns.
         *
         * **NOTE**: This will disable sorting on header click and instead provide column
         * selection and deselection. Sorting is still available via column header menu.
         */
        columns: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} [cells=false]
         * Set to `true` to enable selection of individual cells or a single rectangular
         * range of cells. This will provide cell range selection using click, and
         * potentially drag to select a rectangular range if (@link #cfg!drag} is `true`.
         * You can also use "SHIFT + arrow" key navigation to select a range of cells.
         */
        cells: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} [rows=true]
         * Set to `true` to enable selection of rows by clicking on the selection model's
         * {@link #cfg!checkbox} column, {@link Ext.grid.Grid#cfg!rowNumbers row number column}, or, if (@link #cfg!drag}
         * is `true`, by swiping down the {@link Ext.grid.Grid#cfg!rowNumbers row number column}.
         */
        rows: {
            $value: true,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} [drag]
         * Set to `true` to enables cell and row range selection by dragging.
         */
        drag: false,
 
        /**
         * @cfg {String} [extensible=false]
         * This configures whether this selection model is to implement a mouse based dragging gesture to extend a *contiguous* selection.
         *
         * Note that if there are multiple, discontiguous selected rows or columns, selection extension is not available.
         *
         * If set, then the bottom right corner of the contiguous selection will display a drag handle. By dragging this, an extension area
         * may be defined into which the selection is extended.
         *
         * Upon the end of the drag, the {@link Ext.grid.Grid#beforeselectionextend beforeselectionextend} event will be fired though the
         * encapsulating grid. Event handlers may manipulate the store data in any way.
         *
         * Possible values for this configuration are
         *
         *    - `"x"` Only allow extending the block to the left or right.
         *    - `"y"` Only allow extending the block above or below.
         *    - `"xy"` Allow extending the block in both dimensions.
         *    - `"both"` Allow extending the block in both dimensions.
         *    - `true` Allow extending the block in both dimensions.
         *    - `false` Disable the extensible feature
         *    - `null` Disable the extensible feature
         *
         * It's important to notice that setting this to `"both"`, `"xy"` or `true` will allow you to extend the selection in both
         * directions, but only one direction at a time. It will NOT be possible to drag it diagonally.
         */
        extensible: {
            $value: false,
            lazy: true
        },
 
        /**
         * @cfg {Boolean} [checkbox=false]
         * Configure as `true` to include a checkbox to indicate selection of *Records*. The
         * checkbox cell plays no part in cell or column selection apart from being a selected
         * cell and part of any iteration through selections.
         *
         * See {@link #cfg!headerCheckbox} for inclusion of a "select all" checkbox in the
         * column header of the checkbox column.
         *
         * See {@link #cfg!checkboxDefaults} for how to influence the configuration of the checkbox
         * column header.
         */
        checkbox: false,
 
        /**
         * @cfg {Boolean} [headerCheckbox=true]
         * Configure as `false` to not display the header checkbox at the top of the checkbox column
         * when {@link #checkboxSelect} is set.
         */
        headerCheckbox: true,
 
        /**
         * @cfg {Object} [checkboxDefaults]
         * A config object to configure the checkbox column header if {@link #cfg!checkbox} is set.
         */
        checkboxDefaults: {
            xtype: 'selectioncolumn',
            text: null,
            width: 30
        },
 
        showNumbererColumn: false
    },
 
    /**
     * @event selectionchange
     * Fired *by the grid* after the selection changes. Return `false` to veto the selection extension.
     *
     * Note that the behavior of selectionchange is different in Ext 6.x vs. Ext 5.  In Ext 6.x, if rows
     * are being selected, a block of records is passed as the second parameter.  In Ext 5, the selection
     * object was passed.
     *
     *
     * @param {Ext.grid.Panel} grid The grid whose selection has changed.
     * @param {Ext.dataview.selection.Selection} selection A subclass of
     * {@link Ext.dataview.selection.Selection} describing the new selection.
     */
 
    /**
     * @cfg {Boolean} checkboxSelect [checkboxSelect=false]
     * Enables selection of the row via clicking on checkbox. Note: this feature will add
     * new column at position specified by {@link #checkboxColumnIndex}.
     */
    checkboxSelect: false,
 
    /**
     * @cfg {Number/String} [checkboxColumnIndex=0]
     * The index at which to insert the checkbox column.
     * Supported values are a numeric index, and the strings 'first' and 'last'. Only valid when set
     * *before* render.
     */
    checkboxColumnIndex: 0,
 
    mode: 'multi',
 
    columnSelectCls: Ext.baseCSSPrefix + 'selmodel-column-select',
    rowNumbererHeaderCls: Ext.baseCSSPrefix + 'selmodel-row-numberer-hd',
 
    /**
     * @member Ext.grid.Grid
     * @event beforeselectionextend An event fired when an extension block is extended
     * using a drag gesture. Only fired when the grid's `{@link Ext.grid.Grid.selectable #cfg!selectable}`
     * is configured with the {@link Ext.grid.selection.Model#extensible extensible} config.
     * @param {Ext.grid.Grid} grid The owning grid.
     * @param {Ext.dataview.selection.Selection} An object which encapsulates a contiguous selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {Ext.grid.Location} extension.start The start (top left) cell of the extension area.
     * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means on the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
 
    /**
     * @member Ext.grid.Grid
     * @event selectionextenderdrag An event fired when an extension block is dragged to
     * encompass a new range. Only fired when the grid's `{@link Ext.grid.Grid.selectable #cfg!selectable}`
     * is configured with the {@link Ext.grid.selection.Model#extensible extensible} config.
     * @param {Ext.grid.Grid} grid The owning grid.
     * @param {Ext.dataview.selection.Selection} An object which encapsulates a contiguous selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {HTMLElement} extension.overCell The grid cell over which the mouse is being dragged.
     * @param {Ext.grid.Location} extension.start The start (top left) cell of the extension area.
     * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means on the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
 
    /**
     * @private
     */
    updateView: function (view, oldView) {
        var me = this,
            rowNumberer = me.numbererColumn = view.getRowNumbers(),
            checkbox = me.getCheckbox();
 
        me.callParent([view, oldView]);
 
        if (oldView) {
            me.navigationModel = null;
            Ext.destroy(me.viewListeners);
        }
 
        if (view) {
            // If there is a row numberer column we can use, add our classes to it so it can 
            // get a different UI if the theme requires it. Also the header cursor is a 
            // "select all" diagonal arrow. 
            if (rowNumberer) {
                // If the grid shows a row numberer, add our class 
                rowNumberer.setCell({
                    cls: me.rowNumbererCellCls
                });
                rowNumberer.setCls(me.rowNumbererHeaderCls);
            }
 
            if (checkbox) {
                view.registerColumn(checkbox);
            }
 
            me.viewListeners = view.on(me.getViewListeners());
        }
    },
 
    /**
     * @private
     * Called after the view has completed its initialization.
     * @param view
     */
    onViewCreated: function(view) {
        // Add class to add special cursor pointer to column headers 
        if (this.getColumns()) {
            view.addCls(this.columnSelectCls);
        }
        this.updateHeaderState();
    },
 
    updateDrag: function (drag) {
        this.getView()[drag ? 'on' : 'un']('childtouchstart', 'handleMouseDown', this);
    },
 
    /**
     * @private
     * @param what {"rows"/"records'/"cells"/"columns"} What kind of object is to be selected.
     * @return {Ext.dataview.selection.Selection} A Selection object of the required type.
     */
    getSelection: function (what, reset) {
        // The two are interchangeable, to callers, but virtual stores use 
        // row range selection as opposed to record collection. 
        if (what === 'rows' || what === 'records') {
            what = this.getStore().isVirtualStore ? 'rows' : 'records';
        }
 
        var result = this.callParent(),
            config;
 
        // If called with a required type, ensure that the selection object 
        // is of that type. 
        if (what) {
            what = what.toLowerCase();
            if (!result || result.type !== what) {
                config = {
                    type: what
                };
                if (what === 'records') {
                    config.selected = this.getSelected();
                }
                this.setSelection(config);
                result = this.callParent();
            } else if (reset) {
                result.clear(true);
            }
        }
        return result;
    },
 
    /**
     * Retrieve a configuration to be used in a HeaderContainer.
     * This should be used when checkboxSelect is set to false.
     * @private
     */
    createCheckboxColumn: function(checkboxDefaults) {
        var me = this;
 
        return Ext.apply({
            headerCheckbox: me.getHeaderCheckbox() !== false
        }, checkboxDefaults);
    },
 
    /**
     * @private
     */
    onHeaderTap: function(headerCt, header, e) {
        var me = this,
            sel = me.getSelection(),
            hct,
            range,
            i;
 
        // A click on the numberer column toggles all 
        if (header === this.numbererColumn) {
            me.toggleAll(header, e);
        }
        // A column select click: exclude the checkbox column 
        else if (me.getColumns() && header !== me.getCheckbox()) {
            if (e.shiftKey && sel && sel.lastColumnSelected) {
                sel.clear();
                hct = me.getView().getHeaderContainer();
                range = Ext.Array.sort([hct.indexOfLeaf(sel.lastColumnSelected), hct.indexOf(header)], Ext.Array.numericSortFn);
                for (= range[0]; i <= range[1]; i++) {
                    me.selectColumn(hct.getHeaderAtIndex(i), true);
                }
            } else {
                if (me.isColumnSelected(header)) {
                    me.deselectColumn(header);
                    me.getSelection().lastColumnSelected = null;
                } else {
                    me.selectColumn(header, e.ctrlKey);
                    me.getSelection().lastColumnSelected = header;
                }
            }
        }
    },
 
    /**
     * @private
     */
    toggleAll: function(header, e) {
        var me = this,
            sel = me.getSelection(me.getRows() ? 'rows' : me.getCells() ? 'cells' : 'columns');
 
        e.stopEvent();
        // Not all selected, select all 
        if (!sel || !sel.isAllSelected()) {
            me.selectAll();
        } else {
            me.deselectAll();
        }
        me.updateHeaderState();
        me.lastColumnSelected = null;
    },
 
    selectByLocation: function(location) {
        //<debug> 
        if (!location.isGridLocation) {
            Ext.raise('selectByLocation MUST be passed an Ext.grid.Location');
        }
        //</debug> 
        var me = this,
            record = location.record,
            column = location.column;
 
        if (me.getCells()) {
            me.selectCells(location, location);
        } else if (me.getRows() && record) {
            this.select(record);
        } else if (me.getColumns() && column) {
            me.selectColumn(column);
        }
    },
 
    /**
     * @private
     */
    updateHeaderState: function() {
        // check to see if all records are selected 
        var me = this,
            store = me.getStore(),
            sel = me.getSelection(),
            isChecked = false,
            checkHd = me.getCheckbox(),
            storeCount;
 
        if (store && sel && sel.isRows) {
            storeCount = store.getCount();
            if (store.isBufferedStore) {
                isChecked = sel.allSelected;
            } else {
                isChecked = storeCount > 0 && (storeCount === sel.getCount());
            }
        }
 
        if (checkHd) {
            checkHd.setHeaderStatus(isChecked);
        }
    },
 
    /**
     * Intercepts the grid's updateColumns method.  Adds the checkbox header.
     * @param {Object[]} columns
     * @private
     */
    onColumnUpdate: function(headerCt, columns) {
        var me = this,
            checkColumn = me.getCheckbox();
 
        if (checkColumn) {
            // This is being called from a reconfigure operation - from updateColumns 
            // so we have to preserve our column from destruction 
            if (headerCt) {
                headerCt.remove(checkColumn, false);
            }
 
            columns.push(checkColumn);
        }
    },
 
    select: function(records, keepExisting, suppressEvent) {
        // API docs are inherited 
        var me = this,
            sel = me.getSelection('records'),
            store = me.getStore(),
            len, i, record;
 
        if (!Ext.isArray(records)) {
            records = [records];
        }
        len = records.length;
        for (= 0; i < len; i++) {
            record = records[i];
            if (typeof record === 'number') {
                records[i] = record = store.getAt(record);
            }
        }
 
        // SelectionObject will call fireSelectionChange if necessary 
        sel.add(records, keepExisting, suppressEvent);
    },
 
    deselect: function(records, suppressEvent) {
        // API docs are inherited 
        var me = this,
            sel = me.getSelection('records'),
            store = me.getView().getStore(),
            len, i, record;
 
        if (sel && sel.isRecords) {
            if (!Ext.isArray(records)) {
                records = [records];
            }
            len = records.length;
            for (= 0; i < len; i++) {
                record = records[i];
                if (typeof record === 'number') {
                    records[i] = record = store.getAt(record);
                }
            }
        }
 
        // SelectionObject will call fireSelectionChange if necessary 
        sel.remove(records, suppressEvent);
    },
 
    onCollectionRemove: function(selectedCollection, chunk) {
        this.updateHeaderState();
        this.callParent([selectedCollection, chunk]);
    },
 
    onCollectionAdd: function(selectedCollection, adds) {
        this.updateHeaderState();
        this.callParent([selectedCollection, adds]);
    },
 
    /**
     * This method allows programmatic selection of the cell range.
     *
     *     @example
     *     var store = Ext.create('Ext.data.Store', {
     *         fields  : ['name', 'email', 'phone'],
     *         data    : {
     *             items : [
     *                 { name : 'Lisa',  email : '[email protected]',  phone : '555-111-1224' },
     *                 { name : 'Bart',  email : '[email protected]',  phone : '555-222-1234' },
     *                 { name : 'Homer', email : '[email protected]', phone : '555-222-1244' },
     *                 { name : 'Marge', email : '[email protected]', phone : '555-222-1254' }
     *             ]
     *         },
     *         proxy   : {
     *             type   : 'memory',
     *             reader : {
     *                 type : 'json',
     *                 root : 'items'
     *             }
     *         }
     *     });
     *
     *     var grid = Ext.create('Ext.grid.Grid', {
     *         title    : 'Simpsons',
     *         store    : store,
     *         width    : 400,
     *         renderTo : Ext.getBody(),
     *         columns  : [
     *            columns: [
     *               { text: 'Name',  dataIndex: 'name' },
     *               { text: 'Email', dataIndex: 'email', flex: 1 },
     *               { text: 'Phone', dataIndex: 'phone', width:120 },
     *               {
     *                   text:'Combined', dataIndex: 'name', width : 300,
     *                   renderer: function(value, metaData, record, rowIndex, colIndex, store, view) {
     *                       console.log(arguments);
     *                       return value + ' has email: ' + record.get('email');
     *                   }
     *               }
     *           ],
     *         ],
     *         selType: 'spreadsheet'
     *     });
     *
     *     var selectable = grid.getSelectable();  // get selection model
     *
     *     // We will create range of 4 cells.
     *
     *     // Now set the range  and prevent rangeselect event from being fired.
     *     // We can use a simple array when we have no locked columns.
     *     selectable.selectCells([0, 0], [1, 1], true);
     *
     * @param rangeStart {Ext.grid.Location/Number[]} Range starting position. Can be either Cell context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are relative to the outermost grid, encompassing locked *and* normal sides.
     * @param rangeEnd {Ext.grid.Location/Number[]} Range end position. Can be either Cell context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are relative to the outermost grid, encompassing locked *and* normal sides.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectCells: function(rangeStart, rangeEnd, suppressEvent) {
        var me = this,
            view = me.getView(),
            sel;
 
        rangeStart = rangeStart.isGridLocation ? rangeStart.clone() : new Ext.grid.Location(view, {
            record: rangeStart[0],
            column: rangeStart[1]
        });
        rangeEnd = rangeEnd.isGridLocation ? rangeEnd.clone() : new Ext.grid.Location(view, {
            record: rangeEnd[0],
            column: rangeEnd[1]
        });
 
        me.resetSelection(true);
 
        sel = me.getSelection('cells');
        sel.setRangeStart(rangeStart);
        sel.setRangeEnd(rangeEnd);
 
        if (!suppressEvent) {
            me.fireSelectionChange();
        }
    },
 
    /**
     * Select all the data if possible.
     *
     * If {@link #rows} is `true`, then all *records* will be selected.
     *
     * If {@link #cells} is `true`, then all *rendered cells* will be selected.
     *
     * If {@link #columns} is `true`, then all *columns* will be selected.
     *
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectAll: function (suppressEvent) {
        var me = this,
            sel = me.getSelection(),
            doSelect;
 
        if (me.getRows()) {
            sel = me.getSelection('records');
            doSelect = true;
        }
        else if (me.getCells()) {
            sel = me.getSelection('cells');
            doSelect = true;
        }
        else if (me.getColumns()) {
            sel = me.getSelection('cells');
            doSelect = true;
        }
 
        if (doSelect) {
            sel.selectAll(suppressEvent); //this populates the selection with the records 
        }
    },
 
    /**
     * Clears the selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectAll: function (suppressEvent) {
        var sel = this.getSelection();
        
        if (sel && sel.getCount()) {
            sel.clear(suppressEvent);
        }
    },
 
    /**
     * Select one or more rows.
     * @param rows {Ext.data.Model[]} Records to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectRows: function(rows, keepSelection, suppressEvent) {
        var sel = this.getSelection('records');
 
        if (!keepSelection) {
            this.resetSelection(true);
        }
        sel.add(rows, keepSelection, suppressEvent);
    },
 
    isSelected: function(record) {
        // API docs are inherited. 
        return this.isRowSelected(record);
    },
 
    /**
     * Selects a column.
     * @param {Ext.grid.column.Column} column Column to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectColumn: function(column, keepSelection, suppressEvent) {
        var selData = this.getSelection('columns');
 
        if (!selData.isSelected(column)) {
            if (!keepSelection) {
                selData.clear(suppressEvent);
            }
            selData.add(column);
        }
    },
 
    /**
     * Deselects a column.
     * @param {Ext.grid.column.Column} column Column to deselect.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectColumn: function(column, suppressEvent) {
        var selData = this.getSelection();
 
        if (selData && selData.isColumns && selData.isSelected(column)) {
            selData.remove(column, suppressEvent);
        }
    },
 
    destroy: function() {
        var me = this,
            view = me.getView(),
            checkbox = me.checkbox;
 
        if (view && !view.destroying && checkbox) {
            view.unregisterColumn(checkbox, true);
        }
 
        Ext.destroy(me.viewListeners, me.extensible);
        me.callParent();
    },
 
    //------------------------------------------------------------------------- 
 
    privates: {
        /**
         * @property {Object} axesConfigs 
         * Use when converting the extensible config into a SelectionExtender to create its `axes` config to specify which axes it may extend.
         * @private
         */
        axesConfigs: {
            x: 1,
            y: 2,
            xy: 3,
            both: 3,
            "true": 3 // reserved word MUST be quoted when used an a property name 
        },
 
        /**
         * @return {Object}
         * @private
         */
        getViewListeners: function() {
            return {
                columnschanged: 'onColumnsChanged',
                columnmove: 'onColumnMove',
                keyup: {
                    element: 'innerCt',
                    fn: 'onViewKeyUp',
                    scope: this
                },
                scope: this,
                destroyable: true
            };
        },
 
        /**
         * @private
         */
        onViewKeyUp: function(e) {
            var sel = this.getSelection();
 
            // Released the shift key, terminate a keyboard based range selection 
            if (e.keyCode === e.SHIFT && sel && sel.isRows && sel.getRangeSize()) {
                // Copy the drag range into the selected records collection 
                sel.addRange();
            }
        },
 
        /**
         * @private
         */
        refreshSelection: function() {
            if (this.getSelection().isRecords) {
                this.callParent();
            }
            else {
                this.resetSelection();
            }
        },
 
        /**
         * @private
         */
        onColumnsChanged: function() {
            var me = this,
                selData = me.getSelection(),
                view, selectionChanged;
 
            // When columns have changed, we have to deselect *every* cell in the row range because we do not know where the 
            // columns have gone to. 
            if (selData) {
                view = selData.view;
 
                if (selData.isCells) {
                    if (view.visibleColumns().length) {
                        selData.eachCell(function(location) {
                            view.onCellDeselect(location);
                        });
                    } else {
                        me.clearSelections();
                    }
                }
 
                // We have to deselect columns which have been hidden/removed 
                else if (selData.isColumns) {
                    selectionChanged = false;
                    selData.eachColumn(function(column) {
                        if (!column.isVisible() || !view.isAncestor(column)) {
                            me.remove(column);
                            selectionChanged = true;
                        }
                    });
                }
            }
 
            // This event is fired directly from the HeaderContainer before the view updates. 
            // So we have to wait until idle to update the selection UI. 
            // NB: fireSelectionChange calls updateSelectionExtender after firing its event. 
            Ext.on('idle', selectionChanged ? me.fireSelectionChange : me.updateSelectionExtender, me, {
                single: true
            });
        },
 
        // The selection may have acquired or lost contiguity, so the replicator may need enabling or disabling 
        onColumnMove: function() {
            this.updateSelectionExtender();
        },
 
        /**
         * @private
         */
        resetSelection: function(suppressEvent) {
            var sel = this.getSelection();
 
            if (sel) {
                sel.clear(suppressEvent);
            }
        },
 
        /**
         * Plumbing for drag selection of cell range
         * @private
         */
        handleMouseDown: function(view, location) {
            var me = this,
                e = location.event,
                header = location.column,
                viewLocation = view.getNavigationModel().getLocation(),
                isCheckClick = header === me.getCheckbox(),
                sel;
 
            if (!location.cell) {
                return;
            }
 
            // Ignore right click, shift and alt modifiers. 
            // Also ignore touchstart because e cannot drag select using touches and 
            // ignore when actionableMode is true so we can select the text inside an editor 
            if (e.button || e.shiftKey || e.altKey || e.pointerType ==='touch' || (viewLocation && viewLocation.actionable) || !view.shouldSelectItem(e)) {
                return;
            }
 
            if (header) {
                me.mousedownPosition = location.clone();
 
                if (isCheckClick) {
                    me.checkCellClicked = location.cell.element.dom;
                }
 
                // Differentiate between row and cell selections. 
                if (header === me.numbererColumn || isCheckClick || !me.getCells()) {
                    // Enforce rows setting 
                    if (me.getRows()) {
                        // If checkOnly is set, and we're attempting to select a row outside of the checkbox column, reject 
                        if (!isCheckClick && me.checkboxOnly) {
                            return;
                        }
                        sel = me.getSelection('records');
                        if (!e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    } else if (me.getColumns()) {
                        sel = me.getSelection('columns');
                        if (!e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    } else {
                        return false;
                    }
                } else {
                    sel = me.getSelection('cells');
                    sel.clear();
                }
 
                me.lastDragLocation = null;
 
                // Add the listener after the view has potentially been corrected 
                Ext.getBody().on('mouseup', me.onMouseUp, me, { single: true });
 
                // Only begin the drag process if configured to select what they asked for 
                if (sel) {
                    me.mousemoveListener = view.el.on({
                        mousemove: 'onMouseMove',
                        scope: me,
                        delegate: '.' + Ext.grid.cell.Base.prototype.classCls,
                        destroyable: true
                    });
                }
            }
        },
 
        /**
         * Selects range based on mouse movements
         * @param e
         * @param cell
         * @private
         */
        onMouseMove: function(e, target) {
            var me = this,
                view = me.getView(),
                newLocation = me.dragLocation = new Ext.grid.Location(view, e),
                cell = newLocation.getCell(true),
                overColumn = newLocation.column,
                overRecord = newLocation.record,
                overRowIdx = newLocation.recordIndex,
                lastDragLocation = me.lastDragLocation,
                selData, lastOverRecord, lastOverColumn, recChange, colChange;
 
            // Will fire when outside cells (on borders and headers/footers). Ignore this 
            if (!cell) {
                return;
            }
 
            if (lastDragLocation) {
                lastOverRecord = lastDragLocation.record;
                lastOverColumn = lastDragLocation.column;
            }
 
            // When the mousedown happens in a checkcolumn.... 
            if (me.checkCellClicked) {
                selData = me.getSelection('rows');
                selData.setRangeStart(me.getStore().indexOf(overRecord));
                me.checkCellClicked = null;
                return;
            } else {
                selData = me.getSelection();
            }
 
            // Disable until a valid new selection is announced in fireSelectionChange 
            if (me.getExtensible()) {
                me.getExtensible().disable();
            }
 
            if (overColumn) {
                recChange = overRecord !== lastOverRecord;
                colChange = overColumn !== lastOverColumn;
 
                // Initial mousedown was in rownumberer or checkbox overColumn 
                if (selData.isRows || selData.isRecords) {
                    // Only react if we've changed row 
                    if (recChange) {
                        if (lastOverRecord) {
                            selData.setRangeEnd(overRowIdx);
                        } else {
                            selData.setRangeStart(overRowIdx);
                        }
                    }
                }
                // Selecting cells 
                else if (selData.isCells) {
                    // Only react if we've changed row or overColumn 
                    if (recChange || colChange) {
                        if (lastOverRecord) {
                            selData.setRangeEnd(newLocation);
                        } else {
                            selData.setRangeStart(newLocation);
                        }
                    }
                }
                // Selecting columns 
                else if (selData.isColumns) {
                    // Only react if we've changed overColumn 
                    if (colChange) {
                        if (lastOverColumn) {
                            selData.setRangeEnd(newLocation.column);
                        } else {
                            selData.setRangeStart(newLocation.column);
                        }
                    }
                }
 
                // Focus MUST follow the mouse. 
                // Otherwise the focus may scroll out of the rendered range and revert to document 
                if (recChange || colChange) {
                    view.getNavigationModel().setLocation(newLocation);
                }
                me.lastDragLocation = newLocation;
            }
        },
 
        /**
         * Clean up mousemove event
         * @param e
         * @param target
         * @private
         */
        onMouseUp: function(e, target) {
            var me = this,
                view = me.getView(),
                dragLocation = me.dragLocation,
                changedCell = !dragLocation || !dragLocation.equals(me.mousedownPosition),
                location = e.location;
 
            me.checkCellClicked = null;
 
            if (view && !view.destroyed) {
                // If we catch the event before the View sees it and stamps a position in, we need to know where they mouseupped. 
                if (!location) {
                    e.location = new Ext.grid.Location(view, e);
                }
 
                // Disable until a valid new selection is announced in fireSelectionChange unless it's a click 
                if (me.getExtensible() && changedCell) {
                    me.getExtensible().disable();
                }
 
                me.mousemoveListener.destroy();
 
                // Copy the records encompassed by the drag range into the record collection 
                if (me.getSelection().isRows) {
                    me.getSelection().addRange();
                }
 
                // Fire selection change only if we have dragged - if the mouseup position is different from the mousedown position. 
                // If there has been no drag, the click handler will select the single row 
                else if (changedCell) {
                    me.fireSelectionChange();
                }
            }
        },
 
        /**
         * Called when the grid's Navigation model detects navigation events (`mousedown`, `click` and certain `keydown` events).
         * @param {Ext.event.Event} navigateEvent The event which caused navigation.
         * @private
         */
        onNavigate: function(navigateEvent) {
            var me = this,
                selectingRows = me.getRows(),
                selectingCells = me.getCells(),
                selectingColumns = me.getColumns(),
                checkbox = me.getCheckbox(),
                checkboxOnly = me.checkboxOnly,
                mode = me.getMode(),
                location = navigateEvent.to,
                toColumn = location.column,
                record = location.record,
                sel = me.getSelection(),
                ctrlKey = navigateEvent.ctrlKey,
                shiftKey = navigateEvent.shiftKey,
                changedRow, selectionChanged;
 
            // Honour the stopSelection flag which any prior handlers may set. 
            // A SelectionColumn handles its own processing. 
            if (navigateEvent.stopSelection || toColumn === me.checkboxColumn) {
                return;
            }
 
            // *key* navigation 
            if (!navigateEvent.pointerType && navigateEvent.getKey() !== navigateEvent.SPACE) {
                // CTRL/key just navigates, does not select 
                if (ctrlKey) {
                    return;
                }
 
                // If within a row and not going to affect cell or column selection, then ignore. 
                changedRow = !navigateEvent.from || (location.recordIndex !== navigateEvent.from.recordIndex);
                if (!changedRow && !(selectingCells || selectingColumns)) {
                    return;
                }
            }
 
            // Click is the mouseup at the end of a multi-cell/multi-column select swipe; reject. 
            if (sel && (sel.isCells || (sel.isColumns && selectingRows && !(ctrlKey || shiftKey))) && sel.getCount() > 1 && !shiftKey && navigateEvent.type === 'click') {
                return;
            }
 
            // If all selection types are disabled, or it's not a selecting event, return 
            if (!(selectingCells || selectingColumns || selectingRows) || !record || navigateEvent.type === 'mousedown') {
                return;
            }
 
            // Ctrl/A key - Deselect current selection, or select all if no selection 
            if (ctrlKey && navigateEvent.keyCode === navigateEvent.A && mode === 'multi') {
                // No selection, or only one, select all 
                if (!sel || sel.getCount() < 2) {
                    me.selectAll();
                } else {
                    me.deselectAll();
                }
                me.updateHeaderState();
                return;
            }
 
            if (shiftKey && mode === 'multi') {
                // If the event is in one of the row selecting cells, or cell selecting is turned off 
                if (toColumn === me.numbererColumn || toColumn === me.checkColumn || !(selectingCells || selectingColumns) || (sel && (sel.isRows || sel.isRecords))) {
                    if (selectingRows) {
 
                        // If checkOnly is set, and we're attempting to select a row outside of the checkbox column, reject 
                        if (toColumn !== checkbox && checkboxOnly) {
                            return;
                        }
                        // Ensure selection object is of the correct type 
                        sel = me.getSelection('records');
                        // First shift 
                        if (!sel.getRangeSize()) {
                            sel.setRangeStart(me.selectionStart || location.recordIndex);
                        }
                        sel.setRangeEnd(location.recordIndex);
                        sel.addRange(true);
                        selectionChanged = true;
                    }
                }
                // Navigate event in a normal cell 
                else {
                    if (selectingCells) {
                        // Ensure selection object is of the correct type 
                        sel = me.getSelection('cells');
                        // First shift 
                        if (!sel.getRangeSize()) {
                            sel.setRangeStart(navigateEvent.from || new Ext.grid.Location(me.getView(), {
                                record: 0,
                                column: 0
                            }));
                        }
                        sel.setRangeEnd(location);
                        selectionChanged = true;
                    } else if (selectingColumns) {
                        // Ensure selection object is of the correct type 
                        sel = me.getSelection('columns');
                        if (!sel.getCount()) {
                            sel.setRangeStart(toColumn);
                        }
                        sel.setRangeEnd(toColumn);
                        selectionChanged = true;
                    }
                }
            } else {
                me.selectionStart = null;
                if (sel && mode !== 'multi') {
                    sel.clear(true);
                }
 
                // If the event is in one of the row selecting cells, or we have enabled row selection but not column selection 
                // so prioritize selecting rows 
                if (toColumn === me.numbererColumn || toColumn === checkbox || (selectingRows && !selectingCells)) {
 
                    // If checkOnly is set, and we're attempting to select a row outside of the checkbox column, reject 
                    if (toColumn !== checkbox && checkboxOnly) {
                        return;
                    }
 
                    // Ensure selection object is of the correct type 
                    sel = me.getSelection('records');
 
                    if (sel.isSelected(record)) {
                        if (ctrlKey || toColumn === checkbox || me.getDeselectable()) {
                            sel.remove(record);
                            selectionChanged = true;
                        }
                    } else {
                        sel.add(record, ctrlKey || toColumn === checkbox);
                        selectionChanged = true;
                    }
                    me.selectionStart = location.recordIndex;
                }
                // Navigate event in a normal cell 
                else {
                    // Prioritize cell selection over column selection 
                    if (selectingCells) {
                        // Ensure selection object is of the correct type and cleared. 
                        sel = me.getSelection('cells', true);
                        sel.setRangeStart(location);
                        selectionChanged = true;
                    }
                    else if (selectingColumns) {
                        // Ensure selection object is of the correct type 
                        sel = me.getSelection('columns');
 
                        if (ctrlKey) {
                            if (sel.isSelected(toColumn)) {
                                sel.remove(toColumn);
                            } else {
                                sel.add(toColumn);
                            }
                        } else {
                            sel.setRangeStart(toColumn);
                        }
                        selectionChanged = true;
                    }
                }
            }
 
            // If our configuration allowed selection changes, update check header and fire event 
            if (selectionChanged) {
                // Base class reacts to RecordSelection mutating its record Collection 
                // It will fire the events and update the checked header state. 
                if (!sel.isRecords) {
                    me.fireSelectionChange(null, true);
                }
            }
        },
 
        /**
         * Check if given column is currently selected.
         *
         * @param {Ext.grid.column.Column} column
         * @return {Boolean}
         * @private
         */
        isColumnSelected: function(column) {
            var me = this,
                sel = me.getSelection(),
                ret = false;
 
            if (sel && sel.isColumns) {
                ret = sel.isSelected(column);
            }
 
            return ret;
        },
 
        /**
         * Returns true if specified cell within specified view is selected
         *
         * Used in {@link Ext.grid.Row} rendering to decide upon cell UI treatment.
         * @param {Ext.grid.View} view - impactful when locked columns are used
         * @param {Number/Ext.grid.Location/Ext.data.Model} row - The Row index/record or
         * {@link Ext.grid.Location the grid Location} to test.
         * @param {Number} column - Column index to test.
         *
         * @return {Boolean}
         * @private
         */
        isCellSelected: function(row, column) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection(),
                location;
 
            // One argument - it's a Location 
            if (row.isGridLocation) {
                location = row;
                row = location.recordIndex;
                column = location.column;
            }
            // Row and column passed - convert to Location 
            else {
            }
 
            if (sel) {
                if (sel.isColumns) {
                    if (typeof column === 'number') {
                        column = view.getVisibleColumns()[column];
                    }
                    return sel.isSelected(column);
                }
 
                if (sel.isCells) {
                    location = new Ext.grid.Location(view, {
                        record: row,
                        column: column
                    });
                    return sel.isSelected(location);
                }
 
                // We're selecting records or rows. 
                // The cell is selected if the record is. 
                return sel.isSelected(row);
            }
 
            return false;
        },
 
        /**
         * @private
         */
        updateSelection: function(selection, oldSelection) {
            var view = this.getView();
 
            // Destroy old selection. 
            Ext.destroy(oldSelection);
 
            // Update the UI to match the new selection 
            if (selection && selection.getCount()) {
                view = selection.view;
 
                // Rows; update each selection row 
                if (selection.isRows) {
                    selection.eachRow(view.onRowSelect, view);
                }
                // Columns; update the selection columns for all rows 
                else if (selection.isColumns) {
                    selection.eachCell(view.onCellSelect, view);
                }
                // Cells; update each selection cell 
                else if (selection.isCells) {
                    selection.eachCell(view.onCellSelect, view);
                }
            }
        },
 
        /**
         * Show/hide the extra column headers depending upon rowSelection.
         * @private
         */
        updateRows: function(rows) {
            var me = this,
                sel = me.getSelection(),
                checkHeader = me.getCheckbox();
 
            if (checkHeader) {
                checkHeader.setHidden(!rows);
            }
            if (!rows && sel && sel.isRows) {
                sel.clear();
            }
        },
 
        /**
         * Enable/disable the HeaderContainer's sortOnClick in line with column select on
         * column click.
         * @private
         */
        updateColumns: function(columns) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection();
 
            view.getHeaderContainer().setSortable(!columns);
 
            if (!columns && sel && sel.isColumns) {
                sel.clear();
                me.fireSelectionChange();
            }
            view.toggleCls(me.columnSelectCls, !!columns);
        },
 
        /**
         * @private
         */
        updateCells: function(cells) {
            var me = this,
                sel = me.getSelection();
 
            if (!cells && sel && sel.isCells) {
                sel.clear();
                me.fireSelectionChange();
            }
        },
 
        updateMode: function(mode) {
            // If multi, we can use drag or not, so revert to initial value 
            if (mode === 'multi') {
                this.setDrag(this.getInitialConfig().drag);
            }
            // With 'simple' or 'single', drag makes no sense 
            else if (!this.isConfiguring) {
                this.setDrag(false);
            }
        },
 
        /**
         * @private
         * @param {Ext.data.Model[]} records. *ONLY* passed if called from the base class's
         * onCollectionAdd/Remove observers on the *record* collection.
         * @param {Boolean} selecting. *ONLY* passed if called from the base class's
         * onCollectionAdd/Remove observers on the *record* collection.
         */
        fireSelectionChange: function(records, selecting) {
            var me = this,
                view = me.getView(),
                selection = me.getSelection();
 
            // Inform selection object that we're done 
            me.updateSelectionExtender();
 
            // Our own event 
            me.fireEvent('selectionchange', view,  me.getSelection());
 
            // Fire Grid's selectionchange event. 
            // Only pass records if the selection type can yield them 
            view.fireEvent('selectionchange', view, selection.isRecords ? records : (selection.isCells ? selection.getRecords() : null), selecting, me.getSelection());
        },
 
        updateSelectionExtender: function() {
            var sel = this.getSelection();
            if (sel) {
                sel.onSelectionFinish();
            }
        },
 
        /**
         * Called when a selection has been made. The selection object's onSelectionFinish calls back into this.
         * @param {Ext.dataview.selection.Selection} sel The selection object specific to
         * the selection performed.
         * @param {Ext.grid.Location} [firstCell] The left/top most selected cell.
         * Will be undefined if the selection is clear.
         * @param {Ext.grid.Location} [lastCell] The bottom/right most selected cell.
         * Will be undefined if the selection is clear.
         * @private
         */
        onSelectionFinish: function(sel, firstCell, lastCell) {
            var extensible = this.getExtensible();
 
            if (extensible) {
                extensible.setHandle(firstCell, lastCell);
            }
        },
 
        applyExtensible: function(extensible, oldExtensible) {
            var me = this,
                axes;
 
            // if extensible is false/null we should return undefined so the value 
            // does not get set and we don't call updateExtensible 
            if (!extensible) {
                return undefined;
            }
 
            if (extensible === true || typeof extensible === 'string') {
                axes = me.axesConfigs[extensible];
 
                // if we already have an extensible, just update it's config 
                if (oldExtensible) {
                    oldExtensible.setAxes(axes);
                    return oldExtensible;
                }
 
                extensible = {
                    axes: axes
                };
            } else {
                extensible = Ext.Object.chain(extensible); // don't mutate the user's config 
            }
 
            extensible.view = me.getView();
 
            // if this wasn't a simple axes update, destroy the old extensible 
            // so we don't end up with multiple extensibles on the view. 
            if (oldExtensible) {
                oldExtensible.destroy();
            }
 
            return new Ext.grid.selection.SelectionExtender(extensible);
        },
 
        applyCheckbox: function(checkbox) {
            var me = this;
 
            if (checkbox) {
                me.checkboxOnly = checkbox === 'only';
                me.checkboxColumn = checkbox = Ext.create(me.createCheckboxColumn(me.getCheckboxDefaults()));
            }
 
            return checkbox;
        },
 
        updateCheckbox: function(checkbox, oldCheckbox) {
            var me = this,
                view;
 
            if (!me.isConfiguring) {
                view = me.getView();
                if (oldCheckbox) {
                    view.unregisterColumn(oldCheckbox, true);
                }
 
                if (checkbox) {
                    view.registerColumn(checkbox);
                }
            }
        },
 
        applyView: function(view) {
            // In a locking assembly, we talk to the owner 
            return view.ownerGrid;
        },
 
        /**
         * Called when the SelectionExtender has the mouse released.
         * @param {Object} extension An object describing the type and size of extension.
         * @param {String} extension.type `"rows"` or `"columns"`
         * @param {Ext.grid.Location} extension.start The start (top left) cell of the extension area.
         * @param {Ext.grid.Location} extension.end The end (bottom right) cell of the extension area.
         * @param {number} [extension.columns] The number of columns extended (-ve means on the left side).
         * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
         * @private
         */
        extendSelection: function(extension) {
            var me = this,
                view = me.getView(),
                sel = me.getSelection();
 
            // Announce that the selection is to be extended, and if no objections, extend it 
            if (view.fireEvent('beforeselectionextend', view, sel, extension) !== false) {
                sel.extendRange(extension);
 
                // Base class reacts to RowSelection mutating its record Collection 
                // It will fire the events. 
                if (!sel.isRows) {
                    me.fireSelectionChange();
                }
            }
        },
 
        /**
         * @private
         */
        onIdChanged: function(store, rec, oldId, newId) {
            var sel = this.getSelection();
 
            if (sel && sel.isRecords) {
                sel.getSelected().updateKey(rec, oldId);
            }
        },
 
        /**
         * @private
         */
        onSelectionStoreAdd: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        },
 
        /**
         * @private
         */
        onSelectionStoreClear: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        },
 
        /**
         * @private
         */
        onSelectionStoreLoad: function() {
            this.callParent(arguments);
            this.updateHeaderState();
        }
    }
}, function (GridModel) {
    var RowNumberer = Ext.ClassManager.get('Ext.grid.column.RowNumberer');
    if (RowNumberer) {
        // This class adds the e-resize cursor on hover to indicate availability of selection 
        GridModel.prototype.rowNumbererCellCls =
            Ext.grid.column.RowNumberer.prototype.cellCls + ' ' + Ext.baseCSSPrefix + 'selmodel-row-numberer-cell';
    }
});