/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright (C) 2011-2012 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.
 */

package com.tigervnc.rfb;

import java.awt.color.*;
import java.awt.image.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import com.tigervnc.network.*;
import com.tigervnc.rdr.*;

abstract public class CConnection extends CMsgHandler {

  public CConnection()
  {
    super();
    csecurity = null; is = null; os = null; reader_ = null; writer_ = null;
    shared = false;
    state_ = RFBSTATE_UNINITIALISED; useProtocol3_3 = false;
    framebuffer = null; decoder = new DecodeManager(this);
    security = new SecurityClient();
  }

  // Methods to initialise the connection

  // setServerName() is used to provide a unique(ish) name for the server to
  // which we are connected.  This might be the result of getPeerEndpoint on
  // a TcpSocket, for example, or a host specified by DNS name & port.
  // The serverName is used when verifying the Identity of a host (see RA2).
  public final void setServerName(String name) {
    serverName = name;
  }

  // setShared sets the value of the shared flag which will be sent to the
  // server upon initialisation.
  public final void setShared(boolean s) { shared = s; }

  // setProtocol3_3 configures whether or not the CConnection should
  // only ever support protocol version 3.3
  public final void setProtocol3_3(boolean s) { useProtocol3_3 = s; }

  // setStreams() sets the streams to be used for the connection.  These must
  // be set before initialiseProtocol() and processMsg() are called.  The
  // CSecurity object may call setStreams() again to provide alternative
  // streams over which the RFB protocol is sent (i.e. encrypting/decrypting
  // streams).  Ownership of the streams remains with the caller
  // (i.e. SConnection will not delete them).
  public final void setStreams(InStream is_, OutStream os_)
  {
    is = is_;
    os = os_;
  }

  // setFramebuffer configures the PixelBuffer that the CConnection
  // should render all pixel data in to. Note that the CConnection
  // takes ownership of the PixelBuffer and it must not be deleted by
  // anyone else. Call setFramebuffer again with NULL or a different
  // PixelBuffer to delete the previous one.
  public void setFramebuffer(ModifiablePixelBuffer fb)
  {
    decoder.flush();

    if ((framebuffer != null) && (fb != null)) {
      Rect rect = new Rect();

      Raster data;

      byte[] black = new byte[4];

      // Copy still valid area

      rect.setXYWH(0, 0,
                   Math.min(fb.width(), framebuffer.width()),
                   Math.min(fb.height(), framebuffer.height()));
      data = framebuffer.getBuffer(rect);
      fb.imageRect(framebuffer.getPF(), rect, data);

      // Black out any new areas

      if (fb.width() > framebuffer.width()) {
        rect.setXYWH(framebuffer.width(), 0,
                     fb.width() - framebuffer.width(),
                     fb.height());
        fb.fillRect(rect, black);
      }

      if (fb.height() > framebuffer.height()) {
        rect.setXYWH(0, framebuffer.height(),
                     fb.width(),
                     fb.height() - framebuffer.height());
        fb.fillRect(rect, black);
      }
    }

    framebuffer = fb;
  }

  // initialiseProtocol() should be called once the streams and security
  // types are set.  Subsequently, processMsg() should be called whenever
  // there is data to read on the InStream.
  public final void initialiseProtocol()
  {
    state_ = RFBSTATE_PROTOCOL_VERSION;
  }

  // processMsg() should be called whenever there is data to read on the
  // InStream.  You must have called initialiseProtocol() first.
  public void processMsg()
  {
    switch (state_) {

    case RFBSTATE_PROTOCOL_VERSION: processVersionMsg();        break;
    case RFBSTATE_SECURITY_TYPES:   processSecurityTypesMsg();  break;
    case RFBSTATE_SECURITY:         processSecurityMsg();       break;
    case RFBSTATE_SECURITY_RESULT:  processSecurityResultMsg(); break;
    case RFBSTATE_INITIALISATION:   processInitMsg();           break;
    case RFBSTATE_NORMAL:           reader_.readMsg();          break;
    case RFBSTATE_UNINITIALISED:
      throw new Exception("CConnection.processMsg: not initialised yet?");
    default:
      throw new Exception("CConnection.processMsg: invalid state");
    }
  }

  private void processVersionMsg()
  {
    vlog.debug("reading protocol version");
    AtomicBoolean done = new AtomicBoolean();
    if (!cp.readVersion(is, done)) {
      state_ = RFBSTATE_INVALID;
      throw new Exception("reading version failed: not an RFB server?");
    }
    if (!done.get()) return;

    vlog.info("Server supports RFB protocol version "
              +cp.majorVersion+"."+ cp.minorVersion);

    // The only official RFB protocol versions are currently 3.3, 3.7 and 3.8
    if (cp.beforeVersion(3,3)) {
      String msg = ("Server gave unsupported RFB protocol version "+
                    cp.majorVersion+"."+cp.minorVersion);
      vlog.error(msg);
      state_ = RFBSTATE_INVALID;
      throw new Exception(msg);
    } else if (useProtocol3_3 || cp.beforeVersion(3,7)) {
      cp.setVersion(3,3);
    } else if (cp.afterVersion(3,8)) {
      cp.setVersion(3,8);
    }

    cp.writeVersion(os);
    state_ = RFBSTATE_SECURITY_TYPES;

    vlog.info("Using RFB protocol version "+
              cp.majorVersion+"."+cp.minorVersion);
  }

  private void processSecurityTypesMsg()
  {
    vlog.debug("processing security types message");

    int secType = Security.secTypeInvalid;

    List<Integer> secTypes = new ArrayList<Integer>();
    secTypes = security.GetEnabledSecTypes();

    if (cp.isVersion(3,3)) {

      // legacy 3.3 server may only offer "vnc authentication" or "none"

      secType = is.readU32();
      if (secType == Security.secTypeInvalid) {
        throwConnFailedException();

      } else if (secType == Security.secTypeNone || secType == Security.secTypeVncAuth) {
        Iterator<Integer> i;
        for (i = secTypes.iterator(); i.hasNext(); ) {
          int refType = (Integer)i.next();
          if (refType == secType) {
            secType = refType;
            break;
          }
        }

        if (!secTypes.contains(secType))
          secType = Security.secTypeInvalid;
      } else {
        vlog.error("Unknown 3.3 security type "+secType);
        throw new Exception("Unknown 3.3 security type");
      }

    } else {

      // 3.7 server will offer us a list

      int nServerSecTypes = is.readU8();
      if (nServerSecTypes == 0)
        throwConnFailedException();

      Iterator<Integer> j;

      for (int i = 0; i < nServerSecTypes; i++) {
        int serverSecType = is.readU8();
        vlog.debug("Server offers security type "+
                   Security.secTypeName(serverSecType)+"("+serverSecType+")");

        /*
        * Use the first type sent by server which matches client's type.
        * It means server's order specifies priority.
        */
        if (secType == Security.secTypeInvalid) {
          for (j = secTypes.iterator(); j.hasNext(); ) {
            int refType = (Integer)j.next();
            if (refType == serverSecType) {
              secType = refType;
              break;
            }
          }
        }
      }

      // Inform the server of our decision
      if (secType != Security.secTypeInvalid) {
        os.writeU8(secType);
        os.flush();
        vlog.debug("Choosing security type "+Security.secTypeName(secType)+
                   "("+secType+")");
      }
    }

    if (secType == Security.secTypeInvalid) {
      state_ = RFBSTATE_INVALID;
      vlog.error("No matching security types");
      throw new Exception("No matching security types");
    }

    state_ = RFBSTATE_SECURITY;
    csecurity = security.GetCSecurity(secType);
    processSecurityMsg();
  }

  private void processSecurityMsg() {
    vlog.debug("processing security message");
    if (csecurity.processMsg(this)) {
      state_ = RFBSTATE_SECURITY_RESULT;
      processSecurityResultMsg();
    }
  }

  private void processSecurityResultMsg() {
    vlog.debug("processing security result message");
    int result;
    if (cp.beforeVersion(3,8) && csecurity.getType() == Security.secTypeNone) {
      result = Security.secResultOK;
    } else {
      if (!is.checkNoWait(1)) return;
      result = is.readU32();
    }
    switch (result) {
    case Security.secResultOK:
      securityCompleted();
      return;
    case Security.secResultFailed:
      vlog.debug("auth failed");
      break;
    case Security.secResultTooMany:
      vlog.debug("auth failed - too many tries");
      break;
    default:
      throw new Exception("Unknown security result from server");
    }
    String reason;
    if (cp.beforeVersion(3,8))
      reason = "Authentication failure";
    else
      reason = is.readString();
    state_ = RFBSTATE_INVALID;
    throw new AuthFailureException(reason);
  }

  private void processInitMsg() {
    vlog.debug("reading server initialisation");
    reader_.readServerInit();
  }

  private void throwConnFailedException() {
    state_ = RFBSTATE_INVALID;
    String reason;
    reason = is.readString();
    throw new ConnFailedException(reason);
  }

  private void securityCompleted() {
    state_ = RFBSTATE_INITIALISATION;
    reader_ = new CMsgReader(this, is);
    writer_ = new CMsgWriter(cp, os);
    vlog.debug("Authentication success!");
    authSuccess();
    writer_.writeClientInit(shared);
  }

  // Methods to be overridden in a derived class

  // Note: These must be called by any deriving classes

  public void setDesktopSize(int w, int h) {
    decoder.flush();

    super.setDesktopSize(w,h);
  }

  public void setExtendedDesktopSize(int reason,
                                     int result, int w, int h,
                                     ScreenSet layout) {
    decoder.flush();

    super.setExtendedDesktopSize(reason, result, w, h, layout);
  }

  public void readAndDecodeRect(Rect r, int encoding,
                                ModifiablePixelBuffer pb)
  {
    decoder.decodeRect(r, encoding, pb);
    decoder.flush();
  }

  // getIdVerifier() returns the identity verifier associated with the connection.
  // Ownership of the IdentityVerifier is retained by the CConnection instance.
  //public IdentityVerifier getIdentityVerifier() { return 0; }

  public void framebufferUpdateStart()
  {
    super.framebufferUpdateStart();
  }

  public void framebufferUpdateEnd()
  {
    decoder.flush();

    super.framebufferUpdateEnd();
  }

  public void dataRect(Rect r, int encoding)
  {
    decoder.decodeRect(r, encoding, framebuffer);
  }

  // authSuccess() is called when authentication has succeeded.
  public void authSuccess()
  {
  }

  // serverInit() is called when the ServerInit message is received.  The
  // derived class must call on to CConnection::serverInit().
  public void serverInit()
  {
    state_ = RFBSTATE_NORMAL;
    vlog.debug("initialisation done");
  }

  // Other methods

  public CMsgReader reader() { return reader_; }
  public CMsgWriter writer() { return writer_; }

  public InStream getInStream() { return is; }
  public OutStream getOutStream() { return os; }

  // Access method used by SSecurity implementations that can verify servers'
  // Identities, to determine the unique(ish) name of the server.
  public String getServerName() { return serverName; }

  public static final int RFBSTATE_UNINITIALISED = 0;
  public static final int RFBSTATE_PROTOCOL_VERSION = 1;
  public static final int RFBSTATE_SECURITY_TYPES = 2;
  public static final int RFBSTATE_SECURITY = 3;
  public static final int RFBSTATE_SECURITY_RESULT = 4;
  public static final int RFBSTATE_INITIALISATION = 5;
  public static final int RFBSTATE_NORMAL = 6;
  public static final int RFBSTATE_INVALID = 7;

  public int state() { return state_; }

  public int getServerPort() { return serverPort; }
  public void setServerPort(int port) {
    serverPort = port;
  }

  protected void setState(int s) { state_ = s; }

  protected void setReader(CMsgReader r) { reader_ = r; }
  protected void setWriter(CMsgWriter w) { writer_ = w; }

  protected ModifiablePixelBuffer getFramebuffer() { return framebuffer; }

  public void fence(int flags, int len, byte[] data)
  {
    super.fence(flags, len, data);

    if ((flags & fenceTypes.fenceFlagRequest) != 0)
      return;

    // We cannot guarantee any synchronisation at this level
    flags = 0;

    synchronized(this) {
      writer().writeFence(flags, len, data);
    }
  }

  private void throwAuthFailureException() {
    String reason;
    vlog.debug("state="+state()+", ver="+cp.majorVersion+"."+cp.minorVersion);
    if (state() == RFBSTATE_SECURITY_RESULT && !cp.beforeVersion(3,8)) {
      reason = is.readString();
    } else {
      reason = "Authentication failure";
    }
    state_ = RFBSTATE_INVALID;
    vlog.error(reason);
    throw new AuthFailureException(reason);
  }

  private InStream is;
  private OutStream os;
  private CMsgReader reader_;
  private CMsgWriter writer_;
  private boolean deleteStreamsWhenDone;
  private boolean shared;
  private int state_ = RFBSTATE_UNINITIALISED;

  private String serverName;

  private boolean useProtocol3_3;

  protected ModifiablePixelBuffer framebuffer;
  private DecodeManager decoder;

  public CSecurity csecurity;
  public SecurityClient security;
  public static final int maxSecTypes = 8;
  int nSecTypes;
  int[] secTypes;
  int serverPort;
  boolean clientSecTypeOrder;

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