/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable no-param-reassign */
/* eslint-disable no-restricted-syntax */
/* eslint-disable class-methods-use-this */
/* eslint-disable no-plusplus */
import type { Event } from '@difizen/mana-common';
import { Emitter, isWindows } from '@difizen/mana-common';
import { inject, optional, singleton } from '@difizen/mana-syringe';
import type { IWindowsKeyMapping } from 'native-keymap';

import type { NativeKeyboardLayout } from './keyboard-protocol';
import {
  KeyboardLayoutProvider,
  KeyboardLayoutChangeNotifier,
  KeyValidator,
} from './keyboard-protocol';
import { KeyCode, Key } from './keys';

export type KeyboardLayout = {
  /**
   * Mapping of standard US keyboard keys to the actual key codes to use.
   * See `KeyboardLayoutService.getCharacterIndex` for the index computation.
   */
  readonly key2KeyCode: KeyCode[];
  /**
   * Mapping of KeyboardEvent codes to the characters shown on the user's keyboard
   * for the respective keys.
   */
  readonly code2Character: Record<string, string>;
};

@singleton()
export class KeyboardLayoutService {
  protected readonly layoutProvider: KeyboardLayoutProvider;
  protected readonly layoutChangeNotifier: KeyboardLayoutChangeNotifier;
  protected readonly keyValidator?: KeyValidator | undefined;

  constructor(
    @inject(KeyboardLayoutProvider) layoutProvider: KeyboardLayoutProvider,
    @inject(KeyboardLayoutChangeNotifier)
    layoutChangeNotifier: KeyboardLayoutChangeNotifier,
    @inject(KeyValidator) @optional() keyValidator?: KeyValidator,
  ) {
    this.layoutProvider = layoutProvider;
    this.layoutChangeNotifier = layoutChangeNotifier;
    this.keyValidator = keyValidator;
  }

  private currentLayout?: KeyboardLayout;

  protected updateLayout(newLayout: NativeKeyboardLayout): KeyboardLayout {
    const transformed = this.transformNativeLayout(newLayout);
    this.currentLayout = transformed;
    this.keyboardLayoutChanged.fire(transformed);
    return transformed;
  }

  protected keyboardLayoutChanged = new Emitter<KeyboardLayout>();

  get onKeyboardLayoutChanged(): Event<KeyboardLayout> {
    return this.keyboardLayoutChanged.event;
  }

  async initialize(): Promise<void> {
    this.layoutChangeNotifier.onDidChangeNativeLayout(
      (newLayout: NativeKeyboardLayout) => this.updateLayout(newLayout),
    );
    const initialLayout = await this.layoutProvider.getNativeLayout();
    this.updateLayout(initialLayout);
  }

  /**
   * Resolve a KeyCode of a keybinding using the current keyboard layout.
   * If no keyboard layout has been detected or the layout does not contain the
   * key used in the KeyCode, the KeyCode is returned unchanged.
   */
  resolveKeyCode(inCode: KeyCode): KeyCode {
    const layout = this.currentLayout;
    if (layout && inCode.key) {
      for (let shift = 0; shift <= 1; shift++) {
        const index = this.getCharacterIndex(inCode.key, !!shift);
        const mappedCode = layout.key2KeyCode[index];
        if (mappedCode) {
          const transformed = this.transformKeyCode(inCode, mappedCode, !!shift);
          if (transformed) {
            return transformed;
          }
        }
      }
    }
    return inCode;
  }

  /**
   * Return the character shown on the user's keyboard for the given key.
   * Use this to determine UI representations of keybindings.
   */
  getKeyboardCharacter(key: Key): string {
    const layout = this.currentLayout;
    if (layout) {
      const value = layout.code2Character[key.code];
      if (value && value.replace(/[\n\r\t]/g, '')) {
        return value;
      }
    }
    return key.easyString;
  }

  /**
   * Called when a KeyboardEvent is processed by the KeybindingRegistry.
   * The KeyValidator may trigger a keyboard layout change.
   */
  validateKeyCode(keyCode: KeyCode): void {
    if (this.keyValidator && keyCode.key && keyCode.character) {
      this.keyValidator.validateKey({
        code: keyCode.key.code,
        character: keyCode.character,
        shiftKey: keyCode.shift,
        ctrlKey: keyCode.ctrl,
        altKey: keyCode.alt,
      });
    }
  }

  protected transformKeyCode(
    inCode: KeyCode,
    mappedCode: KeyCode,
    keyNeedsShift: boolean,
  ): KeyCode | undefined {
    if (!inCode.shift && keyNeedsShift) {
      return undefined;
    }
    if (
      mappedCode.alt &&
      (inCode.alt || inCode.ctrl || (inCode.shift && !keyNeedsShift))
    ) {
      return undefined;
    }
    return new KeyCode({
      key: mappedCode.key,
      meta: inCode.meta,
      ctrl: inCode.ctrl || mappedCode.alt,
      shift: (inCode.shift && !keyNeedsShift) || mappedCode.shift,
      alt: inCode.alt || mappedCode.alt,
    });
  }

  protected transformNativeLayout(nativeLayout: NativeKeyboardLayout): KeyboardLayout {
    const key2KeyCode: KeyCode[] = new Array(2 * (Key.MAX_KEY_CODE + 1));
    const code2Character: Record<string, string> = {};
    const { mapping } = nativeLayout;
    for (const code in mapping) {
      if (Object.prototype.hasOwnProperty.call(mapping, code)) {
        const keyMapping = mapping[code];
        const mappedKey = Key.getKey(code);
        if (mappedKey && this.shouldIncludeKey(code)) {
          if (isWindows) {
            this.addWindowsKeyMapping(
              key2KeyCode,
              mappedKey,
              (keyMapping as IWindowsKeyMapping).vkey,
              keyMapping.value,
            );
          } else {
            if (keyMapping.value) {
              this.addKeyMapping(
                key2KeyCode,
                mappedKey,
                keyMapping.value,
                false,
                false,
              );
            }
            if (keyMapping.withShift) {
              this.addKeyMapping(
                key2KeyCode,
                mappedKey,
                keyMapping.withShift,
                true,
                false,
              );
            }
            if (keyMapping.withAltGr) {
              this.addKeyMapping(
                key2KeyCode,
                mappedKey,
                keyMapping.withAltGr,
                false,
                true,
              );
            }
            if (keyMapping.withShiftAltGr) {
              this.addKeyMapping(
                key2KeyCode,
                mappedKey,
                keyMapping.withShiftAltGr,
                true,
                true,
              );
            }
          }
        }
        if (keyMapping.value) {
          code2Character[code] = keyMapping.value;
        }
      }
    }
    return { key2KeyCode, code2Character };
  }

  protected shouldIncludeKey(code: string): boolean {
    // Exclude all numpad keys because they produce values that are already found elsewhere on the keyboard.
    // This can cause problems, e.g. if `Numpad3` maps to `PageDown` then commands bound to `PageDown` would
    // be resolved to `Digit3` (`Numpad3` is associated with `Key.DIGIT3`), effectively blocking the user
    // from typing `3` in an editor.
    return !code.startsWith('Numpad');
  }

  private addKeyMapping(
    key2KeyCode: KeyCode[],
    mappedKey: Key,
    value: string,
    shift: boolean,
    alt: boolean,
  ): void {
    const key = VALUE_TO_KEY[value];
    if (key) {
      const index = this.getCharacterIndex(key.key, key.shift);
      if (key2KeyCode[index] === undefined) {
        key2KeyCode[index] = new KeyCode({
          key: mappedKey,
          shift,
          alt,
          character: value,
        });
      }
    }
  }

  private addWindowsKeyMapping(
    key2KeyCode: KeyCode[],
    mappedKey: Key,
    vkey: string,
    value: string,
  ): void {
    const key = VKEY_TO_KEY[vkey];
    if (key) {
      const index = this.getCharacterIndex(key);
      if (key2KeyCode[index] === undefined) {
        key2KeyCode[index] = new KeyCode({
          key: mappedKey,
          character: value,
        });
      }
    }
  }

  protected getCharacterIndex(key: Key, shift?: boolean): number {
    if (shift) {
      return Key.MAX_KEY_CODE + key.keyCode + 1;
    }
    return key.keyCode;
  }
}

/**
 * Mapping of character values to the corresponding keys on a standard US keyboard layout.
 */
const VALUE_TO_KEY: Record<string, { key: Key; shift?: boolean }> = {
  '`': { key: Key.BACKQUOTE },
  '~': { key: Key.BACKQUOTE, shift: true },
  '1': { key: Key.DIGIT1 },
  '!': { key: Key.DIGIT1, shift: true },
  '2': { key: Key.DIGIT2 },
  '@': { key: Key.DIGIT2, shift: true },
  '3': { key: Key.DIGIT3 },
  '#': { key: Key.DIGIT3, shift: true },
  '4': { key: Key.DIGIT4 },
  $: { key: Key.DIGIT4, shift: true },
  '5': { key: Key.DIGIT5 },
  '%': { key: Key.DIGIT5, shift: true },
  '6': { key: Key.DIGIT6 },
  '^': { key: Key.DIGIT6, shift: true },
  '7': { key: Key.DIGIT7 },
  '&': { key: Key.DIGIT7, shift: true },
  '8': { key: Key.DIGIT8 },
  '*': { key: Key.DIGIT8, shift: true },
  '9': { key: Key.DIGIT9 },
  '(': { key: Key.DIGIT9, shift: true },
  '0': { key: Key.DIGIT0 },
  ')': { key: Key.DIGIT0, shift: true },
  '-': { key: Key.MINUS },
  _: { key: Key.MINUS, shift: true },
  '=': { key: Key.EQUAL },
  '+': { key: Key.EQUAL, shift: true },

  a: { key: Key.KEY_A },
  A: { key: Key.KEY_A, shift: true },
  b: { key: Key.KEY_B },
  B: { key: Key.KEY_B, shift: true },
  c: { key: Key.KEY_C },
  C: { key: Key.KEY_C, shift: true },
  d: { key: Key.KEY_D },
  D: { key: Key.KEY_D, shift: true },
  e: { key: Key.KEY_E },
  E: { key: Key.KEY_E, shift: true },
  f: { key: Key.KEY_F },
  F: { key: Key.KEY_F, shift: true },
  g: { key: Key.KEY_G },
  G: { key: Key.KEY_G, shift: true },
  h: { key: Key.KEY_H },
  H: { key: Key.KEY_H, shift: true },
  i: { key: Key.KEY_I },
  I: { key: Key.KEY_I, shift: true },
  j: { key: Key.KEY_J },
  J: { key: Key.KEY_J, shift: true },
  k: { key: Key.KEY_K },
  K: { key: Key.KEY_K, shift: true },
  l: { key: Key.KEY_L },
  L: { key: Key.KEY_L, shift: true },
  m: { key: Key.KEY_M },
  M: { key: Key.KEY_M, shift: true },
  n: { key: Key.KEY_N },
  N: { key: Key.KEY_N, shift: true },
  o: { key: Key.KEY_O },
  O: { key: Key.KEY_O, shift: true },
  p: { key: Key.KEY_P },
  P: { key: Key.KEY_P, shift: true },
  q: { key: Key.KEY_Q },
  Q: { key: Key.KEY_Q, shift: true },
  r: { key: Key.KEY_R },
  R: { key: Key.KEY_R, shift: true },
  s: { key: Key.KEY_S },
  S: { key: Key.KEY_S, shift: true },
  t: { key: Key.KEY_T },
  T: { key: Key.KEY_T, shift: true },
  u: { key: Key.KEY_U },
  U: { key: Key.KEY_U, shift: true },
  v: { key: Key.KEY_V },
  V: { key: Key.KEY_V, shift: true },
  w: { key: Key.KEY_W },
  W: { key: Key.KEY_W, shift: true },
  x: { key: Key.KEY_X },
  X: { key: Key.KEY_X, shift: true },
  y: { key: Key.KEY_Y },
  Y: { key: Key.KEY_Y, shift: true },
  z: { key: Key.KEY_Z },
  Z: { key: Key.KEY_Z, shift: true },

  '[': { key: Key.BRACKET_LEFT },
  '{': { key: Key.BRACKET_LEFT, shift: true },
  ']': { key: Key.BRACKET_RIGHT },
  '}': { key: Key.BRACKET_RIGHT, shift: true },
  ';': { key: Key.SEMICOLON },
  ':': { key: Key.SEMICOLON, shift: true },
  "'": { key: Key.QUOTE },
  '"': { key: Key.QUOTE, shift: true },
  ',': { key: Key.COMMA },
  '<': { key: Key.COMMA, shift: true },
  '.': { key: Key.PERIOD },
  '>': { key: Key.PERIOD, shift: true },
  '/': { key: Key.SLASH },
  '?': { key: Key.SLASH, shift: true },
  '\\': { key: Key.BACKSLASH },
  '|': { key: Key.BACKSLASH, shift: true },

  '\t': { key: Key.TAB },
  '\r': { key: Key.ENTER },
  '\n': { key: Key.ENTER },
  ' ': { key: Key.SPACE },
};

/**
 * Mapping of Windows Virtual Keys to the corresponding keys on a standard US keyboard layout.
 */
const VKEY_TO_KEY: Record<string, Key> = {
  VK_SHIFT: Key.SHIFT_LEFT,
  VK_LSHIFT: Key.SHIFT_LEFT,
  VK_RSHIFT: Key.SHIFT_RIGHT,
  VK_CONTROL: Key.CONTROL_LEFT,
  VK_LCONTROL: Key.CONTROL_LEFT,
  VK_RCONTROL: Key.CONTROL_RIGHT,
  VK_MENU: Key.ALT_LEFT,
  VK_COMMAND: Key.OS_LEFT,
  VK_LWIN: Key.OS_LEFT,
  VK_RWIN: Key.OS_RIGHT,

  VK_0: Key.DIGIT0,
  VK_1: Key.DIGIT1,
  VK_2: Key.DIGIT2,
  VK_3: Key.DIGIT3,
  VK_4: Key.DIGIT4,
  VK_5: Key.DIGIT5,
  VK_6: Key.DIGIT6,
  VK_7: Key.DIGIT7,
  VK_8: Key.DIGIT8,
  VK_9: Key.DIGIT9,
  VK_A: Key.KEY_A,
  VK_B: Key.KEY_B,
  VK_C: Key.KEY_C,
  VK_D: Key.KEY_D,
  VK_E: Key.KEY_E,
  VK_F: Key.KEY_F,
  VK_G: Key.KEY_G,
  VK_H: Key.KEY_H,
  VK_I: Key.KEY_I,
  VK_J: Key.KEY_J,
  VK_K: Key.KEY_K,
  VK_L: Key.KEY_L,
  VK_M: Key.KEY_M,
  VK_N: Key.KEY_N,
  VK_O: Key.KEY_O,
  VK_P: Key.KEY_P,
  VK_Q: Key.KEY_Q,
  VK_R: Key.KEY_R,
  VK_S: Key.KEY_S,
  VK_T: Key.KEY_T,
  VK_U: Key.KEY_U,
  VK_V: Key.KEY_V,
  VK_W: Key.KEY_W,
  VK_X: Key.KEY_X,
  VK_Y: Key.KEY_Y,
  VK_Z: Key.KEY_Z,

  VK_OEM_1: Key.SEMICOLON,
  VK_OEM_2: Key.SLASH,
  VK_OEM_3: Key.BACKQUOTE,
  VK_OEM_4: Key.BRACKET_LEFT,
  VK_OEM_5: Key.BACKSLASH,
  VK_OEM_6: Key.BRACKET_RIGHT,
  VK_OEM_7: Key.QUOTE,
  VK_OEM_PLUS: Key.EQUAL,
  VK_OEM_COMMA: Key.COMMA,
  VK_OEM_MINUS: Key.MINUS,
  VK_OEM_PERIOD: Key.PERIOD,

  VK_F1: Key.F1,
  VK_F2: Key.F2,
  VK_F3: Key.F3,
  VK_F4: Key.F4,
  VK_F5: Key.F5,
  VK_F6: Key.F6,
  VK_F7: Key.F7,
  VK_F8: Key.F8,
  VK_F9: Key.F9,
  VK_F10: Key.F10,
  VK_F11: Key.F11,
  VK_F12: Key.F12,
  VK_F13: Key.F13,
  VK_F14: Key.F14,
  VK_F15: Key.F15,
  VK_F16: Key.F16,
  VK_F17: Key.F17,
  VK_F18: Key.F18,
  VK_F19: Key.F19,

  VK_BACK: Key.BACKSPACE,
  VK_TAB: Key.TAB,
  VK_RETURN: Key.ENTER,
  VK_CAPITAL: Key.CAPS_LOCK,
  VK_ESCAPE: Key.ESCAPE,
  VK_SPACE: Key.SPACE,
  VK_PRIOR: Key.PAGE_UP,
  VK_NEXT: Key.PAGE_DOWN,
  VK_END: Key.END,
  VK_HOME: Key.HOME,
  VK_INSERT: Key.INSERT,
  VK_DELETE: Key.DELETE,
  VK_LEFT: Key.ARROW_LEFT,
  VK_UP: Key.ARROW_UP,
  VK_RIGHT: Key.ARROW_RIGHT,
  VK_DOWN: Key.ARROW_DOWN,

  VK_NUMLOCK: Key.NUM_LOCK,
  VK_NUMPAD0: Key.DIGIT0,
  VK_NUMPAD1: Key.DIGIT1,
  VK_NUMPAD2: Key.DIGIT2,
  VK_NUMPAD3: Key.DIGIT3,
  VK_NUMPAD4: Key.DIGIT4,
  VK_NUMPAD5: Key.DIGIT5,
  VK_NUMPAD6: Key.DIGIT6,
  VK_NUMPAD7: Key.DIGIT7,
  VK_NUMPAD8: Key.DIGIT8,
  VK_NUMPAD9: Key.DIGIT9,
  VK_MULTIPLY: Key.MULTIPLY,
  VK_ADD: Key.ADD,
  VK_SUBTRACT: Key.SUBTRACT,
  VK_DECIMAL: Key.DECIMAL,
  VK_DIVIDE: Key.DIVIDE,
};
