import { screen, globalShortcut } from 'electron'
import robotjs from 'robotjs'
import { uIOhook, UiohookKey, UiohookWheelEvent } from 'uiohook-napi'
import { pollClipboard } from './poll-clipboard'
import { priceCheckConfig, showWidget as showPriceCheck } from './price-check'
import { isModKey, KeyToElectron, mergeTwoHotkeys } from '../../ipc/KeyToCode'
import { config } from './config'
import { PoeWindow } from './PoeWindow'
import { logger } from './logger'
import { toggleOverlayState, assertOverlayActive, assertPoEActive, overlayOnEvent, overlaySendEvent } from './overlay-window'
import type * as ipc from '../../ipc/ipc-event'
import type * as widget from '../../ipc/widgets'
import { typeInChat } from './game-chat'
import { gameConfig } from './game-config'
import { restoreClipboard } from './clipboard-saver'

export const UiohookToName = Object.fromEntries(Object.entries(UiohookKey).map(([k, v]) => ([v, k])))

export interface ShortcutAction {
  shortcut: string
  keepModKeys?: true
  action: {
    type: 'copy-item'
    eventName: (
      ipc.IpcOpenWiki['name'] |
      ipc.IpcOpenCraftOfExile['name'] |
      ipc.IpcItemCheck['name'] |
      'price-check-quick' |
      'price-check-locked'
    )
    focusOverlay?: boolean
  } | ({
    type: 'trigger-event'
  } & (
    ShortcutActionTriggerEvent<ipc.IpcToggleDelveGrid['name']> |
    ShortcutActionTriggerEvent<ipc.IpcStopwatchAction['name']>
  )) | {
    type: 'stash-search'
    text: string
  } | {
    type: 'toggle-overlay'
  } | {
    type: 'paste-in-chat'
    text: string
    send: boolean
  } | {
    type: 'test-only'
  }
}

interface ShortcutActionTriggerEvent<Name extends ipc.IpcEvent['name']> {
  eventName: Name
  payload: ipc.IpcEventPayload<Name>
}

function shortcutsFromConfig () {
  let actions: ShortcutAction[] = []

  const priceCheckCfg = priceCheckConfig()
  if (priceCheckCfg.hotkey) {
    actions.push({
      shortcut: `${priceCheckCfg.hotkeyHold} + ${priceCheckCfg.hotkey}`,
      action: { type: 'copy-item', eventName: 'price-check-quick' },
      keepModKeys: true
    })
  }
  if (priceCheckCfg.hotkeyLocked) {
    actions.push({
      shortcut: priceCheckCfg.hotkeyLocked,
      action: { type: 'copy-item', eventName: 'price-check-locked' }
    })
  }
  actions.push({
    shortcut: config.get('overlayKey'),
    action: { type: 'toggle-overlay' },
    keepModKeys: true
  })
  if (config.get('wikiKey')) {
    actions.push({
      shortcut: config.get('wikiKey')!,
      action: { type: 'copy-item', eventName: 'MAIN->OVERLAY::open-wiki' }
    })
  }
  if (config.get('craftOfExileKey')) {
    actions.push({
      shortcut: config.get('craftOfExileKey')!,
      action: { type: 'copy-item', eventName: 'MAIN->OVERLAY::open-craft-of-exile' }
    })
  }
  if (config.get('itemCheckKey')) {
    actions.push({
      shortcut: config.get('itemCheckKey')!,
      action: { type: 'copy-item', eventName: 'MAIN->OVERLAY::item-check', focusOverlay: true }
    })
  }
  if (config.get('delveGridKey')) {
    actions.push({
      shortcut: config.get('delveGridKey')!,
      action: { type: 'trigger-event', eventName: 'MAIN->OVERLAY::delve-grid', payload: undefined },
      keepModKeys: true
    })
  }
  for (const command of config.get('commands')) {
    if (command.hotkey) {
      actions.push({
        shortcut: command.hotkey,
        action: { type: 'paste-in-chat', text: command.text, send: command.send }
      })
    }
  }
  const copyItemShortcut = mergeTwoHotkeys('Ctrl + C', gameConfig?.highlightKey || 'Alt')
  if (copyItemShortcut !== 'Ctrl + C') {
    actions.push({
      shortcut: copyItemShortcut,
      action: { type: 'test-only' }
    })
  }
  for (const widget of config.get('widgets')) {
    if (widget.wmType === 'stash-search') {
      const stashSearch = widget as widget.StashSearchWidget
      for (const entry of stashSearch.entries) {
        if (entry.hotkey) {
          actions.push({
            shortcut: entry.hotkey,
            action: { type: 'stash-search', text: entry.text }
          })
        }
      }
    } else if (widget.wmType === 'timer') {
      const stopwatch = widget as widget.StopwatchWidget
      if (stopwatch.toggleKey) {
        actions.push({
          shortcut: stopwatch.toggleKey,
          keepModKeys: true,
          action: {
            type: 'trigger-event',
            eventName: 'MAIN->OVERLAY::stopwatch',
            payload: { wmId: widget.wmId, type: 'start-stop' }
          }
        })
      }
      if (stopwatch.resetKey) {
        actions.push({
          shortcut: stopwatch.resetKey,
          keepModKeys: true,
          action: {
            type: 'trigger-event',
            eventName: 'MAIN->OVERLAY::stopwatch',
            payload: { wmId: widget.wmId, type: 'reset' }
          }
        })
      }
    }
  }

  {
    const allShortcuts = new Set([
      'Ctrl + C', 'Ctrl + V', 'Ctrl + A',
      'Ctrl + F',
      'Ctrl + Enter',
      'Home', 'Delete', 'Enter',
      'ArrowUp', 'ArrowRight', 'ArrowLeft',
      copyItemShortcut
    ])

    for (const action of actions) {
      if (allShortcuts.has(action.shortcut) && action.action.type !== 'test-only') {
        logger.error('Hotkey reserved by the game will not be registered.', { source: 'shortcuts', shortcut: action.shortcut })
      }
    }
    actions = actions.filter(action => !allShortcuts.has(action.shortcut))

    const duplicates = new Set<string>()
    for (const action of actions) {
      if (allShortcuts.has(action.shortcut)) {
        logger.error('It is not possible to use the same hotkey for multiple actions.', { source: 'shortcuts', shortcut: action.shortcut })
        duplicates.add(action.shortcut)
      } else {
        allShortcuts.add(action.shortcut)
      }
    }
    actions = actions.filter(action =>
      !duplicates.has(action.shortcut) ||
      action.action.type === 'toggle-overlay')
  }

  return actions
}

function registerGlobal () {
  const toRegister = shortcutsFromConfig()
  for (const entry of toRegister) {
    const isOk = globalShortcut.register(shortcutToElectron(entry.shortcut), () => {
      if (entry.keepModKeys) {
        const nonModKey = entry.shortcut.split(' + ').filter(key => !isModKey(key))[0]
        robotjs.keyToggle(nonModKey, 'up')
      } else {
        entry.shortcut.split(' + ').reverse().forEach(key => { robotjs.keyToggle(key, 'up') })
      }

      if (entry.action.type === 'toggle-overlay') {
        toggleOverlayState()
      } else if (entry.action.type === 'paste-in-chat') {
        typeInChat(entry.action.text, entry.action.send)
      } else if (entry.action.type === 'trigger-event') {
        overlaySendEvent({ name: entry.action.eventName, payload: entry.action.payload } as ipc.IpcEvent)
      } else if (entry.action.type === 'stash-search') {
        stashSearch(entry.action.text)
      } else if (entry.action.type === 'copy-item') {
        const { action } = entry

        const pressPosition = screen.getCursorScreenPoint()

        pollClipboard()
          .then(clipboard => {
            if (action.eventName === 'price-check-quick' || action.eventName === 'price-check-locked') {
              showPriceCheck({ clipboard, pressPosition, eventName: action.eventName })
            } else {
              overlaySendEvent({
                name: action.eventName,
                payload: { clipboard, position: pressPosition }
              })
              if (action.focusOverlay) {
                assertOverlayActive()
              }
            }
          }).catch(() => {})

        if (!entry.keepModKeys) {
          pressKeysToCopyItemText()
        } else {
          pressKeysToCopyItemText(entry.shortcut.split(' + ').filter(key => isModKey(key)))
        }
      }
    })

    if (!isOk) {
      logger.error('Failed to register a shortcut. It is already registered by another application.', { source: 'shortcuts', shortcut: entry.shortcut })
    }

    if (entry.action.type === 'test-only') {
      globalShortcut.unregister(shortcutToElectron(entry.shortcut))
    }
  }

  logger.verbose('Registered Global', { source: 'shortcuts', total: toRegister.length })
}

function unregisterGlobal () {
  globalShortcut.unregisterAll()
  logger.verbose('Unregistered Global', { source: 'shortcuts' })
}

function pressKeysToCopyItemText (pressedModKeys: string[] = []) {
  let keys = mergeTwoHotkeys('Ctrl + C', gameConfig?.highlightKey || 'Alt').split(' + ')
  keys = keys.filter(key => key !== 'C' && !pressedModKeys.includes(key))

  for (const key of keys) {
    robotjs.keyToggle(key, 'down')
  }

  // finally press `C` to copy text
  robotjs.keyTap('C')

  keys.reverse()
  for (const key of keys) {
    robotjs.keyToggle(key, 'up')
  }
}

export function setupShortcuts () {
  // A value of zero causes the thread to relinquish the remainder of its
  // time slice to any other thread that is ready to run. If there are no other
  // threads ready to run, the function returns immediately
  robotjs.setKeyboardDelay(0)

  if (PoeWindow.isActive) {
    registerGlobal()
  }
  PoeWindow.on('active-change', (isActive) => {
    process.nextTick(() => {
      if (isActive === PoeWindow.isActive) {
        if (isActive) {
          registerGlobal()
        } else {
          unregisterGlobal()
        }
      }
    })
  })

  overlayOnEvent('OVERLAY->MAIN::stash-search', (_, { text }) => { stashSearch(text) })

  uIOhook.on('keydown', (e) => {
    const pressed = eventToString(e)
    logger.debug('Keydown', { source: 'shortcuts', keys: pressed })
  })

  uIOhook.on('keyup', (e) => {
    logger.debug('Keyup', { source: 'shortcuts', key: UiohookToName[e.keycode] || 'unknown' })
  })

  uIOhook.on('wheel', (e) => {
    if (!e.ctrlKey || !PoeWindow.bounds || !PoeWindow.isActive || !config.get('stashScroll')) return

    if (!isGameScrolling(e)) {
      if (e.rotation > 0) {
        robotjs.keyTap('ArrowRight')
      } else if (e.rotation < 0) {
        robotjs.keyTap('ArrowLeft')
      }
    }
  })

  uIOhook.start()
}

function isGameScrolling (mouse: UiohookWheelEvent): boolean {
  if (!PoeWindow.bounds ||
      mouse.x > (PoeWindow.bounds.x + PoeWindow.uiSidebarWidth)) return false

  return (mouse.y > (PoeWindow.bounds.y + PoeWindow.bounds.height * 154 / 1600) &&
          mouse.y < (PoeWindow.bounds.y + PoeWindow.bounds.height * 1192 / 1600))
}

function stashSearch (text: string) {
  restoreClipboard((clipboard) => {
    assertPoEActive()
    clipboard.writeText(text)
    robotjs.keyTap('F', ['Ctrl'])
    robotjs.keyTap('V', ['Ctrl'])
    robotjs.keyTap('Enter')
  })
}

function eventToString (e: { keycode: number, ctrlKey: boolean, altKey: boolean, shiftKey: boolean }) {
  const { ctrlKey, shiftKey, altKey } = e

  let code = UiohookToName[e.keycode]
  if (!code) return 'unknown'

  if (code === 'Shift' || code === 'Alt' || code === 'Ctrl') return code

  if (ctrlKey && shiftKey && altKey) code = `Ctrl + Shift + Alt + ${code}`
  else if (shiftKey && altKey) code = `Shift + Alt + ${code}`
  else if (ctrlKey && shiftKey) code = `Ctrl + Shift + ${code}`
  else if (ctrlKey && altKey) code = `Ctrl + Alt + ${code}`
  else if (altKey) code = `Alt + ${code}`
  else if (ctrlKey) code = `Ctrl + ${code}`
  else if (shiftKey) code = `Shift + ${code}`

  return code
}

function shortcutToElectron (shortcut: string) {
  return shortcut
    .split(' + ')
    .map(k => KeyToElectron[k as keyof typeof KeyToElectron])
    .join('+')
}