/*
 * Copyright 2020 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

import {Direction, DisabledBehavior, Key, KeyboardDelegate, LayoutDelegate, Node, Rect, RefObject, Size} from '@react-types/shared';
import {DOMLayoutDelegate} from '@react-aria/selection';
import {getChildNodes, getFirstItem, getLastItem, getNthItem} from '@react-stately/collections';
import {GridCollection, GridNode} from '@react-types/grid';

export interface GridKeyboardDelegateOptions<C> {
  collection: C,
  disabledKeys: Set<Key>,
  disabledBehavior?: DisabledBehavior,
  ref?: RefObject<HTMLElement | null>,
  direction: Direction,
  collator?: Intl.Collator,
  layoutDelegate?: LayoutDelegate,
  /** @deprecated - Use layoutDelegate instead. */
  layout?: DeprecatedLayout,
  focusMode?: 'row' | 'cell'
}

export class GridKeyboardDelegate<T, C extends GridCollection<T>> implements KeyboardDelegate {
  collection: C;
  protected disabledKeys: Set<Key>;
  protected disabledBehavior: DisabledBehavior;
  protected direction: Direction;
  protected collator: Intl.Collator | undefined;
  protected layoutDelegate: LayoutDelegate;
  protected focusMode: 'row' | 'cell';

  constructor(options: GridKeyboardDelegateOptions<C>) {
    this.collection = options.collection;
    this.disabledKeys = options.disabledKeys;
    this.disabledBehavior = options.disabledBehavior || 'all';
    this.direction = options.direction;
    this.collator = options.collator;
    if (!options.layout && !options.ref) {
      throw new Error('Either a layout or a ref must be specified.');
    }
    this.layoutDelegate = options.layoutDelegate || (options.layout ? new DeprecatedLayoutDelegate(options.layout) : new DOMLayoutDelegate(options.ref!));
    this.focusMode = options.focusMode ?? 'row';
  }

  protected isCell(node: Node<T>): boolean {
    return node.type === 'cell';
  }

  protected isRow(node: Node<T>): boolean {
    return node.type === 'row' || node.type === 'item';
  }

  private isDisabled(item: Node<unknown>) {
    return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key));
  }

  protected findPreviousKey(fromKey?: Key, pred?: (item: Node<T>) => boolean): Key | null {
    let key = fromKey != null
      ? this.collection.getKeyBefore(fromKey)
      : this.collection.getLastKey();

    while (key != null) {
      let item = this.collection.getItem(key);
      if (!item) {
        return null;
      }
      if (!this.isDisabled(item) && (!pred || pred(item))) {
        return key;
      }

      key = this.collection.getKeyBefore(key);
    }
    return null;
  }

  protected findNextKey(fromKey?: Key, pred?: (item: Node<T>) => boolean): Key | null {
    let key = fromKey != null
      ? this.collection.getKeyAfter(fromKey)
      : this.collection.getFirstKey();

    while (key != null) {
      let item = this.collection.getItem(key);
      if (!item) {
        return null;
      }
      if (!this.isDisabled(item) && (!pred || pred(item))) {
        return key;
      }

      key = this.collection.getKeyAfter(key);
      if (key == null) {
        return null;
      }
    }
    return null;
  }

  protected getKeyForItemInRowByIndex(key: Key, index: number = 0): Key | null {
    if (index < 0) {
      return null;
    }

    let item = this.collection.getItem(key);
    if (!item) {
      return null;
    }

    let i = 0;
    for (let child of getChildNodes(item, this.collection) as Iterable<GridNode<T>>) {
      if (child.colSpan && child.colSpan + i > index) {
        return child.key ?? null;
      }

      if (child.colSpan) {
        i = i + child.colSpan - 1;
      }

      if (i === index) {
        return child.key ?? null;
      }

      i++;
    }
    return null;
  }

  getKeyBelow(fromKey: Key): Key | null {
    let key: Key | null = fromKey;
    let startItem = this.collection.getItem(key);
    if (!startItem) {
      return null;
    }

    // If focus was on a cell, start searching from the parent row
    if (this.isCell(startItem)) {
      key = startItem.parentKey ?? null;
    }
    if (key == null) {
      return null;
    }

    // Find the next item
    key = this.findNextKey(key, (item => item.type === 'item'));
    if (key != null) {
      // If focus was on a cell, focus the cell with the same index in the next row.
      if (this.isCell(startItem)) {
        let startIndex = startItem.colIndex ? startItem.colIndex : startItem.index;
        return this.getKeyForItemInRowByIndex(key, startIndex);
      }

      // Otherwise, focus the next row
      if (this.focusMode === 'row') {
        return key;
      }
    }
    return null;
  }

  getKeyAbove(fromKey: Key): Key | null {
    let key: Key | null = fromKey;
    let startItem = this.collection.getItem(key);
    if (!startItem) {
      return null;
    }

    // If focus is on a cell, start searching from the parent row
    if (this.isCell(startItem)) {
      key = startItem.parentKey ?? null;
    }
    if (key == null) {
      return null;
    }

    // Find the previous item
    key = this.findPreviousKey(key, item => item.type === 'item');
    if (key != null) {
      // If focus was on a cell, focus the cell with the same index in the previous row.
      if (this.isCell(startItem)) {
        let startIndex = startItem.colIndex ? startItem.colIndex : startItem.index;
        return this.getKeyForItemInRowByIndex(key, startIndex);
      }

      // Otherwise, focus the previous row
      if (this.focusMode === 'row') {
        return key;
      }
    }
    return null;
  }

  getKeyRightOf(key: Key): Key | null {
    let item = this.collection.getItem(key);
    if (!item) {
      return null;
    }

    // If focus is on a row, focus the first child cell.
    if (this.isRow(item)) {
      let children = getChildNodes(item, this.collection);
      return (this.direction === 'rtl'
        ? getLastItem(children)?.key
        : getFirstItem(children)?.key) ?? null;
    }

    // If focus is on a cell, focus the next cell if any,
    // otherwise focus the parent row.
    if (this.isCell(item) && item.parentKey != null) {
      let parent = this.collection.getItem(item.parentKey);
      if (!parent) {
        return null;
      }
      let children = getChildNodes(parent, this.collection);
      let next = (this.direction === 'rtl'
        ? getNthItem(children, item.index - 1)
        : getNthItem(children, item.index + 1)) ?? null;

      if (next) {
        return next.key ?? null;
      }

      // focus row only if focusMode is set to row
      if (this.focusMode === 'row') {
        return item.parentKey ?? null;
      }

      return (this.direction === 'rtl' ? this.getFirstKey(key) : this.getLastKey(key)) ?? null;
    }
    return null;
  }

  getKeyLeftOf(key: Key): Key | null {
    let item = this.collection.getItem(key);
    if (!item) {
      return null;
    }

    // If focus is on a row, focus the last child cell.
    if (this.isRow(item)) {
      let children = getChildNodes(item, this.collection);
      return (this.direction === 'rtl'
        ? getFirstItem(children)?.key
        : getLastItem(children)?.key) ?? null;
    }

    // If focus is on a cell, focus the previous cell if any,
    // otherwise focus the parent row.
    if (this.isCell(item) && item.parentKey != null) {
      let parent = this.collection.getItem(item.parentKey);
      if (!parent) {
        return null;
      }
      let children = getChildNodes(parent, this.collection);
      let prev = (this.direction === 'rtl'
        ? getNthItem(children, item.index + 1)
        : getNthItem(children, item.index - 1)) ?? null;

      if (prev) {
        return prev.key ?? null;
      }

      // focus row only if focusMode is set to row
      if (this.focusMode === 'row') {
        return item.parentKey ?? null;
      }

      return (this.direction === 'rtl' ? this.getLastKey(key) : this.getFirstKey(key)) ?? null;
    }
    return null;
  }

  getFirstKey(fromKey?: Key, global?: boolean): Key | null {
    let key: Key | null = fromKey ?? null;
    let item: Node<T> | undefined | null;
    if (key != null) {
      item = this.collection.getItem(key);
      if (!item) {
        return null;
      }

      // If global flag is not set, and a cell is currently focused,
      // move focus to the first cell in the parent row.
      if (this.isCell(item) && !global && item.parentKey != null) {
        let parent = this.collection.getItem(item.parentKey);
        if (!parent) {
          return null;
        }
        return getFirstItem(getChildNodes(parent, this.collection))?.key ?? null;
      }
    }

    // Find the first row
    key = this.findNextKey(undefined, item => item.type === 'item');

    // If global flag is set (or if focus mode is cell), focus the first cell in the first row.
    if (key != null && ((item && this.isCell(item) && global) || this.focusMode === 'cell')) {
      let item = this.collection.getItem(key);
      if (!item) {
        return null;
      }
      key = getFirstItem(getChildNodes(item, this.collection))?.key ?? null;
    }

    // Otherwise, focus the row itself.
    return key;
  }

  getLastKey(fromKey?: Key, global?: boolean): Key | null {
    let key: Key | null = fromKey ?? null;
    let item: Node<T> | undefined | null;
    if (key != null) {
      item = this.collection.getItem(key);
      if (!item) {
        return null;
      }

      // If global flag is not set, and a cell is currently focused,
      // move focus to the last cell in the parent row.
      if (this.isCell(item) && !global && item.parentKey != null) {
        let parent = this.collection.getItem(item.parentKey);
        if (!parent) {
          return null;
        }
        let children = getChildNodes(parent, this.collection);
        return getLastItem(children)?.key ?? null;
      }
    }

    // Find the last row
    key = this.findPreviousKey(undefined, item => item.type === 'item');

    // If global flag is set (or if focus mode is cell), focus the last cell in the last row.
    if (key != null && ((item && this.isCell(item) && global) || this.focusMode === 'cell')) {
      let item = this.collection.getItem(key);
      if (!item) {
        return null;
      }
      let children = getChildNodes(item, this.collection);
      key = getLastItem(children)?.key ?? null;
    }

    // Otherwise, focus the row itself.
    return key;
  }

  getKeyPageAbove(fromKey: Key): Key | null {
    let key: Key | null = fromKey;
    let itemRect = this.layoutDelegate.getItemRect(key);
    if (!itemRect) {
      return null;
    }

    let pageY = Math.max(0, itemRect.y + itemRect.height - this.layoutDelegate.getVisibleRect().height);

    while (itemRect && itemRect.y > pageY && key != null) {
      key = this.getKeyAbove(key) ?? null;
      if (key == null) {
        break;
      }
      itemRect = this.layoutDelegate.getItemRect(key);
    }

    return key;
  }

  getKeyPageBelow(fromKey: Key): Key | null {
    let key: Key | null = fromKey;
    let itemRect = this.layoutDelegate.getItemRect(key);

    if (!itemRect) {
      return null;
    }

    let pageHeight = this.layoutDelegate.getVisibleRect().height;
    let pageY = Math.min(this.layoutDelegate.getContentSize().height, itemRect.y + pageHeight);

    while (itemRect && (itemRect.y + itemRect.height) < pageY) {
      let nextKey = this.getKeyBelow(key);
      // If nextKey is undefined, we've reached the last row already
      if (nextKey == null) {
        break;
      }

      itemRect = this.layoutDelegate.getItemRect(nextKey);
      key = nextKey;
    }

    return key;
  }

  getKeyForSearch(search: string, fromKey?: Key): Key | null {
    let key: Key | null = fromKey ?? null;
    if (!this.collator) {
      return null;
    }

    let collection = this.collection;
    key = fromKey ?? this.getFirstKey();
    if (key == null) {
      return null;
    }

    // If the starting key is a cell, search from its parent row.
    let startItem = collection.getItem(key);
    if (!startItem) {
      return null;
    }
    if (startItem.type === 'cell') {
      key = startItem.parentKey ?? null;
    }

    let hasWrapped = false;
    while (key != null) {
      let item = collection.getItem(key);
      if (!item) {
        return null;
      }

      // check row text value for match
      if (item.textValue) {
        let substring = item.textValue.slice(0, search.length);
        if (this.collator.compare(substring, search) === 0) {
          if (this.isRow(item) && this.focusMode === 'cell') {
            return getFirstItem(getChildNodes(item, this.collection))?.key ?? null;
          }

          return item.key;
        }
      }

      key = this.findNextKey(key, item => item.type === 'item');

      // Wrap around when reaching the end of the collection
      if (key == null && !hasWrapped) {
        key = this.getFirstKey();
        hasWrapped = true;
      }
    }

    return null;
  }
}

/* Backward compatibility for old Virtualizer Layout interface. */
interface DeprecatedLayout {
  getLayoutInfo(key: Key): DeprecatedLayoutInfo,
  getContentSize(): Size,
  virtualizer: DeprecatedVirtualizer
}

interface DeprecatedLayoutInfo {
  rect: Rect
}

interface DeprecatedVirtualizer {
  visibleRect: Rect
}

class DeprecatedLayoutDelegate implements LayoutDelegate {
  layout: DeprecatedLayout;

  constructor(layout: DeprecatedLayout) {
    this.layout = layout;
  }

  getContentSize(): Size {
    return this.layout.getContentSize();
  }

  getItemRect(key: Key): Rect | null {
    return this.layout.getLayoutInfo(key)?.rect || null;
  }

  getVisibleRect(): Rect {
    return this.layout.virtualizer.visibleRect;
  }
}
