#include <stdlib.h>
#include <stdio.h>
#include <sys/time.h>
#include <pthread.h>

#include "eztrace_sampling.h"

/* perform dest = src+interval on timeval structures */
#define UPDATE_TIMEVAL(dest, src, interval) do {\
    (dest).tv_sec = (src).tv_sec + ((interval)/((unsigned)1e6)); \
    (dest).tv_usec = (src).tv_usec + ((interval)%((unsigned)1e6)); \
  } while(0)

#define TIME_DIFF(t1, t2) \
        ((t2.tv_sec - t1.tv_sec) * 1000000 + (t2.tv_usec - t1.tv_usec))


#define MAX_SAMPLING_CALLBACKS 100

static struct ezt_sampling_callback_instance callback_instances[MAX_SAMPLING_CALLBACKS];
static int nb_sampling_callbacks = 0;
static struct timeval next_call={.tv_sec=0, .tv_usec=0};

struct __ezt_sampling_thread_instance {
  struct ezt_sampling_callback_instance callback[MAX_SAMPLING_CALLBACKS];
  int nb_callbacks;
  struct timeval next_call;
};

static pthread_key_t __ezt_sampling_thread_key;
static pthread_once_t once_control;

/* Initialize thread-specific data */
struct __ezt_sampling_thread_instance * __ezt_sampling_init_thread() {

  if(!nb_sampling_callbacks)
    /* nothing to process */
    return NULL;

  static int first = 1;
  if(first)
    /* This is the first thread that is initialized. We need to create the pthread_key */
    pthread_key_create(&__ezt_sampling_thread_key, NULL );
  first = 0;


  struct __ezt_sampling_thread_instance * ptr = NULL;
  /* Allocate the thread-specific data */
  ptr = malloc(sizeof(struct __ezt_sampling_thread_instance));
  ptr->nb_callbacks = nb_sampling_callbacks;

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

  /* copy the callbacks */
  unsigned min_interval = callback_instances[0].interval;
  int i;
  for(i=0; i< ptr->nb_callbacks; i++) {
    if(callback_instances[i].interval < min_interval)
      min_interval = callback_instances[i].interval;
    ptr->callback[i].callback = callback_instances[i].callback;
    ptr->callback[i].interval = callback_instances[i].interval;
    UPDATE_TIMEVAL(ptr->callback[i].last_call, cur_time, 0);
    ptr->callback[i].plugin_data = NULL;
  }

  UPDATE_TIMEVAL(ptr->next_call, cur_time, min_interval);

  /* set the structure Thread-specific */
  (void) pthread_setspecific(__ezt_sampling_thread_key, ptr);

  return ptr;
}

/* Calls registered callbacks if needed */
void ezt_sampling_check_callbacks() {

  if(nb_sampling_callbacks) {

    struct __ezt_sampling_thread_instance *ptr = NULL;

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

    pthread_once(&once_control, (void (*)(void))__ezt_sampling_init_thread);

    if ((ptr = pthread_getspecific(__ezt_sampling_thread_key)) == NULL ) {
      /* First time this thread check for callbacks */

        ptr = __ezt_sampling_init_thread();
        if (!ptr) {
            return;
	}
    }

    if(TIME_DIFF(cur_time, ptr->next_call) < 0) {
      /* we need to call add least one callback */

      int i;
      /* browse the list of registered callbacks */
      for(i=0; i<ptr->nb_callbacks; i++) {
	struct ezt_sampling_callback_instance *callback = &ptr->callback[i];

	if(TIME_DIFF(callback->last_call, cur_time) >= (long long)callback->interval) {
	  /* execute the callback */
	  int ret = callback->callback(callback);

	  if(ret == 0) {
	    /* update the timer */
	    UPDATE_TIMEVAL(callback->last_call, cur_time, 0);

	    /* update ptr->next_call */
	    if(TIME_DIFF(cur_time, ptr->next_call) > callback->interval) {
	      /* this callback is the next one */
	      UPDATE_TIMEVAL(ptr->next_call, cur_time, callback->interval);
	    }
	  }
	}
      }
    }
  }
}

/* todo: distinguish thread-specific callbacks from process-specific callbacks
 * todo: add the possibility to add an alarm
 */
/* interval: time in microsecond between calls to callback */
void ezt_sampling_register_callback(ezt_sampling_callback_t callback, unsigned interval) {

  nb_sampling_callbacks++;
  callback_instances[nb_sampling_callbacks-1].callback = callback;
  callback_instances[nb_sampling_callbacks-1].interval  = interval;
  callback_instances[nb_sampling_callbacks-1].last_call.tv_sec = 0;
  callback_instances[nb_sampling_callbacks-1].last_call.tv_usec = 0;

  struct timeval cur_time;
  gettimeofday(&cur_time, NULL);
  if(TIME_DIFF(cur_time, next_call) > interval) {
    /* this callback is the next one */
    UPDATE_TIMEVAL(next_call, cur_time, interval);
  }
}
