/*
 * Clutter.
 *
 * An OpenGL based 'interactive canvas' library.
 *
 * Authored By Matthew Allum  <mallum@openedhand.com>
 *
 * Copyright (C) 2006 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, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/**
 * SECTION:clutter-script
 * @short_description: Loads a scene from UI definition data
 *
 * #ClutterScript is an object used for loading and building parts or a
 * complete scenegraph from external definition data in forms of string
 * buffers or files.
 *
 * The UI definition format is JSON, the JavaScript Object Notation as
 * described by RFC 4627. #ClutterScript can load a JSON data stream,
 * parse it and build all the objects defined into it. Each object must
 * have an "id" and a "type" properties defining the name to be used
 * to retrieve it from #ClutterScript with clutter_script_get_object(),
 * and the class type to be instanciated. Every other attribute will
 * be mapped to the class properties.
 *
 * A #ClutterScript holds a reference on every object it creates from
 * the definition data, except for the stage. Every non-actor object
 * will be finalized when the #ClutterScript instance holding it will
 * be finalized, so they need to be referenced using g_object_ref() in
 * order for them to survive.
 *
 * A simple object might be defined as:
 *
 * <programlisting>
 * {
 *   "id"     : "red-button",
 *   "type"   : "ClutterRectangle",
 *   "width"  : 100,
 *   "height" : 100,
 *   "color"  : "0xff0000ff"
 * }
 * </programlisting>
 *
 * This will produce a red #ClutterRectangle, 100x100 pixels wide, and
 * with a name of "red-button"; it can be retrieved by calling:
 *
 * <programlisting>
 * ClutterActor *red_button;
 *
 * red_button = CLUTTER_ACTOR (clutter_script_get_object (script, "red-button"));
 * </programlisting>
 *
 * and then manipulated with the Clutter API.
 *
 * Packing can be represented using the "children" member, and passing an
 * array of objects or ids of objects already defined (but not packed: the
 * packing rules of Clutter still apply, and an actor cannot be packed
 * in multiple containers without unparenting it in between).
 *
 * Behaviours and timelines can also be defined inside a UI definition
 * buffer:
 *
 * <programlisting>
 * {
 *   "id"          : "rotate-behaviour",
 *   "type"        : "ClutterBehaviourRotate",
 *   "angle-begin" : 0.0,
 *   "angle-end"   : 360.0,
 *   "axis"        : "z-axis",
 *   "alpha"       : {
 *     "timeline" : { "num-frames" : 240, "fps" : 60, "loop" : true },
 *     "function" : "sine"
 *   }
 * }
 * </programlisting>
 *
 * And then to apply a defined behaviour to an actor defined inside the
 * definition of an actor, the "behaviour" member can be used:
 *
 * <programlisting>
 * {
 *   "id" : "my-rotating-actor",
 *   "type" : "ClutterTexture",
 *   ...
 *   "behaviours" : [ "rotate-behaviour" ]
 * }
 * </programlisting>
 *
 * A #ClutterAlpha belonging to a #ClutterBehaviour can only be defined
 * implicitely. A #ClutterTimeline belonging to a #ClutterAlpha can be
 * either defined implicitely or explicitely. Implicitely defined
 * #ClutterAlpha<!-- -->s and #ClutterTimeline<!-- -->s can omit the
 * <varname>id</varname> member, as well as the <varname>type</varname> member,
 * but will not be available using clutter_script_get_object() (they can,
 * however, be extracted using the #ClutterBehaviour and #ClutterAlpha
 * API respectively).
 *
 * Clutter reserves the following names, so classes defining properties
 * through the usual GObject registration process should avoid using these
 * names to avoid collisions:
 *
 * <programlisting><![CDATA[
 *   "id"         := the unique name of a ClutterScript object
 *   "type"       := the class literal name, also used to infer the type function
 *   "type_func"  := the GType function name, for non-standard classes
 *   "children"   := an array of names or objects to add as children
 *   "behaviours" := an array of names or objects to apply to an actor
 * ]]></programlisting>
 *
 * #ClutterScript is available since Clutter 0.6
 */

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

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#include <glib.h>
#include <glib-object.h>

#include "clutter-actor.h"
#include "clutter-alpha.h"
#include "clutter-behaviour.h"
#include "clutter-container.h"
#include "clutter-stage.h"
#include "clutter-texture.h"

#include "clutter-script.h"
#include "clutter-script-private.h"
#include "clutter-scriptable.h"

#include "clutter-private.h"
#include "clutter-debug.h"

#include "json/json-parser.h"

#define CLUTTER_SCRIPT_GET_PRIVATE(obj) \
        (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLUTTER_TYPE_SCRIPT, ClutterScriptPrivate))

struct _ClutterScriptPrivate
{
  GHashTable *objects;

  guint last_merge_id;
  guint last_unknown;

  JsonParser *parser;

  gchar *filename;
  guint is_filename : 1;
};

G_DEFINE_TYPE (ClutterScript, clutter_script, G_TYPE_OBJECT);

static void
warn_missing_attribute (ClutterScript *script,
                        const gchar   *id,
                        const gchar   *attribute)
{
  ClutterScriptPrivate *priv = script->priv;

  if (G_LIKELY (id))
    {
      g_warning ("%s: %d: object `%s' has no `%s' attribute",
                 priv->is_filename ? priv->filename : "<input>",
                 json_parser_get_current_line (priv->parser),
                 id,
                 attribute);
    }
  else
    {
      g_warning ("%s: %d: object has no `%s' attribute",
                 priv->is_filename ? priv->filename : "<input>",
                 json_parser_get_current_line (priv->parser),
                 attribute);
    }
}

#if 0
static void
warn_invalid_value (ClutterScript *script,
                    const gchar   *attribute,
                    JsonNode      *node)
{
  ClutterScriptPrivate *priv = script->priv;

  if (G_LIKELY (node))
    {
      g_warning ("%s: %d: invalid value of type `%s' for attribute `%s'",
                 priv->is_filename ? priv->filename : "<input>",
                 json_parser_get_current_line (priv->parser),
                 json_node_type_name (node),
                 attribute);
    }
  else
    {
      g_warning ("%s: %d: invalid value for attribute `%s'",
                 priv->is_filename ? priv->filename : "<input>",
                 json_parser_get_current_line (priv->parser),
                 attribute);
    }
}
#endif

static const gchar *
get_id_from_node (JsonNode *node)
{
  JsonObject *object;

  switch (JSON_NODE_TYPE (node))
    {
    case JSON_NODE_OBJECT:
      object = json_node_get_object (node);
      if (json_object_has_member (object, "id"))
        {
          JsonNode *id = json_object_get_member (object, "id");

          return json_node_get_string (id);
        }
      break;

    case JSON_NODE_VALUE:
      return json_node_get_string (node);

    default:
      break;
    }

  return NULL;
}

static GList *
parse_children (JsonNode *node)
{
  JsonArray *array;
  GList *retval;
  guint array_len, i;

  if (JSON_NODE_TYPE (node) != JSON_NODE_ARRAY)
    return NULL;

  retval = NULL;

  array = json_node_get_array (node);
  array_len = json_array_get_length (array);

  for (i = 0; i < array_len; i++)
    {
      JsonNode *child = json_array_get_element (array, i);
      const gchar *id;

      id = get_id_from_node (child);
      if (id)
        retval = g_list_prepend (retval, g_strdup (id));
    }

  return g_list_reverse (retval);
}

static GList *
parse_behaviours (JsonNode *node)
{
  JsonArray *array;
  GList *retval;
  guint array_len, i;

  if (JSON_NODE_TYPE (node) != JSON_NODE_ARRAY)
    return NULL;

  retval = NULL;

  array = json_node_get_array (node);
  array_len = json_array_get_length (array);

  for (i = 0; i < array_len; i++)
    {
      JsonNode *child = json_array_get_element (array, i);
      const gchar *id;

      id = get_id_from_node (child);
      if (id)
        retval = g_list_prepend (retval, g_strdup (id));
    }

  return g_list_reverse (retval);
}

static ClutterTimeline *
construct_timeline (ClutterScript *script,
                    JsonObject    *object)
{
  ClutterTimeline *retval = NULL;
  ObjectInfo *oinfo;
  GList *members, *l;
  
  /* we fake an ObjectInfo so we can reuse clutter_script_construct_object()
   * here; we do not save it inside the hash table, because if this had
   * been a named object then we wouldn't have ended up here in the first
   * place
   */
  oinfo = g_slice_new0 (ObjectInfo);
  oinfo->gtype = CLUTTER_TYPE_TIMELINE;
  oinfo->id = g_strdup ("dummy");

  members = json_object_get_members (object);
  for (l = members; l != NULL; l = l->next)
    {
      const gchar *name = l->data;
      JsonNode *node = json_object_get_member (object, name);
      PropertyInfo *pinfo = g_slice_new0 (PropertyInfo);

      pinfo->name = g_strdelimit (g_strdup (name), G_STR_DELIMITERS, '-');
      pinfo->node = node;

      oinfo->properties = g_list_prepend (oinfo->properties, pinfo);
    }

  g_list_free (members);
  
  retval = CLUTTER_TIMELINE (clutter_script_construct_object (script, oinfo));

  /* we transfer ownership to the alpha function later */
  oinfo->is_toplevel = FALSE;
  object_info_free (oinfo);

  return retval;
}

static ClutterAlphaFunc
resolve_alpha_func (const gchar *name)
{
  static GModule *module = NULL;
  ClutterAlphaFunc func;
  GString *symbol_name;
  gchar c, *symbol;
  gint i;

  if (G_UNLIKELY (!module))
    module = g_module_open (NULL, 0);

  CLUTTER_NOTE (SCRIPT, "Looking for `%s' alpha function", name);
  
  if (g_module_symbol (module, name, (gpointer) &func))
    return func;

  symbol_name = g_string_new ("");
  g_string_append (symbol_name, "clutter_");
  for (i = 0; name[i] != '\0'; i++)
    {
      c = name[i];
      
      if (name[i] == '-')
        g_string_append_c (symbol_name, '_');
      else
        g_string_append_c (symbol_name, g_ascii_tolower (name[i]));
    }
  g_string_append (symbol_name, "_func");
  
  symbol = g_string_free (symbol_name, FALSE);

  if (!g_module_symbol (module, symbol, (gpointer)&func))
    func = NULL;

  g_free (symbol);

  return func;
}

GObject *
clutter_script_parse_alpha (ClutterScript *script,
                            JsonNode      *node)
{
  GObject *retval = NULL;
  JsonObject *object;
  ClutterTimeline *timeline = NULL;
  ClutterAlphaFunc alpha_func = NULL;
  JsonNode *val;
  gboolean unref_timeline = FALSE;

  if (JSON_NODE_TYPE (node) != JSON_NODE_OBJECT)
    return NULL;

  object = json_node_get_object (node);
  
  val = json_object_get_member (object, "timeline");
  if (val)
    {
      if (JSON_NODE_TYPE (val) == JSON_NODE_VALUE &&
          json_node_get_string (val) != NULL)
        {
          const gchar *id = json_node_get_string (val);

          timeline =
            CLUTTER_TIMELINE (clutter_script_get_object (script, id));
        }
      else if (JSON_NODE_TYPE (val) == JSON_NODE_OBJECT)
        {
          timeline = construct_timeline (script, json_node_get_object (val));
          unref_timeline = TRUE;
        }
    }

  val = json_object_get_member (object, "function");
  if (val && json_node_get_string (val) != NULL)
    alpha_func = resolve_alpha_func (json_node_get_string (val));

  CLUTTER_NOTE (SCRIPT, "Parsed alpha: %s timeline (%p) and func:%p",
                unref_timeline ? "implicit" : "explicit",
                timeline ? timeline : 0x0,
                alpha_func ? alpha_func : 0x0);

  retval = g_object_new (CLUTTER_TYPE_ALPHA, NULL);
  clutter_alpha_set_func (CLUTTER_ALPHA (retval), alpha_func, NULL, NULL);
  clutter_alpha_set_timeline (CLUTTER_ALPHA (retval), timeline);
  if (unref_timeline)
    g_object_unref (timeline);

  return retval;
}

static void
json_object_end (JsonParser *parser,
                 JsonObject *object,
                 gpointer    user_data)
{
  ClutterScript *script = user_data;
  ClutterScriptPrivate *priv = script->priv;
  ObjectInfo *oinfo;
  JsonNode *val;
  GList *members, *l;

  if (!json_object_has_member (object, "id"))
    {
      gchar *fake;

      if (!json_object_has_member (object, "type"))
        return;

      fake = g_strdup_printf ("script-%d-%d",
                              priv->last_merge_id,
                              priv->last_unknown++);

      val = json_node_new (JSON_NODE_VALUE);
      json_node_set_string (val, fake);
      json_object_add_member (object, "id", val);

      g_free (fake);
    }
      
  if (!json_object_has_member (object, "type"))
    {
      val = json_object_get_member (object, "id");

      warn_missing_attribute (script, json_node_get_string (val), "type");
      return;
    }

  oinfo = g_slice_new0 (ObjectInfo);
  oinfo->merge_id = priv->last_merge_id;
  
  val = json_object_get_member (object, "id");
  oinfo->id = json_node_dup_string (val);

  val = json_object_get_member (object, "type");
  oinfo->class_name = json_node_dup_string (val);

  if (json_object_has_member (object, "type_func"))
    {
      val = json_object_get_member (object, "type_func");
      oinfo->type_func = json_node_dup_string (val);

      json_object_remove_member (object, "type_func");
    }

  if (json_object_has_member (object, "children"))
    {
      val = json_object_get_member (object, "children");
      oinfo->children = parse_children (val);

      json_object_remove_member (object, "children");
    }

  if (json_object_has_member (object, "behaviours"))
    {
      val = json_object_get_member (object, "behaviours");
      oinfo->behaviours = parse_behaviours (val);

      json_object_remove_member (object, "behaviours");
    }

  oinfo->is_toplevel = FALSE;
  oinfo->is_unmerged = FALSE;
  oinfo->has_unresolved = TRUE;

  members = json_object_get_members (object);
  for (l = members; l; l = l->next)
    {
      const gchar *name = l->data;
      PropertyInfo *pinfo;
      JsonNode *node;

      /* we have already parsed these */
      if (strcmp (name, "id") == 0 || strcmp (name, "type") == 0)
        continue;

      node = json_object_get_member (object, name);

      pinfo = g_slice_new (PropertyInfo);

      pinfo->name = g_strdup (name);
      pinfo->node = node;
      pinfo->pspec = NULL;

      oinfo->properties = g_list_prepend (oinfo->properties, pinfo);
    }

  g_list_free (members);

  CLUTTER_NOTE (SCRIPT, "Added object `%s' (type:%s, id:%d) with %d properties",
                oinfo->id,
                oinfo->class_name,
                oinfo->merge_id,
                g_list_length (oinfo->properties));

  g_hash_table_replace (priv->objects, g_strdup (oinfo->id), oinfo);

  oinfo->object = clutter_script_construct_object (script, oinfo);
}

gboolean
clutter_script_parse_node (ClutterScript *script,
                           GValue        *value,
                           const gchar   *name,
                           JsonNode      *node,
                           GParamSpec    *pspec)
{
  GValue node_value = { 0, };
  gboolean retval = FALSE;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (node != NULL, FALSE);

  switch (JSON_NODE_TYPE (node))
    {
    case JSON_NODE_OBJECT:
      if (!pspec)
        return FALSE;
      else
        {
          g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));

          if (G_VALUE_HOLDS (value, CLUTTER_TYPE_ALPHA))
            {
              GObject *alpha;

              alpha = clutter_script_parse_alpha (script, node);
              if (alpha)
                {
                  g_value_set_object (value, alpha);
                  return TRUE;
                }
            }
         }
      return FALSE;

    case JSON_NODE_ARRAY:
      if (!pspec)
        return FALSE;
      else
        {
          g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));

          if (G_VALUE_HOLDS (value, CLUTTER_TYPE_KNOT))
            {
              ClutterKnot knot = { 0, };

              if (clutter_script_parse_knot (script, node, &knot))
                {
                  g_value_set_boxed (value, &knot);
                  return TRUE;
                }
            }
          else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_PADDING))
            {
              ClutterPadding padding = { 0, };

              if (clutter_script_parse_padding (script, node, &padding))
                {
                  g_value_set_boxed (value, &padding);
                  return TRUE;
                }
            }
          else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_MARGIN))
            {
              ClutterMargin margin = { 0, };

              if (clutter_script_parse_margin (script, node, &margin))
                {
                  g_value_set_boxed (value, &margin);
                  return TRUE;
                }
            }
          else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_GEOMETRY))
            {
              ClutterGeometry geom = { 0, };

              if (clutter_script_parse_geometry (script, node, &geom))
                {
                  g_value_set_boxed (value, &geom);
                  return TRUE;
                }
            }
        }
      return FALSE;

    case JSON_NODE_NULL:
      return FALSE;

    case JSON_NODE_VALUE:
      json_node_get_value (node, &node_value);

      if (pspec)
        g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));
      else
        g_value_init (value, G_VALUE_TYPE (&node_value));

      switch (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value)))
        {
        /* fundamental JSON types */
        case G_TYPE_INT:
        case G_TYPE_DOUBLE:
        case G_TYPE_STRING:
        case G_TYPE_BOOLEAN:
          g_value_copy (&node_value, value);
          retval = TRUE;
          break;

        case G_TYPE_UINT:
          g_value_set_uint (value, (guint) g_value_get_int (&node_value));
          retval = TRUE;
          break;

        case G_TYPE_ULONG:
          g_value_set_ulong (value, (gulong) g_value_get_int (&node_value));
          retval = TRUE;
          break;

        case G_TYPE_UCHAR:
          g_value_set_uchar (value, (guchar) g_value_get_int (&node_value));
          retval = TRUE;
          break;

        case G_TYPE_ENUM:
          if (G_VALUE_HOLDS (&node_value, G_TYPE_INT))
            {
              g_value_set_enum (value, g_value_get_int (&node_value));
              retval = TRUE;
            }
          else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
            {
              gint enum_value;

              retval = clutter_script_enum_from_string (G_VALUE_TYPE (value),
                                                        g_value_get_string (&node_value),
                                                        &enum_value);
              if (retval)
                g_value_set_enum (value, enum_value);
            }
          break;

        case G_TYPE_FLAGS:
          if (G_VALUE_HOLDS (&node_value, G_TYPE_INT))
            {
              g_value_set_flags (value, g_value_get_int (&node_value));
              retval = TRUE;
            }
          else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
            {
              gint flags_value;

              retval = clutter_script_flags_from_string (G_VALUE_TYPE (value),
                                                         g_value_get_string (&node_value),
                                                         &flags_value);
              if (retval)
                g_value_set_flags (value, flags_value);
            }
          break;

        case G_TYPE_BOXED:
          if (G_VALUE_HOLDS (value, CLUTTER_TYPE_COLOR))
            {
              if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
                {
                  const gchar *str = g_value_get_string (&node_value);
                  ClutterColor color = { 0, };

                  if (str && str[0] != '\0')
                    clutter_color_parse (str, &color);

                  g_value_set_boxed (value, &color);
                  retval = TRUE;
                }
            }
          break;

        case G_TYPE_OBJECT:
          if (G_VALUE_HOLDS (value, GDK_TYPE_PIXBUF))
            {
              if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
                {
                  const gchar *str = g_value_get_string (&node_value);
                  GdkPixbuf *pixbuf = NULL;
                  gchar *path;
                  GError *error;

                  if (g_path_is_absolute (str))
                    path = g_strdup (str);
                  else
                    {
                      gchar *dirname = NULL;

                      if (script->priv->is_filename)
                        dirname = g_path_get_dirname (script->priv->filename);
                      else
                        dirname = g_get_current_dir ();

                      path = g_build_filename (dirname, str, NULL);
                      g_free (dirname);
                    }

                  error = NULL;
                  pixbuf = gdk_pixbuf_new_from_file (path, &error);
                  if (error)
                    {
                      g_warning ("Unable to open image at path `%s': %s",
                                 path,
                                 error->message);
                                 g_error_free (error);
                    }
                  else
                    {
                      g_value_set_object (value, pixbuf);
                      retval = TRUE;
                    }

                  g_free (path);
                }
            }
          else
            {
              if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
                {
                  const gchar *str = g_value_get_string (&node_value);
                  GObject *object = clutter_script_get_object (script, str);
                  if (object)
                    {
                      g_value_set_object (value, object);
                      retval = TRUE;
                    }
                }
            }
          break;

        default:
          retval = FALSE;
          break;
        }

      g_value_unset (&node_value);
      break;
    }

#if 0
  if (!retval)
    {
      if (pspec)
        {
          g_param_value_defaults (pspec, value);
          retval = TRUE;
        }
    }
#endif

  return retval;
}

static GList *
clutter_script_translate_parameters (ClutterScript  *script,
                                     GObject        *object,
                                     const gchar    *name,
                                     GList          *properties,
                                     GArray        **params)
{
  ClutterScriptable *scriptable = NULL;
  ClutterScriptableIface *iface = NULL;
  GList *l, *unparsed;
  gboolean parse_custom = FALSE;

  *params = g_array_new (FALSE, FALSE, sizeof (GParameter));

  if (CLUTTER_IS_SCRIPTABLE (object))
    {
      scriptable = CLUTTER_SCRIPTABLE (object);
      iface = CLUTTER_SCRIPTABLE_GET_IFACE (scriptable);

      if (iface->parse_custom_node)
        parse_custom = TRUE;
    }

  unparsed = NULL;

  for (l = properties; l != NULL; l = l->next)
    {
      PropertyInfo *pinfo = l->data;
      GParameter param = { NULL };
      gboolean res = FALSE;

      CLUTTER_NOTE (SCRIPT, "Parsing %s property (id:%s)",
                    pinfo->pspec ? "regular" : "custom",
                    pinfo->name);

      if (parse_custom)
        res = iface->parse_custom_node (scriptable, script, &param.value,
                                        pinfo->name,
                                        pinfo->node);

      if (!res)
        res = clutter_script_parse_node (script, &param.value,
                                         pinfo->name,
                                         pinfo->node,
                                         pinfo->pspec);

      if (!res)
        {
          unparsed = g_list_prepend (unparsed, pinfo);
          continue;
        }

      param.name = g_strdup (pinfo->name);
      
      g_array_append_val (*params, param);

      property_info_free (pinfo);
    }

  g_list_free (properties);

  return unparsed;
}

static GList *
clutter_script_get_parameters (ClutterScript  *script,
                               GType           gtype,
                               const gchar    *name,
                               GList          *properties,
                               GArray        **construct_params)
{
  GObjectClass *klass;
  GList *l, *unparsed;

  klass = g_type_class_ref (gtype);
  g_assert (klass != NULL);

  *construct_params = g_array_new (FALSE, FALSE, sizeof (GParameter));

  unparsed = NULL;

  for (l = properties; l != NULL; l = l->next)
    {
      PropertyInfo *pinfo = l->data;
      GParameter param = { NULL };
      GParamSpec *pspec = NULL;

      /* we allow custom property names for classes, so if we
       * don't find a corresponding GObject property for this
       * class we just skip it and let the class itself deal
       * with it later on
       */
      pspec = g_object_class_find_property (klass, pinfo->name);
      if (pspec)
        {
          pinfo->pspec = g_param_spec_ref (pspec);
        }
      else
        {
          pinfo->pspec = NULL;
          unparsed = g_list_prepend (unparsed, pinfo);
          continue;
        }

      if (!(pspec->flags & G_PARAM_CONSTRUCT_ONLY))
        {
          unparsed = g_list_prepend (unparsed, pinfo);
          continue;
        }

      if (!clutter_script_parse_node (script, &param.value,
                                      pinfo->name,
                                      pinfo->node,
                                      pinfo->pspec))
        {
          unparsed = g_list_prepend (unparsed, pinfo);
          continue;
        }

      param.name = g_strdup (pinfo->name);
      
      g_array_append_val (*construct_params, param);

      property_info_free (pinfo);
    }

  g_list_free (properties);

  g_type_class_unref (klass);

  return unparsed;
}

static void
apply_behaviours (ClutterScript *script,
                  ClutterActor  *actor,
                  ObjectInfo    *oinfo)
{
  GObject *object;
  GList *l, *unresolved;

  unresolved = NULL;
  for (l = oinfo->behaviours; l != NULL; l = l->next)
    {
      const gchar *name = l->data;

      object = clutter_script_get_object (script, name);
      if (!object)
        {
          ObjectInfo *oinfo;

          oinfo = g_hash_table_lookup (script->priv->objects, name);
          if (oinfo)
            object = clutter_script_construct_object (script, oinfo);
        }

      if (!object)
        {
          unresolved = g_list_prepend (unresolved, g_strdup (name));
          continue;
        }

      CLUTTER_NOTE (SCRIPT, "Applying behaviour `%s' to actor of type `%s'",
                    name,
                    g_type_name (G_OBJECT_TYPE (actor)));

      clutter_behaviour_apply (CLUTTER_BEHAVIOUR (object), actor);
    }

  g_list_foreach (oinfo->behaviours, (GFunc) g_free, NULL);
  g_list_free (oinfo->behaviours);

  oinfo->behaviours = unresolved;
}

static void
add_children (ClutterScript    *script,
              ClutterContainer *container,
              ObjectInfo       *oinfo)
{
  GObject *object;
  GList *l, *unresolved;

  unresolved = NULL;
  for (l = oinfo->children; l != NULL; l = l->next)
    {
      const gchar *name = l->data;

      object = clutter_script_get_object (script, name);
      if (!object)
        {
          ObjectInfo *oinfo;

          oinfo = g_hash_table_lookup (script->priv->objects, name);
          if (oinfo)
            object = clutter_script_construct_object (script, oinfo);
        }

      if (!object)
        {
          unresolved = g_list_prepend (unresolved, g_strdup (name));
          continue;
        }

      CLUTTER_NOTE (SCRIPT, "Adding children `%s' to actor of type `%s'",
                    name,
                    g_type_name (G_OBJECT_TYPE (container)));

      clutter_container_add_actor (container, CLUTTER_ACTOR (object));
    }

  g_list_foreach (oinfo->children, (GFunc) g_free, NULL);
  g_list_free (oinfo->children);

  oinfo->children = unresolved;
}

GObject *
clutter_script_construct_object (ClutterScript *script,
                                 ObjectInfo    *oinfo)
{
  GObject *object;
  guint i;
  GArray *params;
  GArray *construct_params;
  ClutterScriptable *scriptable = NULL;
  ClutterScriptableIface *iface = NULL;
  gboolean set_custom_property = FALSE;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
  g_return_val_if_fail (oinfo != NULL, NULL);

  /* we have completely updated the object */
  if (oinfo->object && !oinfo->has_unresolved)
    return oinfo->object;

  if (oinfo->gtype == G_TYPE_INVALID)
    {
      if (oinfo->type_func)
        oinfo->gtype = clutter_script_get_type_from_symbol (oinfo->type_func);
      else
        oinfo->gtype = clutter_script_get_type_from_class (oinfo->class_name);
    }

  if (oinfo->gtype == G_TYPE_INVALID)
    return NULL;

  if (oinfo->object)
    object = oinfo->object;
  else if (oinfo->gtype == CLUTTER_TYPE_STAGE)
    {
      /* the stage is a complex beast: we cannot create it using
       * g_object_newv() but we need clutter_script_get_parameters()
       * to add the GParamSpec to the PropertyInfo pspec member, so
       * that we don't have to implement every complex property (like
       * the "color" one) directly inside the ClutterStage class.
       */
      oinfo->properties = clutter_script_get_parameters (script,
                                                         oinfo->gtype,
                                                         oinfo->id,
                                                         oinfo->properties,
                                                         &construct_params);

      object = G_OBJECT (clutter_stage_get_default ());
      
      for (i = 0; i < construct_params->len; i++)
        {
          GParameter *param = &g_array_index (construct_params, GParameter, i);
      
          g_free ((gchar *) param->name);
          g_value_unset (&param->value);
        }

      g_array_free (construct_params, TRUE);
    }
  else
    {
      /* every other object: first, we get the construction parameters */
      oinfo->properties = clutter_script_get_parameters (script,
                                                         oinfo->gtype,
                                                         oinfo->id,
                                                         oinfo->properties,
                                                         &construct_params);

      object = g_object_newv (oinfo->gtype,
                              construct_params->len,
                              (GParameter *) construct_params->data);

      for (i = 0; i < construct_params->len; i++)
        {
          GParameter *param = &g_array_index (construct_params, GParameter, i);
      
          g_free ((gchar *) param->name);
          g_value_unset (&param->value);
        }

      g_array_free (construct_params, TRUE);
   }

  /* shortcut, to avoid typechecking every time */
  if (CLUTTER_IS_SCRIPTABLE (object))
    {
      scriptable = CLUTTER_SCRIPTABLE (object);
      iface = CLUTTER_SCRIPTABLE_GET_IFACE (scriptable);

      if (iface->set_custom_property)
        set_custom_property = TRUE;
    }

  /* then we get the rest of the parameters, asking the object itself
   * to translate them for us, if we cannot do that
   */
  oinfo->properties = clutter_script_translate_parameters (script,
                                                           object,
                                                           oinfo->id,
                                                           oinfo->properties,
                                                           &params);

  /* consume all the properties we could translate in this pass */
  for (i = 0; i < params->len; i++)
    {
      GParameter *param = &g_array_index (params, GParameter, i);

      CLUTTER_NOTE (SCRIPT,
                    "Setting %s property `%s' (type:%s) to object `%s' (id:%s)",
                    set_custom_property ? "custom" : "regular",
                    param->name,
                    g_type_name (G_VALUE_TYPE (&param->value)),
                    g_type_name (oinfo->gtype),
                    oinfo->id);

      if (set_custom_property)
        iface->set_custom_property (scriptable, script,
                                    param->name,
                                    &param->value);
      else
        g_object_set_property (object, param->name, &param->value);

      g_free ((gchar *) param->name);
      g_value_unset (&param->value);
    }
  g_array_free (params, TRUE);

  if (oinfo->children && CLUTTER_IS_CONTAINER (object))
    add_children (script, CLUTTER_CONTAINER (object), oinfo);

  if (oinfo->behaviours && CLUTTER_IS_ACTOR (object))
    apply_behaviours (script, CLUTTER_ACTOR (object), oinfo);

  if (oinfo->properties || oinfo->children || oinfo->behaviours)
    oinfo->has_unresolved = TRUE;
  else
    oinfo->has_unresolved = FALSE;

  if (scriptable)
    clutter_scriptable_set_name (scriptable, oinfo->id);
  else
    g_object_set_data_full (object, "clutter-script-name",
                            g_strdup (oinfo->id),
                            g_free);

  return object;
}

static void
construct_each_object (gpointer key,
                       gpointer value,
                       gpointer data)
{
  ClutterScript *script = data;
  ObjectInfo *oinfo = value;

  if (!oinfo->object)
    oinfo->object = clutter_script_construct_object (script, oinfo);
}

static void
json_parse_end (JsonParser *parser,
                gpointer    user_data)
{
  ClutterScript *script = user_data;
  ClutterScriptPrivate *priv = script->priv;

  g_hash_table_foreach (priv->objects, construct_each_object, script);
}

void
property_info_free (gpointer data)
{
  if (G_LIKELY (data))
    {
      PropertyInfo *pinfo = data;

      if (pinfo->pspec)
        g_param_spec_unref (pinfo->pspec);

      g_free (pinfo->name);

      g_slice_free (PropertyInfo, pinfo);
    }
}

void
object_info_free (gpointer data)
{
  if (G_LIKELY (data))
    {
      ObjectInfo *oinfo = data;

      g_free (oinfo->id);
      g_free (oinfo->class_name);
      g_free (oinfo->type_func);

      g_list_foreach (oinfo->properties, (GFunc) property_info_free, NULL);
      g_list_free (oinfo->properties);

      /* these are ids */
      g_list_foreach (oinfo->children, (GFunc) g_free, NULL);
      g_list_free (oinfo->children);

      g_list_foreach (oinfo->behaviours, (GFunc) g_free, NULL);
      g_list_free (oinfo->behaviours);

      if (oinfo->is_toplevel && oinfo->object)
        {
          g_object_unref (oinfo->object);
          oinfo->object = NULL;
        }

      if (oinfo->is_unmerged && oinfo->object)
        {
          clutter_actor_destroy (CLUTTER_ACTOR (oinfo->object));
          oinfo->object = NULL;
        }

      g_slice_free (ObjectInfo, oinfo);
    }
}

static void
clutter_script_finalize (GObject *gobject)
{
  ClutterScriptPrivate *priv = CLUTTER_SCRIPT_GET_PRIVATE (gobject);

  g_object_unref (priv->parser);
  g_hash_table_destroy (priv->objects);
  g_free (priv->filename);

  G_OBJECT_CLASS (clutter_script_parent_class)->finalize (gobject);
}

static void
clutter_script_class_init (ClutterScriptClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  g_type_class_add_private (klass, sizeof (ClutterScriptPrivate));

  gobject_class->finalize = clutter_script_finalize;
}

static void
clutter_script_init (ClutterScript *script)
{
  ClutterScriptPrivate *priv;

  script->priv = priv = CLUTTER_SCRIPT_GET_PRIVATE (script);

  priv->parser = json_parser_new ();
  g_signal_connect (priv->parser,
                    "object-end", G_CALLBACK (json_object_end),
                    script);
  g_signal_connect (priv->parser,
                    "parse-end", G_CALLBACK (json_parse_end),
                    script);

  priv->is_filename = FALSE;
  priv->last_merge_id = 0;

  priv->objects = g_hash_table_new_full (g_str_hash, g_str_equal,
                                         g_free,
                                         object_info_free);
}

/**
 * clutter_script_new:
 *
 * Creates a new #ClutterScript instance. #ClutterScript can be used
 * to load objects definitions for scenegraph elements, like actors,
 * or behavioural elements, like behaviours and timelines. The
 * definitions must be encoded using the JavaScript Object Notation (JSON)
 * language.
 *
 * Return value: the newly created #ClutterScript instance. Use
 *   g_object_unref() when done.
 *
 * Since: 0.6
 */
ClutterScript *
clutter_script_new (void)
{
  return g_object_new (CLUTTER_TYPE_SCRIPT, NULL);
}

/**
 * clutter_script_load_from_file:
 * @script: a #ClutterScript
 * @filename: the full path to the definition file
 * @error: return location for a #GError, or %NULL
 *
 * Loads the definitions from @filename into @script and merges with
 * the currently loaded ones, if any.
 *
 * Return value: on error, zero is returned and @error is set
 *   accordingly. On success, the merge id for the UI definitions is
 *   returned. You can use the merge id with clutter_script_unmerge().
 *
 * Since: 0.6
 */
guint
clutter_script_load_from_file (ClutterScript  *script,
                               const gchar    *filename,
                               GError        **error)
{
  ClutterScriptPrivate *priv;
  GError *internal_error;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), 0);
  g_return_val_if_fail (filename != NULL, 0);

  priv = script->priv;

  g_free (priv->filename);
  priv->filename = g_strdup (filename);
  priv->is_filename = TRUE;
  priv->last_merge_id += 1;

  internal_error = NULL;
  json_parser_load_from_file (priv->parser, filename, &internal_error);
  if (internal_error)
    {
      g_propagate_error (error, internal_error);
      priv->last_merge_id -= 1;
      return 0;
    }

  return priv->last_merge_id;
}

/**
 * clutter_script_load_from_data:
 * @script: a #ClutterScript
 * @data: a buffer containing the definitions
 * @length: the length of the buffer, or -1 if @data is a NUL-terminated
 *   buffer
 * @error: return location for a #GError, or %NULL
 *
 * Loads the definitions from @data into @script and merges with
 * the currently loaded ones, if any.
 *
 * Return value: on error, zero is returned and @error is set
 *   accordingly. On success, the merge id for the UI definitions is
 *   returned. You can use the merge id with clutter_script_unmerge().
 *
 * Since: 0.6
 */
guint
clutter_script_load_from_data (ClutterScript  *script,
                               const gchar    *data,
                               gsize           length,
                               GError        **error)
{
  ClutterScriptPrivate *priv;
  GError *internal_error;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), 0);
  g_return_val_if_fail (data != NULL, 0);

  priv = script->priv;

  g_free (priv->filename);
  priv->filename = NULL;
  priv->is_filename = FALSE;
  priv->last_merge_id += 1;

  internal_error = NULL;
  json_parser_load_from_data (priv->parser, data, length, &internal_error);
  if (internal_error)
    {
      g_propagate_error (error, internal_error);
      priv->last_merge_id -= 1;
      return 0;
    }

  return priv->last_merge_id;
}

/**
 * clutter_script_get_object:
 * @script: a #ClutterScript
 * @name: the name of the object to retrieve
 *
 * Retrieves the object bound to @name. This function does not increment
 * the reference count of the returned object.
 *
 * Return value: the named object, or %NULL if no object with the
 *   given name was available
 *
 * Since: 0.6
 */
GObject *
clutter_script_get_object (ClutterScript *script,
                           const gchar   *name)
{
  ObjectInfo *oinfo;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
  g_return_val_if_fail (name != NULL, NULL);

  oinfo = g_hash_table_lookup (script->priv->objects, name);
  if (!oinfo)
    return NULL;

  return clutter_script_construct_object (script, oinfo);
}

static GList *
clutter_script_get_objects_valist (ClutterScript *script,
                                   const gchar   *first_name,
                                   va_list        args)
{
  GList *retval = NULL;
  const gchar *name;

  name = first_name;
  while (name)
    {
      retval =
        g_list_prepend (retval, clutter_script_get_object (script, name));

      name = va_arg (args, gchar*);
    }

  return g_list_reverse (retval);
}

/**
 * clutter_script_get_objects:
 * @script: a #ClutterScript
 * @first_name: the name of the first object to retrieve
 * @Varargs: a %NULL-terminated list of names
 *
 * Retrieves a list of objects for the given names. This function does
 * not increment the reference count of the returned objects.
 *
 * Return value: a newly allocated #GList containing the found objects,
 *   or %NULL. Use g_list_free() when done using it.
 *
 * Since: 0.6
 */
GList *
clutter_script_get_objects (ClutterScript *script,
                            const gchar   *first_name,
                            ...)
{
  GList *retval = NULL;
  va_list var_args;

  g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
  g_return_val_if_fail (first_name != NULL, NULL);

  va_start (var_args, first_name);
  retval = clutter_script_get_objects_valist (script, first_name, var_args);
  va_end (var_args);

  return retval;
}

typedef struct {
  ClutterScript *script;
  guint merge_id;
  GSList *ids;
} UnmergeData;

static void
remove_by_merge_id (gpointer key,
                    gpointer value,
                    gpointer data)
{
  gchar *name = key;
  ObjectInfo *oinfo = value;
  UnmergeData *unmerge_data = data;

  if (oinfo->merge_id == unmerge_data->merge_id)
    {
      CLUTTER_NOTE (SCRIPT,
                    "Unmerging object (id:%s, type:%s, merge-id:%d)",
                    oinfo->id,
                    oinfo->class_name,
                    oinfo->merge_id);

      unmerge_data->ids = g_slist_prepend (unmerge_data->ids, g_strdup (name));
      oinfo->is_unmerged = TRUE;
    }
}

/**
 * clutter_script_unmerge_objects:
 * @script: a #ClutterScript
 * @merge_id: merge id returned when loading a UI definition
 *
 * Unmerges the objects identified by @merge_id.
 *
 * Since: 0.6
 */
void
clutter_script_unmerge_objects (ClutterScript *script,
                                guint          merge_id)
{
  ClutterScriptPrivate *priv;
  UnmergeData data;
  GSList *l;

  g_return_if_fail (CLUTTER_IS_SCRIPT (script));
  g_return_if_fail (merge_id > 0);

  priv = script->priv;

  data.script = script;
  data.merge_id = merge_id;
  data.ids = NULL;
  g_hash_table_foreach (priv->objects, remove_by_merge_id, &data);

  for (l = data.ids; l != NULL; l = l->next)
    g_hash_table_remove (priv->objects, l->data);

  g_slist_foreach (data.ids, (GFunc) g_free, NULL);
  g_slist_free (data.ids);

  clutter_script_ensure_objects (script);
}

/**
 * clutter_script_ensure_object:
 * @script: a #ClutterScript
 *
 * Ensure that every object defined inside @script is correctly
 * constructed. You should rarely need to use this function.
 *
 * Since: 0.6
 */
void
clutter_script_ensure_objects (ClutterScript *script)
{
  ClutterScriptPrivate *priv;

  g_return_if_fail (CLUTTER_IS_SCRIPT (script));

  priv = script->priv;
  g_hash_table_foreach (priv->objects, construct_each_object, script);  
}

GQuark
clutter_script_error_quark (void)
{
  return g_quark_from_static_string ("clutter-script-error");
}