/*
 * Clutter.
 *
 * An OpenGL based 'interactive canvas' library.
 *
 * Authored By Matthew Allum  <mallum@openedhand.com>
 *
 * Copyright (C) 2007 OpenedHand
 *
 * This library 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 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 */

/**
 * SECTION:clutter-score
 * @short_description: Controller for multiple timelines
 *
 * #ClutterScore is a base class for sequencing multiple timelines in order.
 * Using #ClutterScore it is possible to start multiple timelines at the
 * same time or launch multiple timelines when a particular timeline has
 * emitted the ClutterTimeline::completed signal.
 *
 * Each time a #ClutterTimeline is started and completed, a signal will be
 * emitted.
 *
 * For example, this code will start two #ClutterTimeline<!-- -->s after
 * a third timeline terminates:
 *
 * |[
 *   ClutterTimeline *timeline_1, *timeline_2, *timeline_3;
 *   ClutterScore *score;
 *
 *   timeline_1 = clutter_timeline_new_for_duration (1000);
 *   timeline_2 = clutter_timeline_new_for_duration (500);
 *   timeline_3 = clutter_timeline_new_for_duration (500);
 *
 *   score = clutter_score_new ();
 *
 *   clutter_score_append (score, NULL,       timeline_1);
 *   clutter_score_append (score, timeline_1, timeline_2);
 *   clutter_score_append (score, timeline_1, timeline_3);
 *
 *   clutter_score_start (score);
 * ]|
 *
 * A #ClutterScore takes a reference on the timelines it manages,
 * so timelines can be safely unreferenced after being appended.
 *
 * New timelines can be appended to the #ClutterScore using
 * clutter_score_append() and removed using clutter_score_remove().
 *
 * Timelines can also be appended to a specific marker on the
 * parent timeline, using clutter_score_append_at_marker().
 *
 * The score can be cleared using clutter_score_remove_all().
 *
 * The list of timelines can be retrieved using
 * clutter_score_list_timelines().
 *
 * The score state is controlled using clutter_score_start(),
 * clutter_score_pause(), clutter_score_stop() and clutter_score_rewind().
 * The state can be queried using clutter_score_is_playing().
 *
 * #ClutterScore is available since Clutter 0.6
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "clutter-score.h"
#include "clutter-main.h"
#include "clutter-marshal.h"
#include "clutter-private.h"
#include "clutter-debug.h"

typedef struct _ClutterScoreEntry       ClutterScoreEntry;

struct _ClutterScoreEntry
{
  /* the entry unique id */
  gulong id;

  ClutterTimeline *timeline;
  ClutterTimeline *parent;

  /* the optional marker on the parent */
  gchar *marker;

  /* signal handlers id */
  gulong complete_id;
  gulong marker_id;

  ClutterScore *score;

  /* pointer back to the tree structure */
  GNode *node;
};

#define CLUTTER_SCORE_GET_PRIVATE(obj)  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLUTTER_TYPE_SCORE, ClutterScorePrivate))

struct _ClutterScorePrivate
{
  GNode      *root;

  GHashTable *running_timelines;

  gulong      last_id;

  guint       is_paused : 1;
  guint       loop      : 1;
};

enum
{
  PROP_0,

  PROP_LOOP
};

enum
{
  TIMELINE_STARTED,
  TIMELINE_COMPLETED,

  STARTED,
  PAUSED,
  COMPLETED,

  LAST_SIGNAL
};

static inline void clutter_score_clear (ClutterScore *score);

G_DEFINE_TYPE (ClutterScore, clutter_score, G_TYPE_OBJECT);

static int score_signals[LAST_SIGNAL] = { 0 }; 

/* Object */

static void
clutter_score_set_property (GObject      *gobject,
			    guint         prop_id,
			    const GValue *value,
			    GParamSpec   *pspec)
{
  ClutterScorePrivate *priv = CLUTTER_SCORE_GET_PRIVATE (gobject);

  switch (prop_id)
    {
    case PROP_LOOP:
      priv->loop = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
  }
}

static void
clutter_score_get_property (GObject    *gobject,
			    guint       prop_id,
			    GValue     *value,
			    GParamSpec *pspec)
{
  ClutterScorePrivate *priv = CLUTTER_SCORE_GET_PRIVATE (gobject);

  switch (prop_id)
    {
    case PROP_LOOP:
      g_value_set_boolean (value, priv->loop);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
clutter_score_finalize (GObject *object)
{
  ClutterScore *score = CLUTTER_SCORE (object);

  clutter_score_stop (score);
  clutter_score_clear (score);

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

static void
clutter_score_class_init (ClutterScoreClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = clutter_score_set_property;
  gobject_class->get_property = clutter_score_get_property;
  gobject_class->finalize     = clutter_score_finalize;

  g_type_class_add_private (klass, sizeof (ClutterScorePrivate));

  /**
   * ClutterScore:loop:
   *
   * Whether the #ClutterScore should restart once finished.
   *
   * Since: 0.6
   */
  g_object_class_install_property (gobject_class,
                                   PROP_LOOP,
                                   g_param_spec_boolean ("loop",
                                                         "Loop",
                                                         "Whether the score should restart once finished",
                                                         FALSE,
                                                         CLUTTER_PARAM_READWRITE));

  /**
   * ClutterScore::timeline-started:
   * @score: the score which received the signal
   * @timeline: the current timeline
   *
   * The ::timeline-started signal is emitted each time a new timeline
   * inside a #ClutterScore starts playing.
   *
   * Since: 0.6
   */
  score_signals[TIMELINE_STARTED] =
    g_signal_new ("timeline-started",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (ClutterScoreClass, timeline_started),
		  NULL, NULL,
		  _clutter_marshal_VOID__OBJECT,
		  G_TYPE_NONE,
		  1, CLUTTER_TYPE_TIMELINE);
  /**
   * ClutterScore::timeline-completed:
   * @score: the score which received the signal
   * @timeline: the completed timeline
   *
   * The ::timeline-completed signal is emitted each time a timeline
   * inside a #ClutterScore terminates.
   *
   * Since: 0.6
   */
  score_signals[TIMELINE_COMPLETED] =
   g_signal_new ("timeline-completed",
                 G_TYPE_FROM_CLASS (gobject_class),
                 G_SIGNAL_RUN_LAST,
                 G_STRUCT_OFFSET (ClutterScoreClass, timeline_completed),
                 NULL, NULL,
                 _clutter_marshal_VOID__OBJECT,
                 G_TYPE_NONE, 1,
                 CLUTTER_TYPE_TIMELINE);
  /**
   * ClutterScore::completed:
   * @score: the score which received the signal
   *
   * The ::completed signal is emitted each time a #ClutterScore terminates.
   *
   * Since: 0.6
   */
  score_signals[COMPLETED] =
    g_signal_new ("completed",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (ClutterScoreClass, completed),
		  NULL, NULL,
		  _clutter_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);
  /**
   * ClutterScore::started:
   * @score: the score which received the signal
   *
   * The ::started signal is emitted each time a #ClutterScore starts playing.
   *
   * Since: 0.6
   */
  score_signals[STARTED] =
    g_signal_new ("started",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (ClutterScoreClass, started),
		  NULL, NULL,
		  _clutter_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);
  /**
   * ClutterScore::paused:
   * @score: the score which received the signal
   *
   * The ::paused signal is emitted each time a #ClutterScore
   * is paused.
   *
   * Since: 0.6
   */
  score_signals[PAUSED] =
    g_signal_new ("paused",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (ClutterScoreClass, paused),
		  NULL, NULL,
		  _clutter_marshal_VOID__VOID,
		  G_TYPE_NONE, 0);
}

static void
clutter_score_init (ClutterScore *self)
{
  ClutterScorePrivate *priv;

  self->priv = priv = CLUTTER_SCORE_GET_PRIVATE (self);

  /* sentinel */
  priv->root = g_node_new (NULL);

  priv->running_timelines = NULL;

  priv->is_paused = FALSE;
  priv->loop = FALSE;

  priv->last_id = 1;
}

/**
 * clutter_score_new:
 *
 * Creates a new #ClutterScore. A #ClutterScore is an object that can
 * hold multiple #ClutterTimeline<!-- -->s in a sequential order.
 *
 * Return value: the newly created #ClutterScore. Use g_object_unref()
 *   when done.
 *
 * Since: 0.6
 */
ClutterScore *
clutter_score_new (void)
{
  return g_object_new (CLUTTER_TYPE_SCORE,  NULL);
}

/**
 * clutter_score_set_loop:
 * @score: a #ClutterScore
 * @loop: %TRUE for enable looping
 *
 * Sets whether @score should loop. A looping #ClutterScore will start
 * from its initial state after the ::complete signal has been fired.
 *
 * Since: 0.6
 */
void
clutter_score_set_loop (ClutterScore *score,
			   gboolean         loop)
{
  g_return_if_fail (CLUTTER_IS_SCORE (score));

  if (score->priv->loop != loop)
    {
      score->priv->loop = loop;

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

/**
 * clutter_score_get_loop:
 * @score: a #ClutterScore
 *
 * Gets whether @score is looping
 *
 * Return value: %TRUE if the score is looping
 *
 * Since: 0.6
 */
gboolean
clutter_score_get_loop (ClutterScore *score)
{
  g_return_val_if_fail (CLUTTER_IS_SCORE (score), FALSE);

  return score->priv->loop;
}

/**
 * clutter_score_is_playing:
 * @score: A #ClutterScore
 *
 * Query state of a #ClutterScore instance.
 *
 * Return Value: %TRUE if score is currently playing
 *
 * Since: 0.6
 */
gboolean
clutter_score_is_playing (ClutterScore *score)
{
  g_return_val_if_fail (CLUTTER_IS_SCORE (score), FALSE);

  if (score->priv->is_paused)
    return FALSE;

  return score->priv->running_timelines
    && g_hash_table_size (score->priv->running_timelines) != 0;
}

/* destroy_entry:
 * @node: a #GNode
 *
 * Frees the #ClutterScoreEntry attached to @node.
 */
static gboolean
destroy_entry (GNode                  *node,
               G_GNUC_UNUSED gpointer  data)
{
  ClutterScoreEntry *entry = node->data;

  if (G_LIKELY (entry != NULL))
    {
      if (entry->marker_id)
        {
          g_signal_handler_disconnect (entry->parent, entry->marker_id);
          entry->marker_id = 0;
        }

      if (entry->complete_id)
        {
          g_signal_handler_disconnect (entry->timeline, entry->complete_id);
          entry->complete_id = 0;
        }

      g_object_unref (entry->timeline);
      g_free (entry->marker);
      g_slice_free (ClutterScoreEntry, entry);

      node->data = NULL;
    }

  /* continue */
  return FALSE;
}

typedef enum {
  FIND_BY_TIMELINE,
  FIND_BY_ID,
  REMOVE_BY_ID,
  LIST_TIMELINES
} TraverseAction;

typedef struct {
  TraverseAction action;

  ClutterScore *score;

  /* parameters */
  union {
    ClutterTimeline *timeline;
    gulong id;
    ClutterScoreEntry *entry;
  } d;

  gpointer result;
} TraverseClosure;

/* multi-purpose traversal function for the N-ary tree used by the score */
static gboolean
traverse_children (GNode    *node,
                   gpointer  data)
{
  TraverseClosure *closure = data;
  ClutterScoreEntry *entry = node->data;
  gboolean retval = FALSE;

  /* root */
  if (!entry)
    return TRUE;

  switch (closure->action)
    {
    case FIND_BY_TIMELINE:
      if (closure->d.timeline == entry->timeline)
        {
          closure->result = node;
          retval = TRUE;
        }
      break;

    case FIND_BY_ID:
      if (closure->d.id == entry->id)
        {
          closure->result = node;
          retval = TRUE;
        }
      break;

    case REMOVE_BY_ID:
      if (closure->d.id == entry->id)
        {
          /* Destroy all the child entries of this node */
          g_node_traverse (node,
                           G_POST_ORDER,
                           G_TRAVERSE_ALL,
                           -1,
                           destroy_entry, NULL);

          /* Keep track of this node so that it will be destroyed
             further up */
          closure->result = node;

          retval = TRUE;
        }
      break;

    case LIST_TIMELINES:
      closure->result = g_slist_prepend (closure->result, entry->timeline);
      retval = FALSE;
      break;
    }

  return retval;
}

static GNode *
find_entry_by_timeline (ClutterScore    *score,
                        ClutterTimeline *timeline)
{
  ClutterScorePrivate *priv = score->priv;
  TraverseClosure closure;

  closure.action = FIND_BY_TIMELINE;
  closure.score = score;
  closure.d.timeline = timeline;
  closure.result = NULL;

  g_node_traverse (priv->root,
                   G_POST_ORDER,
                   G_TRAVERSE_ALL,
                   -1,
                   traverse_children, &closure);

  if (closure.result)
    return closure.result;

  return NULL;
}

static GNode *
find_entry_by_id (ClutterScore *score,
                  gulong        id_)
{
  ClutterScorePrivate *priv = score->priv;
  TraverseClosure closure;

  closure.action = FIND_BY_ID;
  closure.score = score;
  closure.d.id = id_;
  closure.result = NULL;

  g_node_traverse (priv->root,
                   G_POST_ORDER,
                   G_TRAVERSE_ALL,
                   -1,
                   traverse_children, &closure);

  if (closure.result)
    return closure.result;

  return NULL;
}

/* forward declaration */
static void start_entry (ClutterScoreEntry *entry);

static void
start_children_entries (GNode    *node,
                        gpointer  data)
{
  ClutterScoreEntry *entry = node->data;

  /* If data is NULL, start all entries that have no marker, otherwise
     only start entries that have the same marker */
  if (data == NULL
      ? entry->marker == NULL
      : (entry->marker && !strcmp (data, entry->marker)))
    start_entry (entry);
}

static void
on_timeline_marker (ClutterTimeline   *timeline,
                    const gchar       *marker_name,
                    gint               frame_num,
                    ClutterScoreEntry *entry)
{
  GNode *parent;
  CLUTTER_NOTE (SCHEDULER, "timeline [%p] marker ('%s') reached",
		entry->timeline,
                entry->marker);

  parent = find_entry_by_timeline (entry->score, timeline);
  if (!parent)
    return;

  /* start every child */
  if (parent->children)
    {
      g_node_children_foreach (parent,
                               G_TRAVERSE_ALL,
                               start_children_entries,
                               (gpointer) marker_name);
    }
}

static void
on_timeline_completed (ClutterTimeline   *timeline,
                       ClutterScoreEntry *entry)
{
  ClutterScorePrivate *priv = entry->score->priv;

  g_hash_table_remove (priv->running_timelines,
                       GUINT_TO_POINTER (entry->id));

  g_signal_handler_disconnect (timeline, entry->complete_id);
  entry->complete_id = 0;

  CLUTTER_NOTE (SCHEDULER, "timeline [%p] ('%lu') completed", 
		entry->timeline,
                entry->id);

  g_signal_emit (entry->score, score_signals[TIMELINE_COMPLETED], 0,
                 entry->timeline);

  /* start every child */
  if (entry->node->children)
    {
      g_node_children_foreach (entry->node,
                               G_TRAVERSE_ALL,
                               start_children_entries,
                               NULL);
    }

  /* score has finished - fire 'completed' signal */
  if (g_hash_table_size (priv->running_timelines) == 0)
    {
      CLUTTER_NOTE (SCHEDULER, "looks like we finished");
      
      g_signal_emit (entry->score, score_signals[COMPLETED], 0);

      clutter_score_stop (entry->score);
      
      if (priv->loop)
        clutter_score_start (entry->score);
    }
}

static void
start_entry (ClutterScoreEntry *entry)
{
  ClutterScorePrivate *priv = entry->score->priv;

  /* timelines attached to a marker might already be playing when we
   * end up here from the ::completed handler, so we need to perform
   * this check to avoid restarting those timelines
   */
  if (clutter_timeline_is_playing (entry->timeline))
    return;

  entry->complete_id = g_signal_connect (entry->timeline,
                                         "completed",
                                         G_CALLBACK (on_timeline_completed),
                                         entry);

  CLUTTER_NOTE (SCHEDULER, "timeline [%p] ('%lu') started",
                entry->timeline,
                entry->id);

  if (G_UNLIKELY (priv->running_timelines == NULL))
    priv->running_timelines = g_hash_table_new (NULL, NULL);

  g_hash_table_insert (priv->running_timelines,
                       GUINT_TO_POINTER (entry->id),
                       entry);

  clutter_timeline_start (entry->timeline);

  g_signal_emit (entry->score, score_signals[TIMELINE_STARTED], 0,
                 entry->timeline);
}

enum
{
  ACTION_START,
  ACTION_PAUSE,
  ACTION_STOP
};

static void
foreach_running_timeline (gpointer key,
                          gpointer value,
                          gpointer user_data)
{
  ClutterScoreEntry *entry = value;
  gint action = GPOINTER_TO_INT (user_data);

  switch (action)
    {
    case ACTION_START:
      clutter_timeline_start (entry->timeline);
      break;

    case ACTION_PAUSE:
      clutter_timeline_pause (entry->timeline);
      break;

    case ACTION_STOP:
      if (entry->complete_id)
	{
	  g_signal_handler_disconnect (entry->timeline, entry->complete_id);
	  entry->complete_id = 0;
	}
      clutter_timeline_stop (entry->timeline);
      break;
    }
}

/**
 * clutter_score_start:
 * @score: A #ClutterScore
 *
 * Starts the score.
 *
 * Since: 0.6
 */
void
clutter_score_start (ClutterScore *score)
{
  ClutterScorePrivate *priv;

  g_return_if_fail (CLUTTER_IS_SCORE (score));

  priv = score->priv;

  if (priv->is_paused)
    {
      g_hash_table_foreach (priv->running_timelines,
			    foreach_running_timeline,
			    GINT_TO_POINTER (ACTION_START));
      priv->is_paused = FALSE;
    }
  else
    {
      g_signal_emit (score, score_signals[STARTED], 0);
      g_node_children_foreach (priv->root,
                               G_TRAVERSE_ALL,
                               start_children_entries,
                               NULL);
    }
}

/**
 * clutter_score_stop:
 * @score: A #ClutterScore
 *
 * Stops and rewinds a playing #ClutterScore instance.
 *
 * Since: 0.6
 */
void
clutter_score_stop (ClutterScore *score)
{
  ClutterScorePrivate *priv;

  g_return_if_fail (CLUTTER_IS_SCORE (score));

  priv = score->priv;

  if (priv->running_timelines)
    {
      g_hash_table_foreach (priv->running_timelines,
                            foreach_running_timeline,
                            GINT_TO_POINTER (ACTION_STOP));
      g_hash_table_destroy (priv->running_timelines);
      priv->running_timelines = NULL;
    }
}

/**
 * clutter_score_pause:
 * @score: a #ClutterScore
 *
 * Pauses a playing score @score.
 *
 * Since: 0.6
 */
void
clutter_score_pause (ClutterScore *score)
{
  ClutterScorePrivate *priv;

  g_return_if_fail (CLUTTER_IS_SCORE (score));

  priv = score->priv;

  if (!clutter_score_is_playing (score)) 
    return;

  g_hash_table_foreach (priv->running_timelines,
			foreach_running_timeline,
			GINT_TO_POINTER (ACTION_PAUSE));

  priv->is_paused = TRUE;

  g_signal_emit (score, score_signals[PAUSED], 0);
}

/**
 * clutter_score_rewind:
 * @score: A #ClutterScore
 *
 * Rewinds a #ClutterScore to its initial state.
 *
 * Since: 0.6
 */
void
clutter_score_rewind (ClutterScore *score)
{
  gboolean was_playing;

  g_return_if_fail (CLUTTER_IS_SCORE (score));

  was_playing = clutter_score_is_playing (score);

  clutter_score_stop (score);

  if (was_playing)
    clutter_score_start (score);
}

static inline void
clutter_score_clear (ClutterScore *score)
{
  ClutterScorePrivate *priv = score->priv;

  g_node_traverse (priv->root,
                   G_POST_ORDER,
                   G_TRAVERSE_ALL,
                   -1,
                   destroy_entry, NULL);
  g_node_destroy (priv->root);
}

/**
 * clutter_score_append:
 * @score: a #ClutterScore
 * @parent: (allow-none): a #ClutterTimeline in the score, or %NULL
 * @timeline: a #ClutterTimeline
 *
 * Appends a timeline to another one existing in the score; the newly
 * appended timeline will be started when @parent is complete.
 *
 * If @parent is %NULL, the new #ClutterTimeline will be started when
 * clutter_score_start() is called.
 *
 * #ClutterScore will take a reference on @timeline.
 *
 * Return value: the id of the #ClutterTimeline inside the score, or
 *   0 on failure. The returned id can be used with clutter_score_remove()
 *   or clutter_score_get_timeline().
 *
 * Since: 0.6
 */
gulong
clutter_score_append (ClutterScore    *score,
		      ClutterTimeline *parent,
		      ClutterTimeline *timeline)
{
  ClutterScorePrivate *priv;
  ClutterScoreEntry *entry;

  g_return_val_if_fail (CLUTTER_IS_SCORE (score), 0);
  g_return_val_if_fail (parent == NULL || CLUTTER_IS_TIMELINE (parent), 0);
  g_return_val_if_fail (CLUTTER_IS_TIMELINE (timeline), 0);

  priv = score->priv;

  if (!parent)
    {
      entry = g_slice_new (ClutterScoreEntry);
      entry->timeline = g_object_ref (timeline);
      entry->parent = NULL;
      entry->id = priv->last_id;
      entry->marker = NULL;
      entry->marker_id = 0;
      entry->complete_id = 0;
      entry->score = score;

      entry->node = g_node_append_data (priv->root, entry);
    }
  else
    {
      GNode *node;

      node = find_entry_by_timeline (score, parent);
      if (G_UNLIKELY (!node))
        {
          g_warning ("Unable to find the parent timeline inside the score.");
          return 0;
        }

      entry = g_slice_new (ClutterScoreEntry);
      entry->timeline = g_object_ref (timeline);
      entry->parent = parent;
      entry->id = priv->last_id;
      entry->marker = NULL;
      entry->marker_id = 0;
      entry->complete_id = 0;
      entry->score = score;

      entry->node = g_node_append_data (node, entry);
    }

  priv->last_id += 1;

  return entry->id;
}

/**
 * clutter_score_append_at_marker:
 * @score: a #ClutterScore
 * @parent: the parent #ClutterTimeline
 * @marker_name: the name of the marker to use
 * @timeline: the #ClutterTimeline to append
 *
 * Appends @timeline at the given @marker_name on the @parent
 * #ClutterTimeline.
 *
 * If you want to append @timeline at the end of @parent, use
 * clutter_score_append().
 *
 * The #ClutterScore will take a reference on @timeline.
 *
 * Return value: the id of the #ClutterTimeline inside the score, or
 *   0 on failure. The returned id can be used with clutter_score_remove()
 *   or clutter_score_get_timeline().
 *
 * Since: 0.8
 */
gulong
clutter_score_append_at_marker (ClutterScore    *score,
                                ClutterTimeline *parent,
                                const gchar     *marker_name,
                                ClutterTimeline *timeline)
{
  ClutterScorePrivate *priv;
  GNode *node;
  ClutterScoreEntry *entry;
  gchar *marker_reached_signal;

  g_return_val_if_fail (CLUTTER_IS_SCORE (score), 0);
  g_return_val_if_fail (CLUTTER_IS_TIMELINE (parent), 0);
  g_return_val_if_fail (marker_name != NULL, 0);
  g_return_val_if_fail (CLUTTER_IS_TIMELINE (timeline), 0);

  if (!clutter_timeline_has_marker (parent, marker_name))
    {
      g_warning ("The parent timeline has no marker '%s'", marker_name);
      return 0;
    }

  priv = score->priv;

  node = find_entry_by_timeline (score, parent);
  if (G_UNLIKELY (!node))
    {
      g_warning ("Unable to find the parent timeline inside the score.");
      return 0;
    }

  entry = g_slice_new (ClutterScoreEntry);
  entry->timeline = g_object_ref (timeline);
  entry->parent = parent;
  entry->marker = g_strdup (marker_name);
  entry->id = priv->last_id;
  entry->score = score;
  entry->complete_id = 0;

  marker_reached_signal = g_strdup_printf ("marker-reached::%s", marker_name);
  entry->marker_id = g_signal_connect (entry->parent,
                                       marker_reached_signal,
                                       G_CALLBACK (on_timeline_marker),
                                       entry);

  entry->node = g_node_append_data (node, entry);

  g_free (marker_reached_signal);

  priv->last_id += 1;

  return entry->id;
}

/**
 * clutter_score_remove:
 * @score: a #ClutterScore
 * @id_: the id of the timeline to remove
 *
 * Removes the #ClutterTimeline with the given id inside @score. If
 * the timeline has other timelines attached to it, those are removed
 * as well.
 *
 * Since: 0.6
 */
void
clutter_score_remove (ClutterScore *score,
                      gulong        id_)
{
  ClutterScorePrivate *priv;
  TraverseClosure closure;

  g_return_if_fail (CLUTTER_IS_SCORE (score));
  g_return_if_fail (id_ > 0);

  priv = score->priv;

  closure.action = REMOVE_BY_ID;
  closure.score = score;
  closure.d.id = id_;
  closure.result = NULL;

  g_node_traverse (priv->root,
                   G_POST_ORDER,
                   G_TRAVERSE_ALL,
                   -1,
                   traverse_children, &closure);

  if (closure.result)
    g_node_destroy (closure.result);
}

/**
 * clutter_score_remove_all:
 * @score: a #ClutterScore
 *
 * Removes all the timelines inside @score.
 *
 * Since: 0.6
 */
void
clutter_score_remove_all (ClutterScore *score)
{
  ClutterScorePrivate *priv;

  g_return_if_fail (CLUTTER_IS_SCORE (score));

  priv = score->priv;

  /* this will take care of the running timelines */
  clutter_score_stop (score);

  /* destroy all the contents of the tree */
  clutter_score_clear (score);

  /* recreate the sentinel */
  priv->root = g_node_new (NULL);
}

/**
 * clutter_score_get_timeline:
 * @score: a #ClutterScore
 * @id_: the id of the timeline
 *
 * Retrieves the #ClutterTimeline for @id_ inside @score.
 *
 * Return value: (transfer none): the requested timeline, or %NULL. This
 *   function does not increase the reference count on the returned
 *   #ClutterTimeline
 *
 * Since: 0.6
 */
ClutterTimeline *
clutter_score_get_timeline (ClutterScore *score,
                            gulong        id_)
{
  GNode *node;
  ClutterScoreEntry *entry;

  g_return_val_if_fail (CLUTTER_IS_SCORE (score), NULL);
  g_return_val_if_fail (id_ > 0, NULL);

  node = find_entry_by_id (score, id_);
  if (G_UNLIKELY (!node))
    return NULL;

  entry = node->data;

  return entry->timeline;
}

/**
 * clutter_score_list_timelines:
 * @score: a #ClutterScore
 *
 * Retrieves a list of all the #ClutterTimelines managed by @score.
 *
 * Return value: (transfer container) (element-type Clutter.Timeline): a
 *   #GSList containing all the timelines in the score. This function does
 *   not increase the reference count of the returned timelines. Use
 *   g_slist_free() on the returned list to deallocate its resources.
 *
 * Since: 0.6
 */
GSList *
clutter_score_list_timelines (ClutterScore *score)
{
  ClutterScorePrivate *priv;
  TraverseClosure closure;
  GSList *retval;

  g_return_val_if_fail (CLUTTER_IS_SCORE (score), NULL);

  priv = score->priv;

  closure.action = LIST_TIMELINES;
  closure.result = NULL;

  g_node_traverse (priv->root,
                   G_POST_ORDER,
                   G_TRAVERSE_ALL,
                   -1,
                   traverse_children, &closure);

  retval = closure.result;

  return retval;
}
