/*
  Copyright (C) 2008-2021  Sutou Kouhei <kou@clear-code.com>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <string.h>

#include "bench-reporter.h"

typedef struct _BenchItem BenchItem;
struct _BenchItem
{
  gchar *label;
  gint n;
  BenchSetupFunc bench_setup;
  BenchFunc bench;
  BenchTeardownFunc bench_teardown;
  gpointer data;
};

static BenchItem *
bench_item_new(const gchar *label, gint n,
               BenchSetupFunc bench_setup,
               BenchFunc bench,
               BenchTeardownFunc bench_teardown,
               gpointer data)
{
  BenchItem *item;

  item = g_slice_new(BenchItem);

  item->label = g_strdup(label);
  item->n = n;
  item->bench_setup = bench_setup;
  item->bench = bench;
  item->bench_teardown = bench_teardown;
  item->data = data;

  return item;
}

static void
bench_item_free(BenchItem *item)
{
  if (item->label)
    g_free(item->label);

  g_slice_free(BenchItem, item);
}

#define BENCH_REPORTER_GET_PRIVATE(obj)                     \
  bench_reporter_get_instance_private(BENCH_REPORTER(obj))

typedef struct _BenchReporterPrivate BenchReporterPrivate;
struct _BenchReporterPrivate
{
  GList *items;
};

G_DEFINE_TYPE_WITH_PRIVATE(BenchReporter, bench_reporter, G_TYPE_OBJECT)

static void dispose        (GObject         *object);

static void
bench_reporter_class_init(BenchReporterClass *klass)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->dispose = dispose;
}

static void
bench_reporter_init(BenchReporter *reporter)
{
  BenchReporterPrivate *priv;

  priv = BENCH_REPORTER_GET_PRIVATE(reporter);

  priv->items = NULL;
}

static void
dispose(GObject *object)
{
  BenchReporterPrivate *priv;

  priv = BENCH_REPORTER_GET_PRIVATE(object);

  if (priv->items) {
    g_list_foreach(priv->items, (GFunc)bench_item_free, NULL);
    g_list_free(priv->items);
    priv->items = NULL;
  }

  G_OBJECT_CLASS(bench_reporter_parent_class)->dispose(object);
}

BenchReporter *
bench_reporter_new(void)
{
  return g_object_new(BENCH_TYPE_REPORTER, NULL);
}

void
bench_reporter_register(BenchReporter *reporter,
                        const gchar *label, gint n,
                        BenchSetupFunc bench_setup,
                        BenchFunc bench,
                        BenchTeardownFunc bench_teardown,
                        gpointer data)
{
  BenchReporterPrivate *priv;

  priv = BENCH_REPORTER_GET_PRIVATE(reporter);

  priv->items = g_list_append(priv->items, bench_item_new(label, n,
                                                          bench_setup,
                                                          bench,
                                                          bench_teardown,
                                                          data));
}

#define INDENT "  "

static void
print_header(BenchReporterPrivate *priv, gint max_label_length)
{
  gint n_spaces;

  g_print(INDENT);
  for (n_spaces = max_label_length + strlen(": ");
       n_spaces > 0;
       n_spaces--) {
    g_print(" ");
  }
  g_print("(total)    ");
  g_print("(average)  ");
  g_print("(median)\n");
}

static void
print_label(BenchReporterPrivate *priv, BenchItem *item, gint max_label_length)
{
  gint n_left_spaces;

  g_print(INDENT);
  if (item->label) {
    n_left_spaces = max_label_length - strlen(item->label);
  } else {
    n_left_spaces = max_label_length;
  }
  for (; n_left_spaces > 0; n_left_spaces--) {
    g_print(" ");
  }
  if (item->label)
    g_print("%s", item->label);
  g_print(": ");
}

static void
report_elapsed_time(gdouble elapsed_time)
{
  gdouble one_second = 1.0;
  gdouble one_millisecond = one_second / 1000.0;
  gdouble one_microsecond = one_millisecond / 1000.0;

  if (elapsed_time < one_microsecond) {
    g_print("(%.8fms)", elapsed_time * 1000.0);
  } else if (elapsed_time < one_millisecond) {
    g_print("(%.4fms)", elapsed_time * 1000.0);
  } else {
    g_print("(%.4fs) ", elapsed_time);
  }
}

static gdouble
compute_total_elapsed_time(GArray *elapsed_times)
{
  guint i;
  gdouble total = 0.0;

  for (i = 0; i< elapsed_times->len; i++) {
    gdouble elapsed_time = g_array_index(elapsed_times, gdouble, i);
    total += elapsed_time;
  }

  return total;
}

static void
report_elapsed_time_total(GArray *elapsed_times)
{
  report_elapsed_time(compute_total_elapsed_time(elapsed_times));
}

static void
report_elapsed_time_average(GArray *elapsed_times)
{
  gdouble total;
  gdouble average;

  total = compute_total_elapsed_time(elapsed_times);
  average = total / elapsed_times->len;
  report_elapsed_time(average);
}

static gint
compare_elapsed_time(gconstpointer a, gconstpointer b)
{
  const gdouble *elapsed_time1 = a;
  const gdouble *elapsed_time2 = b;

  if (*elapsed_time1 > *elapsed_time2) {
    return 1;
  } else if (*elapsed_time1 < *elapsed_time2) {
    return -1;
  } else {
    return 0;
  }
}

static void
report_elapsed_time_median(GArray *elapsed_times)
{
  gdouble median;

  g_array_sort(elapsed_times, compare_elapsed_time);
  median = g_array_index(elapsed_times, gdouble, elapsed_times->len / 2);
  report_elapsed_time(median);
}

static void
report_elapsed_time_statistics(GArray *elapsed_times)
{
  report_elapsed_time_total(elapsed_times);
  g_print(" ");
  report_elapsed_time_average(elapsed_times);
  g_print(" ");
  report_elapsed_time_median(elapsed_times);
  g_print("\n");
}

static void
run_item(BenchReporterPrivate *priv, BenchItem *item, gint max_label_length)
{
  GTimer *timer;
  GArray *elapsed_times;
  gint i;

  print_label(priv, item, max_label_length);

  elapsed_times = g_array_new(FALSE, FALSE, sizeof(gdouble));

  timer = g_timer_new();
  for (i = 0; i < item->n; i++) {
    gdouble elapsed_time;
    if (item->bench_setup)
      item->bench_setup(item->data);
    g_timer_start(timer);
    item->bench(item->data);
    g_timer_stop(timer);
    elapsed_time = g_timer_elapsed(timer, NULL);
    g_array_append_val(elapsed_times, elapsed_time);
    if (item->bench_teardown)
      item->bench_teardown(item->data);
  }
  g_timer_destroy(timer);

  report_elapsed_time_statistics(elapsed_times);

  g_array_free(elapsed_times, TRUE);

}

void
bench_reporter_run(BenchReporter *reporter)
{
  BenchReporterPrivate *priv;
  GList *node;
  size_t max_label_length = 0;

  priv = BENCH_REPORTER_GET_PRIVATE(reporter);
  for (node = priv->items; node; node = g_list_next(node)) {
    BenchItem *item = node->data;

    if (item->label)
      max_label_length = MAX(max_label_length, strlen(item->label));
  }

  print_header(priv, max_label_length);
  for (node = priv->items; node; node = g_list_next(node)) {
    BenchItem *item = node->data;

    run_item(priv, item, max_label_length);
  }
}
