/*
 * glade-preview-template.c
 *
 * Copyright (C) 2013 Juan Pablo Ugarte
   *
 * Author: Juan Pablo Ugarte <juanpablougarte@gmail.com>
 *
 * 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.1 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 program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <config.h>

#include <string.h>
#include "glade-utils.h"
#include "glade-preview-template.h"

typedef struct
{
  GTypeInfo info;
  GString *template_string;
  GBytes *template_data;

  GtkBuilderConnectFunc connect_func;
  gpointer connect_data;
  gint count;
} TypeData;

/* We need to call gtk_widget_init_template() in the instance init for the
* template to work.
*/
static void
template_init (GTypeInstance *instance, gpointer g_class)
{  
  gtk_widget_init_template (GTK_WIDGET (instance));
}

static void
template_connect_function (GtkBuilder   *builder,
                           GObject      *object,
                           const gchar  *signal_name,
                           const gchar  *handler_name,
                           GObject      *connect_object,
                           GConnectFlags flags,
                           gpointer      data)
{
  /* Ignore signal connections */
}

/* Need to associate the class with a template */
static void
template_class_init (gpointer g_class, gpointer user_data)
{
  TypeData *data = user_data;

  gtk_widget_class_set_template (g_class, data->template_data);

  if (data->connect_func && data->connect_data)
    gtk_widget_class_set_connect_func (g_class, data->connect_func, data->connect_data, NULL);
  else
    gtk_widget_class_set_connect_func (g_class, template_connect_function, NULL, NULL);
}

static GQuark type_data_quark = 0;

static GType
template_generate_type (const gchar *name,
                        const gchar *parent_name,
                        GString *template_string,
                        GtkBuilderConnectFunc connect_func,
                        gpointer connect_data)
{
  GType parent_type, retval;
  gchar *real_name = NULL;
  GTypeQuery query;
  TypeData *data;

  g_return_val_if_fail (name != NULL, 0);
  g_return_val_if_fail (parent_name != NULL, 0);

  parent_type = glade_util_get_type_from_name (parent_name, FALSE);
  g_return_val_if_fail (parent_type != 0, 0);

  if ((retval = g_type_from_name (name)) &&
      (data = g_type_get_qdata (retval, type_data_quark)))
    {
      /* Type already registered! reuse TypeData
       * 
       * If the template and parent class are the same there is no need to
       * register a new type
       */
      if (g_type_parent (retval) == parent_type &&
          template_string->len == data->template_string->len && 
          g_strcmp0 (template_string->str, data->template_string->str) == 0)
        return retval;

      real_name = g_strdup_printf ("GladePreviewTemplate_%s_%d", name, data->count);
    }
  else
    {
      /* We only allocate a TypeData struct once for each type class */
      data = g_new0 (TypeData, 1);
    }

  g_type_query (parent_type, &query);
  g_return_val_if_fail (query.type != 0, 0);

  /* Free old template string */
  if (data->template_string)
    g_string_free (data->template_string, TRUE);

  /* And old bytes reference to template string */
  if (data->template_data)
    g_bytes_unref (data->template_data);

  /* Take ownership, will be freed next time we want to create this type */
  data->template_string = template_string;

  data->info.class_size = query.class_size;
  data->info.instance_size = query.instance_size;
  data->info.class_init = template_class_init;
  data->info.instance_init = template_init;
  data->info.class_data = data;
  data->template_data = g_bytes_new_static (template_string->str, template_string->len);
  data->connect_func = connect_func;
  data->connect_data = connect_data;

  retval = g_type_register_static (parent_type, real_name ? real_name : name, &data->info, 0);

  /* bind TypeData struct with GType */
  if (!data->count)
    g_type_set_qdata (retval, type_data_quark, data);

  data->count++;

  g_free (real_name);

  return retval;
}

typedef struct
{
  gboolean is_template;
  GString *xml;
  gchar *klass, *parent;
  gint indent;
} ParseData;

static void
start_element (GMarkupParseContext  *context,
               const gchar          *element_name,
               const gchar         **attribute_names,
               const gchar         **attribute_values,
               gpointer              user_data,
               GError              **error)
{
  ParseData *state = user_data;
  gboolean is_template = FALSE;
  gint i;

  g_string_append_printf (state->xml, "<%s", element_name);

  if (g_strcmp0 (element_name, "template") == 0)
    state->is_template = is_template = TRUE;

  for (i = 0; attribute_names[i]; i++)
    {
      gchar *escaped_value = g_markup_escape_text (attribute_values[i], -1);

      if (is_template)
        {
          if (!g_strcmp0 (attribute_names[i], "class"))
            {
              TypeData *data;
              GType type;

              state->klass = g_strdup (attribute_values[i]);

              /* If we are in a template then we need to replace the class with
               * the fake name template_generate_type() will use to register
                 * a new class
               */
              if ((type = g_type_from_name (state->klass)) &&
                  (data = g_type_get_qdata (type, type_data_quark)))
                {
                  g_free (escaped_value);
                  escaped_value = g_strdup_printf ("GladePreviewTemplate_%s_%d", state->klass, data->count);
                }
            }
          else if (!g_strcmp0 (attribute_names[i], "parent"))
            state->parent = g_strdup (attribute_values[i]);
        }

      g_string_append_printf (state->xml, " %s=\"%s\"",
                              attribute_names[i], escaped_value);
      g_free (escaped_value);
    }

  g_string_append (state->xml, ">");
}

static void
end_element (GMarkupParseContext *context,
             const gchar         *element_name,
             gpointer             user_data,
             GError             **error)
{
  ParseData *state = user_data;
  g_string_append_printf (state->xml, "</%s>", element_name);
}

static void
text (GMarkupParseContext *context,
      const gchar         *text,
      gsize                text_len,
      gpointer             user_data,
      GError             **error)
{
  gchar *escaped_text = g_markup_escape_text (text, text_len);
  ParseData *state = user_data;

  g_string_append_len (state->xml, escaped_text, -1);

  g_free (escaped_text);
}

static void
passthrough (GMarkupParseContext *context,
             const gchar         *text,
             gsize                text_len,
             gpointer             user_data,
             GError             **error)
{
  ParseData *state = user_data;
  g_string_append_len (state->xml, text, text_len);
  g_string_append_c (state->xml, '\n');
}

GObject *
glade_preview_template_object_new (const gchar          *template_data,
                                   gsize                 len,
                                   GtkBuilderConnectFunc connect_func,
                                   gpointer              connect_data)
{
  GMarkupParser parser = { start_element, end_element, text, passthrough};
  ParseData state = { FALSE, NULL};
  GMarkupParseContext *context;
  GObject *object = NULL;

  if (type_data_quark == 0)
    type_data_quark = g_quark_from_string ("glade-preview-type-data");

  if (len == -1)
    len = strlen (template_data);

  /* Preallocate enough for the string plus the new fake template name */
  state.xml = g_string_sized_new (len + 32);

  context = g_markup_parse_context_new (&parser,
                                        G_MARKUP_TREAT_CDATA_AS_TEXT |
                                        G_MARKUP_PREFIX_ERROR_POSITION,
                                        &state, NULL);

  if (g_markup_parse_context_parse (context, template_data, len, NULL) &&
      g_markup_parse_context_end_parse (context, NULL) && 
      state.is_template)
    {
      GType template_type = template_generate_type (state.klass,
                                                    state.parent,
                                                    state.xml,
                                                    connect_func,
                                                    connect_data);
      if (template_type)
        object = g_object_new (template_type, NULL);
      else
        {
          /* on success template_generate_type() takes ownership of xml */
          g_string_free (state.xml, TRUE);
        }
    }
  else
    g_string_free (state.xml, TRUE);

  g_free (state.klass);
  g_free (state.parent);
  g_markup_parse_context_free (context);

  return object ? g_object_ref_sink (object) : NULL;
}
