#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
#include <pthread.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <errno.h>
#include <stdarg.h>
#include <ctype.h>
#include <sys/time.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <math.h>
#include <errno.h>

#include "orcd.h"
#include "SSocket.h"
#include "serial.h"
#include "ioutils.h"
#include "logutils.h"
#include "timespec.h"
#include "orcutils.h"

#define LOGCHANNEL "TRANS"

int latencycount=0;
long latencysum=0;
long latency2sum=0;
int latencymin=9999;
int latencymax=0;

// returns the response length, or zero if timeout.
int doTransaction(orcd_t *orcd, const void *cmdin, int cmdlen, void *response)
{
  unsigned char thisID; // the remapped transaction id
  unsigned char *cmd=(unsigned char*) cmdin;
  unsigned char buf[ORC_MAX_PACKETSIZE];

  int trials=0;

  struct timeval starttv;
  gettimeofday(&starttv, NULL);

  //  printf("cmd: %s\n",(char*) cmdin);

  ///////////////////////////////////////////
  // Allocate a transaction ID
  ///////////////////////////////////////////
  pthread_mutex_lock(&orcd->scoreboardMutex);

  do {
    trials++;

    // we're fine, grab an ID.
    thisID=orcd->transactionNextID;
    orcd->transactionNextID=(orcd->transactionNextID+1)&0x3f;

    // avoid ID==0
    if (orcd->transactionNextID==0)
      orcd->transactionNextID++;

    // this should never happen!
    if (trials>ORC_MAX_TRANSID)
      {
	log(LOG_ERROR, LOGCHANNEL, "We didn't find an available transaction ID. Sleeping.");
	trials=0;
	pthread_mutex_unlock(&orcd->scoreboardMutex);
	sleep(1); // if we're this backlogged, wait a long time.
	pthread_mutex_lock(&orcd->scoreboardMutex);
      }

  } while(orcd->transactionScoreboard[thisID].status!=ORC_TRANS_FREE);

  // Now, wait until enough old transactions have passed
  while (orcd->transactionsPending>=ORC_MAX_SIMULTRANS)
    {
      //      log(LOG_WARN, LOGCHANNEL,"Saturated channel");

      pthread_cond_wait(&orcd->scoreboardCond, &orcd->scoreboardMutex);
    }
  orcd->transactionsPending++;

  // lock this transaction record until we're ready to wait on it.
  // Note that if an ancient packet (with the same ID) comes back,
  // we will now misinterpret it as the desired response. But this
  // is virtually impossible due to the size of the sequence # space
  // and the limited number outstanding transactions.
  pthread_mutex_lock(&orcd->transactionScoreboard[thisID].mutex);

  if (orcd->transactionScoreboard[thisID].status != ORC_TRANS_FREE)
    log(LOG_WARN,LOGCHANNEL,"Got non-free state transaction from scoreboard\n");

  orcd->transactionScoreboard[thisID].status   = ORC_TRANS_WAITING;
  orcd->transactionScoreboard[thisID].response = response;

  pthread_mutex_unlock(&orcd->scoreboardMutex);
 
  unsigned char oldflags=cmd[2];
 
  ///////////////////////////////////////////
  // Send the packet
  ///////////////////////////////////////////
  pthread_mutex_lock(&orcd->serialwriteMutex);
  buf[0]=0xED;
  buf[1]=cmd[1];
  buf[2]=thisID | (cmd[2]&0xc0);
  memcpy(&buf[3], &cmd[3], cmdlen-4);
  buf[buf[1]-1]=checksum(0, buf, buf[1]-1);

  int writelen;

  if (orcd->serialfd_connected)
    {
      writelen=writeall(orcd->serialfd, buf, buf[1]);
      if (buf[1]!=writelen)
	{
	  log(LOG_ERROR,LOGCHANNEL,"error writing to Orc: %s", strerror(errno));
	  orcd->serialfd_connected=0;
	}
    }
  else
    {
      writelen = 0;
    }

  // update statistics
  orcd->bytecount+=(cmdlen);
  orcd->transactioncount++;

  // we're done 
  pthread_mutex_unlock(&orcd->serialwriteMutex);

  ///////////////////////////////////////////
  // Wait for the response                 //
  ///////////////////////////////////////////

  struct timespec nowtime, wakeuptime;
  
  // exponential backoff
  int thistimeout=orcd->serialtimeoutms*(orcd->serialconsecutivetimeouts+1);

  timespec_now(&nowtime);
  wakeuptime=nowtime;
  timespec_addms(&wakeuptime, thistimeout);

  // wait for an ack
  int responselen=0;

  pthread_cond_timedwait(&orcd->transactionScoreboard[thisID].cond, 
			 &orcd->transactionScoreboard[thisID].mutex, 
			 &wakeuptime);
  
  if (orcd->transactionScoreboard[thisID].status==ORC_TRANS_ACKED)
    {
      responselen=orcd->transactionScoreboard[thisID].responselen;
      // fixup the packet by restoring the original flags
      unsigned char *r=(unsigned char*) response;

      if (r!=NULL)
	{
	  r[2]=oldflags;
	  r[responselen-1]=checksum(r, r[1]-1);
	}

      orcd->serialconsecutivetimeouts=0;
    }
  else
    {
      orcd->serialconsecutivetimeouts++;
      if (orcd->serialconsecutivetimeouts>3)
	orcd->serialconsecutivetimeouts=3;

      responselen=0;
      if (orcd->opt->getOptionBool("verbose"))
	log(LOG_WARN, LOGCHANNEL,"timeout (%3i ms). transid=0x%02x, flags=0x%02x, pending=%i", 
	    thistimeout,thisID,cmd[2], orcd->transactionsPending);
    }

  orcd->transactionScoreboard[thisID].status=ORC_TRANS_FREE;
  orcd->transactionScoreboard[thisID].response=NULL;

  pthread_mutex_unlock(&orcd->transactionScoreboard[thisID].mutex);

  pthread_mutex_lock(&orcd->scoreboardMutex);
  orcd->transactionsPending--;
  pthread_cond_signal(&orcd->scoreboardCond);
  pthread_mutex_unlock(&orcd->scoreboardMutex);


  struct timeval endtv;
  gettimeofday(&endtv, NULL);

  int elapsedus=(endtv.tv_sec-starttv.tv_sec)*1000000+
    endtv.tv_usec-starttv.tv_usec;

  int latency=elapsedus/1000;

  //  printf("LATENCY %02X: %i\n", cmd[2], latency);

  latencysum+=latency;
  latency2sum+=(latency*latency);
  latencymin=latency<latencymin ? latency : latencymin;
  latencymax=latency>latencymax ? latency : latencymax;
  latencycount++;
  if (latencycount==5000 && orcd->opt->getOptionBool("verbose"))
    {
      double latencyvar=(latency2sum-latencysum)/((double)latencycount);
      printf("%i\t%i\t%i\t%.3f\n",latencymin, (int) latencysum/latencycount, 
	     latencymax,sqrt(latencyvar));
      latencymin=9999;
      latencymax=0;
      latencycount=0;
      latencysum=0;
      latency2sum=0;
    }

  return responselen;
}
