import { Combobox, Dialog, Transition } from '@headlessui/react'
import {
  Dispatch,
  Fragment,
  SetStateAction,
  createContext,
  useContext,
  useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionIcon } from './ActionIcon'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { Command, SubCommand } from '../lib/commands'

export type SortedCommand = {
  item: Partial<Command | SubCommand> & { name: string }
}

export const CommandsContext = createContext(
  {} as {
    commands: Command[]
    addCommands: (commands: Command[]) => void
    removeCommands: (commands: Command[]) => void
    commandBarOpen: boolean
    setCommandBarOpen: Dispatch<SetStateAction<boolean>>
  }
)

const CommandBar = () => {
  const { commands, commandBarOpen, setCommandBarOpen } =
    useContext(CommandsContext)
  useHotkeys('meta+k', () => {
    if (commands.length === 0) return
    setCommandBarOpen(!commandBarOpen)
  })

  const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
    null
  )
  // keep track of the current subcommand index
  const [subCommandIndex, setSubCommandIndex] = useState<number>()
  const [subCommandData, setSubCommandData] = useState<{
    [key: string]: string
  }>({})

  // if the subcommand index is null, we're not in a subcommand
  const inSubCommand =
    selectedCommand &&
    'meta' in selectedCommand.item &&
    selectedCommand.item.meta?.args !== undefined &&
    subCommandIndex !== undefined
  const currentSubCommand =
    inSubCommand && 'meta' in selectedCommand.item
      ? selectedCommand.item.meta?.args[subCommandIndex]
      : undefined

  const [query, setQuery] = useState('')

  const availableCommands =
    inSubCommand && currentSubCommand
      ? currentSubCommand.type === 'string'
        ? query
          ? [{ name: query }]
          : currentSubCommand.options
        : currentSubCommand.options
      : commands

  const fuse = new Fuse(availableCommands || [], {
    keys: ['name', 'description'],
  })

  const filteredCommands = query
    ? fuse.search(query)
    : availableCommands?.map((c) => ({ item: c } as SortedCommand))

  function clearState() {
    setQuery('')
    setCommandBarOpen(false)
    setSelectedCommand(null)
    setSubCommandIndex(undefined)
    setSubCommandData({})
  }

  function handleCommandSelection(entry: SortedCommand) {
    // If we have subcommands and have not yet gathered all the
    // data required from them, set the selected command to the
    // current command and increment the subcommand index
    if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
      setSelectedCommand(entry)
      setSubCommandIndex(0)
      setQuery('')
      return
    }

    const { item } = entry
    // If we have just selected a command with no subcommands, run it
    const isCommandWithoutSubcommands =
      'callback' in item && !('meta' in item && item.meta)
    if (isCommandWithoutSubcommands) {
      if (item.callback === undefined) return
      item.callback()
      setCommandBarOpen(false)
      return
    }

    // If we have subcommands and have not yet gathered all the
    // data required from them, set the selected command to the
    // current command and increment the subcommand index
    if (
      selectedCommand &&
      subCommandIndex !== undefined &&
      'meta' in selectedCommand.item
    ) {
      const subCommand = selectedCommand.item.meta?.args[subCommandIndex]

      if (subCommand) {
        const newSubCommandData = {
          ...subCommandData,
          [subCommand.name]: item.name,
        }
        const newSubCommandIndex = subCommandIndex + 1

        // If we have subcommands and have gathered all the data required
        // from them, run the command with the gathered data
        if (
          selectedCommand.item.callback &&
          selectedCommand.item.meta?.args.length === newSubCommandIndex
        ) {
          selectedCommand.item.callback(newSubCommandData)
          setCommandBarOpen(false)
        } else {
          // Otherwise, set the subcommand data and increment the subcommand index
          setSubCommandData(newSubCommandData)
          setSubCommandIndex(newSubCommandIndex)
          setQuery('')
        }
      }
    }
  }

  function getDisplayValue(command: Command) {
    if (command.meta?.displayValue === undefined || !command.meta.args)
      return command.name
    return command.meta?.displayValue(
      command.meta.args.map((c) =>
        subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
      )
    )
  }

  return (
    <Transition.Root
      show={
        commandBarOpen &&
        availableCommands?.length !== undefined &&
        availableCommands.length > 0
      }
      as={Fragment}
      afterLeave={() => clearState()}
    >
      <Dialog
        onClose={() => {
          setCommandBarOpen(false)
          clearState()
        }}
        className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
      >
        <Transition.Child
          enter="duration-100 ease-out"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="duration-75 ease-in"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
          as={Fragment}
        >
          <Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
        </Transition.Child>
        <Transition.Child
          enter="duration-100 ease-out"
          enterFrom="opacity-0 scale-95"
          enterTo="opacity-100 scale-100"
          leave="duration-75 ease-in"
          leaveFrom="opacity-100 scale-100"
          leaveTo="opacity-0 scale-95"
          as={Fragment}
        >
          <Combobox
            value={selectedCommand}
            onChange={handleCommandSelection}
            className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
            as="div"
          >
            <div className="flex gap-2 items-center">
              <ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
              <div>
                {inSubCommand && (
                  <p className="text-liquid-70 dark:text-liquid-30">
                    {selectedCommand.item &&
                      getDisplayValue(selectedCommand.item as Command)}
                  </p>
                )}
                <Combobox.Input
                  onChange={(event) => setQuery(event.target.value)}
                  className="bg-transparent focus:outline-none w-full"
                  onKeyDown={(event) => {
                    if (event.metaKey && event.key === 'k')
                      setCommandBarOpen(false)
                    if (
                      inSubCommand &&
                      event.key === 'Backspace' &&
                      !event.currentTarget.value
                    ) {
                      setSubCommandIndex(subCommandIndex - 1)
                      setSelectedCommand(null)
                    }
                  }}
                  displayValue={(command: SortedCommand) =>
                    command !== null ? command.item.name : ''
                  }
                  placeholder={
                    inSubCommand
                      ? `Enter <${currentSubCommand?.name}>`
                      : 'Search for a command'
                  }
                  value={query}
                  autoCapitalize="off"
                  autoComplete="off"
                  autoCorrect="off"
                  spellCheck="false"
                />
              </div>
            </div>
            <Combobox.Options static className="max-h-96 overflow-y-auto">
              {filteredCommands?.map((commandResult) => (
                <Combobox.Option
                  key={commandResult.item.name}
                  value={commandResult}
                  className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
                >
                  <p>{commandResult.item.name}</p>
                  {(commandResult.item as SubCommand).description && (
                    <p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
                      {(commandResult.item as SubCommand).description}
                    </p>
                  )}
                </Combobox.Option>
              ))}
            </Combobox.Options>
          </Combobox>
        </Transition.Child>
      </Dialog>
    </Transition.Root>
  )
}

export default CommandBar
