/* This is -*- C -*- */
/* vim: set sw=2: */
/* $Id: guppi-attribute-bag.c,v 1.11 2002/01/08 23:19:19 trow Exp $ */

/*
 * guppi-attribute-bag.c
 *
 * Copyright (C) 2001 The Free Software Foundation
 *
 * Developed by Jon Trowbridge <trow@gnu.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <config.h>
#include "guppi-attribute-bag.h"

#include <ctype.h>
#include <string.h>
#include <glib.h>
#include <gtk/gtksignal.h>
#include <gtk/gtkmain.h>
#include <guppi-memory.h>
#include <gnome-xml/xmlmemory.h>
#include "guppi-attribute-flavor-private.h"


static GtkObjectClass *parent_class = NULL;

enum {
  ADDED,
  CHANGED,
  LAST_SIGNAL
};

static guint guppi_attribute_bag_signals[LAST_SIGNAL] = { 0 };

typedef struct _GuppiForwardedSignalInfo GuppiForwardedSignalInfo;
typedef struct _GuppiAttribute GuppiAttribute;

struct _GuppiForwardedSignalInfo {
  GuppiAttribute *attr;
  guint tag;

  gboolean queue;
  guint queued_tag;
};

struct _GuppiAttribute {
  gchar *key;
  gchar *description;

  GuppiAttributeFlavorInfo *info;

  gpointer data;
  
  gboolean has_default;
  gpointer default_data;

  GList *signals;

  GuppiAttributeBag *bag;
};

struct _GuppiAttributeBagPrivate {
  GHashTable *attr_hash;
  GList *attr_list;
};

static GuppiAttribute *
guppi_attribute_new (const gchar *key, const gchar *desc, GuppiAttributeFlavor flavor)
{
  GuppiAttribute *attr;
  GuppiAttributeFlavorInfo *info;

  info = guppi_attribute_flavor_get_info (flavor);
  if (info == NULL)
    return NULL;
  g_assert (info->flavor == flavor);

  attr = guppi_new0 (GuppiAttribute, 1);

  attr->key = guppi_strdup (key);
  attr->description = guppi_strdup (desc);
  attr->info = info;

  return attr;
}

static gint
queued_signal_cb (gpointer closure)
{
  GuppiForwardedSignalInfo *info = closure;

  gtk_signal_emit (GTK_OBJECT (info->attr->bag), guppi_attribute_bag_signals[CHANGED], info->attr->key);
  info->queued_tag = 0;

  return 0;
}

/* We ignore the signature of whatever signal we are capturing, and just forward
   a changed signal for the appropriate key.  To make this work, though, we need to
   do this bit of callback marshalling magic... */
static void
attribute_callback_marshal_cb (GtkObject *obj, gpointer data, guint n_args, GtkArg *args)
{
  GuppiForwardedSignalInfo *info = data;

  if (info->queue) {

    if (info->queued_tag == 0) {
      info->queued_tag = gtk_idle_add (queued_signal_cb, info);
    }

  } else {
    gtk_signal_emit (GTK_OBJECT (info->attr->bag), guppi_attribute_bag_signals[CHANGED], info->attr->key);
  }
}


static void
guppi_attribute_attach_signals (GuppiAttribute *attr, GuppiAttributeBag *bag)
{
  GList *iter = attr->info->signals_to_forward;

  if (attr->data == NULL)
    return;

  while (iter != NULL) {
    GuppiSignalsToForward *stf = iter->data;
    GuppiForwardedSignalInfo *info = guppi_new0 (GuppiForwardedSignalInfo, 1);

    info->attr   = attr;
    info->queue  = stf->queue;
    info->tag    = gtk_signal_connect_full (GTK_OBJECT (attr->data),
					    (gchar *) stf->name,
					    NULL,
					    attribute_callback_marshal_cb,
					    info,
					    NULL, FALSE, FALSE);

    attr->signals = g_list_prepend (attr->signals, info);
    iter = g_list_next (iter);
  }
}

static void
guppi_attribute_detatch_signals (GuppiAttribute *attr)
{
  GList *iter = attr->signals;
      
  if (attr->data == NULL)
    return;
  
  while (iter != NULL) {
    GuppiForwardedSignalInfo *info = iter->data;
    gtk_signal_disconnect (GTK_OBJECT (attr->data), info->tag);
    if (info->queued_tag != 0) {
      gtk_idle_remove (info->queued_tag);
      info->queued_tag = 0;
      guppi_free (info);
    }
    iter = g_list_next (iter);
  }

  g_list_free (attr->signals);
  attr->signals = NULL;
}

static void
guppi_attribute_free (GuppiAttribute *attr)
{
  if (attr != NULL) {

    guppi_free0 (attr->key);
    guppi_free0 (attr->description);

    guppi_attribute_detatch_signals (attr);
    
    if (attr->info->destroy) {
      if (attr->has_default)
	attr->info->destroy (attr->default_data);
      attr->info->destroy (attr->data);
    }

    guppi_free0 (attr);
  }
}

static void
guppi_attribute_bag_finalize (GtkObject *obj)
{
  GuppiAttributeBag *x = GUPPI_ATTRIBUTE_BAG(obj);
  GList *iter;

  for (iter = x->priv->attr_list; iter != NULL; iter = g_list_next (iter)) {
    guppi_attribute_free (iter->data);
  }

  g_list_free (x->priv->attr_list);
  g_hash_table_destroy (x->priv->attr_hash);

  g_free (x->priv);
  x->priv = NULL;

  guppi_finalized (obj);

  if (parent_class->finalize)
    parent_class->finalize (obj);
}

static void
guppi_attribute_bag_class_init (GuppiAttributeBagClass *klass)
{
  GtkObjectClass *object_class = (GtkObjectClass *)klass;

  parent_class = gtk_type_class (GTK_TYPE_OBJECT);

  object_class->finalize = guppi_attribute_bag_finalize;

  guppi_attribute_bag_signals[ADDED] =
    gtk_signal_new ("added",
                    GTK_RUN_FIRST,
                    object_class->type,
                    GTK_SIGNAL_OFFSET (GuppiAttributeBagClass, added),
                    gtk_marshal_NONE__NONE, GTK_TYPE_NONE, 0);

  guppi_attribute_bag_signals[CHANGED] =
    gtk_signal_new ("changed",
                    GTK_RUN_FIRST,
                    object_class->type,
                    GTK_SIGNAL_OFFSET (GuppiAttributeBagClass, changed),
                    gtk_marshal_NONE__POINTER,
		    GTK_TYPE_NONE, 1, GTK_TYPE_POINTER);

  gtk_object_class_add_signals (object_class, guppi_attribute_bag_signals, LAST_SIGNAL);
}

static void
guppi_attribute_bag_init (GuppiAttributeBag *obj)
{
  obj->priv = g_new0 (GuppiAttributeBagPrivate, 1);
  
  obj->priv->attr_hash = g_hash_table_new (g_str_hash, g_str_equal);
}

GtkType
guppi_attribute_bag_get_type (void)
{
  static GtkType guppi_attribute_bag_type = 0;
  if (!guppi_attribute_bag_type) {
    static const GtkTypeInfo guppi_attribute_bag_info = {
      "GuppiAttributeBag",
      sizeof (GuppiAttributeBag),
      sizeof (GuppiAttributeBagClass),
      (GtkClassInitFunc)guppi_attribute_bag_class_init,
      (GtkObjectInitFunc)guppi_attribute_bag_init,
      NULL, NULL, (GtkClassInitFunc)NULL
    };
    guppi_attribute_bag_type = gtk_type_unique (GTK_TYPE_OBJECT, &guppi_attribute_bag_info);
  }
  return guppi_attribute_bag_type;
}

GuppiAttributeBag *
guppi_attribute_bag_new (void)
{
  return GUPPI_ATTRIBUTE_BAG (guppi_type_new (guppi_attribute_bag_get_type ()));
}

/* This should be optimized at some point. */
GuppiAttributeBag *
guppi_attribute_bag_copy (GuppiAttributeBag *src)
{
  GuppiAttributeBag *copy;
  xmlNodePtr node;
  GuppiXMLDocument *doc;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (src), NULL);

  doc = guppi_xml_document_new ();
  node = guppi_attribute_bag_export_xml (src, doc);
  copy = guppi_attribute_bag_new ();
  if (!guppi_attribute_bag_import_xml (copy, doc, node)) {
    guppi_unref0 (copy);
  }

  guppi_xml_document_free (doc);
  xmlFreeNode (node);

  return copy;
}

static void
register_attribute (GuppiAttributeBag *bag, GuppiAttribute *attr)
{
  g_hash_table_insert (bag->priv->attr_hash, attr->key, attr);
  bag->priv->attr_list = g_list_append (bag->priv->attr_list, attr);

  attr->bag = bag;

  gtk_signal_emit (GTK_OBJECT (bag), guppi_attribute_bag_signals[ADDED], attr->key);
}

static gboolean
check_key (const gchar *str)
{
  gint count = 0;

  while (*str && count < 60) {

    if (iscntrl ((gint) *str))
      return FALSE;

    if (((guint) *str) >= 128)
      return FALSE;

    ++str;
    ++count;
  }
  
  return *str == '\0';
}

static const gchar *
get_subkey (const gchar *key, gchar **base_key)
{
  gchar *colon;

  colon = strchr (key, ':');
  if (colon && *(colon+1) == ':' && *(colon+2)) {
    *base_key = guppi_strndup (key, colon - key);
    return colon+2;
  }

  *base_key = guppi_strdup (key); 
  return NULL;
}

void
guppi_attribute_bag_add (GuppiAttributeBag *bag,
			 GuppiAttributeFlavor flavor,
			 const gchar *key,
			 const gchar *description)
{
  GuppiAttribute *attr;
  gchar *base_key;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));
  g_return_if_fail (flavor >= 0);
  g_return_if_fail (key && *key);

  get_subkey (key, &base_key);
  attr = guppi_attribute_new (base_key, description, flavor);
  guppi_free (base_key);

  if (attr == NULL) {
    g_message ("Bad flavor (%d)", flavor);
    return;
  }

  attr->data = attr->info->create_default ();

  register_attribute (bag, attr);
}

void
guppi_attribute_bag_add_with_default (GuppiAttributeBag *bag,
				      GuppiAttributeFlavor flavor,
				      const gchar *key,
				      const gchar *description,
				      ...)
{
  GuppiAttribute *attr;
  const gchar *subkey;
  gchar *base_key;
  va_list args;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));
  g_return_if_fail (flavor >= 0);
  g_return_if_fail (key && *key);

  subkey = get_subkey (key, &base_key);
  attr = guppi_attribute_new (base_key, description, flavor);
  if (attr == NULL) {
    g_message ("Bad flavor (%d)", flavor);
    return;
  }

  g_assert (attr->info->va2p);
  g_assert (attr->info->copy);

  va_start (args, description);
  attr->has_default = TRUE;

  attr->info->va2p (base_key, subkey, &args, &attr->default_data);
  guppi_free (base_key);
  attr->data = attr->info->copy (attr->default_data);
  va_end (args);

  guppi_attribute_attach_signals (attr, bag);

  register_attribute (bag, attr);
}

static GuppiAttribute *
get_by_key (GuppiAttributeBag *bag, const gchar *key)
{
  return (GuppiAttribute *) g_hash_table_lookup (bag->priv->attr_hash, key);
}

gboolean
guppi_attribute_bag_contains (GuppiAttributeBag *bag,
			      const gchar *key)
{
  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);
  g_return_val_if_fail (key && *key, FALSE);

  return guppi_attribute_bag_get_flavor (bag, key) >= 0;
}

GuppiAttributeFlavor
guppi_attribute_bag_get_flavor (GuppiAttributeBag *bag,
				const gchar *key)
{
  GuppiAttribute *attr;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), -1);
  g_return_val_if_fail (key && *key, -1);

  attr = g_hash_table_lookup (bag->priv->attr_hash, key);
  return attr && attr->info ? attr->info->flavor : -1;
}

void
guppi_attribute_bag_foreach (GuppiAttributeBag *bag,
			     GuppiAttributeBagFn fn,
			     gpointer closure)
{
  GList *iter;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));
  if (fn == NULL)
    return;

  for (iter = bag->priv->attr_list; iter != NULL; iter = g_list_next (iter)) {
    GuppiAttribute *attr = iter->data;

    fn (bag, attr->key, closure);
  }
}

gboolean
guppi_attribute_bag_get1 (GuppiAttributeBag *bag, const gchar *key, gpointer dest)
{
  GuppiAttribute *attr;
  const gchar *subkey;
  gchar *base_key;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);
  g_return_val_if_fail (key != NULL, FALSE);
  g_return_val_if_fail (dest != NULL, FALSE);

  if (!check_key (key)) {
    g_warning ("suspicious key");
  }

  if (GPOINTER_TO_UINT (dest) & 3) {
    g_warning ("writing value of '%s' to unaligned pointer", key);
  }

  subkey = get_subkey (key, &base_key);
  attr = get_by_key (bag, base_key);

  if (attr == NULL) {
    g_message ("Unknown property bag key '%s'", key);
    guppi_free (base_key);
    return FALSE;
  }

  attr->info->p2va (base_key, subkey, attr->data, dest);
  guppi_free (base_key);

  return TRUE;
}

gboolean
guppi_attribute_bag_vget (GuppiAttributeBag *bag, va_list varargs)
{
  const gchar *key;
  gpointer dest;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);

  do {
    key = va_arg (varargs, const gchar *);
    if (key != NULL) {
      dest = va_arg (varargs, gpointer);
      if (! guppi_attribute_bag_get1 (bag, key, dest)) {
	return FALSE;
      }
    }
  } while (key != NULL);

  return TRUE;
}

gboolean
guppi_attribute_bag_get (GuppiAttributeBag *bag, ...)
{
  va_list varargs;
  gboolean rv;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);

  va_start (varargs, bag);
  rv = guppi_attribute_bag_vget (bag, varargs);
  va_end (varargs);
  
  return rv;
}

static void
restore_default (GuppiAttributeBag *bag, GuppiAttribute *attr)
{
  gpointer new_data;
  gboolean changed = FALSE;

  g_assert (attr);

  if (attr->has_default) {
    new_data = attr->info->copy (attr->default_data);
  } else {
    new_data = attr->info->create_default ();
  }

  guppi_attribute_detatch_signals (attr);

  if (attr->info->getp) {
    changed = attr->info->getp (attr->key, "_default", new_data, &attr->data);
  } else {
    if (!attr->info->equality (attr->data, new_data)) {
      gpointer old_data = attr->data;
      attr->data = new_data;
      attr->info->destroy (old_data);
      changed = TRUE;
    } else {
      attr->info->destroy (new_data);
    }
  }

  guppi_attribute_attach_signals (attr, bag);

  if (changed) {
    gtk_signal_emit (GTK_OBJECT (bag), guppi_attribute_bag_signals[CHANGED], attr->key);
  }
}

gboolean
guppi_attribute_bag_vset1 (GuppiAttributeBag *bag, const gchar *key, va_list *varargs)
{
  GuppiAttribute *attr;
  gchar *base_key;
  const gchar *subkey;
  gboolean changed;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);
  g_return_val_if_fail (key != NULL, FALSE);
  g_return_val_if_fail (varargs != NULL, FALSE);

  if (! check_key (key)) {
    g_warning ("suspicious key");
  }

  subkey = get_subkey (key, &base_key);
  attr = get_by_key (bag, key);
  if (attr == NULL) {
    guppi_free (base_key);
    g_warning ("Unknown property bag key '%s'", key);
    return FALSE;
  }

  guppi_attribute_detatch_signals (attr);
  changed = attr->info->va2p (base_key, subkey, varargs, &attr->data);
  guppi_attribute_attach_signals (attr, bag);

  if (changed)
    gtk_signal_emit (GTK_OBJECT (bag), guppi_attribute_bag_signals[CHANGED], attr->key);

  guppi_free (base_key);

  return TRUE;
}

gboolean
guppi_attribute_bag_vset (GuppiAttributeBag *bag, va_list *varargs)
{
  const gchar *key;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);
  g_return_val_if_fail (varargs != NULL, FALSE);

  do {

    key = va_arg (*varargs, const gchar *);
    if (key) {
      if (! guppi_attribute_bag_vset1 (bag, key, varargs)) {
	return FALSE;
      }
    }

  } while (key != NULL);

  return TRUE;
}

gboolean
guppi_attribute_bag_set (GuppiAttributeBag *bag, ...)
{
  va_list varargs;
  gboolean rv;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);

  va_start (varargs, bag);
  rv = guppi_attribute_bag_vset (bag, &varargs);
  va_end (varargs);

  return rv;
}

void
guppi_attribute_bag_restore_default (GuppiAttributeBag *bag, const gchar *key)
{
  GuppiAttribute *attr;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));
  g_return_if_fail (key && *key);

  attr = get_by_key (bag, key);
  g_return_if_fail (attr != NULL);
  g_return_if_fail (attr->has_default);

  restore_default (bag, attr);
}

void
guppi_attribute_bag_restore_all_defaults (GuppiAttributeBag *bag)
{
  GList *iter;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));

  for (iter = bag->priv->attr_list; iter != NULL; iter = g_list_next (iter)) {
    restore_default (bag, (GuppiAttribute *) iter->data);
  }
}

void
guppi_attribute_bag_dump (GuppiAttributeBag *bag)
{
  GList *iter;

  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));

  putchar ('\n');
  for (iter = bag->priv->attr_list; iter != NULL; iter = g_list_next (iter)) {

    GuppiAttribute *attr = iter->data;
    gchar *str;

    str = attr->info->export_string ? attr->info->export_string (attr->data) : g_strdup ("--- N/A ---");

    printf ("%12s %-8s [%s]\n",
	    attr->key,
	    attr->info->name,
	    str);
    guppi_free (str);
  }
  putchar ('\n');
  
}

xmlNodePtr
guppi_attribute_bag_export_xml (GuppiAttributeBag *bag, GuppiXMLDocument *doc)
{
  xmlNodePtr bag_node;
  GList *iter;

  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), NULL);
  g_return_val_if_fail (doc != NULL, NULL);

  bag_node = xmlNewNode (doc->ns, "AttributeBag");

  for (iter = bag->priv->attr_list; iter != NULL; iter = g_list_next (iter)) {
    GuppiAttribute *attr = iter->data;

    if (!attr->has_default || !attr->info->equality (attr->data, attr->default_data)) {
    
      xmlNodePtr attr_node = xmlNewNode (doc->ns, "Attribute");
      xmlNewProp (attr_node, "key", attr->key);
      xmlNewProp (attr_node, "type", attr->info->name);

      if (attr->info->export_string) {
	gchar *str = attr->info->export_string (attr->data);
	xmlAddChild (attr_node, xmlNewText (str));
	guppi_free (str);
      } else if (attr->info->export_xml) {
	xmlNodePtr data_node = attr->info->export_xml (doc, attr->data);
	xmlAddChild (attr_node, data_node);
      } else {
	g_assert_not_reached ();
      }
      
      xmlAddChild (bag_node, attr_node);
    }
  }
  
  return bag_node;
}

gboolean
guppi_attribute_bag_import_xml (GuppiAttributeBag *bag, GuppiXMLDocument *doc, xmlNodePtr node)
{
  g_return_val_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag), FALSE);
  g_return_val_if_fail (doc != NULL, FALSE);
  g_return_val_if_fail (node != NULL, FALSE);

  if (strcmp (node->name, "AttributeBag"))
    return FALSE;

  guppi_attribute_bag_restore_all_defaults (bag);

  for (node = node->xmlChildrenNode; node != NULL; node = node->next) {
    gchar *key = NULL;
    gchar *type = NULL;
    GuppiAttribute *attr = NULL;
    gboolean success = FALSE;
    xmlNodePtr value_node = NULL;

    if (!strcmp (node->name, "Attribute")) {
      key = xmlGetProp (node, "key");
      type = xmlGetProp (node, "type");

      if (key && type) {
	attr = get_by_key (bag, key);
	if (attr) {
	  
	  if (!strcmp (attr->info->name, type)) {
	    value_node = node->xmlChildrenNode;

	    if (attr->info->import_string) {

	      gchar *str = xmlNodeGetContent (value_node);
	      success = attr->info->import_string (str, &attr->data);
	      xmlFree (str);
	      
	    } else if (attr->info->import_xml) {

	      success = attr->info->import_xml (doc, value_node, &attr->data);
	      
	    } else {
	      g_assert_not_reached ();
	    }

	    if (!success) {
	      g_warning ("Couldn't parse data for %s/%s", key, type);
	    }

	  } else {
	    g_warning ("type mismatch (%s vs %s) in %s", type, attr->info->name, key);
	  }
	}
      
      }

      xmlFree (key);
      xmlFree (type);
    }

  }

  return TRUE;
}

void
guppi_attribute_bag_spew_xml (GuppiAttributeBag *bag)
{
  GuppiXMLDocument *doc;
  g_return_if_fail (GUPPI_IS_ATTRIBUTE_BAG (bag));

  doc = guppi_xml_document_new ();
  guppi_xml_document_set_root (doc, guppi_attribute_bag_export_xml (bag, doc));
  guppi_xml_document_spew (doc);
  guppi_xml_document_free (doc);
}


syntax highlighted by Code2HTML, v. 0.9.1