/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright 2009-2013 Pierre Ossman <ossman@cendio.se> for Cendio AB
 * Copyright (C) 2011-2013 D. R. Commander.  All Rights Reserved.
 * Copyright (C) 2011-2015 Brian P. Hinz
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

//
// CConn
//
// Methods on CConn are called from both the GUI thread and the thread which
// processes incoming RFB messages ("the RFB thread").  This means we need to
// be careful with synchronization here.
//
// Any access to writer() must not only be synchronized, but we must also make
// sure that the connection is in RFBSTATE_NORMAL.  We are guaranteed this for
// any code called after serverInit() has been called.  Since the DesktopWindow
// isn't created until then, any methods called only from DesktopWindow can
// assume that we are in RFBSTATE_NORMAL.

package com.tigervnc.vncviewer;

import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.awt.Toolkit;

import java.io.IOException;
import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.swing.*;
import javax.swing.ImageIcon;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.*;
import java.util.prefs.*;

import com.tigervnc.rdr.*;
import com.tigervnc.rfb.*;
import com.tigervnc.rfb.Point;
import com.tigervnc.rfb.Exception;
import com.tigervnc.network.Socket;
import com.tigervnc.network.TcpSocket;

import static com.tigervnc.vncviewer.Parameters.*;

public class CConn extends CConnection implements 
  UserPasswdGetter, FdInStreamBlockCallback, ActionListener {

  // 8 colours (1 bit per component)
  static final PixelFormat verylowColorPF =
    new PixelFormat(8, 3, false, true, 1, 1, 1, 2, 1, 0);

  // 64 colours (2 bits per component)
  static final PixelFormat lowColorPF =
    new PixelFormat(8, 6, false, true, 3, 3, 3, 4, 2, 0);

  // 256 colours (2-3 bits per component)
  static final PixelFormat mediumColorPF =
    new PixelFormat(8, 8, false, true, 7, 7, 3, 5, 2, 0);

  static final int KEY_LOC_SHIFT_R = 0;
  static final int KEY_LOC_SHIFT_L = 16;
  static final int SUPER_MASK = 1<<15;

  ////////////////////////////////////////////////////////////////////
  // The following methods are all called from the RFB thread

  public CConn(String vncServerName, Socket socket)
  {
    serverHost = null; serverPort = 0; desktop = null;
    pendingPFChange = false;
    currentEncoding = Encodings.encodingTight; lastServerEncoding = -1;
    formatChange = false; encodingChange = false;
    firstUpdate = true; pendingUpdate = false; continuousUpdates = false;
    forceNonincremental = true; supportsSyncFence = false;

    setShared(shared.getValue());
    sock = socket;
    downKeySym = new HashMap<Integer, Integer>();

    upg = this;

    int encNum = Encodings.encodingNum(preferredEncoding.getValue());
    if (encNum != -1)
      currentEncoding = encNum;

    cp.supportsLocalCursor = true;

    cp.supportsDesktopResize = true;
    cp.supportsExtendedDesktopSize = true;
    cp.supportsDesktopRename = true;

    cp.supportsSetDesktopSize = false;
    cp.supportsClientRedirect = true;

    if (customCompressLevel.getValue())
      cp.compressLevel = compressLevel.getValue();
    else
      cp.compressLevel = -1;

    if (!noJpeg.getValue())
      cp.qualityLevel = qualityLevel.getValue();
    else
      cp.qualityLevel = -1;

    if (sock == null) {
      setServerName(Hostname.getHost(vncServerName));
      setServerPort(Hostname.getPort(vncServerName));
      try {
        if (tunnel.getValue() || !via.getValue().isEmpty()) {
          int localPort = TcpSocket.findFreeTcpPort();
          if (localPort == 0)
            throw new Exception("Could not obtain free TCP port");
          Tunnel.createTunnel(this, localPort);
          sock = new TcpSocket("localhost", localPort);
        } else {
          sock = new TcpSocket(getServerName(), getServerPort());
        }
      } catch (java.lang.Exception e) {
        throw new Exception(e.getMessage());
      }
      vlog.info("connected to host "+getServerName()+" port "+getServerPort());
    } else {
      String name = sock.getPeerEndpoint();
      if (listenMode.getValue())
        vlog.info("Accepted connection from " + name);
      else
        vlog.info("connected to host "+Hostname.getHost(name)+" port "+Hostname.getPort(name));
    }

    // See callback below
    sock.inStream().setBlockCallback(this);

    setStreams(sock.inStream(), sock.outStream());

    initialiseProtocol();

    OptionsDialog.addCallback("handleOptions", this);
  }

  public void refreshFramebuffer()
  {
    forceNonincremental = true;

    // Without fences, we cannot safely trigger an update request directly
    // but must wait for the next update to arrive.
    if (supportsSyncFence)
      requestNewUpdate();
  }

  public String connectionInfo() {
    String info = new String("Desktop name: %s%n"+
                             "Host: %s:%d%n"+
                             "Size: %dx%d%n"+
                             "Pixel format: %s%n"+
                             "  (server default: %s)%n"+
                             "Requested encoding: %s%n"+
                             "Last used encoding: %s%n"+
                             "Line speed estimate: %d kbit/s%n"+
                             "Protocol version: %d.%d%n"+
                             "Security method: %s [%s]%n");
    String infoText =
      String.format(info, cp.name(),
                    sock.getPeerName(), sock.getPeerPort(),
                    cp.width, cp.height,
                    cp.pf().print(),
                    serverPF.print(),
                    Encodings.encodingName(currentEncoding),
                    Encodings.encodingName(lastServerEncoding),
                    sock.inStream().kbitsPerSecond(),
                    cp.majorVersion, cp.minorVersion,
                    Security.secTypeName(csecurity.getType()),
                    csecurity.description());

    return infoText;
  }

  // The RFB core is not properly asynchronous, so it calls this callback
  // whenever it needs to block to wait for more data. Since FLTK is
  // monitoring the socket, we just make sure FLTK gets to run.

  public void blockCallback() {
    try {
      synchronized(this) {
        wait(1);
      }
    } catch (java.lang.InterruptedException e) {
      throw new Exception(e.getMessage());
    }
  }

  // getUserPasswd() is called by the CSecurity object when it needs us to read
  // a password from the user.

  public final boolean getUserPasswd(StringBuffer user, StringBuffer passwd) {
    String title = ("VNC Authentication ["
                    +csecurity.description() + "]");
    String passwordFileStr = passwordFile.getValue();
    PasswdDialog dlg;

    if (user == null && !passwordFileStr.equals("")) {
      InputStream fp = null;
      try {
        fp = new FileInputStream(passwordFileStr);
      } catch(FileNotFoundException e) {
        throw new Exception("Opening password file failed");
      }
      byte[] obfPwd = new byte[256];
      try {
        fp.read(obfPwd);
        fp.close();
      } catch(IOException e) {
        throw new Exception("Failed to read VncPasswd file");
      }
      String PlainPasswd = VncAuth.unobfuscatePasswd(obfPwd);
      passwd.append(PlainPasswd);
      passwd.setLength(PlainPasswd.length());
      return true;
    }

    if (user == null) {
      dlg = new PasswdDialog(title, (user == null), (passwd == null));
    } else {
      if ((passwd == null) && sendLocalUsername.getValue()) {
         user.append((String)System.getProperties().get("user.name"));
         return true;
      }
      dlg = new PasswdDialog(title, sendLocalUsername.getValue(),
         (passwd == null));
    }
    dlg.showDialog();
    if (user != null) {
      if (sendLocalUsername.getValue()) {
         user.append((String)System.getProperties().get("user.name"));
      } else {
         user.append(dlg.userEntry.getText());
      }
    }
    if (passwd != null)
      passwd.append(new String(dlg.passwdEntry.getPassword()));
    return true;
  }

  ////////////////////// CConnection callback methods //////////////////////

  // serverInit() is called when the serverInit message has been received.  At
  // this point we create the desktop window and display it.  We also tell the
  // server the pixel format and encodings to use and request the first update.
  public void serverInit()
  {
    super.serverInit();

    // If using AutoSelect with old servers, start in FullColor
    // mode. See comment in autoSelectFormatAndEncoding.
    if (cp.beforeVersion(3, 8) && autoSelect.getValue())
      fullColor.setParam(true);

    serverPF = cp.pf();

    desktop = new DesktopWindow(cp.width, cp.height, cp.name(), serverPF, this);
    fullColorPF = desktop.getPreferredPF();

    // Force a switch to the format and encoding we'd like
    formatChange = true; encodingChange = true;

    // And kick off the update cycle
    requestNewUpdate();

    // This initial update request is a bit of a corner case, so we need
    // to help out setting the correct format here.
    assert(pendingPFChange);
    cp.setPF(pendingPF);
    pendingPFChange = false;
  }

  // setDesktopSize() is called when the desktop size changes (including when
  // it is set initially).
  public void setDesktopSize(int w, int h)
  {
    super.setDesktopSize(w, h);
    resizeFramebuffer();
  }

  // setExtendedDesktopSize() is a more advanced version of setDesktopSize()
  public void setExtendedDesktopSize(int reason, int result, int w, int h,
                                     ScreenSet layout)
  {
    super.setExtendedDesktopSize(reason, result, w, h, layout);

    if ((reason == screenTypes.reasonClient) &&
        (result != screenTypes.resultSuccess)) {
      vlog.error("SetDesktopSize failed: " + result);
      return;
    }

    resizeFramebuffer();
  }

  // clientRedirect() migrates the client to another host/port
  public void clientRedirect(int port, String host, String x509subject)
  {
    try {
      sock.close();
      sock = new TcpSocket(host, port);
      vlog.info("Redirected to "+host+":"+port);
      setServerName(host);
      setServerPort(port);
      sock.inStream().setBlockCallback(this);
      setStreams(sock.inStream(), sock.outStream());
      if (desktop != null)
        desktop.dispose();
      initialiseProtocol();
    } catch (java.lang.Exception e) {
      throw new Exception(e.getMessage());
    }
  }

  // setName() is called when the desktop name changes
  public void setName(String name)
  {
    super.setName(name);
    if (desktop != null)
      desktop.setName(name);
  }

  // framebufferUpdateStart() is called at the beginning of an update.
  // Here we try to send out a new framebuffer update request so that the
  // next update can be sent out in parallel with us decoding the current
  // one.
  public void framebufferUpdateStart()
  {
    ModifiablePixelBuffer pb;
    PlatformPixelBuffer ppb;

    super.framebufferUpdateStart();

    // Note: This might not be true if sync fences are supported
    pendingUpdate = false;

    requestNewUpdate();

    // We might still be rendering the previous update
    pb = getFramebuffer();
    assert(pb != null);
    ppb = (PlatformPixelBuffer)pb;
    assert(ppb != null);

    //FIXME
  }

  // framebufferUpdateEnd() is called at the end of an update.
  // For each rectangle, the FdInStream will have timed the speed
  // of the connection, allowing us to select format and encoding
  // appropriately, and then request another incremental update.
  public void framebufferUpdateEnd()
  {
    super.framebufferUpdateEnd();

    desktop.updateWindow();

    if (firstUpdate) {
      // We need fences to make extra update requests and continuous
      // updates "safe". See fence() for the next step.
      if (cp.supportsFence)
        writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext, 0, null);

      firstUpdate = false;
    }

    // A format change has been scheduled and we are now past the update
    // with the old format. Time to active the new one.
    if (pendingPFChange) {
      cp.setPF(pendingPF);
      pendingPFChange = false;
    }

    // Compute new settings based on updated bandwidth values
    if (autoSelect.getValue())
      autoSelectFormatAndEncoding();
  }

  // The rest of the callbacks are fairly self-explanatory...

  public void setColourMapEntries(int firstColor, int nColors, int[] rgbs)
  {
    vlog.error("Invalid SetColourMapEntries from server!");
  }

  public void bell()
  {
    if (acceptBell.getValue())
      desktop.getToolkit().beep();
  }

  public void serverCutText(String str, int len)
  {
    StringSelection buffer;

    if (!acceptClipboard.getValue())
      return;

    ClipboardDialog.serverCutText(str);
  }

  public void dataRect(Rect r, int encoding)
  {
    sock.inStream().startTiming();

    if (encoding != Encodings.encodingCopyRect)
      lastServerEncoding = encoding;

    super.dataRect(r, encoding);

    sock.inStream().stopTiming();
  }

  public void setCursor(int width, int height, Point hotspot,
                        byte[] data)
  {
    desktop.setCursor(width, height, hotspot, data);
  }

  public void fence(int flags, int len, byte[] data)
  {
    // can't call super.super.fence(flags, len, data);
    cp.supportsFence = true;

    if ((flags & fenceTypes.fenceFlagRequest) != 0) {
      // We handle everything synchronously so we trivially honor these modes
      flags = flags & (fenceTypes.fenceFlagBlockBefore | fenceTypes.fenceFlagBlockAfter);

      writer().writeFence(flags, len, data);
      return;
    }

    if (len == 0) {
      // Initial probe
      if ((flags & fenceTypes.fenceFlagSyncNext) != 0) {
        supportsSyncFence = true;

        if (cp.supportsContinuousUpdates) {
          vlog.info("Enabling continuous updates");
          continuousUpdates = true;
          writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height);
        }
      }
    } else {
      // Pixel format change
      MemInStream memStream = new MemInStream(data, 0, len);
      PixelFormat pf = new PixelFormat();

      pf.read(memStream);

      cp.setPF(pf);
    }
  }

  ////////////////////// Internal methods //////////////////////
  private void resizeFramebuffer()
  {
    if (desktop == null)
      return;

    if (continuousUpdates)
      writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height);

    desktop.resizeFramebuffer(cp.width, cp.height);
  }

  // autoSelectFormatAndEncoding() chooses the format and encoding appropriate
  // to the connection speed:
  //
  //   First we wait for at least one second of bandwidth measurement.
  //
  //   Above 16Mbps (i.e. LAN), we choose the second highest JPEG quality,
  //   which should be perceptually lossless.
  //
  //   If the bandwidth is below that, we choose a more lossy JPEG quality.
  //
  //   If the bandwidth drops below 256 Kbps, we switch to palette mode.
  //
  //   Note: The system here is fairly arbitrary and should be replaced
  //         with something more intelligent at the server end.
  //
  private void autoSelectFormatAndEncoding()
  {
    long kbitsPerSecond = sock.inStream().kbitsPerSecond();
    long timeWaited = sock.inStream().timeWaited();
    boolean newFullColor = fullColor.getValue();
    int newQualityLevel = qualityLevel.getValue();

    // Always use Tight
    if (currentEncoding != Encodings.encodingTight) {
      currentEncoding = Encodings.encodingTight;
      encodingChange = true;
    }

    // Check that we have a decent bandwidth measurement
    if ((kbitsPerSecond == 0) || (timeWaited < 100))
      return;

    // Select appropriate quality level
    if (!noJpeg.getValue()) {
      if (kbitsPerSecond > 16000)
        newQualityLevel = 8;
      else
        newQualityLevel = 6;

      if (newQualityLevel != qualityLevel.getValue()) {
        vlog.info("Throughput "+kbitsPerSecond+
                  " kbit/s - changing to quality "+newQualityLevel);
        cp.qualityLevel = newQualityLevel;
        qualityLevel.setParam(newQualityLevel);
        encodingChange = true;
      }
    }

    if (cp.beforeVersion(3, 8)) {
      // Xvnc from TightVNC 1.2.9 sends out FramebufferUpdates with
      // cursors "asynchronously". If this happens in the middle of a
      // pixel format change, the server will encode the cursor with
      // the old format, but the client will try to decode it
      // according to the new format. This will lead to a
      // crash. Therefore, we do not allow automatic format change for
      // old servers.
      return;
    }

    // Select best color level
    newFullColor = (kbitsPerSecond > 256);
    if (newFullColor != fullColor.getValue()) {
      vlog.info("Throughput "+kbitsPerSecond+
                " kbit/s - full color is now "+
                (newFullColor ? "enabled" : "disabled"));
      fullColor.setParam(newFullColor);
      formatChange = true;
    }
  }

  // checkEncodings() sends a setEncodings message if one is needed.
  private void checkEncodings()
  {
    if (encodingChange && (writer() != null)) {
      vlog.info("Using " + Encodings.encodingName(currentEncoding) +
        " encoding");
      writer().writeSetEncodings(currentEncoding, true);
      encodingChange = false;
    }
  }

  // requestNewUpdate() requests an update from the server, having set the
  // format and encoding appropriately.
  private void requestNewUpdate()
  {
    if (formatChange) {
      PixelFormat pf;

      /* Catch incorrect requestNewUpdate calls */
      assert(!pendingUpdate || supportsSyncFence);

      if (fullColor.getValue()) {
        pf = fullColorPF;
      } else {
        if (lowColorLevel.getValue() == 0) {
          pf = verylowColorPF;
        } else if (lowColorLevel.getValue() == 1) {
          pf = lowColorPF;
        } else {
          pf = mediumColorPF;
        }
      }

      if (supportsSyncFence) {
        // We let the fence carry the pixel format and switch once we
        // get the response back. That way we will be synchronised with
        // when the server switches.
        MemOutStream memStream = new MemOutStream();

        pf.write(memStream);

        writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext,
                            memStream.length(), (byte[])memStream.data());
      } else {
        // New requests are sent out at the start of processing the last
        // one, so we cannot switch our internal format right now (doing so
        // would mean misdecoding the current update).
        pendingPFChange = true;
        pendingPF = pf;
      }

      String str = pf.print();
      vlog.info("Using pixel format " + str);
      writer().writeSetPixelFormat(pf);

      formatChange = false;
    }

    checkEncodings();

    if (forceNonincremental || !continuousUpdates) {
      pendingUpdate = true;
      writer().writeFramebufferUpdateRequest(new Rect(0, 0, cp.width, cp.height),
                                                 !forceNonincremental);
    }

    forceNonincremental = false;
  }

  public void handleOptions()
  {

    // Checking all the details of the current set of encodings is just
    // a pain. Assume something has changed, as resending the encoding
    // list is cheap. Avoid overriding what the auto logic has selected
    // though.
    if (!autoSelect.getValue()) {
      int encNum = Encodings.encodingNum(preferredEncoding.getValue());

      if (encNum != -1)
        this.currentEncoding = encNum;
    }

    this.cp.supportsLocalCursor = true;

    if (customCompressLevel.getValue())
      this.cp.compressLevel = compressLevel.getValue();
    else
      this.cp.compressLevel = -1;

    if (!noJpeg.getValue() && !autoSelect.getValue())
      this.cp.qualityLevel = qualityLevel.getValue();
    else
      this.cp.qualityLevel = -1;

    this.encodingChange = true;

    // Format changes refreshes the entire screen though and are therefore
    // very costly. It's probably worth the effort to see if it is necessary
    // here.
    PixelFormat pf;

    if (fullColor.getValue()) {
      pf = fullColorPF;
    } else {
      if (lowColorLevel.getValue() == 0)
        pf = verylowColorPF;
      else if (lowColorLevel.getValue() == 1)
        pf = lowColorPF;
      else
        pf = mediumColorPF;
    }

    if (!pf.equal(this.cp.pf())) {
      this.formatChange = true;

      // Without fences, we cannot safely trigger an update request directly
      // but must wait for the next update to arrive.
      if (this.supportsSyncFence)
        this.requestNewUpdate();
    }

  }

  ////////////////////////////////////////////////////////////////////
  // The following methods are all called from the GUI thread

  // close() shuts down the socket, thus waking up the RFB thread.
  public void close() {
    if (closeListener != null) {
      embed.setParam(true);
      JFrame f =
        (JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop);
      if (f != null)
        f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING));
    }
    shuttingDown = true;
    try {
      if (sock != null)
        sock.shutdown();
    } catch (java.lang.Exception e) {
      throw new Exception(e.getMessage());
    }
  }

  void showInfo() {
    Window fullScreenWindow = DesktopWindow.getFullScreenWindow();
    if (fullScreenWindow != null)
      DesktopWindow.setFullScreenWindow(null);
    String info = new String("Desktop name: %s%n"+
                             "Host: %s:%d%n"+
                             "Size: %dx%d%n"+
                             "Pixel format: %s%n"+
                             "  (server default: %s)%n"+
                             "Requested encoding: %s%n"+
                             "Last used encoding: %s%n"+
                             "Line speed estimate: %d kbit/s%n"+
                             "Protocol version: %d.%d%n"+
                             "Security method: %s [%s]%n");
    String msg =
      String.format(info, cp.name(),
                    sock.getPeerName(), sock.getPeerPort(),
                    cp.width, cp.height,
                    cp.pf().print(),
                    serverPF.print(),
                    Encodings.encodingName(currentEncoding),
                    Encodings.encodingName(lastServerEncoding),
                    sock.inStream().kbitsPerSecond(),
                    cp.majorVersion, cp.minorVersion,
                    Security.secTypeName(csecurity.getType()),
                    csecurity.description());
    JOptionPane op = new JOptionPane(msg, JOptionPane.PLAIN_MESSAGE,
                                     JOptionPane.DEFAULT_OPTION);
    JDialog dlg = op.createDialog(desktop, "VNC connection info");
    dlg.setIconImage(VncViewer.frameIcon);
    dlg.setAlwaysOnTop(true);
    dlg.setVisible(true);
    if (fullScreenWindow != null)
      DesktopWindow.setFullScreenWindow(fullScreenWindow);
  }

  public void refresh() {
    writer().writeFramebufferUpdateRequest(new Rect(0,0,cp.width,cp.height), false);
    pendingUpdate = true;
  }

  // writeClientCutText() is called from the clipboard dialog
  public void writeClientCutText(String str, int len) {
    if (state() != RFBSTATE_NORMAL || shuttingDown)
      return;
    writer().writeClientCutText(str, len);
  }

  public void writeKeyEvent(int keysym, boolean down) {
    if (state() != RFBSTATE_NORMAL || shuttingDown)
      return;
    writer().writeKeyEvent(keysym, down);
  }

  public void writeKeyEvent(KeyEvent ev) {
    if (viewOnly.getValue() || shuttingDown)
      return;

    boolean down = (ev.getID() == KeyEvent.KEY_PRESSED);

    int keySym, keyCode = ev.getKeyCode();

    // If neither the keyCode or keyChar are defined, then there's
    // really nothing that we can do with this.  The fn key on OS-X
    // fires events like this when pressed but does not fire a
    // corresponding release event.
    if (keyCode == 0 && ev.getKeyChar() == KeyEvent.CHAR_UNDEFINED)
      return;

    if (!down) {
      Integer iter = downKeySym.get(keyCode);
      if (iter == null) {
        // Note that dead keys will raise this sort of error falsely
        // See https://bugs.openjdk.java.net/browse/JDK-6534883 
        vlog.debug("Unexpected key release of keyCode "+keyCode);
        String fmt = ev.paramString().replaceAll("%","%%");
        vlog.debug(String.format(fmt.replaceAll(",","%n       ")));

        return;
      }

      vlog.debug(String.format("Key released: 0x%04x => 0x%04x",
                 keyCode, iter));

      writeKeyEvent(iter, false);
      downKeySym.remove(keyCode);

      return;
    }

    keySym = Keysyms.translateKeyEvent(ev);
    if (keySym == Keysyms.VoidSymbol)
      return;

    boolean need_cheat = true;
    if (VncViewer.os.startsWith("windows")) {
      // Windows doesn't have a proper AltGr, but handles it using fake
      // Ctrl+Alt. Unfortunately X11 doesn't generally like the combination
      // Ctrl+Alt+AltGr, which we usually end up with when Xvnc tries to
      // get everything in the correct state. Cheat and temporarily release
      // Ctrl and Alt whenever we get a key with a symbol.
      if (KeyEvent.getKeyText(keyCode).isEmpty())
        need_cheat = false;
      else if (!downKeySym.containsValue(Keysyms.Control_L) &&
               !downKeySym.containsValue(Keysyms.Control_R))
        need_cheat = false;
      else if (!downKeySym.containsValue(Keysyms.Alt_L) &&
               !downKeySym.containsValue(Keysyms.Alt_R))
        need_cheat = false;

      if (need_cheat) {
        vlog.info("Faking release of AltGr (Ctrl+Alt)");
        if (downKeySym.containsValue(Keysyms.Control_L))
          writeKeyEvent(Keysyms.Control_L, false);
        if (downKeySym.containsValue(Keysyms.Control_R))
          writeKeyEvent(Keysyms.Control_R, false);
        if (downKeySym.containsValue(Keysyms.Alt_L))
          writeKeyEvent(Keysyms.Alt_L, false);
        if (downKeySym.containsValue(Keysyms.Alt_R))
          writeKeyEvent(Keysyms.Alt_R, false);
      }
    }

    vlog.debug(String.format("Key pressed: 0x%04x '%s' => 0x%04x",
               keyCode, Character.toString(ev.getKeyChar()), keySym));

    downKeySym.put(keyCode, keySym);

    writeKeyEvent(keySym, down);

    if (VncViewer.os.startsWith("windows")) {
      if (need_cheat) {
        vlog.debug("Restoring AltGr state");
        if (downKeySym.containsValue(Keysyms.Control_L))
          writeKeyEvent(Keysyms.Control_L, true);
        if (downKeySym.containsValue(Keysyms.Control_R))
          writeKeyEvent(Keysyms.Control_R, true);
        if (downKeySym.containsValue(Keysyms.Alt_L))
          writeKeyEvent(Keysyms.Alt_L, true);
        if (downKeySym.containsValue(Keysyms.Alt_R))
          writeKeyEvent(Keysyms.Alt_R, true);
      }
    }
  }

  public void writePointerEvent(MouseEvent ev) {
    if (state() != RFBSTATE_NORMAL || shuttingDown)
      return;

    switch (ev.getID()) {
    case MouseEvent.MOUSE_PRESSED:
      buttonMask = 1;
      if ((ev.getModifiers() & KeyEvent.ALT_MASK) != 0) buttonMask = 2;
      if ((ev.getModifiers() & KeyEvent.META_MASK) != 0) buttonMask = 4;
      break;
    case MouseEvent.MOUSE_RELEASED:
      buttonMask = 0;
      break;
    }

    writer().writePointerEvent(new Point(ev.getX(), ev.getY()), buttonMask);
  }

  public void writeWheelEvent(MouseWheelEvent ev) {
    if (state() != RFBSTATE_NORMAL || shuttingDown)
      return;
    int x, y;
    int clicks = ev.getWheelRotation();
    if (clicks < 0) {
      buttonMask = 8;
    } else {
      buttonMask = 16;
    }
    for (int i = 0; i < Math.abs(clicks); i++) {
      x = ev.getX();
      y = ev.getY();
      writer().writePointerEvent(new Point(x, y), buttonMask);
      buttonMask = 0;
      writer().writePointerEvent(new Point(x, y), buttonMask);
    }

  }

  synchronized void releaseDownKeys() {
    for (Map.Entry<Integer, Integer> entry : downKeySym.entrySet())
      writeKeyEvent(entry.getValue(), false);
    downKeySym.clear();
  }

  // this is a special ActionListener passed in by the
  // Java Plug-in software to control applet's close behavior
  public void setCloseListener(ActionListener cl) {
    closeListener = cl;
  }

  public void actionPerformed(ActionEvent e) {}

  public Socket getSocket() {
    return sock;
  }

  ////////////////////////////////////////////////////////////////////
  // The following methods are called from both RFB and GUI threads

  // the following never change so need no synchronization:

  // access to desktop by different threads is specified in DesktopWindow

  // the following need no synchronization:

  public static UserPasswdGetter upg;

  // shuttingDown is set by the GUI thread and only ever tested by the RFB
  // thread after the window has been destroyed.
  boolean shuttingDown = false;

  // reading and writing int and boolean is atomic in java, so no
  // synchronization of the following flags is needed:


  // All menu, options, about and info stuff is done in the GUI thread (apart
  // from when constructed).

  // the following are only ever accessed by the GUI thread:
  int buttonMask;

  private String serverHost;
  private int serverPort;
  private Socket sock;

  protected DesktopWindow desktop;

  private PixelFormat serverPF;
  private PixelFormat fullColorPF;

  private boolean pendingPFChange;
  private PixelFormat pendingPF;

  private int currentEncoding, lastServerEncoding;

  private boolean formatChange;
  private boolean encodingChange;

  private boolean firstUpdate;
  private boolean pendingUpdate;
  private boolean continuousUpdates;

  private boolean forceNonincremental;

  private boolean supportsSyncFence;

  private HashMap<Integer, Integer> downKeySym;
  public ActionListener closeListener = null;

  static LogWriter vlog = new LogWriter("CConn");
}
