import { getLogger } from '@deltachat-desktop/shared/logger'
import { runtime } from '@deltachat-desktop/runtime-interface'

const log = getLogger('renderer/keybindings')

export enum KeybindAction {
  AccountsList_SelectNextAccount = 'accountslist:select-next-chat',
  AccountsList_SelectPreviousAccount = 'accountslist:select-previous-chat',
  ChatList_SelectNextChat = 'chatlist:select-next-chat',
  ChatList_SelectPreviousChat = 'chatlist:select-previous-chat',
  ChatList_ScrollToSelectedChat = 'chatlist:scroll-to-selected-chat',
  /**
   * "Items" instead of "Chats" because searching might show
   * messages and contacts and not just chats.
   */
  ChatList_FocusItems = 'chatlist:focus-items',
  // ChatList_SelectFirstChat = 'chatlist:select-first-chat',
  ChatList_FocusSearchInput = 'chatlist:focus-search',
  ChatList_SearchInChat = 'chatlist:search-in-chat',
  ChatList_ClearSearchInput = 'chatlist:clear-search',
  Composer_Focus = 'composer:focus',
  Composer_SelectReplyToUp = 'composer:select-reply-to-up',
  Composer_SelectReplyToDown = 'composer:select-reply-to-down',
  Composer_CancelReply = 'composer:cancel-reply',
  NewChat_Open = 'new-chat:open',
  CommandPalette_Open = 'command-palette:open',
  CommandPalette_OpenSearch = 'command-palette:open-search',
  Settings_Open = 'settings:open',
  KeybindingCheatSheet_Open = 'keybindinginfo:open',
  MessageList_PageUp = 'msglist:pageup',
  MessageList_PageDown = 'msglist:pagedown',
  GlobalGallery_Open = 'globalgallery:open',

  // Actions that are not necessarily triggered by keybindings
  ChatList_SwitchToArchiveView = 'chatlist:switch-to-archive-view',
  ChatList_SwitchToNormalView = 'chatlist:switch-to-normal-view',
  AboutDialog_Open = 'about:open',
  Chat_Unselect = 'chat:unselect',

  // Composite Actions (actions that trigger other actions)
  // ChatList_FocusAndClearSearchInput = 'chatlist:focus-and-clear-search',
  ChatList_ExitSearch = 'chatlist:exit-search',
  // ChatList_SearchSelectFirstChat = 'chatlist:search-select-first-chat',

  // Debug
  Debug_MaybeNetwork = 'debug:maybe_network',
}

export namespace ActionEmitter {
  const handlers: Partial<{ [key in KeybindAction]: any[] }> = {}

  export function registerHandler(action: KeybindAction, handler: () => void) {
    if (!Array.isArray(handlers[action])) {
      handlers[action] = []
    }
    ;(handlers[action] as any[]).push(handler)
  }

  export function unRegisterHandler(
    action: KeybindAction,
    handler: () => void
  ) {
    if (!Array.isArray(handlers[action])) {
      return
    }
    handlers[action] = (handlers[action] as any[]).filter(h => h !== handler)
  }

  export function emitAction(action: KeybindAction) {
    if (!Array.isArray(handlers[action])) {
      return
    }
    log.debug('fire action', action, 'handlers:', handlers[action])
    ;(handlers[action] as any[]).forEach(handler => handler())
  }
}

/**
 * Detect if the keyboard layout produces Latin letters for letter keys.
 * On Latin-based layouts (US, German, French), letter keys produce a-z.
 * On non-Latin layouts (Russian, Greek, Arabic), letter keys produce other scripts.
 */
function isLatinLayout(
  ev: KeyboardEvent | React.KeyboardEvent<Element>
): boolean {
  // If we're pressing a letter key (KeyA-KeyZ), check if it produces a Latin letter
  if (ev.code.startsWith('Key') && ev.code.length === 4) {
    return /^[a-zA-Z]$/.test(ev.key)
  }
  // For non-letter keys, we can't determine layout from this event
  return true
}

/**
 * Helper for letter-based shortcuts (Ctrl+F, Ctrl+N, etc.)
 * Works correctly for both Latin layouts (German, French) and non-Latin layouts (Russian, Greek).
 * - On Latin layouts: matches by ev.key (the character produced)
 * - On non-Latin layouts: falls back to ev.code (physical key position)
 */
export function matchesLetterShortcut(
  ev: KeyboardEvent | React.KeyboardEvent<Element>,
  letter: string
): boolean {
  const lower = letter.toLowerCase()
  const code = `Key${letter.toUpperCase()}`

  if (ev.key.toLowerCase() === lower) {
    return true // Direct match (works for all Latin layouts)
  }
  // Fallback to physical position for non-Latin layouts
  return !isLatinLayout(ev) && ev.code === code
}

/**
 * Helper for non-letter shortcuts (Ctrl+/, Ctrl+,, etc.)
 * Matches by character, with fallback to physical position only for non-ASCII keys.
 * This prevents capturing unintended shortcuts like Ctrl+- when we want Ctrl+/
 * (on German keyboards, the Slash position produces '-').
 */
export function matchesNonLetterShortcut(
  ev: KeyboardEvent | React.KeyboardEvent<Element>,
  symbol: string,
  code: string
): boolean {
  if (ev.key === symbol) {
    return true // Direct character match
  }
  // Only fall back to code if ev.key is NOT a printable ASCII character.
  // This prevents capturing e.g. Ctrl+- when we want Ctrl+/
  const isPrintableAscii = ev.key.length === 1 && ev.key >= ' ' && ev.key <= '~'
  return !isPrintableAscii && ev.code === code
}
const regexIsPrintable = /\S| /
function isPrintableCharacter(str: KeyboardEvent['key']): boolean {
  // From https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
  return str.length === 1 && regexIsPrintable.test(str)
}

function isInput(el: HTMLElement): boolean {
  return (
    el.tagName === 'TEXTAREA' ||
    el.tagName === 'INPUT' ||
    el.tagName === 'SELECT' ||
    el.isContentEditable
  )
}

function haveOpenDialogs(): boolean {
  return document.querySelector('dialog:modal') != null
}

export function keyDownEvent2Action(
  ev: KeyboardEvent
): KeybindAction | undefined {
  const { isMac } = runtime.getRuntimeInfo()

  // If a dialog is open, disable shortcuts.
  // This also applies to a context menu being open.
  //
  // Perhaps there are some shortcuts that we don't strictly need to disable
  // when a dialog is open, such as `Settings_Open` and `Debug_MaybeNetwork`,
  // but it's probably not important to support that,
  // so let's not complicate things yet.
  if (
    (ev.target instanceof HTMLElement && ev.target.closest('dialog') != null) ||
    // `ev.target.closest('dialog')` doesn't always work as of writing
    // because of `allowDefaultFocus` in `Dialog.tsx`,
    // which `.blur()`s the element inside the dialog
    // and thus makes `<body>` the event target.
    haveOpenDialogs()
  ) {
    return
  }
  // Don't capture keys during IME composition (Chinese, Japanese, Korean input)
  if (ev.isComposing) {
    return
  }
  // When modifying this, don't forget to also update the corresponding
  // `aria-keyshortcuts` properties, and the "Keybindings" help window.
  if (!ev.repeat) {
    // fire only on first press
    if (ev.altKey && ev.code === 'ArrowDown') {
      return KeybindAction.ChatList_SelectNextChat
    } else if (ev.altKey && ev.code === 'ArrowUp') {
      return KeybindAction.ChatList_SelectPreviousChat
    } else if (ev.ctrlKey && ev.code === 'PageDown') {
      if (ev.altKey) {
        return KeybindAction.AccountsList_SelectNextAccount
      } else {
        return KeybindAction.ChatList_SelectNextChat
      }
    } else if (ev.ctrlKey && ev.code === 'PageUp') {
      if (ev.altKey) {
        return KeybindAction.AccountsList_SelectPreviousAccount
      } else {
        return KeybindAction.ChatList_SelectPreviousChat
      }
    } else if (ev.ctrlKey && ev.code === 'Tab') {
      return !ev.shiftKey
        ? KeybindAction.ChatList_SelectNextChat
        : KeybindAction.ChatList_SelectPreviousChat
      // } else if (ev.altKey && ev.code === 'ArrowLeft') {
      // disabled until we find a better keycombination (see https://github.com/deltachat/deltachat-desktop/issues/1796)
      //   return KeybindAction.ChatList_ScrollToSelectedChat
      // }
    } else if (
      (isMac ? ev.metaKey && !ev.ctrlKey : ev.ctrlKey) &&
      matchesLetterShortcut(ev, 'f')
    ) {
      // https://github.com/deltachat/deltachat-desktop/issues/4579
      if (ev.shiftKey) {
        return KeybindAction.ChatList_SearchInChat
      }
      return KeybindAction.ChatList_FocusSearchInput
    } else if ((ev.metaKey || ev.ctrlKey) && matchesLetterShortcut(ev, 'n')) {
      return KeybindAction.NewChat_Open
    } else if ((ev.metaKey || ev.ctrlKey) && matchesLetterShortcut(ev, 'k')) {
      return KeybindAction.CommandPalette_OpenSearch
    } else if ((ev.metaKey || ev.ctrlKey) && matchesLetterShortcut(ev, 'p')) {
      return KeybindAction.CommandPalette_Open
    } else if (ev.ctrlKey && matchesLetterShortcut(ev, 'm')) {
      return KeybindAction.Composer_Focus
    } else if (
      // Also consider adding this to `ev.repeat` when it stops being so sluggish
      ev.code === 'ArrowUp' &&
      (ev.ctrlKey || ev.metaKey) &&
      !(ev.ctrlKey && ev.metaKey) && // Both at the same time
      (ev.target as HTMLElement)?.id === 'composer-textarea-non-edit'
    ) {
      return KeybindAction.Composer_SelectReplyToUp
    } else if (
      ev.code === 'ArrowDown' &&
      (ev.ctrlKey || ev.metaKey) &&
      !(ev.ctrlKey && ev.metaKey) && // Both at the same time
      (ev.target as HTMLElement)?.id === 'composer-textarea-non-edit'
    ) {
      return KeybindAction.Composer_SelectReplyToDown
    } else if (
      (ev.metaKey || ev.ctrlKey) &&
      matchesNonLetterShortcut(ev, ',', 'Comma')
    ) {
      return KeybindAction.Settings_Open
    } else if (ev.code === 'Escape') {
      if ((ev.target as any).id === 'chat-list-search') {
        // Only handle Escape key events from chat list search input
        // because others are handled in Composer and Dialogs already
        return KeybindAction.ChatList_ExitSearch
      }
    } else if (
      (ev.target as any).id === 'chat-list-search' &&
      (ev.key === 'Enter' || ev.code === 'ArrowDown')
    ) {
      // return KeybindAction.ChatList_SearchSelectFirstChat
      return KeybindAction.ChatList_FocusItems
    } else if (ev.code === 'F5') {
      return KeybindAction.Debug_MaybeNetwork
    } else if (ev.code === 'PageUp') {
      if (
        (ev.target as HTMLElement)?.classList.contains(
          'create-or-edit-message-input'
        )
      ) {
        return KeybindAction.MessageList_PageUp
      }
    } else if (ev.code === 'PageDown') {
      if (
        (ev.target as HTMLElement)?.classList.contains(
          'create-or-edit-message-input'
        )
      ) {
        return KeybindAction.MessageList_PageDown
      }
    } else if (
      (ev.metaKey || ev.ctrlKey) &&
      matchesNonLetterShortcut(ev, '/', 'Slash')
    ) {
      return KeybindAction.KeybindingCheatSheet_Open
    } else if (
      // When the user tries to type but the composer is not focused, focus it.
      //
      // Accessibility-wise this should be OK, because
      // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
      // has similar behavior, see the "Printable Characters" part.
      //
      // See also similar code in Element
      // https://github.com/element-hq/element-web/blob/c57d66bb45a89451efefc1fcb2c3131040670da6/apps/web/src/components/structures/LoggedInView.tsx#L680-L706
      //
      // Don't check Shift (uppercase), and apparently Alt
      // also needs to be skipped, because it's a common accent modifier.
      !ev.ctrlKey &&
      !ev.metaKey &&
      !(
        document.activeElement instanceof HTMLElement &&
        isInput(document.activeElement)
      ) &&
      // Regular keyboard "click".
      ev.code !== 'Space' &&
      ev.code !== 'Enter' &&
      isPrintableCharacter(ev.key)
    ) {
      // Not `Composer_Focus` because that focuses the composer
      // only after a `setTimeout`, which would not result
      // in this keystroke getting typed.
      const composer = document.getElementsByClassName(
        'create-or-edit-message-input'
      )[0]
      if (!(composer instanceof HTMLElement)) {
        // Could happen if the composer is not displayed,
        // e.g. for read-only chats (e.g. channels).
        log.info(
          "Tried to focus composer on typing start, but it's not present",
          composer
        )
        return
      }

      composer.focus()
    }
  } else {
    // fire continuesly as long as button is pressed
    if (ev.ctrlKey && ev.code === 'PageDown') {
      if (ev.altKey) {
        return KeybindAction.AccountsList_SelectNextAccount
      } else {
        return KeybindAction.ChatList_SelectNextChat
      }
    } else if (ev.ctrlKey && ev.code === 'PageUp') {
      if (ev.altKey) {
        return KeybindAction.AccountsList_SelectPreviousAccount
      } else {
        return KeybindAction.ChatList_SelectPreviousChat
      }
    } else if (ev.code === 'PageUp') {
      if (
        (ev.target as HTMLElement)?.classList.contains(
          'create-or-edit-message-input'
        )
      ) {
        return KeybindAction.MessageList_PageUp
      }
    } else if (ev.code === 'PageDown') {
      if (
        (ev.target as HTMLElement)?.classList.contains(
          'create-or-edit-message-input'
        )
      ) {
        return KeybindAction.MessageList_PageDown
      }
    } else if (ev.altKey && ev.code === 'ArrowDown') {
      return KeybindAction.ChatList_SelectNextChat
    } else if (ev.altKey && ev.code === 'ArrowUp') {
      return KeybindAction.ChatList_SelectPreviousChat
    }
  }
}

// Implementation of Composite Actions (actions that trigger other actions)

// ActionEmitter.registerHandler(
//   KeybindAction.ChatList_FocusAndClearSearchInput,
//   () => {
//     ActionEmitter.emitAction(KeybindAction.ChatList_FocusSearchInput)
//     ActionEmitter.emitAction(KeybindAction.ChatList_ClearSearchInput)
//   }
// )

ActionEmitter.registerHandler(KeybindAction.ChatList_ExitSearch, () => {
  ActionEmitter.emitAction(KeybindAction.ChatList_ClearSearchInput)
  ActionEmitter.emitAction(KeybindAction.Composer_Focus)
})

// ActionEmitter.registerHandler(
//   KeybindAction.ChatList_SearchSelectFirstChat,
//   () => {
//     ActionEmitter.emitAction(KeybindAction.ChatList_SelectFirstChat)
//     ActionEmitter.emitAction(KeybindAction.Composer_Focus)
//   }
// )
