#include "config.h"

#include "gskprofilerprivate.h"

#define MAX_SAMPLES     32

typedef struct {
  GQuark id;
  char *description;
  gint64 value;
  gint64 n_samples;
  gboolean can_reset : 1;
} NamedCounter;

typedef struct {
  GQuark id;
  char *description;
  gint64 value;
  gint64 start_time;
  gint64 min_value;
  gint64 max_value;
  gint64 avg_value;
  gint64 n_samples;
  gboolean in_flight : 1;
  gboolean can_reset : 1;
  gboolean invert : 1;
} NamedTimer;

typedef struct {
  GQuark id;
  gint64 value;
} Sample;

struct _GskProfiler
{
  GObject parent_instance;

  GHashTable *counters;
  GHashTable *timers;

  Sample timer_samples[MAX_SAMPLES];
  guint last_sample;
};

G_DEFINE_TYPE (GskProfiler, gsk_profiler, G_TYPE_OBJECT)

static void
named_counter_free (gpointer data)
{
  NamedCounter *counter = data;

  if (data == NULL)
    return;

  g_free (counter->description);

  g_slice_free (NamedCounter, counter);
}

static void
named_timer_free (gpointer data)
{
  NamedTimer *timer = data;

  if (data == NULL)
    return;

  g_free (timer->description);

  g_slice_free (NamedTimer, timer);
}

static void
gsk_profiler_finalize (GObject *gobject)
{
  GskProfiler *self = GSK_PROFILER (gobject);

  g_clear_pointer (&self->counters, g_hash_table_unref);
  g_clear_pointer (&self->timers, g_hash_table_unref);

  G_OBJECT_CLASS (gsk_profiler_parent_class)->finalize (gobject);
}

static void
gsk_profiler_class_init (GskProfilerClass *klass)
{
  G_OBJECT_CLASS (klass)->finalize = gsk_profiler_finalize;
}

static void
gsk_profiler_init (GskProfiler *self)
{
  self->counters = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                                          NULL,
                                          named_counter_free);
  self->timers = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                                        NULL,
                                        named_timer_free);
}

GskProfiler *
gsk_profiler_new (void)
{
  return g_object_new (GSK_TYPE_PROFILER, NULL);
}

static NamedCounter *
named_counter_new (GQuark      id,
                   const char *description,
                   gboolean    can_reset)
{
  NamedCounter *res = g_slice_new0 (NamedCounter);

  res->id = id;
  res->description = g_strdup (description);
  res->can_reset = can_reset;

  return res;
}

static NamedCounter *
gsk_profiler_get_counter (GskProfiler *profiler,
                          GQuark       id)
{
  return g_hash_table_lookup (profiler->counters, GINT_TO_POINTER (id));
}

GQuark
gsk_profiler_add_counter (GskProfiler *profiler,
                          const char  *counter_name,
                          const char  *description,
                          gboolean     can_reset)
{
  NamedCounter *counter;
  GQuark id;

  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);

  id = g_quark_from_string (counter_name);
  counter = gsk_profiler_get_counter (profiler, id);
  if (counter != NULL)
    {
      g_critical ("Cannot add a counter '%s' as one already exists.", counter_name);
      return counter->id;
    }

  counter = named_counter_new (id, description, can_reset);
  g_hash_table_insert (profiler->counters, GINT_TO_POINTER (id), counter);

  return counter->id;
}

static NamedTimer *
named_timer_new (GQuark      id,
                 const char *description,
                 gboolean    invert,
                 gboolean    can_reset)
{
  NamedTimer *res = g_slice_new0 (NamedTimer);

  res->id = id;
  res->description = g_strdup (description);
  res->invert = invert;
  res->can_reset = can_reset;

  return res;
}

static NamedTimer *
gsk_profiler_get_timer (GskProfiler *profiler,
                        GQuark       id)
{
  return g_hash_table_lookup (profiler->timers, GINT_TO_POINTER (id));
}

GQuark
gsk_profiler_add_timer (GskProfiler *profiler,
                        const char  *timer_name,
                        const char  *description,
                        gboolean     invert,
                        gboolean     can_reset)
{
  NamedTimer *timer;
  GQuark id;

  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);

  id = g_quark_from_string (timer_name);
  timer = gsk_profiler_get_timer (profiler, id);
  if (timer != NULL)
    {
      g_critical ("Cannot add a timer '%s' as one already exists.", timer_name);
      return timer->id;
    }

  timer = named_timer_new (id, description, invert, can_reset);
  g_hash_table_insert (profiler->timers, GINT_TO_POINTER (id), timer);

  return timer->id;
}

void
gsk_profiler_counter_inc (GskProfiler *profiler,
                          GQuark       counter_id)
{
  gsk_profiler_counter_add (profiler, counter_id, 1);
}

void
gsk_profiler_counter_set (GskProfiler *profiler,
                          GQuark       counter_id,
                          gint64       value)
{
  NamedCounter *counter;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  counter = gsk_profiler_get_counter (profiler, counter_id);
  if (counter == NULL)
    {
      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
                  g_quark_to_string (counter_id), counter_id);
      return;
    }

  counter->value = value;
}

void
gsk_profiler_counter_add (GskProfiler *profiler,
                          GQuark       counter_id,
                          gint64       increment)
{
  NamedCounter *counter;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  counter = gsk_profiler_get_counter (profiler, counter_id);
  if (counter == NULL)
    {
      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
                  g_quark_to_string (counter_id), counter_id);
      return;
    }

  counter->value += increment;
}

void
gsk_profiler_timer_begin (GskProfiler *profiler,
                          GQuark       timer_id)
{
  NamedTimer *timer;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  timer = gsk_profiler_get_timer (profiler, timer_id);
  if (timer == NULL)
    return;

  if (timer->in_flight)
    return;

  timer->in_flight = TRUE;
  timer->start_time = g_get_monotonic_time () * 1000;
}

gint64
gsk_profiler_timer_end (GskProfiler *profiler,
                        GQuark       timer_id)
{
  NamedTimer *timer;
  gint64 diff;

  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);

  timer = gsk_profiler_get_timer (profiler, timer_id);
  if (timer == NULL)
    {
      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
                  g_quark_to_string (timer_id), timer_id);
      return 0;
    }

  if (!timer->in_flight)
    {
      g_critical ("Timer '%s' (id:%d) is not running; did you forget to call gsk_profiler_timer_begin()?",
                  g_quark_to_string (timer->id), timer->id);
      return 0;
    }

  diff = (g_get_monotonic_time () * 1000) - timer->start_time;

  timer->in_flight = FALSE;
  timer->value += diff; 

  return diff;
}

void
gsk_profiler_timer_set (GskProfiler *profiler,
                        GQuark       timer_id,
                        gint64       value)
{
  NamedTimer *timer;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  timer = gsk_profiler_get_timer (profiler, timer_id);
  if (timer == NULL)
    {
      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
                  g_quark_to_string (timer_id), timer_id);
      return;
    }

  if (timer->in_flight)
    {
      g_critical ("Timer '%s' (id:%d) is running; are you sure you don't want to call "
                  "gsk_profiler_timer_end() instead of gsk_profiler_timer_set()?",
                  g_quark_to_string (timer_id), timer_id);
    }

  timer->value = value;
}

gint64
gsk_profiler_counter_get (GskProfiler *profiler,
                          GQuark       counter_id)
{
  NamedCounter *counter;

  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);

  counter = gsk_profiler_get_counter (profiler, counter_id);
  if (counter == NULL)
    {
      g_critical ("No counter '%s' (id:%d) found; did you forget to call gsk_profiler_add_counter()?",
                  g_quark_to_string (counter_id), counter_id);
      return 0;
    }

  return counter->value;
}

gint64
gsk_profiler_timer_get (GskProfiler *profiler,
                        GQuark       timer_id)
{
  NamedTimer *timer;

  g_return_val_if_fail (GSK_IS_PROFILER (profiler), 0);

  timer = gsk_profiler_get_timer (profiler, timer_id);
  if (timer == NULL)
    {
      g_critical ("No timer '%s' (id:%d) found; did you forget to call gsk_profiler_add_timer()?",
                  g_quark_to_string (timer_id), timer_id);
      return 0;
    }

  if (timer->invert)
    return (gint64) (1000000000.0 / (double) timer->value);

  return timer->value;
}

gint64
gsk_profiler_timer_get_start (GskProfiler *profiler,
                              GQuark       timer_id)
{
  NamedTimer *timer;

  timer = gsk_profiler_get_timer (profiler, timer_id);
  if (timer == NULL)
    return 0;

  return timer->start_time;
}

void
gsk_profiler_reset (GskProfiler *profiler)
{
  GHashTableIter iter;
  gpointer value_p = NULL;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  g_hash_table_iter_init (&iter, profiler->counters);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedCounter *counter = value_p;

      if (counter->can_reset)
        counter->value = 0;
    }

  g_hash_table_iter_init (&iter, profiler->timers);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedTimer *timer = value_p;

      if (timer->can_reset)
        {
          timer->value = 0;
          timer->min_value = 0;
          timer->max_value = 0;
          timer->avg_value = 0;
          timer->n_samples = 0;
        }
    }

  profiler->last_sample = 0;
}

void
gsk_profiler_push_samples (GskProfiler *profiler)
{
  GHashTableIter iter;
  gpointer value_p = NULL;
  guint last_sample;

  g_return_if_fail (GSK_IS_PROFILER (profiler));

  g_hash_table_iter_init (&iter, profiler->timers);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedTimer *timer = value_p;
      Sample *s;

      last_sample = profiler->last_sample;
      profiler->last_sample += 1;
      if (profiler->last_sample == MAX_SAMPLES)
        profiler->last_sample = 0;

      s = &(profiler->timer_samples[last_sample]);
      s->id = timer->id;

      if (timer->invert)
        s->value = (gint64) (1000000000.0 / (double) timer->value);
      else
        s->value = timer->value;
    }
}

void
gsk_profiler_append_counters (GskProfiler *profiler,
                              GString     *buffer)
{
  GHashTableIter iter;
  gpointer value_p = NULL;

  g_return_if_fail (GSK_IS_PROFILER (profiler));
  g_return_if_fail (buffer != NULL);

  g_hash_table_iter_init (&iter, profiler->counters);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedCounter *counter = value_p;

      g_string_append_printf (buffer, "%s: %" G_GINT64_FORMAT "\n",
                              counter->description,
                              counter->value);
    }
}

void
gsk_profiler_append_timers (GskProfiler *profiler,
                            GString     *buffer)
{
  GHashTableIter iter;
  gpointer value_p = NULL;
  int i;

  g_return_if_fail (GSK_IS_PROFILER (profiler));
  g_return_if_fail (buffer != NULL);

  g_hash_table_iter_init (&iter, profiler->timers);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedTimer *timer = value_p;

      timer->min_value = G_MAXINT64;
      timer->max_value = G_MININT64;
      timer->avg_value = 0;
      timer->n_samples = 0;
    }

  for (i = 0; i < profiler->last_sample; i++)
    {
      Sample *s = &(profiler->timer_samples[i]);
      NamedTimer *timer;

      if (s->id == 0)
        continue;

      timer = gsk_profiler_get_timer (profiler, s->id);
      timer->min_value = MIN (timer->min_value, s->value);
      timer->max_value = MAX (timer->max_value, s->value);
      timer->avg_value += s->value;
      timer->n_samples += 1;
    }

  g_hash_table_iter_init (&iter, profiler->timers);
  while (g_hash_table_iter_next (&iter, NULL, &value_p))
    {
      NamedTimer *timer = value_p;
      const char *unit = timer->invert ? "" : "usec";
      double scale = timer->invert ? 1.0 : 1000.0;

      g_string_append_printf (buffer, "%s (%s): %.2f",
                              timer->description,
                              unit,
                              (double) timer->value / scale);

      if (timer->n_samples > 1)
        {
          timer->avg_value = timer->avg_value / timer->n_samples;
          g_string_append_printf (buffer, " Min: %.2f Avg: %.2f Max: %.2f (%" G_GINT64_FORMAT " samples)",
                                  (double) timer->min_value / scale,
                                  (double) timer->avg_value / scale,
                                  (double) timer->max_value / scale,
                                  timer->n_samples);
        }

      g_string_append (buffer, "\n");
    }
}
