/* eslint-disable react-refresh/only-export-components */
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from 'react'

export type InputDevice = 'pc' | 'controller'
export type ControllerIconStyle = 'xbox' | 'playstation' | 'nintendo'

export const INPUT_ACTIONS = [
  'navigateUp',
  'navigateDown',
  'navigateLeft',
  'navigateRight',
  'confirm',
  'back',
  'ability1',
  'ability2',
  'ability3',
  'ability4',
  'ability5',
  'ability6',
  'previousTarget',
  'nextTarget',
  'targetParty1',
  'targetParty2',
  'targetParty3',
  'targetParty4',
  'targetParty5',
  'targetParty6',
  'toggleTargetGroup',
  'pause',
] as const

export type InputAction = typeof INPUT_ACTIONS[number]
export type InputBindings = Record<InputAction, string>

export const ACTION_LABELS: Record<InputAction, string> = {
  navigateUp: 'Navigate Up',
  navigateDown: 'Navigate Down',
  navigateLeft: 'Navigate Left',
  navigateRight: 'Navigate Right',
  confirm: 'Confirm / Select',
  back: 'Back',
  ability1: 'Ability Slot 1',
  ability2: 'Ability Slot 2',
  ability3: 'Ability Slot 3',
  ability4: 'Ability Slot 4',
  ability5: 'Ability Slot 5',
  ability6: 'Ability Slot 6',
  previousTarget: 'Previous Party Target',
  nextTarget: 'Next Party Target',
  targetParty1: 'Target Party Member 1',
  targetParty2: 'Target Party Member 2',
  targetParty3: 'Target Party Member 3',
  targetParty4: 'Target Party Member 4',
  targetParty5: 'Target Party Member 5',
  targetParty6: 'Target Party Member 6',
  toggleTargetGroup: 'Switch Raid Target Group',
  pause: 'Pause Menu',
}

export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
  pc: {
    navigateUp: 'ArrowUp',
    navigateDown: 'ArrowDown',
    navigateLeft: 'ArrowLeft',
    navigateRight: 'ArrowRight',
    confirm: 'Enter',
    back: 'Escape',
    ability1: 'Digit1',
    ability2: 'Digit2',
    ability3: 'Digit3',
    ability4: 'Digit4',
    ability5: 'Digit5',
    ability6: 'Digit6',
    previousTarget: 'KeyQ',
    nextTarget: 'KeyE',
    targetParty1: 'F1',
    targetParty2: 'F2',
    targetParty3: 'F3',
    targetParty4: 'F4',
    targetParty5: 'F5',
    targetParty6: 'F6',
    toggleTargetGroup: 'Tab',
    pause: 'Escape',
  },
  controller: {
    navigateUp: 'Axis1-',
    navigateDown: 'Axis1+',
    navigateLeft: 'Axis0-',
    navigateRight: 'Axis0+',
    confirm: 'Button0',
    back: 'Button1',
    ability1: 'Button3',
    ability2: 'Button2',
    ability3: 'Button0',
    ability4: 'Button1',
    ability5: 'Button5',
    ability6: 'Button7',
    previousTarget: 'Button14',
    nextTarget: 'Button15',
    targetParty1: 'Button14',
    targetParty2: 'Button12',
    targetParty3: 'Button15',
    targetParty4: 'Button13',
    targetParty5: 'Button4',
    targetParty6: 'Button11',
    toggleTargetGroup: 'Button6',
    pause: 'Button9',
  },
}

const STORAGE_KEY = 'ashen-halls-input-bindings-v1'
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
const COMBAT_TARGET_NAVIGATION_THROTTLE_MS = 220

type CaptureState = {
  device: InputDevice
  action: InputAction
} | null

type InputContextValue = {
  bindings: Record<InputDevice, InputBindings>
  capture: CaptureState
  lastDevice: InputDevice
  controllerIconStyle: ControllerIconStyle
  directPartyTargeting: boolean
  beginCapture: (device: InputDevice, action: InputAction) => void
  cancelCapture: () => void
  resetBindings: (device: InputDevice) => void
  setControllerIconStyle: (style: ControllerIconStyle) => void
  setDirectPartyTargeting: (enabled: boolean) => void
}

const InputContext = createContext<InputContextValue | null>(null)

function loadBindings(): Record<InputDevice, InputBindings> {
  try {
    const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Partial<Record<InputDevice, Partial<InputBindings>>>
    const controller = { ...DEFAULT_BINDINGS.controller, ...saved.controller }
    const usesLegacyAbilityDefaults = [
      'Button2',
      'Button3',
      'Button4',
      'Button5',
      'Button6',
      'Button7',
    ].every((binding, index) => (
      controller[`ability${index + 1}` as InputAction] === binding
    ))
    if (usesLegacyAbilityDefaults) {
      Object.assign(controller, {
        ability1: DEFAULT_BINDINGS.controller.ability1,
        ability2: DEFAULT_BINDINGS.controller.ability2,
        ability3: DEFAULT_BINDINGS.controller.ability3,
        ability4: DEFAULT_BINDINGS.controller.ability4,
        ability5: DEFAULT_BINDINGS.controller.ability5,
        ability6: DEFAULT_BINDINGS.controller.ability6,
      })
    }
    return {
      pc: { ...DEFAULT_BINDINGS.pc, ...saved.pc },
      controller,
    }
  } catch {
    return structuredClone(DEFAULT_BINDINGS)
  }
}

function loadPreferences() {
  try {
    const saved = JSON.parse(localStorage.getItem(PREFERENCES_STORAGE_KEY) ?? '{}') as {
      controllerIconStyle?: ControllerIconStyle
      directPartyTargeting?: boolean
    }
    return {
      controllerIconStyle: saved.controllerIconStyle ?? 'xbox',
      directPartyTargeting: saved.directPartyTargeting ?? false,
    }
  } catch {
    return {
      controllerIconStyle: 'xbox' as ControllerIconStyle,
      directPartyTargeting: false,
    }
  }
}

function isTextInput(element: Element | null): element is HTMLInputElement | HTMLTextAreaElement {
  return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
}

function bindingGroup(action: InputAction) {
  if (action.startsWith('ability')) return 'abilities'
  if (action.startsWith('targetParty') || action === 'toggleTargetGroup') return 'direct-targeting'
  if (action === 'previousTarget' || action === 'nextTarget') return 'relative-targeting'
  if (action === 'pause') return 'pause'
  return 'navigation'
}

function isVisible(element: HTMLElement) {
  if (element.hidden || element.getAttribute('aria-hidden') === 'true') return false
  return element.getClientRects().length > 0
}

function focusableElements() {
  const keyboard = document.querySelector<HTMLElement>('.controller-keyboard')
  const pauseMenu = document.querySelector<HTMLElement>('.pause-screen')
  const dialog = Array.from(
    document.querySelectorAll<HTMLElement>(
      '.result-screen, .binding-capture, .dual-startup-prompt',
    ),
  ).find(isVisible)
  const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
  return Array.from(
    scope.querySelectorAll<HTMLElement>(
      'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
    ),
  ).filter(isVisible)
}

export function focusFirstControl() {
  const first = focusableElements()[0]
  first?.focus({ preventScroll: true })
  return first
}

function moveFocus(action: InputAction) {
  const candidates = focusableElements()
  if (candidates.length === 0) return
  const current = document.activeElement instanceof HTMLElement
    && candidates.includes(document.activeElement)
    ? document.activeElement
    : null
  if (!current) {
    focusFirstControl()
    return
  }

  const currentRect = current.getBoundingClientRect()
  const currentX = currentRect.left + currentRect.width / 2
  const currentY = currentRect.top + currentRect.height / 2
  const vertical = action === 'navigateUp' || action === 'navigateDown'
  const direction = action === 'navigateUp' || action === 'navigateLeft' ? -1 : 1

  const ranked = candidates
    .filter((candidate) => candidate !== current)
    .map((candidate) => {
      const rect = candidate.getBoundingClientRect()
      const x = rect.left + rect.width / 2
      const y = rect.top + rect.height / 2
      const primary = vertical ? y - currentY : x - currentX
      const secondary = vertical ? Math.abs(x - currentX) : Math.abs(y - currentY)
      return { candidate, primary, score: Math.abs(primary) + secondary * 2.5 }
    })
    .filter(({ primary }) => Math.sign(primary) === direction)
    .sort((a, b) => a.score - b.score)

  const next = ranked[0]?.candidate
  if (!next) return
  next.focus({ preventScroll: true })
}

function hasUiOverlay() {
  return Array.from(
    document.querySelectorAll<HTMLElement>(
      '.pause-screen, .result-screen, .binding-capture, .dual-startup-prompt, .controller-keyboard',
    ),
  ).some(isVisible)
}

function isCombatTargetAction(action: InputAction) {
  return action.startsWith('navigate')
    || action.startsWith('targetParty')
    || action === 'previousTarget'
    || action === 'nextTarget'
    || action === 'toggleTargetGroup'
}

const BUTTON_LABELS: Record<number, string> = {
  0: 'A / Cross',
  1: 'B / Circle',
  2: 'X / Square',
  3: 'Y / Triangle',
  4: 'Left Bumper',
  5: 'Right Bumper',
  6: 'Left Trigger',
  7: 'Right Trigger',
  8: 'View / Select',
  9: 'Menu / Start',
  10: 'Left Stick',
  11: 'Right Stick',
  12: 'D-Pad Up',
  13: 'D-Pad Down',
  14: 'D-Pad Left',
  15: 'D-Pad Right',
  16: 'Home',
}

const KEY_LABELS: Record<string, string> = {
  ArrowUp: 'Up Arrow',
  ArrowDown: 'Down Arrow',
  ArrowLeft: 'Left Arrow',
  ArrowRight: 'Right Arrow',
  Enter: 'Enter',
  Escape: 'Escape',
  Space: 'Space',
}

export function bindingLabel(binding: string, iconStyle: ControllerIconStyle = 'xbox') {
  if (binding.startsWith('Button')) {
    const button = Number(binding.slice(6))
    const faceLabels: Record<ControllerIconStyle, Partial<Record<number, string>>> = {
      xbox: { 0: 'A', 1: 'B', 2: 'X', 3: 'Y' },
      playstation: { 0: '×', 1: '○', 2: '□', 3: '△' },
      nintendo: { 0: 'B', 1: 'A', 2: 'Y', 3: 'X' },
    }
    const shoulderLabels: Record<ControllerIconStyle, Partial<Record<number, string>>> = {
      xbox: { 4: 'LB', 5: 'RB', 6: 'LT', 7: 'RT', 8: 'View', 9: 'Start' },
      playstation: { 4: 'L1', 5: 'R1', 6: 'L2', 7: 'R2', 8: 'Share', 9: 'Options' },
      nintendo: { 4: 'L', 5: 'R', 6: 'ZL', 7: 'ZR', 8: 'Minus', 9: 'Plus' },
    }
    return faceLabels[iconStyle][button]
      ?? shoulderLabels[iconStyle][button]
      ?? BUTTON_LABELS[button]
      ?? `Button ${button}`
  }
  if (binding.startsWith('Axis')) {
    const axis = Number(binding.slice(4, -1))
    const direction = binding.endsWith('-') ? '-' : '+'
    const labels: Record<string, string> = {
      '0-': 'Left Stick Left',
      '0+': 'Left Stick Right',
      '1-': 'Left Stick Up',
      '1+': 'Left Stick Down',
      '2-': 'Right Stick Left',
      '2+': 'Right Stick Right',
      '3-': 'Right Stick Up',
      '3+': 'Right Stick Down',
    }
    return labels[`${axis}${direction}`] ?? `Axis ${axis} ${direction}`
  }
  if (KEY_LABELS[binding]) return KEY_LABELS[binding]
  if (binding.startsWith('Key') || binding.startsWith('Digit')) return binding.slice(-1)
  return binding
}

export function compactBindingLabel(
  binding: string,
  iconStyle: ControllerIconStyle = 'xbox',
) {
  const controllerLabels: Record<string, string> = {
    Button14: 'D-Pad Left',
    Button15: 'D-Pad Right',
  }
  return controllerLabels[binding] ?? bindingLabel(binding, iconStyle)
}

function gamepadTokens(gamepad: Gamepad) {
  const tokens = new Set<string>()
  gamepad.buttons.forEach((button, index) => {
    if (button.pressed || button.value > 0.65) tokens.add(`Button${index}`)
  })
  gamepad.axes.forEach((value, index) => {
    if (value < -0.65) tokens.add(`Axis${index}-`)
    if (value > 0.65) tokens.add(`Axis${index}+`)
  })
  return tokens
}

function setInputValue(input: HTMLInputElement | HTMLTextAreaElement, nextValue: string) {
  const prototype = input instanceof HTMLTextAreaElement
    ? HTMLTextAreaElement.prototype
    : HTMLInputElement.prototype
  const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
  setter?.call(input, nextValue)
  input.dispatchEvent(new Event('input', { bubbles: true }))
}

export function InputProvider({ children }: { children: ReactNode }) {
  const [bindings, setBindings] = useState(loadBindings)
  const [capture, setCapture] = useState<CaptureState>(null)
  const [lastDevice, setLastDevice] = useState<InputDevice>('pc')
  const [preferences, setPreferences] = useState(loadPreferences)
  const [keyboardInput, setKeyboardInput] = useState<HTMLInputElement | HTMLTextAreaElement | null>(null)
  const [keyboardShift, setKeyboardShift] = useState(false)
  const bindingsRef = useRef(bindings)
  const preferencesRef = useRef(preferences)
  const captureRef = useRef(capture)
  const keyboardInputRef = useRef(keyboardInput)
  const previousTokensRef = useRef(new Set<string>())
  const repeatRef = useRef<Record<string, number>>({})
  const lastCombatNavigationRef = useRef(0)

  useEffect(() => {
    bindingsRef.current = bindings
    localStorage.setItem(STORAGE_KEY, JSON.stringify(bindings))
  }, [bindings])

  useEffect(() => {
    localStorage.setItem(PREFERENCES_STORAGE_KEY, JSON.stringify(preferences))
    preferencesRef.current = preferences
  }, [preferences])

  useEffect(() => {
    captureRef.current = capture
  }, [capture])

  useEffect(() => {
    keyboardInputRef.current = keyboardInput
  }, [keyboardInput])

  const assignBinding = useCallback((device: InputDevice, action: InputAction, token: string) => {
    setBindings((current) => {
      const nextDevice = { ...current[device] }
      const previousToken = nextDevice[action]
      const collision = INPUT_ACTIONS.find(
        (candidate) => (
          candidate !== action
          && bindingGroup(candidate) === bindingGroup(action)
          && nextDevice[candidate] === token
        ),
      )
      if (collision) nextDevice[collision] = previousToken
      nextDevice[action] = token
      return { ...current, [device]: nextDevice }
    })
    setCapture(null)
  }, [])

  const closeKeyboard = useCallback(() => {
    const input = keyboardInputRef.current
    setKeyboardInput(null)
    window.requestAnimationFrame(() => input?.focus({ preventScroll: true }))
  }, [])

  const dispatchAction = useCallback((action: InputAction, device: InputDevice) => {
    const uiOverlay = hasUiOverlay()
    const combatActive = Boolean(document.querySelector('[data-combat-active="true"]'))
    if (combatActive && !uiOverlay && isCombatTargetAction(action)) {
      const now = performance.now()
      if (now - lastCombatNavigationRef.current < COMBAT_TARGET_NAVIGATION_THROTTLE_MS) return
      lastCombatNavigationRef.current = now
    }

    setLastDevice(device)
    document.documentElement.dataset.inputDevice = device

    if (action.startsWith('navigate')) {
      if (uiOverlay || !combatActive) moveFocus(action)
    } else if (action === 'confirm') {
      const active = document.activeElement
      if (isTextInput(active)) {
        setKeyboardInput(active)
        window.requestAnimationFrame(() => focusFirstControl())
      } else if (
        active instanceof HTMLElement
        && active.matches('button:not(:disabled), [role="button"]')
        && isVisible(active)
      ) {
        active.click()
      } else {
        focusFirstControl()
      }
    } else if (action === 'back') {
      if (keyboardInputRef.current) {
        closeKeyboard()
      } else if (uiOverlay || !combatActive) {
        const backButton = Array.from(
          document.querySelectorAll<HTMLButtonElement>('.back-button:not(:disabled)'),
        ).find(isVisible)
        backButton?.click()
      }
    }

    window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
      detail: { action, device },
    }))
  }, [closeKeyboard])

  const dispatchControllerToken = useCallback((token: string, repeat = false) => {
    if (captureRef.current?.device === 'controller') {
      if (!repeat) assignBinding('controller', captureRef.current.action, token)
      return
    }
    if (captureRef.current) return

    const combatActive = Boolean(
      document.querySelector('[data-combat-active="true"]'),
    )
    const uiOverlay = hasUiOverlay()
    const menuDpadActions: Partial<Record<string, InputAction>> = {
      Button12: 'navigateUp',
      Button13: 'navigateDown',
      Button14: 'navigateLeft',
      Button15: 'navigateRight',
    }
    const uiPriority = [
      'navigateUp',
      'navigateDown',
      'navigateLeft',
      'navigateRight',
      'confirm',
      'back',
    ] satisfies InputAction[]
    const directTargetActions = [
      'targetParty1',
      'targetParty2',
      'targetParty3',
      'targetParty4',
      'targetParty5',
      'targetParty6',
      'toggleTargetGroup',
    ] satisfies InputAction[]
    const combatPriority = [
      'pause',
      'ability1',
      'ability2',
      'ability3',
      'ability4',
      'ability5',
      'ability6',
      'previousTarget',
      'nextTarget',
      'navigateUp',
      'navigateDown',
      'navigateLeft',
      'navigateRight',
    ] satisfies InputAction[]
    const action = menuDpadActions[token] && (!combatActive || uiOverlay)
      ? menuDpadActions[token]
      : uiOverlay
      ? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token)
      : combatActive && preferencesRef.current.directPartyTargeting
      ? [...directTargetActions, ...combatPriority].find(
          (candidate) => bindingsRef.current.controller[candidate] === token,
        )
      : combatActive && menuDpadActions[token]
        ? menuDpadActions[token]
      : !combatActive && menuDpadActions[token]
      ? menuDpadActions[token]
      : (combatActive ? combatPriority : INPUT_ACTIONS).find(
          (candidate) => bindingsRef.current.controller[candidate] === token,
        )
    if (!action) return
    if (repeat && !action.startsWith('navigate')) return
    dispatchAction(action, 'controller')
  }, [assignBinding, dispatchAction])

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      const active = document.activeElement
      if (captureRef.current?.device === 'pc') {
        event.preventDefault()
        if (event.code === 'Escape') {
          setCapture(null)
          return
        }
        assignBinding('pc', captureRef.current.action, event.code)
        return
      }
      if (captureRef.current || (isTextInput(active) && !keyboardInputRef.current)) return
      const action = INPUT_ACTIONS.find(
        (candidate) => bindingsRef.current.pc[candidate] === event.code,
      )
      if (!action) return
      event.preventDefault()
      if (event.repeat && !action.startsWith('navigate')) return
      dispatchAction(action, 'pc')
    }
    window.addEventListener('keydown', onKeyDown)
    return () => window.removeEventListener('keydown', onKeyDown)
  }, [assignBinding, dispatchAction])

  useEffect(() => {
    const listener = (event: Event) => {
      const detail = (event as CustomEvent<{ token: string; repeat?: boolean }>).detail
      dispatchControllerToken(detail.token, Boolean(detail.repeat))
    }
    window.addEventListener(NATIVE_CONTROLLER_EVENT, listener)
    return () => window.removeEventListener(NATIVE_CONTROLLER_EVENT, listener)
  }, [dispatchControllerToken])

  useEffect(() => {
    const ensureFocus = () => {
      const combatActive = document.querySelector('[data-combat-active="true"]')
      if (combatActive) return
      const candidates = focusableElements()
      const active = document.activeElement
      const activeIsUsable = active instanceof HTMLElement
        && candidates.includes(active)
        && isVisible(active)
      if (
        (!activeIsUsable || document.activeElement === document.body)
        && !keyboardInputRef.current
        && !captureRef.current
      ) {
        focusFirstControl()
      }
    }
    const observer = new MutationObserver(() => {
      window.requestAnimationFrame(ensureFocus)
    })
    observer.observe(document.getElementById('root') ?? document.body, {
      attributes: true,
      attributeFilter: ['aria-hidden', 'class', 'disabled', 'hidden', 'style'],
      childList: true,
      subtree: true,
    })
    window.requestAnimationFrame(ensureFocus)
    return () => observer.disconnect()
  }, [])

  useEffect(() => {
    let frame = 0
    const poll = (time: number) => {
      const gamepad = Array.from(navigator.getGamepads?.() ?? []).find(Boolean)
      const currentTokens = gamepad ? gamepadTokens(gamepad) : new Set<string>()
      const previousTokens = previousTokensRef.current

      currentTokens.forEach((token) => {
        const pressed = !previousTokens.has(token)
        if (pressed && captureRef.current?.device === 'controller') {
          assignBinding('controller', captureRef.current.action, token)
          return
        }
        if (captureRef.current) return
        const action = INPUT_ACTIONS.find(
          (candidate) => bindingsRef.current.controller[candidate] === token,
        )
        const canRepeat = action?.startsWith('navigate') ?? false
        const nextRepeat = repeatRef.current[token] ?? 0
        if (pressed || (canRepeat && time >= nextRepeat)) {
          dispatchControllerToken(token, !pressed)
          repeatRef.current[token] = time + (pressed ? 360 : 125)
        }
      })

      Object.keys(repeatRef.current).forEach((token) => {
        if (!currentTokens.has(token)) delete repeatRef.current[token]
      })
      previousTokensRef.current = currentTokens
      frame = window.requestAnimationFrame(poll)
    }
    frame = window.requestAnimationFrame(poll)
    return () => window.cancelAnimationFrame(frame)
  }, [assignBinding, dispatchControllerToken])

  const contextValue = useMemo<InputContextValue>(() => ({
    bindings,
    capture,
    lastDevice,
    controllerIconStyle: preferences.controllerIconStyle,
    directPartyTargeting: preferences.directPartyTargeting,
    beginCapture: (device, action) => setCapture({ device, action }),
    cancelCapture: () => setCapture(null),
    resetBindings: (device) => setBindings((current) => ({
      ...current,
      [device]: { ...DEFAULT_BINDINGS[device] },
    })),
    setControllerIconStyle: (controllerIconStyle) => setPreferences((current) => ({
      ...current,
      controllerIconStyle,
    })),
    setDirectPartyTargeting: (directPartyTargeting) => setPreferences((current) => ({
      ...current,
      directPartyTargeting,
    })),
  }), [bindings, capture, lastDevice, preferences])

  function typeKeyboardKey(key: string) {
    if (!keyboardInput) return
    const start = keyboardInput.selectionStart ?? keyboardInput.value.length
    const end = keyboardInput.selectionEnd ?? start
    if (key === 'backspace') {
      const from = start === end ? Math.max(0, start - 1) : start
      setInputValue(keyboardInput, keyboardInput.value.slice(0, from) + keyboardInput.value.slice(end))
      window.requestAnimationFrame(() => keyboardInput.setSelectionRange(from, from))
      return
    }
    const value = key === 'space' ? ' ' : keyboardShift ? key.toUpperCase() : key
    const next = keyboardInput.value.slice(0, start) + value + keyboardInput.value.slice(end)
    const maxLength = keyboardInput.maxLength > 0 ? keyboardInput.maxLength : Number.POSITIVE_INFINITY
    const limited = next.slice(0, maxLength)
    setInputValue(keyboardInput, limited)
    const cursor = Math.min(start + value.length, limited.length)
    window.requestAnimationFrame(() => keyboardInput.setSelectionRange(cursor, cursor))
  }

  const keyboardKeys = [
    ...'1234567890',
    ...'qwertyuiop',
    ...'asdfghjkl',
    ...'zxcvbnm',
    '_', '-', '@', '.', '!', '?', '#', '$',
  ]

  return (
    <InputContext.Provider value={contextValue}>
      {children}
      {keyboardInput && (
        <div className="controller-keyboard-backdrop" role="presentation">
          <section className="controller-keyboard" aria-label="On-screen keyboard">
            <div className="controller-keyboard-heading">
              <div>
                <p className="eyebrow">Controller Keyboard</p>
                <strong>{keyboardInput.value || 'Enter text'}</strong>
              </div>
              <button onClick={closeKeyboard} type="button">Done</button>
            </div>
            <div className="controller-keyboard-grid">
              {keyboardKeys.map((key) => (
                <button key={key} onClick={() => typeKeyboardKey(key)} type="button">
                  {keyboardShift ? key.toUpperCase() : key}
                </button>
              ))}
            </div>
            <div className="controller-keyboard-actions">
              <button
                className={keyboardShift ? 'active' : ''}
                onClick={() => setKeyboardShift((value) => !value)}
                type="button"
              >
                Shift
              </button>
              <button onClick={() => typeKeyboardKey('space')} type="button">Space</button>
              <button onClick={() => typeKeyboardKey('backspace')} type="button">Backspace</button>
              <button onClick={closeKeyboard} type="button">Done</button>
            </div>
          </section>
        </div>
      )}
    </InputContext.Provider>
  )
}

export function useInput() {
  const context = useContext(InputContext)
  if (!context) throw new Error('useInput must be used inside InputProvider')
  return context
}

export function useGameAction(
  handler: (action: InputAction, device: InputDevice) => void,
) {
  const handlerRef = useRef(handler)
  useEffect(() => {
    handlerRef.current = handler
  }, [handler])
  useEffect(() => {
    const listener = (event: Event) => {
      const detail = (event as CustomEvent<{ action: InputAction; device: InputDevice }>).detail
      handlerRef.current(detail.action, detail.device)
    }
    window.addEventListener(GAME_ACTION_EVENT, listener)
    return () => window.removeEventListener(GAME_ACTION_EVENT, listener)
  }, [])
}

export function dispatchExternalGameAction(
  action: InputAction,
  device: InputDevice,
) {
  window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
    detail: { action, device },
  }))
}
