/*****************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * $Id$
 *
 * Connect N connections. Z are idle, and X are active. Transfer as fast as
 * possible.
 *
 * Output detailed timing information.
 *
 * 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 */

#ifndef FALSE
#define FALSE 0
#endif

#ifndef TRUE
#define TRUE 1
#endif

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

/* The maximum time (in microseconds) we run the test */
#define RUN_FOR_THIS_LONG (5*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 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];
};

/* this is the struct associated with each file descriptor libcurl tells us
   it is dealing with */
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; /* */
  int evset; /* true if the 'ev' struct has been used in a event_set() call */
  CURLM *multi; /* pointer to the multi handle */
  int *running_handles; /* pointer to the running_handles counter */
};

static struct fdinfo *allsocks;

static int running_handles;

/* we have the timerevent global so that when the final socket-based event is
   done, we can remove the timerevent as well */
static struct event timerevent;

static void update_timeout(CURLM *multi_handle);

/* called from libevent on action on a particular socket ("event") */
static void eventcallback(int fd, short type, void *userp)
{
  struct fdinfo *fdp = (struct fdinfo *)userp;
  CURLMcode rc;

  fprintf(stderr, "EVENT callback type %d\n", type);

  /* tell libcurl to deal with the transfer associated with this socket */
  do {
    rc = curl_multi_socket(fdp->multi, fd, fdp->running_handles);
  } while (rc == CURLM_CALL_MULTI_PERFORM);

  if(rc) {
    fprintf(stderr, "curl_multi_socket() returned %d\n", (int)rc);
  }

  fprintf(stderr, "running_handles: %d\n", *fdp->running_handles);
  if(!*fdp->running_handles) {
    /* last transfer is complete, kill pending timeout */
    fprintf(stderr, "last transfer done, kill timeout\n");
    if(evtimer_pending(&timerevent, NULL))
      evtimer_del(&timerevent);
  }
  else
    update_timeout(fdp->multi);
}

/* called from libevent when our timer event expires */
static void timercallback(int fd, short type, void *userp)
{
  (void)fd; /* not used for this */
  (void)type; /* ignored in here */
  CURLM *multi_handle = (CURLM *)userp;
  int running_handles;
  CURLMcode rc;

  fprintf(stderr, "EVENT timeout\n");

  /* tell libcurl to deal with the transfer associated with this socket */
  do {
    rc = curl_multi_socket(multi_handle, CURL_SOCKET_TIMEOUT,
                           &running_handles);
  } while (rc == CURLM_CALL_MULTI_PERFORM);

  if(running_handles)
    /* Get the current timeout value from libcurl and set a new timeout */
    update_timeout(multi_handle);
}

static void remsock(struct fdinfo *f)
{
  if(!f)
    /* did not find socket to remove! */
    return;

  if(f->evset)
    event_del(&f->ev);

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

static void setsock(struct fdinfo *fdp, curl_socket_t s, CURL *easy,
                    int action)
{
  fdp->sockfd = s;
  fdp->action = action;
  fdp->easy = easy;

  if(fdp->evset)
    /* first remove the existing event if the old setup was used */
    event_del(&fdp->ev);

  /* now use and add the current socket setup to libevent. The EV_PERSIST is
     the key here as otherwise libevent will automatically remove the event
     when it occurs the first time */
  event_set(&fdp->ev, fdp->sockfd,
            (action&CURL_POLL_IN?EV_READ:0)|
            (action&CURL_POLL_OUT?EV_WRITE:0)| EV_PERSIST,
            eventcallback, fdp);

  fdp->evset=1;

  fprintf(stderr, "event_add() for fd %d\n", s);

  /* We don't use any socket-specific timeout but intead we use a single
     global one. This is (mostly) because libcurl doesn't expose any
     particular socket- based timeout value. */
  event_add(&fdp->ev, NULL);
}

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

  fdp->multi = multi;
  fdp->running_handles = &running_handles;
  setsock(fdp, s, easy, action);

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

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

  /* Set this association in libcurl */
  curl_multi_assign(multi, s, fdp);
}

/* on port 8999 we run a fork enabled sws that supports 'idle' and 'stream' */
#define PORT "8999"

#define HOST "127.0.0.1"

#define URL_IDLE   "http://" HOST ":" PORT "/1000"
#if 1
#define URL_ACTIVE "http://" HOST ":" PORT "/1001"
#else
#define URL_ACTIVE "http://localhost/"
#endif

static int socket_callback(CURL *easy,      /* easy handle */
                           curl_socket_t s, /* socket */
                           int what,        /* see above */
                           void *cbp,       /* callback pointer */
                           void *socketp)   /* socket pointer */
{
  struct fdinfo *fdp = (struct fdinfo *)socketp;
  char *whatstr[]={
    "none",
    "IN",
    "OUT",
    "INOUT",
    "REMOVE"};

  fprintf(stderr, "socket %d easy %p what %s\n", s, easy,
          whatstr[what]);

  if(what == CURL_POLL_REMOVE)
    remsock(fdp);
  else {
    if(!fdp) {
      /* not previously known, add it and set association */
      printf("Add info for socket %d %s%s\n", s,
             what&CURL_POLL_IN?"READ":"",
             what&CURL_POLL_OUT?"WRITE":"" );
      addsock(s, easy, what, cbp);
    }
    else {
      /* we already know about it, just change action/timeout */
      printf("Changing info for socket %d from %d to %d\n",
             s, fdp->action, what);
      setsock(fdp, s, easy, what);
    }
  }
  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;
  (void)ptr;

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

  printf("%02d: %d, total %d\n",
         c->id, c->dlcounter, c->global->dlcounter);

  return realsize;
}

struct globalinfo info;
struct connection *conns;

int num_total;
int num_idle;
int num_active;

static void update_timeout(CURLM *multi_handle)
{
  long timeout_ms;
  struct timeval timeout;

  /* Since we need a global timeout to occur after a given time of inactivity,
     we use a single timeout-event. Get the timeout value from libcurl, and
     update it after every call to libcurl. */
  curl_multi_timeout(multi_handle, &timeout_ms);

  /* convert ms to timeval */
  timeout.tv_sec = timeout_ms/1000;
  timeout.tv_usec = (timeout_ms%1000)*1000;
  evtimer_add(&timerevent, &timeout);
}

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

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

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

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

  num_total = num_idle + num_active;

  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;
  }

  event_init(); /* Initalize the event library */

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

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

  /* initialize the timeout event */
  evtimer_set(&timerevent, timercallback, multi_handle);

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

    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]);
    curl_easy_setopt(e, CURLOPT_VERBOSE, 0);
    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;
    }
  }

  curl_multi_setopt(multi_handle, CURLMOPT_SOCKETFUNCTION, socket_callback);
  curl_multi_setopt(multi_handle, CURLMOPT_SOCKETDATA, multi_handle);

  /* we start the action by calling *socket_all() */
  while(CURLM_CALL_MULTI_PERFORM == curl_multi_socket_all(multi_handle,
                                                          &running_handles));

  /* update timeout */
  update_timeout(multi_handle);

  /* event_dispatch() runs the event main loop. It ends when no events are
     left to wait for. */

  event_dispatch();

  {
    /* 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\n",
               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);

  return code;
}
