/*                                           -*- mode: c; c-basic-offset: 2 -*-
 * To compile:
 *
 *     gcc -Wall -g $(pkg-config --cflags --libs gtk+-2.0 cairo) \
 *             akamaru.c main.c -o akamaru
 */

#include <gtk/gtk.h>
#include <cairo.h>
#include <cairo-xlib.h>
#include <gdk/gdkx.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <math.h>

#include "akamaru.h"

static void
model_init_constants (Model *model)
{
  model->elasticity = 0.7;
  model->friction = 10;
  model->gravity.x = 0;
  model->gravity.y = 80;
  model->k = 10;
  model->constrain_iterations = 10;
}

static void
model_init_polygons (Model *model)
{
  const double ground_level = 500;

  model_add_diamond (model, 250, 300);
  model_add_diamond (model, 400, 150);
  model_add_rectangle (model, -100, 200, 200, 250);
  model_add_rectangle (model, -200, ground_level, 1200, ground_level + 400);
  model_add_rectangle (model, 300, 320, 400, 350);
}

static void
model_init_snake (Model *model)
{
  const int num_objects = 20;
  Object *object, *prev, *pprev;
  int i;

  model_init (model);
  model_init_constants (model);
  model_init_polygons (model);

  prev = NULL;
  pprev = NULL;
  for (i = 0; i < num_objects; i++) {
    object = model_add_object (model,
			       g_random_int_range (100, 300),
			       g_random_int_range (100, 300), 1, NULL);

    if (pprev != NULL)
      model_add_stick (model, object, pprev, g_random_int_range (20, 40));
    if (prev != NULL)
      model_add_stick (model, object, prev, g_random_int_range (20, 40));

    pprev = prev;
    prev = object;
  }
}

static void
model_init_rope (Model *model)
{
  const int num_objects = 20;
  const int stick_length = 10;
  Object *object, *prev;
  int i;

  model_init (model);
  model_init_constants (model);
  model_init_polygons (model);

  for (i = 0, prev = NULL; i < num_objects; i++, prev = object) {
    object = model_add_object (model, 200, 40 + i * stick_length, 1, NULL);
    if (prev != NULL)
      model_add_stick (model, object, prev, stick_length);
  }
}

static void
model_init_curtain (Model *model)
{
  const int num_ropes = 5;
  const int num_rope_objects = 15;
  const int stick_length = 10;
  const int rope_offset = 30;
  Offset *offset;
  Object *prev, *object;
  double x, y;
  int i, j;

  model_init (model);
  model_init_constants (model);
  model_init_polygons (model);

  offset = model_add_offset (model, num_ropes, rope_offset, 0);

  for (i = 0; i < num_ropes; i++)
    {
      prev = NULL;
      for (j = 0; j < num_rope_objects; j++)
	{
	  x = 200 + i * rope_offset;
	  y = 40 + j * stick_length;
	  object = model_add_object (model, x, y, 1, NULL);
	  if (j == 0)
	    offset->objects[i] = object;
	  else
	    model_add_stick (model, prev, object, stick_length);
	  prev = object;
	}
    }
}

static void
model_init_grid (Model *model)
{
  const int width = 8, height = 8;
  const int distance = 20;
  Offset *offset;
  Object *last_column[height], *object;
  int i, j;

  model_init (model);
  model_init_constants (model);
  model_init_polygons (model);

  offset = model_add_offset (model, width, distance, 0);

  for (i = 0; i < width; i++)
    {
      for (j = 0; j < height; j++)
	{
	  object = model_add_object (model,
				     200 + i * distance,
				     40 + j * distance, 1, NULL);

	  if (i > 0)
	    model_add_string (model,
			      last_column[j], object, distance);
	  if (j == 0)
	    offset->objects[i] = object;
	  else
	    model_add_string (model,
			      last_column[j - 1], object, distance);
	  last_column[j] = object;
	}
    }
}

static void
model_init_molecule (Model *model)
{
  const int num_objects = 8;
  const int spring_length = 50;
  Object *objects[num_objects];
  int i;

  model_init (model);
  model_init_constants (model);

  for (i = 0; i < num_objects; i++)
    objects[i] = model_add_object (model,
				   g_random_int_range (200, 400),
				   g_random_int_range (200, 400),
				   0, NULL);

  for (i = 0; i < num_objects; i++)
    {
      model_add_spring (model,
			objects[i], objects[(i + 1) % num_objects],
			spring_length);
      model_add_spring (model,
			objects[i], objects[(i + 2) % num_objects],
			spring_length);
    }
}


static void
model_init_wobbly (Model *model)
{
  const int width = 8, height = 8;
  const int distance = 30;
  double x, y;
  int i, j;
  Object *object, *last_column[height];

  model_init (model);
  model_init_constants (model);
  model_init_polygons (model);

  for (i = 0; i < width; i++)
    {
      for (j = 0; j < height; j++)
	{
	  x = 200 + i * distance;
	  y = 40 + j * distance;
	  object = model_add_object (model, x, y, 0, NULL);
	  if (i > 0)
	    model_add_offset_spring (model, last_column[j], object,
				     distance, 0);
	  if (j > 0)
	    model_add_offset_spring (model, last_column[j - 1], object,
				     0, distance);
	  last_column[j] = object;
	}
  }
}

typedef struct {
  Model *model;
  Object *current;
  Object *anchor;
  int distance;
} DockInitClosure;

static void
dock_add_spacer (Object *object, void *data)
{
  DockInitClosure *closure = data;

  /* Skip the anchor object. */
  if (object != closure->anchor && object != closure->current)
    model_add_spacer (closure->model,
		      object, closure->current, closure->distance);
}

static void
model_init_dock (Model *model)
{
  const int num_objects = 8;
  const int distance = 30;
  DockInitClosure closure;
  int i;

  model_init (model);
  model_init_constants (model);
  model_add_enclosing_rectangle (model, 10, 10, 700, 500);

  closure.anchor = model_add_object (model, 0, 0, 0, NULL);
  model_add_anchor (model, closure.anchor, 300, 300);

  closure.model = model;
  closure.distance = distance;
  for (i = 0; i < num_objects; i++) {
    closure.current =
      model_add_object (model, 200 + i * distance / 3, 40, 2, NULL);
    model_add_spring (model, closure.anchor, closure.current, distance);
    model_for_each_object (model, dock_add_spacer, &closure);
  }
}

static void
draw_stick (Stick *stick, void *data)
{
  cairo_t *cr = data;

  cairo_move_to (cr, stick->a->position.x, stick->a->position.y);
  cairo_line_to (cr, stick->b->position.x, stick->b->position.y);
}

static void
draw_string (String *string, void *data)
{
  cairo_t *cr = data;

  cairo_move_to (cr, string->a->position.x, string->a->position.y);
  cairo_line_to (cr, string->b->position.x, string->b->position.y);
}

static void
draw_offset (Offset *offset, void *data)
{
  cairo_t *cr = data;
  int i;

  cairo_move_to (cr,
		 offset->objects[0]->position.x,
		 offset->objects[0]->position.y);
  for (i = 1; i < offset->num_objects; i++)
    cairo_line_to (cr,
		   offset->objects[i]->position.x,
		   offset->objects[i]->position.y);
}

static void
draw_spring (Spring *spring, void *data)
{
  cairo_t *cr = data;

  cairo_move_to (cr, spring->a->position.x, spring->a->position.y);
  cairo_line_to (cr, spring->b->position.x, spring->b->position.y);
}

static void
draw_offset_spring (OffsetSpring *spring, void *data)
{
  cairo_t *cr = data;

  cairo_move_to (cr, spring->a->position.x, spring->a->position.y);
  cairo_line_to (cr, spring->b->position.x, spring->b->position.y);
}

static void
draw_polygon (Polygon *polygon, void *data)
{
  cairo_t *cr = data;
  int i;

  for (i = 0; i < polygon->num_points; i++)
    cairo_line_to (cr, polygon->points[i].x, polygon->points[i].y);
  cairo_close_path (cr);

  if (polygon->enclosing)
    cairo_stroke (cr);
  else
    cairo_fill (cr);
}

static void
draw_object (Object *object, void *data)
{
  cairo_t *cr = data;

  cairo_arc (cr, object->position.x, object->position.y,
	     3, 0, 2 * M_PI);
  cairo_fill (cr);
}

typedef struct _Closure Closure;
struct _Closure {
  GtkWidget *drawing_area;
  GtkWidget *fps_label;
  Model *model;
  Anchor *mouse_anchor;
  int frame_count;
  int i;
  struct timeval start;
};

static void
draw_model (GtkWidget *widget, Model *model)
{
  cairo_t *cr;

  cr = gdk_cairo_create (widget->window);

  cairo_set_source_rgb (cr, 1, 1, 1);
  cairo_paint (cr);

  cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
  cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);

  cairo_set_line_width (cr, 1);
  cairo_set_source_rgba (cr, 0, 0, 0.5, 1);
  model_for_each_polygon (model, draw_polygon, cr);

  cairo_set_source_rgba (cr, 0, 1, 0, 1);
  model_for_each_string (model, draw_string, cr);
  cairo_stroke (cr);

  cairo_set_source_rgba (cr, 0, 0, 0, 0.8);
  model_for_each_spring (model, draw_spring, cr);
  cairo_stroke (cr);

  cairo_set_line_width (cr, 4);
  cairo_set_source_rgba (cr, 0, 0, 0.5, 0.5);
  model_for_each_offset (model, draw_offset, cr);
  cairo_stroke (cr);

  cairo_set_line_width (cr, 2);
  cairo_set_source_rgba (cr, 0, 0, 0, 1);
  model_for_each_stick (model, draw_stick, cr);
  cairo_stroke (cr);

  cairo_set_source_rgba (cr, 1, 0, 1, 0.4);
  model_for_each_offset_spring (model, draw_offset_spring, cr);
  cairo_stroke (cr);

  cairo_set_source_rgba (cr, 1, 0, 0, 0.4);
  model_for_each_object (model, draw_object, cr);

  cairo_destroy (cr);
}

static gboolean
expose_event (GtkWidget      *widget,
	      GdkEventExpose *event,
	      gpointer	      data)
{
  Closure *closure = data;

  draw_model (widget, closure->model);

  return TRUE;
}

static gboolean
button_press_event (GtkWidget	   *widget,
		    GdkEventButton *event,
		    gpointer	    data)
{
  Closure *closure = data;
  Object *object;

  if (event->button != 1)
    return TRUE;

  object = model_find_nearest (closure->model, event->x, event->y);
  closure->mouse_anchor =
    model_add_anchor (closure->model, object, event->x, event->y);

  return TRUE;
}

static gboolean
button_release_event (GtkWidget	     *widget,
		      GdkEventButton *event,
		      gpointer	      data)
{
  Closure *closure = data;

  if ((event->state & GDK_BUTTON1_MASK) == 0)
    return TRUE;

  model_delete_anchor (closure->model, closure->mouse_anchor);
  closure->mouse_anchor = NULL;

  return TRUE;
}

static gboolean
motion_notify_event (GtkWidget	    *widget,
		     GdkEventMotion *event,
		     gpointer	     data)
{
  Closure *closure = data;
  int x, y;
  GdkModifierType state;

  gdk_window_get_pointer (event->window, &x, &y, &state);

  if (closure->mouse_anchor != NULL)
    {
      closure->mouse_anchor->x = x + 0.5;
      closure->mouse_anchor->y = y + 0.5;
    }

  return TRUE;
}

typedef void (*ModelInitFunc) (Model *model);

static void
model_changed (GtkComboBox *combo, gpointer user_data)
{
  Closure *closure = user_data;
  GtkTreeIter iter;
  GtkTreeModel *tree_model;
  ModelInitFunc init;
  char *name;

  tree_model = gtk_combo_box_get_model (combo);
  if (!gtk_combo_box_get_active_iter (combo, &iter))
    return;

  gtk_tree_model_get (tree_model, &iter, 0, &name, 1, &init, -1);

  model_fini (closure->model);
  (*init) (closure->model);
}

static GtkTreeModel *
create_model_store (void)
{
  static struct {
    const char *name;
    ModelInitFunc init;
  } models[] = {
    { "Rope", model_init_rope },
    { "Snake", model_init_snake },
    { "Curtain", model_init_curtain },
    { "Grid", model_init_grid },
    { "Molecule", model_init_molecule },
    { "Wobbly", model_init_wobbly },
    { "Dock", model_init_dock }
  };

  GtkTreeIter iter;
  GtkTreeStore *store;
  gint i;

  store = gtk_tree_store_new (2, G_TYPE_STRING, G_TYPE_POINTER);

  for (i = 0; i < G_N_ELEMENTS(models); i++) {
    gtk_tree_store_append (store, &iter, NULL);
    gtk_tree_store_set (store, &iter,
			0, models[i].name, 1, models[i].init, -1);
 }
  
  return GTK_TREE_MODEL (store);

}

static GtkWidget *
create_model_combo (Closure *closure)
{
  GtkWidget *hbox;
  GtkWidget *combo, *label;
  GtkTreeModel *store;
  GtkCellRenderer *renderer;

  hbox = gtk_hbox_new (FALSE, 8);

  label = gtk_label_new_with_mnemonic ("_Model:");
  gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);

  store = create_model_store ();
  combo = gtk_combo_box_new_with_model (store);
  gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
  g_object_unref (store);

  renderer = gtk_cell_renderer_text_new ();
  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
  gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer,
				  "text", 0,
				  NULL);

  gtk_label_set_mnemonic_widget (GTK_LABEL (label), combo);
  gtk_box_pack_start (GTK_BOX (hbox), combo, FALSE, FALSE, 0);
  g_signal_connect (combo, "changed",
		    G_CALLBACK (model_changed), closure);

  label = gtk_label_new ("Frames per second: 0");
  gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);

  closure->fps_label = label;

  return hbox;
}

static void
create_window (Closure *closure)
{
  GtkWidget *window;
  GtkWidget *frame;
  GtkWidget *vbox;
  GtkWidget *da;
  GtkWidget *model_combo;

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW (window), "赤丸");

  g_signal_connect (window, "destroy",
		    G_CALLBACK (gtk_main_quit), &window);

  gtk_container_set_border_width (GTK_CONTAINER (window), 8);

  vbox = gtk_vbox_new (FALSE, 8);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 8);
  gtk_container_add (GTK_CONTAINER (window), vbox);

  /*
   * Create the drawing area
   */
      
  frame = gtk_frame_new (NULL);
  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
  gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
      
  da = gtk_drawing_area_new ();
  /* set a minimum size */
  gtk_widget_set_size_request (da, 200, 200);

  gtk_container_add (GTK_CONTAINER (frame), da);

  /* Signals used to handle backing pixmap */
      
  g_signal_connect (da, "expose_event",
		    G_CALLBACK (expose_event), closure);
      
  /* Event signals */
      
  g_signal_connect (da, "motion_notify_event",
		    G_CALLBACK (motion_notify_event), closure);
  g_signal_connect (da, "button_press_event",
		    G_CALLBACK (button_press_event), closure);
  g_signal_connect (da, "button_release_event",
		    G_CALLBACK (button_release_event), closure);

  /* Ask to receive events the drawing area doesn't normally
   * subscribe to
   */
  gtk_widget_set_events (da, gtk_widget_get_events (da)
			 | GDK_LEAVE_NOTIFY_MASK
			 | GDK_BUTTON_PRESS_MASK
			 | GDK_BUTTON_RELEASE_MASK
			 | GDK_POINTER_MOTION_MASK
			 | GDK_POINTER_MOTION_HINT_MASK);

  model_combo = create_model_combo (closure);
  gtk_box_pack_start (GTK_BOX (vbox), model_combo, FALSE, FALSE, 0);

  closure->drawing_area = da;
}

static gint
timeout_callback (gpointer data)
{
  Closure *closure = data;

  model_step (closure->model, 0.2);

  closure->i++;
  if (closure->i == 1) {
    gtk_widget_queue_draw (closure->drawing_area);
    closure->i = 0;
    closure->frame_count++;
  }

  if (closure->frame_count == 200) {
    struct timeval end, elapsed;
    double total;
    char text[50];

    closure->frame_count = 0;
    gettimeofday (&end, NULL);
    if (closure->start.tv_usec > end.tv_usec) {
      end.tv_usec += 1000000;
      end.tv_sec--;
    }

    elapsed.tv_usec = end.tv_usec - closure->start.tv_usec;
    elapsed.tv_sec = end.tv_sec - closure->start.tv_sec;

    total = elapsed.tv_sec + ((double) elapsed.tv_usec / 1e6);
    if (total < 0) {
      total = 0;
    }
    closure->start = end;
    snprintf (text, sizeof text, "Frames per second: %.2f", 200 / total);
    gtk_label_set_text (GTK_LABEL (closure->fps_label), text);
  }

  return TRUE;
}

int
main (int argc, char *argv[])
{
  Closure closure;
  Model model;

  gtk_init (&argc, &argv);
  model_init_rope (&model);
  create_window (&closure);
  closure.i = 0;
  closure.model = &model;
  closure.mouse_anchor = NULL;
  closure.frame_count = 0;
  gettimeofday (&closure.start, NULL);
  g_timeout_add (40, timeout_callback, &closure);
  gtk_widget_show_all (gtk_widget_get_toplevel (closure.drawing_area));
  gtk_main ();

  return 0;
}
