/*
 * noVNC: HTML5 VNC client
 * Copyright (C) 2019 The noVNC Authors
 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
 */

import * as Log from '../util/logging.js';
import { stopEvent } from '../util/events.js';
import * as KeyboardUtil from './util.js';
import KeyTable from './keysym.js';
import * as browser from '../util/browser.js';

//
// Keyboard event handler
//

export default class Keyboard {
  constructor(target) {
    this._target = target || null;

    this._keyDownList = {}; // List of depressed keys
    // (even if they are happy)
    this._altGrArmed = false; // Windows AltGr detection

    // keep these here so we can refer to them later
    this._eventHandlers = {
      keyup: this._handleKeyUp.bind(this),
      keydown: this._handleKeyDown.bind(this),
      blur: this._allKeysUp.bind(this),
    };

    // ===== EVENT HANDLERS =====

    this.onkeyevent = () => {}; // Handler for key press/release
  }

  // ===== PRIVATE METHODS =====

  _sendKeyEvent(keysym, code, down) {
    if (down) {
      this._keyDownList[code] = keysym;
    } else {
      // Do we really think this key is down?
      if (!(code in this._keyDownList)) {
        return;
      }
      delete this._keyDownList[code];
    }

    Log.Debug('onkeyevent ' + (down ? 'down' : 'up') + ', keysym: ' + keysym, ', code: ' + code);
    this.onkeyevent(keysym, code, down);
  }

  _getKeyCode(e) {
    const code = KeyboardUtil.getKeycode(e);
    if (code !== 'Unidentified') {
      return code;
    }

    // Unstable, but we don't have anything else to go on
    if (e.keyCode) {
      // 229 is used for composition events
      if (e.keyCode !== 229) {
        return 'Platform' + e.keyCode;
      }
    }

    // A precursor to the final DOM3 standard. Unfortunately it
    // is not layout independent, so it is as bad as using keyCode
    if (e.keyIdentifier) {
      // Non-character key?
      if (e.keyIdentifier.substr(0, 2) !== 'U+') {
        return e.keyIdentifier;
      }

      const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
      const char = String.fromCharCode(codepoint).toUpperCase();

      return 'Platform' + char.charCodeAt();
    }

    return 'Unidentified';
  }

  _handleKeyDown(e) {
    const code = this._getKeyCode(e);
    let keysym = KeyboardUtil.getKeysym(e);

    // Windows doesn't have a proper AltGr, but handles it using
    // fake Ctrl+Alt. However the remote end might not be Windows,
    // so we need to merge those in to a single AltGr event. We
    // detect this case by seeing the two key events directly after
    // each other with a very short time between them (<50ms).
    if (this._altGrArmed) {
      this._altGrArmed = false;
      clearTimeout(this._altGrTimeout);

      if (code === 'AltRight' && e.timeStamp - this._altGrCtrlTime < 50) {
        // FIXME: We fail to detect this if either Ctrl key is
        //        first manually pressed as Windows then no
        //        longer sends the fake Ctrl down event. It
        //        does however happily send real Ctrl events
        //        even when AltGr is already down. Some
        //        browsers detect this for us though and set the
        //        key to "AltGraph".
        keysym = KeyTable.XK_ISO_Level3_Shift;
      } else {
        this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
      }
    }

    // We cannot handle keys we cannot track, but we also need
    // to deal with virtual keyboards which omit key info
    if (code === 'Unidentified') {
      if (keysym) {
        // If it's a virtual keyboard then it should be
        // sufficient to just send press and release right
        // after each other
        this._sendKeyEvent(keysym, code, true);
        this._sendKeyEvent(keysym, code, false);
      }

      stopEvent(e);
      return;
    }

    // Alt behaves more like AltGraph on macOS, so shuffle the
    // keys around a bit to make things more sane for the remote
    // server. This method is used by RealVNC and TigerVNC (and
    // possibly others).
    if (browser.isMac() || browser.isIOS()) {
      switch (keysym) {
        case KeyTable.XK_Super_L:
          keysym = KeyTable.XK_Alt_L;
          break;
        case KeyTable.XK_Super_R:
          keysym = KeyTable.XK_Super_L;
          break;
        case KeyTable.XK_Alt_L:
          keysym = KeyTable.XK_Mode_switch;
          break;
        case KeyTable.XK_Alt_R:
          keysym = KeyTable.XK_ISO_Level3_Shift;
          break;
      }
    }

    // Is this key already pressed? If so, then we must use the
    // same keysym or we'll confuse the server
    if (code in this._keyDownList) {
      keysym = this._keyDownList[code];
    }

    // macOS doesn't send proper key releases if a key is pressed
    // while meta is held down
    if ((browser.isMac() || browser.isIOS()) && e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight') {
      this._sendKeyEvent(keysym, code, true);
      this._sendKeyEvent(keysym, code, false);
      stopEvent(e);
      return;
    }

    // macOS doesn't send proper key events for modifiers, only
    // state change events. That gets extra confusing for CapsLock
    // which toggles on each press, but not on release. So pretend
    // it was a quick press and release of the button.
    if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') {
      this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
      this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
      stopEvent(e);
      return;
    }

    // Windows doesn't send proper key releases for a bunch of
    // Japanese IM keys so we have to fake the release right away
    const jpBadKeys = [
      KeyTable.XK_Zenkaku_Hankaku,
      KeyTable.XK_Eisu_toggle,
      KeyTable.XK_Katakana,
      KeyTable.XK_Hiragana,
      KeyTable.XK_Romaji,
    ];
    if (browser.isWindows() && jpBadKeys.includes(keysym)) {
      this._sendKeyEvent(keysym, code, true);
      this._sendKeyEvent(keysym, code, false);
      stopEvent(e);
      return;
    }

    stopEvent(e);

    // Possible start of AltGr sequence? (see above)
    if (code === 'ControlLeft' && browser.isWindows() && !('ControlLeft' in this._keyDownList)) {
      this._altGrArmed = true;
      this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
      this._altGrCtrlTime = e.timeStamp;
      return;
    }

    this._sendKeyEvent(keysym, code, true);
  }

  _handleKeyUp(e) {
    stopEvent(e);

    const code = this._getKeyCode(e);

    // We can't get a release in the middle of an AltGr sequence, so
    // abort that detection
    if (this._altGrArmed) {
      this._altGrArmed = false;
      clearTimeout(this._altGrTimeout);
      this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
    }

    // See comment in _handleKeyDown()
    if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') {
      this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
      this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
      return;
    }

    this._sendKeyEvent(this._keyDownList[code], code, false);

    // Windows has a rather nasty bug where it won't send key
    // release events for a Shift button if the other Shift is still
    // pressed
    if (browser.isWindows() && (code === 'ShiftLeft' || code === 'ShiftRight')) {
      if ('ShiftRight' in this._keyDownList) {
        this._sendKeyEvent(this._keyDownList['ShiftRight'], 'ShiftRight', false);
      }
      if ('ShiftLeft' in this._keyDownList) {
        this._sendKeyEvent(this._keyDownList['ShiftLeft'], 'ShiftLeft', false);
      }
    }
  }

  _handleAltGrTimeout() {
    this._altGrArmed = false;
    clearTimeout(this._altGrTimeout);
    this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true);
  }

  _allKeysUp() {
    Log.Debug('>> Keyboard.allKeysUp');
    for (let code in this._keyDownList) {
      this._sendKeyEvent(this._keyDownList[code], code, false);
    }
    Log.Debug('<< Keyboard.allKeysUp');
  }

  // ===== PUBLIC METHODS =====

  grab() {
    //Log.Debug(">> Keyboard.grab");

    this._target.addEventListener('keydown', this._eventHandlers.keydown);
    this._target.addEventListener('keyup', this._eventHandlers.keyup);

    // Release (key up) if window loses focus
    window.addEventListener('blur', this._eventHandlers.blur);

    //Log.Debug("<< Keyboard.grab");
  }

  ungrab() {
    //Log.Debug(">> Keyboard.ungrab");

    this._target.removeEventListener('keydown', this._eventHandlers.keydown);
    this._target.removeEventListener('keyup', this._eventHandlers.keyup);
    window.removeEventListener('blur', this._eventHandlers.blur);

    // Release (key up) all keys that are in a down state
    this._allKeysUp();

    //Log.Debug(">> Keyboard.ungrab");
  }
}
