import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  appendFloatingText,
  groupFloatingTextsByMember,
  stripFloatingTextSide,
  type BasicFloatingCombatText,
} from '../combat/combatPresentation'

type FloatingSide = 'player' | 'cpu'
type SideFloatingCombatText = BasicFloatingCombatText & {
  side: FloatingSide
}

type FloatingCombatTextOptions = {
  durationMs?: number
}

export function useFloatingCombatText<TText extends BasicFloatingCombatText>({
  durationMs = 900,
}: FloatingCombatTextOptions = {}) {
  const [floatingTexts, setFloatingTexts] = useState<TText[]>([])
  const nextId = useRef(1)
  const expirationsRef = useRef(new Map<number, number>())
  const cleanupTimerRef = useRef<number | null>(null)

  const clearCleanupTimer = useCallback(() => {
    if (cleanupTimerRef.current === null) return
    window.clearTimeout(cleanupTimerRef.current)
    cleanupTimerRef.current = null
  }, [])

  const scheduleCleanup = useCallback(() => {
    function scheduleNext() {
      clearCleanupTimer()
      let nextExpiry = Infinity
      for (const expiresAt of expirationsRef.current.values()) {
        nextExpiry = Math.min(nextExpiry, expiresAt)
      }
      if (!Number.isFinite(nextExpiry)) return

      cleanupTimerRef.current = window.setTimeout(() => {
        cleanupTimerRef.current = null
        const now = performance.now()
        let removed = false
        for (const [id, expiresAt] of expirationsRef.current) {
          if (expiresAt > now) continue
          expirationsRef.current.delete(id)
          removed = true
        }
        if (removed) {
          setFloatingTexts((current) => current.filter((text) => expirationsRef.current.has(text.id)))
        }
        scheduleNext()
      }, Math.max(0, nextExpiry - performance.now()))
    }

    scheduleNext()
  }, [clearCleanupTimer])

  const clearFloatingTexts = useCallback(() => {
    clearCleanupTimer()
    expirationsRef.current.clear()
    setFloatingTexts([])
  }, [clearCleanupTimer])

  const addFloatingText = useCallback((entry: Omit<TText, 'id'>) => {
    if (entry.value <= 0) return
    const id = nextId.current++
    expirationsRef.current.set(id, performance.now() + durationMs)
    setFloatingTexts((current) => appendFloatingText(current, { ...entry, id } as TText))
    scheduleCleanup()
  }, [durationMs, scheduleCleanup])

  useEffect(() => () => clearCleanupTimer(), [clearCleanupTimer])

  const floatingTextsByMember = useMemo(
    () => groupFloatingTextsByMember(floatingTexts),
    [floatingTexts],
  )

  return {
    floatingTexts,
    floatingTextsByMember,
    addFloatingText,
    clearFloatingTexts,
  }
}

export function useSidedFloatingCombatText(options?: FloatingCombatTextOptions) {
  const {
    floatingTexts,
    addFloatingText,
    clearFloatingTexts,
  } = useFloatingCombatText<SideFloatingCombatText>(options)
  const playerFloatingTexts = useMemo(
    () => floatingTexts.filter((entry) => entry.side === 'player'),
    [floatingTexts],
  )
  const cpuFloatingTexts = useMemo(
    () => floatingTexts.filter((entry) => entry.side === 'cpu'),
    [floatingTexts],
  )
  const playerFloatingTextsByMember = useMemo(
    () => groupFloatingTextsByMember(playerFloatingTexts),
    [playerFloatingTexts],
  )
  const cpuFloatingTextsByMember = useMemo(
    () => groupFloatingTextsByMember(cpuFloatingTexts),
    [cpuFloatingTexts],
  )
  const dualScreenFloatingTexts = useMemo(
    () => stripFloatingTextSide(playerFloatingTexts),
    [playerFloatingTexts],
  )

  return {
    floatingTexts,
    playerFloatingTexts,
    cpuFloatingTexts,
    playerFloatingTextsByMember,
    cpuFloatingTextsByMember,
    dualScreenFloatingTexts,
    addFloatingText,
    clearFloatingTexts,
  }
}
