/* 
 *  Copyright (C) 2002 Ricardo Fernández Pascual
 *
 *  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, 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.
 */

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

/**
 * Saving and loading bookmarks in XBEL
 */

/* system includes */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include <libxml/xmlmemory.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

#include <glib/gi18n.h>
#include <libgnome/gnome-util.h>

#include "xbel.h"
#include "gul-gobject-misc.h"

#define XBEL_GALEON_OWNER "http://galeon.sourceforge.net/"

/**
 * Private typedefs
 */
typedef struct _GbXBELMetadata GbXBELMetadata;

/**
 * Private functions, only availble from this file
 */
static void		gb_xbel_class_init		(GbXBELClass *klass);
static void		gb_xbel_init			(GbXBEL *io);
static GbBookmarkSet *	gb_xbel_load_from_file		(GbIO *io, const gchar *filename);
static GbBookmarkSet *	gb_xbel_load_from_string	(GbIO *io, const gchar *data);
static gboolean		gb_xbel_save_to_file		(GbIO *io, GbBookmarkSet *set,
							 const gchar *filename);
static gboolean		gb_xbel_save_to_string		(GbIO *io, GbBookmarkSet *set, gchar **data);
static gchar *		gb_xbel_format_name 		(GbIO *io);
static GList *		gb_xbel_extensions 		(GbIO *io);

static GbBookmarkSet *	xbel_read			(xmlDocPtr item);
static GbBookmark *	xbel_read_node 			(GbBookmarkSet *set, xmlNodePtr node);

static xmlNodePtr	get_child 			(xmlNodePtr parent, const gchar *name);
static gchar *		get_child_value_string		(xmlNodePtr parent, const gchar *name);
static gboolean		get_child_value_boolean		(xmlNodePtr parent, const gchar *name);
static glong		get_child_value_long		(xmlNodePtr parent, const gchar *name);
static GbXBELMetadata *	xbel_read_metadata		(xmlNodePtr node);
static GbXBELMetadata *	xbel_read_metadata_node		(xmlNodePtr node);

static void 		gb_xbel_metadata_free		(GbXBELMetadata *md);
static GbXBELMetadata *	gb_xbel_metadata_new		(void);

static void		xbel_save_node 			(xmlNodePtr xmlNode, GbBookmarkSet *set, 
							 GbBookmark *b);
static void		xbel_save_site			(xmlNodePtr xmlNode, GbSite *b);
static xmlDocPtr	xbel_save_root			(GbBookmarkSet *set);
static void		xbel_save_folder		(xmlNodePtr xmlNode, GbBookmarkSet *set,
							 GbFolder *b);
static void		xbel_save_alias			(xmlNodePtr xmlNode, GbBookmark *b);
static void		xbel_save_alias_placeholder	(xmlNodePtr xmlNode, GbBookmark *b);

static GbXBELMetadata *	xbel_save_common_data		(xmlNodePtr xmlNode, GbBookmark *b);
static void 		xbel_save_galeon_metadata	(xmlNodePtr node, GbXBELMetadata *md);

static void		xbel_save_node_link_xml_node	(GbBookmark *b);

static gchar *		xbel_escape_smart_site_history_item (const gchar *item);
static gchar *		xbel_unescape_smart_site_history_item (const gchar *item);

static xmlDtdPtr 	xml_doc_get_dtd			(xmlDocPtr doc);
static xmlNodePtr	xml_node_update_text_child	(xmlNodePtr parent, xmlNsPtr ns,
							 const xmlChar *name, const xmlChar *content);
static void 		xml_node_remove_child		(xmlNodePtr parent, const xmlChar *name);

static xmlAttrPtr       xmlSetRawProp (xmlNodePtr node, const xmlChar *name, const xmlChar *value);
static xmlChar *        xmlGetRawProp (xmlNodePtr node, const xmlChar *name);

/* safe strlen */
#define sstrlen(s) ((s) ? strlen ((s)) : 0)

/**
 * Galeon's XBEL metadata info
 */ 
struct _GbXBELMetadata 
{
	gchar *nick;
	gchar *smarturl;
	guint entry_width;
	gboolean entry_folded;
	gchar *pixmap;
	gboolean toolbar;
	gboolean context;
	gboolean default_f;
	gboolean autobm;
	GTime t_add;
	GTime t_vis;
	GTime t_mod;
	guint tb_style;
	guint acc_key;
	guint acc_mod;
	GbFileFormatVersion version;
	GSList *smart_site_history;
};

/**
 * XBEL object
 */

MAKE_GET_TYPE (gb_xbel, "GbXBEL", GbXBEL, gb_xbel_class_init, 
	       gb_xbel_init, GB_TYPE_IO);

static void
gb_xbel_class_init (GbXBELClass *klass)
{

	klass->parent_class.gb_io_load_from_file = gb_xbel_load_from_file;
	klass->parent_class.gb_io_load_from_string = gb_xbel_load_from_string;
	klass->parent_class.gb_io_save_to_file = gb_xbel_save_to_file;
	klass->parent_class.gb_io_save_to_string = gb_xbel_save_to_string;
	klass->parent_class.gb_io_format_name = gb_xbel_format_name;
	klass->parent_class.gb_io_extensions = gb_xbel_extensions;
	
}

static void 
gb_xbel_init (GbXBEL *io)
{
}

GbXBEL *
gb_xbel_new (void)
{
	return g_object_new (GB_TYPE_XBEL, NULL);
}

/**
 * Start the import of a XBEL document, with a given filename and
 * the default bookmarks folder
 */
GbBookmarkSet *
gb_xbel_load_from_file (GbIO *io, const gchar *filename)
{
	GbBookmarkSet *set = NULL;
	xmlDocPtr doc;
	
	if (!(g_file_test (filename, G_FILE_TEST_EXISTS))) {
		/* no bookmarks, ERROR */
		return NULL;
	}

	doc = xmlParseFile (filename);
	
	if (doc) {
		set = xbel_read (doc);
	} else {
		g_warning ("unable to parse bookmarks file: %s", filename);
		return NULL;
	}
	
	if (set)
	{
		gb_bookmark_set_set_filename (set, filename);
		gb_bookmark_set_set_io (set, io);
		gb_bookmark_set_set_needs_saving (set, FALSE);
	}

	return set;
}

GbBookmarkSet *
gb_xbel_load_from_string (GbIO *io, const gchar *data)
{
	GbBookmarkSet *set = NULL;
	xmlDocPtr doc;
	
	doc = xmlParseMemory (data, sstrlen (data));
	
	if (doc) {
		set = xbel_read (doc);
	} else {
		g_warning ("unable to parse bookmarks data: %s", data);
	}

	if (set)
	{
		gb_bookmark_set_set_io (set, io);
	}

	return set;
}

gboolean
gb_xbel_save_to_file (GbIO *io, GbBookmarkSet *set,
		      const gchar *filename)
{
        xmlDocPtr doc;

        g_return_val_if_fail (filename != NULL, FALSE);

        doc = xbel_save_root (set);

        /* Try to save this document */
        if (xmlSaveFormatFile (filename, doc, 1) <= 0) 
	{
		/* ERROR */
		return FALSE;
        }

	return TRUE;
}


gboolean
gb_xbel_save_to_string (GbIO *io, GbBookmarkSet *set, gchar **data)
{
        xmlDocPtr doc;
	xmlChar *mem;
	gint size;
	
        g_return_val_if_fail (data != NULL, FALSE);

	/* Add the root XBEL node */
        doc = xbel_save_root (set);

        /* Try to save this document */
        xmlDocDumpMemory (doc, &mem, &size);
	if (size <= 0) 
	{
		/* ERROR */
		return FALSE;
        }

	*data = mem;

	return TRUE;
}



gchar *
gb_xbel_format_name (GbIO *io)
{
	return g_strdup (_("XBEL bookmarks format"));
}

GList *
gb_xbel_extensions (GbIO *io)
{
	static gchar *extensions[] = { "xbel", "xml", NULL };
	GList *l = NULL;
	int i;
	for (i = 0; extensions[i] != NULL; i++)
	{
		l = g_list_append (l, g_strdup (extensions[i]));
	}
	return l;
}

static GbBookmarkSet * 
xbel_read (xmlDocPtr doc) 
{
	GbBookmarkSet *set = NULL;
	GbBookmark *root;
	xmlNodePtr item = doc->children;	

	/* FIXME: very hacky */
	while (!(item->children && !strcmp (item->name, "xbel"))  && item->next)
	{
		item = item->next;
	}

	g_return_val_if_fail (item, NULL);

	set = gb_bookmark_set_new ();
	gb_bookmark_set_set_xbel_doc (set, doc);
	root = xbel_read_node (set, item);
	
	if (!set->root && GB_IS_FOLDER (root))
	{
		gb_bookmark_set_set_root (set, GB_FOLDER (root));
	}
	
	gb_bookmark_set_resolve_aliases (set);

	g_object_unref (G_OBJECT (root));
	return set;
}

static GbBookmark *
xbel_read_node (GbBookmarkSet *set, xmlNodePtr node)
{ 
	GbBookmark *item = NULL;
	GbXBELMetadata *md = NULL;
	xmlNodePtr node2;
        g_return_val_if_fail (node != NULL, NULL);
	
	if (node->name == NULL) 
	{
		/*  Unknown node */
		g_warning ("xml node with NULL name!");
	} 
	else if (!strcmp (node->name, "folder")
	    || !strcmp (node->name, "xbel")) 
	{
		GbFolder *folder;
		gchar *title = get_child_value_string (node, "title");
		gchar *folded_s = xmlGetProp (node, "folded");
		gboolean folded = FALSE;
		if (folded_s
		    && (!g_ascii_strcasecmp (folded_s, "yes") 
			|| !g_ascii_strcasecmp (folded_s, "true")))
		{
			folded = TRUE;
		}
		xmlFree (folded_s);

		md = xbel_read_metadata (node);

		if (!title)
		{
			xmlNodePtr tn = get_child (node, "title");
			if (tn != NULL) /* it was present */
				title = g_strdup ("");
		}
		if (!title) 
		{
			if (!strcmp (node->name, "xbel"))
				title = g_strdup (_("XBEL bookmarks root"));
			else
				title = g_strdup (_("Untitled folder"));
		}
		if (md->autobm)
		{
			folder = (GbFolder *) gb_auto_folder_new (set, title);
		}
		else
		{
			folder = gb_folder_new (set, title);
		}
		
		g_free (title);

		if (set->root == NULL && !strcmp (node->name, "xbel"))
		{
			gb_bookmark_set_set_root (set, folder);
		}

		if (set->root == folder)
		{
			set->file_format_version = md->version;
		}

		gb_folder_set_create_toolbar (folder, md->toolbar);
		gb_folder_set_expanded (folder, !folded);
		/*  TODO: gb_folder_set_toolbar_style */

		if (md->default_f)
		{
			gb_bookmark_set_set_default_folder (set, folder);
		}
		
		item = (GbBookmark *) folder;

		for (node2 = node->children; node2 != NULL;
		     node2 = node2->next) {
			GbBookmark *b = xbel_read_node (set, node2);
			if (b) 
			{
				gb_folder_add_child (folder, b, -1);
				g_object_unref (G_OBJECT (b));
			}
		}

	} 
	else if (!strcmp (node->name, "bookmark")) 
	{
		gchar *title = get_child_value_string (node, "title");
                gchar *url = xmlGetRawProp (node, "href");
		GbSite *site;
		md = xbel_read_metadata (node);

		/* if title == NULL it may be because it was not specified
		   or because it is "". Let's try to check it */
		if (!title)
		{
			xmlNodePtr tn = get_child (node, "title");
			if (tn != NULL) /* it was present */
				title = g_strdup ("");
		}
		if (md->smarturl)
		{
			site = (GbSite *) gb_smart_site_new (set, title, url, md->smarturl);
			if (md->entry_width > 0)
			{
				gb_smart_site_set_entry_size (GB_SMART_SITE (site), 0, md->entry_width);
			}
			gb_smart_site_set_folded (GB_SMART_SITE (site), md->entry_folded);
			gb_smart_site_set_history (GB_SMART_SITE (site), 0, md->smart_site_history);
		}
		else
		{
			site = gb_site_new (set, title, url);
		}

		gb_site_set_time_visited (site, md->t_vis);
		gb_site_set_accel (site, md->acc_key, md->acc_mod);

		item = (GbBookmark *) site;
                g_free (title); 
		xmlFree (url);
        } 
	else if (!strcmp (node->name, "alias")) 
	{
		gchar *refstr = xmlGetProp (node, "ref");

		item = GB_BOOKMARK (gb_alias_placeholder_new (set, refstr));
		g_free (refstr);

		md = xbel_read_metadata (node);

		gb_alias_placeholder_set_create_toolbar (GB_ALIAS_PLACEHOLDER (item), md->toolbar);
        }
	else if (!strcmp (node->name, "separator")) 
	{
		item  = GB_BOOKMARK (gb_separator_new (set));
	}
	else 
	{
		/* Unknown or title, info... */
        }

	if (item) 
	{
		gchar *idstr = xmlGetProp (node, "id");
		gchar *pixmap = xmlGetProp (node, "icon");
		gchar *notes = get_child_value_string (node, "desc");
		if (idstr) 
		{
			gb_bookmark_set_id (item, idstr);
			xmlFree (idstr);
		}
		/* this is probably wrong, but it is harmless because the setting in
		 * the metadata will take precedence, if present. Konqueror uses this attribute */
		if (pixmap) gb_bookmark_set_pixmap (item, pixmap);
		if (notes) gb_bookmark_set_notes (item, notes);
		if (md) 
		{
			if (md->nick) gb_bookmark_set_nick (item, md->nick);
			if (md->pixmap) gb_bookmark_set_pixmap (item, md->pixmap);
			gb_bookmark_set_add_to_context_menu (item, md->context);
			gb_bookmark_set_time_modified (item, md->t_mod);
			gb_bookmark_set_time_added (item, md->t_add);
			
		}
		xmlFree (pixmap);
		g_free (notes);

		gb_bookmark_set_xbel_node (item, node);
	}
	gb_xbel_metadata_free (md);

	return item;
}

static GbXBELMetadata *
xbel_read_metadata (xmlNodePtr node)
{
        xmlNodePtr info_node = get_child (node, "info");
        if (info_node) 
	{
                /* Iterate over the metadata children */
                for (info_node = info_node->children;
		     info_node != NULL; info_node = info_node->next) 
		{
			if (!strcmp (info_node->name, "metadata")) 
			{
				gchar *owner = xmlGetProp (info_node, "owner");
				if (owner && !strcmp (owner, XBEL_GALEON_OWNER))
				{
					g_free (owner);
					/* there can be only one metadata node */
					return xbel_read_metadata_node (info_node);
				}
				g_free (owner);
			}
		}
	}
	return gb_xbel_metadata_new ();
}


static GbXBELMetadata *
xbel_read_metadata_node (xmlNodePtr node)
{
	GbXBELMetadata *md = gb_xbel_metadata_new ();
	gchar *smart_site_data;
	gchar *file_format_version;
	gchar *smart_site_history;

	/* This is a Galeon metadata element, extract the data from it */
	md->nick = get_child_value_string (node, "nick");
	md->smarturl = get_child_value_string (node, "smarturl");
	smart_site_data = get_child_value_string (node, "smart_site_data");
	smart_site_history = get_child_value_string (node, "smart_site_history");
	md->pixmap = get_child_value_string (node, "pixmap");
	md->toolbar = get_child_value_boolean (node, "create_toolbar");
	md->context = get_child_value_boolean (node, "create_context");
	md->default_f = get_child_value_boolean (node, "default_folder");
	md->autobm = get_child_value_boolean (node, "autobookmarks");
	md->t_add = get_child_value_long (node, "time_added");
	md->t_vis = get_child_value_long (node, "time_visited");
	md->t_mod = get_child_value_long (node, "time_modified");
	md->tb_style = get_child_value_long (node, "toolbar_style");
	md->acc_key = get_child_value_long (node, "accel_key");
	md->acc_mod = get_child_value_long (node, "accel_mods");
	md->entry_folded = FALSE;
	md->entry_width = 0;
	file_format_version = get_child_value_string (node, "galeon_xbel_version");

	if (smart_site_data)
	{
		gchar **options = g_strsplit (smart_site_data, ",", 0);
		gint i;
		for (i = 0; options[i]; ++i)
		{			
			if (!strncmp (options[i], "width=", 6))
			{
				md->entry_width = atoi (&options[i][6]);
			}
			if (!strncmp (options[i], "folded=", 7))
			{
				md->entry_folded = atoi (&options[i][7]);
			}
		}
		g_free (smart_site_data);
		g_strfreev (options);
	}

	if (smart_site_history)
	{
		gchar **items = g_strsplit (smart_site_history, ",", 0);
		gint i;
		if (items[0] && items[1])
		{
			/* the first item is the parameter index */
			/* for now, only one parameter is supported */

			/* the second is the number of items, and it's ignored for now */

			if (!strcmp (g_strstrip (items[0]), "0"))
			{
				GSList *history = NULL;
				for (i = 2; items[i]; ++i)
				{			
					gchar *item = xbel_unescape_smart_site_history_item (items[i]);
					history = g_slist_prepend (history, item);
				}
				md->smart_site_history = g_slist_reverse (history);
			}
		}
		g_free (smart_site_history);
		g_strfreev (items);
	}

	if (file_format_version)
	{
		if (!strcmp (file_format_version, "1"))
		{
			md->version = GB_FILE_FORMAT_VERSION_GALEON_1;
		}
		else if (!strcmp (file_format_version, "2"))
		{
			md->version = GB_FILE_FORMAT_VERSION_GALEON_2;
		}
		else
		{
			md->version = GB_FILE_FORMAT_VERSION_UNKNOWN;
		}
		g_free (file_format_version);
	}

	return md;
}

static void
gb_xbel_metadata_free (GbXBELMetadata *md)
{
	if (!md) return;
	g_slist_foreach (md->smart_site_history, (GFunc) g_free, NULL);
	g_slist_free (md->smart_site_history);
	g_free (md->nick);
	g_free (md->smarturl);
	g_free (md->pixmap);
	g_free (md);
}

static GbXBELMetadata *
gb_xbel_metadata_new (void)
{
	return g_new0 (GbXBELMetadata, 1);
}

/* saving */

/**
 * Treat a root item specially and use it as the root of the XBEL tree
 */
static xmlDocPtr
xbel_save_root (GbBookmarkSet *set)
{
        xmlNodePtr newNode;
	xmlDtdPtr dtd;
        xmlDocPtr doc;

	if (set->xbel_doc)
	{
		doc = set->xbel_doc;
	}
	else
	{
		doc = xmlNewDoc ("1.0");
		gb_bookmark_set_set_xbel_doc (set, doc);
	}

	set->file_format_version = GB_FILE_FORMAT_VERSION_GALEON_2;

	if (GB_BOOKMARK (set->root)->xbel_node && xmlDocGetRootElement (doc) == GB_BOOKMARK (set->root)->xbel_node)
	{
		newNode = GB_BOOKMARK (set->root)->xbel_node;
	}
	else
	{
		newNode = xmlNewDocNode (doc, NULL, "xbel", NULL);
		xmlDocSetRootElement (doc, newNode);
		/* children xml nodes may have been freed, we must discard them :-( */
		gb_bookmark_set_discard_xml_nodes (set);
		gb_bookmark_set_xbel_node (GB_BOOKMARK (set->root), newNode);
	}

	xmlSetProp(newNode, "version", "1.0");

	/* I'm not sure if the following is OK */
	if (xml_doc_get_dtd (doc) == NULL)
	{
		dtd = xmlNewDtd
			(doc, "xbel", 
			 "+//IDN python.org//DTD XML Bookmark "
			 "Exchange Language 1.0//EN//XML", 
			 "http://www.python.org/topics/xml/dtds/xbel-1.0.dtd");
		xmlAddPrevSibling (doc->children, (xmlNodePtr) dtd);
	}

        /* In Galeon the root of the tree is just a folder. However,
	   XBEL complicates the situation by have a special root element,
	   XBEL. This is identical to FOLDER apart from the name of the
	   tag... Thus this hack.  */
	if (GB_IS_FOLDER (set->root))
	{
		GbFolder *b = set->root;
		GbXBELMetadata *md;
		GbBookmark *child;

		xmlSetProp (newNode, "folded", b->expanded ? "no" : "yes");
		
		xml_node_update_text_child (newNode, NULL, "title", ((GbBookmark *) b)->name);
		md = xbel_save_common_data (newNode, (GbBookmark *) b);
		
		md->autobm = GB_IS_AUTO_FOLDER (b);
		md->toolbar = b->create_toolbar;
		md->default_f = b == set->default_folder;
		/* TODO: md->tb_style =  */
		
		xbel_save_galeon_metadata (newNode, md);
		gb_xbel_metadata_free (md);
		
		for (child = b->child; child != NULL; child = child->next)
			xbel_save_node (newNode, set, child);
	}
	return doc;
}

/**
 * Add an item of unknown type to the tree. This delegates the
 * responsibility of adding nodes to the tree to other functions.
 */
static void
xbel_save_node (xmlNodePtr parent_node, GbBookmarkSet *set, GbBookmark *b)
{
        g_return_if_fail (parent_node != NULL);

	if (gb_bookmark_is_alias (b))
	{
		xbel_save_alias (parent_node, b);
	}
	else
	{
		if (GB_IS_SITE (b))
		{
			xbel_save_site (parent_node, GB_SITE (b));
		} 
		else if (GB_IS_FOLDER (b))
		{
			xbel_save_folder (parent_node, set, GB_FOLDER (b));
		}
		else if (GB_IS_SEPARATOR (b))
		{
			if (!b->xbel_node)
			{
				xmlNodePtr newNode = xmlNewChild (parent_node, NULL, "separator", NULL);
				gb_bookmark_set_xbel_node (b, newNode);
			}
			xbel_save_node_link_xml_node (b);
		}
		else if (GB_IS_ALIAS_PLACEHOLDER (b))
		{
			xbel_save_alias_placeholder (parent_node, b);
		}
		else
		{
			const gchar *s = g_type_name (G_TYPE_FROM_INSTANCE (b));
			g_warning ("Detected unknown bookmark item type \"%s\"", s);
		}
	}
}

/**
 * Link the xmls node of the bookmark in the proper place
 */
static void
xbel_save_node_link_xml_node (GbBookmark *b)
{
	xmlNodePtr newNode;
	
	g_return_if_fail (GB_IS_BOOKMARK (b));
	g_return_if_fail (GB_IS_BOOKMARK (b->parent));
	g_return_if_fail (b->xbel_node);
	g_return_if_fail (GB_BOOKMARK (b->parent)->xbel_node);

	if (b->prev)
	{
		/* put it after it's left brother, which must be already correctly positioned */
		g_assert (b->prev->parent == b->parent);
		g_assert (b->prev->xbel_node->parent == GB_BOOKMARK (b->parent)->xbel_node);
		newNode = xmlAddNextSibling (b->prev->xbel_node, b->xbel_node);
	}
	else
	{
		xmlNodePtr prev;
		
		/* try to put it after title and info nodes, or at the beginning */

		prev = get_child (GB_BOOKMARK (b->parent)->xbel_node, "info");
		if (!prev)
		{
			prev = get_child (GB_BOOKMARK (b->parent)->xbel_node, "title");
		}

		if (prev)
		{
			if (prev->next != b->xbel_node)
			{
				newNode = xmlAddNextSibling (prev, b->xbel_node);
			}
			else
			{
				newNode = b->xbel_node;
			}
			g_assert (newNode->parent);
		}
		else
		{
			if (GB_BOOKMARK (b->parent)->xbel_node->children)
			{
				if (GB_BOOKMARK (b->parent)->xbel_node->children != b->xbel_node)
				{
					newNode = xmlAddPrevSibling (GB_BOOKMARK (b->parent)->xbel_node->children, b->xbel_node);
				}
				else
				{
					newNode = b->xbel_node;
				}
				g_assert (newNode->parent);
			}
			else
			{
				xmlUnlinkNode (b->xbel_node);
				newNode = xmlAddChild (GB_BOOKMARK (b->parent)->xbel_node, b->xbel_node);
				g_assert (newNode->parent);
			}
		}
	}
	
	gb_bookmark_set_xbel_node (b, newNode);

	g_assert (b->xbel_node->parent == GB_BOOKMARK (b->parent)->xbel_node);
}

static void
xbel_save_site (xmlNodePtr parent_node, GbSite *b)
{
        xmlNodePtr newNode;
        GbXBELMetadata *md;
        g_return_if_fail(parent_node != NULL);

        if (GB_BOOKMARK (b)->xbel_node)
	{
		newNode = GB_BOOKMARK (b)->xbel_node;
	}
	else
	{
		newNode = xmlNewChild (parent_node, NULL, "bookmark", NULL);
		gb_bookmark_set_xbel_node (GB_BOOKMARK (b), newNode);
	}

        xmlSetRawProp (newNode, "href", b->url);
        xml_node_update_text_child (newNode, NULL, "title", ((GbBookmark *) b)->name);

	md = xbel_save_common_data (newNode, (GbBookmark *) b);
	if (GB_IS_SMART_SITE (b))
	{
		md->smarturl = g_strdup (GB_SMART_SITE (b)->smarturl);
		md->entry_folded = GB_SMART_SITE (b)->folded;
		md->entry_width = gb_smart_site_get_entry_size (GB_SMART_SITE (b), 0);
		md->smart_site_history = gb_smart_site_get_history (GB_SMART_SITE (b), 0);
	}
	md->t_vis = b->time_visited;
	md->acc_mod = b->accel_mods;
	md->acc_key = b->accel_key;

	xbel_save_galeon_metadata (newNode, md);
	gb_xbel_metadata_free (md);

	xbel_save_node_link_xml_node (GB_BOOKMARK (b));
}

static void
xbel_save_folder (xmlNodePtr parent_node, GbBookmarkSet *set, GbFolder *b)
{
        xmlNodePtr newNode;
        GbXBELMetadata *md;
	g_return_if_fail (parent_node != NULL);
	
        if (GB_BOOKMARK (b)->xbel_node)
	{
		newNode = GB_BOOKMARK (b)->xbel_node;
	}
	else
	{
		newNode = xmlNewChild (parent_node, NULL, "folder", NULL);
		gb_bookmark_set_xbel_node (GB_BOOKMARK (b), newNode);
	}

        xmlSetProp (newNode, "folded", b->expanded ? "no" : "yes");
        xml_node_update_text_child (newNode, NULL, "title", ((GbBookmark *) b)->name);

	md = xbel_save_common_data (newNode, GB_BOOKMARK (b));
	
	md->autobm = GB_IS_AUTO_FOLDER (b);
	md->toolbar = b->create_toolbar;
	md->default_f = b == set->default_folder;
	/* TODO: md->tb_style =  */
	
	xbel_save_galeon_metadata (newNode, md);
	gb_xbel_metadata_free (md);
	
        /* now iterate over the children of this folder */
	if (!gb_bookmark_is_alias (b))
	{
		GbBookmark *child;
		for (child = b->child; child != NULL; child = child->next)
			xbel_save_node (newNode, set, child);
	}

	xbel_save_node_link_xml_node (GB_BOOKMARK (b));
}


static void
xbel_save_alias (xmlNodePtr parent_node, GbBookmark *b)
{
        xmlNodePtr newNode;
	GbXBELMetadata *md;
	GbBookmark *real = gb_bookmark_real_bookmark (b);

        g_return_if_fail (parent_node != NULL);

        if (GB_BOOKMARK (b)->xbel_node)
	{
		newNode = GB_BOOKMARK (b)->xbel_node;
	}
	else
	{
		newNode = xmlNewChild (parent_node, NULL, "alias", NULL);
		gb_bookmark_set_xbel_node (GB_BOOKMARK (b), newNode);
	}

	xmlSetProp (newNode, "ref", real->id);

	md = xbel_save_common_data (newNode, b);
	if (GB_IS_FOLDER (b)) 
	{
		md->toolbar = GB_FOLDER (b)->create_toolbar;
	}

        if (md->toolbar 
	    || md->context
	    || md->tb_style
	    || md->t_add)
	{
		xbel_save_galeon_metadata (newNode, md);
	}
	gb_xbel_metadata_free (md);

	xbel_save_node_link_xml_node (GB_BOOKMARK (b));
}

static void
xbel_save_alias_placeholder (xmlNodePtr parent_node, GbBookmark *b)
{
        xmlNodePtr newNode;
	GbXBELMetadata *md;

        g_return_if_fail (parent_node != NULL);

        if (GB_BOOKMARK (b)->xbel_node)
	{
		newNode = GB_BOOKMARK (b)->xbel_node;
	}
	else
	{
		newNode = xmlNewChild (parent_node, NULL, "alias", NULL);
		gb_bookmark_set_xbel_node (GB_BOOKMARK (b), newNode);
	}

	if (GB_ALIAS_PLACEHOLDER (b)->alias_of_id)
	{
		xmlSetProp (newNode, "ref", GB_ALIAS_PLACEHOLDER (b)->alias_of_id);
	}

	md = xbel_save_common_data (newNode, b);
	if (GB_IS_FOLDER (b)) 
	{
		md->toolbar = GB_FOLDER (b)->create_toolbar;
	}

        if (md->toolbar 
	    || md->context
	    || md->tb_style
	    || md->t_add)
	{
		xbel_save_galeon_metadata (newNode, md);
	}
	gb_xbel_metadata_free (md);

	xbel_save_node_link_xml_node (GB_BOOKMARK (b));
}

static GbXBELMetadata *
xbel_save_common_data (xmlNodePtr bookmark_node, GbBookmark *b)
{
	GbXBELMetadata *md = gb_xbel_metadata_new ();

	g_return_val_if_fail (b->xbel_node == bookmark_node, NULL);

	if (b->id != NULL && !gb_bookmark_is_alias (b))
	{
		xmlSetProp (bookmark_node, "id", b->id);
	}
	if (!gb_bookmark_is_alias (b) && sstrlen (b->notes) > 0) 
	{
		xml_node_update_text_child (bookmark_node, NULL, "desc", b->notes);
	}
	else
	{
		xml_node_remove_child (bookmark_node, "desc");
	}

	if (sstrlen (b->nick) > 0)
	{
		md->nick = g_strdup (b->nick);
	}
	if (sstrlen (b->pixmap_file) > 0)
	{
		md->pixmap = g_strdup (b->pixmap_file);
	}
	md->context = b->add_to_context_menu;
	md->t_add = b->time_added;
	md->t_mod = b->time_modified;
	
	if (b->set && b == GB_BOOKMARK (b->set->root))
	{
		md->version = b->set->file_format_version;
	}

	return md;
}

static xmlNodePtr
xbel_save_galeon_metadata_find_node (xmlNodePtr node)
{
        xmlNodePtr infoNode;
        xmlNodePtr metaNode;
	xmlNodePtr first_info = NULL;
	
        for (infoNode = node->children ; infoNode != NULL ; infoNode = infoNode->next)
	{
		if (!strcmp (infoNode->name, "info"))
		{
			if (!first_info)
			{
				first_info = infoNode;
			}

			for (metaNode = infoNode->children ; metaNode != NULL ; metaNode = metaNode->next)
			{
				if (!strcmp (metaNode->name, "metadata"))
				{
					gchar *owner = xmlGetProp (metaNode, "owner");
					if (owner && !strcmp (owner, XBEL_GALEON_OWNER))
					{
						g_free (owner);
						return metaNode;
					}
					g_free (owner);
				}
			}
		}
	}

	infoNode = first_info ? first_info : xmlNewChild (node, NULL, "info", NULL);
	metaNode = xmlNewChild (infoNode, NULL, "metadata", NULL);
	xmlSetProp (metaNode, "owner", XBEL_GALEON_OWNER);
	
	return metaNode;
}

static void
xbel_save_galeon_metadata_remove_metadata (xmlNodePtr node)
{
        xmlNodePtr infoNode;
        xmlNodePtr metaNode;
	xmlNodePtr first_info = NULL;
	xmlNodePtr found = NULL;
	
        for (infoNode = node->children ; infoNode != NULL ; infoNode = infoNode->next)
	{
		if (!strcmp (infoNode->name, "info"))
		{
			if (!first_info)
			{
				first_info = infoNode;
			}

			for (metaNode = infoNode->children ; metaNode != NULL ; metaNode = metaNode->next)
			{
				if (!strcmp (metaNode->name, "metadata"))
				{
					gchar *owner = xmlGetProp (metaNode, "owner");
					if (owner && !strcmp (owner, XBEL_GALEON_OWNER))
					{
						g_free (owner);
						found = metaNode;
						break;
					}
					g_free (owner);
				}
			}
		}
	}

	if (found)
	{
		xmlUnlinkNode (found);
		xmlFreeNode (found);
	}
}

static void 
xbel_save_galeon_metadata (xmlNodePtr node, GbXBELMetadata *md)
{
        g_return_if_fail (node != NULL);

        if (md->toolbar 
	    || md->context
	    || md->default_f
	    || md->tb_style
	    || md->t_add
	    || md->t_mod
	    || md->t_vis
	    || md->acc_key
	    || md->acc_mod
	    || md->autobm
	    || (sstrlen (md->nick) > 0) 
	    || (sstrlen (md->pixmap) > 0) 
	    || (sstrlen (md->smarturl) > 0)
	    || md->version
	    || md->smart_site_history)
	{
		xmlNodePtr metaNode;
		GString *s;

		s = g_string_new ("");

                metaNode = xbel_save_galeon_metadata_find_node (node);
		g_assert (metaNode);

                /* export the nick name */
                if (sstrlen (md->nick) > 0)
		{
                        xml_node_update_text_child (metaNode, NULL, "nick", md->nick);
                }
		else
		{
                        xml_node_remove_child (metaNode, "nick");
                }

                /* pixmap path */
                if (sstrlen (md->pixmap) > 0) 
		{
                        xml_node_update_text_child (metaNode, NULL, "pixmap", md->pixmap);
                }
		else
		{
                        xml_node_remove_child (metaNode, "pixmap");
                }

                /* create a toolbar */
                if (md->toolbar) 
		{
			xml_node_update_text_child (metaNode, NULL, "create_toolbar", "yes");
                }
		else
		{
			xml_node_remove_child (metaNode, "create_toolbar");
                }

                /* create a context menu */
                if (md->context) 
		{
			xml_node_update_text_child (metaNode, NULL, "create_context", "yes");
                }
		else
		{
			xml_node_remove_child (metaNode, "create_context");
                }

		/* is this the default_folder? */
                if (md->default_f)
		{
			xml_node_update_text_child (metaNode, NULL, "default_folder", "yes");
                }
		else
		{
			xml_node_remove_child (metaNode, "default_folder");
                }

		if (md->tb_style != 0) 
		{
			g_string_printf (s, "%d", md->tb_style);
			xml_node_update_text_child (metaNode, NULL, "toolbar_style", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "toolbar_style");
		}

		if (md->t_vis != 0)
		{
			g_string_printf (s, "%d", md->t_vis);
			xml_node_update_text_child (metaNode, NULL, "time_visited", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "time_visited");
		}

		if (md->t_mod != 0)
		{
			g_string_printf (s, "%d", md->t_mod);
			xml_node_update_text_child (metaNode, NULL, "time_modified", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "time_modified");
		}

		if (md->t_add != 0) 
		{
			g_string_printf (s, "%d", md->t_add);
			xml_node_update_text_child (metaNode, NULL, "time_added", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "time_added");
		}

		if (md->acc_mod != 0) 
		{
			g_string_printf (s, "%d", md->acc_mod);
			xml_node_update_text_child (metaNode, NULL, "accel_mods", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "accel_mods");
		}

		if (md->acc_key != 0) 
		{
			g_string_printf (s, "%d", md->acc_key);
			xml_node_update_text_child (metaNode, NULL, "accel_key", s->str);
		}
		else
		{
			xml_node_remove_child (metaNode, "accel_key");
		}

		if (sstrlen (md->smarturl) > 0) 
		{
			gchar *smart_data = g_strdup_printf ("width=%d,folded=%d", md->entry_width, 
							     md->entry_folded);
			xml_node_update_text_child (metaNode, NULL, "smarturl",
					 md->smarturl);
			xml_node_update_text_child (metaNode, NULL, "smart_site_data",
					 smart_data);
			g_free (smart_data);

		}
		else
		{
			xml_node_remove_child (metaNode, "smarturl");
			xml_node_remove_child (metaNode, "smart_site_data");
		}

		if (md->smart_site_history)
		{
			GString *shist = g_string_new (NULL);
			GSList *li;
			g_string_printf (shist, "0,%d,", g_slist_length (md->smart_site_history));
			for (li = md->smart_site_history; li; li = li->next)
			{
				gchar *item = xbel_escape_smart_site_history_item (li->data);
				g_string_append (shist, item);
				if (li->next)
				{
					g_string_append (shist, ",");
				}
				g_free (item);
			}
			xml_node_update_text_child (metaNode, NULL, "smart_site_history",
						    shist->str);
			g_string_free (shist, TRUE);
		}
		else
		{
			xml_node_remove_child (metaNode, "smart_site_history");
		}

		if (md->autobm)
		{
			xml_node_update_text_child (metaNode, NULL, "autobookmarks", "yes");
		}
		else
		{
			xml_node_remove_child (metaNode, "autobookmarks");
		}

		if (md->version)
		{
			switch (md->version)
			{
			case GB_FILE_FORMAT_VERSION_UNKNOWN:
				/* shouldn't happen */
				break;
			case GB_FILE_FORMAT_VERSION_GALEON_1:
				xml_node_update_text_child (metaNode, NULL, "galeon_xbel_version", "1");
				break;
			case GB_FILE_FORMAT_VERSION_GALEON_2:
				xml_node_update_text_child (metaNode, NULL, "galeon_xbel_version", "2");
				break;
			}
		}
		g_string_free (s, TRUE);
        }
	else
	{
		xbel_save_galeon_metadata_remove_metadata (node);
	}
}

/*************************************/

/**
 * These probably don't belong here...
 */

/**
 * LibXML Utility Function: Iterate through all children of parent returning a
 * pointer to the first child which is called name.
 */
static xmlNodePtr
get_child (xmlNodePtr parent, const gchar *name)
{
        xmlNodePtr child;
        g_return_val_if_fail (parent != NULL, NULL);
        g_return_val_if_fail (sstrlen (name) > 0, NULL);
        for (child = parent->children ; child != NULL ; child = child->next)
		if (!strcmp (child->name, name))
                        return child;
        return NULL;
}

/**
 * LibXML Utility Function: Get the text value of the first child of parent 
 * with the name name.
 */
static gchar *
get_child_value_string (xmlNodePtr parent, const gchar *name)
{
        xmlNodePtr child = get_child (parent, name);
        if (child) {
		gchar *v = xmlNodeGetContent (child);
		gchar *ret = NULL;
		if (v) ret = g_strdup (v);
		xmlFree (v);
		return ret;
	} else return NULL;
}

static gboolean
get_child_value_boolean (xmlNodePtr parent, const gchar *name)
{
	gchar *sval = get_child_value_string (parent, name);
	gboolean ret = (sval 
			&& (!g_ascii_strcasecmp (sval, "yes")
			    || !g_ascii_strcasecmp (sval, "true")))
		? TRUE : FALSE;
	g_free (sval);
	return ret;
}

static glong
get_child_value_long (xmlNodePtr parent, const gchar *name)
{
	gchar *sval = get_child_value_string (parent, name);
        glong ret = sval ? atol (sval) : 0;
	g_free (sval);
	return ret;
}

/* turns "," into ";" and ";" into "\;" */
static gchar *
xbel_escape_smart_site_history_item (const gchar *item)
{
	GString *s = g_string_new (item);
	gchar *ret;
	int i;
	
	for (i = 0; s->str[i]; ++i)
	{
		if (s->str[i] == ',')
		{
			s->str[i] = ';';
		}
		else if (s->str[i] == ';')
		{
			g_string_insert_c (s, i, '\\');
			++i;
		}
	}

	ret = s->str;
	g_string_free (s, FALSE);
	return ret;
}

/* reverses the previous function */
static gchar *
xbel_unescape_smart_site_history_item (const gchar *item)
{
	GString *s = g_string_new (item);
	gchar *ret;
	int i;
	
	for (i = 0; s->str[i]; ++i)
	{
		if (s->str[i] == ';')
		{
			s->str[i] = ',';
		}
		else if (s->str[i] == '\\'
			&& s->str[i + 1] == ';')
		{
			g_string_erase (s, i, 1);
		}
	}

	ret = s->str;
	g_string_free (s, FALSE);
	return ret;
}

/** 
 * Gets the dtd of a document. There must be a better way... 
 */
static xmlDtdPtr 
xml_doc_get_dtd (xmlDocPtr doc)
{
	xmlNodePtr n;
	for (n = doc->children; n; n = n->next)
	{
		if (n->type == XML_DTD_NODE)
		{
			return (xmlDtdPtr) n;
		}
	}
	return NULL;
}

static xmlNodePtr 
xml_node_update_text_child (xmlNodePtr parent, xmlNsPtr ns,
			    const xmlChar *name, const xmlChar *content) 
{
        xmlNodePtr child = get_child (parent, name);
	if (child)
	{
		xmlNodePtr dt = xmlNewDocText (parent->doc, content);
		if (child->children)
		{
			xmlNodePtr old = xmlReplaceNode (child->children, dt);
			xmlFreeNode (old);
		}
		else
		{
			xmlAddChild (child, dt);
		}
		return child;
	}
	else
	{
		return xmlNewTextChild (parent, ns, name, content);
	}
}

static void 
xml_node_remove_child (xmlNodePtr parent, const xmlChar *name)
{
        xmlNodePtr child = get_child (parent, name);
	if (child)
	{
		xmlUnlinkNode (child);
		xmlFreeNode (child);
	}
}


/*
 * Helper functions to preserve compatability with encoding of urls in the
 * bookmarks file, this is used because libxml1 was broken ! (see bug 56487)
 */

static gchar *
escape_xml_prop (const gchar *text)
{
        GString *out = g_string_new (NULL);
        guchar c;
        while ((c = *text))
        {
                if (c<0x20 || c>0x7F || c=='&' || c=='<' || c=='>' || c=='%' ||
		    c=='"') /* Should anything else be escaped? */
                        g_string_append_printf (out, "%%%02X", c);
                else
                        g_string_append_c (out, c);
                ++text;
        }
        text = out->str;
        g_string_free (out, FALSE); /* don't free char data, caller must */
        return (gchar *)text;
}

/**
 * xmlSetRawProp: set a property in an XML file which has
 * value encoded.  This works around the newline-losingness and broken
 * entitiy handling of libxml1. (see bug #56487)
 */
static xmlAttrPtr
xmlSetRawProp (xmlNodePtr node, const xmlChar *name, const xmlChar *value)
{
	xmlAttrPtr attr;
	gchar *escaped = escape_xml_prop (value);
	attr = xmlSetProp (node, name, escaped);
	g_free (escaped);
	return attr;
}


static gchar *
unescape_hexed_string (const gchar *text)
{
	GString *out = g_string_new (NULL);
	const gchar *u, *cur = text;
	gchar decodebuf[3];

	decodebuf[2] = '\0';

	while ((u = strchr (cur, '%')))
	{
		decodebuf[0]=u[1];
		decodebuf[1]=u[2];
		g_string_append_printf (out, "%.*s%c", u-cur, cur, 
					(char)strtol(decodebuf, NULL, 16));
		cur = u + 3;
	}

	if (cur && *cur)
	{
		/* attach rest of string */
		g_string_append (out, cur);
	}

	u = out->str;
	g_string_free (out, FALSE); /* don't free char data, caller must */

	return (gchar *)u;
}

/**
 * xmlGetRawProp: get a property in an XML file which has been encoded This
 * works around the newline-losingness and broken entitiy handling of
 * libxml1. (see bug #56487)
 */
static xmlChar *
xmlGetRawProp (xmlNodePtr node, const xmlChar *name)
{
	xmlChar *value;
	gchar *unescaped;
	value = xmlGetProp (node, name);
	if (value == NULL)
	{
		return NULL;
	}
	unescaped = unescape_hexed_string (value);
	xmlFree (value);
	return unescaped;
}
