/*
 * This file is part of the Main Menu.
 *
 * Copyright (c) 2007 Novell, Inc.
 *
 * The Main Menu 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.
 *
 * The Main Menu 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
 * the Main Menu; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "bookmark-agent.h"

#ifdef HAVE_CONFIG_H
#	include <config.h>
#else
#	define PACKAGE "mate-main-menu"
#endif

#include <gtk/gtk.h>

#include <string.h>
#include <stdlib.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>

#include "libslab-utils.h"

#define USER_APPS_STORE_FILE_NAME "applications.xbel"
#define USER_DOCS_STORE_FILE_NAME "documents.xbel"
#define USER_DIRS_STORE_FILE_NAME "places.xbel"
#define SYSTEM_STORE_FILE_NAME    "system-items.xbel"
#define CALC_TEMPLATE_FILE_NAME   "empty.ots"
#define WRITER_TEMPLATE_FILE_NAME "empty.ott"

#define GTK_BOOKMARKS_FILE "bookmarks"

#define TYPE_IS_RECENT(type) ((type) == BOOKMARK_STORE_RECENT_APPS || (type) == BOOKMARK_STORE_RECENT_DOCS)

typedef struct {
	BookmarkStoreType        type;

	BookmarkItem           **items;
	gint                     n_items;
	BookmarkStoreStatus      status;

	GBookmarkFile           *store;
	gboolean                 needs_sync;

	gchar                   *store_path;
	gchar                   *user_store_path;
	gboolean                 user_modifiable;
	gboolean                 reorderable;
	const gchar             *store_filename;

	GFileMonitor            *store_monitor;
	GFileMonitor            *user_store_monitor;

	void                  (* update_path) (BookmarkAgent *);
	void                  (* load_store)  (BookmarkAgent *);
	void                  (* save_store)  (BookmarkAgent *);
	void                  (* create_item) (BookmarkAgent *, const gchar *);

	gchar                   *gtk_store_path;
	GFileMonitor            *gtk_store_monitor;
} BookmarkAgentPrivate;

enum {
	PROP_0,
	PROP_ITEMS,
	PROP_STATUS
};

static BookmarkAgent *instances [BOOKMARK_STORE_N_TYPES];

static BookmarkAgentClass *bookmark_agent_parent_class = NULL;

static void           bookmark_agent_base_init  (BookmarkAgentClass *);
static void           bookmark_agent_class_init (BookmarkAgentClass *);
static void           bookmark_agent_init       (BookmarkAgent      *);
static BookmarkAgent *bookmark_agent_new        (BookmarkStoreType   );

static void get_property (GObject *, guint, GValue *, GParamSpec *);
static void set_property (GObject *, guint, const GValue *, GParamSpec *);
static void finalize     (GObject *);

static void update_agent (BookmarkAgent *);
static void update_items (BookmarkAgent *);
static void save_store   (BookmarkAgent *);
static gint get_rank     (BookmarkAgent *, const gchar *);
static void set_rank     (BookmarkAgent *, const gchar *, gint);

static void load_xbel_store          (BookmarkAgent *);
static void load_places_store        (BookmarkAgent *);
static void update_user_spec_path    (BookmarkAgent *);
static void save_xbel_store          (BookmarkAgent *);
static void create_app_item          (BookmarkAgent *, const gchar *);
static void create_doc_item          (BookmarkAgent *, const gchar *);
static void create_dir_item          (BookmarkAgent *, const gchar *);

static void store_monitor_cb (GFileMonitor *, GFile *, GFile *,
                              GFileMonitorEvent, gpointer);
static void weak_destroy_cb  (gpointer, GObject *);

static gchar *find_package_data_file (const gchar *filename);

static gint BookmarkAgent_private_offset;

static inline gpointer bookmark_agent_get_instance_private (BookmarkAgent *this)
{
	return (G_STRUCT_MEMBER_P (this, BookmarkAgent_private_offset));
}

GType
bookmark_agent_get_type ()
{
	static GType g_define_type_id = 0;

	if (G_UNLIKELY (g_define_type_id == 0)) {
		static const GTypeInfo info = {
			sizeof (BookmarkAgentClass),
			(GBaseInitFunc) bookmark_agent_base_init,
			NULL,
			(GClassInitFunc) bookmark_agent_class_init,
			NULL, NULL,
			sizeof (BookmarkAgent), 0,
			(GInstanceInitFunc) bookmark_agent_init,
			NULL
		};

		g_define_type_id = g_type_register_static (
			G_TYPE_OBJECT, "BookmarkAgent", & info, 0);
		G_ADD_PRIVATE (BookmarkAgent);
	}

	return g_define_type_id;
}

gboolean
bookmark_agent_has_item (BookmarkAgent *this, const gchar *uri)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);
	return g_bookmark_file_has_item (priv->store, uri);
}

void
bookmark_agent_add_item (BookmarkAgent *this, const BookmarkItem *item)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	if (! item)
		return;

	g_return_if_fail (priv->user_modifiable);
	g_return_if_fail (item->uri);
	g_return_if_fail (item->mime_type);

	g_bookmark_file_set_mime_type (priv->store, item->uri, item->mime_type);

	if (item->mtime)
#if GLIB_CHECK_VERSION(2,66,0)
		g_bookmark_file_set_modified_date_time (priv->store, item->uri, item->mtime);
#else
		g_bookmark_file_set_modified (priv->store, item->uri, item->mtime);
#endif

	if (item->title)
		g_bookmark_file_set_title (priv->store, item->uri, item->title);

	g_bookmark_file_add_application (priv->store, item->uri, item->app_name, item->app_exec);

	set_rank (this, item->uri, g_bookmark_file_get_size (priv->store) - 1);

	save_store (this);
}

void
bookmark_agent_move_item (BookmarkAgent *this, const gchar *uri, const gchar *uri_new)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	GError *error = NULL;

	if (! TYPE_IS_RECENT (priv->type))
		return;

	gtk_recent_manager_move_item (gtk_recent_manager_get_default (), uri, uri_new, &error);
	if (error) {
		g_warning ("Unable to update %s with renamed file, [%s] -> [%s]: %s",
		           priv->store_path, uri, uri_new, error->message);
		g_error_free (error);
	}
}

void
bookmark_agent_purge_items (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	GError *error = NULL;

	gchar **uris = NULL;
	gsize   uris_len;
	gint    i;
	g_return_if_fail (priv->user_modifiable);

	uris = g_bookmark_file_get_uris (priv->store, &uris_len);
	if (TYPE_IS_RECENT (priv->type)) {
		for (i = 0; i < uris_len; i++) {
			gtk_recent_manager_remove_item (gtk_recent_manager_get_default (), uris [i], &error);
			if (error) {
				g_warning ("Unable to remove [%s] from %s: %s",
				           priv->store_path, uris [i], error->message);
				g_error_free (error);
			}
		}
	} else {
		for (i = 0; i < uris_len; i++) {
			g_bookmark_file_remove_item (priv->store, uris [i], NULL);
		}
		save_store (this);
	}
	g_strfreev (uris);
}

void
bookmark_agent_remove_item (BookmarkAgent *this, const gchar *uri)
{
        BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);
        gint rank;

        GError *error = NULL;

        gchar **uris = NULL;
        gint    rank_i;
        gint    i;

        g_return_if_fail (priv->user_modifiable);

	if (! bookmark_agent_has_item (this, uri))
		return;

	if (TYPE_IS_RECENT (priv->type)) {
		gtk_recent_manager_remove_item (gtk_recent_manager_get_default (), uri, &error);
		if (error) {
			g_warning ("Unable to remove [%s] from %s: %s", priv->store_path, uri, error->message);
			g_error_free (error);
		}
	}
	else {
		rank = get_rank (this, uri);

		g_bookmark_file_remove_item (priv->store, uri, NULL);

		if (rank >= 0) {
			uris = g_bookmark_file_get_uris (priv->store, NULL);

			for (i =  0; uris && uris [i]; ++i) {
				rank_i = get_rank (this, uris [i]);

				if (rank_i > rank)
					set_rank (this, uris [i], rank_i - 1);
			}

			g_strfreev (uris);
		}

		save_store (this);
	}
}

void
bookmark_agent_reorder_items (BookmarkAgent *this, const gchar **uris)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gint i;

	g_return_if_fail (priv->reorderable);

	for (i = 0; uris && uris [i]; ++i)
		set_rank (this, uris [i], i);

	save_store (this);
}

#if !GLIB_CHECK_VERSION(2,66,0)
static gint
recent_item_mru_comp_func (gconstpointer a, gconstpointer b)
{
	return ((BookmarkItem *) b)->mtime - ((BookmarkItem *) a)->mtime;
}
#endif

static GList *
make_items_from_bookmark_file (BookmarkAgent *this, GBookmarkFile *store)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);
	gchar **uris;
	gint i;
	GList *items_ordered;

	if (!store)
		return NULL;

	uris = g_bookmark_file_get_uris (store, NULL);
	items_ordered = NULL;

	for (i = 0; uris && uris [i]; ++i) {
		gboolean include;

		if (priv->type == BOOKMARK_STORE_RECENT_APPS)
			include = g_bookmark_file_has_group (store, uris [i], "recently-used-apps", NULL);
		else
			include = ! g_bookmark_file_get_is_private (store, uris [i], NULL);

		if (include) {
			BookmarkItem *item;

			item = g_new0 (BookmarkItem, 1);

			item->uri       = g_strdup (uris [i]);
			item->mime_type = g_bookmark_file_get_mime_type (store, uris [i], NULL);
#if GLIB_CHECK_VERSION(2,66,0)
			item->mtime     = g_bookmark_file_get_modified_date_time  (store, uris [i], NULL);
#else
			item->mtime     = g_bookmark_file_get_modified (store, uris [i], NULL);
#endif

			items_ordered = g_list_prepend (items_ordered, item);
		}
	}

#if GLIB_CHECK_VERSION(2,66,0)
	items_ordered = g_list_sort (items_ordered, g_date_time_compare);
#else
	items_ordered = g_list_sort (items_ordered, recent_item_mru_comp_func);
#endif
	g_strfreev (uris);

	return items_ordered;
}

void
bookmark_agent_update_from_bookmark_file (BookmarkAgent *this, GBookmarkFile *store)
{
	BookmarkAgentPrivate *priv;
	GList *items_ordered;
	GList  *node;

	g_return_if_fail (IS_BOOKMARK_AGENT (this));

	priv = bookmark_agent_get_instance_private (this);

	items_ordered = make_items_from_bookmark_file (this, store);

	g_bookmark_file_free (priv->store);
	priv->store = g_bookmark_file_new ();

	for (node = items_ordered; node; node = node->next) {
		BookmarkItem *item;

		item = (BookmarkItem *) node->data;

		g_bookmark_file_set_mime_type (priv->store, item->uri, item->mime_type);
		#if GLIB_CHECK_VERSION(2,66,0)
		g_bookmark_file_set_modified_date_time  (priv->store, item->uri, item->mtime);
		#else
		g_bookmark_file_set_modified (priv->store, item->uri, item->mtime);
		#endif

		bookmark_item_free (item);
	}

	g_list_free (items_ordered);

	update_items (this);
}

void
bookmark_item_free (BookmarkItem *item)
{
	if (! item)
		return;

	g_free (item->uri);
	g_free (item->title);
	g_free (item->mime_type);
	g_free (item->icon);
	g_free (item->app_name);
	g_free (item->app_exec);
	g_free (item);
}

static void
bookmark_agent_base_init (BookmarkAgentClass *this_class)
{
	gint i;

	for (i = 0; i < BOOKMARK_STORE_N_TYPES; ++i)
		instances [i] = NULL;
}

static void
bookmark_agent_class_init (BookmarkAgentClass *this_class)
{
	GObjectClass *g_obj_class = G_OBJECT_CLASS (this_class);

	GParamSpec *items_pspec;
	GParamSpec *status_pspec;

	if (BookmarkAgent_private_offset != 0)
		g_type_class_adjust_private_offset (this_class, &BookmarkAgent_private_offset);

	g_obj_class->get_property = get_property;
	g_obj_class->set_property = set_property;
	g_obj_class->finalize     = finalize;

	items_pspec = g_param_spec_pointer (
		BOOKMARK_AGENT_ITEMS_PROP, BOOKMARK_AGENT_ITEMS_PROP,
		"the null-terminated list which contains the bookmark items in this store",
		G_PARAM_READABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB);

	status_pspec = g_param_spec_int (
		BOOKMARK_AGENT_STORE_STATUS_PROP, BOOKMARK_AGENT_STORE_STATUS_PROP, "the status of the store",
		BOOKMARK_STORE_DEFAULT_ONLY, BOOKMARK_STORE_USER, BOOKMARK_STORE_DEFAULT,
		G_PARAM_READABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB);

	g_object_class_install_property (g_obj_class, PROP_ITEMS,  items_pspec);
	g_object_class_install_property (g_obj_class, PROP_STATUS, status_pspec);

	bookmark_agent_parent_class = g_type_class_peek_parent (this_class);
}

static void
bookmark_agent_init (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	priv->type                = -1;

 	priv->items               = NULL;
 	priv->n_items             = 0;
	priv->status              = BOOKMARK_STORE_ABSENT;

	priv->store               = NULL;
	priv->needs_sync          = FALSE;

	priv->store_path          = NULL;
	priv->user_store_path     = NULL;
	priv->user_modifiable     = FALSE;
	priv->reorderable         = FALSE;
	priv->store_filename      = NULL;

	priv->store_monitor       = NULL;
	priv->user_store_monitor  = NULL;

	priv->update_path         = NULL;
	priv->load_store          = NULL;
	priv->save_store          = NULL;
	priv->create_item         = NULL;

	priv->gtk_store_path      = NULL;
	priv->gtk_store_monitor   = NULL;
}

static BookmarkAgent *
bookmark_agent_new (BookmarkStoreType type)
{
	BookmarkAgent        *this;
	BookmarkAgentPrivate *priv;
	GFile *gtk_store_file;

	this = g_object_new (BOOKMARK_AGENT_TYPE, NULL);
	priv = bookmark_agent_get_instance_private (this);

	priv->type  = type;
	priv->store = g_bookmark_file_new ();

	switch (type) {
		case BOOKMARK_STORE_USER_APPS:
			priv->store_filename = USER_APPS_STORE_FILE_NAME;
			priv->create_item    = create_app_item;

			break;

		case BOOKMARK_STORE_USER_DOCS:
			priv->store_filename = USER_DOCS_STORE_FILE_NAME;
			priv->create_item    = create_doc_item;

			break;

		case BOOKMARK_STORE_USER_DIRS:
			priv->store_filename = USER_DIRS_STORE_FILE_NAME;
			priv->create_item    = create_dir_item;

			priv->user_modifiable = TRUE;
			priv->reorderable     = FALSE;

			priv->load_store = load_places_store;

			priv->gtk_store_path = g_build_filename (g_get_user_config_dir (),
                                                     "gtk-3.0", GTK_BOOKMARKS_FILE, NULL);
			gtk_store_file = g_file_new_for_path (priv->gtk_store_path);
			priv->gtk_store_monitor = g_file_monitor_file (gtk_store_file,
								       0, NULL, NULL);
			if (priv->gtk_store_monitor) {
				g_signal_connect (priv->gtk_store_monitor, "changed",
						  G_CALLBACK (store_monitor_cb), this);
			}

			g_object_unref (gtk_store_file);

			break;

		case BOOKMARK_STORE_RECENT_APPS:
		case BOOKMARK_STORE_RECENT_DOCS:
			priv->user_modifiable = TRUE;
			priv->reorderable     = FALSE;

			priv->store_path = g_build_filename (g_get_user_data_dir (), "recently-used.xbel", NULL);

			break;

		case BOOKMARK_STORE_SYSTEM:
			priv->store_filename = SYSTEM_STORE_FILE_NAME;
			priv->create_item    = create_app_item;

			break;

		default:
			break;
	}

	if (
		type == BOOKMARK_STORE_USER_APPS || type == BOOKMARK_STORE_USER_DOCS ||
		type == BOOKMARK_STORE_USER_DIRS || type == BOOKMARK_STORE_SYSTEM)
	{
		priv->user_modifiable = TRUE;

		priv->user_store_path = g_build_filename (
			g_get_user_data_dir (), PACKAGE, priv->store_filename, NULL);

		priv->update_path = update_user_spec_path;
	}

	if (type == BOOKMARK_STORE_USER_APPS || type == BOOKMARK_STORE_USER_DOCS || type == BOOKMARK_STORE_SYSTEM) {
		priv->reorderable = TRUE;
		priv->load_store  = load_xbel_store;
		priv->save_store  = save_xbel_store;
	}

	update_agent (this);

	return this;
}

BookmarkAgent *
bookmark_agent_get_instance (BookmarkStoreType type)
{
	g_return_val_if_fail (0 <= type, NULL);
	g_return_val_if_fail (type < BOOKMARK_STORE_N_TYPES, NULL);

	if (! instances [type]) {
		instances [type] = bookmark_agent_new (type);
		g_object_weak_ref (G_OBJECT (instances [type]), weak_destroy_cb, GINT_TO_POINTER (type));
	}
	else
		g_object_ref (G_OBJECT (instances [type]));

	return instances [type];
}

static void
get_property (GObject *g_obj, guint prop_id, GValue *value, GParamSpec *pspec)
{
	BookmarkAgent        *this = BOOKMARK_AGENT (g_obj);
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	switch (prop_id) {
		case PROP_ITEMS:
			g_value_set_pointer (value, priv->items);
			break;

		case PROP_STATUS:
			g_value_set_int (value, priv->status);
			break;
	}
}

static void
set_property (GObject *g_obj, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	/* no writeable properties */
}

static void
finalize (GObject *g_obj)
{
	BookmarkAgent *this = BOOKMARK_AGENT (g_obj);
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gint i;

	for (i = 0; priv->items && priv->items [i]; ++i)
		bookmark_item_free (priv->items [i]);

	g_free (priv->items);
	g_free (priv->store_path);
	g_free (priv->user_store_path);
	g_free (priv->gtk_store_path);

	if (priv->store_monitor) {
		g_signal_handlers_disconnect_by_func (priv->store_monitor, store_monitor_cb, this);
		g_file_monitor_cancel (priv->store_monitor);
		g_object_unref (priv->store_monitor);
	}

	if (priv->user_store_monitor) {
		g_signal_handlers_disconnect_by_func (priv->user_store_monitor, store_monitor_cb, this);
		g_file_monitor_cancel (priv->user_store_monitor);
		g_object_unref (priv->user_store_monitor);
	}

	if (priv->gtk_store_monitor) {
		g_signal_handlers_disconnect_by_func (priv->gtk_store_monitor, store_monitor_cb, this);
		g_file_monitor_cancel (priv->gtk_store_monitor);
		g_object_unref (priv->gtk_store_monitor);
	}

	g_bookmark_file_free (priv->store);

	G_OBJECT_CLASS (bookmark_agent_parent_class)->finalize (g_obj);
}

static void
update_agent (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	if (priv->update_path)
		priv->update_path (this);

	if (priv->load_store)
		priv->load_store (this);

	update_items (this);
}

static void
update_items (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar    **uris            = NULL;
	gchar    **uris_ordered    = NULL;
	gsize      n_uris          = 0;
	gint       rank            = -1;
	gint       rank_corr       = -1;
	gboolean   needs_update    = FALSE;
	gboolean   store_corrupted = FALSE;
	gchar     *new_title, *old_title;

	gint i;

	uris = g_bookmark_file_get_uris (priv->store, & n_uris);
	uris_ordered = g_new0 (gchar *, n_uris + 1);
	uris_ordered [n_uris] = NULL;

	for (i = 0; uris && uris [i]; ++i) {
		rank = get_rank (this, uris [i]);

		if (rank < 0 || rank >= n_uris)
			rank = i;

		if (uris_ordered [rank]) {
			store_corrupted = TRUE;
			rank_corr = rank;

			for (rank = 0; rank < n_uris; ++rank)
				if (! uris_ordered [rank])
					break;

			g_warning (
				"store corruption [%s] - multiple uris with same rank (%d): [%s] [%s], moving latter to %d",
				priv->store_path, rank_corr, uris_ordered [rank_corr], uris [i], rank);
		}

		set_rank (this, uris [i], rank);

		uris_ordered [rank] = uris [i];
	}

	if (priv->n_items != n_uris)
		needs_update = TRUE;

	for (i = 0; ! needs_update && uris_ordered && uris_ordered [i]; ++i) {
		if (priv->type == BOOKMARK_STORE_USER_DIRS) {
			new_title = g_bookmark_file_get_title (priv->store, uris_ordered [i], NULL);
			old_title = priv->items [i]->title;
			if (!new_title && !old_title) {
				if (strcmp (priv->items [i]->uri, uris_ordered [i]))
					needs_update = TRUE;
			}
			else if ((new_title && !old_title) || (!new_title && old_title))
				needs_update = TRUE;
			else if (strcmp (old_title, new_title))
				needs_update = TRUE;
			g_free (new_title);
		}
		else if (strcmp (priv->items [i]->uri, uris_ordered [i]))
			needs_update = TRUE;
	}

	if (needs_update) {
		for (i = 0; priv->items && priv->items [i]; ++i)
			bookmark_item_free (priv->items [i]);

		g_free (priv->items);

		priv->n_items = n_uris;
		priv->items = g_new0 (BookmarkItem *, priv->n_items + 1);

		for (i = 0; uris_ordered && uris_ordered [i]; ++i) {
			priv->items [i]            = g_new0 (BookmarkItem, 1);
			priv->items [i]->uri       = g_strdup (uris_ordered [i]);
			priv->items [i]->title     = g_bookmark_file_get_title     (priv->store, uris_ordered [i], NULL);
			priv->items [i]->mime_type = g_bookmark_file_get_mime_type (priv->store, uris_ordered [i], NULL);
			#if GLIB_CHECK_VERSION(2,66,0)
			priv->items [i]->mtime     = g_bookmark_file_get_modified_date_time (priv->store, uris_ordered [i], NULL);
			#else
			priv->items [i]->mtime     = g_bookmark_file_get_modified (priv->store, uris_ordered [i], NULL);
			#endif
			priv->items [i]->app_name  = NULL;
			priv->items [i]->app_exec  = NULL;

			g_bookmark_file_get_icon (priv->store, uris_ordered [i], & priv->items [i]->icon, NULL, NULL);
		}

		/* Since the bookmark store for recently-used items is updated by the caller of BookmarkAgent,
		 * we don't emit notifications in that case.  The caller will know when to update itself.
		 */
		if (!TYPE_IS_RECENT (priv->type))
			g_object_notify (G_OBJECT (this), BOOKMARK_AGENT_ITEMS_PROP);
	}

	if (store_corrupted)
		save_store (this);

	g_strfreev (uris);
	g_free (uris_ordered);
}

static void
save_store (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar *dir;

	g_return_if_fail (priv->user_modifiable);

	priv->needs_sync = TRUE;
	priv->update_path (this);

	dir = g_path_get_dirname (priv->store_path);
	g_mkdir_with_parents (dir, 0700);
	g_free (dir);

	priv->save_store (this);
	update_items (this);
}

static gint
get_rank (BookmarkAgent *this, const gchar *uri)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar **groups;
	gint    rank;

	gint i;

	if (! priv->reorderable)
		return -1;

	groups = g_bookmark_file_get_groups (priv->store, uri, NULL, NULL);
	rank   = -1;

	for (i = 0; groups && groups [i]; ++i) {
		if (g_str_has_prefix (groups [i], "rank-")) {
			if (rank >= 0)
				g_warning (
					"store corruption - multiple ranks for same uri: [%s] [%s]",
					priv->store_path, uri);

			rank = atoi (& groups [i] [5]);
		}
	}

	g_strfreev (groups);

	return rank;
}

static void
set_rank (BookmarkAgent *this, const gchar *uri, gint rank)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar **groups;
	gchar  *group;

	gint i;

	if (! (priv->reorderable && bookmark_agent_has_item (this, uri)))
		return;

	groups = g_bookmark_file_get_groups (priv->store, uri, NULL, NULL);

	for (i = 0; groups && groups [i]; ++i)
		if (g_str_has_prefix (groups [i], "rank-"))
			g_bookmark_file_remove_group (priv->store, uri, groups [i], NULL);

	g_strfreev (groups);

	group = g_strdup_printf ("rank-%d", rank);
	g_bookmark_file_add_group (priv->store, uri, group);
	g_free (group);
}

static void
load_xbel_store (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar **uris = NULL;

	GError *error = NULL;

	gint i;
	gboolean success;

	if (!priv->store_path)
		success = FALSE;
	else {
		success = g_bookmark_file_load_from_file (priv->store, priv->store_path, & error);
	}

	if (!success) {
		g_bookmark_file_free (priv->store);
		priv->store = g_bookmark_file_new ();

		if (error) {
			g_debug ("Couldn't load bookmark file [%s]: %s", priv->store_path, error->message);
			g_error_free (error);
		} else {
			g_debug ("Couldn't load bookmark file [NULL]");
		}
		return;
	}

	uris = g_bookmark_file_get_uris (priv->store, NULL);

	for (i = 0; uris && uris [i]; ++i)
		priv->create_item (this, uris [i]);

	g_strfreev (uris);
}

static void
load_places_store (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar **uris;
	gchar **groups;
	gchar **bookmarks = NULL;

	gchar  *buf, *label, *uri;

	gint i, j, bookmark_len;

	load_xbel_store (this);

	uris = g_bookmark_file_get_uris (priv->store, NULL);

	for (i = 0; uris && uris [i]; ++i) {
		groups = g_bookmark_file_get_groups (priv->store, uris [i], NULL, NULL);

		for (j = 0; groups && groups [j]; ++j) {
			if (! strcmp (groups [j], "gtk-bookmarks")) {
				g_bookmark_file_remove_item (priv->store, uris [i], NULL);

				break;
			}
		}

		g_strfreev (groups);
	}

	g_strfreev (uris);

	g_file_get_contents (priv->gtk_store_path, & buf, NULL, NULL);

	if (buf) {
		bookmarks = g_strsplit (buf, "\n", -1);
		g_free (buf);
	}

	for (i = 0; bookmarks && bookmarks [i]; ++i) {
		bookmark_len = strlen (bookmarks [i]);
		if (bookmark_len > 0) {
			label = strstr (bookmarks[i], " ");
			if (label != NULL)
				uri = g_strndup (bookmarks [i], bookmark_len - strlen (label));
			else
				uri = bookmarks [i];
			g_bookmark_file_add_group (priv->store, uri, "gtk-bookmarks");
			priv->create_item (this, uri);
			if (label != NULL) {
				label++;
				if (strlen (label) > 0)
					g_bookmark_file_set_title (priv->store, uri, label);
				g_free (uri);
			}
		}
	}

	g_strfreev (bookmarks);
}

static gchar *
find_package_data_file (const gchar *filename)
{
	const gchar * const *dirs = NULL;
	gchar               *path = NULL;
	gint                 i;

	dirs = g_get_system_data_dirs ();

	for (i = 0; ! path && dirs && dirs [i]; ++i) {
		path = g_build_filename (dirs [i], PACKAGE, filename, NULL);

		if (! g_file_test (path, G_FILE_TEST_EXISTS)) {
			g_free (path);
			path = NULL;
		}
	}

	return path;
}

static void
update_user_spec_path (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gboolean  use_user_path;
	gchar    *path = NULL;

	BookmarkStoreStatus status;

	use_user_path = priv->user_modifiable &&
		(priv->needs_sync || g_file_test (priv->user_store_path, G_FILE_TEST_EXISTS));

	if (use_user_path)
		path = g_strdup (priv->user_store_path);
	else
		path = find_package_data_file (priv->store_filename);

	if (use_user_path)
		status = BOOKMARK_STORE_USER;
	else if (path && priv->user_modifiable)
		status = BOOKMARK_STORE_DEFAULT;
	else if (path)
		status = BOOKMARK_STORE_DEFAULT_ONLY;
	else
		status = BOOKMARK_STORE_ABSENT;

	if (priv->status != status) {
		priv->status = status;
		g_object_notify (G_OBJECT (this), BOOKMARK_AGENT_STORE_STATUS_PROP);

		if (priv->user_store_monitor) {
			g_file_monitor_cancel (priv->user_store_monitor);
			g_object_unref (priv->user_store_monitor);
			priv->user_store_monitor = NULL;
		}

		if (priv->status == BOOKMARK_STORE_DEFAULT) {
			GFile *user_store_file;

			user_store_file = g_file_new_for_path (priv->user_store_path);
			priv->user_store_monitor = g_file_monitor_file (user_store_file,
									0, NULL, NULL);
			if (priv->user_store_monitor) {
				g_signal_connect (priv->user_store_monitor, "changed",
						  G_CALLBACK (store_monitor_cb), this);
			}

			g_object_unref (user_store_file);
		}
	}

	if (g_strcmp0 (priv->store_path, path)) {
		g_free (priv->store_path);
		priv->store_path = path;

		if (priv->store_monitor) {
			g_file_monitor_cancel (priv->store_monitor);
			g_object_unref (priv->store_monitor);
		}

		if (priv->store_path) {
			GFile *store_file;

			store_file = g_file_new_for_path (priv->store_path);
			priv->store_monitor = g_file_monitor_file (store_file,
								   0, NULL, NULL);
			if (priv->store_monitor) {
				g_signal_connect (priv->store_monitor, "changed",
						  G_CALLBACK (store_monitor_cb), this);
			}

			g_object_unref (store_file);
		}
	}
	else
		g_free (path);
}

static void
save_xbel_store (BookmarkAgent *this)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	GError *error = NULL;

	if (g_bookmark_file_to_file (priv->store, priv->store_path, &error))
		return;

	if (error) {
		g_warning ("Couldn't save bookmark file [%s]: %s", priv->store_path, error->message);
		g_error_free (error);
	} else {
		g_warning ("Couldn't save bookmark file [%s]", priv->store_path);
	}
}

static void
create_app_item (BookmarkAgent *this, const gchar *uri)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	MateDesktopItem *ditem;
	gchar *uri_new = NULL;

	ditem = libslab_mate_desktop_item_new_from_unknown_id (uri);

	if (ditem) {
		uri_new = g_strdup (mate_desktop_item_get_location (ditem));
		mate_desktop_item_unref (ditem);
	}

	if (! uri_new)
		return;

	if (g_strcmp0 (uri, uri_new))
		g_bookmark_file_move_item (priv->store, uri, uri_new, NULL);

	g_free (uri_new);
}

static void
create_doc_item (BookmarkAgent *this, const gchar *uri)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar *uri_new = NULL;

	if ((strcmp (uri, "BLANK_SPREADSHEET") == 0) || (strcmp (uri, "BLANK_DOCUMENT") == 0)) {
		gchar *template = NULL;
		gchar *file;

		gchar *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS));
		if (!dir)
			dir = g_build_filename (g_get_home_dir (), "Documents", NULL);

		if (strcmp (uri, "BLANK_SPREADSHEET") == 0) {
			g_bookmark_file_set_title (priv->store, uri, "BLANK_SPREADSHEET");
			file = g_strconcat (_("New Spreadsheet"), ".ots", NULL);
			template = find_package_data_file (CALC_TEMPLATE_FILE_NAME);
		} else {
			g_bookmark_file_set_title (priv->store, uri, "BLANK_DOCUMENT");
			file = g_strconcat (_("New Document"), ".ott", NULL);
			template = find_package_data_file (WRITER_TEMPLATE_FILE_NAME);
		}

		gchar *path = g_build_filename (dir, file, NULL);
		if (!g_file_test (path, G_FILE_TEST_EXISTS)) {
			g_mkdir_with_parents (dir, 0700);

			if (template != NULL) {
				gchar *contents;
				gsize length;

				if (g_file_get_contents (template, &contents, &length, NULL))
					g_file_set_contents (path, contents, length, NULL);

				g_free (contents);
			} else {
				fclose (g_fopen (path, "w"));
			}
		}

		uri_new = g_filename_to_uri (path, NULL, NULL);

		g_free (dir);
		g_free (file);
		g_free (path);
		g_free (template);
	}

	if (!uri_new)
		return;

	if (g_strcmp0 (uri, uri_new))
		g_bookmark_file_move_item (priv->store, uri, uri_new, NULL);

	g_free (uri_new);
}

static void
create_dir_item (BookmarkAgent *this, const gchar *uri)
{
	BookmarkAgentPrivate *priv = bookmark_agent_get_instance_private (this);

	gchar *uri_new = NULL;
	gchar *path = NULL;
	gchar *name = NULL;
	gchar *icon = NULL;

	gchar *search_string = NULL;

	gboolean gotta_free_name = FALSE;

	if (strcmp (uri, "HOME") == 0) {
		uri_new = g_filename_to_uri (g_get_home_dir (), NULL, NULL);
		name = g_strdup (C_("Home folder", "Home"));
		gotta_free_name = TRUE;
		icon = "user-home";
	} else if (strcmp (uri, "DOCUMENTS") == 0) {
		path = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS));
		if (!path)
			path = g_build_filename (g_get_home_dir (), "Documents", NULL);
		name = _("Documents");
		uri_new = g_filename_to_uri (path, NULL, NULL);
	} else if (strcmp (uri, "DESKTOP") == 0) {
		path = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP));
		if (!path)
			path = g_build_filename (g_get_home_dir (), "Desktop", NULL);
		name = _("Desktop");
		uri_new = g_filename_to_uri (path, NULL, NULL);
		icon = "user-desktop";
	} else if (strcmp (uri, "file:///") == 0) {
		icon = "drive-harddisk";
		name = _("File System");
	} else if (strcmp (uri, "network:") == 0) {
		icon = "network-workgroup";
		name = _("Network Servers");
	} else if (g_str_has_prefix (uri, "x-caja-search")) {
		icon = "system-search";

		path = g_build_filename (g_get_user_data_dir (), "caja", "searches", & uri [21], NULL);

		if (g_file_test (path, G_FILE_TEST_EXISTS)) {
			gchar *buf = NULL;
			g_file_get_contents (path, &buf, NULL, NULL);

			gchar *tag_open_ptr  = NULL;
			gchar *tag_close_ptr = NULL;

			if (buf) {
				tag_open_ptr  = strstr (buf, "<text>");
				tag_close_ptr = strstr (buf, "</text>");
			}

			if (tag_open_ptr && tag_close_ptr) {
				tag_close_ptr [0] = '\0';
				tag_close_ptr [0] = 'a';
				search_string = g_strdup_printf ("\"%s\"", &tag_open_ptr[6]);
			}

			g_free (buf);
		}

		if (search_string) {
			name = search_string;
			gotta_free_name = TRUE;
		} else {
			name = _("Search");
		}
	}

	if (icon)
		g_bookmark_file_set_icon (priv->store, uri, icon, "image/png");

	if (name)
		g_bookmark_file_set_title (priv->store, uri, name);

	if (uri_new && g_strcmp0 (uri, uri_new))
		g_bookmark_file_move_item (priv->store, uri, uri_new, NULL);

	if (gotta_free_name) {
		g_free (name);
	}

	g_free (path);
	g_free (uri_new);
}

static void
store_monitor_cb (GFileMonitor *mon, GFile *f1, GFile *f2,
                  GFileMonitorEvent event_type, gpointer user_data)
{
	update_agent (BOOKMARK_AGENT (user_data));
}

static void
weak_destroy_cb (gpointer data, GObject *g_obj)
{
	instances [GPOINTER_TO_INT (data)] = NULL;
}
