/* GObject - GLib Type, Object, Parameter and Signal Library
 * Copyright (C) 2009 Red Hat, Inc.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include <math.h>
#include <string.h>
#include <glib-object.h>
#include "../testcommon.h"

#define DEFAULT_TEST_TIME 2 /* seconds */

static GType
simple_register_class (const char *name, GType parent, ...)
{
  GInterfaceInfo interface_info = { NULL, NULL, NULL };
  va_list args;
  GType type, interface;

  va_start (args, parent);
  type = g_type_register_static_simple (parent, name, sizeof (GObjectClass),
      NULL, parent == G_TYPE_INTERFACE ? 0 : sizeof (GObject), NULL, 0);
  for (;;)
    {
      interface = va_arg (args, GType);
      if (interface == 0)
        break;
      g_type_add_interface_static (type, interface, &interface_info);
    }
  va_end (args);

  return type;
}

/* test emulating liststore behavior for interface lookups */

static GType liststore;
static GType liststore_interfaces[6];

static gpointer 
register_types (void)
{
  static gsize inited = 0;
  if (g_once_init_enter (&inited))
    {
      liststore_interfaces[0] = simple_register_class ("GtkBuildable", G_TYPE_INTERFACE, 0);
      liststore_interfaces[1] = simple_register_class ("GtkTreeDragDest", G_TYPE_INTERFACE, 0);
      liststore_interfaces[2] = simple_register_class ("GtkTreeModel", G_TYPE_INTERFACE, 0);
      liststore_interfaces[3] = simple_register_class ("GtkTreeDragSource", G_TYPE_INTERFACE, 0);
      liststore_interfaces[4] = simple_register_class ("GtkTreeSortable", G_TYPE_INTERFACE, 0);
      liststore_interfaces[5] = simple_register_class ("UnrelatedInterface", G_TYPE_INTERFACE, 0);

      liststore = simple_register_class ("GtkListStore", G_TYPE_OBJECT, 
          liststore_interfaces[0], liststore_interfaces[1], liststore_interfaces[2],
          liststore_interfaces[3], liststore_interfaces[4], (GType) 0);

      g_once_init_leave (&inited, 1);
    }
  return NULL;
}

static void 
liststore_is_a_run (gpointer data)
{
  guint i;

  for (i = 0; i < 1000; i++)
    {
      g_assert (g_type_is_a (liststore, liststore_interfaces[0]));
      g_assert (g_type_is_a (liststore, liststore_interfaces[1]));
      g_assert (g_type_is_a (liststore, liststore_interfaces[2]));
      g_assert (g_type_is_a (liststore, liststore_interfaces[3]));
      g_assert (g_type_is_a (liststore, liststore_interfaces[4]));
      g_assert (!g_type_is_a (liststore, liststore_interfaces[5]));
    }
}

static gpointer
liststore_get_class (void)
{
  register_types ();
  return g_type_class_ref (liststore);
}

static void 
liststore_interface_peek_run (gpointer klass)
{
  guint i;
  gpointer iface;

  for (i = 0; i < 1000; i++)
    {
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[1]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[2]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[3]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[4]);
      g_assert (iface);
    }
}

static void 
liststore_interface_peek_same_run (gpointer klass)
{
  guint i;
  gpointer iface;

  for (i = 0; i < 1000; i++)
    {
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
      iface = g_type_interface_peek (klass, liststore_interfaces[0]);
      g_assert (iface);
    }
}

#if 0
/* DUMB test doing nothing */

static gpointer 
no_setup (void)
{
  return NULL;
}

static void 
no_run (gpointer data)
{
}
#endif

static void 
no_reset (gpointer data)
{
}

static void 
no_teardown (gpointer data)
{
}

typedef struct _PerformanceTest PerformanceTest;
struct _PerformanceTest {
  const char *name;

  gpointer (*setup) (void);
  void (*run) (gpointer data);
  void (*reset) (gpointer data);
  void (*teardown) (gpointer data);
};

static const PerformanceTest tests[] = {
  { "liststore-is-a",
    register_types,
    liststore_is_a_run,
    no_reset,
    no_teardown },
  { "liststore-interface-peek",
    liststore_get_class,
    liststore_interface_peek_run,
    no_reset,
    g_type_class_unref },
  { "liststore-interface-peek-same",
    liststore_get_class,
    liststore_interface_peek_same_run,
    no_reset,
    g_type_class_unref },
#if 0
  { "nothing",
    no_setup,
    no_run,
    no_reset,
    no_teardown }
#endif
};

static gboolean verbose = FALSE;
static guint n_threads = 0;
static gboolean list = FALSE;
static int test_length = DEFAULT_TEST_TIME;

static GOptionEntry cmd_entries[] = {
  {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
   "Print extra information", NULL},
  {"threads", 't', 0, G_OPTION_ARG_INT, &n_threads,
   "number of threads to run in parallel", NULL},
  {"seconds", 's', 0, G_OPTION_ARG_INT, &test_length,
   "Time to run each test in seconds", NULL},
  {"list", 'l', 0, G_OPTION_ARG_NONE, &list, 
   "List all available tests and exit", NULL},
  G_OPTION_ENTRY_NULL
};

static gpointer
run_test_thread (gpointer user_data)
{
  const PerformanceTest *test = user_data;
  gpointer data;
  double elapsed;
  GTimer *timer, *total;
  GArray *results;

  total = g_timer_new ();
  g_timer_start (total);

  /* Set up test */
  timer = g_timer_new ();
  data = test->setup ();
  results = g_array_new (FALSE, FALSE, sizeof (double));

  /* Run the test */
  do
    {
      g_timer_reset (timer);
      g_timer_start (timer);
      test->run (data);
      g_timer_stop (timer);
      elapsed = g_timer_elapsed (timer, NULL);
      g_array_append_val (results, elapsed);
      test->reset (data);
    }
  while (g_timer_elapsed (total, NULL) < test_length);

  /* Tear down */
  test->teardown (data);
  g_timer_destroy (timer);
  g_timer_destroy (total);

  return results;
}

static int
compare_doubles (gconstpointer a, gconstpointer b)
{
  double d = *(double *) a - *(double *) b;

  if (d < 0)
    return -1;
  if (d > 0)
    return 1;
  return 0;
}

static void
print_results (GArray *array)
{
  double min, max, avg;
  guint i;

  g_array_sort (array, compare_doubles);

  /* FIXME: discard outliers */

  min = g_array_index (array, double, 0) * 1000;
  max = g_array_index (array, double, array->len - 1) * 1000;
  avg = 0;
  for (i = 0; i < array->len; i++)
    {
      avg += g_array_index (array, double, i);
    }
  avg = avg / array->len * 1000;

  g_print ("  %u runs, min/avg/max = %.3f/%.3f/%.3f ms\n", array->len, min, avg, max);
}

static void
run_test (const PerformanceTest *test)
{
  GArray *results;

  g_print ("Running test \"%s\"\n", test->name);

  if (n_threads == 0) {
    results = run_test_thread ((gpointer) test);
  } else {
    guint i;
    GThread **threads;
    GArray *thread_results;
      
    threads = g_new (GThread *, n_threads);
    for (i = 0; i < n_threads; i++) {
      threads[i] = g_thread_new (NULL, run_test_thread, (gpointer) test);
      g_assert (threads[i] != NULL);
    }

    results = g_array_new (FALSE, FALSE, sizeof (double));
    for (i = 0; i < n_threads; i++) {
      thread_results = g_thread_join (threads[i]);
      g_array_append_vals (results, thread_results->data, thread_results->len);
      g_array_free (thread_results, TRUE);
    }
    g_free (threads);
  }

  print_results (results);
  g_array_free (results, TRUE);
}

static const PerformanceTest *
find_test (const char *name)
{
  gsize i;
  for (i = 0; i < G_N_ELEMENTS (tests); i++)
    {
      if (strcmp (tests[i].name, name) == 0)
	return &tests[i];
    }
  return NULL;
}

int
main (int   argc,
      char *argv[])
{
  const PerformanceTest *test;
  GOptionContext *context;
  GError *error = NULL;
  gsize i;

  context = g_option_context_new ("GObject performance tests");
  g_option_context_add_main_entries (context, cmd_entries, NULL);
  if (!g_option_context_parse (context, &argc, &argv, &error))
    {
      g_printerr ("%s: %s\n", argv[0], error->message);
      return 1;
    }

  if (list)
    {
      for (i = 0; i < G_N_ELEMENTS (tests); i++)
        {
          g_print ("%s\n", tests[i].name);
        }
      return 0;
    }

  if (argc > 1)
    {
      int k;
      for (k = 1; k < argc; k++)
	{
	  test = find_test (argv[k]);
	  if (test)
	    run_test (test);
	}
    }
  else
    {
      for (i = 0; i < G_N_ELEMENTS (tests); i++)
	run_test (&tests[i]);
    }

  g_option_context_free (context);
  return 0;
}
