import { useCallback, useEffect, useMemo, useReducer, useState } from "react"
import {
  hitReducer,
  initlialLastLeftTile,
  initlialTarget,
  initlialTargetPreview,
  initlialMouseCursor,
} from "../utils/helpers"
import {
  Hit,
  Mode,
  MouseCursor,
  Target,
  Position,
} from "../../interfaces/frontend"
import { PointerProps } from "../../components/GamefieldPointer"

const modes: Mode[] = [
  {
    pointerGrid: Array.from(Array(3), () => Array.from(Array(3))),
    type: "radar",
  },
  {
    pointerGrid: Array.from(Array(3), () => Array.from(Array(1))),
    type: "htorpedo",
  },
  {
    pointerGrid: Array.from(Array(1), () => Array.from(Array(3))),
    type: "vtorpedo",
  },
  { pointerGrid: [[{ x: 0, y: 0 }]], type: "missile" },
]

function useGameEvent(count: number) {
  const [lastLeftTile, setLastLeftTile] =
    useState<Position>(initlialLastLeftTile)
  const [target, setTarget] = useState<Target>(initlialTarget)
  const [eventReady, setEventReady] = useState(false)
  const [appearOK, setAppearOK] = useState(false)
  const [targetPreview, setTargetPreview] = useState<Target>(
    initlialTargetPreview
  )
  const [mouseCursor, setMouseCursor] =
    useState<MouseCursor>(initlialMouseCursor)
  const [hits, DispatchHits] = useReducer(hitReducer, [] as Hit[])
  const [mode, setMode] = useState(0)

  const targetList = useCallback(
    (target: Position) => {
      const { pointerGrid, type } = modes[mode]
      const xLength = pointerGrid.length
      const yLength = pointerGrid[0].length
      const { x: targetX, y: targetY } = target
      return pointerGrid
        .map((arr, i) => {
          return arr.map((_, i2) => {
            const relativeX = -Math.floor(xLength / 2) + i
            const relativeY = -Math.floor(yLength / 2) + i2
            const x = targetX + (relativeX ?? 0)
            const y = targetY + (relativeY ?? 0)
            return {
              x,
              y,
              type,
              edges: [
                i === 0 ? "left" : "",
                i === xLength - 1 ? "right" : "",
                i2 === 0 ? "top" : "",
                i2 === yLength - 1 ? "bottom" : "",
              ],
            }
          })
        })
        .reduce((prev, curr) => [...prev, ...curr], [])
    },
    [mode]
  )

  const isHit = useCallback(
    (x: number, y: number) => {
      return hits.filter((h) => h.x === x && h.y === y)
    },
    [hits]
  )

  const settingTarget = useCallback(
    (isGameTile: boolean, x: number, y: number) => {
      if (!isGameTile || isHit(x, y).length) return
      setMouseCursor((e) => ({ ...e, shouldShow: false }))
      setTarget((t) => {
        if (t.x === x && t.y === y && t.show) {
          DispatchHits({
            type: "fireMissile",
            payload: { hit: (x + y) % 2 !== 0, x, y },
          })
          return { preview: false, show: false, x, y }
        } else {
          const target = { preview: false, show: true, x, y }
          const hasAnyBorder = targetList(target).filter(({ x, y }) =>
            isBorder(x, y, count)
          ).length
          if (hasAnyBorder) return t
          return target
        }
      })
    },
    [count, isHit, targetList]
  )

  const isSet = useCallback(
    (x: number, y: number) => {
      return (
        !!targetList(target).filter((field) => x === field.x && y === field.y)
          .length && target.show
      )
    },
    [target, targetList]
  )

  const composeTargetTiles = useCallback(
    (target: Target): PointerProps[] => {
      const { preview, show } = target
      const result = targetList(target).map(({ x, y, type, edges }) => {
        return {
          preview,
          x,
          y,
          show,
          type,
          edges,
          imply: !!isHit(x, y).length || (!!isSet(x, y) && preview),
        }
      })
      return result
    },
    [isHit, isSet, targetList]
  )

  // handle visibility and position change of targetPreview
  useEffect(() => {
    const { show, x, y } = targetPreview
    // if mouse has moved too quickly and last event was entering and leaving the same field, it must have gone outside the grid
    const hasLeft = x === lastLeftTile.x && y === lastLeftTile.y
    const isSet = x === target.x && y === target.y && target.show

    if (show && !appearOK) setTargetPreview((e) => ({ ...e, show: false }))
    if (
      !show &&
      mouseCursor.shouldShow &&
      eventReady &&
      appearOK &&
      !isHit(x, y).length &&
      !isSet &&
      !hasLeft
    )
      setTargetPreview((e) => ({ ...e, show: true }))
  }, [
    targetPreview,
    mouseCursor.shouldShow,
    isHit,
    eventReady,
    appearOK,
    lastLeftTile,
    target,
  ])

  // enable targetPreview event again after 200 ms.
  useEffect(() => {
    setEventReady(false)
    const previewTarget = { x: mouseCursor.x, y: mouseCursor.y }
    const hasAnyBorder = targetList(previewTarget).filter(({ x, y }) =>
      isBorder(x, y, count)
    ).length
    if (targetPreview.show || !appearOK || hasAnyBorder) return
    const autoTimeout = setTimeout(() => {
      setTargetPreview((e) => ({ ...e, ...previewTarget }))
      setEventReady(true)
      setAppearOK(true)
    }, 300)

    // or abort if state has changed early
    return () => {
      clearTimeout(autoTimeout)
    }
  }, [
    appearOK,
    count,
    mouseCursor.x,
    mouseCursor.y,
    targetList,
    targetPreview.show,
  ])

  // approve targetPreview new position after 200 mil. sec.
  useEffect(() => {
    // early return to start cooldown only when about to show up
    const autoTimeout = setTimeout(
      () => {
        setAppearOK(!targetPreview.show)
      },
      targetPreview.show ? 500 : 300
    )

    // or abort if movement is repeated early
    return () => {
      clearTimeout(autoTimeout)
    }
  }, [targetPreview.show])

  return {
    tilesProps: { count, settingTarget, setMouseCursor, setLastLeftTile },
    pointersProps: { composeTargetTiles, target, targetPreview },
    targetsProps: { setMode, setTarget },
    hits,
  }
}

function isBorder(x: number, y: number, count: number) {
  return x < 2 || x > count + 1 || y < 2 || y > count + 1
}

export default useGameEvent
