/* gtktimeline.c
 *
 * Copyright (C) 2007 Carlos Garnacho <carlos@imendio.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <glib.h>
#include <gdk/gdk.h>
#include "gtksettings.h"
#include "gtktypeutils.h"
#include "gtktimeline.h"
#include "gtkenums.h"
#include "gtkalias.h"
#include "gtkprivate.h"

#define GTK_TIMELINE_GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GTK_TYPE_TIMELINE, GtkTimelinePriv))
#define MSECS_PER_SEC 1000
#define FRAME_INTERVAL(nframes) (MSECS_PER_SEC / nframes)
#define DEFAULT_FPS 40

typedef struct GtkTimelinePriv GtkTimelinePriv;

struct GtkTimelinePriv
{
  guint n_frames;
  guint fps;
  guint cur_frame;
  guint source_id;

  GdkScreen *screen;
  GtkTimelineProgressFunc progress_func;

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

enum {
  PROP_0,
  PROP_FPS,
  PROP_N_FRAMES,
  PROP_LOOP,
  PROP_DIRECTION,
  PROP_SCREEN,
};

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

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


static void  gtk_timeline_set_property  (GObject         *object,
					 guint            prop_id,
					 const GValue    *value,
					 GParamSpec      *pspec);
static void  gtk_timeline_get_property  (GObject         *object,
					 guint            prop_id,
					 GValue          *value,
					 GParamSpec      *pspec);
static void  gtk_timeline_finalize      (GObject *object);


G_DEFINE_TYPE (GtkTimeline, gtk_timeline, G_TYPE_OBJECT)


static void
gtk_timeline_class_init (GtkTimelineClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->set_property = gtk_timeline_set_property;
  object_class->get_property = gtk_timeline_get_property;
  object_class->finalize = gtk_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,
						      GTK_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_N_FRAMES,
				   g_param_spec_uint ("n-frames",
						      "Number of frames",
						      "Number of frames for the timeline",
						      1,
						      G_MAXUINT,
						      1,
						      GTK_PARAM_READWRITE));
  g_object_class_install_property (object_class,
				   PROP_LOOP,
				   g_param_spec_boolean ("loop",
							 "Loop",
							 "Whether the timeline loops or not",
							 FALSE,
							 GTK_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",
						      GTK_TYPE_TIMELINE_DIRECTION,
						      GTK_TIMELINE_DIRECTION_FORWARD,
						      GTK_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,
							GTK_PARAM_READWRITE));

  signals[STARTED] =
    g_signal_new ("started",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GtkTimelineClass, 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 (GtkTimelineClass, 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 (GtkTimelineClass, 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 (GtkTimelineClass, frame),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__DOUBLE,
		  G_TYPE_NONE, 1,
		  G_TYPE_DOUBLE);

  signals[DIRECTION_CHANGED] =
    g_signal_new ("direction-changed",
		  G_TYPE_FROM_CLASS (object_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (GtkTimelineClass, direction_changed),
		  NULL, NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);

  g_type_class_add_private (class, sizeof (GtkTimelinePriv));
}

static void
gtk_timeline_init (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  priv->fps = DEFAULT_FPS;
  priv->n_frames = 1;
  priv->direction = GTK_TIMELINE_DIRECTION_FORWARD;
  priv->screen = gdk_screen_get_default ();
}

static void
gtk_timeline_set_property (GObject      *object,
			   guint         prop_id,
			   const GValue *value,
			   GParamSpec   *pspec)
{
  GtkTimeline *timeline;
  GtkTimelinePriv *priv;

  timeline = GTK_TIMELINE (object);
  priv = GTK_TIMELINE_GET_PRIV (timeline);

  switch (prop_id)
    {
    case PROP_FPS:
      gtk_timeline_set_fps (timeline, g_value_get_uint (value));
      break;
    case PROP_N_FRAMES:
      gtk_timeline_set_n_frames (timeline, g_value_get_uint (value));
      break;
    case PROP_LOOP:
      gtk_timeline_set_loop (timeline, g_value_get_boolean (value));
      break;
    case PROP_SCREEN:
      gtk_timeline_set_screen (timeline,
			       GDK_SCREEN (g_value_get_object (value)));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gtk_timeline_get_property (GObject    *object,
			   guint       prop_id,
			   GValue     *value,
			   GParamSpec *pspec)
{
  GtkTimeline *timeline;
  GtkTimelinePriv *priv;

  timeline = GTK_TIMELINE (object);
  priv = GTK_TIMELINE_GET_PRIV (timeline);

  switch (prop_id)
    {
    case PROP_FPS:
      g_value_set_uint (value, priv->fps);
      break;
    case PROP_N_FRAMES:
      g_value_set_uint (value, priv->n_frames);
      break;
    case PROP_LOOP:
      g_value_set_boolean (value, priv->loop);
      break;
    case PROP_SCREEN:
      g_value_set_object (value, priv->screen);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gtk_timeline_finalize (GObject *object)
{
  GtkTimelinePriv *priv;

  priv = GTK_TIMELINE_GET_PRIV (object);

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

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

static gboolean
gtk_timeline_run_frame (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;
  gdouble progress;

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  if (priv->progress_func)
    {
      progress = (priv->progress_func) ((gdouble) priv->cur_frame / priv->n_frames);
      progress = CLAMP (progress, 0.0, 1.0);
    }
  else
    progress = (gdouble) priv->cur_frame / priv->n_frames;

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

  if (!priv->loop &&
      ((priv->direction == GTK_TIMELINE_DIRECTION_FORWARD &&
	priv->cur_frame >= priv->n_frames) ||
       (priv->direction == GTK_TIMELINE_DIRECTION_BACKWARD &&
	priv->cur_frame <= 0)))
    {
      if (priv->source_id)
	{
	  g_source_remove (priv->source_id);
	  priv->source_id = 0;
	}

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

  return TRUE;
}

static gboolean
gtk_timeline_idle_func (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  if (priv->direction == GTK_TIMELINE_DIRECTION_FORWARD)
    {
      if (priv->cur_frame >= priv->n_frames)
	gtk_timeline_rewind (timeline);
      else
	priv->cur_frame++;
    }
  else
    {
      if (priv->cur_frame <= 0)
	gtk_timeline_rewind (timeline);
      else
	priv->cur_frame--;
    }

  return gtk_timeline_run_frame (timeline);
}

/**
 * gtk_timeline_new:
 * @n_frames: number of frames for the timeline
 *
 * Creates a new #GtkTimeline with the specified number of frames.
 *
 * Return Value: the newly created #GtkTimeline
 **/
GtkTimeline *
gtk_timeline_new (guint n_frames)
{
  return g_object_new (GTK_TYPE_TIMELINE,
		       "n-frames", n_frames,
		       NULL);
}

GtkTimeline *
gtk_timeline_new_for_screen (guint      n_frames,
			     GdkScreen *screen)
{
  return g_object_new (GTK_TYPE_TIMELINE,
		       "n-frames", n_frames,
		       "screen", screen,
		       NULL);
}

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

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

  priv = GTK_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)
	{
	  /* 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) gtk_timeline_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.
       */
      priv->cur_frame =
	(priv->direction == GTK_TIMELINE_DIRECTION_FORWARD) ? priv->n_frames : 0;

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

/**
 * gtk_timeline_pause:
 * @timeline: A #GtkTimeline
 *
 * Pauses the timeline.
 **/
void
gtk_timeline_pause (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

  priv = GTK_TIMELINE_GET_PRIV (timeline);

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

/**
 * gtk_timeline_rewind:
 * @timeline: A #GtkTimeline
 *
 * Rewinds the timeline.
 **/
void
gtk_timeline_rewind (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  priv->cur_frame =
    (priv->direction == GTK_TIMELINE_DIRECTION_FORWARD) ? 0 : priv->n_frames;
}

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

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), FALSE);

  priv = GTK_TIMELINE_GET_PRIV (timeline);

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

/**
 * gtk_timeline_get_fps:
 * @timeline: A #GtkTimeline
 *
 * Returns the number of frames per second.
 *
 * Return Value: frames per second
 **/
guint
gtk_timeline_get_fps (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), 1);

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

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

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

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  priv->fps = fps;

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

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

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

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), FALSE);

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

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

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

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

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

/**
 * gtk_timeline_get_n_frames:
 * @timeline: A #GtkTimeline
 *
 * Returns the number of frames of the timeline.
 *
 * Return Value: number of frames.
 **/
guint
gtk_timeline_get_n_frames (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), 0);

  priv = GTK_TIMELINE_GET_PRIV (timeline);

  return priv->n_frames;
}

/**
 * gtk_timeline_set_n_frames:
 * @timeline: A #GtkTimeline
 * @n_frames: number of frames
 *
 * Sets the number of frames of the timeline.
 **/
void
gtk_timeline_set_n_frames (GtkTimeline *timeline,
			   guint        n_frames)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));
  g_return_if_fail (n_frames > 0);

  priv = GTK_TIMELINE_GET_PRIV (timeline);
  priv->n_frames = n_frames;

  g_object_notify (G_OBJECT (timeline), "n-frames");
}

/**
 * gtk_timeline_get_direction:
 * @timeline: A #GtkTimeline
 *
 * Returns the direction of the timeline.
 *
 * Return Value: direction
 **/
GtkTimelineDirection
gtk_timeline_get_direction (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), GTK_TIMELINE_DIRECTION_FORWARD);

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

/**
 * gtk_timeline_set_direction:
 * @timeline: A #GtkTimeline
 * @direction: direction
 *
 * Sets the direction of the timeline.
 **/
void
gtk_timeline_set_direction (GtkTimeline          *timeline,
			    GtkTimelineDirection  direction)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

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

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

GdkScreen *
gtk_timeline_get_screen (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), NULL);

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

void
gtk_timeline_set_screen (GtkTimeline *timeline,
			 GdkScreen   *screen)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));
  g_return_if_fail (GDK_IS_SCREEN (screen));

  priv = GTK_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");
}

/**
 * gtk_timeline_set_progress_func:
 * @timeline: A #GtkTimeline
 * @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
gtk_timeline_set_progress_func (GtkTimeline             *timeline,
				GtkTimelineProgressFunc  progress_func)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

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

#define __GTK_TIMELINE_C__
#include "gtkaliasdef.c"
