/* eslint-disable max-lines, max-statements */
import { type PayloadAction } from '@reduxjs/toolkit';
import { BLANK, EMPTY_CELL } from '@scrabble-solver/constants';
import { Board, type Cell } from '@scrabble-solver/types';
import {
  type ChangeEvent,
  type ChangeEventHandler,
  type ClipboardEventHandler,
  type KeyboardEventHandler,
  type RefObject,
  createRef,
  useCallback,
  useMemo,
  useState,
} from 'react';
import { useDispatch } from 'react-redux';

import { useLatest } from '@/hooks';
import { LOCALE_FEATURES } from '@/i18n';
import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from '@/lib';
import { boardSlice, cellFiltersSlice, selectConfig, selectLocale, useTypedSelector } from '@/state';
import { type Direction, type Point } from '@/types';

import { createGrid, getPositionInGrid } from '../lib';

const toggleDirection = (direction: Direction) => (direction === 'vertical' ? 'horizontal' : 'vertical');

interface State {
  activeIndex: Point;
  direction: Direction;
  inputRefs: RefObject<HTMLInputElement | null>[][];
}

interface Actions {
  insertValue: (position: Point, value: string) => void;
  onChange: ChangeEventHandler<HTMLInputElement>;
  onDirectionToggle: () => void;
  onFocus: (x: number, y: number) => void;
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
  onPaste: ClipboardEventHandler<HTMLInputElement>;
}

export const useGrid = (rows: Cell[][]): [State, Actions] => {
  const height = rows.length;
  const width = rows[0].length;
  const dispatch = useDispatch();
  const config = useTypedSelector(selectConfig);
  const locale = useTypedSelector(selectLocale);
  const inputRefs = useMemo(
    () => createGrid<RefObject<HTMLInputElement | null>>(width, height, () => createRef()),
    [width, height],
  );
  const [activeIndex, setActiveIndex] = useState<Point>({ x: 0, y: 0 });
  const [direction, setLastDirection] = useState<Direction>('horizontal');
  const directionRef = useLatest(direction);

  const safeActiveIndex = useMemo(
    () => ({
      x: Math.min(activeIndex.x, width - 1),
      y: Math.min(activeIndex.y, height - 1),
    }),
    [activeIndex, width, height],
  );

  const changeActiveIndex = useCallback(
    (offsetX: number, offsetY: number) => {
      const x = Math.min(Math.max(safeActiveIndex.x + offsetX, 0), width - 1);
      const y = Math.min(Math.max(safeActiveIndex.y + offsetY, 0), height - 1);
      setActiveIndex({ x, y });
      inputRefs[y][x].current?.focus();
    },
    [safeActiveIndex, height, inputRefs, width],
  );

  const getInputRefPosition = useCallback(
    (inputRef: HTMLInputElement): Point | undefined => {
      return getPositionInGrid(inputRefs, (ref) => ref.current === inputRef);
    },
    [inputRefs],
  );

  const moveFocus = useCallback(
    (offset: number) => {
      if (directionRef.current === 'horizontal') {
        changeActiveIndex(offset, 0);
      } else {
        changeActiveIndex(0, offset);
      }
    },
    [changeActiveIndex, directionRef],
  );

  const insertValue = useCallback(
    (position: Point, value: string) => {
      const characters = value ? extractCharacters(config, value).filter((character) => character !== BLANK) : [BLANK];
      const actions: PayloadAction<unknown>[] = [];
      let board = new Board({ rows: rows.map((row) => row.map((cell) => cell.clone())) });
      let { x, y } = position;

      const scheduleMoveFocus = () => {
        if (directionRef.current === 'horizontal') {
          ++x;
        } else {
          ++y;
        }
      };

      characters.forEach((character) => {
        if (x >= config.boardWidth || y >= config.boardHeight) {
          return;
        }

        const canCheckUp = y - 1 > 0;
        const canCheckLeft = x > 0;
        const canCheckRight = x + 1 < width;
        const canCheckDown = y + 1 < height;

        if (canCheckUp) {
          const cellUp = board.rows[y - 1][x];
          const twoCharacterCandidate = cellUp.tile.character + character;

          if (!cellUp.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
            const action = boardSlice.actions.changeCellValue({ x, y: y - 1, value: twoCharacterCandidate });
            board = boardSlice.reducer(board, action);
            actions.push(action);
            return;
          }
        }

        if (canCheckDown) {
          const cellDown = board.rows[y + 1][x];
          const twoCharacterCandidate = character + cellDown.tile.character;

          if (!cellDown.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
            const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
            const action2 = boardSlice.actions.changeCellValue({ x, y: y + 1, value: EMPTY_CELL });
            board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
            actions.push(action1, action2);
            scheduleMoveFocus();
            return;
          }
        }

        if (canCheckLeft) {
          const cellLeft = board.rows[y][x - 1];
          const twoCharacterCandidate = cellLeft.tile.character + character;

          if (!cellLeft.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
            const action = boardSlice.actions.changeCellValue({ x: x - 1, y, value: twoCharacterCandidate });
            board = boardSlice.reducer(board, action);
            actions.push(action);
            return;
          }
        }

        if (canCheckRight) {
          const cellRight = board.rows[y][x + 1];
          const twoCharacterCandidate = character + cellRight.tile.character;

          if (!cellRight.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
            const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
            const action2 = boardSlice.actions.changeCellValue({ x: x + 1, y, value: EMPTY_CELL });
            board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
            actions.push(action1, action2);
            scheduleMoveFocus();
            return;
          }
        }

        if (!canCheckDown || !canCheckRight) {
          const cell = board.rows[y][x];
          const twoCharacterCandidate = cell.tile.character + character;

          if (!cell.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
            const action = boardSlice.actions.changeCellValue({ x, y, value: twoCharacterCandidate });
            board = boardSlice.reducer(board, action);
            actions.push(action);
            return;
          }
        }

        const action = boardSlice.actions.changeCellValue({ x, y, value: character });
        board = boardSlice.reducer(board, action);
        actions.push(action);
        scheduleMoveFocus();
      });

      moveFocus(Math.abs(position.x - x) + Math.abs(position.y - y));
      actions.forEach(dispatch);
    },
    [config, directionRef, dispatch, height, moveFocus, rows, width],
  );

  const onChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      const position = getInputRefPosition(event.target);

      if (!position) {
        return;
      }

      const value = extractInputValue(event.target);

      if (!value) {
        dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
        moveFocus(-1);
        return;
      }

      if (value === EMPTY_CELL) {
        const { x, y } = position;
        const cell = rows[y][x];

        if (cell.hasTile()) {
          dispatch(boardSlice.actions.toggleCellIsBlank(position));
          return;
        }
      }

      insertValue(position, value);
    },
    [dispatch, insertValue, moveFocus, rows, getInputRefPosition],
  );

  const onDirectionToggle = useCallback(() => setLastDirection(toggleDirection), []);

  const onFocus = useCallback((x: number, y: number) => {
    setActiveIndex({ x, y });
  }, []);

  const onKeyDown = useMemo(() => {
    return createKeyboardNavigation({
      onArrowDown: (event) => {
        event.preventDefault();

        if (direction === 'horizontal') {
          onDirectionToggle();
        } else {
          changeActiveIndex(0, 1);
        }
      },
      onArrowLeft: (event) => {
        event.preventDefault();

        if (direction === 'vertical') {
          onDirectionToggle();
          changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? -1 : 0, 0);
        } else {
          changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? -1 : 1, 0);
        }
      },
      onArrowRight: (event) => {
        event.preventDefault();

        if (direction === 'vertical') {
          onDirectionToggle();
          changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? 0 : -1, 0);
        } else {
          changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? 1 : -1, 0);
        }
      },
      onArrowUp: (event) => {
        event.preventDefault();

        if (direction === 'horizontal') {
          onDirectionToggle();
        }

        changeActiveIndex(0, -1);
      },
      onBackspace: (event) => {
        const position = getInputRefPosition(event.target as HTMLInputElement);

        if (!position) {
          return;
        }

        event.preventDefault();
        dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
        moveFocus(-1);
      },
      onDelete: (event) => {
        const position = getInputRefPosition(event.target as HTMLInputElement);

        if (!position) {
          return;
        }

        event.preventDefault();
        dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
        moveFocus(1);
      },
      onKeyDown: (event) => {
        const position = getInputRefPosition(event.target as HTMLInputElement);

        if (!position) {
          return;
        }

        const { x, y } = position;
        const character = event.key.toLocaleLowerCase();
        const twoCharacterTile = config.getTwoCharacterTileByPrefix(character);

        if (isCtrl(event) && twoCharacterTile) {
          event.preventDefault();
          dispatch(boardSlice.actions.changeCellValue({ x, y, value: twoCharacterTile }));
          moveFocus(1);
          return;
        }

        const cell = rows[y][x];

        if (isCtrl(event) && character === 'g') {
          event.preventDefault();

          if (!cell.hasTile()) {
            dispatch(cellFiltersSlice.actions.toggle(position));
          }

          return;
        }

        const twoCharacterCandidate = cell.tile.character + character;

        if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
          event.preventDefault();
          dispatch(boardSlice.actions.changeCellValue({ ...position, value: twoCharacterCandidate }));
          moveFocus(1);
          return;
        }

        if (event.target instanceof HTMLInputElement && event.target.value.toLocaleLowerCase() === character) {
          // change event did not fire because the same character was typed over the current one
          // but we still want to move the caret
          event.preventDefault();
          moveFocus(1);
        }
      },
      onSpace: (event) => {
        const position = getInputRefPosition(event.target as HTMLInputElement);

        if (!position) {
          return;
        }

        event.preventDefault();
        dispatch(boardSlice.actions.toggleCellIsBlank(position));
      },
    });
  }, [changeActiveIndex, config, direction, dispatch, getInputRefPosition, locale, moveFocus, onDirectionToggle, rows]);

  const onPaste = useCallback<ClipboardEventHandler>(
    (event) => {
      if (!(event.target instanceof HTMLInputElement)) {
        return;
      }

      const position = getInputRefPosition(event.target);

      if (!position) {
        return;
      }

      event.preventDefault();

      const value = event.clipboardData.getData('text/plain').toLocaleLowerCase();
      insertValue(position, value);
    },
    [getInputRefPosition, insertValue],
  );

  return [
    { activeIndex: safeActiveIndex, direction, inputRefs },
    { insertValue, onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
  ];
};
