/*
 * noVNC: HTML5 VNC client
 * Copyright (C) 2019 The noVNC Authors
 * Licensed under MPL 2.0 (see LICENSE.txt)
 *
 * See README.md for usage and integration instructions.
 */

import * as Log from "../core/util/logging.js";
import _, { l10n } from "./localization.js";
import {
  isTouchDevice,
  isSafari,
  hasScrollbarGutter,
  dragThreshold,
} from "../core/util/browser.js";
import { setCapture, getPointerEvent } from "../core/util/events.js";
import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js";
import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js";
import * as WebUtil from "./webutil.js";

const PAGE_TITLE = "noVNC";

const UI = {
  connected: false,
  desktopName: "",

  statusTimeout: null,
  hideKeyboardTimeout: null,
  idleControlbarTimeout: null,
  closeControlbarTimeout: null,

  controlbarGrabbed: false,
  controlbarDrag: false,
  controlbarMouseDownClientY: 0,
  controlbarMouseDownOffsetY: 0,

  lastKeyboardinput: null,
  defaultKeyboardinputLen: 100,

  inhibitReconnect: true,
  reconnectCallback: null,
  reconnectPassword: null,

  prime() {
    return WebUtil.initSettings().then(() => {
      if (
        document.readyState === "interactive" ||
        document.readyState === "complete"
      ) {
        return UI.start();
      }

      return new Promise((resolve, reject) => {
        document.addEventListener("DOMContentLoaded", () =>
          UI.start().then(resolve).catch(reject)
        );
      });
    });
  },

  // Render default UI and initialize settings menu
  start() {
    UI.initSettings();

    // Translate the DOM
    l10n.translateDOM();

    WebUtil.fetchJSON("./package.json")
      .then((packageInfo) => {
        Array.from(document.getElementsByClassName("noVNC_version")).forEach(
          (el) => (el.innerText = packageInfo.version)
        );
      })
      .catch((err) => {
        Log.Error("Couldn't fetch package.json: " + err);
        Array.from(document.getElementsByClassName("noVNC_version_wrapper"))
          .concat(
            Array.from(
              document.getElementsByClassName("noVNC_version_separator")
            )
          )
          .forEach((el) => (el.style.display = "none"));
      });

    // Adapt the interface for touch screen devices
    if (isTouchDevice) {
      document.documentElement.classList.add("noVNC_touch");
      // Remove the address bar
      setTimeout(() => window.scrollTo(0, 1), 100);
    }

    // Restore control bar position
    if (WebUtil.readSetting("controlbar_pos") === "right") {
      UI.toggleControlbarSide();
    }

    UI.initFullscreen();

    // Setup event handlers
    UI.addControlbarHandlers();
    UI.addTouchSpecificHandlers();
    UI.addExtraKeysHandlers();
    UI.addMachineHandlers();
    UI.addConnectionControlHandlers();
    UI.addClipboardHandlers();
    UI.addSettingsHandlers();
    document
      .getElementById("noVNC_status")
      .addEventListener("click", UI.hideStatus);

    // Bootstrap fallback input handler
    UI.keyboardinputReset();

    UI.openControlbar();

    UI.updateVisualState("init");

    document.documentElement.classList.remove("noVNC_loading");

    let autoconnect = WebUtil.getConfigVar("autoconnect", false);
    if (autoconnect === "true" || autoconnect == "1") {
      autoconnect = true;
      UI.connect();
    } else {
      autoconnect = false;
      // Show the connect panel on first load unless autoconnecting
      UI.openConnectPanel();
    }

    return Promise.resolve(UI.rfb);
  },

  initFullscreen() {
    // Only show the button if fullscreen is properly supported
    // * Safari doesn't support alphanumerical input while in fullscreen
    if (
      !isSafari() &&
      (document.documentElement.requestFullscreen ||
        document.documentElement.mozRequestFullScreen ||
        document.documentElement.webkitRequestFullscreen ||
        document.body.msRequestFullscreen)
    ) {
      document
        .getElementById("noVNC_fullscreen_button")
        .classList.remove("noVNC_hidden");
      UI.addFullscreenHandlers();
    }
  },

  initSettings() {
    // Logging selection dropdown
    const llevels = ["error", "warn", "info", "debug"];
    for (let i = 0; i < llevels.length; i += 1) {
      UI.addOption(
        document.getElementById("noVNC_setting_logging"),
        llevels[i],
        llevels[i]
      );
    }

    // Settings with immediate effects
    UI.initSetting("logging", "warn");
    UI.updateLogging();

    // if port == 80 (or 443) then it won't be present and should be
    // set manually
    let port = window.location.port;
    if (!port) {
      if (window.location.protocol.substring(0, 5) == "https") {
        port = 443;
      } else if (window.location.protocol.substring(0, 4) == "http") {
        port = 80;
      }
    }

    /* Populate the controls if defaults are provided in the URL */
    UI.initSetting("host", window.location.hostname);
    UI.initSetting("port", port);
    UI.initSetting("encrypt", window.location.protocol === "https:");
    UI.initSetting("view_clip", false);
    //UI.initSetting("resize", "off");
    UI.initSetting("quality", 6);
    UI.initSetting("compression", 2);
    UI.initSetting("shared", true);
    UI.initSetting("view_only", false);
    UI.initSetting("show_dot", false);
    //UI.initSetting("path", "websockify");
    UI.initSetting("repeaterID", "");
    //UI.initSetting("reconnect", false);
    UI.initSetting("reconnect_delay", 5000);

    // ######################## CUSTOM CODE ########################
    // IMPORTANT: All the init settings from above need to be commented out
    // Otherwise the changes here will not work
    UI.initSetting("resize", "remote");
    UI.initSetting("reconnect", true);
    // update settings cannot be used anymore to force settings
    WebUtil.setSetting(
      "path",
      window.location.pathname
        .substring(0, window.location.pathname.lastIndexOf("/"))
        .replace(/^\//, "") + "/websockify"
    );
    // ######################## END CUSTOM CODE ########################

    UI.setupSettingLabels();
  },
  // Adds a link to the label elements on the corresponding input elements
  setupSettingLabels() {
    const labels = document.getElementsByTagName("LABEL");
    for (let i = 0; i < labels.length; i++) {
      const htmlFor = labels[i].htmlFor;
      if (htmlFor != "") {
        const elem = document.getElementById(htmlFor);
        if (elem) elem.label = labels[i];
      } else {
        // If 'for' isn't set, use the first input element child
        const children = labels[i].children;
        for (let j = 0; j < children.length; j++) {
          if (children[j].form !== undefined) {
            children[j].label = labels[i];
            break;
          }
        }
      }
    }
  },

  /* ------^-------
   *     /INIT
   * ==============
   * EVENT HANDLERS
   * ------v------*/

  addControlbarHandlers() {
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("mousemove", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("mouseup", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("mousedown", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("keydown", UI.activateControlbar);

    document
      .getElementById("noVNC_control_bar")
      .addEventListener("mousedown", UI.keepControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("keydown", UI.keepControlbar);

    document
      .getElementById("noVNC_view_drag_button")
      .addEventListener("click", UI.toggleViewDrag);

    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("mousedown", UI.controlbarHandleMouseDown);
    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("mouseup", UI.controlbarHandleMouseUp);
    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("mousemove", UI.dragControlbarHandle);
    // resize events aren't available for elements
    window.addEventListener("resize", UI.updateControlbarHandle);

    const exps = document.getElementsByClassName("noVNC_expander");
    for (let i = 0; i < exps.length; i++) {
      exps[i].addEventListener("click", UI.toggleExpander);
    }
  },

  addTouchSpecificHandlers() {
    document
      .getElementById("noVNC_keyboard_button")
      .addEventListener("click", UI.toggleVirtualKeyboard);

    UI.touchKeyboard = new Keyboard(
      document.getElementById("noVNC_keyboardinput")
    );
    UI.touchKeyboard.onkeyevent = UI.keyEvent;
    UI.touchKeyboard.grab();
    document
      .getElementById("noVNC_keyboardinput")
      .addEventListener("input", UI.keyInput);
    document
      .getElementById("noVNC_keyboardinput")
      .addEventListener("focus", UI.onfocusVirtualKeyboard);
    document
      .getElementById("noVNC_keyboardinput")
      .addEventListener("blur", UI.onblurVirtualKeyboard);
    document
      .getElementById("noVNC_keyboardinput")
      .addEventListener("submit", () => false);

    document.documentElement.addEventListener(
      "mousedown",
      UI.keepVirtualKeyboard,
      true
    );

    document
      .getElementById("noVNC_control_bar")
      .addEventListener("touchstart", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("touchmove", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("touchend", UI.activateControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("input", UI.activateControlbar);

    document
      .getElementById("noVNC_control_bar")
      .addEventListener("touchstart", UI.keepControlbar);
    document
      .getElementById("noVNC_control_bar")
      .addEventListener("input", UI.keepControlbar);

    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("touchstart", UI.controlbarHandleMouseDown);
    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("touchend", UI.controlbarHandleMouseUp);
    document
      .getElementById("noVNC_control_bar_handle")
      .addEventListener("touchmove", UI.dragControlbarHandle);
  },

  addExtraKeysHandlers() {
    document
      .getElementById("noVNC_toggle_extra_keys_button")
      .addEventListener("click", UI.toggleExtraKeys);
    document
      .getElementById("noVNC_toggle_ctrl_button")
      .addEventListener("click", UI.toggleCtrl);
    document
      .getElementById("noVNC_toggle_windows_button")
      .addEventListener("click", UI.toggleWindows);
    document
      .getElementById("noVNC_toggle_alt_button")
      .addEventListener("click", UI.toggleAlt);
    document
      .getElementById("noVNC_send_tab_button")
      .addEventListener("click", UI.sendTab);
    document
      .getElementById("noVNC_send_esc_button")
      .addEventListener("click", UI.sendEsc);
    document
      .getElementById("noVNC_send_ctrl_alt_del_button")
      .addEventListener("click", UI.sendCtrlAltDel);
  },

  addMachineHandlers() {
    document
      .getElementById("noVNC_shutdown_button")
      .addEventListener("click", () => UI.rfb.machineShutdown());
    document
      .getElementById("noVNC_reboot_button")
      .addEventListener("click", () => UI.rfb.machineReboot());
    document
      .getElementById("noVNC_reset_button")
      .addEventListener("click", () => UI.rfb.machineReset());
    document
      .getElementById("noVNC_power_button")
      .addEventListener("click", UI.togglePowerPanel);
  },

  addConnectionControlHandlers() {
    document
      .getElementById("noVNC_disconnect_button")
      .addEventListener("click", UI.disconnect);
    document
      .getElementById("noVNC_connect_button")
      .addEventListener("click", UI.connect);
    document
      .getElementById("noVNC_cancel_reconnect_button")
      .addEventListener("click", UI.cancelReconnect);

    document
      .getElementById("noVNC_credentials_button")
      .addEventListener("click", UI.setCredentials);
  },

  addClipboardHandlers() {
    document
      .getElementById("noVNC_clipboard_button")
      .addEventListener("click", UI.toggleClipboardPanel);
    document
      .getElementById("noVNC_clipboard_text")
      .addEventListener("change", UI.clipboardSend);
    document
      .getElementById("noVNC_clipboard_clear_button")
      .addEventListener("click", UI.clipboardClear);
  },

  // Add a call to save settings when the element changes,
  // unless the optional parameter changeFunc is used instead.
  addSettingChangeHandler(name, changeFunc) {
    const settingElem = document.getElementById("noVNC_setting_" + name);
    if (changeFunc === undefined) {
      changeFunc = () => UI.saveSetting(name);
    }
    settingElem.addEventListener("change", changeFunc);
  },

  addSettingsHandlers() {
    document
      .getElementById("noVNC_settings_button")
      .addEventListener("click", UI.toggleSettingsPanel);

    UI.addSettingChangeHandler("encrypt");
    UI.addSettingChangeHandler("resize");
    UI.addSettingChangeHandler("resize", UI.applyResizeMode);
    UI.addSettingChangeHandler("resize", UI.updateViewClip);
    UI.addSettingChangeHandler("quality");
    UI.addSettingChangeHandler("quality", UI.updateQuality);
    UI.addSettingChangeHandler("compression");
    UI.addSettingChangeHandler("compression", UI.updateCompression);
    UI.addSettingChangeHandler("view_clip");
    UI.addSettingChangeHandler("view_clip", UI.updateViewClip);
    UI.addSettingChangeHandler("shared");
    UI.addSettingChangeHandler("view_only");
    UI.addSettingChangeHandler("view_only", UI.updateViewOnly);
    UI.addSettingChangeHandler("show_dot");
    UI.addSettingChangeHandler("show_dot", UI.updateShowDotCursor);
    UI.addSettingChangeHandler("host");
    UI.addSettingChangeHandler("port");
    UI.addSettingChangeHandler("path");
    UI.addSettingChangeHandler("repeaterID");
    UI.addSettingChangeHandler("logging");
    UI.addSettingChangeHandler("logging", UI.updateLogging);
    UI.addSettingChangeHandler("reconnect");
    UI.addSettingChangeHandler("reconnect_delay");
  },

  addFullscreenHandlers() {
    document
      .getElementById("noVNC_fullscreen_button")
      .addEventListener("click", UI.toggleFullscreen);

    window.addEventListener("fullscreenchange", UI.updateFullscreenButton);
    window.addEventListener("mozfullscreenchange", UI.updateFullscreenButton);
    window.addEventListener(
      "webkitfullscreenchange",
      UI.updateFullscreenButton
    );
    window.addEventListener("msfullscreenchange", UI.updateFullscreenButton);
  },

  /* ------^-------
   * /EVENT HANDLERS
   * ==============
   *     VISUAL
   * ------v------*/

  // Disable/enable controls depending on connection state
  updateVisualState(state) {
    document.documentElement.classList.remove("noVNC_connecting");
    document.documentElement.classList.remove("noVNC_connected");
    document.documentElement.classList.remove("noVNC_disconnecting");
    document.documentElement.classList.remove("noVNC_reconnecting");

    const transitionElem = document.getElementById("noVNC_transition_text");
    switch (state) {
      case "init":
        break;
      case "connecting":
        transitionElem.textContent = _("Connecting...");
        document.documentElement.classList.add("noVNC_connecting");
        break;
      case "connected":
        document.documentElement.classList.add("noVNC_connected");
        break;
      case "disconnecting":
        transitionElem.textContent = _("Disconnecting...");
        document.documentElement.classList.add("noVNC_disconnecting");
        break;
      case "disconnected":
        break;
      case "reconnecting":
        transitionElem.textContent = _("Reconnecting...");
        document.documentElement.classList.add("noVNC_reconnecting");
        break;
      default:
        Log.Error("Invalid visual state: " + state);
        UI.showStatus(_("Internal error"), "error");
        return;
    }

    if (UI.connected) {
      UI.updateViewClip();

      UI.disableSetting("encrypt");
      UI.disableSetting("shared");
      UI.disableSetting("host");
      UI.disableSetting("port");
      UI.disableSetting("path");
      UI.disableSetting("repeaterID");

      // Hide the controlbar after 2 seconds
      UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
    } else {
      UI.enableSetting("encrypt");
      UI.enableSetting("shared");
      UI.enableSetting("host");
      UI.enableSetting("port");
      UI.enableSetting("path");
      UI.enableSetting("repeaterID");
      UI.updatePowerButton();
      UI.keepControlbar();
    }

    // State change closes dialogs as they may not be relevant
    // anymore
    UI.closeAllPanels();
    document
      .getElementById("noVNC_credentials_dlg")
      .classList.remove("noVNC_open");
  },

  showStatus(text, statusType, time) {
    const statusElem = document.getElementById("noVNC_status");

    if (typeof statusType === "undefined") {
      statusType = "normal";
    }

    // Don't overwrite more severe visible statuses and never
    // errors. Only shows the first error.
    if (statusElem.classList.contains("noVNC_open")) {
      if (statusElem.classList.contains("noVNC_status_error")) {
        return;
      }
      if (
        statusElem.classList.contains("noVNC_status_warn") &&
        statusType === "normal"
      ) {
        return;
      }
    }

    clearTimeout(UI.statusTimeout);

    switch (statusType) {
      case "error":
        statusElem.classList.remove("noVNC_status_warn");
        statusElem.classList.remove("noVNC_status_normal");
        statusElem.classList.add("noVNC_status_error");
        break;
      case "warning":
      case "warn":
        statusElem.classList.remove("noVNC_status_error");
        statusElem.classList.remove("noVNC_status_normal");
        statusElem.classList.add("noVNC_status_warn");
        break;
      case "normal":
      case "info":
      default:
        statusElem.classList.remove("noVNC_status_error");
        statusElem.classList.remove("noVNC_status_warn");
        statusElem.classList.add("noVNC_status_normal");
        break;
    }

    statusElem.textContent = text;
    statusElem.classList.add("noVNC_open");

    // If no time was specified, show the status for 1.5 seconds
    if (typeof time === "undefined") {
      time = 1500;
    }

    // Error messages do not timeout
    if (statusType !== "error") {
      UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
    }
  },

  hideStatus() {
    clearTimeout(UI.statusTimeout);
    document.getElementById("noVNC_status").classList.remove("noVNC_open");
  },

  activateControlbar(event) {
    clearTimeout(UI.idleControlbarTimeout);
    // We manipulate the anchor instead of the actual control
    // bar in order to avoid creating new a stacking group
    document
      .getElementById("noVNC_control_bar_anchor")
      .classList.remove("noVNC_idle");
    UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
  },

  idleControlbar() {
    // Don't fade if a child of the control bar has focus
    if (
      document
        .getElementById("noVNC_control_bar")
        .contains(document.activeElement) &&
      document.hasFocus()
    ) {
      UI.activateControlbar();
      return;
    }

    document
      .getElementById("noVNC_control_bar_anchor")
      .classList.add("noVNC_idle");
  },

  keepControlbar() {
    clearTimeout(UI.closeControlbarTimeout);
  },

  openControlbar() {
    document.getElementById("noVNC_control_bar").classList.add("noVNC_open");
  },

  closeControlbar() {
    UI.closeAllPanels();
    document.getElementById("noVNC_control_bar").classList.remove("noVNC_open");
    UI.rfb.focus();
  },

  toggleControlbar() {
    if (
      document
        .getElementById("noVNC_control_bar")
        .classList.contains("noVNC_open")
    ) {
      UI.closeControlbar();
    } else {
      UI.openControlbar();
    }
  },

  toggleControlbarSide() {
    // Temporarily disable animation, if bar is displayed, to avoid weird
    // movement. The transitionend-event will not fire when display=none.
    const bar = document.getElementById("noVNC_control_bar");
    const barDisplayStyle = window.getComputedStyle(bar).display;
    if (barDisplayStyle !== "none") {
      bar.style.transitionDuration = "0s";
      bar.addEventListener(
        "transitionend",
        () => (bar.style.transitionDuration = "")
      );
    }

    const anchor = document.getElementById("noVNC_control_bar_anchor");
    if (anchor.classList.contains("noVNC_right")) {
      WebUtil.writeSetting("controlbar_pos", "left");
      anchor.classList.remove("noVNC_right");
    } else {
      WebUtil.writeSetting("controlbar_pos", "right");
      anchor.classList.add("noVNC_right");
    }

    // Consider this a movement of the handle
    UI.controlbarDrag = true;
  },

  showControlbarHint(show) {
    const hint = document.getElementById("noVNC_control_bar_hint");
    if (show) {
      hint.classList.add("noVNC_active");
    } else {
      hint.classList.remove("noVNC_active");
    }
  },

  dragControlbarHandle(e) {
    if (!UI.controlbarGrabbed) return;

    const ptr = getPointerEvent(e);

    const anchor = document.getElementById("noVNC_control_bar_anchor");
    if (ptr.clientX < window.innerWidth * 0.1) {
      if (anchor.classList.contains("noVNC_right")) {
        UI.toggleControlbarSide();
      }
    } else if (ptr.clientX > window.innerWidth * 0.9) {
      if (!anchor.classList.contains("noVNC_right")) {
        UI.toggleControlbarSide();
      }
    }

    if (!UI.controlbarDrag) {
      const dragDistance = Math.abs(
        ptr.clientY - UI.controlbarMouseDownClientY
      );

      if (dragDistance < dragThreshold) return;

      UI.controlbarDrag = true;
    }

    const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;

    UI.moveControlbarHandle(eventY);

    e.preventDefault();
    e.stopPropagation();
    UI.keepControlbar();
    UI.activateControlbar();
  },

  // Move the handle but don't allow any position outside the bounds
  moveControlbarHandle(viewportRelativeY) {
    const handle = document.getElementById("noVNC_control_bar_handle");
    const handleHeight = handle.getBoundingClientRect().height;
    const controlbarBounds = document
      .getElementById("noVNC_control_bar")
      .getBoundingClientRect();
    const margin = 10;

    // These heights need to be non-zero for the below logic to work
    if (handleHeight === 0 || controlbarBounds.height === 0) {
      return;
    }

    let newY = viewportRelativeY;

    // Check if the coordinates are outside the control bar
    if (newY < controlbarBounds.top + margin) {
      // Force coordinates to be below the top of the control bar
      newY = controlbarBounds.top + margin;
    } else if (
      newY >
      controlbarBounds.top + controlbarBounds.height - handleHeight - margin
    ) {
      // Force coordinates to be above the bottom of the control bar
      newY =
        controlbarBounds.top + controlbarBounds.height - handleHeight - margin;
    }

    // Corner case: control bar too small for stable position
    if (controlbarBounds.height < handleHeight + margin * 2) {
      newY =
        controlbarBounds.top + (controlbarBounds.height - handleHeight) / 2;
    }

    // The transform needs coordinates that are relative to the parent
    const parentRelativeY = newY - controlbarBounds.top;
    handle.style.transform = "translateY(" + parentRelativeY + "px)";
  },

  updateControlbarHandle() {
    // Since the control bar is fixed on the viewport and not the page,
    // the move function expects coordinates relative the the viewport.
    const handle = document.getElementById("noVNC_control_bar_handle");
    const handleBounds = handle.getBoundingClientRect();
    UI.moveControlbarHandle(handleBounds.top);
  },

  controlbarHandleMouseUp(e) {
    if (e.type == "mouseup" && e.button != 0) return;

    // mouseup and mousedown on the same place toggles the controlbar
    if (UI.controlbarGrabbed && !UI.controlbarDrag) {
      UI.toggleControlbar();
      e.preventDefault();
      e.stopPropagation();
      UI.keepControlbar();
      UI.activateControlbar();
    }
    UI.controlbarGrabbed = false;
    UI.showControlbarHint(false);
  },

  controlbarHandleMouseDown(e) {
    if (e.type == "mousedown" && e.button != 0) return;

    const ptr = getPointerEvent(e);

    const handle = document.getElementById("noVNC_control_bar_handle");
    const bounds = handle.getBoundingClientRect();

    // Touch events have implicit capture
    if (e.type === "mousedown") {
      setCapture(handle);
    }

    UI.controlbarGrabbed = true;
    UI.controlbarDrag = false;

    UI.showControlbarHint(true);

    UI.controlbarMouseDownClientY = ptr.clientY;
    UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
    e.preventDefault();
    e.stopPropagation();
    UI.keepControlbar();
    UI.activateControlbar();
  },

  toggleExpander(e) {
    if (this.classList.contains("noVNC_open")) {
      this.classList.remove("noVNC_open");
    } else {
      this.classList.add("noVNC_open");
    }
  },

  /* ------^-------
   *    /VISUAL
   * ==============
   *    SETTINGS
   * ------v------*/

  // Initial page load read/initialization of settings
  initSetting(name, defVal) {
    // Check Query string followed by cookie
    let val = WebUtil.getConfigVar(name);
    if (val === null) {
      val = WebUtil.readSetting(name, defVal);
    }
    WebUtil.setSetting(name, val);
    UI.updateSetting(name);
    return val;
  },

  // Set the new value, update and disable form control setting
  forceSetting(name, val) {
    WebUtil.setSetting(name, val);
    UI.updateSetting(name);
    UI.disableSetting(name);
  },

  // Update cookie and form control setting. If value is not set, then
  // updates from control to current cookie setting.
  updateSetting(name) {
    // Update the settings control
    let value = UI.getSetting(name);

    const ctrl = document.getElementById("noVNC_setting_" + name);
    if (ctrl.type === "checkbox") {
      ctrl.checked = value;
    } else if (typeof ctrl.options !== "undefined") {
      for (let i = 0; i < ctrl.options.length; i += 1) {
        if (ctrl.options[i].value === value) {
          ctrl.selectedIndex = i;
          break;
        }
      }
    } else {
      /*Weird IE9 error leads to 'null' appearring
            in textboxes instead of ''.*/
      if (value === null) {
        value = "";
      }
      ctrl.value = value;
    }
  },

  // Save control setting to cookie
  saveSetting(name) {
    const ctrl = document.getElementById("noVNC_setting_" + name);
    let val;
    if (ctrl.type === "checkbox") {
      val = ctrl.checked;
    } else if (typeof ctrl.options !== "undefined") {
      val = ctrl.options[ctrl.selectedIndex].value;
    } else {
      val = ctrl.value;
    }
    WebUtil.writeSetting(name, val);
    //Log.Debug("Setting saved '" + name + "=" + val + "'");
    return val;
  },

  // Read form control compatible setting from cookie
  getSetting(name) {
    const ctrl = document.getElementById("noVNC_setting_" + name);
    let val = WebUtil.readSetting(name);
    if (
      typeof val !== "undefined" &&
      val !== null &&
      ctrl.type === "checkbox"
    ) {
      if (val.toString().toLowerCase() in { 0: 1, no: 1, false: 1 }) {
        val = false;
      } else {
        val = true;
      }
    }
    return val;
  },

  // These helpers compensate for the lack of parent-selectors and
  // previous-sibling-selectors in CSS which are needed when we want to
  // disable the labels that belong to disabled input elements.
  disableSetting(name) {
    const ctrl = document.getElementById("noVNC_setting_" + name);
    ctrl.disabled = true;
    ctrl.label.classList.add("noVNC_disabled");
  },

  enableSetting(name) {
    const ctrl = document.getElementById("noVNC_setting_" + name);
    ctrl.disabled = false;
    ctrl.label.classList.remove("noVNC_disabled");
  },

  /* ------^-------
   *   /SETTINGS
   * ==============
   *    PANELS
   * ------v------*/

  closeAllPanels() {
    UI.closeSettingsPanel();
    UI.closePowerPanel();
    UI.closeClipboardPanel();
    UI.closeExtraKeys();
  },

  /* ------^-------
   *   /PANELS
   * ==============
   * SETTINGS (panel)
   * ------v------*/

  openSettingsPanel() {
    UI.closeAllPanels();
    UI.openControlbar();

    // Refresh UI elements from saved cookies
    UI.updateSetting("encrypt");
    UI.updateSetting("view_clip");
    UI.updateSetting("resize");
    UI.updateSetting("quality");
    UI.updateSetting("compression");
    UI.updateSetting("shared");
    UI.updateSetting("view_only");
    UI.updateSetting("path");
    UI.updateSetting("repeaterID");
    UI.updateSetting("logging");
    UI.updateSetting("reconnect");
    UI.updateSetting("reconnect_delay");

    document.getElementById("noVNC_settings").classList.add("noVNC_open");
    document
      .getElementById("noVNC_settings_button")
      .classList.add("noVNC_selected");
  },

  closeSettingsPanel() {
    document.getElementById("noVNC_settings").classList.remove("noVNC_open");
    document
      .getElementById("noVNC_settings_button")
      .classList.remove("noVNC_selected");
  },

  toggleSettingsPanel() {
    if (
      document.getElementById("noVNC_settings").classList.contains("noVNC_open")
    ) {
      UI.closeSettingsPanel();
    } else {
      UI.openSettingsPanel();
    }
  },

  /* ------^-------
   *   /SETTINGS
   * ==============
   *     POWER
   * ------v------*/

  openPowerPanel() {
    UI.closeAllPanels();
    UI.openControlbar();

    document.getElementById("noVNC_power").classList.add("noVNC_open");
    document
      .getElementById("noVNC_power_button")
      .classList.add("noVNC_selected");
  },

  closePowerPanel() {
    document.getElementById("noVNC_power").classList.remove("noVNC_open");
    document
      .getElementById("noVNC_power_button")
      .classList.remove("noVNC_selected");
  },

  togglePowerPanel() {
    if (
      document.getElementById("noVNC_power").classList.contains("noVNC_open")
    ) {
      UI.closePowerPanel();
    } else {
      UI.openPowerPanel();
    }
  },

  // Disable/enable power button
  updatePowerButton() {
    if (UI.connected && UI.rfb.capabilities.power && !UI.rfb.viewOnly) {
      document
        .getElementById("noVNC_power_button")
        .classList.remove("noVNC_hidden");
    } else {
      document
        .getElementById("noVNC_power_button")
        .classList.add("noVNC_hidden");
      // Close power panel if open
      UI.closePowerPanel();
    }
  },

  /* ------^-------
   *    /POWER
   * ==============
   *   CLIPBOARD
   * ------v------*/

  openClipboardPanel() {
    UI.closeAllPanels();
    UI.openControlbar();

    document.getElementById("noVNC_clipboard").classList.add("noVNC_open");
    document
      .getElementById("noVNC_clipboard_button")
      .classList.add("noVNC_selected");
  },

  closeClipboardPanel() {
    document.getElementById("noVNC_clipboard").classList.remove("noVNC_open");
    document
      .getElementById("noVNC_clipboard_button")
      .classList.remove("noVNC_selected");
  },

  toggleClipboardPanel() {
    if (
      document
        .getElementById("noVNC_clipboard")
        .classList.contains("noVNC_open")
    ) {
      UI.closeClipboardPanel();
    } else {
      UI.openClipboardPanel();
    }
  },

  clipboardReceive(e) {
    Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
    document.getElementById("noVNC_clipboard_text").value = e.detail.text;
    Log.Debug("<< UI.clipboardReceive");
  },

  clipboardClear() {
    document.getElementById("noVNC_clipboard_text").value = "";
    UI.rfb.clipboardPasteFrom("");
  },

  clipboardSend() {
    const text = document.getElementById("noVNC_clipboard_text").value;
    Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
    UI.rfb.clipboardPasteFrom(text);
    Log.Debug("<< UI.clipboardSend");
  },

  /* ------^-------
   *  /CLIPBOARD
   * ==============
   *  CONNECTION
   * ------v------*/

  openConnectPanel() {
    document.getElementById("noVNC_connect_dlg").classList.add("noVNC_open");
  },

  closeConnectPanel() {
    document.getElementById("noVNC_connect_dlg").classList.remove("noVNC_open");
  },

  connect(event, password) {
    // Ignore when rfb already exists
    if (typeof UI.rfb !== "undefined") {
      return;
    }

    const host = UI.getSetting("host");
    const port = UI.getSetting("port");
    const path = UI.getSetting("path");

    if (typeof password === "undefined") {
      password = WebUtil.getConfigVar("password");
      UI.reconnectPassword = password;
    }

    if (password === null) {
      password = undefined;
    }

    UI.hideStatus();

    if (!host) {
      Log.Error("Can't connect when host is: " + host);
      UI.showStatus(_("Must set host"), "error");
      return;
    }

    UI.closeConnectPanel();

    UI.updateVisualState("connecting");

    let url;

    url = UI.getSetting("encrypt") ? "wss" : "ws";

    url += "://" + host;
    if (port) {
      url += ":" + port;
    }
    url += "/" + path;

    UI.rfb = new RFB(document.getElementById("noVNC_container"), url, {
      shared: UI.getSetting("shared"),
      repeaterID: UI.getSetting("repeaterID"),
      credentials: { password: password },
    });
    UI.rfb.addEventListener("connect", UI.connectFinished);
    UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
    UI.rfb.addEventListener("credentialsrequired", UI.credentials);
    UI.rfb.addEventListener("securityfailure", UI.securityFailed);
    UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
    UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
    UI.rfb.addEventListener("bell", UI.bell);
    UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
    UI.rfb.clipViewport = UI.getSetting("view_clip");
    UI.rfb.scaleViewport = UI.getSetting("resize") === "scale";
    UI.rfb.resizeSession = UI.getSetting("resize") === "remote";
    UI.rfb.qualityLevel = parseInt(UI.getSetting("quality"));
    UI.rfb.compressionLevel = parseInt(UI.getSetting("compression"));
    UI.rfb.showDotCursor = UI.getSetting("show_dot");

    UI.updateViewOnly(); // requires UI.rfb

    // ######################## CUSTOM CODE ########################
    var clipboard = document.getElementById("clipboard");
    var controlBar = document.getElementById("noVNC_control_bar");
    var controlBarClipboard = document.getElementById("noVNC_clipboard_text");

    UI.rfb.addEventListener("clipboard", function (e) {
      clipboard.value = e.detail.text;
    });

    UI.rfb.addEventListener("connect", function (e) {
      var viewer = document.getElementsByTagName("canvas")[0];

      document.body.addEventListener("paste", function (e) {
        try {
          console.log("paste");
          if (controlBarClipboard.classList.contains("noVNC_open")) {
            return;
          }
          if (UI.rfb != null) {
            text = e.clipboardData.getData("text");
            console.log("copy clipboard from local to vnc: " + text);
            controlBarClipboard.value = text;
            UI.rfb.clipboardPasteFrom(text);
          }
        } catch (error) {
          console.error(error);
        }
      });

      document.body.addEventListener("keydown", function (e) {
        try {
          console.log("document keydown: " + e.keyCode);
          if (controlBar.classList.contains("noVNC_open")) {
            return;
          }
          if (e.keyCode !== 86) {
            e.preventDefault();
          } else {
            if (navigator.clipboard) {
              navigator.clipboard
                .readText()
                .then((text) => {
                  console.log("copy clipboard from local to vnc: " + text);
                  controlBarClipboard.value = text;
                  UI.rfb.clipboardPasteFrom(text);
                })
                .catch((err) => {
                  console.error("Failed to read clipboard contents: ", err);
                });
            }
          }
          setTimeout(function () {
            viewer.dispatchEvent(new e.constructor(e.type, e));
            if (UI.rfb != null) {
              UI.rfb.focus();
            }
          }, 1);
        } catch (error) {
          console.error(error);
        }
      });

      document.body.addEventListener("keyup", function (e) {
        try {
          console.log("document keyup");
          if (controlBar.classList.contains("noVNC_open")) {
            return;
          }

          if (e.keyCode !== 86) {
            e.preventDefault();
          }
          setTimeout(function () {
            viewer.dispatchEvent(new e.constructor(e.type, e));
            if (UI.rfb != null) {
              UI.rfb.focus();
            }
          }, 1);
        } catch (error) {
          console.error(error);
        }
      });

      viewer.addEventListener("keydown", function (e) {
        try {
          console.log("viewer keydown");
          if (e.ctrlKey) {
            if (controlBar.classList.contains("noVNC_open")) {
              return;
            }
            document.activeElement.blur();
          }
        } catch (error) {
          console.error(error);
        }
      });

      viewer.addEventListener("keyup", function (e) {
        try {
          console.log("viewer keyup: " + e.keyCode);
          if (e.ctrlKey && [67, 88].includes(e.keyCode)) {
            if (controlBar.classList.contains("noVNC_open")) {
              return;
            }
            if (clipboard != null) {
              console.log(
                "copy clipboard from vnc to local: " + clipboard.value
              );
              clipboard.focus();
              clipboard.select();

              document.execCommand("copy");
              if (UI.rfb != null) {
                UI.rfb.focus();
              }
            }
          }
        } catch (error) {
          console.error(error);
        }
      });
    });
    // ######################## END CUSTOM CODE ########################
  },

  disconnect() {
    UI.rfb.disconnect();

    UI.connected = false;

    // Disable automatic reconnecting
    UI.inhibitReconnect = true;

    UI.updateVisualState("disconnecting");

    // Don't display the connection settings until we're actually disconnected
  },

  reconnect() {
    UI.reconnectCallback = null;

    // if reconnect has been disabled in the meantime, do nothing.
    if (UI.inhibitReconnect) {
      return;
    }

    UI.connect(null, UI.reconnectPassword);
  },

  cancelReconnect() {
    if (UI.reconnectCallback !== null) {
      clearTimeout(UI.reconnectCallback);
      UI.reconnectCallback = null;
    }

    UI.updateVisualState("disconnected");

    UI.openControlbar();
    UI.openConnectPanel();
  },

  connectFinished(e) {
    UI.connected = true;
    UI.inhibitReconnect = false;

    let msg;
    if (UI.getSetting("encrypt")) {
      msg = _("Connected (encrypted) to ") + UI.desktopName;
    } else {
      msg = _("Connected (unencrypted) to ") + UI.desktopName;
    }
    UI.showStatus(msg);
    UI.updateVisualState("connected");

    // Do this last because it can only be used on rendered elements
    UI.rfb.focus();
  },

  disconnectFinished(e) {
    const wasConnected = UI.connected;

    // This variable is ideally set when disconnection starts, but
    // when the disconnection isn't clean or if it is initiated by
    // the server, we need to do it here as well since
    // UI.disconnect() won't be used in those cases.
    UI.connected = false;

    UI.rfb = undefined;

    if (!e.detail.clean) {
      UI.updateVisualState("disconnected");
      if (wasConnected) {
        UI.showStatus(_("Something went wrong, connection is closed"), "error");
      } else {
        UI.showStatus(_("Failed to connect to server"), "error");
      }
    } else if (
      UI.getSetting("reconnect", false) === true &&
      !UI.inhibitReconnect
    ) {
      UI.updateVisualState("reconnecting");

      const delay = parseInt(UI.getSetting("reconnect_delay"));
      UI.reconnectCallback = setTimeout(UI.reconnect, delay);
      return;
    } else {
      UI.updateVisualState("disconnected");
      UI.showStatus(_("Disconnected"), "normal");
    }

    document.title = PAGE_TITLE;

    UI.openControlbar();
    UI.openConnectPanel();
  },

  securityFailed(e) {
    let msg = "";
    // On security failures we might get a string with a reason
    // directly from the server. Note that we can't control if
    // this string is translated or not.
    if ("reason" in e.detail) {
      msg =
        _("New connection has been rejected with reason: ") + e.detail.reason;
    } else {
      msg = _("New connection has been rejected");
    }
    UI.showStatus(msg, "error");
  },

  /* ------^-------
   *  /CONNECTION
   * ==============
   *   PASSWORD
   * ------v------*/

  credentials(e) {
    // FIXME: handle more types

    document
      .getElementById("noVNC_username_block")
      .classList.remove("noVNC_hidden");
    document
      .getElementById("noVNC_password_block")
      .classList.remove("noVNC_hidden");

    let inputFocus = "none";
    if (e.detail.types.indexOf("username") === -1) {
      document
        .getElementById("noVNC_username_block")
        .classList.add("noVNC_hidden");
    } else {
      inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
    }
    if (e.detail.types.indexOf("password") === -1) {
      document
        .getElementById("noVNC_password_block")
        .classList.add("noVNC_hidden");
    } else {
      inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
    }
    document
      .getElementById("noVNC_credentials_dlg")
      .classList.add("noVNC_open");

    setTimeout(() => document.getElementById(inputFocus).focus(), 100);

    Log.Warn("Server asked for credentials");
    UI.showStatus(_("Credentials are required"), "warning");
  },

  setCredentials(e) {
    // Prevent actually submitting the form
    e.preventDefault();

    let inputElemUsername = document.getElementById("noVNC_username_input");
    const username = inputElemUsername.value;

    let inputElemPassword = document.getElementById("noVNC_password_input");
    const password = inputElemPassword.value;
    // Clear the input after reading the password
    inputElemPassword.value = "";

    UI.rfb.sendCredentials({ username: username, password: password });
    UI.reconnectPassword = password;
    document
      .getElementById("noVNC_credentials_dlg")
      .classList.remove("noVNC_open");
  },

  /* ------^-------
   *  /PASSWORD
   * ==============
   *   FULLSCREEN
   * ------v------*/

  toggleFullscreen() {
    if (
      document.fullscreenElement || // alternative standard method
      document.mozFullScreenElement || // currently working methods
      document.webkitFullscreenElement ||
      document.msFullscreenElement
    ) {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      }
    } else {
      if (document.documentElement.requestFullscreen) {
        document.documentElement.requestFullscreen();
      } else if (document.documentElement.mozRequestFullScreen) {
        document.documentElement.mozRequestFullScreen();
      } else if (document.documentElement.webkitRequestFullscreen) {
        document.documentElement.webkitRequestFullscreen(
          Element.ALLOW_KEYBOARD_INPUT
        );
      } else if (document.body.msRequestFullscreen) {
        document.body.msRequestFullscreen();
      }
    }
    UI.updateFullscreenButton();
  },

  updateFullscreenButton() {
    if (
      document.fullscreenElement || // alternative standard method
      document.mozFullScreenElement || // currently working methods
      document.webkitFullscreenElement ||
      document.msFullscreenElement
    ) {
      document
        .getElementById("noVNC_fullscreen_button")
        .classList.add("noVNC_selected");
    } else {
      document
        .getElementById("noVNC_fullscreen_button")
        .classList.remove("noVNC_selected");
    }
  },

  /* ------^-------
   *  /FULLSCREEN
   * ==============
   *     RESIZE
   * ------v------*/

  // Apply remote resizing or local scaling
  applyResizeMode() {
    if (!UI.rfb) return;

    UI.rfb.scaleViewport = UI.getSetting("resize") === "scale";
    UI.rfb.resizeSession = UI.getSetting("resize") === "remote";
  },

  /* ------^-------
   *    /RESIZE
   * ==============
   * VIEW CLIPPING
   * ------v------*/

  // Update viewport clipping property for the connection. The normal
  // case is to get the value from the setting. There are special cases
  // for when the viewport is scaled or when a touch device is used.
  updateViewClip() {
    if (!UI.rfb) return;

    const scaling = UI.getSetting("resize") === "scale";

    if (scaling) {
      // Can't be clipping if viewport is scaled to fit
      UI.forceSetting("view_clip", false);
      UI.rfb.clipViewport = false;
    } else if (!hasScrollbarGutter) {
      // Some platforms have scrollbars that are difficult
      // to use in our case, so we always use our own panning
      UI.forceSetting("view_clip", true);
      UI.rfb.clipViewport = true;
    } else {
      UI.enableSetting("view_clip");
      UI.rfb.clipViewport = UI.getSetting("view_clip");
    }

    // Changing the viewport may change the state of
    // the dragging button
    UI.updateViewDrag();
  },

  /* ------^-------
   * /VIEW CLIPPING
   * ==============
   *    VIEWDRAG
   * ------v------*/

  toggleViewDrag() {
    if (!UI.rfb) return;

    UI.rfb.dragViewport = !UI.rfb.dragViewport;
    UI.updateViewDrag();
  },

  updateViewDrag() {
    if (!UI.connected) return;

    const viewDragButton = document.getElementById("noVNC_view_drag_button");

    if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
      // We are no longer clipping the viewport. Make sure
      // viewport drag isn't active when it can't be used.
      UI.rfb.dragViewport = false;
    }

    if (UI.rfb.dragViewport) {
      viewDragButton.classList.add("noVNC_selected");
    } else {
      viewDragButton.classList.remove("noVNC_selected");
    }

    if (UI.rfb.clipViewport) {
      viewDragButton.classList.remove("noVNC_hidden");
    } else {
      viewDragButton.classList.add("noVNC_hidden");
    }
  },

  /* ------^-------
   *   /VIEWDRAG
   * ==============
   *    QUALITY
   * ------v------*/

  updateQuality() {
    if (!UI.rfb) return;

    UI.rfb.qualityLevel = parseInt(UI.getSetting("quality"));
  },

  /* ------^-------
   *   /QUALITY
   * ==============
   *  COMPRESSION
   * ------v------*/

  updateCompression() {
    if (!UI.rfb) return;

    UI.rfb.compressionLevel = parseInt(UI.getSetting("compression"));
  },

  /* ------^-------
   *  /COMPRESSION
   * ==============
   *    KEYBOARD
   * ------v------*/

  showVirtualKeyboard() {
    if (!isTouchDevice) return;

    const input = document.getElementById("noVNC_keyboardinput");

    if (document.activeElement == input) return;

    input.focus();

    try {
      const l = input.value.length;
      // Move the caret to the end
      input.setSelectionRange(l, l);
    } catch (err) {
      // setSelectionRange is undefined in Google Chrome
    }
  },

  hideVirtualKeyboard() {
    if (!isTouchDevice) return;

    const input = document.getElementById("noVNC_keyboardinput");

    if (document.activeElement != input) return;

    input.blur();
  },

  toggleVirtualKeyboard() {
    if (
      document
        .getElementById("noVNC_keyboard_button")
        .classList.contains("noVNC_selected")
    ) {
      UI.hideVirtualKeyboard();
    } else {
      UI.showVirtualKeyboard();
    }
  },

  onfocusVirtualKeyboard(event) {
    document
      .getElementById("noVNC_keyboard_button")
      .classList.add("noVNC_selected");
    if (UI.rfb) {
      UI.rfb.focusOnClick = false;
    }
  },

  onblurVirtualKeyboard(event) {
    document
      .getElementById("noVNC_keyboard_button")
      .classList.remove("noVNC_selected");
    if (UI.rfb) {
      UI.rfb.focusOnClick = true;
    }
  },

  keepVirtualKeyboard(event) {
    const input = document.getElementById("noVNC_keyboardinput");

    // Only prevent focus change if the virtual keyboard is active
    if (document.activeElement != input) {
      return;
    }

    // Only allow focus to move to other elements that need
    // focus to function properly
    if (event.target.form !== undefined) {
      switch (event.target.type) {
        case "text":
        case "email":
        case "search":
        case "password":
        case "tel":
        case "url":
        case "textarea":
        case "select-one":
        case "select-multiple":
          return;
      }
    }

    event.preventDefault();
  },

  keyboardinputReset() {
    const kbi = document.getElementById("noVNC_keyboardinput");
    kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
    UI.lastKeyboardinput = kbi.value;
  },

  keyEvent(keysym, code, down) {
    if (!UI.rfb) return;

    UI.rfb.sendKey(keysym, code, down);
  },

  // When normal keyboard events are left uncought, use the input events from
  // the keyboardinput element instead and generate the corresponding key events.
  // This code is required since some browsers on Android are inconsistent in
  // sending keyCodes in the normal keyboard events when using on screen keyboards.
  keyInput(event) {
    if (!UI.rfb) return;

    const newValue = event.target.value;

    if (!UI.lastKeyboardinput) {
      UI.keyboardinputReset();
    }
    const oldValue = UI.lastKeyboardinput;

    let newLen;
    try {
      // Try to check caret position since whitespace at the end
      // will not be considered by value.length in some browsers
      newLen = Math.max(event.target.selectionStart, newValue.length);
    } catch (err) {
      // selectionStart is undefined in Google Chrome
      newLen = newValue.length;
    }
    const oldLen = oldValue.length;

    let inputs = newLen - oldLen;
    let backspaces = inputs < 0 ? -inputs : 0;

    // Compare the old string with the new to account for
    // text-corrections or other input that modify existing text
    for (let i = 0; i < Math.min(oldLen, newLen); i++) {
      if (newValue.charAt(i) != oldValue.charAt(i)) {
        inputs = newLen - i;
        backspaces = oldLen - i;
        break;
      }
    }

    // Send the key events
    for (let i = 0; i < backspaces; i++) {
      UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
    }
    for (let i = newLen - inputs; i < newLen; i++) {
      UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
    }

    // Control the text content length in the keyboardinput element
    if (newLen > 2 * UI.defaultKeyboardinputLen) {
      UI.keyboardinputReset();
    } else if (newLen < 1) {
      // There always have to be some text in the keyboardinput
      // element with which backspace can interact.
      UI.keyboardinputReset();
      // This sometimes causes the keyboard to disappear for a second
      // but it is required for the android keyboard to recognize that
      // text has been added to the field
      event.target.blur();
      // This has to be ran outside of the input handler in order to work
      setTimeout(event.target.focus.bind(event.target), 0);
    } else {
      UI.lastKeyboardinput = newValue;
    }
  },

  /* ------^-------
   *   /KEYBOARD
   * ==============
   *   EXTRA KEYS
   * ------v------*/

  openExtraKeys() {
    UI.closeAllPanels();
    UI.openControlbar();

    document.getElementById("noVNC_modifiers").classList.add("noVNC_open");
    document
      .getElementById("noVNC_toggle_extra_keys_button")
      .classList.add("noVNC_selected");
  },

  closeExtraKeys() {
    document.getElementById("noVNC_modifiers").classList.remove("noVNC_open");
    document
      .getElementById("noVNC_toggle_extra_keys_button")
      .classList.remove("noVNC_selected");
  },

  toggleExtraKeys() {
    if (
      document
        .getElementById("noVNC_modifiers")
        .classList.contains("noVNC_open")
    ) {
      UI.closeExtraKeys();
    } else {
      UI.openExtraKeys();
    }
  },

  sendEsc() {
    UI.sendKey(KeyTable.XK_Escape, "Escape");
  },

  sendTab() {
    UI.sendKey(KeyTable.XK_Tab, "Tab");
  },

  toggleCtrl() {
    const btn = document.getElementById("noVNC_toggle_ctrl_button");
    if (btn.classList.contains("noVNC_selected")) {
      UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
      btn.classList.remove("noVNC_selected");
    } else {
      UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
      btn.classList.add("noVNC_selected");
    }
  },

  toggleWindows() {
    const btn = document.getElementById("noVNC_toggle_windows_button");
    if (btn.classList.contains("noVNC_selected")) {
      UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
      btn.classList.remove("noVNC_selected");
    } else {
      UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
      btn.classList.add("noVNC_selected");
    }
  },

  toggleAlt() {
    const btn = document.getElementById("noVNC_toggle_alt_button");
    if (btn.classList.contains("noVNC_selected")) {
      UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
      btn.classList.remove("noVNC_selected");
    } else {
      UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
      btn.classList.add("noVNC_selected");
    }
  },

  sendCtrlAltDel() {
    UI.rfb.sendCtrlAltDel();
    // See below
    UI.rfb.focus();
    UI.idleControlbar();
  },

  sendKey(keysym, code, down) {
    UI.rfb.sendKey(keysym, code, down);

    // Move focus to the screen in order to be able to use the
    // keyboard right after these extra keys.
    // The exception is when a virtual keyboard is used, because
    // if we focus the screen the virtual keyboard would be closed.
    // In this case we focus our special virtual keyboard input
    // element instead.
    if (
      document
        .getElementById("noVNC_keyboard_button")
        .classList.contains("noVNC_selected")
    ) {
      document.getElementById("noVNC_keyboardinput").focus();
    } else {
      UI.rfb.focus();
    }
    // fade out the controlbar to highlight that
    // the focus has been moved to the screen
    UI.idleControlbar();
  },

  /* ------^-------
   *   /EXTRA KEYS
   * ==============
   *     MISC
   * ------v------*/

  updateViewOnly() {
    if (!UI.rfb) return;
    UI.rfb.viewOnly = UI.getSetting("view_only");

    // Hide input related buttons in view only mode
    if (UI.rfb.viewOnly) {
      document
        .getElementById("noVNC_keyboard_button")
        .classList.add("noVNC_hidden");
      document
        .getElementById("noVNC_toggle_extra_keys_button")
        .classList.add("noVNC_hidden");
      document
        .getElementById("noVNC_clipboard_button")
        .classList.add("noVNC_hidden");
    } else {
      document
        .getElementById("noVNC_keyboard_button")
        .classList.remove("noVNC_hidden");
      document
        .getElementById("noVNC_toggle_extra_keys_button")
        .classList.remove("noVNC_hidden");
      document
        .getElementById("noVNC_clipboard_button")
        .classList.remove("noVNC_hidden");
    }
  },

  updateShowDotCursor() {
    if (!UI.rfb) return;
    UI.rfb.showDotCursor = UI.getSetting("show_dot");
  },

  updateLogging() {
    WebUtil.initLogging(UI.getSetting("logging"));
  },

  updateDesktopName(e) {
    UI.desktopName = e.detail.name;
    // Display the desktop name in the document title
    document.title = e.detail.name + " - " + PAGE_TITLE;
  },

  bell(e) {
    if (WebUtil.getConfigVar("bell", "on") === "on") {
      const promise = document.getElementById("noVNC_bell").play();
      // The standards disagree on the return value here
      if (promise) {
        promise.catch((e) => {
          if (e.name === "NotAllowedError") {
            // Ignore when the browser doesn't let us play audio.
            // It is common that the browsers require audio to be
            // initiated from a user action.
          } else {
            Log.Error("Unable to play bell: " + e);
          }
        });
      }
    }
  },

  //Helper to add options to dropdown.
  addOption(selectbox, text, value) {
    const optn = document.createElement("OPTION");
    optn.text = text;
    optn.value = value;
    selectbox.options.add(optn);
  },

  /* ------^-------
   *    /MISC
   * ==============
   */
};

// Set up translations
const LINGUAS = [
  "cs",
  "de",
  "el",
  "es",
  "ja",
  "ko",
  "nl",
  "pl",
  "ru",
  "sv",
  "tr",
  "zh_CN",
  "zh_TW",
];
l10n.setup(LINGUAS);
if (l10n.language === "en" || l10n.dictionary !== undefined) {
  UI.prime();
} else {
  WebUtil.fetchJSON("app/locale/" + l10n.language + ".json")
    .then((translations) => {
      l10n.dictionary = translations;
    })
    .catch((err) => Log.Error("Failed to load translations: " + err))
    .then(UI.prime);
}

export default UI;
