/*****************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * $Id$
 *
 * Connect N connections. Z are idle, and X are active. Transfer as fast as
 * possible.
 *
 * Run for a specific amount of time (10 secs for now). Output detailed timing
 * information.
 *
 * The same is hiper.c but instead using the new *socket() API instead of the
 * "old" *perform() call.
 *
 * Uses libevent.
 *
 */

/* The maximum number of simultanoues connections/transfers we support */
#define NCONNECTIONS 50000

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <sys/poll.h>

#include <curl/curl.h>

#include <event.h> /* for libevent */

#define MICROSEC 1000000 /* number of microseconds in one second */

/* The maximum time (in microseconds) we run the test */
#define RUN_FOR_THIS_LONG (20*MICROSEC)

/* Number of loops (seconds) we allow the total download amount and alive
   connections to remain the same until we bail out. Set this slightly higher
   when using asynch supported libcurl. */
#define IDLE_TIME 10

struct ourfdset {
  /* __fds_bits is what the Linux glibc headers use when they declare the
     fd_set struct so by using this we can actually avoid the typecase for the
     FD_SET() macro usage but it would hardly be portable */
  char __fds_bits[NCONNECTIONS/8];
};
#define FD2_ZERO(x) FD_ZERO((fd_set *)x)

typedef struct ourfdset fd2_set;

struct globalinfo {
  size_t dlcounter;
};

struct connection {
  CURL *e;
  int id; /* just a counter for easy browsing */
  char *url;
  size_t dlcounter;
  struct globalinfo *global;
  char error[CURL_ERROR_SIZE];
};

struct fdinfo {
  /* create a link list of fdinfo structs */
  struct fdinfo *next;
  struct fdinfo *prev;
  curl_socket_t sockfd;
  CURL *easy;
  int action; /* as set by libcurl */
  long timeout; /* as set by libcurl */
  struct event ev;
};

static struct fdinfo *allsocks;

static struct fdinfo *findsock(curl_socket_t s)
{
  /* return the struct for the given socket */
  struct fdinfo *fdp = allsocks;

  while(fdp) {
    if(fdp->sockfd == s)
      break;
    fdp = fdp->next;
  }
  return fdp; /* a struct pointer or NULL */
}

static void remsock(curl_socket_t s)
{
  struct fdinfo *fdp;
  while(fdp) {
    if(fdp->sockfd == s)
      break;
    fdp = fdp->next;
  }
  if(!fdp)
    /* did not find socket to remove! */
    return;

  if(fdp->prev)
    fdp->prev->next = fdp->next;
  if(fdp->next)
    fdp->next->prev = fdp->prev;
  else
    /* this was the last entry */
    allsocks = NULL;
}


static void addsock(curl_socket_t s, CURL *easy, int action, long timeout)
{
  struct fdinfo *fdp = calloc(sizeof(struct fdinfo), 1);

  fdp->sockfd = s;
  fdp->action = action;
  fdp->timeout = timeout;
  fdp->easy = easy;

  if(allsocks) {
    fdp->next = allsocks;
    allsocks->prev = fdp;

    /* now set allsocks to point to the new struct */
    allsocks = fdp;
  }
  else
    allsocks = fdp;
}

static void fdinfo2fdset(fd2_set *fdread, fd2_set *fdwrite, int *maxfd)
{
  struct fdinfo *fdp = allsocks;
  while(fdp) {
    if(fdp->action & CURL_POLL_IN)
      FD_SET(fdp->sockfd, (fd_set *)fdread);
    if(fdp->action & CURL_POLL_OUT)
      FD_SET(fdp->sockfd, (fd_set *)fdwrite);

    if(fdp->sockfd > *maxfd)
      *maxfd = fdp->sockfd;

    fdp = fdp->next;
  }
}

/* on port 8999 we run a modified (fork-) sws that supports pure idle and full
   stream mode */
#define PORT "8999"

#define HOST "192.168.1.13"

#define URL_IDLE   "http://" HOST ":" PORT "/1000"
#define URL_ACTIVE "http://" HOST ":" PORT "/1001"


static int socket_callback(CURL *easy,      /* easy handle */
                           curl_socket_t s, /* socket */
                           int what,        /* see above */
                           long ms,         /* timeout for wait */
                           void *userp)     /* "private" pointer */
{
  printf("socket %d easy %p what %d timeout %ld\n", s, easy, what, ms);

  if(what == CURL_POLL_REMOVE)
    remsock(s);
  else if(!findsock(s))
    addsock(s, easy, what, ms);

  return 0; /* return code meaning? */
}


static size_t
writecallback(void *ptr, size_t size, size_t nmemb, void *data)
{
  size_t realsize = size * nmemb;
  struct connection *c = (struct connection *)data;

  c->dlcounter += realsize;
  c->global->dlcounter += realsize;

#if 0
  printf("%02d: %d, total %d\n",
         c->id, c->dlcounter, c->global->dlcounter);
#endif
  return realsize;
}

/* return the diff between two timevals, in us */
static long tvdiff(struct timeval *newer, struct timeval *older)
{
  return (newer->tv_sec-older->tv_sec)*1000000+
    (newer->tv_usec-older->tv_usec);
}


/* store the start time of the program in this variable */
static struct timeval timer;

static void timer_start(void)
{
  /* capture the time of the start moment */
  gettimeofday(&timer, NULL);
}

static struct timeval cont; /* at this moment we continued */

int still_running; /* keep number of running handles */

struct conncount {
  long time_us;
  long laps;
  long maxtime;
};

static struct timeval timerpause;
static void timer_pause(void)
{
  /* capture the time of the pause moment */
  gettimeofday(&timerpause, NULL);

  /* If we have a previous continue (all times except the first), we can now
     store the time for a whole "lap" */
  if(cont.tv_sec) {
    long lap;

    lap = tvdiff(&timerpause, &cont);
  }
}

static long paused; /* amount of us we have been pausing */

static void timer_continue(void)
{
  /* Capture the time of the restored operation moment, now calculate how long
     time we were paused and added that to the 'paused' variable.
   */
  gettimeofday(&cont, NULL);

  paused += tvdiff(&cont, &timerpause);
}

static long total; /* amount of us from start to stop */
static void timer_total(void)
{
  struct timeval stop;
  /* Capture the time of the operation stopped moment, now calculate how long
     time we were running and how much of that pausing.
   */
  gettimeofday(&stop, NULL);

  total = tvdiff(&stop, &timer);
}

struct globalinfo info;
struct connection *conns;

long selects;
long selectsalive;
long timeouts;

long performalive;
long performselect;
long topselect;

int num_total;
int num_idle;
int num_active;

static void report(void)
{
  int i;
  long active = total - paused;
  long numdl = 0;

  for(i=0; i < num_total; i++) {
    if(conns[i].dlcounter)
      numdl++;
  }

  printf("Summary from %d simultanoues transfers (%d active)\n",
         num_total, num_active);
  printf("%d out of %d connections provided data\n", numdl, num_total);

  printf("Total time: %ldus select(): %ldus curl_multi_perform(): %ldus\n",
         total, paused, active);

  printf("%d calls to select(), average %d alive "
         "Average time: %dus\n",
         selects, selectsalive/selects,
         paused/selects);
  printf(" Average number of readable connections per select() return: %d\n",
         performselect/selects);
  printf(" Max number of readable connections for a single select() "
         "return: %d\n",
         topselect);

  printf("%ld select() timeouts\n", timeouts);

  printf("Downloaded %ld bytes in %ld bytes/sec, %ld usec/byte\n",
         info.dlcounter,
         info.dlcounter/(total/1000000),
         total/info.dlcounter);

}

int main(int argc, char **argv)
{
  CURLM *multi_handle;
  CURLMsg *msg;
  CURLcode code = CURLE_OK;
  CURLMcode mcode = CURLM_OK;
  int rc;
  int i;

  int prevalive=-1;
  int prevsamecounter=0;
  int prevtotal = -1;
  fd2_set fdsizecheck;
  int selectmaxamount;

  memset(&info, 0, sizeof(struct globalinfo));

  selectmaxamount = sizeof(fdsizecheck) * 8;
  printf("select() supports max %d connections\n", selectmaxamount);

  if(argc < 3) {
    printf("Usage: hiper [num idle] [num active]\n");
    return 1;
  }

  num_idle = atoi(argv[1]);
  num_active = atoi(argv[2]);

  num_total = num_idle + num_active;

  if(num_total > selectmaxamount) {
    printf("Requested more connections than supported!\n");
    return 4;
  }

  conns = calloc(num_total, sizeof(struct connection));
  if(!conns) {
    printf("Out of memory\n");
    return 3;
  }

  if(num_total >= NCONNECTIONS) {
    printf("Too many connections requested, increase NCONNECTIONS!\n");
    return 2;
  }

  printf("About to do %d connections\n", num_total);

  /* init the multi stack */
  multi_handle = curl_multi_init();

  for(i=0; i< num_total; i++) {
    CURL *e;
    char *nl;

    memset(&conns[i], 0, sizeof(struct connection));

    if(i < num_idle)
      conns[i].url = URL_IDLE;
    else
      conns[i].url = URL_ACTIVE;

    e  = curl_easy_init();

    if(!e) {
      printf("curl_easy_init() for handle %d failed, exiting!\n", i);
      return 2;
    }

    conns[i].e = e;
    conns[i].id = i;
    conns[i].global = &info;

    curl_easy_setopt(e, CURLOPT_URL, conns[i].url);
    curl_easy_setopt(e, CURLOPT_WRITEFUNCTION, writecallback);
    curl_easy_setopt(e, CURLOPT_WRITEDATA, &conns[i]);
#if 0
    curl_easy_setopt(e, CURLOPT_VERBOSE, 1);
#endif
    curl_easy_setopt(e, CURLOPT_ERRORBUFFER, conns[i].error);
    curl_easy_setopt(e, CURLOPT_PRIVATE, &conns[i]);

    /* add the easy to the multi */
    if(CURLM_OK != curl_multi_add_handle(multi_handle, e)) {
      printf("curl_multi_add_handle() returned error for %d\n", i);
      return 3;
    }
  }

  /* we start the action by calling *socket() right away */
  while(CURLM_CALL_MULTI_PERFORM ==
        curl_multi_socket_all(multi_handle, socket_callback, NULL));

  printf("Starting timer, expects to run for %ldus\n", RUN_FOR_THIS_LONG);
  timer_start();

  while(1) {
    struct timeval timeout;
    int rc; /* select() return code */

    fd2_set fdread;
    fd2_set fdwrite;
    int maxfd;

    FD2_ZERO(&fdread);
    FD2_ZERO(&fdwrite);

    /* set a suitable timeout to play around with */
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;

    /* convert file descriptors from the transfers to fd_sets */
    fdinfo2fdset(&fdread, &fdwrite, &maxfd);

    timer_pause();
    selects++;
    selectsalive += still_running;
    rc = select(maxfd+1,
                (fd_set *)&fdread,
                (fd_set *)&fdwrite,
                NULL, &timeout);

    timer_continue();

    switch(rc) {
    case -1:
      /* select error */
      break;
    case 0:
      timeouts++;
    default:
      /* timeout or readable/writable sockets */
#if 0
      curl_multi_socket(multi_handle, CURL_SOCKET_BAD, conns[0].e,
                        socket_callback, NULL);
#endif
      curl_multi_socket_all(multi_handle, socket_callback, NULL);

      performselect += rc;
      if(rc > topselect)
        topselect = rc;
      break;
    }

    if(total > RUN_FOR_THIS_LONG) {
      printf("Stopped after %ldus\n", total);
      break;
    }

    if(prevalive != still_running) {
      printf("%d connections alive\n", still_running);
    }
    prevalive = still_running;

    timer_total(); /* calculate the total time spent so far */
  }

  if(still_running != num_total) {
    /* something made connections fail, extract the reason and tell */
    int msgs_left;
    struct connection *cptr;
    while ((msg = curl_multi_info_read(multi_handle, &msgs_left))) {
      if (msg->msg == CURLMSG_DONE) {
        curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &cptr);

        printf("%d => (%d) %s", cptr->id, msg->data.result, cptr->error);
      }
    }

  }

  curl_multi_cleanup(multi_handle);

  /* cleanup all the easy handles */
  for(i=0; i< num_total; i++)
    curl_easy_cleanup(conns[i].e);

  report();

  return code;
}
