#include "eztrace-core/eztrace_sampling.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.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 _Thread_local struct _ezt_sampling_thread_instance* _thread_sampling = NULL;


/* Initialize thread-specific data */
static struct _ezt_sampling_thread_instance* _ezt_sampling_init_thread() {

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

  /* Allocate the thread-specific data */
  _thread_sampling = malloc(sizeof(struct _ezt_sampling_thread_instance));
  _thread_sampling->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 < _thread_sampling->nb_callbacks; i++) {
    if (callback_instances[i].interval < min_interval)
      min_interval = callback_instances[i].interval;
    _thread_sampling->callback[i].callback = callback_instances[i].callback;
    _thread_sampling->callback[i].interval = callback_instances[i].interval;
    UPDATE_TIMEVAL(_thread_sampling->callback[i].last_call, cur_time, 0);
    _thread_sampling->callback[i].plugin_data = NULL;
  }

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

  return _thread_sampling;
}

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

  if (nb_sampling_callbacks) {

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

    if(!_thread_sampling)  {
      /* First time this thread check for callbacks */

      _ezt_sampling_init_thread();
      if (!_thread_sampling) {
        return;
      }
    }

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

      int i;
      /* browse the list of registered callbacks */
      for (i = 0; i < _thread_sampling->nb_callbacks; i++) {
        struct ezt_sampling_callback_instance* callback = &_thread_sampling->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 _thread_sampling->next_call */
            if (TIME_DIFF(cur_time, _thread_sampling->next_call) > (long long)callback->interval) {
              /* this callback is the next one */
              UPDATE_TIMEVAL(_thread_sampling->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) > (long long) interval) {
    /* this callback is the next one */
    UPDATE_TIMEVAL(next_call, cur_time, interval);
  }
}
