#include <gdk/gdk.h>
#ifdef GDK_WINDOWING_X11
#  include <gdk/gdkx.h>
#endif
#include <string.h>
#include "gtkwidgetprofiler.h"
#include "marshalers.h"
#include "typebuiltins.h"

typedef enum {
  STATE_NOT_CREATED,
  STATE_INSTRUMENTED_NOT_MAPPED,
  STATE_INSTRUMENTED_MAPPED
} State;

struct _GtkWidgetProfilerPrivate {
  State state;

  GtkWidget *profiled_widget;
  GtkWidget *toplevel;

  int n_iterations;

  GTimer *timer;

  gulong toplevel_expose_event_id;
  gulong toplevel_property_notify_event_id;

  GdkAtom profiler_atom;

  guint profiling : 1;
  guint resize : 1;
};

G_DEFINE_TYPE (GtkWidgetProfiler, gtk_widget_profiler, G_TYPE_OBJECT);

static void gtk_widget_profiler_finalize (GObject *object);

enum {
  CREATE_WIDGET,
  REPORT,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

static void
gtk_widget_profiler_class_init (GtkWidgetProfilerClass *class)
{
  GObjectClass *object_class;

  object_class = (GObjectClass *) class;

  signals[CREATE_WIDGET] =
    g_signal_new ("create-widget",
		  G_OBJECT_CLASS_TYPE (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GtkWidgetProfilerClass, create_widget),
		  NULL, NULL,
		  _gtk_marshal_OBJECT__VOID,
		  G_TYPE_OBJECT, 0);

  signals[REPORT] =
    g_signal_new ("report",
		  G_OBJECT_CLASS_TYPE (object_class),
		  G_SIGNAL_RUN_FIRST,
		  G_STRUCT_OFFSET (GtkWidgetProfilerClass, report),
		  NULL, NULL,
		  _gtk_marshal_VOID__ENUM_OBJECT_DOUBLE,
		  G_TYPE_NONE, 3,
		  GTK_TYPE_WIDGET_PROFILER_REPORT,
		  G_TYPE_OBJECT,
		  G_TYPE_DOUBLE);

  object_class->finalize = gtk_widget_profiler_finalize;
}

static void
gtk_widget_profiler_init (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = g_new0 (GtkWidgetProfilerPrivate, 1);
  profiler->priv = priv;

  priv->state = STATE_NOT_CREATED;
  priv->n_iterations = 1;
  priv->resize = FALSE;

  priv->timer = g_timer_new ();

  priv->profiler_atom = gdk_atom_intern ("GtkWidgetProfiler", FALSE);
}

static void
reset_state (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  if (priv->toplevel)
    {
      g_signal_handler_disconnect (priv->toplevel, priv->toplevel_expose_event_id);
      priv->toplevel_expose_event_id = 0;

      g_signal_handler_disconnect (priv->toplevel, priv->toplevel_property_notify_event_id);
      priv->toplevel_property_notify_event_id = 0;

      gtk_widget_destroy (priv->toplevel);
      priv->toplevel = NULL;
      priv->profiled_widget = NULL;
    }

  priv->state = STATE_NOT_CREATED;
}

static void
gtk_widget_profiler_finalize (GObject *object)
{
  GtkWidgetProfiler *profiler;
  GtkWidgetProfilerPrivate *priv;

  profiler = GTK_WIDGET_PROFILER (object);
  priv = profiler->priv;

  reset_state (profiler);
  g_timer_destroy (priv->timer);

  g_free (priv);

  G_OBJECT_CLASS (gtk_widget_profiler_parent_class)->finalize (object);
}

GtkWidgetProfiler *
gtk_widget_profiler_new (void)
{
  return g_object_new (GTK_TYPE_WIDGET_PROFILER, NULL);
}

void
gtk_widget_profiler_set_num_iterations (GtkWidgetProfiler *profiler,
					gint               n_iterations)
{
  GtkWidgetProfilerPrivate *priv;

  g_return_if_fail (GTK_IS_WIDGET_PROFILER (profiler));
  g_return_if_fail (n_iterations > 0);

  priv = profiler->priv;
  priv->n_iterations = n_iterations;
}

static void
gtk_widget_profiler_start (GtkWidgetProfiler *profiler)
{
#ifdef GDK_WINDOWING_X11
  /* grab display, so we really only benchmark this program */
  gdk_x11_display_grab (gdk_display_get_default ());
#endif
  g_timer_reset (profiler->priv->timer);
}

static void
gtk_widget_profiler_stop (GtkWidgetProfiler *profiler, 
			  GtkWidgetProfilerReport report)
{
  GtkWidgetProfilerPrivate *priv = profiler->priv;
  gdouble elapsed = g_timer_elapsed (priv->timer, NULL);
#ifdef GDK_WINDOWING_X11
  /* ungrab previously grabbed display */
  gdk_x11_display_ungrab (gdk_display_get_default ());
#endif
  /* signal report */
  g_signal_emit (profiler, signals[REPORT], 0, report, priv->profiled_widget, elapsed);
  /* make sure other apps can do their stuff, so they don't mess up the next measurement */
  g_usleep (0);
}

static GtkWidget *
create_widget_via_emission (GtkWidgetProfiler *profiler)
{
  GtkWidget *widget;

  widget = NULL;
  g_signal_emit (profiler, signals[CREATE_WIDGET], 0, &widget);
  if (!widget)
    g_error ("The profiler emitted the \"create-widget\" signal but the signal handler returned no widget!");

  if (GTK_WIDGET_VISIBLE (widget) || GTK_WIDGET_MAPPED (widget))
    g_error ("The handler for \"create-widget\" must return an unmapped and unshown widget");

  return widget;
}

static gboolean
toplevel_property_notify_event_cb (GtkWidget *widget, GdkEventProperty *event, gpointer data)
{
  GtkWidgetProfiler *profiler;
  GtkWidgetProfilerPrivate *priv;

  profiler = GTK_WIDGET_PROFILER (data);
  priv = profiler->priv;

  if (event->atom != priv->profiler_atom)
    return FALSE;

  /* Finish timing map/expose */
  gtk_widget_profiler_stop (profiler, GTK_WIDGET_PROFILER_REPORT_EXPOSE);

  gtk_main_quit (); /* This will get us back to the end of profile_map_expose() */
  return TRUE;
}

static gboolean
toplevel_idle_after_expose_cb (gpointer data)
{
  GtkWidgetProfiler *profiler;
  GtkWidgetProfilerPrivate *priv;

  profiler = GTK_WIDGET_PROFILER (data);
  priv = profiler->priv;

  gdk_property_change (priv->toplevel->window,
		       priv->profiler_atom,
		       gdk_atom_intern ("STRING", FALSE),
		       8,
		       GDK_PROP_MODE_REPLACE,
		       (guchar *) "hello",
		       strlen ("hello"));

  return FALSE;
}

static gboolean
toplevel_expose_event_cb (GtkWidget *widget, GdkEventExpose *event, gpointer data)
{
  GtkWidgetProfiler *profiler;

  profiler = GTK_WIDGET_PROFILER (data);

  g_idle_add_full (G_PRIORITY_HIGH, toplevel_idle_after_expose_cb, profiler, NULL);
  return FALSE;
}

static void
instrument_toplevel (GtkWidgetProfiler *profiler,
		     GtkWidget         *toplevel)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  priv->toplevel_expose_event_id = g_signal_connect (toplevel, "expose-event",
						     G_CALLBACK (toplevel_expose_event_cb), profiler);

  gtk_widget_add_events (toplevel, GDK_PROPERTY_CHANGE_MASK);
  priv->toplevel_property_notify_event_id = g_signal_connect (toplevel, "property-notify-event",
							      G_CALLBACK (toplevel_property_notify_event_cb), profiler);
}

#define SCREEN_FRACTION_FOR_RESIZE_PARENT_WINDOW 0.9

static GtkWidget *
ensure_and_get_toplevel (GtkWidgetProfiler *profiler,
			 GtkWidget *widget)
{
  GtkWidget *toplevel;
  GtkWidget *window;
  GtkWidget *fixed;
  GtkWidgetProfilerPrivate *priv;
  
  priv = profiler->priv;
  
  toplevel = gtk_widget_get_toplevel (widget);
  if (GTK_WIDGET_TOPLEVEL (toplevel)) {
    return toplevel;
  }

  g_assert (toplevel == widget); /* we don't want extraneous ancestors */
  
  window = gtk_window_new (GTK_WINDOW_POPUP);

  /* Small hack so that the toplevel still gets expose events even when
   * the child occupies the whole surface
   */
  gtk_container_set_border_width (GTK_CONTAINER (window), 1);
  
  if (priv->resize) {
    gtk_window_set_default_size (GTK_WINDOW (window),
				 SCREEN_FRACTION_FOR_RESIZE_PARENT_WINDOW * gdk_screen_width (),
				 SCREEN_FRACTION_FOR_RESIZE_PARENT_WINDOW * gdk_screen_height ());
    fixed = gtk_fixed_new ();
    gtk_container_add (GTK_CONTAINER (window), fixed);
    gtk_fixed_put (GTK_FIXED (fixed), widget, 0, 0);
  } else {
    gtk_container_add (GTK_CONTAINER (window), widget);
  }
  
  return window;
}

static GtkWidget *
get_instrumented_toplevel (GtkWidgetProfiler *profiler,
			   GtkWidget         *widget)
{
  GtkWidget *window;

  window = ensure_and_get_toplevel (profiler, widget);
  instrument_toplevel (profiler, window);

  return window;
}

static void
map_widget (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;
  g_assert (priv->state == STATE_INSTRUMENTED_NOT_MAPPED);

  /* Time map.
   *
   * FIXME: we are really timing a show_all(); we don't really wait for all the "map_event" signals
   * to happen.  Should we rename GTK_WIDGET_PROFILER_REPORT_MAP report to something else?
   */

  gtk_widget_show_all (priv->toplevel);
  priv->state = STATE_INSTRUMENTED_MAPPED;
}

static void
profile_map_expose (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  g_assert (priv->state == STATE_INSTRUMENTED_NOT_MAPPED);

  gtk_widget_profiler_start (profiler);
  map_widget (profiler);
  gtk_widget_profiler_stop (profiler, GTK_WIDGET_PROFILER_REPORT_MAP);

  /* Time expose; this gets recorded in toplevel_property_notify_event_cb() */

  gtk_widget_profiler_start (profiler);
  gtk_main ();
}

static void
profile_destroy (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  g_assert (priv->state != STATE_NOT_CREATED);

  gtk_widget_profiler_start (profiler);
  reset_state (profiler);
  gtk_widget_profiler_stop (profiler, GTK_WIDGET_PROFILER_REPORT_DESTROY);
}

static void
create_widget (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  g_assert (priv->state == STATE_NOT_CREATED);

  priv->profiled_widget = create_widget_via_emission (profiler);
  priv->toplevel = get_instrumented_toplevel (profiler, priv->profiled_widget);

  priv->state = STATE_INSTRUMENTED_NOT_MAPPED;
}

/* The "boot time" of a widget is the time needed to
 *
 *   1. Create the widget
 *   2. Map it
 *   3. Expose it
 *   4. Destroy it.
 *
 * This runs a lot of interesting code:  instantiation, size requisition and
 * allocation, realization, mapping, exposing, destruction.
 */
static void
profile_boot (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;

  priv = profiler->priv;

  g_assert (priv->state == STATE_NOT_CREATED);

  /* Time creation */

  gtk_widget_profiler_start (profiler);
  create_widget (profiler);
  gtk_widget_profiler_stop (profiler, GTK_WIDGET_PROFILER_REPORT_CREATE);

  /* Start timing map/expose */

  profile_map_expose (profiler);

  /* Profile destruction */

  profile_destroy (profiler);
}

/* To measure expose time, we trigger a full expose on the toplevel window.  We
 * do the same as xrefresh(1), i.e. we map and unmap a window to make the other
 * one expose.
 */
static void
profile_expose (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;
  GdkWindow *window;
  GdkWindowAttr attr;
  int attr_mask;

  priv = profiler->priv;

  g_assert (priv->state == STATE_INSTRUMENTED_MAPPED);

  /* Time creation */

  attr.x = 0;
  attr.y = 0;
  attr.width = priv->toplevel->allocation.width;
  attr.height = priv->toplevel->allocation.width;
  attr.wclass = GDK_INPUT_OUTPUT;
  attr.window_type = GDK_WINDOW_CHILD;
  attr.event_mask = 0;

  attr_mask = GDK_WA_X | GDK_WA_Y;

  window = gdk_window_new (priv->toplevel->window, &attr, attr_mask);
  gdk_window_set_back_pixmap (window, NULL, TRUE); /* avoid flicker */

  gdk_window_show (window);
  gdk_window_hide (window);
  gdk_window_destroy (window);

  /* Time expose; this gets recorded in toplevel_property_notify_event_cb() */

  gtk_widget_profiler_start (profiler);
  gtk_main ();
}

static void
profile_expose_with_size (GtkWidgetProfiler *profiler, int width, int height, float fraction)
{
  GtkWidgetProfilerPrivate *priv;
  GdkWindow *window;
  GdkWindowAttr attr;
  int attr_mask;

  priv = profiler->priv;

  g_assert (priv->state == STATE_INSTRUMENTED_MAPPED);

  gtk_widget_set_size_request (priv->profiled_widget, (int) (fraction * width), (int) (fraction * height));
  gtk_widget_queue_resize (priv->profiled_widget);

  /* Time creation */

  attr.x = 0;
  attr.y = 0;
  attr.width = priv->toplevel->allocation.width;
  attr.height = priv->toplevel->allocation.height;
  attr.wclass = GDK_INPUT_OUTPUT;
  attr.window_type = GDK_WINDOW_CHILD;
  attr.event_mask = 0;

  attr_mask = GDK_WA_X | GDK_WA_Y;

  window = gdk_window_new (priv->toplevel->window, &attr, attr_mask);
  gdk_window_set_back_pixmap (window, NULL, TRUE); /* avoid flicker */

  gdk_window_show (window);
  gdk_window_hide (window);
  gdk_window_destroy (window);

  /* Time expose; this gets recorded in toplevel_property_notify_event_cb() */

  gtk_widget_profiler_start (profiler);
  gtk_main ();
}

void
gtk_widget_profiler_profile_boot (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;
  int i, n;

  g_return_if_fail (GTK_IS_WIDGET_PROFILER (profiler));

  priv = profiler->priv;
  g_return_if_fail (!priv->profiling);

  reset_state (profiler);
  priv->profiling = TRUE;

  n = priv->n_iterations;
  for (i = 0; i < n; i++)
    profile_boot (profiler);

  priv->profiling = FALSE;
}

void
gtk_widget_profiler_profile_expose (GtkWidgetProfiler *profiler)
{
  GtkWidgetProfilerPrivate *priv;
  int i, n;

  g_return_if_fail (GTK_IS_WIDGET_PROFILER (profiler));

  priv = profiler->priv;
  g_return_if_fail (!priv->profiling);

  reset_state (profiler);
  priv->profiling = TRUE;

  create_widget (profiler);
  map_widget (profiler);

  n = priv->n_iterations;
  for (i = 0; i < n; i++)
    profile_expose (profiler);

  priv->profiling = FALSE;

  reset_state (profiler);
}

void
gtk_widget_profiler_profile_expose_with_resize (GtkWidgetProfiler *profiler, float min_fraction, float max_fraction)
{
  GtkWidgetProfilerPrivate *priv;
  int   width, height;
  float step, fraction;

  g_return_if_fail (GTK_IS_WIDGET_PROFILER (profiler));
  g_assert ((min_fraction <= max_fraction) && (min_fraction >= 0) && (max_fraction <= 1));

  priv = profiler->priv;
  g_return_if_fail (!priv->profiling);

  reset_state (profiler);
  priv->profiling = TRUE;
  priv->resize    = TRUE;

  create_widget (profiler);
  map_widget (profiler);

  step = (max_fraction - min_fraction) / ((float) priv->n_iterations - 1);
  width  = gdk_screen_width ();
  height = gdk_screen_height ();

/*   /\* To the right... *\/ */
/*   for (fraction = min_fraction; fraction <= max_fraction; fraction += step) { */
/*     profile_expose_with_size (profiler, (int) (fraction * width), (int) (min_fraction * height)); */
/*   } */

/*   /\* ... and back again *\/ */
/*   for (fraction = max_fraction; fraction >= min_fraction; fraction -= step) { */
/*     profile_expose_with_size (profiler, (int) (fraction * width), (int) (min_fraction * height)); */
/*   } */

/*   /\* Down... *\/ */
/*   for (fraction = min_fraction; fraction <= max_fraction; fraction += step) { */
/*     profile_expose_with_size (profiler, (int) (fraction * width), (int) (min_fraction * height)); */
/*   } */

/*   /\* ... and back again *\/ */
/*   for (fraction = max_fraction; fraction >= min_fraction; fraction -= step) { */
/*     profile_expose_with_size (profiler, (int) (fraction * width), (int) (min_fraction * height)); */
/*   } */

  /* Down and to the right... */
  for (fraction = min_fraction; fraction <= max_fraction; fraction += step) {
    profile_expose_with_size (profiler, width, height, fraction);
  }

/*   /\* ... and back again *\/ */
/*   for (fraction = max_fraction; fraction >= min_fraction; fraction -= step) { */
/*     profile_expose_with_size (profiler, (int) (fraction * width), (int) (min_fraction * height)); */
/*   } */

  priv->profiling = FALSE;

  reset_state (profiler);
}
