/**
 * A class which encapsulates a range of cells defining a selection in a grid.
 *
 * Note that when range start and end points are represented by an array, the
 * order is traditional `x, y` order, that is column index followed by row index.
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Cells', {
    extend: 'Ext.grid.selection.Selection',
 
    type: 'cells',
 
    /**
     * @property {Boolean} isCells
     * This property indicates the this selection represents selected cells.
     * @readonly
     */
    isCells: true,
 
    //-------------------------------------------------------------------------
    // Base Selection API
 
    clone: function() {
        var me = this,
            result = new me.self(me.view);
 
        if (me.startCell) {
            result.startCell = me.startCell.clone();
            result.endCell = me.endCell.clone();
        }
 
        return result;
    },
 
    /**
     * Returns `true` if the passed {@link Ext.grid.CellContext cell context} is selected.
     * @param {Ext.grid.CellContext} cellContext The cell context to test.
     * @return {Boolean} `true` if the passed {@link Ext.grid.CellContext cell context} is selected.
     */
    contains: function(cellContext) {
        var range;
 
        if (!cellContext || !cellContext.isCellContext) {
            return false;
        }
 
        if (this.startCell) {
            // get start and end rows in the range
            range = this.getRowRange();
 
            if (cellContext.rowIdx >= range[0] && cellContext.rowIdx <= range[1]) {
                // get start and end columns in the range
                range = this.getColumnRange();
 
                return (cellContext.colIdx >= range[0] && cellContext.colIdx <= range[1]);
            }
        }
 
        return false;
    },
 
    eachRow: function(fn, scope) {
        var me = this,
            rowRange = me.getRowRange(),
            context = new Ext.grid.CellContext(me.view),
            rowIdx;
 
        for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
            context.setRow(rowIdx);
 
            if (fn.call(scope || me, context.record) === false) {
                return;
            }
        }
    },
 
    eachColumn: function(fn, scope) {
        var me = this,
            colRange = me.getColumnRange(),
            context = new Ext.grid.CellContext(me.view),
            colIdx;
 
        for (colIdx = colRange[0]; colIdx <= colRange[1]; colIdx++) {
            context.setColumn(colIdx);
 
            if (fn.call(scope || me, context.column, colIdx) === false) {
                return;
            }
        }
    },
 
    eachCell: function(fn, scope) {
        var me = this,
            rowRange = me.getRowRange(),
            colRange = me.getColumnRange(),
            context = new Ext.grid.CellContext(me.view),
            rowIdx, colIdx;
 
        for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
            context.setRow(rowIdx);
 
            for (colIdx = colRange[0]; colIdx <= colRange[1]; colIdx++) {
                context.setColumn(colIdx);
 
                if (fn.call(scope || me, context, colIdx, rowIdx) === false) {
                    return;
                }
            }
        }
    },
 
    /**
     * @return {Number} The row index of the first row in the range or zero if no range.
     */
    getFirstRowIndex: function() {
        return this.startCell ? Math.min(this.startCell.rowIdx, this.endCell.rowIdx) : 0;
    },
 
    /**
     * @return {Number} The row index of the last row in the range or -1 if no range.
     */
    getLastRowIndex: function() {
        return this.startCell ? Math.max(this.startCell.rowIdx, this.endCell.rowIdx) : -1;
    },
 
    /**
     * @return {Number} The column index of the first column in the range or zero if no range.
     */
    getFirstColumnIndex: function() {
        return this.startCell ? Math.min(this.startCell.colIdx, this.endCell.colIdx) : 0;
    },
 
    /**
     * @return {Number} The column index of the last column in the range or -1 if no range.
     */
    getLastColumnIndex: function() {
        return this.startCell ? Math.max(this.startCell.colIdx, this.endCell.colIdx) : -1;
    },
 
    //-------------------------------------------------------------------------
 
    privates: {
        /**
         * @private
         */
        clear: function() {
            var me = this,
                view = me.view;
 
            if (view.getVisibleColumnManager().getColumns().length) {
                me.eachCell(function(cellContext) {
                    view.onCellDeselect(cellContext);
                });
            }
 
            me.startCell = me.endCell = null;
        },
 
        /**
         * Used during drag/shift+downarrow range selection on start.
         * @param {Ext.grid.CellContext} startCell The start cell of the cell drag selection.
         * @param {Ext.grid.CellContext} endCell The end cell of the cell drag selection.
         * @private
         */
        setRangeStart: function(startCell, endCell) {
            // Must clone them. Users might use one instance and reconfigure it to navigate.
            this.startCell = (this.endCell = startCell.clone()).clone();
            this.view.onCellSelect(startCell);
        },
 
        /**
         * Used during drag/shift+downarrow range selection on drag.
         * @param {Ext.grid.CellContext} endCell The end cell of the cell drag selection.
         * @private
         */
        setRangeEnd: function(endCell) {
            var me = this,
                view = me.view,
                rows = view.all,
                cell = new Ext.grid.CellContext(view),
                maxColIdx = view.getVisibleColumnManager().getColumns().length - 1,
                range, lastRange, rowStart, rowEnd, colStart, colEnd, rowIdx, colIdx;
 
            me.endCell = endCell.clone();
            range = me.getRange();
            lastRange = me.lastRange || range;
 
            rowStart = Math.max(Math.min(range[0][1], lastRange[0][1]), rows.startIndex);
            rowEnd = Math.min(Math.max(range[1][1], lastRange[1][1]), rows.endIndex);
 
            colStart = Math.min(range[0][0], lastRange[0][0]);
            colEnd = Math.min(Math.max(range[1][0], lastRange[1][0]), maxColIdx);
 
            // Loop through the union of last range and current range
            for (rowIdx = rowStart; rowIdx <= rowEnd; rowIdx++) {
                for (colIdx = colStart; colIdx <= colEnd; colIdx++) {
                    cell.setPosition(rowIdx, colIdx);
 
                    // If we are outside the current range, deselect
                    if (rowIdx < range[0][1] || rowIdx > range[1][1] || colIdx < range[0][0] ||
                        colIdx > range[1][0]) {
                        view.onCellDeselect(cell);
                    }
                    else {
                        view.onCellSelect(cell);
                    }
                }
            }
 
            me.lastRange = range;
        },
 
        extendRange: function(extensionVector) {
            var me = this,
                newEndCell;
 
            if (extensionVector[extensionVector.type] < 0) {
                newEndCell = me.endCell.clone().setPosition(
                    me.getLastRowIndex(), me.getLastColumnIndex()
                );
                
                me.startCell = extensionVector.start.clone();
                me.setRangeEnd(newEndCell);
                me.view.getNavigationModel().setPosition(extensionVector.start);
            }
            else {
                me.startCell = me.startCell.setPosition(
                    me.getFirstRowIndex(), me.getFirstColumnIndex()
                );
                
                me.setRangeEnd(extensionVector.end);
                me.view.getNavigationModel().setPosition(extensionVector.end);
            }
        },
 
        reduceRange: function(extensionVector) {
            var me = this,
                newEndCell;
 
            if (extensionVector[extensionVector.type] < 0) {
                newEndCell = extensionVector.end.clone();
                me.startCell = extensionVector.start.clone();
                me.setRangeEnd(newEndCell);
                me.view.getNavigationModel().setPosition(extensionVector.start);
            }
        },
 
        /**
         * Returns the `[[x, y],[x,y]]` coordinates in top-left to bottom-right order
         * of the current selection.
         *
         * If no selection, returns [[0, 0],[-1, -1]] so that an incrementing iteration
         * will not execute.
         *
         * @return {Number[][]} 
         * @private
         */
        getRange: function() {
            return [[this.getFirstColumnIndex(), this.getFirstRowIndex()],
                    [this.getLastColumnIndex(), this.getLastRowIndex()]];
        },
 
        /**
         * Returns the size of the selection rectangle.
         * @return {Number} 
         * @private
         */
        getRangeSize: function() {
            return this.getCount();
        },
 
        /**
         * Returns the number of cells selected.
         * @return {Number} The nuimber of cells selected
         * @private
         */
        getCount: function() {
            var range = this.getRange();
 
            return (range[1][0] - range[0][0] + 1) * (range[1][1] - range[0][1] + 1);
        },
 
        /**
         * @private
         */
        selectAll: function() {
            var me = this,
                view = me.view;
 
            me.clear();
            me.setRangeStart(new Ext.grid.CellContext(view).setPosition(0, 0));
            me.setRangeEnd(new Ext.grid.CellContext(view).setPosition(
                view.dataSource.getCount() - 1,
                view.getVisibleColumnManager().getColumns().length - 1
            ));
        },
 
        /**
         * @return {Boolean} 
         * @private
         */
        isAllSelected: function() {
            var start = this.rangeStart,
                end = this.rangeEnd;
 
            // All selected only if we encompass the entire store and every visible column
            if (start) {
                if (!start.colIdx && !start.rowIdx) {
                    // eslint-disable-next-line max-len
                    return end.colIdx === end.view.getVisibleColumnManager().getColumns().length - 1 && end.rowIdx === end.view.dataSource.getCount - 1;
                }
            }
 
            return false;
        },
 
        /**
         * @return {Number[]} The column range which encapsulates the range.
         * @private
         */
        getColumnRange: function() {
            return [this.getFirstColumnIndex(), this.getLastColumnIndex()];
        },
 
        /**
         * @private
         * Called through {@link Ext.grid.selection.SpreadsheetModel#getLastSelected}
         * by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection` property.
         * It should yield the last record selected.
         */
        getLastSelected: function() {
            return this.view.dataSource.getAt(this.endCell.rowIdx);
        },
 
        /**
         * Returns the row range which encapsulates the range - the view range that needs
         * updating.
         * @return {Number[]} 
         * @private
         */
        getRowRange: function() {
            return [this.getFirstRowIndex(), this.getLastRowIndex()];
        },
 
        onSelectionFinish: function() {
            var me = this;
 
            if (me.getCount()) {
                me.view.getSelectionModel().onSelectionFinish(
                    me,
                    new Ext.grid.CellContext(me.view).setPosition(
                        me.getFirstRowIndex(), me.getFirstColumnIndex()
                    ),
                    new Ext.grid.CellContext(me.view).setPosition(
                        me.getLastRowIndex(), me.getLastColumnIndex()
                    )
                );
            }
            else {
                me.view.getSelectionModel().onSelectionFinish(me);
            }
        }
    }
});