import { Arr, Type, type Optional } from '@ephox/katamari';
import { PlatformDetection } from '@ephox/sand';

const isMac = PlatformDetection.detect().os.isMacOS();
const isFirefox = PlatformDetection.detect().browser.isFirefox();

interface KeyInfo {
  readonly keyCode: number;
  readonly code: string;
  readonly key: string;
  readonly data?: string;
  readonly shiftKey?: boolean;
}

const keys: KeyInfo[] = [
  { keyCode: 8, code: 'Backspace', key: 'Backspace' },
  { keyCode: 9, code: 'Tab', key: 'Tab' },
  { keyCode: 13, code: 'Enter', key: 'Enter' },
  { keyCode: 16, code: 'ShiftLeft', key: 'Shift', shiftKey: true },
  { keyCode: 16, code: 'ShiftRight', key: 'Shift', shiftKey: true },
  { keyCode: 17, code: 'ControlLeft', key: 'Control' },
  { keyCode: 17, code: 'ControlRight', key: 'Control' },
  { keyCode: 18, code: 'AltLeft', key: 'Alt' },
  { keyCode: 18, code: 'AltRight', key: 'Alt' },
  { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
  { keyCode: 27, code: 'Escape', key: 'Escape' },
  { keyCode: 32, code: 'Space', key: ' ', data: ' ' },
  { keyCode: 32, code: 'Space', key: ' ', data: '\u00a0' }, // Special handling for non-breaking space when we go from data to event
  { keyCode: 33, code: 'PageUp', key: 'PageUp' },
  { keyCode: 34, code: 'PageDown', key: 'PageDown' },
  { keyCode: 35, code: 'End', key: 'End' },
  { keyCode: 36, code: 'Home', key: 'Home' },
  { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
  { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
  { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
  { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
  { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
  { keyCode: 45, code: 'Insert', key: 'Insert' },
  { keyCode: 46, code: 'Delete', key: 'Delete' },
  { keyCode: 48, code: 'Digit0', key: '0', data: '0' },
  { keyCode: 48, code: 'Digit0', key: ')', data: ')', shiftKey: true },
  { keyCode: 49, code: 'Digit1', key: '1', data: '1' },
  { keyCode: 49, code: 'Digit1', key: '!', data: '!', shiftKey: true },
  { keyCode: 50, code: 'Digit2', key: '2', data: '2' },
  { keyCode: 50, code: 'Digit2', key: '@', data: '@', shiftKey: true },
  { keyCode: 51, code: 'Digit3', key: '3', data: '3' },
  { keyCode: 51, code: 'Digit3', key: '#', data: '#', shiftKey: true },
  { keyCode: 52, code: 'Digit4', key: '4', data: '4' },
  { keyCode: 52, code: 'Digit4', key: '$', data: '$', shiftKey: true },
  { keyCode: 53, code: 'Digit5', key: '5', data: '5' },
  { keyCode: 53, code: 'Digit5', key: '%', data: '%', shiftKey: true },
  { keyCode: 54, code: 'Digit6', key: '6', data: '6' },
  { keyCode: 54, code: 'Digit6', key: '^', data: '^', shiftKey: true },
  { keyCode: 55, code: 'Digit7', key: '7', data: '7' },
  { keyCode: 55, code: 'Digit7', key: '&', data: '&', shiftKey: true },
  { keyCode: 56, code: 'Digit8', key: '8', data: '8' },
  { keyCode: 56, code: 'Digit8', key: '*', data: '*', shiftKey: true },
  { keyCode: 57, code: 'Digit9', key: '9', data: '9' },
  { keyCode: 57, code: 'Digit9', key: '(', data: '(', shiftKey: true },
  { keyCode: 65, code: 'KeyA', key: 'a', data: 'a' },
  { keyCode: 65, code: 'KeyA', key: 'A', data: 'A', shiftKey: true },
  { keyCode: 66, code: 'KeyB', key: 'b', data: 'b' },
  { keyCode: 66, code: 'KeyB', key: 'B', data: 'B', shiftKey: true },
  { keyCode: 67, code: 'KeyC', key: 'c', data: 'c' },
  { keyCode: 67, code: 'KeyC', key: 'C', data: 'C', shiftKey: true },
  { keyCode: 68, code: 'KeyD', key: 'd', data: 'd' },
  { keyCode: 68, code: 'KeyD', key: 'D', data: 'D', shiftKey: true },
  { keyCode: 69, code: 'KeyE', key: 'e', data: 'e' },
  { keyCode: 69, code: 'KeyE', key: 'E', data: 'E', shiftKey: true },
  { keyCode: 70, code: 'KeyF', key: 'f', data: 'f' },
  { keyCode: 70, code: 'KeyF', key: 'F', data: 'F', shiftKey: true },
  { keyCode: 71, code: 'KeyG', key: 'g', data: 'g' },
  { keyCode: 71, code: 'KeyG', key: 'G', data: 'G', shiftKey: true },
  { keyCode: 72, code: 'KeyH', key: 'h', data: 'h' },
  { keyCode: 72, code: 'KeyH', key: 'H', data: 'H', shiftKey: true },
  { keyCode: 73, code: 'KeyI', key: 'i', data: 'i' },
  { keyCode: 73, code: 'KeyI', key: 'I', data: 'I', shiftKey: true },
  { keyCode: 74, code: 'KeyJ', key: 'j', data: 'j' },
  { keyCode: 74, code: 'KeyJ', key: 'J', data: 'J', shiftKey: true },
  { keyCode: 75, code: 'KeyK', key: 'k', data: 'k' },
  { keyCode: 75, code: 'KeyK', key: 'K', data: 'K', shiftKey: true },
  { keyCode: 76, code: 'KeyL', key: 'l', data: 'l' },
  { keyCode: 76, code: 'KeyL', key: 'L', data: 'L', shiftKey: true },
  { keyCode: 77, code: 'KeyM', key: 'm', data: 'm' },
  { keyCode: 77, code: 'KeyM', key: 'M', data: 'M', shiftKey: true },
  { keyCode: 78, code: 'KeyN', key: 'n', data: 'n' },
  { keyCode: 78, code: 'KeyN', key: 'N', data: 'N', shiftKey: true },
  { keyCode: 79, code: 'KeyO', key: 'o', data: 'o' },
  { keyCode: 79, code: 'KeyO', key: 'O', data: 'O', shiftKey: true },
  { keyCode: 80, code: 'KeyP', key: 'p', data: 'p' },
  { keyCode: 80, code: 'KeyP', key: 'P', data: 'P', shiftKey: true },
  { keyCode: 81, code: 'KeyQ', key: 'q', data: 'q' },
  { keyCode: 81, code: 'KeyQ', key: 'Q', data: 'Q', shiftKey: true },
  { keyCode: 82, code: 'KeyR', key: 'r', data: 'r' },
  { keyCode: 82, code: 'KeyR', key: 'R', data: 'R', shiftKey: true },
  { keyCode: 83, code: 'KeyS', key: 's', data: 's' },
  { keyCode: 83, code: 'KeyS', key: 'S', data: 'S', shiftKey: true },
  { keyCode: 84, code: 'KeyT', key: 't', data: 't' },
  { keyCode: 84, code: 'KeyT', key: 'T', data: 'T', shiftKey: true },
  { keyCode: 85, code: 'KeyU', key: 'u', data: 'u' },
  { keyCode: 85, code: 'KeyU', key: 'U', data: 'U', shiftKey: true },
  { keyCode: 86, code: 'KeyV', key: 'v', data: 'v' },
  { keyCode: 86, code: 'KeyV', key: 'V', data: 'V', shiftKey: true },
  { keyCode: 87, code: 'KeyW', key: 'w', data: 'w' },
  { keyCode: 87, code: 'KeyW', key: 'W', data: 'W', shiftKey: true },
  { keyCode: 88, code: 'KeyX', key: 'x', data: 'x' },
  { keyCode: 88, code: 'KeyX', key: 'X', data: 'X', shiftKey: true },
  { keyCode: 89, code: 'KeyY', key: 'y', data: 'y' },
  { keyCode: 89, code: 'KeyY', key: 'Y', data: 'Y', shiftKey: true },
  { keyCode: 90, code: 'KeyZ', key: 'z', data: 'z' },
  { keyCode: 90, code: 'KeyZ', key: 'Z', data: 'Z', shiftKey: true },
  // Firefox Mac returns 224 for the keycode for meta
  { keyCode: isFirefox && isMac ? 224 : 91, code: 'MetaLeft', key: 'Meta' },
  { keyCode: 92, code: 'MetaRight', key: 'Meta' },
  { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
  { keyCode: 96, code: 'Numpad0', key: '0', data: '0' },
  { keyCode: 97, code: 'Numpad1', key: '1', data: '1' },
  { keyCode: 98, code: 'Numpad2', key: '2', data: '2' },
  { keyCode: 99, code: 'Numpad3', key: '3', data: '3' },
  { keyCode: 100, code: 'Numpad4', key: '4', data: '4' },
  { keyCode: 101, code: 'Numpad5', key: '5', data: '5' },
  { keyCode: 102, code: 'Numpad6', key: '6', data: '6' },
  { keyCode: 103, code: 'Numpad7', key: '7', data: '7' },
  { keyCode: 104, code: 'Numpad8', key: '8', data: '8' },
  { keyCode: 105, code: 'Numpad9', key: '9', data: '9' },
  { keyCode: 106, code: 'NumpadMultiply', key: '*', data: '*' },
  { keyCode: 107, code: 'NumpadAdd', key: '+', data: '+' },
  { keyCode: 109, code: 'NumpadSubtract', key: '-', data: '-' },
  { keyCode: 110, code: 'NumpadDecimal', key: '.', data: '.' },
  { keyCode: 111, code: 'NumpadDivide', key: '/', data: '/' },
  { keyCode: 112, code: 'F1', key: 'F1' },
  { keyCode: 113, code: 'F2', key: 'F2' },
  { keyCode: 114, code: 'F3', key: 'F3' },
  { keyCode: 115, code: 'F4', key: 'F4' },
  { keyCode: 116, code: 'F5', key: 'F5' },
  { keyCode: 117, code: 'F6', key: 'F6' },
  { keyCode: 118, code: 'F7', key: 'F7' },
  { keyCode: 119, code: 'F8', key: 'F8' },
  { keyCode: 120, code: 'F9', key: 'F9' },
  { keyCode: 121, code: 'F10', key: 'F10' },
  { keyCode: 122, code: 'F11', key: 'F11' },
  { keyCode: 123, code: 'F12', key: 'F12' },
  { keyCode: 144, code: 'NumLock', key: 'NumLock' },
  { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
  { keyCode: 186, code: 'Semicolon', key: ';', data: ';' },
  { keyCode: 186, code: 'Semicolon', key: ':', data: ':', shiftKey: true },
  { keyCode: 187, code: 'Equal', key: '=', data: '=' },
  { keyCode: 188, code: 'Comma', key: ',', data: ',' },
  { keyCode: 188, code: 'Comma', key: '<', data: '<', shiftKey: true },
  { keyCode: 189, code: 'Minus', key: '-', data: '-' },
  { keyCode: 189, code: 'Minus', key: '_', data: '_', shiftKey: true },
  { keyCode: 190, code: 'Period', key: '.', data: '.' },
  { keyCode: 190, code: 'Period', key: '>', data: '>', shiftKey: true },
  { keyCode: 191, code: 'Slash', key: '/', data: '/' },
  { keyCode: 191, code: 'Slash', key: '?', data: '?', shiftKey: true },
  { keyCode: 192, code: 'Backquote', key: '`', data: '`' },
  { keyCode: 192, code: 'Backquote', key: '~', data: '~', shiftKey: true },
  { keyCode: 219, code: 'BracketLeft', key: '[', data: '[' },
  { keyCode: 219, code: 'BracketLeft', key: '{', data: '{', shiftKey: true },
  { keyCode: 220, code: 'Backslash', key: '\\', data: '\\' },
  { keyCode: 220, code: 'Backslash', key: '|', data: '|', shiftKey: true },
  { keyCode: 221, code: 'BracketRight', key: ']', data: ']' },
  { keyCode: 221, code: 'BracketRight', key: '}', data: '}', shiftKey: true },
  { keyCode: 222, code: 'Quote', key: '\'', data: '\'' },
  { keyCode: 222, code: 'Quote', key: '"', data: '"', shiftKey: true }
];

const createKeyboardEvent = (
  view: typeof globalThis,
  event: string,
  ctrlKey: boolean,
  altKey: boolean,
  shiftKey: boolean,
  metaKey: boolean,
  key: KeyInfo
) => {
  if (event === 'keypress') {
    const data = key.data;

    if (Type.isUndefined(data)) {
      throw new Error('Unable to create keypress event from key without data: ' + JSON.stringify(key));
    }

    const charCode = data.charCodeAt(0);

    return new view.KeyboardEvent(event, {
      key: key.key,
      code: key.code,
      keyCode: charCode,
      which: charCode,
      charCode,
      shiftKey: key.shiftKey === true || shiftKey,
      cancelable: true,
      bubbles: true,
      ctrlKey,
      altKey,
      metaKey
    });
  } else {
    return new view.KeyboardEvent(event, {
      key: key.key,
      code: key.code,
      keyCode: key.keyCode,
      which: key.keyCode,
      shiftKey: key.shiftKey === true || shiftKey,
      cancelable: true,
      bubbles: true,
      ctrlKey,
      altKey,
      metaKey
    });
  }
};

export const getKeyEventFromKeyCode = (
  view: typeof globalThis,
  event: string,
  ctrlKey: boolean,
  altKey: boolean,
  shiftKey: boolean,
  metaKey: boolean,
  keyCode: number
): Optional<KeyboardEvent> => {
  if (shiftKey) {
    return Arr.find(keys, (key) => key.keyCode === keyCode && key.shiftKey === true).orThunk(() =>
      Arr.find(keys, (key) => key.keyCode === keyCode)
    ).map((key) => createKeyboardEvent(view, event, ctrlKey, altKey, shiftKey, metaKey, key));
  } else {
    return Arr.find(keys, (key) => key.keyCode === keyCode)
      .map((key) => createKeyboardEvent(view, event, ctrlKey, altKey, shiftKey, metaKey, key));
  }
};

export const getKeyEventFromData = (view: typeof globalThis, event: string, data: string): Optional<KeyboardEvent> =>
  Arr.find(keys, (key) => key.data === data).map((key) => createKeyboardEvent(view, event, false, false, false, false, key));
