#include "krfblogin.h"
#include "krfbconnection.h"

/* OPIE */
#include <opie2/odebug.h>
using namespace Opie::Core;

/* QT */
#include <qtimer.h>

/* STD */
#include <assert.h>
extern "C" {
#include "vncauth.h"
}

// The length of the various messages (used to decide how many bytes to
// wait for).
const int ServerVersionLength = 12;
const int ClientVersionLength = 12;
const int AuthSchemeLength = 4;
const int FailureReasonSizeLength = 4;
const int ChallengeLength = 16;
const int AuthResultLength = 4;

// Authentication results
enum AuthResult {
  AuthOk,
  AuthFailed,
  AuthTooMany
};

typedef unsigned char CARD8;
typedef unsigned short CARD16;
typedef unsigned long CARD32;

const int endianTest = 1;

// Endian stuff
#define Swap16IfLE(s) \
    (*(char *)&endianTest ? ((((s) & 0xff) << 8) | (((s) >> 8) & 0xff)) : (s))

#define Swap32IfLE(l) \
    (*(char *)&endianTest ? ((((l) & 0xff000000) >> 24) | \
			     (((l) & 0x00ff0000) >> 8)  | \
			     (((l) & 0x0000ff00) << 8)  | \
			     (((l) & 0x000000ff) << 24))  : (l))

KRFBLogin::KRFBLogin( KRFBConnection *con )
  : QObject( con, "RFB login manager" )
{
  assert( con );
  this->con = con;
  currentState = AwaitingServerVersion;

  connect( this, SIGNAL( error(const QString&) ),
	   con, SIGNAL( error(const QString&) ) );


  owarn << "Waiting for server version..." << oendl; 

  static QString statusMsg = tr( "Waiting for server version..." );
  emit status( statusMsg );

  // Kick off the state machine
  connect( con, SIGNAL( gotEnoughData() ), SLOT( gotServerVersion() ) );
  con->waitForData( ServerVersionLength );
}

KRFBLogin::~KRFBLogin()
{

}

KRFBLogin::State KRFBLogin::state() const
{
  return currentState;
}

void KRFBLogin::gotServerVersion()
{
  owarn << "Got server version" << oendl; 

  disconnect( con, SIGNAL( gotEnoughData() ), 
	      this, SLOT( gotServerVersion() ) );

  // Read the server's version message
  char serverVersion[ ServerVersionLength + 1 ];
  con->read( serverVersion, ServerVersionLength );
  serverVersion[ ServerVersionLength ] = '\0';

  QCString rfbString( serverVersion, ServerVersionLength + 1 );
  versionString = rfbString;

  QRegExp regexp( "RFB [0-9][0-9][0-9]\\.[0-9][0-9][0-9]\n" );

  if ( rfbString.find( regexp ) == -1 ) {
    static QString msg = tr( "Error: Invalid server version, %1" ).arg( rfbString );

    owarn << msg << oendl; 
    emit error( msg );
    currentState = Error;
    return;
  }

  // Calculate the actual version number
  serverMajor = (serverVersion[4] - '0') * 100
    + (serverVersion[5] - '0') * 10
    + (serverVersion[6] - '0');
  serverMinor = (serverVersion[8] - '0') * 100
    + (serverVersion[9] - '0') * 10
    + (serverVersion[10] - '0');

  owarn << "Server Version: " << serverMajor << "." << serverMinor << "" << oendl; 

  if ( serverMajor != 3 ) {
    QString msg = tr( "Error: Unsupported server version, %1" )
      .arg( rfbString );

    owarn << msg << oendl; 
    emit error( msg );
    currentState = Error;
    return;    
  }
  
  if ( serverMinor != 3 ) {
    owarn << "Minor version mismatch: " << serverMinor << "" << oendl; 
  }

  // Setup for the next state
  sendClientVersion();

  connect( con, SIGNAL( gotEnoughData() ), SLOT( gotAuthScheme() ) );
  con->waitForData( AuthSchemeLength );
}

void KRFBLogin::gotAuthScheme()
{
  disconnect( con, SIGNAL( gotEnoughData() ), 
	      this, SLOT( gotAuthScheme() ) );

  // Got data
  CARD32 scheme;
  con->read( &scheme, AuthSchemeLength );
  scheme = Swap32IfLE( scheme );

  static QString statusMsgOk = tr( "Logged in" );

  switch ( scheme ) {
  case 0:
    owarn << "Failed" << oendl; 
    // Handle failure
    connect( con, SIGNAL( gotEnoughData() ), SLOT( gotFailureReasonSize() ) );
    con->waitForData( FailureReasonSizeLength );
    break;
  case 1:
    // Handle no auth
    emit status( statusMsgOk );
    con->gotRFBConnection();
    break;
  case 2:
    // Handle VNC auth
    connect( con, SIGNAL( gotEnoughData() ), SLOT( gotChallenge() ) );
    con->waitForData( ChallengeLength );
    break;
  default:
    owarn << "Unknown authentication scheme, 0x" << scheme << "" << oendl; 
    currentState = Error;
    break;
  };
}

void KRFBLogin::gotChallenge()
{
  disconnect( con, SIGNAL( gotEnoughData() ),
	      this, SLOT( gotChallenge() ) );

  QTimer::singleShot( 0, this, SLOT(getPassword()) );
}

void KRFBLogin::getPassword()
{
  // Got data
  CARD8 challenge[ ChallengeLength ];
  con->read( challenge, ChallengeLength );

  // Last chance to enter a password
  if ( con->options_->password.isNull() ) {
    owarn << "krfblogin needs a password" << oendl; 
    emit passwordRequired( con );
  }

  if ( con->options_->password.isNull() ) {
    QString msg = tr( "Error: This server requires a password, but none "
			"has been specified.\n" );
    
    emit error( msg );
    return;
  }

  vncEncryptBytes( (unsigned char *) challenge, QCString(con->options_->password.latin1()).data() );
  con->write( challenge, ChallengeLength );

  connect( con, SIGNAL( gotEnoughData() ), SLOT( gotAuthResult() ) );
  con->waitForData( AuthResultLength );
}

void KRFBLogin::gotFailureReasonSize()
{
  disconnect( con, SIGNAL( gotEnoughData() ), this,
	      SLOT( gotFailureReasonSize() ) );
}

void KRFBLogin::gotAuthResult()
{
  // Got data
  disconnect( con, SIGNAL( gotEnoughData() ), this,
	      SLOT( gotAuthResult() ) );

  long result;
  con->read( &result, AuthResultLength );
  result = Swap32IfLE( result );

  owarn << "Authentication Result is 0x" << result << "" << oendl; 

  static QString failed = tr( "Error: The password you specified was incorrect." );
  static QString tooMany = tr( "Error: Too many invalid login attempts have been made\n"
				 "to this account, please try later." );

  static QString statusMsgOk = tr( "Logged in" );
  static QString statusMsgFailed = tr( "Login Failed" );
  static QString statusMsgTooMany = tr( "Too many failures" );

  switch( result ) {
  case AuthOk:
    emit status( statusMsgOk );
    con->gotRFBConnection();
    break;
  case AuthFailed:
    owarn << "Dammit" << oendl; 
    emit status( statusMsgFailed );
    emit error( failed );
    break;
  case AuthTooMany:
    emit status( statusMsgTooMany );
    emit error( tooMany );    
    break;
  default:
    owarn << "Invalid authentication result, " << result << "" << oendl; 
    break;
  }
}

void KRFBLogin::sendClientVersion()
{
  owarn << "Sending client version" << oendl; 
  con->write( (void*)"RFB 003.003\n", ClientVersionLength );
}
