/*
 * gcc -o picviewer `pkg-config --cflags --libs clutter-0.8` picviewer.c
 */

#include <clutter/clutter.h>
#include <stdlib.h>
#include <strings.h>

static ClutterActor *stage = NULL;

/* For showing the filename: */
static ClutterActor *label_filename = NULL;

/* For rotating all images around an ellipse: */
static ClutterTimeline *timeline_rotation = NULL;

/* For moving one image up and scaling it: */
static ClutterTimeline *timeline_moveup = NULL;
static ClutterBehaviour *behaviour_scale = NULL;
static ClutterBehaviour *behaviour_path = NULL;
static ClutterBehaviour *behaviour_opacity = NULL;

/* The y position of the ellipse of images: */
static const gint ELLIPSE_Y = 390;
static const gint ELLIPSE_HEIGHT = 450; /* The distance from front to back 
                                           when it's rotated 90 degrees. */
static const gint IMAGE_HEIGHT = 100;

static gboolean on_texture_button_press (ClutterActor *actor,
                                         ClutterEvent *event, gpointer data);

static const double angle_step = 30;

typedef struct Item {
	ClutterActor *actor;
	ClutterBehaviour *ellipse_behaviour;
	gchar* filepath;
} Item;

static Item *item_at_front = NULL;

static GSList *list_items = 0;

static void on_foreach_clear_list_items(gpointer data, gpointer user_data)
{
	Item* item = (Item *) data;

	/* We don't need to unref the actor because the floating reference
	 * was taken by the stage. */
	g_object_unref(item->ellipse_behaviour);
	g_free(item->filepath);
	g_free(item);
}

static void scale_texture_default(ClutterActor *texture)
{
	int pixbuf_height = 0;
	clutter_texture_get_base_size(CLUTTER_TEXTURE (texture),
	                              NULL, &pixbuf_height);

	const gdouble scale = pixbuf_height ?
	                      IMAGE_HEIGHT / (gdouble) pixbuf_height : 0;
	clutter_actor_set_scale(texture, scale, scale);
}

static void load_images(const gchar* directory_path)
{
	g_return_if_fail(directory_path);

	/* Clear any existing images: */
	g_slist_foreach (list_items, on_foreach_clear_list_items, NULL);
	g_slist_free (list_items);

	/* Create a new list: */
	list_items = NULL;

	/* Discover the images in the directory: */
	GError *error = NULL;
	GDir *dir = g_dir_open (directory_path, 0, &error);
	if (error) {
		g_warning("g_dir_open() failed: %s\n", error->message);
		g_clear_error(&error);
		return;
	}

	const gchar *filename = NULL;
	while ( (filename = g_dir_read_name(dir)) ) {
		gchar *path = g_build_filename (directory_path, filename, NULL);

		/* Try to load the file as an image: */
		ClutterActor *actor = clutter_texture_new_from_file (path, NULL);
		if (actor) {
			Item* item = g_new0(Item, 1);

			item->actor = actor;
			item->filepath = g_strdup(path);

			/* Make sure that all images are shown with the
			 * same height: */
			scale_texture_default(item->actor);

			list_items = g_slist_append(list_items, item);
		}

		g_free (path);
	}
}

static void add_to_ellipse_behaviour(ClutterTimeline *timeline_rotation,
                                     gdouble start_angle, Item *item)
{
	g_return_if_fail (timeline_rotation);

	ClutterAlpha *alpha = clutter_alpha_new_full(timeline_rotation,
	                                             CLUTTER_ALPHA_SINE_INC,
						     NULL, NULL);

	item->ellipse_behaviour = clutter_behaviour_ellipse_new(alpha, 
			320, ELLIPSE_Y, /* x, y */
			ELLIPSE_HEIGHT, ELLIPSE_HEIGHT, /* width, height */
			CLUTTER_ROTATE_CW,
			start_angle, start_angle + 360);
	clutter_behaviour_ellipse_set_angle_tilt(
			CLUTTER_BEHAVIOUR_ELLIPSE(item->ellipse_behaviour), 
			CLUTTER_X_AXIS, -90);
	/* Note that ClutterAlpha has a floating reference,
	 * so we don't need to unref it. */

	clutter_behaviour_apply(item->ellipse_behaviour, item->actor);
}

static void add_image_actors()
{
	int x = 20;
	int y = 0;
	gdouble angle = 0;
	GSList *list = list_items;
	while (list) {
		/* Add the actor to the stage: */
		Item *item = (Item *) list->data;
		ClutterActor *actor = item->actor;
		clutter_container_add_actor(CLUTTER_CONTAINER (stage), actor);

		/* Set an initial position: */
		clutter_actor_set_position(actor, x, y);
		y += 100;

		/* Allow the actor to emit events.
		 * By default only the stage does this.
		 */
		clutter_actor_set_reactive(actor, TRUE);

		/* Connect signal handlers for events: */
		g_signal_connect(actor, "button-press-event",
		                 G_CALLBACK(on_texture_button_press), item);

		add_to_ellipse_behaviour(timeline_rotation, angle, item);
		angle += angle_step;

		clutter_actor_show(actor);

		list = g_slist_next(list);
	}
}

static gdouble angle_in_360(gdouble angle)
{
	gdouble result = angle;
	while (result >= 360)
		result -= 360;

	return result;
}

/* This signal handler is called when the item has finished 
 * moving up and increasing in size.
 */
static void on_timeline_moveup_completed(ClutterTimeline* timeline,
                                         gpointer user_data)
{
	/* Unref this timeline because we have now finished with it: */
	g_object_unref(timeline_moveup);
	timeline_moveup = NULL;

	g_object_unref(behaviour_scale);
	behaviour_scale = NULL;

	g_object_unref(behaviour_path);
	behaviour_path = NULL;

	g_object_unref(behaviour_opacity);
	behaviour_opacity = NULL;
}

/* This signal handler is called when the items have completely 
 * rotated around the ellipse.
 */
static void on_timeline_rotation_completed(ClutterTimeline *timeline,
                                           gpointer user_data)
{
	/* All the items have now been rotated, so that the clicked 
	 * item is at the front. */
	/* Now we transform just this one item gradually some more,
	 * and show the filename: */

	char *tmp_fn;

	/* Transform the image: */
	ClutterActor *actor = item_at_front->actor;
	timeline_moveup = clutter_timeline_new(60 /* frames */,
	                                       15 /* frames per second. */);
	ClutterAlpha *alpha = clutter_alpha_new_full(timeline_moveup,
	                                             CLUTTER_ALPHA_SINE_INC, NULL, NULL);

	/* Scale the item from its normal scale to approximately twice
	 * the normal scale: */
	gdouble scale_start = 0;
	clutter_actor_get_scale(actor, &scale_start, NULL);
	const gdouble scale_end = scale_start * 1.8;

	behaviour_scale = clutter_behaviour_scale_new(alpha,
	                                              scale_start, scale_start,
						      scale_end, scale_end);
	clutter_behaviour_apply(behaviour_scale, actor);

	/* Move the item up the y axis: */
	ClutterKnot knots[2];
	knots[0].x = clutter_actor_get_x(actor);
	knots[0].y = clutter_actor_get_y(actor);
	knots[1].x = knots[0].x;
	knots[1].y = knots[0].y - 250;
	behaviour_path = clutter_behaviour_path_new(alpha,
	                                            knots, G_N_ELEMENTS(knots));
	clutter_behaviour_apply(behaviour_path, actor);

	/* Show the filename gradually: */
	clutter_label_set_text(CLUTTER_LABEL (label_filename),
			(tmp_fn = rindex(item_at_front->filepath, '/')) == NULL ?
			item_at_front->filepath : tmp_fn + 1);
	behaviour_opacity = clutter_behaviour_opacity_new(alpha, 0, 255);
	clutter_behaviour_apply(behaviour_opacity, label_filename);

	/* Start the timeline and handle its "completed" signal,
	 * so we can unref it. */
	g_signal_connect(timeline_moveup, "completed",
	                 G_CALLBACK (on_timeline_moveup_completed), NULL);
	clutter_timeline_start(timeline_moveup);

	/* Note that ClutterAlpha has a floating reference, so we 
	 * don't need to unref it. */
}

static void rotate_all_until_item_is_at_front(Item *item)
{
	g_return_if_fail (item);

	clutter_timeline_stop(timeline_rotation);

	/* Stop the other timeline in case that is active at the same time: */
	if (timeline_moveup)
		clutter_timeline_stop(timeline_moveup);

	clutter_actor_set_opacity(label_filename, 0);

	/* Get the item's position in the list: */
	const gint pos = g_slist_index(list_items, item);
	g_assert(pos != -1);

	if (!item_at_front && list_items)
		item_at_front = (Item *) list_items->data;

	gint pos_front = 0;
	if (item_at_front)
		pos_front = g_slist_index(list_items, item_at_front);
	g_assert (pos_front != -1);

	/* const gint pos_offset_before_start = pos_front - pos; */ 

	/* Calculate the end angle of the first item: */
	const gdouble angle_front = 180;
	gdouble angle_start = angle_front - (angle_step * pos_front);
	angle_start = angle_in_360(angle_start);
	gdouble angle_end = angle_front - (angle_step * pos);

	gdouble angle_diff = 0;

	/* Set the end angles: */
	GSList *list = list_items;
	while (list) {
		Item *this_item = (Item *) list->data;

		/* Reset its size: */
		scale_texture_default(this_item->actor);

		angle_start = angle_in_360(angle_start);
		angle_end = angle_in_360(angle_end);

		/* Move 360 instead of 0 
		 * when moving for the first time,
		 * and when clicking on something that is already at the front.
		 */
		if (item_at_front == item)
			angle_end += 360;

		clutter_behaviour_ellipse_set_angle_start(
				CLUTTER_BEHAVIOUR_ELLIPSE(this_item->ellipse_behaviour), angle_start);
		clutter_behaviour_ellipse_set_angle_end (
				CLUTTER_BEHAVIOUR_ELLIPSE(this_item->ellipse_behaviour), angle_end);

		if (this_item == item) {
			if (angle_start < angle_end)
				angle_diff =  angle_end - angle_start;
			else
				angle_diff = 360 - (angle_start - angle_end);
		}

		/* TODO: Set the number of frames, depending on the angle.
		 * otherwise the actor will take the same amount of time to reach 
		 * the end angle regardless of how far it must move, causing it to 
		 * move very slowly if it does not have far to move.
		 */
		angle_end += angle_step;
		angle_start += angle_step;
		list = g_slist_next (list);
	}

	/* Set the number of frames to be proportional to the distance to travel,
	   so the speed is always the same: */
	gint pos_to_move = 0;
	if (pos_front < pos) {
		const gint count = g_slist_length(list_items);
		pos_to_move = count + (pos - pos_front);
	}
	else {
		pos_to_move = pos_front - pos;
	}

	clutter_timeline_set_n_frames (timeline_rotation, angle_diff);

	/* Remember what item will be at the front when this timeline finishes: */
	item_at_front = item;

	clutter_timeline_start (timeline_rotation);
}

static gboolean on_texture_button_press(ClutterActor *actor,
                                        ClutterEvent *event,
					gpointer user_data)
{
	/* Ignore the events if the timeline_rotation is running
	 * (that is, if the objects are moving), to simplify things:
	 */
	if (timeline_rotation &&
	    clutter_timeline_is_playing (timeline_rotation)) {
		printf("on_texture_button_press(): ignoring\n");
		return FALSE;
	}
	else
		printf("on_texture_button_press(): handling\n");

	Item *item = (Item *) user_data;
	rotate_all_until_item_is_at_front (item);

	return TRUE;
}

int main(int argc, char *argv[])
{
	ClutterColor stage_color = {
		0xB0, 0xB0, 0xB0, 0xff
	}; /* light gray */

	if (argc < 2) {
		fprintf(stderr, "No image directory given!\n");
		return -1;
	}

	clutter_init(&argc, &argv);

	/* Get the stage and set its size and color: */
	stage = clutter_stage_get_default();
	clutter_actor_set_size(stage, 800, 600);
	clutter_stage_set_color(CLUTTER_STAGE(stage), &stage_color);

	/* Create and add a label actor, hidden at first: */
	label_filename = clutter_label_new();
	ClutterColor label_color = { 0x60, 0x60, 0x90, 0xff }; /* blueish */
	clutter_label_set_color(CLUTTER_LABEL(label_filename), &label_color);
	clutter_label_set_font_name(CLUTTER_LABEL(label_filename), "Arial 24");
	clutter_actor_set_position(label_filename, 10, 10);
	clutter_actor_set_opacity(label_filename, 0);
	clutter_container_add_actor(CLUTTER_CONTAINER(stage), label_filename);
	clutter_actor_show(label_filename);

	/* Add a plane under the ellipse of images: */
	ClutterColor rect_color = {
		0xff, 0xff, 0xff, 0xff
	}; /* white */
	ClutterActor *rect = clutter_rectangle_new_with_color(&rect_color);
	clutter_actor_set_height(rect, ELLIPSE_HEIGHT + 20);
	clutter_actor_set_width(rect, clutter_actor_get_width(stage) + 100);
	/* Position it so that its center is under the images: */
	clutter_actor_set_position (rect, 
			-(clutter_actor_get_width (rect) - clutter_actor_get_width (stage)) / 2, 
			ELLIPSE_Y + IMAGE_HEIGHT - (clutter_actor_get_height (rect) / 2));
	/* Rotate it around its center: */
	clutter_actor_set_rotation(rect,
	                           CLUTTER_X_AXIS, -90, 0,
				   (clutter_actor_get_height (rect) / 2), 0);
	clutter_container_add_actor(CLUTTER_CONTAINER (stage), rect);
	clutter_actor_show(rect);

	/* Show the stage: */
	clutter_actor_show(stage);

	timeline_rotation = clutter_timeline_new(60 /* frames */,
	                                         30 /* frames per second. */);
	g_signal_connect(timeline_rotation,
	                 "completed",
			 G_CALLBACK(on_timeline_rotation_completed), NULL);

	/* Add an actor for each image: */
	load_images(argv[1]);
	add_image_actors();

	/* clutter_timeline_set_loop(timeline_rotation, TRUE); */

	/* Move them a bit to start with: */
	if (list_items)
		rotate_all_until_item_is_at_front((Item *) list_items->data);


	/* Start the main loop, so we can respond to events: */
	clutter_main();

	/* Free the list items and the list: */
	g_slist_foreach(list_items, on_foreach_clear_list_items, NULL);
	g_slist_free(list_items);

	g_object_unref(timeline_rotation);

	return EXIT_SUCCESS;
}

