/*
 * Copyright (C) 2005 Ray Strode <rstrode@redhat.com>,
 *                    Matthias Clasen <mclasen@redhat.com>,
 *                    Søren Sandmann <sandmann@redhat.com>
 * Copyright (C) 2012-2021 MATE Developers
 *
 * This program 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 of the
 * License, or (at your option) any later version.
 *
 * This program 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 program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * Originally written by: Ray Strode <rstrode@redhat.com>
 *
 * Later contributions by: Matthias Clasen <mclasen@redhat.com>
 *                         Søren Sandmann <sandmann@redhat.com>
 */

#include "config.h"

#include <math.h>
#include <stdlib.h>
#include <sysexits.h>
#include <time.h>

#include <glib.h>
#include <glib/gi18n.h>

#include <gdk/gdk.h>
#include <gtk/gtk.h>

#include "gs-theme-window.h"

#ifndef trunc
#define trunc(x) (((x) > 0.0) ? floor((x)) : -floor(-(x)))
#endif

#ifndef OPTIMAL_FRAME_RATE
#define OPTIMAL_FRAME_RATE (25.0)
#endif

#ifndef STAT_PRINT_FREQUENCY
#define STAT_PRINT_FREQUENCY (2000)
#endif

#ifndef FLOATER_MAX_SIZE
#define FLOATER_MAX_SIZE (128.0)
#endif

#ifndef FLOATER_MIN_SIZE
#define FLOATER_MIN_SIZE (16.0)
#endif
#ifndef FLOATER_DEFAULT_COUNT
#define FLOATER_DEFAULT_COUNT (5)
#endif

#ifndef SMALL_ANGLE
#define SMALL_ANGLE (0.025 * G_PI)
#endif

#ifndef BIG_ANGLE
#define BIG_ANGLE (0.125 * G_PI)
#endif

#ifndef GAMMA
#define GAMMA 2.2
#endif

typedef struct _Point Point;
typedef struct _Path Path;
typedef struct _Rectangle Rectangle;
typedef struct _ScreenSaverFloater ScreenSaverFloater;
typedef struct _CachedSource CachedSource;
typedef struct _ScreenSaver ScreenSaver;

struct _Point
{
	gdouble x, y;
};

struct _Path
{
	Point start_point;
	Point start_control_point;
	Point end_control_point;
	Point end_point;

	gdouble x_linear_coefficient,
	        y_linear_coefficient;

	gdouble x_quadratic_coefficient,
	        y_quadratic_coefficient;

	gdouble x_cubic_coefficient,
	        y_cubic_coefficient;

	gdouble duration;
};

struct _CachedSource
{
	cairo_pattern_t *pattern;
	gint width, height;
};

struct _Rectangle
{
	Point top_left_point;
	Point bottom_right_point;
};

struct _ScreenSaverFloater
{
	GdkRectangle bounds;

	Point start_position;
	Point position;

	gdouble scale;
	gdouble opacity;

	Path *path;
	gdouble path_start_time;
	gdouble path_start_scale;
	gdouble path_end_scale;

	gdouble angle;
	gdouble angle_increment;
};

struct _ScreenSaver
{
	GtkWidget  *drawing_area;
	Rectangle canvas_rectangle;
	GHashTable *cached_sources;

	char *filename;

	gdouble first_update_time;

	gdouble last_calculated_stats_time,
	        current_calculated_stats_time;
	gint update_count, frame_count;

	gdouble updates_per_second;
	gdouble frames_per_second;

	guint state_update_timeout_id;
	guint stats_update_timeout_id;

	GList *floaters;
	gint max_floater_count;

	guint should_do_rotations: 1;
	guint should_show_paths : 1;
	guint draw_ops_pending : 1;
};

static Path *path_new (Point *start_point,
                       Point *start_control_point,
                       Point *end_control_point,
                       Point *end_point,
                       gdouble duration);
static void path_free (Path *path);

static ScreenSaverFloater *screen_saver_floater_new (ScreenSaver *screen_saver,
        Point       *position,
        gdouble      scale);
static void screen_saver_floater_free (ScreenSaver        *screen_saver,
                                       ScreenSaverFloater *floater);
static gboolean screen_saver_floater_is_off_canvas (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater);
static gboolean screen_saver_floater_should_bubble_up (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             performance_ratio,
        gdouble            *duration);
static gboolean screen_saver_floater_should_come_on_screen (ScreenSaver   *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             performance_ratio,
        gdouble            *duration);
static Point screen_saver_floater_get_position_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time);
static gdouble screen_saver_floater_get_scale_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time);
static gdouble screen_saver_floater_get_angle_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time);

static Path *screen_saver_floater_create_path_to_on_screen (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration);
static Path *screen_saver_floater_create_path_to_bubble_up (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration);
static Path *screen_saver_floater_create_path_to_random_point (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration);
static Path *screen_saver_floater_create_path (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater);
static void screen_saver_floater_update_state (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time);

static gboolean screen_saver_floater_do_draw (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        cairo_t            *context);

static CachedSource *cached_source_new (cairo_pattern_t *pattern,
                                        gint             width,
                                        gint             height);
static void cached_source_free (CachedSource *source);

static ScreenSaver *screen_saver_new (GtkWidget       *drawing_area,
                                      const gchar     *filename,
                                      gint             max_floater_count,
                                      gboolean         should_do_rotations,
                                      gboolean         should_show_paths);
static void screen_saver_free (ScreenSaver *screen_saver);
static gdouble screen_saver_get_timestamp (ScreenSaver *screen_saver);
static void screen_saver_get_initial_state (ScreenSaver *screen_saver);
static void screen_saver_update_state (ScreenSaver *screen_saver,
                                       gdouble      time);
static gboolean screen_saver_do_update_state (ScreenSaver *screen_saver);
static gboolean screen_saver_do_update_stats (ScreenSaver *screen_saver);
static gdouble screen_saver_get_updates_per_second (ScreenSaver *screen_saver);
static gdouble screen_saver_get_frames_per_second (ScreenSaver *screen_saver);
static gdouble screen_saver_get_image_cache_usage (ScreenSaver *screen_saver);
static void screen_saver_create_floaters (ScreenSaver *screen_saver);
static void screen_saver_destroy_floaters (ScreenSaver *screen_saver);
static void screen_saver_on_size_allocate (ScreenSaver   *screen_saver,
        GtkAllocation *allocation);
static void screen_saver_on_draw (ScreenSaver    *screen_saver,
        cairo_t *context);
static gboolean do_print_screen_saver_stats (ScreenSaver *screen_saver);
static GdkPixbuf *gamma_correct (const GdkPixbuf *input_pixbuf);

static CachedSource*
cached_source_new (cairo_pattern_t *pattern,
                   gint              width,
                   gint             height)
{
	CachedSource *source;

	source = g_new (CachedSource, 1);
	source->pattern = cairo_pattern_reference (pattern);
	source->width = width;
	source->height = height;

	return source;
}

static void
cached_source_free (CachedSource *source)
{
	if (source == NULL)
		return;

	cairo_pattern_destroy (source->pattern);

	g_free (source);
}

static Path *
path_new (Point *start_point,
          Point *start_control_point,
          Point *end_control_point,
          Point *end_point,
          gdouble duration)
{
	Path *path;

	path = g_new (Path, 1);
	path->start_point = *start_point;
	path->start_control_point = *start_control_point;
	path->end_control_point = *end_control_point;
	path->end_point = *end_point;
	path->duration = duration;

	/* we precompute the coefficients to the cubic bezier curve here
	 * so that we don't have to do it repeatedly later The equation is:
	 *
	 * B(t) = A * t^3 + B * t^2 + C * t + start_point
	 */
	path->x_linear_coefficient = 3 * (start_control_point->x - start_point->x);
	path->x_quadratic_coefficient = 3 * (end_control_point->x -
	                                     start_control_point->x) -
	                                path->x_linear_coefficient;
	path->x_cubic_coefficient = end_point->x - start_point->x -
	                            path->x_linear_coefficient -
	                            path->x_quadratic_coefficient;

	path->y_linear_coefficient = 3 * (start_control_point->y - start_point->y);
	path->y_quadratic_coefficient = 3 * (end_control_point->y -
	                                     start_control_point->y) -
	                                path->y_linear_coefficient;
	path->y_cubic_coefficient = end_point->y - start_point->y -
	                            path->y_linear_coefficient -
	                            path->y_quadratic_coefficient;
	return path;
}

static void
path_free (Path *path)
{
	g_free (path);
}

static ScreenSaverFloater*
screen_saver_floater_new (ScreenSaver *screen_saver,
                          Point       *position,
                          gdouble      scale)
{
	ScreenSaverFloater *floater;

	floater = g_new (ScreenSaverFloater, 1);
	floater->bounds.width = 0;
	floater->start_position = *position;
	floater->position = *position;
	floater->scale = scale;
	floater->opacity = pow (scale, 1.0 / GAMMA);
	floater->path = NULL;
	floater->path_start_time = 0.0;
	floater->path_start_scale = 1.0;
	floater->path_end_scale = 0.0;

	floater->angle = 0.0;
	floater->angle_increment = 0.0;

	return floater;
}

void
screen_saver_floater_free (ScreenSaver        *screen_saver,
                           ScreenSaverFloater *floater)
{
	if (floater == NULL)
		return;

	path_free (floater->path);

	g_free (floater);
}

static gboolean
screen_saver_floater_is_off_canvas (ScreenSaver        *screen_saver,
                                    ScreenSaverFloater *floater)
{
	if ((floater->position.x < screen_saver->canvas_rectangle.top_left_point.x) ||
	        (floater->position.x > screen_saver->canvas_rectangle.bottom_right_point.x) ||
	        (floater->position.y < screen_saver->canvas_rectangle.top_left_point.y) ||
	        (floater->position.y > screen_saver->canvas_rectangle.bottom_right_point.y))
		return TRUE;

	return FALSE;
}

static gboolean
screen_saver_floater_should_come_on_screen (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             performance_ratio,
        gdouble            *duration)
{

	if (!screen_saver_floater_is_off_canvas (screen_saver, floater))
		return FALSE;

	if ((fabs (performance_ratio - .5) >= G_MINDOUBLE) &&
	        (g_random_double () > .5))
	{
		if (duration)
			*duration = g_random_double_range (3.0, 7.0);

		return TRUE;
	}

	return FALSE;
}

static gboolean
screen_saver_floater_should_bubble_up (ScreenSaver        *screen_saver,
                                       ScreenSaverFloater *floater,
                                       gdouble             performance_ratio,
                                       gdouble            *duration)
{

	if ((performance_ratio < .5) && (g_random_double () > .5))
	{
		if (duration)
			*duration = performance_ratio * 30.0;

		return TRUE;
	}

	if ((floater->scale < .3) && (g_random_double () > .6))
	{
		if (duration)
			*duration = 30.0;

		return TRUE;
	}

	return FALSE;
}

static Point
screen_saver_floater_get_position_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time)
{
	Point point;

	time = time / floater->path->duration;

	point.x = floater->path->x_cubic_coefficient * (time * time * time) +
	          floater->path->x_quadratic_coefficient * (time * time) +
	          floater->path->x_linear_coefficient * (time) +
	          floater->path->start_point.x;
	point.y = floater->path->y_cubic_coefficient * (time * time * time) +
	          floater->path->y_quadratic_coefficient * (time * time) +
	          floater->path->y_linear_coefficient * (time) +
	          floater->path->start_point.y;

	return point;
}

static gdouble
screen_saver_floater_get_scale_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time)
{
	gdouble completion_ratio, total_scale_growth, new_scale;

	completion_ratio = time / floater->path->duration;
	total_scale_growth = (floater->path_end_scale - floater->path_start_scale);
	new_scale = floater->path_start_scale + total_scale_growth * completion_ratio;

	return CLAMP (new_scale, 0.0, 1.0);
}

static gdouble
screen_saver_floater_get_angle_from_time (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             time)
{
	gdouble completion_ratio;
	gdouble total_rotation;

	completion_ratio = time / floater->path->duration;
	total_rotation = floater->angle_increment * floater->path->duration;

	return floater->angle + total_rotation * completion_ratio;
}

static Path *
screen_saver_floater_create_path_to_on_screen (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration)
{
	Point start_position, end_position, start_control_point, end_control_point;
	start_position = floater->position;

	end_position.x = g_random_double_range (.25, .75) *
	                 (screen_saver->canvas_rectangle.top_left_point.x +
	                  screen_saver->canvas_rectangle.bottom_right_point.x);
	end_position.y = g_random_double_range (.25, .75) *
	                 (screen_saver->canvas_rectangle.top_left_point.y +
	                  screen_saver->canvas_rectangle.bottom_right_point.y);

	start_control_point.x = start_position.x + .9 * (end_position.x - start_position.x);
	start_control_point.y = start_position.y + .9 * (end_position.y - start_position.y);

	end_control_point.x = start_position.x + 1.0 * (end_position.x - start_position.x);
	end_control_point.y = start_position.y + 1.0 * (end_position.y - start_position.y);

	return path_new (&start_position, &start_control_point, &end_control_point,
	                 &end_position, duration);
}

static Path *
screen_saver_floater_create_path_to_bubble_up (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration)
{
	Point start_position, end_position, start_control_point, end_control_point;

	start_position = floater->position;
	end_position.x = start_position.x;
	end_position.y = screen_saver->canvas_rectangle.top_left_point.y - FLOATER_MAX_SIZE;
	start_control_point.x = .5 * start_position.x;
	start_control_point.y = .5 * start_position.y;
	end_control_point.x = 1.5 * end_position.x;
	end_control_point.y = .5 * end_position.y;

	return path_new (&start_position, &start_control_point, &end_control_point,
	                 &end_position, duration);
}

static Path *
screen_saver_floater_create_path_to_random_point (ScreenSaver        *screen_saver,
        ScreenSaverFloater *floater,
        gdouble             duration)
{
	Point start_position, end_position, start_control_point, end_control_point;

	start_position = floater->position;

	end_position.x = start_position.x +
	                 (g_random_double_range (-.5, .5) * 4 * FLOATER_MAX_SIZE);
	end_position.y = start_position.y +
	                 (g_random_double_range (-.5, .5) * 4 * FLOATER_MAX_SIZE);

	start_control_point.x = start_position.x + .95 * (end_position.x - start_position.x);
	start_control_point.y = start_position.y + .95 * (end_position.y - start_position.y);

	end_control_point.x = start_position.x + 1.0 * (end_position.x - start_position.x);
	end_control_point.y = start_position.y + 1.0 * (end_position.y - start_position.y);

	return path_new (&start_position, &start_control_point, &end_control_point,
	                 &end_position, duration);
}

static Path *
screen_saver_floater_create_path (ScreenSaver        *screen_saver,
                                  ScreenSaverFloater *floater)
{
	gdouble performance_ratio;
	gdouble duration;

	performance_ratio =
	    screen_saver_get_frames_per_second (screen_saver) / OPTIMAL_FRAME_RATE;

	if (fabs (performance_ratio) <= G_MINDOUBLE)
		performance_ratio = 1.0;

	if (screen_saver_floater_should_bubble_up (screen_saver, floater, performance_ratio, &duration))
		return screen_saver_floater_create_path_to_bubble_up (screen_saver, floater, duration);

	if (screen_saver_floater_should_come_on_screen (screen_saver, floater, performance_ratio, &duration))
		return screen_saver_floater_create_path_to_on_screen (screen_saver, floater, duration);

	return screen_saver_floater_create_path_to_random_point (screen_saver, floater,
	        g_random_double_range (3.0, 7.0));
}

static void
screen_saver_floater_update_state (ScreenSaver        *screen_saver,
                                   ScreenSaverFloater *floater,
                                   gdouble             time)
{
	gdouble performance_ratio;

	performance_ratio =
	    screen_saver_get_frames_per_second (screen_saver) / OPTIMAL_FRAME_RATE;

	if (floater->path == NULL)
	{
		floater->path = screen_saver_floater_create_path (screen_saver, floater);
		floater->path_start_time = time;

		floater->path_start_scale = floater->scale;

		if (g_random_double () > .5)
			floater->path_end_scale = g_random_double_range (0.10, performance_ratio);

		/* poor man's distribution */
		if (screen_saver->should_do_rotations &&
		        (g_random_double () < .75 * performance_ratio))
		{
			gint r;

			r = g_random_int_range (0, 100);
			if (r < 80)
				floater->angle_increment = 0.0;
			else if (r < 95)
				floater->angle_increment = g_random_double_range (-SMALL_ANGLE, SMALL_ANGLE);
			else
				floater->angle_increment = g_random_double_range (-BIG_ANGLE, BIG_ANGLE);
		}
	}

	if (time < (floater->path_start_time + floater->path->duration))
	{
		gdouble path_time;

		path_time = time - floater->path_start_time;

		floater->position =
		    screen_saver_floater_get_position_from_time (screen_saver, floater,
		            path_time);
		floater->scale =
		    screen_saver_floater_get_scale_from_time (screen_saver, floater, path_time);

		floater->angle =
		    screen_saver_floater_get_angle_from_time (screen_saver, floater, path_time);

		floater->opacity = pow (floater->scale, 1.0 / GAMMA);
	}
	else
	{
		path_free (floater->path);

		floater->path = NULL;
		floater->path_start_time = 0.0;
	}
}

static GdkPixbuf *
gamma_correct (const GdkPixbuf *input_pixbuf)
{
	gint x, y, width, height, rowstride;
	GdkPixbuf *output_pixbuf;
	guchar *pixels;

	output_pixbuf = gdk_pixbuf_copy (input_pixbuf);
	pixels = gdk_pixbuf_get_pixels (output_pixbuf);

	width = gdk_pixbuf_get_width (output_pixbuf);
	height = gdk_pixbuf_get_height (output_pixbuf);
	rowstride = gdk_pixbuf_get_rowstride (output_pixbuf);

	for (y = 0; y < height; y++)
		for (x = 0; x < width; x++)
		{
			guchar *alpha_channel;
			guchar opacity;

			alpha_channel = pixels + y * (rowstride / 4) + x + 3;
			opacity = (guchar) (255 * pow ((*alpha_channel / 255.0), 1.0 / GAMMA));

			*alpha_channel = opacity;
		}

	return output_pixbuf;
}

static gboolean
screen_saver_floater_do_draw (ScreenSaver        *screen_saver,
                              ScreenSaverFloater *floater,
                              cairo_t            *context)
{
	gint size;
	CachedSource *source;

	size = CLAMP ((int) (FLOATER_MAX_SIZE * floater->scale),
	              FLOATER_MIN_SIZE, FLOATER_MAX_SIZE);

	source = g_hash_table_lookup (screen_saver->cached_sources, GINT_TO_POINTER (size));

	if (source == NULL)
	{
		GdkPixbuf *pixbuf;
		GError *error;

		pixbuf = NULL;
		error = NULL;

		pixbuf = gdk_pixbuf_new_from_file_at_size (screen_saver->filename, size, -1,
		         &error);
		if (pixbuf == NULL)
		{
			g_assert (error != NULL);
			g_printerr ("%s", _(error->message));
			g_error_free (error);
			return FALSE;
		}

		if (gdk_pixbuf_get_has_alpha (pixbuf))
			gamma_correct (pixbuf);

		gdk_cairo_set_source_pixbuf (context, pixbuf, 0.0, 0.0);

		source = cached_source_new (cairo_get_source (context),
		                            gdk_pixbuf_get_width (pixbuf),
		                            gdk_pixbuf_get_height (pixbuf));
		g_object_unref (pixbuf);
		g_hash_table_insert (screen_saver->cached_sources, GINT_TO_POINTER (size),
		                     source);
	}

	cairo_save (context);

	if (screen_saver->should_do_rotations && (fabs (floater->angle) > G_MINDOUBLE))
	{
		floater->bounds.width = G_SQRT2 * source->width + 2;
		floater->bounds.height = G_SQRT2 * source->height + 2;
		floater->bounds.x = (int) (floater->position.x - .5 * G_SQRT2 * source->width) - 1;
		floater->bounds.y = (int) (floater->position.y - .5 * G_SQRT2 * source->height) - 1;

		cairo_translate (context,
		                 trunc (floater->position.x),
		                 trunc (floater->position.y));
		cairo_rotate (context, floater->angle);
		cairo_translate (context,
		                 -trunc (floater->position.x),
		                 -trunc (floater->position.y));
	}
	else
	{
		floater->bounds.width = source->width + 2;
		floater->bounds.height = source->height + 2;
		floater->bounds.x = (int) (floater->position.x - .5 * source->width) - 1;
		floater->bounds.y = (int) (floater->position.y - .5 * source->height) - 1;
	}

	cairo_translate (context,
	                 trunc (floater->position.x - .5 * source->width),
	                 trunc (floater->position.y - .5 * source->height));

	cairo_set_source (context, source->pattern);

	cairo_rectangle (context,
	                 trunc (.5 * (source->width - floater->bounds.width)),
	                 trunc (.5 * (source->height - floater->bounds.height)),
	                 floater->bounds.width, floater->bounds.height);

	cairo_clip (context);
	cairo_paint_with_alpha (context, floater->opacity);
	cairo_restore (context);

	if (screen_saver->should_show_paths && (floater->path != NULL))
	{
		gdouble dash_pattern[] = { 5.0 };

		cairo_save (context);
		cairo_set_source_rgba (context, 1.0, 1.0, 1.0, .2 * floater->opacity);
		cairo_move_to (context,
		               floater->path->start_point.x,
		               floater->path->start_point.y);
		cairo_curve_to (context,
		                floater->path->start_control_point.x,
		                floater->path->start_control_point.y,
		                floater->path->end_control_point.x,
		                floater->path->end_control_point.y,
		                floater->path->end_point.x,
		                floater->path->end_point.y);
		cairo_set_line_cap (context, CAIRO_LINE_CAP_ROUND);
		cairo_stroke (context);
		cairo_set_source_rgba (context, 1.0, 0.0, 0.0, .5 * floater->opacity);
		cairo_rectangle (context,
		                 floater->path->start_point.x - 3,
		                 floater->path->start_point.y - 3,
		                 6, 6);
		cairo_fill (context);
		cairo_set_source_rgba (context, 0.0, 0.5, 0.0, .5 * floater->opacity);
		cairo_arc (context,
		           floater->path->start_control_point.x,
		           floater->path->start_control_point.y,
		           3, 0.0, 2.0 * G_PI);
		cairo_stroke (context);
		cairo_set_source_rgba (context, 0.5, 0.0, 0.5, .5 * floater->opacity);
		cairo_arc (context,
		           floater->path->end_control_point.x,
		           floater->path->end_control_point.y,
		           3, 0.0, 2.0 * G_PI);
		cairo_stroke (context);
		cairo_set_source_rgba (context, 0.0, 0.0, 1.0, .5 * floater->opacity);
		cairo_rectangle (context,
		                 floater->path->end_point.x - 3,
		                 floater->path->end_point.y - 3,
		                 6, 6);
		cairo_fill (context);

		cairo_set_dash (context, dash_pattern, G_N_ELEMENTS (dash_pattern), 0);
		cairo_set_source_rgba (context, .5, .5, .5, .2 * floater->scale);
		cairo_move_to (context, floater->path->start_point.x,
		               floater->path->start_point.y);
		cairo_line_to (context, floater->path->start_control_point.x,
		               floater->path->start_control_point.y);
		cairo_stroke (context);

		cairo_move_to (context, floater->path->end_point.x,
		               floater->path->end_point.y);
		cairo_line_to (context, floater->path->end_control_point.x,
		               floater->path->end_control_point.y);
		cairo_stroke (context);

		cairo_restore (context);
	}

	return TRUE;
}

static ScreenSaver *
screen_saver_new (GtkWidget       *drawing_area,
                  const gchar     *filename,
                  gint             max_floater_count,
                  gboolean         should_do_rotations,
                  gboolean         should_show_paths)
{
	ScreenSaver *screen_saver;

	screen_saver = g_new (ScreenSaver, 1);
	screen_saver->filename = g_strdup (filename);
	screen_saver->drawing_area = drawing_area;
	screen_saver->cached_sources =
	    g_hash_table_new_full (NULL, NULL, NULL,
	                           (GDestroyNotify) cached_source_free);

	g_signal_connect_swapped (G_OBJECT (drawing_area), "size-allocate",
	                          G_CALLBACK (screen_saver_on_size_allocate),
	                          screen_saver);

	g_signal_connect_swapped (G_OBJECT (drawing_area), "draw",
	                          G_CALLBACK (screen_saver_on_draw),
	                          screen_saver);

	screen_saver->first_update_time = 0.0;
	screen_saver->current_calculated_stats_time = 0.0;
	screen_saver->last_calculated_stats_time = 0.0;
	screen_saver->update_count = 0;
	screen_saver->frame_count = 0;
	screen_saver->updates_per_second = 0.0;
	screen_saver->frames_per_second = 0.0;
	screen_saver->floaters = NULL;
	screen_saver->max_floater_count = max_floater_count;

	screen_saver->should_show_paths = should_show_paths;
	screen_saver->should_do_rotations = should_do_rotations;

	screen_saver_get_initial_state (screen_saver);

	screen_saver->state_update_timeout_id =
	    g_timeout_add (1000 / (2.0 * OPTIMAL_FRAME_RATE),
	                   (GSourceFunc) screen_saver_do_update_state, screen_saver);

	screen_saver->stats_update_timeout_id =
	    g_timeout_add (1000, (GSourceFunc) screen_saver_do_update_stats,
	                   screen_saver);

	return screen_saver;
}

static void
screen_saver_free (ScreenSaver *screen_saver)
{
	if (screen_saver == NULL)
		return;

	g_free (screen_saver->filename);

	g_hash_table_destroy (screen_saver->cached_sources);

	if (screen_saver->state_update_timeout_id != 0) {
		g_source_remove (screen_saver->state_update_timeout_id);
		screen_saver->state_update_timeout_id = 0;
	}

	if (screen_saver->stats_update_timeout_id != 0) {
		g_source_remove (screen_saver->stats_update_timeout_id);
		screen_saver->stats_update_timeout_id = 0;
	}

	screen_saver_destroy_floaters (screen_saver);

	g_free (screen_saver);
}

static gdouble
screen_saver_get_timestamp (ScreenSaver *screen_saver)
{
	const gdouble microseconds_per_second = (gdouble ) G_USEC_PER_SEC;
	gdouble timestamp;
	GTimeVal now = { 0L, /* zero-filled */ };

	g_get_current_time (&now);
	timestamp = ((microseconds_per_second * now.tv_sec) + now.tv_usec) /
	            microseconds_per_second;

	return timestamp;
}

static void
screen_saver_create_floaters (ScreenSaver *screen_saver)
{
	gint i;

	for (i = 0; i < screen_saver->max_floater_count; i++)
	{
		ScreenSaverFloater *floater;
		Point position;
		gdouble scale;

		position.x = g_random_double_range (screen_saver->canvas_rectangle.top_left_point.x,
		                                    screen_saver->canvas_rectangle.bottom_right_point.x);
		position.y = g_random_double_range (screen_saver->canvas_rectangle.top_left_point.y,
		                                    screen_saver->canvas_rectangle.bottom_right_point.y);

		scale = g_random_double ();

		floater = screen_saver_floater_new (screen_saver, &position, scale);

		screen_saver->floaters = g_list_prepend (screen_saver->floaters,
		                         floater);
	}
}

static gdouble
screen_saver_get_updates_per_second (ScreenSaver *screen_saver)
{
	return screen_saver->updates_per_second;
}

static gdouble
screen_saver_get_frames_per_second (ScreenSaver *screen_saver)
{
	return screen_saver->frames_per_second;
}

static gdouble
screen_saver_get_image_cache_usage (ScreenSaver *screen_saver)
{
	static const gdouble cache_capacity = (FLOATER_MAX_SIZE - FLOATER_MIN_SIZE + 1);

	return g_hash_table_size (screen_saver->cached_sources) / cache_capacity;
}

static void
screen_saver_destroy_floaters (ScreenSaver *screen_saver)
{
	if (screen_saver->floaters == NULL)
		return;

	g_list_foreach (screen_saver->floaters, (GFunc) screen_saver_floater_free,
	                NULL);
	g_list_free (screen_saver->floaters);

	screen_saver->floaters = NULL;
}

static void
screen_saver_on_size_allocate (ScreenSaver   *screen_saver,
                               GtkAllocation *allocation)
{
	Rectangle canvas_rectangle;

	canvas_rectangle.top_left_point.x = allocation->x - .1 * allocation->width;
	canvas_rectangle.top_left_point.y = allocation->y - .1 * allocation->height;

	canvas_rectangle.bottom_right_point.x = allocation->x + (1.1 * allocation->width);
	canvas_rectangle.bottom_right_point.y = allocation->y + (1.1 * allocation->height);

	screen_saver->canvas_rectangle = canvas_rectangle;
}

static gint
compare_floaters (ScreenSaverFloater *a,
                  ScreenSaverFloater *b)
{
	if (a->scale > b->scale)
		return 1;
	else if (fabs (a->scale - b->scale) <= G_MINDOUBLE)
		return 0;
	else
		return -1;
}

static void
screen_saver_on_draw (ScreenSaver    *screen_saver,
                      cairo_t        *context)
{
	GList *tmp;

	if (screen_saver->floaters == NULL)
		screen_saver_create_floaters (screen_saver);

	screen_saver->floaters = g_list_sort (screen_saver->floaters,
	                                      (GCompareFunc)compare_floaters);

	for (tmp = screen_saver->floaters; tmp != NULL; tmp = tmp->next)
	{
		ScreenSaverFloater *floater;

		floater = (ScreenSaverFloater *) tmp->data;

		if (!screen_saver_floater_do_draw (screen_saver, floater, context))
		{
			gtk_main_quit ();
			break;
		}
	}

	screen_saver->draw_ops_pending = TRUE;
	screen_saver->frame_count++;
}

static void
screen_saver_update_state (ScreenSaver *screen_saver,
                           gdouble      time)
{
	GList *tmp;

	tmp = screen_saver->floaters;
	while (tmp != NULL)
	{
		ScreenSaverFloater *floater;
		floater = (ScreenSaverFloater *) tmp->data;

		screen_saver_floater_update_state (screen_saver, floater, time);

		if (screen_saver->drawing_area != NULL &&
		    gtk_widget_get_realized (screen_saver->drawing_area) &&
		    (floater->bounds.width > 0) && (floater->bounds.height > 0))
		{
			gint size;
			size = CLAMP ((int) (FLOATER_MAX_SIZE * floater->scale),
			              FLOATER_MIN_SIZE, FLOATER_MAX_SIZE);

			gtk_widget_queue_draw_area (screen_saver->drawing_area,
			                            floater->bounds.x,
			                            floater->bounds.y,
			                            floater->bounds.width,
			                            floater->bounds.height);

			/* the edges could concievably be spread across two
			 * pixels so we add +2 to invalidated region
			 */
			if (screen_saver->should_do_rotations)
				gtk_widget_queue_draw_area (screen_saver->drawing_area,
				                            (int) (floater->position.x -
				                                   .5 * G_SQRT2 * size),
				                            (int) (floater->position.y -
				                                   .5 * G_SQRT2 * size),
				                            G_SQRT2 * size + 2,
				                            G_SQRT2 * size + 2);
			else
				gtk_widget_queue_draw_area (screen_saver->drawing_area,
				                            (int) (floater->position.x -
				                                   .5 * size),
				                            (int) (floater->position.y -
				                                   .5 * size),
				                            size + 2, size + 2);

			if  (screen_saver->should_show_paths)
				gtk_widget_queue_draw (screen_saver->drawing_area);
		}

		tmp = tmp->next;
	}
}

static void
screen_saver_get_initial_state (ScreenSaver *screen_saver)
{
	screen_saver->first_update_time = screen_saver_get_timestamp (screen_saver);
	screen_saver_update_state (screen_saver, 0.0);
}

static gboolean
screen_saver_do_update_state (ScreenSaver *screen_saver)
{
	gdouble current_update_time;

	/* flush pending requests to the X server and block for
	 * replies before proceeding to help prevent the X server from
	 * getting overrun with requests
	 */
	if (screen_saver->draw_ops_pending)
	{
		GdkDisplay *display;

		display = gtk_widget_get_display (GTK_WIDGET(screen_saver->drawing_area));
		gdk_display_flush (display);
		screen_saver->draw_ops_pending = FALSE;
	}

	current_update_time = screen_saver_get_timestamp (screen_saver);
	screen_saver_update_state (screen_saver, current_update_time -
	                           screen_saver->first_update_time);
	screen_saver->update_count++;
	return TRUE;
}

static gboolean
screen_saver_do_update_stats (ScreenSaver *screen_saver)
{
	gdouble last_calculated_stats_time, seconds_since_last_stats_update;

	last_calculated_stats_time = screen_saver->current_calculated_stats_time;
	screen_saver->current_calculated_stats_time =
	    screen_saver_get_timestamp (screen_saver);
	screen_saver->last_calculated_stats_time = last_calculated_stats_time;

	if (fabs (last_calculated_stats_time) <= G_MINDOUBLE)
		return TRUE;

	seconds_since_last_stats_update =
	    screen_saver->current_calculated_stats_time - last_calculated_stats_time;

	screen_saver->updates_per_second =
	    screen_saver->update_count / seconds_since_last_stats_update;
	screen_saver->frames_per_second =
	    screen_saver->frame_count / seconds_since_last_stats_update;

	screen_saver->update_count = 0;
	screen_saver->frame_count = 0;

	return TRUE;
}

static gboolean
do_print_screen_saver_stats (ScreenSaver *screen_saver)
{

	g_print ("updates per second: %.2f, frames per second: %.2f, "
	         "image cache %.0f%% full\n",
	         screen_saver_get_updates_per_second (screen_saver),
	         screen_saver_get_frames_per_second (screen_saver),
	         screen_saver_get_image_cache_usage (screen_saver) * 100.0);

	return TRUE;
}

int
main (int   argc,
      char *argv[])
{
	ScreenSaver     *screen_saver;
	GtkWidget       *window;
	GtkWidget       *drawing_area;
	gboolean         should_show_paths = FALSE;
	gboolean         should_do_rotations = FALSE;
	gboolean         should_print_stats = FALSE;
	gint             max_floater_count = FLOATER_DEFAULT_COUNT;
	gchar           *geometry = NULL;
	gchar          **filenames = NULL;
	GError          *error = NULL;

	GOptionEntry options[] = {
	        { "show-paths", 'p', 0, G_OPTION_ARG_NONE, &should_show_paths,
		  N_("Show paths that images follow"), NULL },
		{ "do-rotations", 'r', 0, G_OPTION_ARG_NONE, &should_do_rotations,
		  N_("Occasionally rotate images as they move"), NULL },
		{ "print-stats", 's', 0, G_OPTION_ARG_NONE, &should_print_stats,
		  N_("Print out frame rate and other statistics"), NULL },
		{ "number-of-images", 'n', 0, G_OPTION_ARG_INT, &max_floater_count,
		  N_("The maximum number of images to keep on screen"), N_("MAX_IMAGES") },
		{ "geometry", 0, 0, G_OPTION_ARG_STRING, &geometry,
		  N_("The initial size and position of window"), N_("WIDTHxHEIGHT+X+Y") },
		{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
		  N_("The source image to use"), NULL },
		{NULL}
	};

	bindtextdomain (GETTEXT_PACKAGE, MATELOCALEDIR);
	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
	textdomain (GETTEXT_PACKAGE);

	gtk_init_with_args (&argc, &argv,
	                    /* translators: the word "image" here
	                     * represents a command line argument
	                     */
	                    _("image - floats images around the screen"),
	                    options, GETTEXT_PACKAGE, &error);


	if (error != NULL)
	{
		g_printerr (_("%s. See --help for usage information.\n"),
		            _(error->message));
		g_error_free (error);
		return EX_SOFTWARE;
	}

	if ((filenames == NULL) || (filenames[0] == NULL) ||
	        (filenames[1] != NULL))
	{
		g_printerr (_("You must specify one image.  See --help for usage "
		              "information.\n"));
		return EX_USAGE;
	}

	window = gs_theme_window_new ();

	g_signal_connect (G_OBJECT (window), "delete-event",
	                  G_CALLBACK (gtk_main_quit), NULL);

	drawing_area = GTK_WIDGET (gtk_drawing_area_new ());

	gtk_widget_show (drawing_area);
	gtk_container_add (GTK_CONTAINER (window), drawing_area);

	screen_saver = screen_saver_new (drawing_area,
	                                 filenames[0], max_floater_count,
	                                 should_do_rotations, should_show_paths);
	g_strfreev (filenames);

	if (should_print_stats)
		g_timeout_add (STAT_PRINT_FREQUENCY,
		               (GSourceFunc) do_print_screen_saver_stats,
		               screen_saver);

	if ((geometry == NULL)
	        || !gtk_window_parse_geometry (GTK_WINDOW (window), geometry))
		gtk_window_set_default_size (GTK_WINDOW (window), 640, 480);

	gtk_widget_show (window);

	gtk_main ();

	screen_saver_free (screen_saver);

	return EX_OK;
}
