import {
    createContext,
    useCallback,
    useContext,
    useEffect,
    useMemo,
    useRef,
    useState,
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'

import ShortcutHelpDialog from '@/components/users/ShortcutHelpDialog'
import {
    DEFAULT_KEY_BINDINGS,
    getConflictingShortcutActionIds,
    isEditableEventTarget,
    keyBindingFromKeyboardEvent,
    resolveShortcutScope,
    serialiseKeyBinding,
    SHORTCUT_DEFINITIONS,
    SHORTCUT_SCOPE_LABELS,
} from '@/lib/keyboardShortcuts'
import {
    getClientKeyboardSettings,
    getEffectiveKeyBindings,
    setClientKeyboardSettings,
} from '@/lib/settings'
import { useUnsavedChangesGuard } from '@/lib/useUnsavedChangesGuard'

import type {
    KeyBinding,
    ShortcutActionId,
    ShortcutScope,
} from '@/lib/keyboardShortcuts'
import type { ClientKeyboardSettings } from '@/lib/settings'
import type { PropsWithChildren } from 'react'

type ShortcutHandler = () => void
type ShortcutHandlers = Partial<Record<ShortcutActionId, ShortcutHandler>>

type KeyboardShortcutsContextValue = {
    activeScope:           ShortcutScope
    keyboardSettings:      ClientKeyboardSettings
    effectiveBindings:     Record<ShortcutActionId, KeyBinding | null>
    conflictActionIds:     Set<ShortcutActionId>
    saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
    registerHandlers:      (handlers: ShortcutHandlers) => () => void
}

const KeyboardShortcutsContext =
    createContext<KeyboardShortcutsContextValue | null> (null)


const isFocusableShortcutTarget = (element: HTMLElement): boolean => {
  if (element.closest ('[hidden], [aria-hidden="true"]') != null)
    return false

  if (
      (element instanceof HTMLInputElement
       || element instanceof HTMLButtonElement
       || element instanceof HTMLSelectElement
       || element instanceof HTMLTextAreaElement)
      && element.disabled
  )
    return false

  const style = window.getComputedStyle (element)
  if (
      style.display === 'none'
      || style.visibility === 'hidden'
      || style.pointerEvents === 'none'
  )
    return false

  return element.getClientRects ().length > 0
}


const focusSearchTarget = (): void => {
  const targets = [
      ...document.querySelectorAll<HTMLElement> (
	  '[data-shortcut-focus-search="true"]',
      ),
  ]
  const target = targets.find (isFocusableShortcutTarget)

  if (target == null)
    return

  target.focus ()

  if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
    target.select ()
}


export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
  const location = useLocation ()
  const navigate = useNavigate ()
  const { confirmDiscardNavigation } = useUnsavedChangesGuard ()

  const [keyboardSettings, setKeyboardSettingsState] =
      useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
  const [registeredHandlers, setRegisteredHandlers] =
      useState<Record<number, ShortcutHandlers>> ({ })
  const [shortcutHelpOpen, setShortcutHelpOpen] = useState (false)
  const registrationIdRef = useRef (0)

  const activeScope = useMemo<ShortcutScope> (
      () => resolveShortcutScope (location.pathname),
      [location.pathname],
  )
  const effectiveBindings = useMemo (
      () => getEffectiveKeyBindings (keyboardSettings.bindings),
      [keyboardSettings.bindings],
  )
  const conflictActionIds = useMemo (
      () => getConflictingShortcutActionIds (effectiveBindings),
      [effectiveBindings],
  )
  const mergedRegisteredHandlers = useMemo<ShortcutHandlers> (
      () => Object.values (registeredHandlers).reduce<ShortcutHandlers> (
	  (handlers, currentHandlers) => ({ ...handlers, ...currentHandlers }),
	  { },
      ),
      [registeredHandlers],
  )

  const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
    const next = setClientKeyboardSettings (settings)
    setKeyboardSettingsState (next)
  }, [])

  const registerHandlers = useCallback ((handlers: ShortcutHandlers) => {
    const registrationId = ++registrationIdRef.current

    setRegisteredHandlers (current => ({
	...current,
	[registrationId]: handlers,
    }))

    return () => {
      setRegisteredHandlers (current => {
	  const next = { ...current }
	  delete next[registrationId]
	  return next
      })
    }
  }, [])

  const guardedNavigate = useCallback ((path: string) => {
    confirmDiscardNavigation ().then (confirmed => {
      if (confirmed)
	navigate (path)
    })
  }, [confirmDiscardNavigation, navigate])

  const builtinHandlers = useMemo<ShortcutHandlers> (
      () => ({
	  'global.openShortcutHelp': () => {
	    setShortcutHelpOpen (true)
	  },
	  'global.focusSearch': () => {
	    focusSearchTarget ()
	  },
	  'navigation.posts': () => guardedNavigate ('/posts'),
	  'navigation.tags': () => guardedNavigate ('/tags'),
	  'navigation.materials': () => guardedNavigate ('/materials'),
	  'navigation.wiki': () => guardedNavigate ('/wiki'),
	  'settings.account': () => guardedNavigate ('/users/settings?tab=account'),
	  'settings.theme': () => guardedNavigate ('/users/settings?tab=theme'),
	  'settings.keyboard': () => guardedNavigate ('/users/settings?tab=keyboard'),
	  'settings.behavior': () => guardedNavigate ('/users/settings?tab=behavior'),
      }),
      [guardedNavigate],
  )

  const availableActionIds = useMemo<Set<ShortcutActionId>> (
      () => new Set (
	  SHORTCUT_DEFINITIONS
	      .filter (definition =>
		  definition.scope === 'global' || definition.scope === activeScope)
	      .filter (definition =>
		  builtinHandlers[definition.id] != null
		  || mergedRegisteredHandlers[definition.id] != null)
	      .map (definition => definition.id),
      ),
      [activeScope, builtinHandlers, mergedRegisteredHandlers],
  )

  useEffect (() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (
	  keyboardSettings.enabled === false
	  || event.defaultPrevented
	  || event.isComposing
	  || event.key === 'Process'
	  || event.repeat
      )
	return

      if (isEditableEventTarget (event.target))
	return

      if (shortcutHelpOpen)
	return

      const binding = keyBindingFromKeyboardEvent (event)
      if (binding == null)
	return

      const serialisedBinding = serialiseKeyBinding (binding)
      if (serialisedBinding == null)
	return

      const candidates = SHORTCUT_DEFINITIONS.filter (definition => (
	  definition.scope === 'global'
	  || definition.scope === activeScope
      )).filter (definition => {
	  const currentBinding = effectiveBindings[definition.id]
	  return (
	      currentBinding != null
	      && serialiseKeyBinding (currentBinding) === serialisedBinding
	  )
      }).filter (definition => !(conflictActionIds.has (definition.id)))

      if (candidates.length !== 1)
	return

      const actionId = candidates[0].id
      const handler = mergedRegisteredHandlers[actionId] ?? builtinHandlers[actionId]

      if (handler == null)
	return

      event.preventDefault ()
      handler ()
    }

    document.addEventListener ('keydown', onKeyDown)
    return () => document.removeEventListener ('keydown', onKeyDown)
  }, [
      activeScope,
      conflictActionIds,
      effectiveBindings,
      builtinHandlers,
      keyboardSettings.enabled,
      mergedRegisteredHandlers,
      shortcutHelpOpen,
  ])

  const value = useMemo<KeyboardShortcutsContextValue> (() => ({
      activeScope,
      keyboardSettings,
      effectiveBindings,
      conflictActionIds,
      saveKeyboardSettings,
      registerHandlers,
  }), [
      activeScope,
      conflictActionIds,
      effectiveBindings,
      keyboardSettings,
      registerHandlers,
      saveKeyboardSettings,
  ])

  return (
      <KeyboardShortcutsContext.Provider value={value}>
	{children}
	<ShortcutHelpDialog
	    open={shortcutHelpOpen}
	    onOpenChange={setShortcutHelpOpen}
	    activeScope={activeScope}
	    effectiveBindings={effectiveBindings}
	    conflictActionIds={conflictActionIds}
	    availableActionIds={availableActionIds}/>
      </KeyboardShortcutsContext.Provider>)
}


export const useKeyboardShortcutSettings = (): KeyboardShortcutsContextValue => {
  const context = useContext (KeyboardShortcutsContext)

  if (context == null)
    throw new Error ('KeyboardShortcutsProvider が必要です．')

  return context
}


export const useKeyboardShortcuts = (handlers: ShortcutHandlers): void => {
  const { registerHandlers } = useKeyboardShortcutSettings ()
  const handlersRef = useRef (handlers)
  const actionIdsKey = Object.keys (handlers).sort ().join ('|')

  useEffect (() => {
    handlersRef.current = handlers
  }, [handlers])

  const stableHandlers = useMemo<ShortcutHandlers> (() => {
    return actionIdsKey.split ('|').filter (actionId => actionId !== '')
	.reduce<ShortcutHandlers> ((nextHandlers, actionId) => {
	  const key = actionId as ShortcutActionId
	  nextHandlers[key] = () => {
	    handlersRef.current[key]?.()
	  }
	  return nextHandlers
	}, { })
  }, [actionIdsKey])

  useEffect (
      () => registerHandlers (stableHandlers),
      [registerHandlers, stableHandlers],
  )
}

export {
    DEFAULT_KEY_BINDINGS,
    SHORTCUT_DEFINITIONS,
    SHORTCUT_SCOPE_LABELS,
}
