/*
 * 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 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).
 *
 * 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>
 *
 * 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-script.h"
#include "clutter-script-private.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;

  JsonParser *parser;

  gchar *filename;
  guint is_filename : 1;
};

G_DEFINE_TYPE (ClutterScript, clutter_script, G_TYPE_OBJECT);

static void object_info_free (gpointer data);

static GType
resolve_type (const gchar *symbol)
{
  static GModule *module = NULL;
  GTypeGetFunc func;
  GType gtype = G_TYPE_INVALID;

  if (!module)
    module = g_module_open (NULL, 0);
  
  if (g_module_symbol (module, symbol, (gpointer)&func))
    gtype = func ();
  
  return gtype;
}

/* tries to map a name in camel case into the corresponding get_type()
 * function, e.g.:
 *
 *   ClutterRectangle -> clutter_rectangle_get_type
 *   ClutterCloneTexture -> clutter_clone_texture_get_type
 *
 * taken from GTK+, gtkbuilder.c
 */
static GType
resolve_type_lazily (const gchar *name)
{
  static GModule *module = NULL;
  GTypeGetFunc func;
  GString *symbol_name = g_string_new ("");
  char c, *symbol;
  int i;
  GType gtype = G_TYPE_INVALID;

  if (!module)
    module = g_module_open (NULL, 0);
  
  for (i = 0; name[i] != '\0'; i++)
    {
      c = name[i];
      /* skip if uppercase, first or previous is uppercase */
      if ((c == g_ascii_toupper (c) &&
           i > 0 && name[i-1] != g_ascii_toupper (name[i-1])) ||
          (i > 2 && name[i]   == g_ascii_toupper (name[i]) &&
           name[i-1] == g_ascii_toupper (name[i-1]) &&
           name[i-2] == g_ascii_toupper (name[i-2])))
        g_string_append_c (symbol_name, '_');
      g_string_append_c (symbol_name, g_ascii_tolower (c));
    }
  g_string_append (symbol_name, "_get_type");
  
  symbol = g_string_free (symbol_name, FALSE);

  if (g_module_symbol (module, symbol, (gpointer)&func))
    gtype = func ();
  
  g_free (symbol);

  return gtype;
}

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

  if (!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;

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

  CLUTTER_NOTE (SCRIPT, "Looking for `%s' alpha function", symbol);

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

  g_free (symbol);

  return func;
}

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

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

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;

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

      if (JSON_NODE_TYPE (node) == JSON_NODE_VALUE)
        {
          PropertyInfo *pinfo = g_slice_new (PropertyInfo);
          GValue value = { 0, };

          pinfo->property_name = g_strdup (name);

          json_node_get_value (node, &value);
          g_value_init (&pinfo->value, G_VALUE_TYPE (&value));
          g_value_transform (&value, &pinfo->value);
          g_value_unset (&value);

          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 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 ClutterUnit
get_units_from_node (JsonNode *node)
{
  ClutterUnit retval = 0;
  GValue value = { 0, };

  if (JSON_NODE_TYPE (node) != JSON_NODE_VALUE)
    return 0;

  json_node_get_value (node, &value);
  switch (G_VALUE_TYPE (&value))
    {
    case G_TYPE_INT:
      retval = CLUTTER_UNITS_FROM_INT (g_value_get_int (&value));
      break;
    
    default:
      break;
    }

  return retval;
}

static PropertyInfo *
parse_member_to_property (ClutterScript *script,
                          ObjectInfo    *info,
                          const gchar   *name,
                          JsonNode      *node)
{
  PropertyInfo *retval = NULL;
  GValue value = { 0, };

  switch (JSON_NODE_TYPE (node))
    {
    case JSON_NODE_VALUE:
      retval = g_slice_new (PropertyInfo);
      retval->property_name = g_strdup (name);

      json_node_get_value (node, &value);
      g_value_init (&retval->value, G_VALUE_TYPE (&value));
      g_value_transform (&value, &retval->value);
      g_value_unset (&value);
      break;

    case JSON_NODE_OBJECT:
      if (strcmp (name, "alpha") == 0)
        {
          JsonObject *object = json_node_get_object (node);
          ClutterAlpha *alpha = NULL;
          ClutterTimeline *timeline = NULL;
          ClutterAlphaFunc func = NULL;
          JsonNode *val;
          gboolean unref_timeline = FALSE;

          retval = g_slice_new (PropertyInfo);
          retval->property_name = g_strdup (name);

          alpha = clutter_alpha_new ();
          
          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)
            func = resolve_alpha_func (json_node_get_string (val));

          alpha = g_object_new (CLUTTER_TYPE_ALPHA, NULL);
          clutter_alpha_set_func (alpha, func, NULL, NULL);
          clutter_alpha_set_timeline (alpha, timeline);
          if (unref_timeline)
            g_object_unref (timeline);

          g_value_init (&retval->value, CLUTTER_TYPE_ALPHA);
          g_value_set_object (&retval->value, G_OBJECT (alpha));
        }
      break;

    case JSON_NODE_ARRAY:
      if (strcmp (name, "margin") == 0)
        {
          JsonArray *array = json_node_get_array (node);
          JsonNode *val;
          gint array_len, i;
          ClutterMargin margin = { 0, };

          array_len = json_array_get_length (array);
          for (i = 0; i < array_len; i++)
            {
              ClutterUnit units;

              val = json_array_get_element (array, i);
              units = get_units_from_node (val);

              switch (i)
                {
                case 0:
                  margin.top = units; 
                  margin.right = margin.top;
                  margin.bottom = margin.top;
                  margin.left = margin.top;
                  break;
                case 1:
                  margin.right = margin.left = units; 
                  break;
                case 2:
                  margin.bottom = units; 
                  break;
                case 3:
                  margin.left = units; 
                  break;
                }
            }

          retval = g_slice_new (PropertyInfo);
          retval->property_name = g_strdup (name);
          g_value_init (&retval->value, CLUTTER_TYPE_MARGIN);
          g_value_set_boxed (&retval->value, &margin);
        }
      else if (strcmp (name, "padding") == 0)
        {
          JsonArray *array = json_node_get_array (node);
          JsonNode *val;
          gint array_len, i;
          ClutterPadding padding = { 0, };

          array_len = json_array_get_length (array);
          for (i = 0; i < array_len; i++)
            {
              ClutterUnit units;

              val = json_array_get_element (array, i);
              units = get_units_from_node (val);

              switch (i)
                {
                case 0:
                  padding.top = units;
                  padding.right = padding.top;
                  padding.bottom = padding.top;
                  padding.left = padding.top;
                  break;
                case 1:
                  padding.right = padding.left = units;
                  break;
                case 2:
                  padding.bottom = units; 
                  break;
                case 3:
                  padding.left = units; 
                  break;
                }
            }

          retval = g_slice_new (PropertyInfo);
          retval->property_name = g_strdup (name);
          g_value_init (&retval->value, CLUTTER_TYPE_PADDING);
          g_value_set_boxed (&retval->value, &padding);
        }
      else if (strcmp (name, "clip") == 0)
        {
          JsonArray *array = json_node_get_array (node);
          JsonNode *val;
          gint i;
          ClutterGeometry geom = { 0, };

          /* this is quite evil indeed */
          for (i = 0; i < json_array_get_length (array); i++)
            {
              val = json_array_get_element (array, i);
              switch (i)
                {
                case 0: geom.x      = json_node_get_int (val); break;
                case 1: geom.y      = json_node_get_int (val); break;
                case 2: geom.width  = json_node_get_int (val); break;
                case 3: geom.height = json_node_get_int (val); break;
                }
            }

          retval = g_slice_new (PropertyInfo);
          retval->property_name = g_strdup (name);
          g_value_init (&retval->value, CLUTTER_TYPE_GEOMETRY);
          g_value_set_boxed (&retval->value, &geom);
        }
      else if ((strcmp (name, "children") == 0) ||
               (strcmp (name, "behaviours") == 0))
        {
          JsonArray *array = json_node_get_array (node);
          JsonNode *val;
          gint i, array_len;
          GList *children;

          children = NULL;
          array_len = json_array_get_length (array);
          for (i = 0; i < array_len; i++)
            {
              const gchar *id;

              val = json_array_get_element (array, i);
              id = get_id_from_node (val);
              if (id)
                children = g_list_prepend (children, g_strdup (id));
              else
                warn_invalid_value (script, "id", val);
            }
          
          if (name[0] == 'c') /* children */
            info->children = g_list_reverse (children);
          else
            info->behaviours = g_list_reverse (children);
        }
      break;

    case JSON_NODE_NULL:
      break;
    }

  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;

  /* ignore any non-named object */
  if (!json_object_has_member (object, "id"))
    return;

  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);
  
  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"))
    {
      /* remove "type_func", as it's not usef by anything else */
      val = json_object_get_member (object, "type_func");
      oinfo->type_func = json_node_dup_string (val);
      json_object_remove_member (object, "type_func");
    }

  oinfo->is_toplevel = FALSE;

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

      if (strcmp (name, "id") == 0 || strcmp (name, "type") == 0)
        continue;

      val = json_object_get_member (object, name);

      pinfo = parse_member_to_property (script, oinfo, name, val);
      if (!pinfo)
        continue;

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

      CLUTTER_NOTE (SCRIPT, "Added property `%s' (type:%s) for class `%s'",
                    pinfo->property_name,
                    g_type_name (G_VALUE_TYPE (&pinfo->value)),
                    oinfo->class_name);
    }
  g_list_free (members);

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

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

/* the property translation sequence is split in two: the first
 * half is done in parse_member_to_property(), which translates
 * from JSON data types into valid property types. the second
 * half is done here, where property types are translated into
 * the correct type for the given property GType
 */
static gboolean
translate_property (ClutterScript *script,
                    GType          gtype,
                    const gchar   *name,
                    const GValue  *src,
                    GValue        *dest)
{
  gboolean retval = FALSE;

  /* colors have a parse function, so we can pass it the
   * string we get from the parser
   */
  if (strcmp (name, "color") == 0)
    {
      ClutterColor color = { 0, };
      const gchar *color_str;

      if (G_VALUE_HOLDS (src, G_TYPE_STRING))
        {
          color_str = g_value_get_string (src);
          clutter_color_parse (color_str, &color);
        }

      g_value_init (dest, CLUTTER_TYPE_COLOR);
      g_value_set_boxed (dest, &color);

      return TRUE;
    }

  /* pixbufs are specified using the path to the file name; the
   * path can be absolute or relative to the current directory.
   * we need to load the pixbuf from the file and print a
   * warning here in case it didn't work.
   */
  if (strcmp (name, "pixbuf") == 0)
    {
      GdkPixbuf *pixbuf = NULL;
      const gchar *string;
      gchar *path;
      GError *error;

      if (G_VALUE_HOLDS (src, G_TYPE_STRING))
        string = g_value_get_string (src);
      else
        return FALSE;

      if (g_path_is_absolute (string))
        path = g_strdup (string);
      else
        {
          if (script->priv->is_filename)
            {
              gchar *dirname;

              dirname = g_path_get_dirname (script->priv->filename);
              path = g_build_filename (dirname, string, NULL);
              
              g_free (dirname);
            }
          else
            {
              gchar *dirname;
              
              dirname = g_get_current_dir ();
              path = g_build_filename (dirname, string, NULL);

              g_free (dirname);
            }
        }

      error = NULL;
      pixbuf = gdk_pixbuf_new_from_file (path, &error);
      if (error)
        {
          g_warning ("Unable to open pixbuf at path `%s': %s",
                     path,
                     error->message);
          g_error_free (error);
          g_free (path);
          return FALSE;
        }

      CLUTTER_NOTE (SCRIPT, "Setting pixbuf [%p] from file `%s'",
                    pixbuf,
                    path);
      
      g_free (path);
      
      g_value_init (dest, GDK_TYPE_PIXBUF);
      g_value_set_object (dest, pixbuf);

      return TRUE;
    }

  CLUTTER_NOTE (SCRIPT, "Copying property `%s' to type `%s'",
                name,
                g_type_name (gtype));

  /* fall back scenario */
  g_value_init (dest, gtype);

  switch (G_TYPE_FUNDAMENTAL (gtype))
    {
    case G_TYPE_ULONG:
      g_value_set_ulong (dest, (gulong) g_value_get_int (src));
      break;

    case G_TYPE_UINT:
      g_value_set_uint (dest, (guint) g_value_get_int (src));
      retval = TRUE;
      break;

    case G_TYPE_UCHAR:
      g_value_set_uchar (dest, (guchar) g_value_get_int (src));
      retval = TRUE;
      break;

    case G_TYPE_ENUM:
    case G_TYPE_FLAGS:
      {
        gint value;

        if (G_VALUE_HOLDS (src, G_TYPE_STRING))
          {
            const gchar *string = g_value_get_string (src);

            if (G_TYPE_FUNDAMENTAL (gtype) == G_TYPE_ENUM)
              retval = clutter_script_enum_from_string (gtype, string, &value);
            else
              retval = clutter_script_flags_from_string (gtype, string, &value);
          }
        else if (G_VALUE_HOLDS (src, G_TYPE_INT))
          {
            value = g_value_get_int (src);
          }

        if (retval)
          {
            if (G_TYPE_FUNDAMENTAL (gtype) == G_TYPE_ENUM)
              g_value_set_enum (dest, value);
            else
              g_value_set_flags (dest, value);
          }
        }
      break;

    default:
      g_value_copy (src, dest);
      retval = TRUE;
      break;
    }

  return retval;
}

/* translates the PropertyInfo structure into a GParameter array to
 * be fed to g_object_newv()
 */
static void
translate_properties (ClutterScript  *script,
                      ObjectInfo     *oinfo,
                      guint          *n_params,
                      GParameter    **params)
{
  GList *l;
  GParamSpec *pspec;
  GObjectClass *oclass;
  GArray *parameters;

  oclass = g_type_class_ref (oinfo->gtype);
  g_assert (oclass != NULL);

  parameters = g_array_new (FALSE, FALSE, sizeof (GParameter));

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

      pspec = g_object_class_find_property (oclass, pinfo->property_name);
      if (!pspec)
        {
          g_warning ("Unknown property `%s' for class `%s'",
                     pinfo->property_name,
                     g_type_name (oinfo->gtype));
          continue;
        }

      param.name = pinfo->property_name;
      if (!translate_property (script, G_PARAM_SPEC_VALUE_TYPE (pspec),
                               param.name,
                               &pinfo->value,
                               &param.value))
        {
          g_warning ("Unable to set property `%s' for class `%s'",
                     pinfo->property_name,
                     g_type_name (oinfo->gtype));
          continue;
        }

      g_array_append_val (parameters, param);
    }

  *n_params = parameters->len;
  *params = (GParameter *) g_array_free (parameters, FALSE);

  g_type_class_unref (oclass);
}

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

  for (l = 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);
          else
            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);
    }
}

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

  for (l = 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);
          else
            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));
    }
}

static GObject *
construct_stage (ClutterScript *script,
                 ObjectInfo    *oinfo)
{
  GObjectClass *oclass = g_type_class_ref (CLUTTER_TYPE_STAGE);
  GList *l;

  if (oinfo->object)
    return oinfo->object;

  oinfo->object = G_OBJECT (clutter_stage_get_default ());

  for (l = oinfo->properties; l; l = l->next)
    {
      PropertyInfo *pinfo = l->data;
      const gchar *name = pinfo->property_name;
      GParamSpec *pspec;
      GValue value = { 0, };

      pspec = g_object_class_find_property (oclass, name);
      if (!pspec)
        {
          g_warning ("Unknown property `%s' for class `ClutterStage'",
                     name);
          continue;
        }

      if (!translate_property (script, G_PARAM_SPEC_VALUE_TYPE (pspec),
                               name,
                               &pinfo->value,
                               &value))
        {
          g_warning ("Unable to set property `%s' for class `%s'",
                     pinfo->property_name,
                     g_type_name (oinfo->gtype));
          continue;
        }

      g_object_set_property (oinfo->object, name, &value);

      g_value_unset (&value);
    }

  g_type_class_unref (oclass);

  if (oinfo->children)
    {
      /* we know ClutterStage is a ClutterContainer */
      add_children (script,
                    CLUTTER_CONTAINER (oinfo->object),
                    oinfo->children);
    }

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

  return oinfo->object;
}

GObject *
clutter_script_construct_object (ClutterScript *script,
                                 ObjectInfo    *oinfo)
{
  guint n_params, i;
  GParameter *params;

  if (oinfo->object)
    return oinfo->object;

  if (oinfo->gtype == G_TYPE_INVALID)
    {
      if (oinfo->type_func)
        oinfo->gtype = resolve_type (oinfo->type_func);
      else
        oinfo->gtype = resolve_type_lazily (oinfo->class_name);

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

  /* the stage is a special case: it's a singleton, it cannot
   * be created by the user and it's owned by the backend. hence,
   * we cannot follow the usual pattern here
   */
  if (g_type_is_a (oinfo->gtype, CLUTTER_TYPE_STAGE))
    return construct_stage (script, oinfo);

  params = NULL;
  translate_properties (script, oinfo, &n_params, &params);

  CLUTTER_NOTE (SCRIPT, "Creating instance for type `%s' (params:%d)",
                g_type_name (oinfo->gtype),
                n_params);

  oinfo->object = g_object_newv (oinfo->gtype, n_params, params);

  for (i = 0; i < n_params; i++)
    {
      GParameter param = params[i];
      g_value_unset (&param.value);
    }

  g_free (params);

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

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

  if (CLUTTER_IS_BEHAVIOUR (oinfo->object) ||
      CLUTTER_IS_TIMELINE (oinfo->object))
    oinfo->is_toplevel = TRUE;

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

  return oinfo->object;
}

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

  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, for_each_object, script);
}

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

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

      for (l = oinfo->properties; l; l = l->next)
        {
          PropertyInfo *pinfo = l->data;

          g_free (pinfo->property_name);
          g_value_unset (&pinfo->value);
        }
      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);

      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, a positive integer is returned.
 *
 * 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;

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

  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, a positive integer is returned.
 *
 * 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;

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

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

gboolean
clutter_script_enum_from_string (GType        type, 
                                 const gchar *string,
                                 gint        *enum_value)
{
  GEnumClass *eclass;
  GEnumValue *ev;
  gchar *endptr;
  gint value;
  gboolean retval = TRUE;
  
  g_return_val_if_fail (G_TYPE_IS_ENUM (type), 0);
  g_return_val_if_fail (string != NULL, 0);
  
  value = strtoul (string, &endptr, 0);
  if (endptr != string) /* parsed a number */
    *enum_value = value;
  else
    {
      eclass = g_type_class_ref (type);
      ev = g_enum_get_value_by_name (eclass, string);
      if (!ev)
	ev = g_enum_get_value_by_nick (eclass, string);

      if (ev)
	*enum_value = ev->value;
      else
        retval = FALSE;
      
      g_type_class_unref (eclass);
    }

  return retval;
}

gboolean
clutter_script_flags_from_string (GType        type, 
                                  const gchar *string,
                                  gint        *flags_value)
{
  GFlagsClass *fclass;
  gchar *endptr, *prevptr;
  guint i, j, ret, value;
  gchar *flagstr;
  GFlagsValue *fv;
  const gchar *flag;
  gunichar ch;
  gboolean eos;

  g_return_val_if_fail (G_TYPE_IS_FLAGS (type), 0);
  g_return_val_if_fail (string != 0, 0);

  ret = TRUE;
  
  value = strtoul (string, &endptr, 0);
  if (endptr != string) /* parsed a number */
    *flags_value = value;
  else
    {
      fclass = g_type_class_ref (type);

      flagstr = g_strdup (string);
      for (value = i = j = 0; ; i++)
	{
	  
	  eos = flagstr[i] == '\0';
	  
	  if (!eos && flagstr[i] != '|')
	    continue;
	  
	  flag = &flagstr[j];
	  endptr = &flagstr[i];
	  
	  if (!eos)
	    {
	      flagstr[i++] = '\0';
	      j = i;
	    }
	  
	  /* trim spaces */
	  for (;;)
	    {
	      ch = g_utf8_get_char (flag);
	      if (!g_unichar_isspace (ch))
		break;
	      flag = g_utf8_next_char (flag);
	    }
	  
	  while (endptr > flag)
	    {
	      prevptr = g_utf8_prev_char (endptr);
	      ch = g_utf8_get_char (prevptr);
	      if (!g_unichar_isspace (ch))
		break;
	      endptr = prevptr;
	    }
	  
	  if (endptr > flag)
	    {
	      *endptr = '\0';
	      fv = g_flags_get_value_by_name (fclass, flag);
	      
	      if (!fv)
		fv = g_flags_get_value_by_nick (fclass, flag);
	      
	      if (fv)
		value |= fv->value;
	      else
		{
		  ret = FALSE;
		  break;
		}
	    }
	  
	  if (eos)
	    {
	      *flags_value = value;
	      break;
	    }
	}
      
      g_free (flagstr);
      
      g_type_class_unref (fclass);
    }

  return ret;
}

gboolean
clutter_script_value_from_data (ClutterScript  *script,
                                GType           gtype,
                                const gchar    *data,
                                GValue         *value,
                                GError        **error)
{
  return FALSE;
}

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