import { isDefined } from '@slickgrid-universal/utils';
import { SlickEvent, SlickEventData, SlickEventHandler, SlickRange } from '../core/index.js';
import { SlickCellRangeSelector } from './slickCellRangeSelector.js';
export class SlickHybridSelectionModel {
    constructor(options) {
        // hybrid selection model is CellSelectionModel except when selecting
        // specific columns, which behave as RowSelectionModel
        // --
        // public API
        this.pluginName = 'HybridSelectionModel';
        // --
        // protected props
        this._cachedPageRowCount = 0;
        this._prevKeyDown = '';
        this._ranges = [];
        this._activeSelectionIsRow = false;
        this._defaults = {
            selectActiveCell: true,
            selectActiveRow: true,
            dragToSelect: false,
            autoScrollWhenDrag: true,
            handleRowMoveManagerColumn: true, // Row Selection on RowMoveManager column
            rowSelectColumnIds: [], // Row Selection on these columns
            rowSelectOverride: undefined, // function to toggle Row Selection Models
            cellRangeSelector: undefined,
            selectionType: 'mixed',
        };
        this.onSelectedRangesChanged = new SlickEvent('onSelectedRangesChanged');
        this._eventHandler = new SlickEventHandler();
        this._options = { ...this._defaults, ...options };
    }
    get eventHandler() {
        return this._eventHandler;
    }
    get gridOptions() {
        return this._grid?.getOptions();
    }
    set activeSelectionIsRow(value) {
        this._activeSelectionIsRow = value;
    }
    // Region: Setup
    // -----------------------------------------------------------------------------
    init(grid) {
        this._grid = grid;
        this._options = { ...this._defaults, ...this._options };
        this._selector = this._options.cellRangeSelector;
        if (this._options.selectionType === 'cell') {
            this._activeSelectionIsRow = false;
        }
        else if (this._options.selectionType === 'row') {
            this._activeSelectionIsRow = true;
        }
        // add PubSub instance to all SlickEvent
        const pubSub = grid.getPubSubService();
        if (pubSub) {
            this.onSelectedRangesChanged.setPubSubService(pubSub);
        }
        if (!this._selector && (!this._activeSelectionIsRow || (this._activeSelectionIsRow && this._options.dragToSelect))) {
            this._selector = new SlickCellRangeSelector(this._options?.dragToSelect
                ? {
                    selectionCss: { border: 'none' },
                    autoScroll: this._options?.autoScrollWhenDrag,
                }
                : {
                    selectionCss: { border: '2px solid gray' },
                    copyToSelectionCss: { border: '2px solid purple' },
                });
            this._options.cellRangeSelector = this._selector;
        }
        if (grid.hasDataView()) {
            this._dataView = grid.getData();
        }
        this._eventHandler
            .subscribe(this._grid.onActiveCellChanged, this.handleActiveCellChange.bind(this))
            .subscribe(this._grid.onClick, this.handleClick.bind(this))
            .subscribe(this._grid.onKeyDown, this.handleKeyDown.bind(this));
        if (this._selector) {
            this._grid.registerPlugin(this._selector);
            this._eventHandler
                .subscribe(this._selector.onCellRangeSelecting, (e, args) => this.handleCellRangeSelected(e, { ...args, caller: 'onCellRangeSelecting' }))
                .subscribe(this._selector.onCellRangeSelected, (e, args) => this.handleCellRangeSelected(e, { ...args, caller: 'onCellRangeSelected' }))
                .subscribe(this._selector.onBeforeCellRangeSelected, this.handleBeforeCellRangeSelected.bind(this));
        }
        // not entirely sure why but it seems that we need to invalidate & rerender all the cells to avoid seeing blank cells
        this._grid.invalidate();
    }
    destroy() {
        this.dispose();
    }
    dispose() {
        this._eventHandler.unsubscribeAll();
        if (this._selector) {
            this._grid?.unregisterPlugin(this._selector);
        }
        this._selector?.destroy();
        this._selector?.dispose();
    }
    getOptions() {
        return this._options;
    }
    setOptions(options) {
        this._options = { ...this._options, ...options };
    }
    // Region: CellSelectionModel Members
    // -----------------------------------------------------------------------------
    removeInvalidRanges(ranges) {
        const result = [];
        for (let i = 0; i < ranges.length; i++) {
            const r = ranges[i];
            if (this._grid.canCellBeSelected(r.fromRow, r.fromCell) && this._grid.canCellBeSelected(r.toRow, r.toCell)) {
                result.push(r);
            }
        }
        return result;
    }
    rangesAreEqual(range1, range2) {
        let areDifferent = range1.length !== range2.length;
        if (!areDifferent) {
            for (let i = 0; i < range1.length; i++) {
                if (range1[i].fromCell !== range2[i].fromCell ||
                    range1[i].fromRow !== range2[i].fromRow ||
                    range1[i].toCell !== range2[i].toCell ||
                    range1[i].toRow !== range2[i].toRow) {
                    areDifferent = true;
                    break;
                }
            }
        }
        return !areDifferent;
    }
    // Region: RowSelectionModel Members
    // -----------------------------------------------------------------------------
    rangesToRows(ranges) {
        const rows = [];
        for (let i = 0; i < ranges.length; i++) {
            for (let j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
                rows.push(j);
            }
        }
        return rows;
    }
    rowsToRanges(rows) {
        const ranges = [];
        const lastCell = this._grid.getColumns().length - 1;
        rows.forEach((row) => ranges.push(new SlickRange(row, 0, row, lastCell)));
        return ranges;
    }
    getRowsRange(from, to) {
        let i;
        const rows = [];
        for (i = from; i <= to; i++) {
            rows.push(i);
        }
        for (i = to; i < from; i++) {
            rows.push(i);
        }
        return rows;
    }
    getCellRangeSelector() {
        return this._selector;
    }
    getSelectedRanges() {
        return this._ranges;
    }
    getSelectedRows() {
        return this.rangesToRows(this._ranges);
    }
    setSelectedRows(rows) {
        this.setSelectedRanges(this.rowsToRanges(rows), 'SlickHybridSelectionModel.setSelectedRows', '');
    }
    // Region: Shared Members
    // -----------------------------------------------------------------------------
    /** Provide a way to force a recalculation of page row count (for example on grid resize) */
    resetPageRowCount() {
        this._cachedPageRowCount = 0;
    }
    setSelectedRanges(ranges, caller = 'SlickHybridSelectionModel.setSelectedRanges', selectionMode = '') {
        // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
        if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) {
            return;
        }
        if (this._activeSelectionIsRow) {
            this._ranges = ranges;
            // provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
            // that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
            const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode } }), this._ranges);
            this.onSelectedRangesChanged.notify(this._ranges, eventData);
        }
        else {
            // if range has not changed, don't fire onSelectedRangesChanged
            const rangeHasChanged = !this.rangesAreEqual(this._ranges, ranges);
            this._ranges = this.removeInvalidRanges(ranges);
            if (rangeHasChanged) {
                // provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
                // that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
                const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode, addDragHandle: true } }), this._ranges);
                this.onSelectedRangesChanged.notify(this._ranges, eventData);
            }
        }
    }
    currentSelectionModeIsRow() {
        return this._activeSelectionIsRow;
    }
    refreshSelections() {
        if (this._activeSelectionIsRow) {
            this.setSelectedRows(this.getSelectedRows());
        }
        else {
            this.setSelectedRanges(this.getSelectedRanges(), undefined, '');
        }
    }
    rowSelectionModelIsActive(data) {
        if (this._options.selectionType === 'cell') {
            return false;
        }
        else if (this._options.selectionType === 'row') {
            return true;
        }
        // work out required selection mode
        if (this._options?.rowSelectOverride) {
            return this._options?.rowSelectOverride(data, this, this._grid);
        }
        if (!isDefined(data.cell)) {
            return false;
        }
        if (this.gridOptions.enableRowMoveManager && this.isHandlerColumn(data.cell)) {
            return true;
        }
        const targetColumn = this._grid.getVisibleColumns()[data.cell];
        if (targetColumn) {
            return this._options?.rowSelectColumnIds?.includes(`${targetColumn.id}`) || false;
        }
        return false;
    }
    handleActiveCellChange(_e, args) {
        this._prevSelectedRow = undefined;
        const isCellDefined = isDefined(args.cell);
        const isRowDefined = isDefined(args.row);
        this._activeSelectionIsRow = this.rowSelectionModelIsActive(args);
        if (this._activeSelectionIsRow) {
            if (this._options?.selectActiveRow && isRowDefined) {
                this.setSelectedRanges([new SlickRange(args.row, 0, args.row, this._grid.getColumns().length - 1)], undefined, '');
            }
        }
        else {
            if (this._options?.selectActiveCell && isRowDefined && isCellDefined) {
                // if any row selections are visible, leave them untouched unless `selectActiveCell` is enabled
                if (this._options.selectActiveRow) {
                    this.setSelectedRanges([new SlickRange(args.row, args.cell)], undefined, '');
                }
            }
            else if (!this._options?.selectActiveCell || (!isRowDefined && !isCellDefined)) {
                // clear the previous selection once the cell changes
                this.setSelectedRanges([], undefined, '');
            }
        }
    }
    isKeyAllowed(key, isShiftKeyPressed) {
        return [
            'ArrowLeft',
            'ArrowRight',
            'ArrowUp',
            'ArrowDown',
            'PageDown',
            'PageUp',
            'Home',
            'End',
            ...(!isShiftKeyPressed ? ['a', 'A'] : []),
        ].some((k) => k === key);
    }
    handleKeyDown(e) {
        if (!this._activeSelectionIsRow) {
            let ranges;
            let last;
            const colLn = this._grid.getColumns().length;
            const active = this._grid.getActiveCell();
            let dataLn = 0;
            if (this._dataView && 'getPagingInfo' in this._dataView) {
                dataLn = this._dataView?.getPagingInfo().pageSize || this._dataView.getLength();
            }
            else {
                dataLn = this._grid.getDataLength();
            }
            if (active && (e.shiftKey || e.ctrlKey) && !e.altKey && this.isKeyAllowed(e.key, e.shiftKey)) {
                ranges = this.getSelectedRanges().slice();
                if (!ranges.length) {
                    ranges.push(new SlickRange(active.row, active.cell));
                }
                // keyboard can work with last range only
                last = ranges.pop();
                if (typeof last?.contains === 'function') {
                    // can't handle selection out of active cell
                    if (!last.contains(active.row, active.cell)) {
                        last = new SlickRange(active.row, active.cell);
                    }
                    let dRow = last.toRow - last.fromRow;
                    let dCell = last.toCell - last.fromCell;
                    let toCell;
                    let toRow = 0;
                    // when using Ctrl+{a, A} we will change our position to cell 0,0 and select all grid cells
                    if (e.ctrlKey && e.key?.toLowerCase() === 'a') {
                        this._grid.setActiveCell(0, 0, false, false, true);
                        active.row = 0;
                        active.cell = 0;
                        toCell = colLn - 1;
                        toRow = dataLn - 1;
                    }
                    // walking direction
                    const dirRow = active.row === last.fromRow ? 1 : -1;
                    const dirCell = active.cell === last.fromCell ? 1 : -1;
                    const isSingleKeyMove = e.key.startsWith('Arrow');
                    if (isSingleKeyMove && !e.ctrlKey) {
                        // single cell move: (Arrow{Up/ArrowDown/ArrowLeft/ArrowRight})
                        if (e.key === 'ArrowLeft') {
                            dCell -= dirCell;
                        }
                        else if (e.key === 'ArrowRight') {
                            dCell += dirCell;
                        }
                        else if (e.key === 'ArrowUp') {
                            dRow -= dirRow;
                        }
                        else if (e.key === 'ArrowDown') {
                            dRow += dirRow;
                        }
                        toRow = active.row + dirRow * dRow;
                    }
                    else {
                        // multiple cell moves: (Home, End, Page{Up/Down}), we need to know how many rows are displayed on a page
                        if (this._cachedPageRowCount < 1) {
                            this._cachedPageRowCount = this._grid.getViewportRowCount();
                        }
                        if (this._prevSelectedRow === undefined) {
                            this._prevSelectedRow = active.row;
                        }
                        if ((!e.ctrlKey && e.shiftKey && e.key === 'Home') || (e.ctrlKey && e.shiftKey && e.key === 'ArrowLeft')) {
                            toCell = 0;
                            toRow = active.row;
                        }
                        else if ((!e.ctrlKey && e.shiftKey && e.key === 'End') || (e.ctrlKey && e.shiftKey && e.key === 'ArrowRight')) {
                            toCell = colLn - 1;
                            toRow = active.row;
                        }
                        else if (e.ctrlKey && e.shiftKey && e.key === 'ArrowUp') {
                            toRow = 0;
                        }
                        else if (e.ctrlKey && e.shiftKey && e.key === 'ArrowDown') {
                            toRow = dataLn - 1;
                        }
                        else if (e.ctrlKey && e.shiftKey && e.key === 'Home') {
                            toCell = 0;
                            toRow = 0;
                        }
                        else if (e.ctrlKey && e.shiftKey && e.key === 'End') {
                            toCell = colLn - 1;
                            toRow = dataLn - 1;
                        }
                        else if (e.key === 'PageUp') {
                            if (this._prevSelectedRow >= 0) {
                                toRow = this._prevSelectedRow - this._cachedPageRowCount;
                            }
                            if (toRow < 0) {
                                toRow = 0;
                            }
                        }
                        else if (e.key === 'PageDown') {
                            if (this._prevSelectedRow <= dataLn - 1) {
                                toRow = this._prevSelectedRow + this._cachedPageRowCount;
                            }
                            if (toRow > dataLn - 1) {
                                toRow = dataLn - 1;
                            }
                        }
                        this._prevSelectedRow = toRow;
                    }
                    // define new selection range
                    toCell ??= active.cell + dirCell * dCell;
                    const newLast = new SlickRange(active.row, active.cell, toRow, toCell);
                    if (this.removeInvalidRanges([newLast]).length) {
                        ranges.push(newLast);
                        const viewRow = dirRow > 0 ? newLast.toRow : newLast.fromRow;
                        const viewCell = dirCell > 0 ? newLast.toCell : newLast.fromCell;
                        if (isSingleKeyMove) {
                            this._grid.scrollRowIntoView(viewRow);
                            this._grid.scrollCellIntoView(viewRow, viewCell, false);
                        }
                        else {
                            this._grid.scrollRowIntoView(toRow);
                            this._grid.scrollCellIntoView(toRow, viewCell, false);
                        }
                    }
                    else {
                        ranges.push(last);
                    }
                    this.setSelectedRanges(ranges, undefined, '');
                    e.preventDefault();
                    e.stopPropagation();
                    this._prevKeyDown = e.key;
                }
            }
        }
        else {
            const activeRow = this._grid.getActiveCell();
            if (this.gridOptions.multiSelect &&
                activeRow &&
                e.shiftKey &&
                !e.ctrlKey &&
                !e.altKey &&
                !e.metaKey &&
                (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
                let selectedRows = this.getSelectedRows();
                selectedRows.sort((x, y) => x - y);
                if (!selectedRows.length) {
                    selectedRows = [activeRow.row];
                }
                let active;
                let top = selectedRows[0];
                let bottom = selectedRows[selectedRows.length - 1];
                if (e.key === 'ArrowDown') {
                    active = activeRow.row < bottom || top === bottom ? ++bottom : ++top;
                }
                else {
                    active = activeRow.row < bottom ? --bottom : --top;
                }
                if (active >= 0 && active < this._grid.getDataLength()) {
                    this._grid.scrollRowIntoView(active);
                    const tempRanges = this.rowsToRanges(this.getRowsRange(top, bottom));
                    this.setSelectedRanges(tempRanges);
                }
                e.preventDefault();
                e.stopPropagation();
            }
        }
    }
    handleClick(e) {
        if (this._activeSelectionIsRow) {
            const cell = this._grid.getCellFromEvent(e);
            if (!cell || !this._grid.canCellBeActive(cell.row, cell.cell)) {
                return false;
            }
            if (!this.gridOptions.multiSelect || (!e.ctrlKey && !e.shiftKey && !e.metaKey)) {
                return false;
            }
            let selection = this.rangesToRows(this._ranges);
            const idx = selection.indexOf(cell.row);
            if (idx === -1 && (e.ctrlKey || e.metaKey)) {
                selection.push(cell.row);
                this._grid.setActiveCell(cell.row, cell.cell);
            }
            else if (idx !== -1 && (e.ctrlKey || e.metaKey)) {
                selection = selection.filter((o) => o !== cell.row);
                this._grid.setActiveCell(cell.row, cell.cell);
            }
            else if (selection.length && e.shiftKey) {
                const last = selection.pop();
                const from = Math.min(cell.row, last);
                const to = Math.max(cell.row, last);
                selection = [];
                for (let i = from; i <= to; i++) {
                    if (i !== last) {
                        selection.push(i);
                    }
                }
                selection.push(last);
                this._grid.setActiveCell(cell.row, cell.cell);
            }
            const tempRanges = this.rowsToRanges(selection);
            this.setSelectedRanges(tempRanges);
            e.stopImmediatePropagation();
            return true;
        }
    }
    /** is the column a column Row Move OR Select Row Move */
    isHandlerColumn(columnIndex) {
        const columns = this._grid.getColumns();
        const col = columns[columnIndex].behavior || '';
        return /move|selectAndMove/.test(col);
    }
    handleBeforeCellRangeSelected(e, cell) {
        if (this._activeSelectionIsRow) {
            let isRowMoveColumn = false;
            if (this.gridOptions.enableRowMoveManager) {
                isRowMoveColumn = this.isHandlerColumn(cell.cell) ?? false;
            }
            if (this._grid.getEditorLock().isActive() || isRowMoveColumn) {
                e.stopPropagation();
                return false;
            }
            this._grid.setActiveCell(cell.row, cell.cell);
        }
        else {
            const cell = this._grid.getCellFromEvent(e);
            const activeCell = this._grid.getActiveCell();
            if (this._grid.getEditorLock().isActive() && activeCell && cell && activeCell.row === cell.row && activeCell.cell === cell.cell) {
                e.stopPropagation();
                return false;
            }
        }
    }
    handleCellRangeSelected(_e, args) {
        if (this._activeSelectionIsRow) {
            if (!this.gridOptions.multiSelect || (!this._options?.selectActiveRow && this._options.selectionType !== 'row')) {
                return false;
            }
            this.setSelectedRanges([new SlickRange(args.range.fromRow, 0, args.range.toRow, this._grid.getColumns().length - 1)], undefined, args.selectionMode);
        }
        else {
            if (args.caller === 'onCellRangeSelecting') {
                return false;
            }
            this._grid.setActiveCell(args.range.fromRow, args.range.fromCell, args.allowAutoEdit ? undefined : false, false, true);
            this.setSelectedRanges([args.range], undefined, args.selectionMode);
        }
        return true;
    }
}
//# sourceMappingURL=slickHybridSelectionModel.js.map