/*
 * Copyright (C) 2005 Ray Strode <rstrode@redhat.com>,
 *                    Matthias Clasen <mclasen@redhat.com>,
 *                    Søren Sandmann <sandmann@redhat.com>
 * Copyright (C) 2018 Sean Davis <bluesabre@xfce.org>
 *
 * 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; see the file COPYING.LGPL.  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>
 *                         Sean Davis <bluesabre@xfce.org>
 */

#include <config.h>

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

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

#include <libxfce4util/libxfce4util.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

static gboolean should_show_paths = FALSE;
static gboolean should_do_rotations = FALSE;
static gboolean should_print_stats = FALSE;
static gint max_floater_count = FLOATER_DEFAULT_COUNT;
static gchar *geometry = NULL;
static gchar **filenames = NULL;

static 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}
};


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;

    gboolean should_do_rotations: 1;
    gboolean should_show_paths : 1;
    gboolean 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             local_max_floater_count,
                                      gboolean         local_should_do_rotations,
                                      gboolean         local_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 (gpointer user_data);
static gboolean screen_saver_do_update_stats (gpointer user_data);
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 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         local_max_floater_count,
                  gboolean     local_should_do_rotations,
                  gboolean     local_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 = local_max_floater_count;

    screen_saver->should_show_paths = local_should_show_paths;
    screen_saver->should_do_rotations = local_should_do_rotations;
    screen_saver->draw_ops_pending = TRUE;

    screen_saver_get_initial_state (screen_saver);

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

    screen_saver->stats_update_timeout_id =
        g_timeout_add (1000, 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;
    GDateTime *now;
    gdouble now_sec;
    gint now_usec;

    now = g_date_time_new_now_local ();
    now_sec = g_date_time_to_unix (now);
    now_usec = g_date_time_get_microsecond (now);

    g_date_time_unref (now);

    timestamp = ((microseconds_per_second * now_sec) + now_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 (gpointer user_data) {
    ScreenSaver *screen_saver = user_data;
    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 (gpointer user_data) {
    ScreenSaver *screen_saver = user_data;
    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 (gpointer user_data) {
    ScreenSaver *screen_saver = user_data;

    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;
    GError          *error;
    gboolean         success;

    error = NULL;

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

    success = 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 (!success) {
        g_printerr (_("Failed to initialize the windowing system."));
        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), "destroy",
                      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, do_print_screen_saver_stats, screen_saver);

    G_GNUC_BEGIN_IGNORE_DEPRECATIONS /* GTK 3.20 */
    if ((geometry == NULL)
            || !gtk_window_parse_geometry (GTK_WINDOW (window), geometry))
        gtk_window_set_default_size (GTK_WINDOW (window), 640, 480);
    G_GNUC_END_IGNORE_DEPRECATIONS

    gtk_widget_show (window);

    gtk_main ();

    screen_saver_free (screen_saver);

    return EX_OK;
}
