/* -*- c-file-style: "GNU" -*- */
/*
 * Copyright (C) CNRS, INRIA, Universite Bordeaux 1, Telecom SudParis
 * See COPYING in top-level directory.
 */

#ifndef _REENTRANT
#define _REENTRANT
#endif

#include <eztrace-core/eztrace_config.h>
#include <eztrace-core/eztrace_sampling.h>
#include <eztrace-lib/eztrace.h>
#include <papi.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#error PAPI plugin not yet ported to eztrace 2.0 API
/* set to 1 when all the hooks are set.
 * This is usefull in order to avoid recursive calls
 */
static int _papi_initialized = 0;

//#define VERBOSE 1
/* We don't use the FUNCTION_NAME macro (defined in src/core/eztrace.h)
 * to avoid infinite loops (since it calls record_counters).
 */
#ifdef FUNCTION_NAME
#undef FUNCTION_NAME
#endif

#ifdef VERBOSE
#define FUNCTION_NAME fprintf(stderr, "Calling [%s]\n", __func__)
#else
#define FUNCTION_NAME (void)0
#endif

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

static pthread_key_t papi_thread_info_key;

struct _papi_thread_info {
  int Events;
  long_long* values;
  struct timeval last_tick;
};

static int nb_events;
static int Events;
static int* Event_codes;
static char** Event_names;
static char** units_names;

struct _papi_thread_info* papi_init_hw_counter();

#define handle_error(n) \
  fprintf(stderr, "%s: PAPI error %d: %s\n", __func__, n, PAPI_strerror(n))

int papi_record(struct ezt_sampling_callback_instance* instance) {
  if (!_papi_initialized || !nb_events)
    return 0;

  struct _papi_thread_info* ptr = NULL;
  struct timeval cur_tick;
  gettimeofday(&cur_tick, NULL);

  /* retrieve the thread-specific structure */
  if (!instance->plugin_data) {
    ptr = papi_init_hw_counter(instance);
    if (!ptr) {
      return -1;
    }
  }
  ptr = instance->plugin_data;

  /* read hardware counter */
  // this region needs to be protected in order to disable infinite recursion due to the intercepting of functions.
  // Because, PAPI functions can possibly be intercepted too.
  EZTRACE_PROTECT {
    EZTRACE_PROTECT_ON();
    int ret;
    /* get the values of counters */
    if ((ret = PAPI_stop(ptr->Events, ptr->values)) != PAPI_OK) {
      handle_error(ret);
      eztrace_abort();
    }

    /* restart counters */
    ret = PAPI_start(ptr->Events);
    if (ret != PAPI_OK) {
      handle_error(ret);
      eztrace_abort();
    }

    int duration = TIME_DIFF(instance->last_call, cur_tick); /* duration in usec */
    int i;

    for (i = 0; i < nb_events; i++) {
      // As this region is protected, the unprotected function is called
      EZTRACE_EVENT3_PACKED_UNPROTECTED(EZTRACE_PAPI_MEASUREMENT, i, ptr->values[i], duration);
    }

    /* reset the timer */
    instance->last_call.tv_usec = cur_tick.tv_usec;
    instance->last_call.tv_sec = cur_tick.tv_sec;

    EZTRACE_PROTECT_OFF();
    return 0;
  }

  return 1;
}

/* Initialize the PAPI counter interface for a thread */
struct _papi_thread_info* papi_init_hw_counter(struct ezt_sampling_callback_instance* instance) {

  if (!nb_events)
    /* no counter were selected. We don't need to do anything */
    return NULL;

  struct _papi_thread_info* ptr = NULL;
  // this region needs to be protected in order to disable infinite recursion due to the intercepting of functions.
  // Because, PAPI functions can possibly be intercepted too.
  EZTRACE_PROTECT {
    EZTRACE_PROTECT_ON();

    int ret;
    /* initialize the thread-specific structure */
    if (instance->plugin_data == NULL) {

      /* This thread has not been initialized yet */
      PAPI_register_thread();

      ptr = malloc(sizeof(struct _papi_thread_info));
      ptr->values = malloc(sizeof(long_long) * nb_events);
      int i;

      /* Create a new EventSet */
      ptr->Events = PAPI_NULL;
      ret = PAPI_create_eventset(&ptr->Events);
      if (ret != PAPI_OK) {
        handle_error(ret);
        eztrace_abort();
      }

      /* copy Events to the EventSet */
      for (i = 0; i < nb_events; i++) {
        ret = PAPI_add_event(ptr->Events, Event_codes[i]);
        if (ret != PAPI_OK) {
          handle_error(ret);
          eztrace_abort();
        }
      }

      /* Start the counter */
      if ((ret = PAPI_start(ptr->Events)) != PAPI_OK) {
        if (ret == PAPI_ECNFLCT) {
          fprintf(stderr, "PAPI error %d (%s): It is likely that you selected too many counters to monitor\n",
                  ret, PAPI_strerror(ret));
          exit(-1);
        }
      }

      /* set the structure Thread-specific */
      instance->plugin_data = ptr;

      /* first measurement */
      gettimeofday(&instance->last_call, NULL);
      int retval = PAPI_stop(ptr->Events, ptr->values);
      if (retval != PAPI_OK) {
        fprintf(stderr, "PAPI_stop() failed\n");
      }

      retval = PAPI_start(ptr->Events);
      if (retval != PAPI_OK) {
        fprintf(stderr, "PAPI_start() failed\n");
        exit(1);
      }
    }
    EZTRACE_PROTECT_OFF();
  }

  return ptr;
}

static int _papi_add_counter(char* counter_name) {

  nb_events++;
  Event_codes = realloc(Event_codes, sizeof(int) * nb_events);
  Event_names = realloc(Event_names, sizeof(char*) * nb_events);
  units_names = realloc(units_names, sizeof(char*) * nb_events);

  int code;
  int retval;
  if ((retval = PAPI_event_name_to_code(counter_name, &code)) == PAPI_OK) {
    retval = PAPI_add_event(Events, code);
    if (retval != PAPI_OK) {
      handle_error(retval);
      nb_events--;
      return retval;
    } else {
#if PAPI_VERSION > PAPI_VERSION_NUMBER(5, 0, 0, 0)
      PAPI_event_info_t info;
      retval = PAPI_get_event_info(code, &info);
      if (retval != PAPI_OK) {
        /* PAPI_get_event_info failed. Let's assume there is no unit */
        handle_error(retval);
        asprintf(&units_names[nb_events - 1], " ");
      } else {
        asprintf(&units_names[nb_events - 1], "%s", info.units);
      }

#else
      asprintf(&units_names[nb_events - 1], " ");
#endif
      Event_codes[nb_events - 1] = code;
      asprintf(&Event_names[nb_events - 1], "%s", counter_name);
      return PAPI_OK;
    }
  }

  handle_error(retval);
  nb_events--;
  return retval;
}

/* select the counters to monitor */
static void _papi_select_counters() {
  int invalid_counters = 0;
  char* env1 = getenv("EZTRACE_PAPI_COUNTERS");

  nb_events = 0;
  Events = PAPI_NULL;

  if (PAPI_create_eventset(&Events) != PAPI_OK) {
    eztrace_abort();
  }
  if (env1) {
    /* EZTRACE_PAPI_COUNTERS is defined */

    /* strtok may modify env, so let's make a copy of it */
    char* env;
    asprintf(&env, "%s", env1);
    char* token = strtok(env, " ");

    /* for each token, get the code id and fill the Events array */
    while (token) {
      if (_papi_add_counter(token) != PAPI_OK) {
        invalid_counters = 1;
      }
      token = strtok(NULL, " ");
    }

  } else {
    /* EZTRACE_PAPI_COUNTERS is not defined, use the default values */
    _papi_add_counter("PAPI_TOT_INS");
  }

  if (invalid_counters) {
    fprintf(stderr, "Available counters:\n");
    _papi_print_available_counters();
  }
}

/* In some cases (eg. using -O3 with gcc), the compiler may optimize too agressively and remove the function array symbol.
 * This results in pptrace being unable to load the module (since the symbol is not found).
 * This can be "solved" by declaring 2 callbacks to avoid the compiler optimization
 */
void (*libfoo)(void*);
void (*libfoo2)(void*);
START_INTERCEPT_MODULE(papi)
INTERCEPT2("foo", libfoo)
INTERCEPT2("foo2", libfoo2)
END_INTERCEPT_MODULE(papi)

static void _papi_init(void) __attribute__((constructor));
static void _papi_init(void) {
  DYNAMIC_INTERCEPT_ALL_MODULE(papi);

  int ret;
  pthread_key_create(&papi_thread_info_key, NULL);

  /* initialize PAPI */
  ret = PAPI_library_init(PAPI_VER_CURRENT);
  if (ret != PAPI_VER_CURRENT && ret > 0) {
    fprintf(stderr, "PAPI library version mismatch!\n");
    exit(1);
  }

  /* There may be several threads, so let's tell this to PAPI */
  if ((ret = PAPI_thread_init(pthread_self)) != PAPI_OK)
    handle_error(ret);

  _papi_init_counter_ids();
  _papi_select_counters();

  char* env = getenv("EZTRACE_PAPI_SAMPLE_INTERVAL");
  int sample_interval = 100; /* by default, check counter every 100 µs */
  if (env) {
    sample_interval = atoi(env);
    if (sample_interval <= 0) {
      fprintf(stderr, "[EZTrace] please enter a value > 0 when setting EZTRACE_PAPI_SAMPLE_INTERVAL\n");
      sample_interval = 100;
    }
  }

  if (eztrace_autostart_enabled())
    eztrace_start();

  _papi_initialized = 1;

  /* Tell how many counters (and which counters) are monitored */
  EZTRACE_EVENT_PACKED_1(EZTRACE_PAPI_INIT, nb_events);
  int i;
  for (i = 0; i < nb_events; i++) {
    EZTRACE_EVENT_PACKED_2(EZTRACE_PAPI_INIT_COUNTERS, i, Event_codes[i]);

    litl_write_probe_raw(_ezt_trace.litl_trace,
                         EZTRACE_PAPI_INIT_COUNTERS,
                         strlen(Event_names[i]),
                         (litl_data_t*)Event_names[i]);
    litl_write_probe_raw(_ezt_trace.litl_trace,
                         EZTRACE_PAPI_INIT_COUNTERS,
                         strlen(units_names[i]),
                         (litl_data_t*)units_names[i]);
    /* todo: add event description */
  }

  ezt_sampling_register_callback(papi_record, sample_interval);
}

static void _papi_conclude(void) __attribute__((destructor));
static void _papi_conclude(void) {
  _papi_initialized = 0;
  eztrace_stop();
}
