/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/*
 *  Copyright (C) 2003 Takuro Ashie
 *
 *  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.
 *
 *  $Id: kz-xbel.c 2394 2006-01-06 02:58:42Z makeinu $
 */

#include "kazehakase.h"
#include "kz-xbel.h"
#include "kz-xml.h"
#include "utils.h"
#include "kz-smart-bookmark.h"

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

static gboolean kz_xbel_is_supported       (KzBookmarkFile  *bookmark,
					    const gchar     *buf);
static void     kz_xbel_init               (KzBookmarkFile  *bookmark);
static gboolean kz_xbel_from_string        (KzBookmarkFile  *bookmark,
					    const gchar     *buffer,
					    guint            length,
					    GError         **error);
static gchar   *kz_xbel_to_string          (KzBookmarkFile   *bookmark);

static void     kz_xbel_notify             (GObject     *object,
					    GParamSpec  *pspec,
					    KzXML       *xml);

static void     kz_xbel_build_tree         (KzBookmark  *bookmark);
static void     kz_xbel_insert_xml_node    (KzBookmark  *bookmark,
					    KzBookmark  *parent,
					    KzBookmark  *sibling);
static void     kz_xbel_remove_xml_node    (KzBookmark  *bookmark);
static void     kz_xbel_connect_signals    (KzBookmark  *bookmark);
static void     kz_xbel_disconnect_signals (KzBookmark  *bookmark);
static void     cb_bookmark_insert_child   (KzBookmark  *bookmark,
					    KzBookmark  *child,
					    KzBookmark  *sibling);
static void     cb_bookmark_remove_child   (KzBookmark  *bookmark,
					    KzBookmark  *child);
static void     cb_bookmark_notify         (GObject     *object,
					    GParamSpec  *spec);

static void     xml_node_set_title         (KzXMLNode   *parent,
					    const gchar *title);


static GQuark xml_quark = 0;
static GQuark node_quark = 0;
static GQuark root_quark = 0;
static GQuark building_quark = 0;


#define BOOKMARK_SET_BUILDING(b) \
	g_object_set_qdata(G_OBJECT(b), building_quark, GINT_TO_POINTER(TRUE))
#define BOOKMARK_UNSET_BUILDING(b) \
	g_object_set_qdata(G_OBJECT(b), building_quark, GINT_TO_POINTER(FALSE))
#define BOOKMARK_IS_BUILDING(b) \
	GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(b), building_quark))


static KzBookmarkFileType xbel_file_type =
{
	priority_hint: 0,
	file_type:     "XBEL",
	init:          kz_xbel_init,
	is_supported:  kz_xbel_is_supported,
	from_string:   kz_xbel_from_string,
	to_string:     kz_xbel_to_string,
};


KzBookmarkFileType *
kz_xbel_get_file_types (gint idx)
{
	if (idx == 0)
		return &xbel_file_type;
	else
		return NULL;
}


static gboolean
kz_xbel_is_supported(KzBookmarkFile *bookmark, const gchar *buf)
{
	const gchar *pos;

	g_return_val_if_fail(buf, FALSE);

	if (!g_str_has_prefix(buf, "<?xml")) return FALSE;

	pos = buf;

	/* find first element */
	do
		pos = strchr(pos + 1, '<');
	while (pos && pos[1] == '!');

	if (pos && g_str_has_prefix(pos, "<xbel"))
		return TRUE;

	return FALSE;
}


static void
kz_xbel_init (KzBookmarkFile *bookmark)
{
	KzXML *xml;

	if (!xml_quark)
		xml_quark = g_quark_from_string("KzXBEL::KzXML");
	if (!root_quark)
		root_quark = g_quark_from_string("KzXBEL::XMLRootNode");
	if (!node_quark)
		node_quark = g_quark_from_string("KzXBEL::XMLNode");
	if (!building_quark)
		building_quark = g_quark_from_string("KzXBEL::Building");

	xml = kz_xml_new();
	g_object_set_qdata_full(G_OBJECT(bookmark), xml_quark,
				xml, (GDestroyNotify) g_object_unref);

	g_object_set(G_OBJECT(bookmark),
		     "type", KZ_BOOKMARK_PURE_FOLDER,
		     NULL);

	g_signal_connect(bookmark, "notify",
			 G_CALLBACK(kz_xbel_notify), xml);
	g_signal_connect_after(bookmark, "insert-child",
			       G_CALLBACK(cb_bookmark_insert_child), xml);
	g_signal_connect_after(bookmark, "remove-child",
			       G_CALLBACK(cb_bookmark_remove_child), xml);
}


static gboolean
kz_xbel_from_string (KzBookmarkFile *bookmark,
		     const gchar *buffer, guint length,
		     GError **error)
{
	KzXML *xml;

	g_return_val_if_fail(KZ_IS_BOOKMARK_FILE(bookmark), FALSE);

	xml = g_object_get_qdata(G_OBJECT(bookmark), xml_quark);
	g_return_val_if_fail(KZ_IS_XML(xml), FALSE);

	if (!kz_xml_load_xml(xml, buffer, length) ||
	    !kz_xml_get_root_element(xml))
	{
		KzXMLNode *node, *title_node, *title, *space;
		const gchar *bookmark_title;

		/*
		 *  <xbel version=1.0 folded=no>\n (space1)
		 *  <title>Bookmarks</title>\n       (space2)
		 *  </xbel>
		 */
		node = kz_xml_element_node_new("xbel");
		kz_xml_node_set_attr(node, "version", "1.0");
		kz_xml_node_set_attr(node, "folded", "no");	
		kz_xml_node_set_attr(node, "xmlns:kz", KAZEHAKASE_URI"2004");	
		/* kz_xml_node_set_attr(node, "id", "????????"); */
		kz_xml_node_append_child(xml->root, node);

		/* space1 */
		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);

		{
			title_node = kz_xml_element_node_new("title");
			kz_xml_node_append_child(node, title_node);

			bookmark_title = kz_bookmark_get_title(KZ_BOOKMARK(bookmark));
			if(bookmark_title)
				title = kz_xml_text_node_new(bookmark_title);
			else
				title = kz_xml_text_node_new("Bookmarks");
			kz_xml_node_append_child(title_node, title);
		}

		/* space2 */
		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);
	}

	kz_xbel_build_tree(KZ_BOOKMARK(bookmark));

	return TRUE;
}


static gchar *
kz_xbel_to_string (KzBookmarkFile *bookmark)
{
	KzXML *xml;

	g_return_val_if_fail(KZ_IS_BOOKMARK_FILE(bookmark), NULL);

	xml = g_object_get_qdata(G_OBJECT(bookmark), xml_quark);
	g_return_val_if_fail(KZ_IS_XML(xml), NULL);

	kz_xml_node_arrange_indent(xml->root, 0);

	return kz_xml_node_to_xml(xml->root);
}


static void
kz_xbel_notify (GObject *object, GParamSpec *pspec, KzXML *xml)
{
	KzBookmark *bookmark;
	KzXMLNode *node;
	const gchar *prop;
        GValue value = { 0 };

	g_return_if_fail(KZ_IS_BOOKMARK(object));
	g_return_if_fail(KZ_IS_XML(xml));

	bookmark = KZ_BOOKMARK(object);

	if (BOOKMARK_IS_BUILDING(bookmark)) return;

	node = kz_xml_get_root_element(xml);
	if (!node) return;
	g_return_if_fail(kz_xml_node_name_is(node, "xbel"));

	prop = g_param_spec_get_name(pspec);
	g_return_if_fail(prop);

        g_value_init(&value, G_PARAM_SPEC_VALUE_TYPE(pspec));
        g_object_get_property(object, prop, &value);

	/*
	 * FIXME! We should use hash table.
	 */
	if (!strcmp(prop, "title"))
	{
		gchar *title = g_value_dup_string(&value);

		xml_node_set_title(node, title);
		g_free(title);
	}
	g_value_unset(&value);
}


static KzXMLNode *
xml_node_get_named_node (KzXMLNode *parent, const gchar *name)
{
	KzXMLNode *node;

	g_return_val_if_fail(parent, NULL);
	g_return_val_if_fail(name && *name, NULL);

	for (node = kz_xml_node_first_child(parent);
	     node;
	     node = kz_xml_node_next(node))
	{
		if (kz_xml_node_name_is(node, name))
			return node;
	}

	return NULL;
}


static void
xml_node_set_location (KzXMLNode *node, KzBookmark *bookmark);
static void
xml_node_set_interval (KzXMLNode *node, KzBookmark *bookmark);

static void
parse_metadata_node (KzBookmark *bookmark, KzXMLNode *parent)
{
	KzXMLNode *node;
	KzBookmark *smart_history = NULL;

	g_return_if_fail(KZ_IS_BOOKMARK(bookmark));

	if (!kz_xml_node_is_element(parent)) return;

	for (node = kz_xml_node_first_child(parent);
	     node;
	     node = kz_xml_node_next(node))
	{
		const GList *attrs, *a_node;
		const gchar *owner;
		const gchar *smart_regex  = NULL;
		const gchar *smart_uri    = NULL;
		const gchar *smart_encode = NULL;
		gboolean urlencode = FALSE;
		gint interval;
		gboolean is_smart_history = FALSE;

		if (!kz_xml_node_is_element(node)) continue;

		if (!kz_xml_node_name_is(node, "metadata")) continue;
			
		/* We should ignore other owners such as Galeon. */
		owner = kz_xml_node_get_attr(node, "owner");
		if (!owner || strcmp(owner, KAZEHAKASE_URI))
			continue;
			
		attrs = kz_xml_node_get_attrs(node);

		for (a_node = attrs; a_node; a_node = g_list_next(a_node))
		{
			KzXMLAttr *attr = a_node->data;

			if (g_str_has_prefix(attr->name, "kz:update_interval"))
			{
				interval = atoi(attr->value);
				kz_bookmark_file_set_interval(KZ_BOOKMARK_FILE(bookmark), interval);
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_regex"))
			{
				smart_regex = attr->value;
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_uri"))
			{
				smart_uri = attr->value;
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_encode"))
			{
				smart_encode = attr->value;
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_urlencode"))
			{
				urlencode = TRUE;
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_history") && !smart_history)
			{
				smart_history = KZ_BOOKMARK(kz_bookmark_file_new(attr->value,
							    NULL, NULL));
				is_smart_history = TRUE;
			}
			else if (g_str_has_prefix(attr->name, "kz:xmlrpc"))
			{
				kz_bookmark_file_set_xmlrpc(KZ_BOOKMARK_FILE(bookmark), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:xmlrpc_user"))
			{
				kz_bookmark_file_set_xmlrpc_user(KZ_BOOKMARK_FILE(bookmark), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:xmlrpc_pass"))
			{
				kz_bookmark_file_set_xmlrpc_pass(KZ_BOOKMARK_FILE(bookmark), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_xmlrpc"))
			{
				kz_bookmark_file_set_xmlrpc(KZ_BOOKMARK_FILE(smart_history), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_xmlrpc_user"))
			{
				kz_bookmark_file_set_xmlrpc_user(KZ_BOOKMARK_FILE(smart_history), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:smart_xmlrpc_pass"))
			{
				kz_bookmark_file_set_xmlrpc_pass(KZ_BOOKMARK_FILE(smart_history), attr->value);
			}
			else if (g_str_has_prefix(attr->name, "kz:current_position"))
			{
				guint pos = (guint)atoi(attr->value);
				kz_bookmark_set_current(bookmark, pos);
			}
			else if (g_str_has_prefix(attr->name, "kz:lock"))
			{
				if (!strcmp(attr->value, "yes"))
					kz_bookmark_set_lock(bookmark, TRUE);
			}
			else if (g_str_has_prefix(attr->name, "kz:auto_refresh"))
			{
				if (!strcmp(attr->value, "yes"))
					kz_bookmark_set_auto_refresh(bookmark, TRUE);
			}
			else if (g_str_has_prefix(attr->name, "kz:javascript"))
			{
				if (!strcmp(attr->value, "yes"))
					kz_bookmark_set_javascript(bookmark, TRUE);
				if (!strcmp(attr->value, "no"))
					kz_bookmark_set_javascript(bookmark, FALSE);
			}
		}

		if (smart_regex && smart_uri)
		{
			 kz_smart_bookmark_append_property(
				KZ_SMART_BOOKMARK(bookmark),
				smart_regex, smart_uri, 
				smart_encode, urlencode);
		}
	}

	if (smart_history)
	{
		kz_bookmark_file_load_start(KZ_BOOKMARK_FILE(smart_history));
		kz_smart_bookmark_set_history (KZ_SMART_BOOKMARK(bookmark),
					       smart_history);
		g_object_unref(smart_history);
	}
}

static KzXMLNode *
xml_node_find_metadata_node (KzXMLNode *parent)
{
	KzXMLNode *info_node, *node;

	info_node = xml_node_get_named_node(parent, "info");
	if (!info_node) return NULL;

	for (node = kz_xml_node_first_child(info_node);
	     node;
	     node = kz_xml_node_next(node))
	{
		const gchar *owner;
	
		if (!kz_xml_node_name_is(node, "metadata"))
			continue;

		owner = kz_xml_node_get_attr(node, "owner");
		if (owner && !strcmp(owner, KAZEHAKASE_URI))
			return node;
	}

	return NULL;
}


static gboolean
xml_node_has_smart_property (KzXMLNode *parent)
{
	const GList *attrs, *list;
	KzXMLNode *info_node, *node;
	gboolean ret = FALSE;
			
	info_node = xml_node_get_named_node(parent, "info");
	if (!info_node) return FALSE;
	
	for (node = kz_xml_node_first_child(info_node);
	     node;
	     node = kz_xml_node_next(node))
	{
		const gchar *owner;
		if (!kz_xml_node_name_is(node, "metadata"))
			continue;

		owner = kz_xml_node_get_attr(node, "owner");
		if (!owner || strcmp(owner, KAZEHAKASE_URI))
			continue;

		attrs = kz_xml_node_get_attrs(node);

		for (list = attrs; list; list = g_list_next(list))
		{
			KzXMLAttr *attr = list->data;

			if (g_str_has_prefix(attr->name, "kz:smart"))
			{
				ret = TRUE;
				break;
			}
		}
		if (ret) break;
	}
	return ret;
}


static gboolean
xml_node_has_location (KzXMLNode *parent)
{
	KzXMLNode *child;

	g_return_val_if_fail(parent, FALSE);

	if (!kz_xml_node_name_is(parent, "folder"))
		return FALSE;

	child = xml_node_find_metadata_node(parent);
	if (!child) return FALSE;

	if (kz_xml_node_get_attr(child, "kz:location"))
		return TRUE;
	
	return FALSE;
}


static const gchar *
xml_node_get_location (KzXMLNode *parent)
{
	KzXMLNode *child;
	const gchar *attr;

	g_return_val_if_fail(parent, FALSE);

	if (!kz_xml_node_name_is(parent, "folder"))
		return NULL;

	child = xml_node_find_metadata_node(parent);
	if (!child) return NULL;

	/* Does metadata node have location attr? */
	attr = kz_xml_node_get_attr(child, "kz:location");
	if (!attr) return NULL;
	
	return attr;
}


static void
parse_child_node (KzBookmark *bookmark, KzXMLNode *parent)
{
	KzXMLNode *node;

	g_return_if_fail(KZ_IS_BOOKMARK(bookmark));

	if (!kz_xml_node_is_element(parent)) return;

	for (node = kz_xml_node_first_child(parent);
	     node;
	     node = kz_xml_node_next(node))
	{
		if (!kz_xml_node_is_element(node)) continue;

		if (kz_xml_node_name_is(node, "title"))
		{
			gchar *title;

			if (kz_bookmark_get_title(bookmark) &&
			    !g_object_get_qdata(G_OBJECT(bookmark), xml_quark))
			{
				g_warning("title element is duplicated!");
				continue;
			}
			title = kz_xml_node_to_str(node);
			kz_bookmark_set_title(bookmark, title);
			g_free(title);
		}
		else if (kz_xml_node_name_is(node, "desc"))
		{
			gchar *desc;

			if (kz_bookmark_get_description(bookmark))
			{
				g_warning("desc element is duplicated!");
				continue;
			}
			desc = kz_xml_node_to_str(node);
			kz_bookmark_set_description(bookmark, desc);
			g_free(desc);
		}
		else if (kz_xml_node_name_is(node, "folder"))
		{
			KzBookmark *folder;
			const gchar *folded_str;
			const gchar *id;
			gboolean folded;

			if (xml_node_has_location(node))
			{
#warning FIXME! If the file does not exit, we should creat it.
				const gchar *location = xml_node_get_location(node);

				folder = KZ_BOOKMARK(kz_bookmark_file_new(location,
							      NULL, NULL));
				kz_bookmark_file_load_start(KZ_BOOKMARK_FILE(folder));
			}
			else
			{
				folder = kz_bookmark_pure_folder_new();
			}

			folded_str = kz_xml_node_get_attr(node, "folded");
			folded = folded_str && !strcmp(folded_str, "yes");
			kz_bookmark_set_folded(folder, folded);

			id = kz_xml_node_get_attr(node, "id");
			if (id)
				kz_bookmark_set_id(folder, id);

			BOOKMARK_SET_BUILDING(folder);
			g_object_set_qdata(G_OBJECT(folder),
					   node_quark, node);
			kz_bookmark_append(bookmark, folder);
			parse_child_node(folder, node);
			BOOKMARK_UNSET_BUILDING(folder);

			g_object_unref(folder);
		}
		else if (kz_xml_node_name_is(node, "bookmark"))
		{
			KzBookmark *child_bookmark;
			const GList *attrs = kz_xml_node_get_attrs(node);
			const GList *list;

			if (xml_node_has_smart_property(node))
				child_bookmark = kz_smart_bookmark_new();
			else
				child_bookmark = kz_bookmark_new();

			BOOKMARK_SET_BUILDING(bookmark);
			g_object_set_qdata(G_OBJECT(child_bookmark),
					   node_quark, node);
			for (list = attrs; list; list = g_list_next(list))
			{
				KzXMLAttr *attr = list->data;
				const gchar *name = attr->name;

				if (!strcmp(name, "href"))
				{
					kz_bookmark_set_link(child_bookmark, attr->value);
				}
				else if (!strcmp(name, "added"))
				{
					guint t = 0;
					if (str_isdigit(attr->value))
						t = atoi(attr->value);	
					kz_bookmark_set_added_time(child_bookmark, t);
				}
				else if (!strcmp(name, "visited"))
				{
					guint t = 0;
					if (str_isdigit(attr->value))
						t = atoi(attr->value);	
					kz_bookmark_set_last_visited(child_bookmark, t);
				}
				else if (!strcmp(name, "id"))
				{
					kz_bookmark_set_id(child_bookmark, attr->value);
				}
			}

			parse_child_node(child_bookmark, node);
			/*
			 * To append child is after parsing child's nodes,
			 * because smart bookmark action needs bookmark title 
			 * in child's nodes.
			 */
			kz_bookmark_append(bookmark, child_bookmark);
			BOOKMARK_UNSET_BUILDING(bookmark);
			g_object_unref(child_bookmark);
		}
		else if (kz_xml_node_name_is(node, "separator"))
		{
			KzBookmark *separator = kz_bookmark_separator_new();

			BOOKMARK_SET_BUILDING(separator);
			g_object_set_qdata(G_OBJECT(separator),
					   node_quark, node);
			kz_bookmark_append(bookmark, separator);
			BOOKMARK_UNSET_BUILDING(separator);
			g_object_unref(separator);
		}
		else if (kz_xml_node_name_is(node, "alias"))
		{
			g_warning("KzXBEL::alias element is not supported yet."
				  "Please implement me!");
		}
		else if (kz_xml_node_name_is(node, "info"))
		{
			parse_metadata_node(bookmark, node);
		}
	}
}


static void
kz_xbel_build_tree (KzBookmark *bookmark)
{
	KzXML *xml;
	KzXMLNode *node;

	g_return_if_fail(KZ_IS_BOOKMARK(bookmark));

	xml = g_object_get_qdata(G_OBJECT(bookmark), xml_quark);
	g_return_if_fail(KZ_IS_XML(xml));

	node = kz_xml_get_root_element (xml);
	if (!node) return;
	g_return_if_fail (kz_xml_node_name_is(node, "xbel"));

	BOOKMARK_SET_BUILDING(bookmark);
	g_object_set_qdata(G_OBJECT(bookmark),
			   root_quark, node);
	parse_child_node(bookmark, node);
	BOOKMARK_UNSET_BUILDING(bookmark);
}


static KzXMLNode *
xml_node_ensure_info_node (KzXMLNode *node)
{
	KzXMLNode *info_node, *space;

	info_node = xml_node_get_named_node(node, "info");

	if (info_node)
		return info_node;
		
	info_node = kz_xml_element_node_new("info");
	kz_xml_node_append_child(node, info_node);

		/* space1 */
	space = kz_xml_text_node_new("\n");
	kz_xml_node_append_child(info_node, space);

		/* space4 */
	space = kz_xml_text_node_new("\n");
	kz_xml_node_append_child(node, space);

	return info_node;
}

static void
xml_node_append_title (KzXMLNode *node, KzBookmark *bookmark)
{
	const gchar *title = kz_bookmark_get_title(bookmark);
	KzXMLNode *title_node, *text_node, *space;

	/*
	 *  <title>title</title>\n
	 */

	/* title */
	title_node = kz_xml_element_node_new("title");
	kz_xml_node_append_child(node, title_node);
	text_node = kz_xml_text_node_new(title);
	kz_xml_node_append_child(title_node, text_node);

	/* space1 */
	space = kz_xml_text_node_new("\n");
	kz_xml_node_append_child(node, space);
}


static KzXMLNode *
xml_node_append_metadata_node (KzXMLNode *node)
{
	KzXMLNode *space, *info_node, *metadata_node;

	/*
	 *  <info>\n      (space1)
	 *  <metadata>\n  (space2)
	 *  </metadata>\n (space3)
	 *  </info>\n     (space4)
	 */

	/* info */
	info_node = xml_node_ensure_info_node(node);

	/* metadata */
	metadata_node = xml_node_find_metadata_node(node);
	if (metadata_node) return metadata_node;

	metadata_node = kz_xml_element_node_new("metadata");
	kz_xml_node_set_attr(metadata_node, "owner", KAZEHAKASE_URI);
	kz_xml_node_append_child(info_node, metadata_node);

	/* space3 */
	space = kz_xml_text_node_new("\n");
	kz_xml_node_append_child(info_node, space);

	return metadata_node;
}


static void
xml_node_set_location (KzXMLNode *node, KzBookmark *bookmark)
{
	const gchar *location;
	KzXMLNode *metadata_node;

	location = kz_bookmark_file_get_location(KZ_BOOKMARK_FILE(bookmark));
	g_return_if_fail(location);

	metadata_node = xml_node_find_metadata_node(node);
	
	if (!metadata_node)
		metadata_node = xml_node_append_metadata_node(node);

	kz_xml_node_set_attr(metadata_node, "kz:location", location);
}


static void
xml_node_set_interval (KzXMLNode *node, KzBookmark *bookmark)
{
	gchar *interval;
	KzXMLNode *metadata_node;

	interval = g_strdup_printf("%d", kz_bookmark_file_get_interval(KZ_BOOKMARK_FILE(bookmark)));

	metadata_node = xml_node_find_metadata_node(node);
	
	if (!metadata_node)
		metadata_node = xml_node_append_metadata_node(node);

	/* update_interval */
	kz_xml_node_set_attr(metadata_node, "kz:update_interval", interval);

	g_free(interval);
}


static void
xml_node_set_smart_list (KzXMLNode *node,
			 KzBookmark *bookmark,
		       	 const GList *smart_list)
{
	const GList *smart_node;	
	KzXMLNode *info_node, *child_node;

	/* get the info node, create it if not exist */
	info_node = xml_node_ensure_info_node(node);

	/* remove all previous smart properties */
	child_node = kz_xml_node_first_child(info_node);
	while (child_node)
	{
		if (kz_xml_node_name_is(child_node, "metadata"))
		{
			const gchar *owner, *smart_regex;
			owner = kz_xml_node_get_attr(child_node, "owner");
			smart_regex = kz_xml_node_get_attr(child_node,
							   "kz:smart_regex");
			if (owner && 
			   !strcmp(owner, KAZEHAKASE_URI) &&
			    smart_regex)
			{
				KzXMLNode *tmp = child_node;
				child_node = kz_xml_node_next(child_node);
				kz_xml_node_remove_child(info_node,
							 tmp);
				kz_xml_node_unref(tmp);

				/* space is also removed */
				if (kz_xml_node_is_space(child_node))
				{
					KzXMLNode *tmp = child_node;
					child_node = kz_xml_node_next(child_node);
					kz_xml_node_remove_child(info_node,
								 tmp);
					kz_xml_node_unref(tmp);
				}
			}
			else
				child_node = kz_xml_node_next(child_node);
		}
		else
			child_node = kz_xml_node_next(child_node);
	}

	/* append new smart properties */
	for (smart_node = smart_list; smart_node; smart_node = g_list_next(smart_node))
	{
		KzSmartBookmarkProperty *prop;
		KzXMLNode *metadata_node, *space;
		
		metadata_node = kz_xml_element_node_new("metadata");
		kz_xml_node_set_attr(metadata_node, "owner", KAZEHAKASE_URI);
		kz_xml_node_append_child(info_node, metadata_node);

		/* space */
		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(info_node, space);

		prop = smart_node->data;

		if (prop->regex)
		{
			kz_xml_node_set_attr(metadata_node,
					     "kz:smart_regex", prop->regex);
		}
		if (prop->uri)
		{
			kz_xml_node_set_attr(metadata_node,
					     "kz:smart_uri", prop->uri);
		}
		if (prop->encode)
		{
			kz_xml_node_set_attr(metadata_node,
					     "kz:smart_encode", prop->encode);
		}
		if (prop->urlencode)
		{
			kz_xml_node_set_attr(metadata_node,
					     "kz:smart_urlencode", "yes");
		}
	}
}


static void
xml_node_set_smart_history (KzXMLNode *node,
			    KzBookmark *bookmark,
		       	    KzBookmark *smart_history)
{
	const gchar *history_location = NULL;
	const gchar *xmlrpc, *xml_user, *xml_pass;
	KzXMLNode *info_node, *child_node, *smart_history_node = NULL;

	if (!KZ_IS_BOOKMARK_FILE(smart_history))
		return;

	history_location = kz_bookmark_file_get_location(
					KZ_BOOKMARK_FILE(smart_history));

	xmlrpc           = kz_bookmark_file_get_xmlrpc(
					KZ_BOOKMARK_FILE(smart_history));

	xml_user         = kz_bookmark_file_get_xmlrpc_user(
					KZ_BOOKMARK_FILE(smart_history));

	xml_pass         = kz_bookmark_file_get_xmlrpc_pass(
					KZ_BOOKMARK_FILE(smart_history));

	/* get the info node, create it if not exist */
	info_node = xml_node_ensure_info_node(node);
	
	for (child_node = kz_xml_node_first_child(info_node);
	     child_node;
	     child_node = kz_xml_node_next(child_node))
	{
		const gchar *owner;
		const gchar *tmp;
	
		if (!kz_xml_node_name_is(child_node, "metadata"))
			continue;

		owner = kz_xml_node_get_attr(child_node, "owner");
		if (!owner || strcmp(owner, KAZEHAKASE_URI))
			continue;

		tmp = kz_xml_node_get_attr(child_node, "kz:smart_history");

		if (tmp)
		{
			smart_history_node = child_node;
			break;
		}
	}

	if (!smart_history_node)
	{
		smart_history_node = kz_xml_element_node_new("metadata");
		kz_xml_node_set_attr(smart_history_node, "owner", KAZEHAKASE_URI);
		kz_xml_node_append_child(info_node, smart_history_node);
	}

	kz_xml_node_set_attr(smart_history_node,
			     "kz:smart_history", history_location);


	if (xmlrpc)
	{
		kz_xml_node_set_attr(smart_history_node,
				     "kz:smart_xmlrpc", xmlrpc);
	}
	if (xml_user)
	{
		kz_xml_node_set_attr(smart_history_node,
				     "kz:smart_xmlrpc_user", xml_user);
	}
	if (xml_pass)
	{
		kz_xml_node_set_attr(smart_history_node,
				     "kz:smart_xmlrpc_pass", xml_pass);
	}
}


static KzXMLNode *
create_xml_node (KzBookmark *bookmark)
{
	KzXMLNode *node = NULL, *space;

	g_return_val_if_fail(KZ_IS_BOOKMARK(bookmark), NULL);

	if (kz_bookmark_is_separator(bookmark))
	{
		node = kz_xml_element_node_new("separator");
	}
	else if (KZ_IS_BOOKMARK_FILE(bookmark))
	{
		const gchar *location = kz_bookmark_file_get_location(KZ_BOOKMARK_FILE(bookmark));
		guint interval        = kz_bookmark_file_get_interval(KZ_BOOKMARK_FILE(bookmark));

		if (!location || !*location)
		{
			/* warning? */
		}

		/*
		 * <folder>\n
		 * <title/>
		 * ...
		 * </folder>
		 */

		node = kz_xml_element_node_new("folder");
		xml_node_append_title (node, bookmark);

		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);

		if (location)
			xml_node_set_location(node, bookmark);
		if (interval)
			xml_node_set_interval(node, bookmark);
	}
	else if (kz_bookmark_is_folder(bookmark))
	{
		KzXMLNode *metanode;
		gint pos = kz_bookmark_get_current(bookmark);
		gboolean lock = kz_bookmark_get_lock(bookmark);
		gboolean auto_refresh = kz_bookmark_get_auto_refresh(bookmark);
		gboolean javascript = kz_bookmark_get_javascript(bookmark);
		/*
		 * <folder>\n
		 * <title/>
		 * ...
		 * </folder>
		 */

		node = kz_xml_element_node_new("folder");

		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);

		xml_node_append_title (node, bookmark);
		
		if (pos || lock || auto_refresh)
		{
			metanode = xml_node_find_metadata_node(node);
	
			if (!metanode)
				metanode = xml_node_append_metadata_node(node);

			if (pos)
			{
				gchar *pos_str = g_strdup_printf("%d", pos);
				kz_xml_node_set_attr(metanode, "kz:current_position", pos_str);
			}
			if (lock)
			{
				kz_xml_node_set_attr(metanode, "kz:lock", "yes");
			}
			if (auto_refresh)
			{
				kz_xml_node_set_attr(metanode, "kz:auto_refresh", "yes");
			}
		}

		if (javascript)
		{
			const gchar *str;
				str = "yes";
			metanode = xml_node_find_metadata_node(node);
	
			if (!metanode)
				metanode = xml_node_append_metadata_node(node);
			kz_xml_node_set_attr(metanode, "kz:javascript", str);
		}
		else
		{
			gboolean javascript_default = TRUE;
			const gchar *str;

			metanode = xml_node_find_metadata_node(node);
			if (!metanode)
				metanode = xml_node_append_metadata_node(node);

			KZ_CONF_GET("Global", "use_javascript", javascript_default, BOOL);

			if(javascript_default)
				str = "yes";
			else
				str = "no";
			kz_xml_node_set_attr(metanode, "kz:javascript", str);
		}
	}
	else if (KZ_IS_SMART_BOOKMARK(bookmark))
	{
		const GList *smart_list;
		const gchar *uri = kz_bookmark_get_link(bookmark);
		
		smart_list = kz_smart_bookmark_get_smart_list(
				KZ_SMART_BOOKMARK(bookmark));
	
		node = kz_xml_element_node_new("bookmark");
		if (uri)
			kz_xml_node_set_attr(node, "href", uri);
		if (smart_list)
		{
			xml_node_set_smart_list(node, 
						bookmark,
						smart_list);
		}
		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);

		xml_node_append_title (node, bookmark);
	}
	else
	{
		const gchar *uri = kz_bookmark_get_link(bookmark);
		const gchar *id = kz_bookmark_get_id(bookmark);
		guint visited = kz_bookmark_get_last_visited(bookmark);
		guint added = kz_bookmark_get_added_time(bookmark);
		/*
		 * <bookmark>\n
		 * <title/>
		 * </bookmark>
		 */

		node = kz_xml_element_node_new("bookmark");
		if (uri)
			kz_xml_node_set_attr(node, "href", uri);
		if (id)
			kz_xml_node_set_attr(node, "id", id);
		if (visited != 0)
		{
			gchar *str = g_strdup_printf("%d", visited);
			kz_xml_node_set_attr(node, "visited", str);
			g_free(str);
		}
		if (added != 0)
		{
			gchar *str = g_strdup_printf("%d", added);
			kz_xml_node_set_attr(node, "added", str);
			g_free(str);
		}

		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(node, space);

		xml_node_append_title (node, bookmark);
	}

	return node;
}


static void
kz_xbel_connect_signals (KzBookmark *bookmark)
{
	g_signal_connect(bookmark, "notify",
			 G_CALLBACK(cb_bookmark_notify), NULL);

	if (KZ_IS_BOOKMARK_FILE(bookmark)) return;
	if (!kz_bookmark_is_folder(bookmark)) return;

	g_signal_connect_after(bookmark, "insert-child",
			       G_CALLBACK(cb_bookmark_insert_child),
			       NULL);
	g_signal_connect_after(bookmark, "remove-child",
			       G_CALLBACK(cb_bookmark_remove_child),
			       NULL);

	/* for children */
	{
		GList *node, *children;

		children = kz_bookmark_get_children(bookmark);
		for (node = children; node; node = g_list_next(node))
		{
			KzBookmark *child = node->data;
			kz_xbel_connect_signals(child);
		}
		g_list_free(children);
	}
}


static void
kz_xbel_disconnect_signals (KzBookmark *bookmark)
{
	g_signal_handlers_disconnect_by_func
		(bookmark,
		 G_CALLBACK(cb_bookmark_notify), NULL);

	if (KZ_IS_BOOKMARK_FILE(bookmark)) return;
	if (!kz_bookmark_is_folder(bookmark)) return;

	g_signal_handlers_disconnect_by_func
		(bookmark,
		 G_CALLBACK(cb_bookmark_insert_child), NULL);
	g_signal_handlers_disconnect_by_func
		(bookmark,
		 G_CALLBACK(cb_bookmark_remove_child), NULL);

	/* for children */
	{
		GList *node, *children;

		children = kz_bookmark_get_children(bookmark);
		for (node = children; node; node = g_list_next(node))
		{
			KzBookmark *child = node->data;
			kz_xbel_disconnect_signals(child);
		}
		g_list_free(children);
	}
}


static void
cb_bookmark_insert_child (KzBookmark *bookmark,
			  KzBookmark *child, KzBookmark *sibling)
{
	kz_xbel_insert_xml_node(child, bookmark, sibling);
	kz_xbel_connect_signals(child);
}


static void
cb_bookmark_remove_child (KzBookmark *bookmark, KzBookmark *child)
{
	kz_xbel_disconnect_signals(child);
	kz_xbel_remove_xml_node(child);
}


static void
xml_node_set_title (KzXMLNode *parent, const gchar *title)
{
	KzXMLNode *title_node = NULL, *node, *child;

	g_return_if_fail(parent);

	node = kz_xml_node_first_child(parent);

	/* find title node */
	for (; node; node = kz_xml_node_next(node))
	{
		if (kz_xml_node_name_is(node, "title"))
		{
			title_node = node;
			break;
		}
	}

	g_return_if_fail(title_node);

	/* remove old title */
	child = kz_xml_node_first_child(title_node);
	while(child)
	{
		KzXMLNode *next = kz_xml_node_next(child);

		child = kz_xml_node_remove_child(title_node, child);
		kz_xml_node_unref(child);
		child = next;
	}

	/* set new title */
	child = kz_xml_text_node_new(title);
	kz_xml_node_append_child(title_node, child);
}


static void
xml_node_set_description (KzXMLNode *parent, const gchar *desc)
{
	KzXMLNode *desc_node = NULL, *node, *child;

	g_return_if_fail(parent);

	node = kz_xml_node_first_child(parent);

	/* find desc node */
	for (; node; node = kz_xml_node_next(node))
	{
		if (kz_xml_node_name_is(node, "desc"))
		{
			desc_node = node;
			break;
		}
	}

	/* remove old title, or create new desc node */
	if (desc_node)
	{
		child = kz_xml_node_first_child(node);
		while(child)
		{
			KzXMLNode *next = kz_xml_node_next(child);

			child = kz_xml_node_remove_child(node, child);
			kz_xml_node_unref(child);
			child = next;
		}
	}
	else
	{
		KzXMLNode *space;

		desc_node = kz_xml_element_node_new("desc");
		kz_xml_node_append_child(parent, desc_node);

		space = kz_xml_text_node_new("\n");
		kz_xml_node_append_child(parent, space);
	}

	/* set new description */
	child = kz_xml_text_node_new(desc);
	kz_xml_node_append_child(desc_node, child);
}


static void
cb_bookmark_notify (GObject *object, GParamSpec *pspec)
{
	KzBookmark *bookmark;
	KzXMLNode *node;
	const gchar *prop;
        GValue value = { 0 };

	g_return_if_fail(KZ_IS_BOOKMARK(object));
	bookmark = KZ_BOOKMARK(object);

	if (BOOKMARK_IS_BUILDING(bookmark)) return;

	node = g_object_get_qdata(G_OBJECT(bookmark), node_quark);
	g_return_if_fail(node);
	g_return_if_fail(kz_xml_node_name_is(node, "bookmark") ||
			 kz_xml_node_name_is(node, "folder"));

	prop = g_param_spec_get_name(pspec);
	g_return_if_fail(prop);

        g_value_init(&value, G_PARAM_SPEC_VALUE_TYPE(pspec));
        g_object_get_property(object, prop, &value);

	/*
	 * FIXME! We should use hash table.
	 */
	if (!strcmp(prop, "id"))
	{
		gchar *id = g_value_dup_string(&value);

		kz_xml_node_set_attr(node, "id", id);
		g_free(id);
	}
	else if (!strcmp(prop, "title"))
	{
		gchar *title = g_value_dup_string(&value);

		xml_node_set_title(node, title);
		g_free(title);
	}
	else if (!strcmp(prop, "link"))
	{
		gchar *uri = g_value_dup_string(&value);

		kz_xml_node_set_attr(node, "href", uri);
		g_free(uri);
	}
	else if (!strcmp(prop, "description"))
	{
		gchar *desc = g_value_dup_string(&value);

		xml_node_set_description(node, desc);
		g_free(desc);
	}
	else if (!strcmp(prop, "location"))
	{
		xml_node_set_location(node, bookmark);
	}
	else if (!strcmp(prop, "interval"))
	{
		xml_node_set_interval(node, bookmark);
	}
	else if (!strcmp(prop, "last-visited"))
	{
		guint t = g_value_get_uint(&value);
		gchar *t_str = g_strdup_printf("%d", t);
		kz_xml_node_set_attr(node, "visited", t_str);
		g_free(t_str);
	}
	else if (!strcmp(prop, "added-time"))
	{
		guint t = g_value_get_uint(&value);
		gchar *t_str = g_strdup_printf("%d", t);
		kz_xml_node_set_attr(node, "added", t_str);
		g_free(t_str);
	}
	else if (!strcmp(prop, "smart-list"))
	{
		const GList *smart_list;
		smart_list =  g_value_get_pointer(&value);
		xml_node_set_smart_list(node, bookmark, smart_list);
	}
	else if (!strcmp(prop, "smart-history"))
	{
		KzBookmark *smart_history;
		smart_history =  g_value_get_object(&value);
		xml_node_set_smart_history(node, bookmark, smart_history);
	}
	else if (!strcmp(prop, "xmlrpc"))
	{
		KzXMLNode *metanode;
		gchar *xmlrpc = g_value_dup_string(&value);
		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:xmlrpc", xmlrpc);
		g_free(xmlrpc);
	}
	else if (!strcmp(prop, "xmlrpc-user"))
	{
		KzXMLNode *metanode;
		gchar *xmlrpc = g_value_dup_string(&value);
		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:xmlrpc_user", xmlrpc);
		g_free(xmlrpc);
	}
	else if (!strcmp(prop, "xmlrpc-pass"))
	{
		KzXMLNode *metanode;
		gchar *xmlrpc = g_value_dup_string(&value);
		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:xmlrpc_user", xmlrpc);
		g_free(xmlrpc);
	}
	else if (!strcmp(prop, "current"))
	{
		KzXMLNode *metanode;
		guint pos = g_value_get_uint(&value);
		gchar *pos_str = g_strdup_printf("%d", pos);
		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:current_position", pos_str);
		g_free(pos_str);
	}
	else if (!strcmp(prop, "lock"))
	{
		KzXMLNode *metanode;
		const gchar *str;
		gboolean lock = g_value_get_boolean(&value);
		
		if (lock)
			str = "yes";
		else
			str = "no";

		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:lock", str);
	}
	else if (!strcmp(prop, "auto-refresh"))
	{
		KzXMLNode *metanode;
		const gchar *str;
		gboolean auto_refresh = g_value_get_boolean(&value);
		
		if (auto_refresh)
			str = "yes";
		else
			str = "no";

		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:auto_refresh", str);
	}
	else if (!strcmp(prop, "javascript"))
	{
		KzXMLNode *metanode;
		const gchar *str;
		gboolean javascript = g_value_get_boolean(&value);
		
		if (javascript)
			str = "yes";
		else
			str = "no";

		metanode = xml_node_find_metadata_node(node);
	
		if (!metanode)
			metanode = xml_node_append_metadata_node(node);

		kz_xml_node_set_attr(metanode, "kz:javascript", str);
	}
	g_value_unset(&value);
}


#if 0
void
kz_xbel_save (KzXBEL *xbel)
{
	const gchar *location;;

	g_return_if_fail(KZ_IS_XBEL(xbel));

	if (xbel->xml)
		kz_xml_node_arrange_indent(xbel->xml->root, 0);

	location = kz_bookmark_get_location(KZ_BOOKMARK(xbel));
	if (location && *location)
		kz_xml_save(xbel->xml, location);
}
#endif


static void
kz_xbel_insert_xml_node (KzBookmark *bookmark,
			 KzBookmark *parent, KzBookmark *sibling)
{
	KzXMLNode *node, *space;
	KzXMLNode *parent_node, *sibling_node = NULL;

	g_return_if_fail(KZ_IS_BOOKMARK(parent));
	g_return_if_fail(KZ_IS_BOOKMARK(bookmark));
	g_return_if_fail(!sibling || KZ_IS_BOOKMARK(sibling));

	node = g_object_get_qdata(G_OBJECT(bookmark), node_quark);
	if (node) return;

	parent_node = g_object_get_qdata(G_OBJECT(parent),
					 node_quark);
	if (!parent_node)
	{
		KzXML *xml;

		g_return_if_fail(KZ_IS_BOOKMARK(parent));

		xml = g_object_get_qdata(G_OBJECT(parent), xml_quark);
		g_return_if_fail(KZ_IS_XML(xml));

		parent_node = kz_xml_get_root_element(xml);
		g_return_if_fail (kz_xml_node_name_is(parent_node, "xbel"));
	}

	if (sibling)
		sibling_node = g_object_get_qdata(G_OBJECT(sibling),
						  node_quark);

	node = create_xml_node(bookmark);
	g_object_set_qdata(G_OBJECT(bookmark), node_quark, node);

	kz_xml_node_insert_before(parent_node, node, sibling_node);
	space = kz_xml_text_node_new("\n");
	kz_xml_node_insert_before(parent_node, space, kz_xml_node_next(node));

	if (kz_bookmark_is_folder(bookmark) && !KZ_IS_BOOKMARK_FILE(bookmark))
	{
		GList *node, *children;

		children = kz_bookmark_get_children(bookmark);
		for (node = children; node; node = g_list_next(node))
		{
			KzBookmark *child = node->data;
			kz_xbel_insert_xml_node (child, bookmark, NULL);
		}
		g_list_free(children);
	}
}


static void
kz_xbel_remove_xml_node (KzBookmark *bookmark)
{
	KzXMLNode *node, *parent;

	g_return_if_fail(KZ_IS_BOOKMARK(bookmark));

	if (kz_bookmark_is_folder(bookmark))
	{
		GList *node, *children;

		children = kz_bookmark_get_children(bookmark);
		for (node = children; node; node = g_list_next(node))
		{
			KzBookmark *child = node->data;
			kz_xbel_remove_xml_node (child);
		}
		g_list_free(children);
	}

	node = g_object_get_qdata(G_OBJECT(bookmark), node_quark);
	if (!node) return;

	parent = kz_xml_node_parent(node);
	if (parent)
	{
		KzXMLNode *space;

		space = kz_xml_node_next(node);
		if (space && kz_xml_node_is_space(space))
		{
			space = kz_xml_node_remove_child(parent, space);
			kz_xml_node_unref(space);
		}

		node = kz_xml_node_remove_child(parent, node);
	}

	kz_xml_node_unref(node);
	g_object_set_qdata(G_OBJECT(bookmark), node_quark, NULL);
}
