/* gsd-timeline.c
 *
 * Copyright (C) 2008 Carlos Garnacho  <carlos@imendio.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include <glib.h>
#include <gtk/gtk.h>
#include <math.h>
#include "gsd-timeline.h"

#define GSD_TIMELINE_GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GSD_TYPE_TIMELINE, GsdTimelinePriv))
#define MSECS_PER_SEC 1000
#define FRAME_INTERVAL(nframes) (MSECS_PER_SEC / nframes)
#define DEFAULT_FPS 30

typedef struct GsdTimelinePriv GsdTimelinePriv;

struct GsdTimelinePriv
{
  guint duration;
  guint fps;
  guint source_id;

  GTimer *timer;

  GdkScreen *screen;
  GsdTimelineProgressType progress_type;
  GsdTimelineProgressFunc progress_func;

  guint loop      : 1;
  guint direction : 1;
};

enum {
  PROP_0,
  PROP_FPS,
  PROP_DURATION,
  PROP_LOOP,
  PROP_DIRECTION,
  PROP_SCREEN,
  PROP_PROGRESS_TYPE,
};

enum {
  STARTED,
  PAUSED,
  FINISHED,
  FRAME,
  LAST_SIGNAL
};

static guint signals [LAST_SIGNAL] = { 0, };


static void  gsd_timeline_set_property  (GObject         *object,
					 guint            prop_id,
					 const GValue    *value,
					 GParamSpec      *pspec);
static void  gsd_timeline_get_property  (GObject         *object,
					 guint            prop_id,
					 GValue          *value,
					 GParamSpec      *pspec);
static void  gsd_timeline_finalize      (GObject *object);


G_DEFINE_TYPE (GsdTimeline, gsd_timeline, G_TYPE_OBJECT)


GType
gsd_timeline_direction_get_type (void)
{
  static GType type = 0;

  if (G_UNLIKELY (type == 0))
    {
      static const GEnumValue values[] = {
	{ GSD_TIMELINE_DIRECTION_FORWARD,  "GSD_TIMELINE_DIRECTION_FORWARD",  "forward" },
	{ GSD_TIMELINE_DIRECTION_BACKWARD, "GSD_TIMELINE_DIRECTION_BACKWARD", "backward" },
	{ 0, NULL, NULL }
      };

      type = g_enum_register_static (g_intern_static_string ("GsdTimelineDirection"), values);
    }

  return type;
}

GType
gsd_timeline_progress_type_get_type (void)
{
  static GType type = 0;

  if (G_UNLIKELY (type == 0))
    {
      static const GEnumValue values[] = {
	{ GSD_TIMELINE_PROGRESS_LINEAR,      "GSD_TIMELINE_PROGRESS_LINEAR",      "linear" },
	{ GSD_TIMELINE_PROGRESS_SINUSOIDAL,  "GSD_TIMELINE_PROGRESS_SINUSOIDAL",  "sinusoidal" },
	{ GSD_TIMELINE_PROGRESS_EXPONENTIAL, "GSD_TIMELINE_PROGRESS_EXPONENTIAL", "exponential" },
	{ 0, NULL, NULL }
      };

      type = g_enum_register_static (g_intern_static_string ("GsdTimelineProgressType"), values);
    }

  return type;
}

static void
gsd_timeline_class_init (GsdTimelineClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->set_property = gsd_timeline_set_property;
  object_class->get_property = gsd_timeline_get_property;
  object_class->finalize = gsd_timeline_finalize;

  g_object_class_install_property (object_class,
				   PROP_FPS,
				   g_param_spec_uint ("fps",
						      "FPS",
						      "Frames per second for the timeline",
						      1,
						      G_MAXUINT,
						      DEFAULT_FPS,
						      G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_DURATION,
				   g_param_spec_uint ("duration",
						      "Animation Duration",
						      "Animation Duration",
						      0,
						      G_MAXUINT,
						      0,
						      G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_LOOP,
				   g_param_spec_boolean ("loop",
							 "Loop",
							 "Whether the timeline loops or not",
							 FALSE,
							 G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_DIRECTION,
				   g_param_spec_enum ("direction",
						      "Direction",
						      "Whether the timeline moves forward or backward in time",
						      GSD_TYPE_TIMELINE_DIRECTION,
						      GSD_TIMELINE_DIRECTION_FORWARD,
						      G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_DIRECTION,
				   g_param_spec_enum ("progress-type",
						      "Progress type",
						      "Type of progress through the timeline",
						      GSD_TYPE_TIMELINE_PROGRESS_TYPE,
						      GSD_TIMELINE_PROGRESS_LINEAR,
						      G_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_SCREEN,
				   g_param_spec_object ("screen",
							"Screen",
							"Screen to get the settings from",
							GDK_TYPE_SCREEN,
							G_PARAM_READWRITE));

  signals[STARTED] =
    g_signal_new ("started",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GsdTimelineClass, started),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);

  signals[PAUSED] =
    g_signal_new ("paused",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GsdTimelineClass, paused),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);

  signals[FINISHED] =
    g_signal_new ("finished",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GsdTimelineClass, finished),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);

  signals[FRAME] =
    g_signal_new ("frame",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GsdTimelineClass, frame),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__DOUBLE,
		  G_TYPE_NONE, 1,
		  G_TYPE_DOUBLE);

  g_type_class_add_private (class, sizeof (GsdTimelinePriv));
}

static void
gsd_timeline_init (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  priv->fps = DEFAULT_FPS;
  priv->duration = 0;
  priv->direction = GSD_TIMELINE_DIRECTION_FORWARD;
  priv->screen = gdk_screen_get_default ();
}

static void
gsd_timeline_set_property (GObject      *object,
			   guint         prop_id,
			   const GValue *value,
			   GParamSpec   *pspec)
{
  GsdTimeline *timeline;

  timeline = GSD_TIMELINE (object);

  switch (prop_id)
    {
    case PROP_FPS:
      gsd_timeline_set_fps (timeline, g_value_get_uint (value));
      break;
    case PROP_DURATION:
      gsd_timeline_set_duration (timeline, g_value_get_uint (value));
      break;
    case PROP_LOOP:
      gsd_timeline_set_loop (timeline, g_value_get_boolean (value));
      break;
    case PROP_DIRECTION:
      gsd_timeline_set_direction (timeline, g_value_get_enum (value));
      break;
    case PROP_SCREEN:
      gsd_timeline_set_screen (timeline,
			       GDK_SCREEN (g_value_get_object (value)));
      break;
    case PROP_PROGRESS_TYPE:
      gsd_timeline_set_progress_type (timeline, g_value_get_enum (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gsd_timeline_get_property (GObject    *object,
			   guint       prop_id,
			   GValue     *value,
			   GParamSpec *pspec)
{
  GsdTimeline *timeline;
  GsdTimelinePriv *priv;

  timeline = GSD_TIMELINE (object);
  priv = GSD_TIMELINE_GET_PRIV (timeline);

  switch (prop_id)
    {
    case PROP_FPS:
      g_value_set_uint (value, priv->fps);
      break;
    case PROP_DURATION:
      g_value_set_uint (value, priv->duration);
      break;
    case PROP_LOOP:
      g_value_set_boolean (value, priv->loop);
      break;
    case PROP_DIRECTION:
      g_value_set_enum (value, priv->direction);
      break;
    case PROP_SCREEN:
      g_value_set_object (value, priv->screen);
      break;
    case PROP_PROGRESS_TYPE:
      g_value_set_enum (value, priv->progress_type);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gsd_timeline_finalize (GObject *object)
{
  GsdTimelinePriv *priv;

  priv = GSD_TIMELINE_GET_PRIV (object);

  if (priv->source_id)
    {
      g_source_remove (priv->source_id);
      priv->source_id = 0;
    }

  if (priv->timer)
      g_timer_destroy (priv->timer);

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

/* Sinusoidal progress */
static gdouble
sinusoidal_progress (gdouble progress)
{
  return (sinf ((progress * G_PI) / 2));
}

static gdouble
exponential_progress (gdouble progress)
{
  return progress * progress;
}

static GsdTimelineProgressFunc
progress_type_to_func (GsdTimelineProgressType type)
{
  if (type == GSD_TIMELINE_PROGRESS_SINUSOIDAL)
    return sinusoidal_progress;
  else if (type == GSD_TIMELINE_PROGRESS_EXPONENTIAL)
    return exponential_progress;

  return NULL;
}

static gboolean
gsd_timeline_run_frame (GsdTimeline *timeline,
			gboolean     enable_animations)
{
  GsdTimelinePriv *priv;
  gdouble linear_progress, progress;
  guint elapsed_time;
  GsdTimelineProgressFunc progress_func = NULL;

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (enable_animations)
    {
      elapsed_time = (guint) (g_timer_elapsed (priv->timer, NULL) * 1000);

      linear_progress = (gdouble) elapsed_time / priv->duration;

      if (priv->direction == GSD_TIMELINE_DIRECTION_BACKWARD)
	linear_progress = 1 - linear_progress;

      linear_progress = CLAMP (linear_progress, 0., 1.);

      if (priv->progress_func)
	progress_func = priv->progress_func;
      else if (priv->progress_type)
	progress_func = progress_type_to_func (priv->progress_type);

      if (progress_func)
	progress = (progress_func) (linear_progress);
      else
	progress = linear_progress;
    }
  else
    progress = (priv->direction == GSD_TIMELINE_DIRECTION_FORWARD) ? 1.0 : 0.0;

  g_signal_emit (timeline, signals [FRAME], 0,
		 CLAMP (progress, 0.0, 1.0));

  if ((priv->direction == GSD_TIMELINE_DIRECTION_FORWARD && progress >= 1.0) ||
      (priv->direction == GSD_TIMELINE_DIRECTION_BACKWARD && progress <= 0.0))
    {
      if (!priv->loop)
	{
	  if (priv->source_id)
	    {
	      g_source_remove (priv->source_id);
	      priv->source_id = 0;
	    }

	  g_signal_emit (timeline, signals [FINISHED], 0);
	  return FALSE;
	}
      else
	gsd_timeline_rewind (timeline);
    }

  return TRUE;
}

static gboolean
gsd_timeline_frame_idle_func (GsdTimeline *timeline)
{
  return gsd_timeline_run_frame (timeline, TRUE);
}

/**
 * gsd_timeline_new:
 * @duration: duration in milliseconds for the timeline
 *
 * Creates a new #GsdTimeline with the specified number of frames.
 *
 * Return Value: the newly created #GsdTimeline
 **/
GsdTimeline *
gsd_timeline_new (guint duration)
{
  return g_object_new (GSD_TYPE_TIMELINE,
		       "duration", duration,
		       NULL);
}

GsdTimeline *
gsd_timeline_new_for_screen (guint      duration,
			     GdkScreen *screen)
{
  return g_object_new (GSD_TYPE_TIMELINE,
		       "duration", duration,
		       "screen", screen,
		       NULL);
}

/**
 * gsd_timeline_start:
 * @timeline: A #GsdTimeline
 *
 * Runs the timeline from the current frame.
 **/
void
gsd_timeline_start (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;
  GtkSettings *settings;
  gboolean enable_animations = FALSE;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (priv->screen)
    {
      settings = gtk_settings_get_for_screen (priv->screen);
      g_object_get (settings, "gtk-enable-animations", &enable_animations, NULL);
    }

  if (enable_animations)
    {
      if (!priv->source_id)
	{
	  if (priv->timer)
	    g_timer_continue (priv->timer);
	  else
	    priv->timer = g_timer_new ();

	  /* sanity check */
	  g_assert (priv->fps > 0);

	  g_signal_emit (timeline, signals [STARTED], 0);

	  priv->source_id = gdk_threads_add_timeout (FRAME_INTERVAL (priv->fps),
						     (GSourceFunc) gsd_timeline_frame_idle_func,
						     timeline);
	}
    }
  else
    {
      /* If animations are not enabled, only run the last frame,
       * it take us instantaneously to the last state of the animation.
       * The only potential flaw happens when people use the ::finished
       * signal to trigger another animation, or even worse, finally
       * loop into this animation again.
       */
      g_signal_emit (timeline, signals [STARTED], 0);
      gsd_timeline_run_frame (timeline, FALSE);
    }
}

/**
 * gsd_timeline_pause:
 * @timeline: A #GsdTimeline
 *
 * Pauses the timeline.
 **/
void
gsd_timeline_pause (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (priv->source_id)
    {
      g_source_remove (priv->source_id);
      priv->source_id = 0;
      g_timer_stop (priv->timer);
      g_signal_emit (timeline, signals [PAUSED], 0);
    }
}

/**
 * gsd_timeline_rewind:
 * @timeline: A #GsdTimeline
 *
 * Rewinds the timeline.
 **/
void
gsd_timeline_rewind (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  /* destroy and re-create timer if neccesary  */
  if (priv->timer)
    {
      g_timer_destroy (priv->timer);

      if (gsd_timeline_is_running (timeline))
	priv->timer = g_timer_new ();
      else
	priv->timer = NULL;
    }
}

/**
 * gsd_timeline_is_running:
 * @timeline: A #GsdTimeline
 *
 * Returns whether the timeline is running or not.
 *
 * Return Value: %TRUE if the timeline is running
 **/
gboolean
gsd_timeline_is_running (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), FALSE);

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  return (priv->source_id != 0);
}

/**
 * gsd_timeline_get_fps:
 * @timeline: A #GsdTimeline
 *
 * Returns the number of frames per second.
 *
 * Return Value: frames per second
 **/
guint
gsd_timeline_get_fps (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), 1);

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  return priv->fps;
}

/**
 * gsd_timeline_set_fps:
 * @timeline: A #GsdTimeline
 * @fps: frames per second
 *
 * Sets the number of frames per second that
 * the timeline will play.
 **/
void
gsd_timeline_set_fps (GsdTimeline *timeline,
		      guint        fps)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));
  g_return_if_fail (fps > 0);

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  priv->fps = fps;

  if (gsd_timeline_is_running (timeline))
    {
      g_source_remove (priv->source_id);
      priv->source_id = gdk_threads_add_timeout (FRAME_INTERVAL (priv->fps),
						 (GSourceFunc) gsd_timeline_run_frame,
						 timeline);
    }

  g_object_notify (G_OBJECT (timeline), "fps");
}

/**
 * gsd_timeline_get_loop:
 * @timeline: A #GsdTimeline
 *
 * Returns whether the timeline loops to the
 * beginning when it has reached the end.
 *
 * Return Value: %TRUE if the timeline loops
 **/
gboolean
gsd_timeline_get_loop (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), FALSE);

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  return priv->loop;
}

/**
 * gsd_timeline_set_loop:
 * @timeline: A #GsdTimeline
 * @loop: %TRUE to make the timeline loop
 *
 * Sets whether the timeline loops to the beginning
 * when it has reached the end.
 **/
void
gsd_timeline_set_loop (GsdTimeline *timeline,
		       gboolean     loop)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  priv->loop = loop;

  g_object_notify (G_OBJECT (timeline), "loop");
}

void
gsd_timeline_set_duration (GsdTimeline *timeline,
			   guint        duration)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  priv->duration = duration;

  g_object_notify (G_OBJECT (timeline), "duration");
}

guint
gsd_timeline_get_duration (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), 0);

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  return priv->duration;
}

/**
 * gsd_timeline_get_direction:
 * @timeline: A #GsdTimeline
 *
 * Returns the direction of the timeline.
 *
 * Return Value: direction
 **/
GsdTimelineDirection
gsd_timeline_get_direction (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), GSD_TIMELINE_DIRECTION_FORWARD);

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  return priv->direction;
}

/**
 * gsd_timeline_set_direction:
 * @timeline: A #GsdTimeline
 * @direction: direction
 *
 * Sets the direction of the timeline.
 **/
void
gsd_timeline_set_direction (GsdTimeline          *timeline,
			    GsdTimelineDirection  direction)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  priv->direction = direction;

  g_object_notify (G_OBJECT (timeline), "direction");
}

GdkScreen *
gsd_timeline_get_screen (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), NULL);

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  return priv->screen;
}

void
gsd_timeline_set_screen (GsdTimeline *timeline,
			 GdkScreen   *screen)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));
  g_return_if_fail (GDK_IS_SCREEN (screen));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (priv->screen)
    g_object_unref (priv->screen);

  priv->screen = g_object_ref (screen);

  g_object_notify (G_OBJECT (timeline), "screen");
}

void
gsd_timeline_set_progress_type (GsdTimeline             *timeline,
				GsdTimelineProgressType  type)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  priv->progress_type = type;

  g_object_notify (G_OBJECT (timeline), "progress-type");
}

GsdTimelineProgressType
gsd_timeline_get_progress_type (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), GSD_TIMELINE_PROGRESS_LINEAR);

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (priv->progress_func)
    return GSD_TIMELINE_PROGRESS_LINEAR;

  return priv->progress_type;
}

/**
 * gsd_timeline_set_progress_func:
 * @timeline: A #GsdTimeline
 * @progress_func: progress function
 *
 * Sets the progress function. This function will be used to calculate
 * a different progress to pass to the ::frame signal based on the
 * linear progress through the timeline. Setting progress_func
 * to %NULL will make the timeline use the default function,
 * which is just a linear progress.
 *
 * All progresses are in the [0.0, 1.0] range.
 **/
void
gsd_timeline_set_progress_func (GsdTimeline             *timeline,
				GsdTimelineProgressFunc  progress_func)
{
  GsdTimelinePriv *priv;

  g_return_if_fail (GSD_IS_TIMELINE (timeline));

  priv = GSD_TIMELINE_GET_PRIV (timeline);
  priv->progress_func = progress_func;
}

gdouble
gsd_timeline_get_progress (GsdTimeline *timeline)
{
  GsdTimelinePriv *priv;
  GsdTimelineProgressFunc progress_func = NULL;
  gdouble linear_progress, progress;
  guint elapsed_time;

  g_return_val_if_fail (GSD_IS_TIMELINE (timeline), 0.0);

  priv = GSD_TIMELINE_GET_PRIV (timeline);

  if (!priv->timer)
    return 0.;

  elapsed_time = (guint) (g_timer_elapsed (priv->timer, NULL) * 1000);

  linear_progress = (gdouble) elapsed_time / priv->duration;

  if (priv->direction == GSD_TIMELINE_DIRECTION_BACKWARD)
    linear_progress = 1 - linear_progress;

  linear_progress = CLAMP (linear_progress, 0., 1.);

  if (priv->progress_func)
    progress_func = priv->progress_func;
  else if (priv->progress_type)
    progress_func = progress_type_to_func (priv->progress_type);

  if (progress_func)
    progress = (progress_func) (linear_progress);
  else
    progress = linear_progress;

  return CLAMP (progress, 0., 1.);
}
