/**
 * A class which encapsulates a range of columns defining a selection in a grid.
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Columns', {
    extend: 'Ext.grid.selection.Selection',

    type: 'columns',

    /**
     * @property {Boolean} isColumns
     * This property indicates the this selection represents selected columns.
     * @readonly
     */
    isColumns: true,

    //-------------------------------------------------------------------------
    // Base Selection API

    clone: function() {
        var me = this,
            result = new me.self(me.view),
            columns = me.selectedColumns;

        if (columns) {
            result.selectedColumns = Ext.Array.slice(columns);
        }

        return result;
    },

    eachRow: function(fn, scope) {
        var columns = this.selectedColumns;

        if (columns && columns.length) {
            this.view.dataSource.each(fn, scope || this);
        }
    },

    eachColumn: function(fn, scope) {
        var me = this,
            view = me.view,
            columns = me.selectedColumns,
            len,
            i,
            context = new Ext.grid.CellContext(view);

        if (columns) {
            len = columns.length;

            for (i = 0; i < len; i++) {
                context.setColumn(columns[i]);

                if (fn.call(scope || me, context.column, context.colIdx) === false) {
                    return false;
                }
            }
        }
    },

    eachCell: function(fn, scope) {
        var me = this,
            view = me.view,
            columns = me.selectedColumns,
            len,
            i,
            context = new Ext.grid.CellContext(view);

        if (columns) {
            len = columns.length;

            // Use Store#each instead of copying the entire dataset into an array
            // and iterating that.
            view.dataSource.each(function(record) {
                context.setRow(record);

                for (i = 0; i < len; i++) {
                    context.setColumn(columns[i]);

                    if (fn.call(scope || me, context, context.colIdx, context.rowIdx) === false) {
                        return false;
                    }
                }
            });
        }
    },

    //-------------------------------------------------------------------------
    // Methods unique to this type of Selection

    /**
     * Returns `true` if the passed {@link Ext.grid.column.Column column} is selected.
     * @param {Ext.grid.column.Column} column The column to test.
     * @return {Boolean} `true` if the passed {@link Ext.grid.column.Column column} is selected.
     */
    contains: function(column) {
        var selectedColumns = this.selectedColumns;

        if (column && column.isColumn && selectedColumns && selectedColumns.length) {
            return Ext.Array.contains(selectedColumns, column);
        }

        return false;
    },

    /**
     * Returns the number of columns selected.
     * @return {Number} The number of columns selected.
     */
    getCount: function() {
        var selectedColumns = this.selectedColumns;

        return selectedColumns ? selectedColumns.length : 0;
    },

    /**
     * Returns the columns selected.
     * @return {Ext.grid.column.Column[]} The columns selected.
     */
    getColumns: function() {
        return this.selectedColumns || [];
    },

    //-------------------------------------------------------------------------

    privates: {
        /**
         * Adds the passed Column to the selection.
         * @param {Ext.grid.column.Column} column
         * @private
         */
        add: function(column) {
            //<debug>
            if (!column.isColumn) {
                Ext.raise('Column selection must be passed a grid Column header object');
            }
            //</debug>

            Ext.Array.include((this.selectedColumns || (this.selectedColumns = [])), column);
            this.refreshColumns(column);
        },

        /**
         * @private
         */
        clear: function() {
            var me = this,
                prevSelection = me.selectedColumns;

            if (prevSelection && prevSelection.length) {
                me.selectedColumns = [];
                me.refreshColumns.apply(me, prevSelection);
            }

            me.setRangeStart(null);
        },

        setRangeStart: function(startColumn) {
            var me = this,
                prevSelection = me.getColumns();

            me.startColumn = startColumn;

            if (startColumn != null) {
                me.selectedColumns = [startColumn];
                prevSelection.push(startColumn);
                me.refreshColumns.apply(me, prevSelection);
            }
        },

        setRangeEnd: function(endColumn) {
            var me = this,
                prevSelection = me.getColumns(),
                colManager = this.view.ownerGrid.getVisibleColumnManager(),
                columns = colManager.getColumns(),
                start = colManager.indexOf(me.startColumn),
                end = colManager.indexOf(endColumn),
                i;

            // Allow looping through columns
            if (end < start) {
                i = start;
                start = end;
                end = i;
            }

            me.selectedColumns = [];

            for (i = start; i <= end; i++) {
                me.selectedColumns.push(columns[i]);
                prevSelection.push(columns[i]);
            }

            me.refreshColumns.apply(me, prevSelection);
        },

        /**
         * @return {Boolean}
         * @private
         */
        isAllSelected: function() {
            var selectedColumns = this.selectedColumns;

            // All selected means all columns, across both views if we are in a locking assembly.
            // eslint-disable-next-line max-len
            return selectedColumns && selectedColumns.length === this.view.ownerGrid.getVisibleColumnManager().getColumns().length;
        },

        /**
         * @private
         */
        refreshColumns: function(column) {
            var me = this,
                view = me.view,
                rows = view.all,
                rowIdx,
                columns = arguments,
                len = columns.length,
                colIdx,
                cellContext = new Ext.grid.CellContext(view),
                selected = [];

            if (view.rendered) {
                for (colIdx = 0; colIdx < len; colIdx++) {
                    selected[colIdx] = me.contains(columns[colIdx]);
                }

                for (rowIdx = rows.startIndex; rowIdx <= rows.endIndex; rowIdx++) {
                    cellContext.setRow(rowIdx);

                    for (colIdx = 0; colIdx < len; colIdx++) {
                        // Note colIdx is not the column's visible index.
                        // setColumn must be passed the column object
                        cellContext.setColumn(columns[colIdx]);

                        if (selected[colIdx]) {
                            view.onCellSelect(cellContext);
                        }
                        else {
                            view.onCellDeselect(cellContext);
                        }
                    }
                }
            }
        },

        /**
         * Removes the passed Column from the selection.
         * @param {Ext.grid.column.Column} column
         * @private
         */
        remove: function(column) {
            //<debug>
            if (!column.isColumn) {
                Ext.raise('Column selection must be passed a grid Column header object');
            }
            //</debug>

            if (this.selectedColumns) {
                Ext.Array.remove(this.selectedColumns, column);

                // Might be being called because of column removal/hiding.
                // In which case the view will have selected cells removed, so no refresh needed.
                if (column.getView() && column.isVisible()) {
                    this.refreshColumns(column);
                }
            }
        },

        /**
         * @private
         */
        selectAll: function() {
            var me = this;

            me.clear();
            me.selectedColumns = me.view.getSelectionModel().lastContiguousColumnRange =
                me.view.getVisibleColumnManager().getColumns();
            me.refreshColumns.apply(me, me.selectedColumns);
        },

        extendRange: function(extensionVector) {
            var me = this,
                columns = me.view.getVisibleColumnManager().getColumns(),
                i;

            for (i = extensionVector.start.colIdx; i <= extensionVector.end.colIdx; i++) {
                me.add(columns[i]);
            }
        },

        reduceRange: function(extensionVector) {
            var me = this,
                columns = me.view.getVisibleColumnManager().getColumns(),
                startIdx = extensionVector.start.colIdx,
                endIdx = extensionVector.end.colIdx,
                reduceTo = Math.abs(startIdx - endIdx) + 1,
                diff = me.selectedColumns.length - reduceTo,
                i;

            for (i = diff; i > 0; i--) {
                me.remove(columns[endIdx + i]);
            }
        },

        onSelectionFinish: function() {
            var me = this,
                range = me.getContiguousSelection();

            if (range) {
                me.view.getSelectionModel().onSelectionFinish(
                    me,
                    new Ext.grid.CellContext(me.view).setPosition(0, range[0]),
                    new Ext.grid.CellContext(me.view).setPosition(
                        me.view.dataSource.getCount() - 1, range[1]
                    )
                );
            }
            else {
                me.view.getSelectionModel().onSelectionFinish(me);
            }
        },

        /**
         * @return {Array} `[startColumn, endColumn]` if the selection represents
         * a visually contiguous set of columns.
         * The SelectionReplicator is only enabled if there is a contiguous block.
         * @private
         */
        getContiguousSelection: function() {
            var selection = Ext.Array.sort(this.getColumns(), function(c1, c2) {
                    // Use index *in ownerGrid* so that a locking assembly
                    // can order columns correctly
                    return c1.getView().ownerGrid.getVisibleColumnManager().indexOf(c1) -
                           c2.getView().ownerGrid.getVisibleColumnManager().indexOf(c2);
                }),
                len = selection.length,
                i;

            if (len) {
                for (i = 1; i < len; i++) {
                    if (selection[i].getVisibleIndex() !== selection[i - 1].getVisibleIndex() + 1) {
                        return false;
                    }
                }

                return [selection[0], selection[len - 1]];
            }
        }
    }
});