/* GTK - The GIMP Toolkit
 * 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 <gtk/gtktimeline.h>
#include <gtk/gtktypebuiltins.h>
#include <gtk/gtksettings.h>
#include <math.h>

#define MSECS_PER_SEC 1000
#define FRAME_INTERVAL(nframes) (MSECS_PER_SEC / nframes)
#define DEFAULT_FPS 30

typedef struct GtkTimelinePriv GtkTimelinePriv;

struct GtkTimelinePriv
{
  guint duration;
  guint fps;
  guint source_id;

  GTimer *timer;

  gdouble progress;
  gdouble last_progress;

  GdkScreen *screen;

  GtkTimelineProgressType progress_type;

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

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

enum {
  STARTED,
  PAUSED,
  FINISHED,
  FRAME,
  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 *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  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,
                                                      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_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 (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);

  g_type_class_add_private (klass, sizeof (GtkTimelinePriv));
}

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

  priv = timeline->priv = G_TYPE_INSTANCE_GET_PRIVATE (timeline,
                                                       GTK_TYPE_TIMELINE,
                                                       GtkTimelinePriv);

  priv->fps = DEFAULT_FPS;
  priv->duration = 0.0;
  priv->direction = GTK_TIMELINE_DIRECTION_FORWARD;
  priv->screen = gdk_screen_get_default ();

  priv->last_progress = 0;
}

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

  timeline = GTK_TIMELINE (object);

  switch (prop_id)
    {
    case PROP_FPS:
      _gtk_timeline_set_fps (timeline, g_value_get_uint (value));
      break;
    case PROP_DURATION:
      _gtk_timeline_set_duration (timeline, g_value_get_uint (value));
      break;
    case PROP_LOOP:
      _gtk_timeline_set_loop (timeline, g_value_get_boolean (value));
      break;
    case PROP_DIRECTION:
      _gtk_timeline_set_direction (timeline, g_value_get_enum (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 = timeline->priv;

  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;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
_gtk_timeline_finalize (GObject *object)
{
  GtkTimelinePriv *priv;
  GtkTimeline *timeline;

  timeline = (GtkTimeline *) object;
  priv = timeline->priv;

  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 (_gtk_timeline_parent_class)->finalize (object);
}

static gdouble
calculate_progress (gdouble                 linear_progress,
                    GtkTimelineProgressType progress_type)
{
  gdouble progress;

  progress = linear_progress;

  switch (progress_type)
    {
    case GTK_TIMELINE_PROGRESS_LINEAR:
      break;
    case GTK_TIMELINE_PROGRESS_EASE_IN_OUT:
      progress *= 2;

      if (progress < 1)
        progress = pow (progress, 3) / 2;
      else
        progress = (pow (progress - 2, 3) + 2) / 2;

      break;
    case GTK_TIMELINE_PROGRESS_EASE:
      progress = (sin ((progress - 0.5) * G_PI) + 1) / 2;
      break;
    case GTK_TIMELINE_PROGRESS_EASE_IN:
      progress = pow (progress, 3);
      break;
    case GTK_TIMELINE_PROGRESS_EASE_OUT:
      progress = pow (progress - 1, 3) + 1;
      break;
    default:
      g_warning ("Timeline progress type not implemented");
    }

  return progress;
}

static gboolean
gtk_timeline_run_frame (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;
  gdouble delta_progress, progress;
  guint elapsed_time;

  priv = timeline->priv;

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

  if (priv->animations_enabled)
    {
      delta_progress = (gdouble) elapsed_time / priv->duration;
      progress = priv->last_progress;

      if (priv->direction == GTK_TIMELINE_DIRECTION_BACKWARD)
        progress -= delta_progress;
      else
        progress += delta_progress;

      priv->last_progress = progress;

      progress = CLAMP (progress, 0., 1.);
    }
  else
    progress = (priv->direction == GTK_TIMELINE_DIRECTION_FORWARD) ? 1.0 : 0.0;

  priv->progress = progress;
  g_signal_emit (timeline, signals [FRAME], 0,
                 calculate_progress (progress, priv->progress_type));

  if ((priv->direction == GTK_TIMELINE_DIRECTION_FORWARD && progress == 1.0) ||
      (priv->direction == GTK_TIMELINE_DIRECTION_BACKWARD && progress == 0.0))
    {
      gboolean loop;

      loop = priv->loop && priv->animations_enabled;

      if (loop)
        _gtk_timeline_rewind (timeline);
      else
        {
          if (priv->source_id)
            {
              g_source_remove (priv->source_id);
              priv->source_id = 0;
            }
          g_timer_stop (priv->timer);
          g_signal_emit (timeline, signals [FINISHED], 0);
          return FALSE;
        }
    }

  return TRUE;
}

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

GtkTimeline *
_gtk_timeline_new_for_screen (guint      duration,
                              GdkScreen *screen)
{
  return g_object_new (GTK_TYPE_TIMELINE,
                       "duration", duration,
                       "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 = timeline->priv;

  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);

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

      priv->animations_enabled = enable_animations;

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

      if (enable_animations)
        priv->source_id = gdk_threads_add_timeout (FRAME_INTERVAL (priv->fps),
                                                   (GSourceFunc) gtk_timeline_run_frame,
                                                   timeline);
      else
        priv->source_id = gdk_threads_add_idle ((GSourceFunc) 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 = timeline->priv;

  if (priv->source_id)
    {
      g_timer_stop (priv->timer);
      g_source_remove (priv->source_id);
      priv->source_id = 0;
      g_signal_emit (timeline, signals [PAUSED], 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 = timeline->priv;

  if (_gtk_timeline_get_direction (timeline) != GTK_TIMELINE_DIRECTION_FORWARD)
    priv->progress = priv->last_progress = 1.;
  else
    priv->progress = priv->last_progress = 0.;

  /* reset timer */
  if (priv->timer)
    {
      g_timer_start (priv->timer);

      if (!priv->source_id)
        g_timer_stop (priv->timer);
    }
}

/**
 * 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 = timeline->priv;

  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 = timeline->priv;
  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 = timeline->priv;

  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_run_frame,
                                                 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 = timeline->priv;
  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 = timeline->priv;

  if (loop != priv->loop)
    {
      priv->loop = loop;
      g_object_notify (G_OBJECT (timeline), "loop");
    }
}

void
_gtk_timeline_set_duration (GtkTimeline *timeline,
                            guint        duration)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

  priv = timeline->priv;

  if (duration != priv->duration)
    {
      priv->duration = duration;
      g_object_notify (G_OBJECT (timeline), "duration");
    }
}

guint
_gtk_timeline_get_duration (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), 0);

  priv = timeline->priv;

  return priv->duration;
}

/**
 * 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 = timeline->priv;
  priv->direction = direction;
}

/**
 * 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 = timeline->priv;
  return priv->direction;
}

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 = timeline->priv;

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

  priv->screen = g_object_ref (screen);

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

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

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), NULL);

  priv = timeline->priv;
  return priv->screen;
}

gdouble
_gtk_timeline_get_progress (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), 0.);

  priv = timeline->priv;
  return calculate_progress (priv->progress, priv->progress_type);
}

GtkTimelineProgressType
_gtk_timeline_get_progress_type (GtkTimeline *timeline)
{
  GtkTimelinePriv *priv;

  g_return_val_if_fail (GTK_IS_TIMELINE (timeline), GTK_TIMELINE_PROGRESS_LINEAR);

  priv = timeline->priv;
  return priv->progress_type;
}

void
_gtk_timeline_set_progress_type (GtkTimeline             *timeline,
                                 GtkTimelineProgressType  progress_type)
{
  GtkTimelinePriv *priv;

  g_return_if_fail (GTK_IS_TIMELINE (timeline));

  priv = timeline->priv;
  priv->progress_type = progress_type;
}