//
//  Distributed Balls (version 6)
//
//  A testing application for the distributed scenes.
//


#ifdef _MSC_VER
// Pro linker: Budeme linkovat s main,
// nikoliv WinMain, jak je na Windows zvykem
#pragma comment(linker, "/entry:\"mainCRTStartup\"")
#endif


#if defined(__WIN32__) || defined(_WIN32)
#include <Inventor/Win/SoWin.h>
#include <Inventor/Win/viewers/SoWinExaminerViewer.h>
#else
#include <Inventor/Qt/SoQt.h>
#include <Inventor/Qt/viewers/SoQtExaminerViewer.h>
#endif

#include <Inventor/SbTime.h>
#include <Inventor/nodes/SoAnnotation.h>
#include <Inventor/nodes/SoComplexity.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoDirectionalLight.h>
#include <Inventor/nodes/SoEnvironment.h>
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoMatrixTransform.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/nodes/SoRotation.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoSphere.h>
#include <Inventor/nodes/SoText2.h>
#include <Inventor/nodes/SoTranslation.h>
#include <Inventor/sensors/SoOneShotSensor.h>
#include <Inventor/errors/SoReadError.h>
#include <time.h>

#if defined(__WIN32__) || defined(_WIN32)
# include <conio.h>
#else
# include <termios.h>
# include <fcntl.h>
#endif

#if defined(__WIN32__) || defined(_WIN32)
# include <process.h> // for _getpid()
# define getpid _getpid
#else
# include <unistd.h> // for getpid()
#endif

#include <Inventor/cve/SoComputer.h>
#include <Inventor/cve/SoNetwork.h>
#include <Inventor/cve/SbNetListener.h>
#include <Inventor/cve/SoDistributionGroup.h>
#include <Inventor/cve/SbTransaction.h>
#include <Inventor/cve/SbTransactionP.h>
#include <Inventor/cve/TNet.h>
#include "SoPerfGraph.h"
#include "Ball.h"
#include "CollisionAssert.h"

int listeningPort = 1043;
static const char transactionStartSimName[] = "startSimulation";
static SbBool startSignalled = FALSE;
static SbBool barrierReached = FALSE;


float tableSize = 10.f;
float tableSize2 = tableSize / 2.f;


SoSeparator *root;
SbList<Ball*> ballList;
Ball *myBall = NULL;
SoText2 *text2;

int testId;
int participRole;
int ms_delay = 50;
int ms_simLatency = 0;//300;
float speed = 0.3f; //1.f;
float slowDown = 1.f;
SoComputer *server = NULL;
SoPerfGraph *perfGraphFPS;
SoPerfGraph *perfGraphNet;
SoPerfGraph *commitDelayGraph;
static int lastNetTotal = 0;
int ballCommits = 0;
int ballAborts = 0;
int abortSteps = 0;
int numCollisions = 0;
int numRestarts = 0;
int sumSpeculative = 0;
int numSteps = 0;
SbBool abortStepDone = FALSE;


static int tryKeyPress()
{
#if defined(__WIN32__) || defined(_WIN32)
  // Windows uses _kbhit() and _getch()
  if (_kbhit() == 0)
    return -1;
  return _getch();
#else

  // Unix and Linux uses termios functions
  static SbBool active = FALSE;
  static struct termios origAttr, attr;
  char ch;
  int n;

  if (!active) {
    active = TRUE;

    tcgetattr(0, &origAttr);
    attr = origAttr;
    attr.c_lflag &= ~ICANON;
    attr.c_lflag &= ~ECHO;
    attr.c_lflag &= ~ISIG;
    attr.c_cc[VMIN] = 1;
    attr.c_cc[VTIME] = 0;
    tcsetattr(0, TCSANOW, &attr);
  }

  attr.c_cc[VMIN] = 0;
  tcsetattr(0, TCSANOW, &attr);
  n = read(0, &ch, 1);
  attr.c_cc[VMIN] = 1;
  tcsetattr(0, TCSANOW, &attr);
  
  if (n == 0)
    return -1;

  active = FALSE;
  tcsetattr(0,TCSANOW, &origAttr);
  return ch;
#endif
}



static void transactionStartSimCB(SbTransaction *tr, const SbName &callbackName, 
                                  void *buf, size_t bufsize)
{
  if (!startSignalled) {
    startSignalled = TRUE;
    
    if (tr->isSenderLocalhost()) {
      SbTransaction *tr2 = new SbTransaction(tr->getDistributionGroup());
      tr2->setCommitCallback(transactionStartSimName);
      tr2->schedule();
      printf("startSimulationTransaction scheduling start barrier.\n");
    }

    printf("startSimulationTransaction commited.\n");
    return;
  }

  barrierReached = TRUE;
  printf("startBarrier reached.\n");
}



static void timeTick(void * data, SoSensor * sensor)
{
  static SbTime lastTime = SbTime::zero();
  SbTime dt;

  if (lastTime == SbTime::zero()) {
    lastTime = SbTime::getTimeOfDay();
    dt = SbTime::zero();
  } else {
    SbTime ct = SbTime::getTimeOfDay();
    dt = ct - lastTime;
    lastTime = ct;
  }                  

  // update FPS graph
  if (dt.getValue() != 0.)
    perfGraphFPS->appendValue(1.f/float(dt.getValue()));

  // update network traffic graph
  int newNetTotal = tnGet(TN_TOTAL_BYTES_SENT, 0) + tnGet(TN_TOTAL_BYTES_RECEIVED, 0);
  perfGraphNet->appendValue(float(newNetTotal - lastNetTotal));
  lastNetTotal = newNetTotal;

  // adjust simulation speed
  dt *= speed;

  // this shall make simulation more safe against jumping balls outside the table
  if (dt.getValue() > 1.)
    dt.setValue(1.);


  // update myBall
  myBall->timeTick(dt);

  // update test string
  SoDistributionGroup *dg = SoNetwork::getDefaultDistributionGroup();
  if (rand() % 10 == 0) {
    static int c = 0;
    SbString s;
    s.sprintf("Modified string %i.", c);
    if (dg->getThisComputerIndex() % 2 == 1)  c++;
    else  c--;
    SbTransaction *tr = new SbTransaction;
    SbIntList list;
    text2->string.transactionNotifyRead(SoField::LATEST, tr, NULL, list, TRUE);
    text2->string.transactionWrite(0, 1, &s, tr, NULL, list);
    tr->schedule();
  }
  

  abortStepDone = FALSE;
  SoNetwork::process();

  // handle collision
  if (myBall->collisionHappened) {
    myBall->collisionHappened = FALSE;
    myBall->setPosAndSpeed(myBall->collisionPos, myBall->collisionSpeed, SoField::COMMITTED);
    numRestarts++;
  }

#if 0
  printf("Balls: commits %i, aborts %i, abortSteps %i, collisions %i, numRestarts %i.\n",
         ballCommits, ballAborts, abortSteps, numCollisions, numRestarts);
#elif 0
  sumSpeculative += myBall->matrix->matrix.getCveNumValues(FALSE);
  numSteps++;
  printf("Num of speculative tr: %i (avg: %.2f), num of transactions in dg: %i.\n",
    myBall->matrix->matrix.getCveNumValues(FALSE), float(sumSpeculative)/numSteps,
    dg->transactionList.getLength());
#endif

  if (dg->getState() != SoDistributionGroup::RUNNING) {
    printf("SoDistributionGroup state changed to DISCONNECTED. Shutting down application.\n");
#if defined(_WIN32)  || defined(__WIN32__)
    SoWin::exitMainLoop();
#else
    SoQt::exitMainLoop();
#endif
    return;
  }  

  commitDelayGraph->appendValue(float(SbTimeStamp::generate(dg).getTime() - dg->commitTime.getTime()));

  // update circles
  int i;
  for (i=0; i<ballList.getLength(); i++)
    ballList[i]->updateCircles();

  // Sleep shortly
  if (ms_delay != -1) {
#if defined(__WIN32__) || defined(_WIN32)
    Sleep(ms_delay); // value in ms
#else
    usleep(ms_delay*1000); // value in us
#endif
  }

  sensor->schedule();
}



int main(int argc, char **argv)
{
  // CVE setup
  putenv("CVE_DEBUG_TRANSACTIONS=1");
  putenv("CVE_DEBUG_DISTRIBUTION_GROUP=1");
  putenv("CVE_PRINT_STATISTICS=1");
  putenv("CVE_VERIFY_TRANSACTION_EXECUTION=0");

  // Inicializace nahodnych cisel
  srand((unsigned int)time(NULL) + getpid());

  // Inicializuj Inventor
#if defined(__WIN32__) || defined(_WIN32)
  HWND window = SoWin::init(argc, argv, argv[0]);
#else
  QWidget* window = SoQt::init(argv[0]);
#endif
  if (window == NULL)  exit(1);

#if 0 // experimental code for understanding of some strange float-point precision bug
//  char n[8] = { (char)0xb6, (char)0xd2, (char)0xee, (char)0x40, (char)0xb1, (char)0x00, (char)0xd1, (char)0x41 };
  char n[8] = { (char)0x4c, (char)0x4e, (char)0xce, (char)0x91, (char)0xbe, (char)0x00, (char)0xd1, (char)0x41 };
  SbString s;
  s.sprintf("%.15lg", *((double*)&n));
  printf("%s\n", s.getString());
  s.sprintf("%.16lg", *((double*)&n));
  printf("%s\n", s.getString());
  s.sprintf("%.17lg", *((double*)&n));
  printf("%s\n", s.getString());
  s.sprintf("%.18lg", *((double*)&n));
  printf("%s\n", s.getString());
#endif

#if 0 // this demonstrates floating-point SoOutput/SoInput transfer bug
  double time = 1141117146.7954047;
  //double time = 1141117147.5657706;
  SbString s;
  s.sprintf("%.17lg", time); // 17 is required precision for doubles to not miss even the least significant bit.

  // debugging code - ok
  double d2;
  assert(sscanf(s.getString(), "%lg", &d2) == 1);
  if (time != d2) {
    printf("FAILED: %s, time: %.17f, d2: %.17f\n", s.getString(), time, d2);
    assert(time == d2);
  }

  // debugging code - fails
  char buf[200];
  SoOutput out2;
  out2.setBuffer(&buf[0], 200, NULL);
  out2.write(s.getString());

  d2 = 0.;
  SoInput in2;
  in2.setBuffer(&buf[0], 200);
  assert(in2.read(d2));
  if (time != d2) {
    printf("FAILED: %s, time: %.17f, d2: %.17f\n", s.getString(), time, d2);
    assert(time == d2);
  }
#endif

  // Vytvoř kořen scény
  root = new SoSeparator;
  root->ref();

  // Vlož kameru do scény
  SoPerspectiveCamera *camera = new SoPerspectiveCamera;
  camera->position.setValue(0.f, -5.f, 20.f);
  camera->pointAt(SbVec3f(0.f,0.f,0.f), SbVec3f(0.f,1.f,0.f));
  root->addChild(camera);

  // Environment
  SoEnvironment *e = new SoEnvironment;
  e->ambientIntensity.setValue(1.f);
  root->addChild(e);

  // Světlo
  root->addChild(new SoDirectionalLight);

  // Komplexita
  SoComplexity *complexity = new SoComplexity;
  complexity->value.setValue(10.f);
  root->addChild(complexity);

  // Playground
  SoSeparator *pg = new SoSeparator;
  root->addChild(pg);

  SoMaterial *material = new SoMaterial;
  material->ambientColor.setValue(0.f, 0.5f, 0.f);
  material->diffuseColor.setValue(0.f, 0.5f, 0.f);
  pg->addChild(material);

  SoSeparator *pgGround = new SoSeparator;
  pg->addChild(pgGround);

  SoTranslation *tr1 = new SoTranslation;
  tr1->translation.setValue(0.f, 0.f, -Ball::radius-1.f);
  pgGround->addChild(tr1);

  SoCube *cube1 = new SoCube;
  cube1->width = tableSize;
  cube1->height = tableSize;
  cube1->depth = 2.f;
  pgGround->addChild(cube1);

  SoSeparator *pgBorder = new SoSeparator;
  pg->addChild(pgBorder);

  SoTranslation *tr2 = new SoTranslation;
  tr2->translation.setValue(0.f, tableSize2+1.f, 0.f);
  pgBorder->addChild(tr2);

  SoCube *cube2 = new SoCube;
  cube2->width = tableSize;
  cube2->height = 2.f;
  cube2->depth = 2.f;
  pgBorder->addChild(cube2);

  SoRotation *rot = new SoRotation;
  rot->rotation.setValue(SbVec3f(0.f,0.f,1.f), float(M_PI_2));

  pg->addChild(rot);
  pg->addChild(pgBorder);
  pg->addChild(rot);
  pg->addChild(pgBorder);
  pg->addChild(rot);
  pg->addChild(pgBorder);

  SoAnnotation *extraText = new SoAnnotation;
  text2 = new SoText2;
  text2->string = "Initial text.";
  extraText->addChild(text2);
  root->addChild(extraText);

  // Argument parsing
  int i;
  testId = 0;
  participRole = 0;
  SbInetAddr addr;
  SbBool addrParsed = FALSE;
  SbInetAddr addr2;
  SbBool addr2Parsed = FALSE;

  // Parse arguments
  for (i=1; i<argc; i++) {

    // -t1, -t2, ...
    if ((strcmp(argv[i], "-t1") == 0 || strcmp(argv[i], "-t2") == 0 ||
         strcmp(argv[i], "-t3") == 0 || strcmp(argv[i], "-t4") == 0) && testId == 0) {
      testId = (strcmp(argv[i], "-t1") == 0) ? 1 :
               (strcmp(argv[i], "-t2") == 0) ? 2 :
               (strcmp(argv[i], "-t3") == 0) ? 3 : 4;
    } else

    // -r1, -r2, ...
    if ((strcmp(argv[i], "-r1") == 0 || strcmp(argv[i], "-r2") == 0 ||
         strcmp(argv[i], "-r3") == 0 || strcmp(argv[i], "-r4") == 0) && participRole == 0) {
      participRole = (strcmp(argv[i], "-r1") == 0) ? 1 :
                     (strcmp(argv[i], "-r2") == 0) ? 2 :
                     (strcmp(argv[i], "-r3") == 0) ? 3 : 4;
    } else

    // -l num - listening port
    if (strcmp(argv[i], "-l") == 0) {
      i++;
      if (i<argc)  listeningPort = atoi(argv[i]);
      else {
        printf("Listening port not specified.\n");
        exit(99);
      }
      if (listeningPort == 0) {
        printf("Wrong listening port argument.\n");
        exit(99);
      }
    } else

    // --delay int (in miliseconds)
    if (strcmp(argv[i], "--delay") == 0) {
      if (argc <= (i+1)) {
        printf("Missing value for parameter --delay.\n");
        exit(-1);
      }
      i++;
      if (sscanf(argv[i], "%i", &ms_delay) != 1 ||
          ms_delay < 0) {
        printf("Invalid value following argument --delay.\n");
        exit(-1);
      }
    } else

    // --sim-latency int (in miliseconds)
    if (strcmp(argv[i], "--sim-latency") == 0) {
      if (argc <= (i+1)) {
        printf("Missing value for parameter --sim-latency.\n");
        exit(-1);
      }
      i++;
      if (sscanf(argv[i], "%i", &ms_simLatency) != 1 ||
          ms_simLatency < 0) {
        printf("Invalid value following argument --sim-latency.\n");
        exit(-1);
      }
    } else

    // --speed float (as multiplier to real speed)
    if (strcmp(argv[i], "--speed") == 0) {
      if (argc <= (i+1)) {
        printf("Missing value for parameter --delay.\n");
        exit(-1);
      }
      i++;
      if (sscanf(argv[i], "%f", &speed) != 1) {
        printf("Invalid value following argument --speed.\n");
        exit(-1);
      }
    } else

    // --slow-down float (as multiplier to real speed)
    if (strcmp(argv[i], "--slow-down") == 0) {
      if (argc <= (i+1)) {
        printf("Missing value for parameter --slow-down.\n");
        exit(-1);
      }
      i++;
      if (sscanf(argv[i], "%f", &slowDown) != 1) {
        printf("Invalid value following argument --slow-down.\n");
        exit(-1);
      }
    } else
    // get ip:port address
    if (!addrParsed) {
      if (addr.setString(argv[i]))
        addrParsed = TRUE;
    } else

    if (!addr2Parsed) {
      if (addr2.setString(argv[i]))
        addr2Parsed = TRUE;
    } else {

      // bad argument
      printf("Invalid argument \"%s\".\n", argv[i]);
      exit(-1);
    }
  }

  // if testId and participRole was not assigned, make them default
  if (testId == 0)
    testId = 1;
  if (participRole == 0)
    participRole = 1;

  if (participRole == 3 && !addr2Parsed) {
    printf("Addr2 has not been specified.\n");
    exit(-1);
  }

  speed *= slowDown;
  ms_delay = int(float(ms_delay) / slowDown);
  ms_simLatency = int(float(ms_simLatency) / slowDown);

  printf("Arguments parsed. Initializing network...\n");

  
  // Network initialization
  SoNetwork::init();
  
  // set zero network latency first (this helps to synchronize clocks among the computers)
  tnSet(TN_GLOBAL_LATENCY, 0, 0);
  tnSet(TN_GLOBAL_LATENCY_DISPERSION, 0, 0);

  Ball::collisionAssert = new CollisionAssert;
  SoDistributionGroup *dg = SoNetwork::getDefaultDistributionGroup();
  dg->registerAssert(Ball::collisionAssert);

  // set update model
//  if (participRole == 2)
    dg->updateGraphModel = SoDistributionGroup::COMMITTED_VALUE;
//  else
  //  dg->updateGraphModel = SoDistributionGroup::LATEST_VALUE;

  // Initialize replication group
  if (!dg->startListening(listeningPort))
    printf("Listening error.\n");
  else {
    // Print listening info
    char buf[16];
    if (!tnIP2Str(dg->getListeningAddress().getIP(), buf, sizeof(buf)))
      strcpy(buf, "???");
    printf("Listening at %s:%i\n", buf, dg->getListeningAddress().getPort());
  }

  // SERVER CODE
  if (!addrParsed) {

  // CLIENT CODE
  } else {

    // set port if unspecified
    if (addr.getPort() == 0)
      addr.setPort(listeningPort);

    // connect to distribution group
    //printf("Trying to connect to %s...\n", addr.getString().getString());
    dg->connect(addr);
  }

  // wait for identity in DistributionGroup
  while (dg->getThisComputerIndex() == -1) {
    SoNetwork::sleep(100);

    SoNetwork::process();
    if (dg->getState() == SoDistributionGroup::DISCONNECTED) {
      printf("SoDistributionGroup state changed to DISCONNECTED. Exiting.\n");
      return 0;
    }
  }

  if (dg->active[dg->getThisComputerIndex()] == FALSE) {

    do {
      SoNetwork::sleep(100);

      SoNetwork::process();
      if (dg->getState() == SoDistributionGroup::DISCONNECTED) {
        printf("SoDistributionGroup state changed to DISCONNECTED. Exiting.\n");
        return 0;
      }
    } while (dg->active[dg->getThisComputerIndex()] == FALSE);
  }

  printf("Waiting for other computers. Press SPACE to start.\n");
  SbTransaction::registerCallback(transactionStartSimName, transactionStartSimCB);
  while (!startSignalled) {
    SoNetwork::sleep(100);

    SoNetwork::process();
    if (dg->getState() == SoDistributionGroup::DISCONNECTED) {
      printf("SoDistributionGroup state changed to DISCONNECTED. Exiting.\n");
      return 0;
    }

    if (tryKeyPress() == 32) {
      printf("SPACE pressed. Sending startSimulationTransaction...\n");
      SbTransaction *tr = new SbTransaction(dg);
      tr->setCommitCallback(transactionStartSimName);
      tr->schedule();
    }
  }

  int num = dg->computer.getNum();
  printf("Initializing application with %i computer(s)...\n", num);

  // set network latency
  tnSet(TN_GLOBAL_LATENCY, 0, ms_simLatency);
  //tnSet(TN_GLOBAL_LATENCY_DISPERSION, 0, 300);

  // text connection
  dg->appendToDict(&text2->string);

  // Three balls, each driven by different computer
  for (i=0; i<num; i++) {
    Ball *ball = new Ball;
    ballList.append(ball);
    dg->appendToDict(ball->getSceneGraph());
    ball->activate(root);
  }

  // set myBall
  myBall = ballList[dg->getThisComputerIndex()];

  if (ballList.getLength() >= 2)
    ballList[1]->material->diffuseColor.setValue(0.5f, 0.0f, 0.0f);
  if (ballList.getLength() >= 3)
    ballList[2]->material->diffuseColor.setValue(0.5f, 0.5f, 0.0f);

  assert(dg->computer[dg->getThisComputerIndex()] == SoNetwork::getThisComputer() && 
         "Wrong index of thisComputer in replicationGroup.");

  for (i=0; i<num; i++)
    ballList[i]->randomInit(i);

  printf("Initialization finished. Entering mainloop...\n");


  SoOneShotSensor *timeSensor = new SoOneShotSensor(timeTick, NULL);
  timeSensor->schedule();

  // Vytvoř renderovací okno
#if defined(__WIN32__)
  SoWinExaminerViewer *viewer = new SoWinExaminerViewer(window);
#else
  SoQtExaminerViewer *viewer = new SoQtExaminerViewer(window);
#endif

  // Nastav parametry kamery
  camera->viewAll(root, viewer->getViewportRegion());

  // Vytvor performance monitory
  SoPerfGraph::initClass();
  perfGraphFPS = new SoPerfGraph;
  perfGraphFPS->horScreenOrigin = SoPerfGraph::RIGHT;
  perfGraphFPS->horAlignment = SoPerfGraph::RIGHT;
  perfGraphFPS->position.setValue(-0.03f,0.03f);
  root->addChild(perfGraphFPS);

  perfGraphNet = new SoPerfGraph;
  perfGraphNet->horScreenOrigin = SoPerfGraph::RIGHT;
  perfGraphNet->horAlignment = SoPerfGraph::RIGHT;
  perfGraphNet->position.setValue(-0.03f,0.23f);
  root->addChild(perfGraphNet);

  commitDelayGraph = new SoPerfGraph;
  commitDelayGraph->horScreenOrigin = SoPerfGraph::RIGHT;
  commitDelayGraph->horAlignment = SoPerfGraph::RIGHT;
  commitDelayGraph->position.setValue(-0.03f,0.43f);
  root->addChild(commitDelayGraph);

  // Nastav renderovací okno
  viewer->setSceneGraph(root);
  viewer->setTitle("Distributed Balls 6");
  viewer->show();

  // Zobraz okna a rozjeď renderovací smyčku
#if defined(__WIN32__)
  SoWin::show(window);
  SoWin::mainLoop();
#else
  SoQt::show(window);
  SoQt::mainLoop();
#endif

  // Uvolni zdroje
#if !defined(NDEBUG)
  printf("Number of opened connections (before closing attempt): %i\n", tnGet(TN_NUM_CONNECTIONS, 0));

  dg->deactivate();

  SoNetwork::disconnectAllComputers(1000, FALSE);
  SoNetwork::closeAllListeners();

  if (tnGet(TN_NUM_CONNECTIONS, 0) != 0)
    printf("ERROR: Some connections have been left open.\n");

  SoNetwork::disconnectAllComputers(0, TRUE);

  delete Ball::collisionAssert;
  delete timeSensor;
  delete viewer;
  root->unref();
  assert(SoNetwork::getThisComputer());
  for (i=0; i<ballList.getLength(); i++)
    delete ballList[i];
  ballList.truncate(0);
  //delete server;
  Ball::cleanup();
  SoPerfGraph::cleanup();
  //SoDB::finish();
  printf("Done.\n");
#endif

  return 0;
}
