// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { some } from '@lumino/algorithm';

import { ISignal, Signal } from '@lumino/signaling';

import { DataModel } from './datamodel';

/**
 * A base class for creating data grid selection models.
 *
 * #### Notes
 * If the predefined selection models are insufficient for a particular
 * use case, a custom model can be defined which derives from this class.
 */
export abstract class SelectionModel {
  /**
   * Construct a new selection model.
   *
   * @param options - The options for initializing the model.
   */
  constructor(options: SelectionModel.IOptions) {
    this.dataModel = options.dataModel;
    this._selectionMode = options.selectionMode || 'cell';
    this.dataModel.changed.connect(this.onDataModelChanged, this);
  }

  /**
   * Whether the selection model is empty.
   *
   * #### Notes
   * An empty selection model will yield an empty `selections` iterator.
   */
  abstract readonly isEmpty: boolean;

  /**
   * The row index of the cursor.
   *
   * This is `-1` if the selection model is empty.
   */
  abstract readonly cursorRow: number;

  /**
   * The column index of the cursor.
   *
   * This is `-1` if the selection model is empty.
   */
  abstract readonly cursorColumn: number;

  /**
   * Move cursor down/up/left/right while making sure it remains
   * within the bounds of selected rectangles
   *
   * @param direction - The direction of the movement.
   */
  abstract moveCursorWithinSelections(
    direction: SelectionModel.CursorMoveDirection
  ): void;

  /**
   * Get the current selection in the selection model.
   *
   * @returns The current selection or `null`.
   *
   * #### Notes
   * This is the selection which holds the cursor.
   */
  abstract currentSelection(): SelectionModel.Selection | null;

  /**
   * Get an iterator of the selections in the model.
   *
   * @returns A new iterator of the selections in the model.
   *
   * #### Notes
   * The data grid will render the selections in order.
   */
  abstract selections(): IterableIterator<SelectionModel.Selection>;

  /**
   * Select the specified cells.
   *
   * @param args - The arguments for the selection.
   */
  abstract select(args: SelectionModel.SelectArgs): void;

  /**
   * Clear all selections in the selection model.
   */
  abstract clear(): void;

  /**
   * A signal emitted when the selection model has changed.
   */
  get changed(): ISignal<this, void> {
    return this._changed;
  }

  /**
   * The data model associated with the selection model.
   */
  readonly dataModel: DataModel;

  /**
   * Get the selection mode for the model.
   */
  get selectionMode(): SelectionModel.SelectionMode {
    return this._selectionMode;
  }

  /**
   * Set the selection mode for the model.
   *
   * #### Notes
   * This will clear the selection model.
   */
  set selectionMode(value: SelectionModel.SelectionMode) {
    // Bail early if the mode does not change.
    if (this._selectionMode === value) {
      return;
    }

    // Update the internal mode.
    this._selectionMode = value;

    // Clear the current selections.
    this.clear();
  }

  /**
   * Test whether any selection intersects a row.
   *
   * @param index - The row index of interest.
   *
   * @returns Whether any selection intersects the row.
   *
   * #### Notes
   * This method may be reimplemented in a subclass.
   */
  isRowSelected(index: number): boolean {
    return some(this.selections(), s => Private.containsRow(s, index));
  }

  /**
   * Test whether any selection intersects a column.
   *
   * @param index - The column index of interest.
   *
   * @returns Whether any selection intersects the column.
   *
   * #### Notes
   * This method may be reimplemented in a subclass.
   */
  isColumnSelected(index: number): boolean {
    return some(this.selections(), s => Private.containsColumn(s, index));
  }

  /**
   * Test whether any selection intersects a cell.
   *
   * @param row - The row index of interest.
   *
   * @param column - The column index of interest.
   *
   * @returns Whether any selection intersects the cell.
   *
   * #### Notes
   * This method may be reimplemented in a subclass.
   */
  isCellSelected(row: number, column: number): boolean {
    return some(this.selections(), s => Private.containsCell(s, row, column));
  }

  /**
   * A signal handler for the data model `changed` signal.
   *
   * @param args - The arguments for the signal.
   *
   * #### Notes
   * Selection model implementations should update their selections
   * in a manner that is relevant for the changes to the data model.
   *
   * The default implementation of this method is a no-op.
   */
  protected onDataModelChanged(
    sender: DataModel,
    args: DataModel.ChangedArgs
  ): void {
    // pass
  }

  /**
   * Emit the `changed` signal for the selection model.
   *
   * #### Notes
   * Subclasses should call this method whenever the selection model
   * has changed so that attached data grids can update themselves.
   */
  protected emitChanged(): void {
    this._changed.emit(undefined);
  }

  private _changed = new Signal<this, void>(this);
  private _selectionMode: SelectionModel.SelectionMode = 'cell';
}

/**
 * The namespace for the `SelectionModel` class statics.
 */
export namespace SelectionModel {
  /**
   * A type alias for the selection mode.
   */
  export type SelectionMode = 'row' | 'column' | 'cell';

  /**
   * A type alias for the cursor move direction.
   */
  export type CursorMoveDirection = 'up' | 'down' | 'left' | 'right' | 'none';

  /**
   * A type alias for the clear mode.
   */
  export type ClearMode = 'all' | 'current' | 'none';

  /**
   * A type alias for the select args.
   */
  export type SelectArgs = {
    /**
     * The first row of the selection.
     *
     * This may be greater than `r2`.
     */
    r1: number;

    /**
     * The first column of the selection.
     *
     * This may be greater than `c2`.
     */
    c1: number;

    /**
     * The last row of the selection.
     *
     * This may be less than `r1`.
     */
    r2: number;

    /**
     * The last column of the selection.
     *
     * This may be less than `c1`.
     */
    c2: number;

    /**
     * The row index for the cursor.
     *
     * This should be contained within the selection.
     */
    cursorRow: number;

    /**
     * The column index for the cursor.
     *
     * This should be contained within the selection.
     */
    cursorColumn: number;

    /**
     * Which of the existing selections to clear.
     */
    clear: ClearMode;
  };

  /**
   * A type alias for a selection in a selection model.
   */
  export type Selection = {
    /**
     * The first row of the selection.
     *
     * This may be greater than `r2`.
     */
    readonly r1: number;

    /**
     * The first column of the selection.
     *
     * This may be greater than `c2`.
     */
    readonly c1: number;

    /**
     * The last row of the selection.
     *
     * This may be less than `r1`.
     */
    readonly r2: number;

    /**
     * The last column of the selection.
     *
     * This may be less than `c1`.
     */
    readonly c2: number;
  };

  /**
   * An options object for initializing a selection model.
   */
  export interface IOptions {
    /**
     * The data model for the selection model.
     */
    dataModel: DataModel;

    /**
     * The selection mode for the model.
     *
     * The default is `'cell'`.
     */
    selectionMode?: SelectionMode;
  }
}

/**
 * The namespace for the module implementation details.
 */
namespace Private {
  /**
   * Test whether a selection contains a given row.
   */
  export function containsRow(
    selection: SelectionModel.Selection,
    row: number
  ): boolean {
    let { r1, r2 } = selection;
    return (row >= r1 && row <= r2) || (row >= r2 && row <= r1);
  }

  /**
   * Test whether a selection contains a given column.
   */
  export function containsColumn(
    selection: SelectionModel.Selection,
    column: number
  ): boolean {
    let { c1, c2 } = selection;
    return (column >= c1 && column <= c2) || (column >= c2 && column <= c1);
  }

  /**
   * Test whether a selection contains a given cell.
   */
  export function containsCell(
    selection: SelectionModel.Selection,
    row: number,
    column: number
  ): boolean {
    return containsRow(selection, row) && containsColumn(selection, column);
  }
}
