import {
  keyCode as keyCode_,
  SlickEvent as SlickEvent_,
  SlickEventData as SlickEventData_,
  SlickEventHandler as SlickEventHandler_,
  SlickRange as SlickRange_,
  Utils as Utils_
} from '../slick.core.js';
import { Draggable as Draggable_ } from '../slick.interactions.js';
import { SlickCellRangeDecorator as SlickCellRangeDecorator_ } from './slick.cellrangedecorator.js';
import { SlickCellRangeSelector as SlickCellRangeSelector_ } from './slick.cellrangeselector.js';
import type { CustomDataView, HybridSelectionModelOption, OnActiveCellChangedEventArgs, SelectionModel } from '../models/index.js';
import type { SlickDataView } from '../slick.dataview.js';
import type { SlickCrossGridRowMoveManager as SlickCrossGridRowMoveManager_ } from './slick.crossgridrowmovemanager.js';
import type { SlickRowMoveManager as SlickRowMoveManager_ } from './slick.rowmovemanager.js';
import type { SlickGrid } from '../slick.grid.js';

// for (iife) load Slick methods from global Slick object, or use imports for (esm)
const Draggable = IIFE_ONLY ? Slick.Draggable : Draggable_;
const keyCode = IIFE_ONLY ? Slick.keyCode : keyCode_;
const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_;
const SlickEventData = IIFE_ONLY ? Slick.EventData : SlickEventData_;
const SlickEventHandler = IIFE_ONLY ? Slick.EventHandler : SlickEventHandler_;
const SlickRange = IIFE_ONLY ? Slick.Range : SlickRange_;
const SlickCellRangeDecorator = IIFE_ONLY ? Slick.CellRangeDecorator : SlickCellRangeDecorator_;
const SlickCellRangeSelector = IIFE_ONLY ? Slick.CellRangeSelector : SlickCellRangeSelector_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;

export class SlickHybridSelectionModel implements SelectionModel {
  // hybrid selection model is CellSelectionModel except when selecting
  // specific columns, which behave as RowSelectionModel

  // --
  // public API
  pluginName = 'HybridSelectionModel' as const;
  onSelectedRangesChanged = new SlickEvent<SlickRange_[]>('onSelectedRangesChanged');

  // --
  // protected props
  protected _cachedPageRowCount = 0;
  protected _dataView?: CustomDataView | SlickDataView;
  protected _eventHandler = new SlickEventHandler();
  protected _grid!: SlickGrid;
  protected _prevSelectedRow?: number;
  protected _prevKeyDown = '';
  protected _ranges: SlickRange_[] = [];
  protected _selector?: SlickCellRangeSelector_;
  protected _isRowMoveManagerHandler: any;
  protected _activeSelectionIsRow = false;
  protected _options: HybridSelectionModelOption;
  protected _defaults: HybridSelectionModelOption = {
    selectActiveCell: true,
    selectActiveRow: true,
    dragToSelect: false,
    autoScrollWhenDrag: true,
    handleRowMoveManagerColumn: true, // Row Selection on RowMoveManage column
    rowSelectColumnIds: [],         // Row Selection on these columns
    rowSelectOverride: undefined,     // function to toggle Row Selection Models
    cellRangeSelector: undefined,
    selectionType: 'mixed',
  };

  constructor(options?: HybridSelectionModelOption) {
    this._options = Utils.extend(true, {}, this._defaults, options);
  }

  // Region: Setup
  // -----------------------------------------------------------------------------

  init(grid: SlickGrid) {
    if (Draggable === undefined) {
      throw new Error('Slick.Draggable is undefined, make sure to import "slick.interactions.js"');
    }

    this._grid = grid;
    Utils.addSlickEventPubSubWhenDefined(grid.getPubSubService(), this);

    if (this._options?.selectionType === 'cell') {
      this._activeSelectionIsRow = false;
    } else if (this._options?.selectionType === 'row') {
      this._activeSelectionIsRow = true;
    }

    if (!this._selector && (!this._activeSelectionIsRow || (this._activeSelectionIsRow && this._options.dragToSelect))) {
      if (!SlickCellRangeDecorator) {
        throw new Error('Slick.CellRangeDecorator is required when option dragToSelect set to true');
      }
      this._selector = new SlickCellRangeSelector(
        this._options?.dragToSelect
          ? {
            selectionCss: { border: 'none' } as CSSStyleDeclaration,
            autoScroll: this._options?.autoScrollWhenDrag,
          }
          : {
            selectionCss: { border: '2px solid gray' } as CSSStyleDeclaration,
            copyToSelectionCss: { border: '2px solid purple' } as CSSStyleDeclaration,
          }
      );
      this._options.cellRangeSelector = this._selector;
    }

    if (grid.hasDataView()) {
      this._dataView = grid.getData<CustomDataView | SlickDataView>();
    }

    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) {
      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' }))
      this._selector.onBeforeCellRangeSelected.subscribe(this.handleBeforeCellRangeSelected.bind(this));
    }
  }

  destroy() {
    this._eventHandler.unsubscribeAll();
    if (this._selector) {
      this._grid?.unregisterPlugin(this._selector);
    }
    this._selector?.destroy();
  }

  getOptions(): HybridSelectionModelOption {
    return this._options;
  }

  // Region: CellSelectionModel Members
  // -----------------------------------------------------------------------------

  protected removeInvalidRanges(ranges: SlickRange_[]) {
    const result: SlickRange_[] = [];

    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;
  }

  protected rangesAreEqual(range1: SlickRange_[], range2: SlickRange_[]) {
    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
  // -----------------------------------------------------------------------------

  protected rangesToRows(ranges: SlickRange_[]): number[] {
    const rows: number[] = [];
    for (let i = 0; i < ranges.length; i++) {
      for (let j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
        rows.push(j);
      }
    }
    return rows;
  }

  protected rowsToRanges(rows: number[]) {
    const ranges: SlickRange_[] = [];
    const lastCell = this._grid.getColumns().length - 1;
    rows.forEach(row => ranges.push(new SlickRange(row, 0, row, lastCell)));
    return ranges;
  }

  protected getRowsRange(from: number, to: number) {
    let i;
    const rows: number[] = [];
    for (i = from; i <= to; i++) {
      rows.push(i);
    }
    for (i = to; i < from; i++) {
      rows.push(i);
    }
    return rows;
  }

  getCellRangeSelector(): SlickCellRangeSelector_ | undefined {
    return this._selector;
  }

  getSelectedRanges(): SlickRange_[] {
    return this._ranges;
  }

  getSelectedRows(): number[] {
    return this.rangesToRows(this._ranges);
  }

  setSelectedRows(rows: number[]): void {
    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: SlickRange_[], 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 range has not changed, don't fire onSelectedRangesChanged
    const rangeHasChanged = !this.rangesAreEqual(this._ranges, ranges);

    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 {
      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, '');
    }
  }

  getRowMoveManagerPlugin(): SlickRowMoveManager_ | SlickCrossGridRowMoveManager_ | undefined {
    return this._grid.getPluginByName('RowMoveManager') || this._grid.getPluginByName('CrossGridRowMoveManager');
  }

  rowSelectionModelIsActive(data: OnActiveCellChangedEventArgs): boolean {
    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 (!Utils.isDefined(data.cell)) { return false; }

    if (this._options?.handleRowMoveManagerColumn) {
      const rowMoveManager = this.getRowMoveManagerPlugin();
      if (rowMoveManager?.isHandlerColumn(data.cell)) { return true; }
    }

    const targetColumn = this._grid.getVisibleColumns()[data.cell];
    if (targetColumn) {
      return this._options?.rowSelectColumnIds?.includes('' + targetColumn.id) || false;
    }
    return false;
  }

  protected handleActiveCellChange(_e: SlickEventData_, args: OnActiveCellChangedEventArgs) {
    this._prevSelectedRow = undefined;
    const isCellDefined = Utils.isDefined(args.cell);
    const isRowDefined = Utils.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, '');
      }
    }
  }

  protected isKeyAllowed(key: string, isShiftKeyPressed?: boolean): boolean {
    return [
      'ArrowLeft',
      'ArrowRight',
      'ArrowUp',
      'ArrowDown',
      'PageDown',
      'PageUp',
      'Home',
      'End',
      ...(!isShiftKeyPressed ? ['a', 'A'] : []),
    ].some((k) => k === key);
  }

  protected handleKeyDown(e: SlickEventData_) {
    if (!this._activeSelectionIsRow) {
      let ranges: SlickRange_[], last: SlickRange_;
      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 as string, 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() as SlickRange_;

        // 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: undefined | number;
        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})
          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 new_last = new SlickRange(active.row, active.cell, toRow, toCell);
        if (this.removeInvalidRanges([new_last]).length) {
          ranges.push(new_last);
          const viewRow = dirRow > 0 ? new_last.toRow : new_last.fromRow;
          const viewCell = dirCell > 0 ? new_last.toCell : new_last.fromCell;

          if (isSingleKeyMove) {
            this._grid.scrollRowIntoView(viewRow);
            this._grid.scrollCellIntoView(viewRow, viewCell);
          } else {
            this._grid.scrollRowIntoView(toRow);
            this._grid.scrollCellIntoView(toRow, viewCell);
          }
        } else {
          ranges.push(last);
        }

        this.setSelectedRanges(ranges, undefined, '');

        e.preventDefault();
        e.stopPropagation();
        this._prevKeyDown = e.key as string;
      }
    } else {
      const activeRow = this._grid.getActiveCell();
      const isMultiSelect = this._grid.getOptions().multiSelect !== false;
      if (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: number;
        let top = selectedRows[0];
        let bottom = selectedRows[selectedRows.length - 1];

        if (e.which === keyCode.DOWN) {
          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);
          if (!isMultiSelect) {
            top = active;
            bottom = active;
          }
          const tempRanges = this.rowsToRanges(this.getRowsRange(top, bottom));
          this.setSelectedRanges(tempRanges);
        }

        e.preventDefault();
        e.stopPropagation();
      }
    }
  }

  protected handleClick(e: SlickEventData_): boolean | void {
    if (!this._activeSelectionIsRow) { return; }

    const cell = this._grid.getCellFromEvent(e);
    if (!cell || !this._grid.canCellBeActive(cell.row, cell.cell)) {
      return false;
    }

    if (!this._grid.getOptions().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() as number;
      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;
  }

  protected handleBeforeCellRangeSelected(e: SlickEventData_, cell: { row: number; cell: number; }): boolean | void {
    if (this._activeSelectionIsRow) {
      if (!this._isRowMoveManagerHandler) {
        const rowMoveManager = this._grid.getPluginByName<SlickRowMoveManager_>('RowMoveManager') || this._grid.getPluginByName<SlickCrossGridRowMoveManager_>('CrossGridRowMoveManager');
        this._isRowMoveManagerHandler = rowMoveManager ? rowMoveManager.isHandlerColumn : Utils.noop;
      }
      if (this._grid.getEditorLock().isActive() || this._isRowMoveManagerHandler(cell.cell)) {
        e.stopPropagation();
        return false;
      }
      this._grid.setActiveCell(cell.row, cell.cell);
    } else {
      if (this._grid.getEditorLock().isActive()) {
        e.stopPropagation();
        return false;
      }
    }
  }

  protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; selectionMode: string; allowAutoEdit?: boolean; caller: 'onCellRangeSelecting' | 'onCellRangeSelected' }) {
    //console.log('hybridSelectionModel.handleCellRangeSelected: ' + JSON.stringify(args.range) + '/' + args.selectionMode);
    if (this._activeSelectionIsRow) {
      if (!this._grid.getOptions().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;
  }
}

// extend Slick namespace on window object when building as iife
if (IIFE_ONLY && window.Slick) {
  Utils.extend(true, window, {
    Slick: {
      HybridSelectionModel: SlickHybridSelectionModel
    }
  });
}
