#include "config.h"

#include "eos-profile-cmds.h"
#include "eos-profile-utils.h"

#include "endless/eosprofile-private.h"
#include "endless/gvdb/gvdb-reader.h"

#include <math.h>

static GPtrArray *files;

static const double
scale_val (double val)
{
  if (val >= G_USEC_PER_SEC)
    return val / G_USEC_PER_SEC;

  if (val >= 1000)
    return val / 1000.0;

  return val;
}

static const char *
unit_for (double val)
{
  enum {
    SECONDS,
    MILLISECONDS,
    MICROSECONDS
  };

  const char *units[] = {
    [SECONDS] = "s",
    [MILLISECONDS] = "ms",
    [MICROSECONDS] = "µs",
  };

  if (val >= G_USEC_PER_SEC)
    return units[SECONDS];

  if (val >= 1000)
    return units[MILLISECONDS];

  return units[MICROSECONDS];
}

gboolean
eos_profile_cmd_show_parse_args (int    argc,
                                 char **argv)
{
  files = g_ptr_array_new ();

  for (int i = 1; i < argc; i++)
    g_ptr_array_add (files, argv[i]);

  if (files->len == 0)
    {
      g_ptr_array_unref (files);
      return FALSE;
    }

  return TRUE;
}

static void
print_probe (const char *name)
{
  eos_profile_util_print_message ("PROBE", EOS_PRINT_COLOR_GREEN,
                                  "%s",
                                  name);
}

static void
print_location (const char *file,
                gint32      line,
                const char *function)
{
  eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                  "  ┕━ • location: %s() at %s:%d",
                                  function,
                                  file,
                                  line);
}

static void
print_samples (const char *name,
               gint32      n_samples,
               GVariant   *array)
{
  g_autoptr(GArray) samples = g_array_new (FALSE, FALSE, sizeof (ProfileSample));

  GVariantIter iter;
  g_variant_iter_init (&iter, array);

  gint64 start, end;
  while (g_variant_iter_next (&iter, "(xx)", &start, &end))
    {
      g_array_append_vals (samples,
                           &(ProfileSample) {
                             .start_time = start,
                             .end_time = end,
                           },
                           1);
   }

 gint64 min_sample = G_MAXINT64, max_sample = 0;
 gint64 total = 0;

 g_autoptr(GArray) valid_samples = g_array_new (FALSE, FALSE, sizeof (guint));

 for (int i = 0; i < samples->len; i++)
   {
     const ProfileSample *sample = &g_array_index (samples, ProfileSample, i);

     gint64 delta = sample->end_time - sample->start_time;

     /* If the probe never got stopped we need to skip this sample */
     if (delta < 0)
       continue;

     g_array_append_val (valid_samples, i);

     if (delta < min_sample)
       min_sample = delta;
     if (delta > max_sample)
       max_sample = delta;

     total += delta;
   }

  g_autofree char *msg = NULL;

  if (valid_samples->len > 1)
    {
      double avg = total / (double) valid_samples->len;
      double s = 0;
      double s_part = 0;

      for (int i = 1; i < valid_samples->len - 1; i++)
        {
          guint idx = g_array_index (valid_samples, guint, i);
          const ProfileSample *sample = &g_array_index (samples, ProfileSample, idx);

          gint64 delta = sample->end_time - sample->start_time;
          g_assert (delta >= 0);

          double deviation = delta - avg;
          s_part += (deviation * deviation);
        }

      if (valid_samples->len > 1)
        s = sqrt (s_part / (double) valid_samples->len - 1);
      else
        s = 0.0;

      g_autofree char *stddev = g_strdup_printf (", σ: %g", s);

      eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                      "  ┕━ • %d samples",
                                      valid_samples->len);
      eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                      "     ┕━ • total time: %d %s\n"
                                      "     ┕━ • avg: %g %s, min: %d %s, max: %d %s%s",
                                      (int) scale_val (total), unit_for (total),
                                      scale_val (avg), unit_for (avg),
                                      (int) scale_val (min_sample), unit_for (min_sample),
                                      (int) scale_val (max_sample), unit_for (max_sample),
                                      s == 0.0 || isnan (s) ? "" : stddev);
    }
  else if (valid_samples->len == 1)
    {
      eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                      "  ┕━ • 1 sample");
      eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                      "     ┕━ • total time: %d %s",
                                      (int) scale_val (total),
                                      unit_for (total));
    }
  else
    {
      eos_profile_util_print_message (NULL, EOS_PRINT_COLOR_NONE,
                                      "  ┕━ • Not enough valid samples found");
    }
}

static gboolean
print_probes (const char *probe_name,
              const char *file,
              const char *function,
              gint32      line,
              gint32      n_samples,
              GVariant   *samples,
              gpointer    data G_GNUC_UNUSED)
{
  print_probe (probe_name);

  print_location (file, line, function);

  if (n_samples > 0)
    print_samples (probe_name, n_samples, samples);

  return TRUE;
}

int
eos_profile_cmd_show_main (void)
{
  g_assert (files != NULL);

  for (int i = 0; i < files->len; i++)
    {
      const char *filename = g_ptr_array_index (files, i);
      g_autoptr(GError) error = NULL;

      eos_profile_util_print_message ("INFO", EOS_PRINT_COLOR_BLUE,
                                      "Loading profiling data from '%s'",
                                      filename);

      GvdbTable *db = gvdb_table_new (filename, TRUE, &error);

      if (error != NULL)
        {
          eos_profile_util_print_error ("Unable to load '%s': %s\n", filename, error->message);
          return 1;
        }

      GVariant *v = gvdb_table_get_raw_value (db, PROBE_DB_META_VERSION_KEY);
      gint32 version = v != NULL ? g_variant_get_int32 (v) : -1;

      if (version != PROBE_DB_VERSION)
        {
          eos_profile_util_print_error ("Unable to load '%s': invalid version\n");
          return 1;
        }

      v = gvdb_table_get_raw_value (db, PROBE_DB_META_APPID_KEY);
      if (v != NULL)
        {
          const char *appid = g_variant_get_string (v, NULL);

          eos_profile_util_print_message ("INFO", EOS_PRINT_COLOR_BLUE,
                                          "Application: %s",
                                          appid);
          g_clear_pointer (&v, g_variant_unref);
        }

      g_clear_pointer (&v, g_variant_unref);
      v = gvdb_table_get_raw_value (db, PROBE_DB_META_PROFILE_KEY);
      if (v != NULL)
        {
          gint64 profile_time = g_variant_get_int64 (v);

          eos_profile_util_print_message ("INFO", EOS_PRINT_COLOR_BLUE,
                                          "Total profile time: %d %s",
                                          (int) scale_val (profile_time),
                                          unit_for (profile_time));
          g_clear_pointer (&v, g_variant_unref);
        }

      v = gvdb_table_get_raw_value (db, PROBE_DB_META_START_KEY);
      if (v != NULL)
        {
          g_autoptr(GDateTime) dt =
            g_date_time_new_from_unix_local (g_variant_get_int64 (v));
          g_autofree char *start_time =
            g_date_time_format (dt, "%Y-%m-%d %T");

          eos_profile_util_print_message ("INFO", EOS_PRINT_COLOR_BLUE,
                                          "Start time: %s",
                                          start_time);
          g_clear_pointer (&v, g_variant_unref);
        }

      eos_profile_util_foreach_probe_v1 (db, print_probes, NULL);

      gvdb_table_free (db);
    }

  return 0;
}
