/* istanbul ignore file */
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';

import { useTableVariablesContext } from './TableVariablesContext';

type ElementsMap = { [row: number]: { [column: number]: HTMLElement } };

/** Value of the {@link TableFastActiveItemContext} */
export interface TableFastActiveItemContextValue {
  /** Function that register interactive element for given table cell */
  registerCellElement: (row: number, column: number, element: HTMLElement | null) => void;
  /** Returns focus to current active table element */
  refocusActiveItem: () => void;
  /**
   * Set interactive element for given cell as active table element.
   *
   * Interactive element should be registered before using this function by
   * {@link TableFastActiveItemContextValue.registerCellElement}.
   */
  setActiveItem: (row: number, column: number) => void;
  /** Change table active element to next or previous cell */
  changeActiveItem: (rowOffset: -1 | 0 | 1, columnOffset: -1 | 0 | 1) => void;
}

const TableFastActiveItemContext = createContext<TableFastActiveItemContextValue | undefined>(undefined);

/**
 * Context that allows to change table focusable element.
 *
 * We should use such custom implementation because of performance issues.
 *
 * This context expect that some interactive element will be registered for each table cell.
 * Then it will be possible to change focus by {@link TableFastActiveItemContextValue.setActiveItem} or
 * {@link TableFastActiveItemContextValue.changeActiveItem} functions.
 */
export function TableFastActiveItemContextProvider({ children }: PropsWithChildren<{}>) {
  const { totalTableRenderedRows, totalTableRenderedColumns } = useTableVariablesContext();

  const activeCellItemsRef = useRef<ElementsMap>({});
  const currentActiveElementRef = useRef<{ row: number; column: number }>({ row: 0, column: 0 });

  const getCurrentFocusableItem = useCallback(() => {
    const { row: selectedRow, column: selectedColumn } = currentActiveElementRef.current;
    return (
      (activeCellItemsRef.current[selectedRow]
        ? activeCellItemsRef.current[selectedRow][selectedColumn]
        : null) || null
    );
  }, []);

  const clearActiveItem = useCallback(() => {
    const currentFocusableItem = getCurrentFocusableItem();
    if (currentFocusableItem) {
      currentFocusableItem.tabIndex = -1;
    }
  }, [getCurrentFocusableItem]);

  const setActiveItem = useCallback(
    (row: number, column: number, autoFocus = true) => {
      if (row < 0 || row >= totalTableRenderedRows) {
        return;
      }
      if (column < 0 || column >= totalTableRenderedColumns) {
        return;
      }

      clearActiveItem();
      currentActiveElementRef.current = { row, column };

      const focusableItem = getCurrentFocusableItem();
      if (focusableItem) {
        focusableItem.tabIndex = 0;
        // If set focus in this thick, outlive will be visible on click by cell
        if (autoFocus) {
          setTimeout(() => {
            focusableItem.focus();
          }, 0);
        }
      }
    },
    [clearActiveItem, getCurrentFocusableItem, totalTableRenderedRows, totalTableRenderedColumns],
  );

  useEffect(() => {
    // Clear out of range items
    Object.entries(activeCellItemsRef.current).forEach(([row, columns]) => {
      Object.keys(columns).forEach((column) => {
        if (Number(column) >= totalTableRenderedColumns || Number(row) >= totalTableRenderedRows) {
          (activeCellItemsRef.current as any)[row][column] = null;
        }
      });
    });

    // Fix current active element
    if (
      currentActiveElementRef.current.row >= totalTableRenderedRows ||
      currentActiveElementRef.current.column >= totalTableRenderedColumns
    ) {
      setActiveItem(0, 0, false);
    }
  }, [totalTableRenderedRows, totalTableRenderedColumns, setActiveItem]);

  const refocusActiveItem = useCallback(() => {
    const focusableItem = getCurrentFocusableItem();
    if (focusableItem) {
      focusableItem.focus();
    }
  }, [getCurrentFocusableItem]);

  const registerCellElement = useCallback(
    (row: number, column: number, element: HTMLElement | null) => {
      if (!activeCellItemsRef.current[row]) {
        activeCellItemsRef.current[row] = {};
      }

      const handleElementClick = () => {
        setActiveItem(row, column);
      };

      if (element) {
        activeCellItemsRef.current[row][column] = element;

        const { row: selectedRow, column: selectedColumn } = currentActiveElementRef.current;
        element.tabIndex = row === selectedRow && column === selectedColumn ? 0 : -1;

        // Don't use click event because it can broke Checkbox
        element.addEventListener('mousedown', handleElementClick);
      } else {
        if (activeCellItemsRef.current[row] && activeCellItemsRef.current[row][column]) {
          activeCellItemsRef.current[row][column].removeEventListener('mousedown', handleElementClick);
        }
        delete activeCellItemsRef.current[row][column];
      }
    },
    [setActiveItem],
  );

  const changeActiveItem = useCallback(
    (rowOffset: -1 | 0 | 1, columnOffset: -1 | 0 | 1) => {
      let { row, column } = currentActiveElementRef.current;
      row = row + rowOffset;
      column = column + columnOffset;

      setActiveItem(row, column);
    },
    [setActiveItem],
  );

  const value: TableFastActiveItemContextValue = useMemo(
    () => ({
      registerCellElement,
      refocusActiveItem,
      setActiveItem,
      changeActiveItem,
    }),
    [changeActiveItem, refocusActiveItem, registerCellElement, setActiveItem],
  );

  return <TableFastActiveItemContext.Provider value={value}>{children}</TableFastActiveItemContext.Provider>;
}

/** Returns functions that allow to change table focusable element */
export function useTableFastActiveItemContext(): TableFastActiveItemContextValue {
  const value = useContext(TableFastActiveItemContext);
  if (!value) {
    throw new Error(
      'useTableFastActiveItemContext should be used only inside TableFastActiveItemContextProvider',
    );
  }

  return value;
}
