import type { Ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { getElementBoundaries } from '#utils';
import type { ShortcutGroup } from '@components/Shortcuts/Shortcuts.types';
import { getFocusableChildren } from '../../composables/useFocusTrap';
import type { Column, GridProps, UseGridDataReturn, UseGridLayoutReturn } from './Grid.types';
import { renderCallback } from './useGridLayout';

/** I like to rove it, rove it. */
export const rove = (el: HTMLElement) => {
  el.setAttribute('tabindex', '-1');
};
/** So much that I'd do it on every single focusable element. */
export const roveFocusableChildren = (el: HTMLElement) => getFocusableChildren(el)?.forEach(rove);
/** Nevermind I don't like it that much… */
export const unrove = (el: HTMLElement) => {
  el.setAttribute('tabindex', '0');
};

/**
 * Augments grid's keyboard navigation.
 * Active row and col indices are based on aria indices, which start at 1.
 * @props Grid props.
 * @param gridRef Reference to grid element.
 * @param data Grid data.
 * @param layout Grid layout.
 */
export function useGridControls<Cols extends Record<string, Column>, Items extends any[]>(
  props: GridProps<Cols, Items>,
  gridRef: Ref<HTMLElement | null>,
  data: UseGridDataReturn,
  layout: UseGridLayoutReturn,
) {
  const quickNavLabel = (direction: 'up' | 'down') =>
    `Jump ${data.pageSize.value} rows ${direction}`;

  /** List of elements whose tabindices are changed based on focus. */
  let editedTabindexEls: HTMLElement[] = [];
  /** Is focus set on the grid? */
  const isGridFocused = ref<boolean>(false);
  /** Scrollable grid content boundaries.  */
  const gridScrollerBoundaries = ref<{ top: number; bottom: number }>({ top: 0, bottom: 0 });
  /** Scrollable grid content element. */
  const gridScrollerElement = ref<HTMLElement | null>(null);

  /** Is the first row reached? */
  const firstRowReached = computed(() => data.activeRow.value <= 1);
  /** Is the last row reached? */
  const lastRowReached = computed(() => data.activeRow.value >= data.rowCount.value + 1);
  /** Active row selector. */
  const activeRowSelector = computed(() => `[aria-rowindex="${data.activeRow.value}"]`);
  /** Is the first row reached? */
  const firstColReached = computed(() => data.activeCol.value <= 1);
  /** Is the last row reached? */
  const lastColReached = computed(() => data.activeCol.value >= data.colCount.value);
  /** Active column selector. */
  const activeColumnSelector = computed(() => `[aria-colindex="${data.activeCol.value}"]`);

  const scrollTo = (x: number, y: number) => gridScrollerElement.value?.scrollTo(x, y);
  const scrollToStartX = () => scrollTo(0, layout.scrollTop.value);
  const scrollToEndX = () => scrollTo(layout.scrollWidth.value, layout.scrollTop.value);
  const scrollToStartY = () => scrollTo(layout.scrollLeft.value, 0);
  const scrollToEndY = () => scrollTo(layout.scrollLeft.value, layout.scrollHeight.value);
  const scrollToActiveRow = () =>
    scrollTo(
      layout.scrollLeft.value,
      data.activeRow.value * props.minItemSize - (gridRef.value?.clientHeight ?? 0) / 2,
    );

  /** Whenever a line is mounted, let's add roving tabindex behaviour. */
  onMounted(() => {
    if (!gridRef.value) return;
    gridScrollerElement.value = gridRef.value.querySelector('.grid');
    gridScrollerBoundaries.value = getElementBoundaries(gridScrollerElement.value ?? gridRef.value);
    roveFocusableChildren(gridRef.value);
  });

  /** Resets roving tab index on elements whose tabindices were changed. */
  function resetRovingTabindex() {
    editedTabindexEls.forEach(rove);
    editedTabindexEls = [];
  }

  /**
   * Gets active cell.
   * @param awaitRowRender Whether to wait for row to be rendered.
   */
  async function getActiveCell(awaitRowRender = true) {
    const getCellSelector = () => `${activeRowSelector.value} ${activeColumnSelector.value}`;
    const getCell = () => gridRef.value?.querySelector<HTMLElement>(getCellSelector());
    renderCallback.value?.reject();
    await nextTick();

    return (
      getCell() ??
      (await new Promise((resolve, reject) => {
        if (awaitRowRender) {
          resolve(undefined);
          return;
        }
        renderCallback.value = { resolve, reject, row: data.activeRow.value };
      })
        .then(() => getCell())
        .catch(() => undefined))
    );
  }

  /**
   * Focuses active cell, or first focusable item of active cell if any.
   * @param autoscroll Whether to scroll to the active cell.
   * @param awaitRowRender Whether to wait for row to be rendered.
   */
  async function focusActiveCell(autoscroll = true, awaitRowRender = true) {
    resetRovingTabindex();
    const cell = await getActiveCell(awaitRowRender);
    if (!cell) return scrollToActiveRow();

    const focusableChildren = getFocusableChildren(cell);
    const restoreTabindices = (el: HTMLElement) => {
      unrove(el);
      editedTabindexEls.push(el);
      return el;
    };

    const target = focusableChildren?.length
      ? // Reset native tabindex for cell's focusable elements…
        [...focusableChildren].map((el) => restoreTabindices(el))[0]
      : // …or the cell itself as part of the navigation.
        restoreTabindices(cell);

    if (autoscroll) {
      const { bottom, top } = getElementBoundaries(cell);
      const rowSize = cell.parentElement?.getBoundingClientRect().height ?? 0;
      if (gridScrollerBoundaries.value.bottom - bottom <= 1 * rowSize) {
        gridScrollerElement.value?.scrollTo(
          gridScrollerElement.value.scrollLeft,
          gridScrollerElement.value.scrollTop + rowSize,
        );
      } else if (top - gridScrollerBoundaries.value.top <= 1 * rowSize) {
        gridScrollerElement.value?.scrollTo(
          gridScrollerElement.value.scrollLeft,
          gridScrollerElement.value.scrollTop - rowSize,
        );
      }
    }

    target.focus({
      preventScroll: !autoscroll,
    });
  }

  /**
   * Handles cell click.
   * @param row Row index.
   * @param col Row index.
   */
  function handleCellClick(row: number, col: number) {
    data.activeRow.value = row;
    data.activeCol.value = col;
    focusActiveCell(false);
  }

  /**
   * Handles keyboard shortcuts.
   * @param e Keyboard event.
   */
  function handleShortcut(e: KeyboardEvent) {
    if (['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'PageUp', 'PageDown'].includes(e.key)) {
      e.preventDefault();
      e.stopPropagation();
    }

    switch (e.key) {
      case 'ArrowDown': {
        focusNextRow();
        return;
      }
      case 'ArrowUp': {
        focusPreviousRow();
        return;
      }
      case 'ArrowRight': {
        focusNextCol();
        return;
      }
      case 'ArrowLeft': {
        focusPreviousCol();
        return;
      }
      case 'PageUp': {
        focusPreviousRow(data.pageSize.value);
        return;
      }
      case 'PageDown': {
        focusNextRow(data.pageSize.value);
        return;
      }
      case 'Home': {
        focusFirstRow();
        return;
      }
      case 'End': {
        focusLastRow();
        return;
      }
      case 'Escape': {
        resetFocus();
        return;
      }
      case 'h':
        data.isShortcutListOpen.value = !data.isShortcutListOpen.value;
        break;
      default:
    }
  }

  function resetFocus() {
    data.activeRow.value = 0;
    data.activeCol.value = 1;
    gridRef.value?.focus();
  }

  function focusFirstRow() {
    data.activeRow.value = 2;
    scrollToStartY();
    nextTick(() => focusActiveCell(false));
  }

  function focusLastRow() {
    data.activeRow.value = data.rowCount.value + 1;
    scrollToEndY();
    nextTick(() => focusActiveCell(false));
  }

  function focusNextRow(step = 1) {
    if (!lastRowReached.value) {
      data.activeRow.value = Math.min(data.activeRow.value + step, data.rowCount.value + 1);
      nextTick(() => focusActiveCell());
    }
  }

  function focusPreviousRow(step = 1) {
    if (!firstRowReached.value) {
      data.activeRow.value = Math.max(data.activeRow.value - step, 1);
      nextTick(() => focusActiveCell());
    }
  }

  function focusNextCol() {
    if (!lastColReached.value) {
      data.activeCol.value += 1;
      nextTick(() => focusActiveCell());
    } else {
      scrollToEndX();
    }
  }

  function focusPreviousCol() {
    if (!firstColReached.value) {
      data.activeCol.value -= 1;
      nextTick(() => focusActiveCell());
    } else {
      scrollToStartX();
    }
  }

  const shortcuts = computed<ShortcutGroup[]>(() => [
    {
      // Basic navigation.
      items: [
        { key: 'ArrowUp', label: 'Previous row', disabled: firstRowReached.value },
        { key: 'ArrowDown', label: 'Next row', disabled: lastRowReached.value },
        { key: 'ArrowLeft', label: 'Previous column', disabled: firstColReached.value },
        { key: 'ArrowRight', label: 'Next column', disabled: lastColReached.value },
      ],
    },
    {
      // Quick navigation.
      items: [
        { key: 'PageUp', label: quickNavLabel('up'), disabled: firstRowReached.value },
        { key: 'PageDown', label: quickNavLabel('down'), disabled: lastRowReached.value },
        { key: 'Home', label: 'First row', disabled: firstRowReached.value },
        { key: 'End', label: 'Last row', disabled: lastRowReached.value },
      ],
    },
  ]);

  const setGridFocus = () => {
    isGridFocused.value = true;
  };

  const unsetGridFocus = () => {
    isGridFocused.value = false;
  };

  onMounted(() => {
    gridRef.value?.addEventListener('focus', setGridFocus);
    gridRef.value?.addEventListener('blur', unsetGridFocus);
  });

  onBeforeUnmount(() => {
    gridRef.value?.removeEventListener('focus', setGridFocus);
    gridRef.value?.removeEventListener('blur', unsetGridFocus);
  });

  return {
    /** List of shortcuts. */
    shortcuts,
    /** Handles cell click. */
    handleCellClick,
    /** Handles keyboard shortcuts. */
    handleShortcut,

    /** Is focus set on the grid? */
    isGridFocused,
    /** Is the first row reached? */
    firstRowReached,
    /** Is the last row reached? */
    lastRowReached,
    /** Is the first row reached? */
    firstColReached,
    /** Is the last row reached? */
    lastColReached,
  };
}
