/*
A library to allow applictions to provide simple indications of
information to be displayed to users of the application through the
interface shell.

Copyright 2009 Canonical Ltd.

Authors:
    Ted Gould <ted@canonical.com>

This program is free software: you can redistribute it and/or modify it 
under the terms of either or both of the following licenses:

1) the GNU Lesser General Public License version 3, as published by the 
Free Software Foundation; and/or
2) the GNU Lesser General Public License version 2.1, as published by 
the Free Software Foundation.

This program is distributed in the hope that it will be useful, but 
WITHOUT ANY WARRANTY; without even the implied warranties of 
MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR 
PURPOSE.  See the applicable version of the GNU Lesser General Public 
License for more details.

You should have received a copy of both the GNU Lesser General Public 
License version 3 and version 2.1 along with this program.  If not, see 
<http://www.gnu.org/licenses/>
*/

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

#include "listener.h"
#include "listener-marshal.h"
#include <dbus/dbus-glib-bindings.h>
#include "dbus-indicate-client.h"
#include "dbus-listener-client.h"
#include "interests-priv.h"

/* Errors */
enum {
	LAST_ERROR
};

/* Signals */
enum {
	INDICATOR_ADDED,
	INDICATOR_REMOVED,
	INDICATOR_MODIFIED,
	SERVER_ADDED,
	SERVER_REMOVED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

#include "listener-private.h"

typedef struct {
	DBusGProxy * proxy;
	DBusGProxy * property_proxy;
	DBusGConnection * connection;
	gchar * name;
	gchar * type;
	IndicateListener * listener;
	GHashTable * indicators;
	guint introspect_level;

	IndicateListenerServer server;
} proxy_t;

static gint
proxy_t_equal (gconstpointer pa, gconstpointer pb)
{
	proxy_t * a = (proxy_t *)pa; proxy_t * b = (proxy_t *)pb;

	if (a->connection == b->connection) {
		return g_strcmp0(a->name, b->name);
	} else {
		/* we're only using this for equal, not sorting */
		return 1;
	}
}

typedef struct {
	DBusGConnection * bus;
	gchar * name;
	gboolean startup;
} proxy_todo_t;

G_DEFINE_TYPE (IndicateListener, indicate_listener, G_TYPE_OBJECT);

/* Prototypes */
static void indicate_listener_finalize (GObject * obj);
static void dbus_owner_change (DBusGProxy * proxy, const gchar * name, const gchar * prev, const gchar * new, IndicateListener * listener);
static void proxy_struct_destroy (gpointer data);
static void build_todo_list_cb (DBusGProxy * proxy, char ** names, GError * error, void * data);
static void todo_list_add (const gchar * name, DBusGProxy * proxy, IndicateListener * listener, gboolean startup);
static gboolean todo_idle (gpointer data);
static void get_type_cb (IndicateListener * listener, IndicateListenerServer * server, gchar * type, gpointer data);
static void proxy_server_added (DBusGProxy * proxy, const gchar * type, proxy_t * proxyt);
static void proxy_indicator_added (DBusGProxy * proxy, guint id, const gchar * type, proxy_t * proxyt);
static void proxy_indicator_removed (DBusGProxy * proxy, guint id, const gchar * type, proxy_t * proxyt);
static void proxy_indicator_modified (DBusGProxy * proxy, guint id, const gchar * type, proxy_t * proxyt);
static void proxy_get_indicator_list (DBusGProxy * proxy, GArray * indicators, GError * error, gpointer data);
static void proxy_get_indicator_type (DBusGProxy * proxy, gchar * type, GError * error, gpointer data);
static void proxy_indicators_free (gpointer data);
static void introspect_this (DBusGProxy * proxy, char * OUT_data, GError * error, gpointer data);

/* DBus interface */
gboolean _indicate_listener_get_indicator_servers (IndicateListener * listener, GList * servers);

/* Need the above prototypes */
#include "dbus-listener-server.h"

/* Code */
static void
indicate_listener_class_init (IndicateListenerClass * class)
{
	/* g_debug("Listener Class Initialized"); */
	GObjectClass * gobj;
	gobj = G_OBJECT_CLASS(class);

	g_type_class_add_private (class, sizeof (IndicateListenerPrivate));

	gobj->finalize = indicate_listener_finalize;

	signals[INDICATOR_ADDED] = g_signal_new(INDICATE_LISTENER_SIGNAL_INDICATOR_ADDED,
	                                        G_TYPE_FROM_CLASS (class),
	                                        G_SIGNAL_RUN_LAST,
	                                        G_STRUCT_OFFSET (IndicateListenerClass, indicator_added),
	                                        NULL, NULL,
	                                        _indicate_listener_marshal_VOID__POINTER_POINTER_STRING,
	                                        G_TYPE_NONE, 3, INDICATE_TYPE_LISTENER_SERVER, INDICATE_TYPE_LISTENER_INDICATOR, G_TYPE_STRING);
	signals[INDICATOR_REMOVED] = g_signal_new(INDICATE_LISTENER_SIGNAL_INDICATOR_REMOVED,
	                                        G_TYPE_FROM_CLASS (class),
	                                        G_SIGNAL_RUN_LAST,
	                                        G_STRUCT_OFFSET (IndicateListenerClass, indicator_removed),
	                                        NULL, NULL,
	                                        _indicate_listener_marshal_VOID__POINTER_POINTER_STRING,
	                                        G_TYPE_NONE, 3, INDICATE_TYPE_LISTENER_SERVER, INDICATE_TYPE_LISTENER_INDICATOR, G_TYPE_STRING);
	signals[INDICATOR_MODIFIED] = g_signal_new(INDICATE_LISTENER_SIGNAL_INDICATOR_MODIFIED,
	                                        G_TYPE_FROM_CLASS (class),
	                                        G_SIGNAL_RUN_LAST,
	                                        G_STRUCT_OFFSET (IndicateListenerClass, indicator_modified),
	                                        NULL, NULL,
	                                        _indicate_listener_marshal_VOID__POINTER_POINTER_STRING_STRING,
	                                        G_TYPE_NONE, 4, INDICATE_TYPE_LISTENER_SERVER, INDICATE_TYPE_LISTENER_INDICATOR, G_TYPE_STRING, G_TYPE_STRING);
	signals[SERVER_ADDED] = g_signal_new(INDICATE_LISTENER_SIGNAL_SERVER_ADDED,
	                                        G_TYPE_FROM_CLASS (class),
	                                        G_SIGNAL_RUN_LAST,
	                                        G_STRUCT_OFFSET (IndicateListenerClass, server_added),
	                                        NULL, NULL,
	                                        _indicate_listener_marshal_VOID__POINTER_STRING,
	                                        G_TYPE_NONE, 2, INDICATE_TYPE_LISTENER_SERVER, G_TYPE_STRING);
	signals[SERVER_REMOVED] = g_signal_new(INDICATE_LISTENER_SIGNAL_SERVER_REMOVED,
	                                        G_TYPE_FROM_CLASS (class),
	                                        G_SIGNAL_RUN_LAST,
	                                        G_STRUCT_OFFSET (IndicateListenerClass, server_removed),
	                                        NULL, NULL,
	                                        _indicate_listener_marshal_VOID__POINTER_STRING,
	                                        G_TYPE_NONE, 2, INDICATE_TYPE_LISTENER_SERVER, G_TYPE_STRING);

	dbus_g_object_register_marshaller(_indicate_listener_marshal_VOID__UINT_STRING,
	                                  G_TYPE_NONE,
	                                  G_TYPE_UINT,
	                                  G_TYPE_STRING,
	                                  G_TYPE_INVALID);

	return;
}

static void
indicate_listener_init (IndicateListener * listener)
{
	/* g_debug("Listener Object Initialized"); */
	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);
	GError *error = NULL;

	/* Get the buses */
	priv->session_bus = dbus_g_bus_get(DBUS_BUS_SESSION, &error);
	if (error != NULL) {
		g_error("Unable to get session bus: %s", error->message);
		g_error_free(error);
		return;
	}

	priv->system_bus = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
	if (error != NULL) {
		g_error("Unable to get system bus: %s", error->message);
		g_error_free(error);
		return;
	}

	/* Set up the DBUS service proxies */
	priv->dbus_proxy_session = dbus_g_proxy_new_for_name_owner (priv->session_bus,
	                                                                DBUS_SERVICE_DBUS,
	                                                                DBUS_PATH_DBUS,
	                                                                DBUS_INTERFACE_DBUS,
	                                                                &error);
	if (error != NULL) {
		g_error("Unable to get dbus proxy on session bus: %s", error->message);
		g_error_free(error);
		return;
	}

	priv->dbus_proxy_system = dbus_g_proxy_new_for_name_owner (priv->system_bus,
	                                                               DBUS_SERVICE_DBUS,
	                                                               DBUS_PATH_DBUS,
	                                                               DBUS_INTERFACE_DBUS,
	                                                               &error);
	if (error != NULL) {
		g_error("Unable to get dbus proxy on system bus: %s", error->message);
		g_error_free(error);
		return;
	}

	/* Set up name change signals */
	dbus_g_proxy_add_signal(priv->dbus_proxy_session, "NameOwnerChanged",
	                        G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
	                        G_TYPE_INVALID);
	dbus_g_proxy_connect_signal(priv->dbus_proxy_session, "NameOwnerChanged",
	                            G_CALLBACK(dbus_owner_change), listener, NULL);
	dbus_g_proxy_add_signal(priv->dbus_proxy_system, "NameOwnerChanged",
	                        G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
	                        G_TYPE_INVALID);
	dbus_g_proxy_connect_signal(priv->dbus_proxy_system, "NameOwnerChanged",
	                            G_CALLBACK(dbus_owner_change), listener, NULL);

	/* Initialize Data structures */
	priv->proxies_working = NULL;
	priv->proxies_possible = NULL;

	/* TODO: Look at some common scenarios and find out how to make this sized */
	priv->proxy_todo = g_array_new(FALSE, TRUE, sizeof(proxy_todo_t));
	priv->todo_idle = 0;

	/*            WARNING              */
	/* Starting massive asynchronisity */
	/*                                 */

	/* Build todo list */
	org_freedesktop_DBus_list_names_async (priv->dbus_proxy_session, build_todo_list_cb, listener);
	org_freedesktop_DBus_list_names_async (priv->dbus_proxy_system, build_todo_list_cb, listener);

	return;
}

static void
indicate_listener_finalize (GObject * obj)
{
	IndicateListener * listener = INDICATE_LISTENER(obj);
	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);
	
	if (priv->todo_idle != 0) {
		g_idle_remove_by_data(obj);
	}
	/* Hack: proxy_struct_destroy() lacks a user_data parameter, but since the
	 * caller is responsible for handling params on the stack, it works
	 */
	g_list_foreach(priv->proxies_possible, (GFunc)proxy_struct_destroy, NULL);
	g_list_free(priv->proxies_possible);
	g_list_foreach(priv->proxies_working, (GFunc)proxy_struct_destroy, NULL);
	g_list_free(priv->proxies_working);
	G_OBJECT_CLASS (indicate_listener_parent_class)->finalize (obj);
	return;
}

IndicateListener *
indicate_listener_new (void)
{
	IndicateListener * listener;
	listener = g_object_new(INDICATE_TYPE_LISTENER, NULL);
	return listener;
}

static IndicateListener * default_indicate_listener = NULL;

IndicateListener *
indicate_listener_ref_default (void)
{
	if (default_indicate_listener != NULL) {
		g_object_ref(default_indicate_listener);
	} else {
		default_indicate_listener = g_object_new(INDICATE_TYPE_LISTENER, NULL);
		g_object_add_weak_pointer(G_OBJECT(default_indicate_listener),
		                          (gpointer *)&default_indicate_listener);
	}

	return default_indicate_listener;
}

static void
dbus_owner_change (DBusGProxy * proxy, const gchar * name, const gchar * prev, const gchar * new, IndicateListener * listener)
{
	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);

	DBusGConnection * bus;
	gchar * bus_name;
	if (proxy == priv->dbus_proxy_system) {
		bus = priv->system_bus;
		bus_name = "system";
	} else {
		bus = priv->session_bus;
		bus_name = "session";
	}

	/* g_debug("Name change on %s bus: '%s' from '%s' to '%s'", bus_name, name, prev, new); */

	if (prev != NULL && prev[0] == '\0') {
		todo_list_add(name, proxy, listener, FALSE);
	}
	if (new != NULL && new[0] == '\0') {
		proxy_t searchitem;
		searchitem.connection = bus;
		searchitem.name = (gchar *)name; /* Droping const, not that it isn't, but to remove the warning */

		GList * proxyt_item;
		proxyt_item = g_list_find_custom(priv->proxies_working, &searchitem, proxy_t_equal);
		if (proxyt_item != NULL) {
			proxy_struct_destroy((proxy_t *)proxyt_item->data);
			priv->proxies_working = g_list_remove(priv->proxies_working, proxyt_item->data);
		}
		proxyt_item = g_list_find_custom(priv->proxies_possible, &searchitem, proxy_t_equal);
		if (proxyt_item != NULL) {
			proxy_struct_destroy((proxy_t *)proxyt_item->data);
			priv->proxies_possible = g_list_remove(priv->proxies_possible, proxyt_item->data);
		}
	}

	return;
}

static void
proxy_struct_destroy_indicators (gpointer key, gpointer value, gpointer data)
{
	gchar * type = (gchar *)key;
	GHashTable * indicators = (GHashTable *)value;
	proxy_t * proxy_data = data;

	GList * keys = g_hash_table_get_keys(indicators);
	GList * indicator;
	for (indicator = keys; indicator != NULL; indicator = indicator->next) {
		guint id = (guint)indicator->data;
		g_signal_emit(proxy_data->listener, signals[INDICATOR_REMOVED], 0, &proxy_data->server, GUINT_TO_POINTER(id), type, TRUE);
	}
	g_list_free(keys);

	g_hash_table_remove_all(indicators);
	return;
}

static void
proxy_struct_destroy (gpointer data)
{
	proxy_t * proxy_data = data;

	/* TODO: Clear the indicators by signaling */
	if (proxy_data->indicators != NULL) {
		g_hash_table_foreach(proxy_data->indicators,
							 proxy_struct_destroy_indicators,
							 proxy_data);
		g_hash_table_remove_all(proxy_data->indicators);

		g_signal_emit(proxy_data->listener, signals[SERVER_REMOVED], 0, &proxy_data->server, proxy_data->type, TRUE);
		proxy_data->indicators = NULL;
	}

	if (DBUS_IS_G_PROXY(proxy_data->property_proxy)) {
		g_object_unref(G_OBJECT(proxy_data->property_proxy));
	}

	if (DBUS_IS_G_PROXY(proxy_data->proxy)) {
		g_object_unref(G_OBJECT(proxy_data->proxy));
	}

	g_free(proxy_data->name);
	if (proxy_data->type != NULL) {
		g_free(proxy_data->type);
	}
	g_free(proxy_data);

	return;
}

static void
build_todo_list_cb (DBusGProxy * proxy, char ** names, GError * error, void * data)
{
	IndicateListener * listener = INDICATE_LISTENER(data);

	if (error != NULL) {
		g_warning("Unable to get names: %s", error->message);
		return;
	}

	guint i = 0;
	for (i = 0; names[i] != NULL; i++) {
		todo_list_add(names[i], proxy, listener, TRUE);
	}

	return;
}

static void
todo_list_add (const gchar * name, DBusGProxy * proxy, IndicateListener * listener, gboolean startup)
{
	if (name == NULL || name[0] != ':') {
		return;
	}

	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);

	DBusGConnection * bus;
	gchar * bus_name;
	if (proxy == priv->dbus_proxy_system) {
		bus = priv->system_bus;
		bus_name = "system";
	} else {
		bus = priv->session_bus;
		bus_name = "session";
	}
	/* g_debug ("Adding on %s bus: %s", bus_name, name); */

	proxy_todo_t todo;
	todo.name = g_strdup(name);
	todo.bus  = bus;
	todo.startup = startup;

	g_array_append_val(priv->proxy_todo, todo);

	if (priv->todo_idle == 0) {
		priv->todo_idle = g_idle_add(todo_idle, listener);
	}

	return;
}

gboolean
todo_idle (gpointer data)
{
	IndicateListener * listener = INDICATE_LISTENER(data);
	if (listener == NULL) {
		g_error("Listener got lost in todo_idle");
		return FALSE;
	}

	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);

	if (priv->proxy_todo->len == 0) {
		/* Basically if we have no todo, we need to stop running.  This
		 * is done this way to make the function error handling simpler
		 * and results in an extra run */
		priv->todo_idle = 0;
		return FALSE;
	}

	proxy_todo_t * todo = &g_array_index(priv->proxy_todo, proxy_todo_t, priv->proxy_todo->len - 1);

	proxy_t * proxyt = g_new0(proxy_t, 1);
	proxyt->name = todo->name;
	proxyt->type = NULL;
	proxyt->proxy = dbus_g_proxy_new_for_name(todo->bus,
	                                          proxyt->name,
	                                          "/org/freedesktop/indicate",
	                                          "org.freedesktop.indicator");
	proxyt->property_proxy = NULL;
	proxyt->listener = listener;
	proxyt->indicators = NULL;
	proxyt->connection = todo->bus;
	proxyt->server.name = todo->name;
	proxyt->server.proxy = proxyt->proxy;
	proxyt->server.connection = proxyt->connection;

	priv->proxy_todo = g_array_remove_index(priv->proxy_todo, priv->proxy_todo->len - 1);

	if (proxyt->proxy == NULL) {
		g_warning("Unable to create proxy for %s", proxyt->name);
		return TRUE;
	}

	dbus_g_proxy_add_signal(proxyt->proxy, "ServerShow",
	                        G_TYPE_STRING, G_TYPE_INVALID);
	dbus_g_proxy_connect_signal(proxyt->proxy, "ServerShow",
	                            G_CALLBACK(proxy_server_added), proxyt, NULL);

	priv->proxies_possible = g_list_prepend(priv->proxies_possible, proxyt);

	/* Look through the introspection data to see if this
	   is already a server */
	introspect_this (NULL, NULL, NULL, proxyt);

	return TRUE;
}

static void
get_type_cb (IndicateListener * listener, IndicateListenerServer * server, gchar * type, gpointer data)
{
	if (type == NULL) {
		/* This is usually caused by an error getting the type,
		 * which would mean that this isn't an indicator server */
		return;
	}

	proxy_t * proxyt = (proxy_t *)data;

	proxy_server_added (proxyt->proxy, type, proxyt);
	org_freedesktop_indicator_get_indicator_list_async(proxyt->proxy, proxy_get_indicator_list, proxyt);

	return;
}

typedef struct {
	guint id;
	proxy_t * proxyt;
} indicator_type_t;

static void
proxy_get_indicator_list (DBusGProxy * proxy, GArray * indicators, GError * error, gpointer data)
{
	if (error != NULL) {
		return;
	}

	proxy_t * proxyt = (proxy_t *)data;

	int i;
	for (i = 0; i < indicators->len; i++) {
		indicator_type_t * itt = g_new(indicator_type_t, 1);
		itt->id = g_array_index(indicators, guint, i);
		itt->proxyt = proxyt;

		org_freedesktop_indicator_get_indicator_property_async(proxyt->proxy, itt->id, "type", proxy_get_indicator_type, itt);
	}

	return;
}

static void
proxy_get_indicator_type (DBusGProxy * proxy, gchar * type, GError * error, gpointer data)
{
	if (error != NULL) {
		g_warning("Get Indicator Type returned error: %s", error->message);
		return;
	}

	indicator_type_t * itt = (indicator_type_t *)data;
	guint id = itt->id;
	proxy_t * proxyt = itt->proxyt;

	g_free(itt);

	return proxy_indicator_added(proxy, id, type, proxyt);
}

static void
proxy_server_added (DBusGProxy * proxy, const gchar * type, proxy_t * proxyt)
{
	if (proxyt->indicators == NULL) {
		proxyt->indicators = g_hash_table_new_full(g_str_hash, g_str_equal,
		                                           g_free, proxy_indicators_free);
		/* Elevate to working */
		IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(proxyt->listener);

		GList * proxyt_item;
		proxyt_item = g_list_find_custom(priv->proxies_possible, proxyt, proxy_t_equal);
		if (proxyt_item != NULL) {
			priv->proxies_possible = g_list_remove(priv->proxies_possible, proxyt_item->data);
		}
		priv->proxies_working = g_list_prepend(priv->proxies_working, proxyt);

		dbus_g_proxy_add_signal(proxyt->proxy, "IndicatorAdded",
								G_TYPE_UINT, G_TYPE_STRING, G_TYPE_INVALID);
		dbus_g_proxy_connect_signal(proxyt->proxy, "IndicatorAdded",
									G_CALLBACK(proxy_indicator_added), proxyt, NULL);
		dbus_g_proxy_add_signal(proxyt->proxy, "IndicatorRemoved",
								G_TYPE_UINT, G_TYPE_STRING, G_TYPE_INVALID);
		dbus_g_proxy_connect_signal(proxyt->proxy, "IndicatorRemoved",
									G_CALLBACK(proxy_indicator_removed), proxyt, NULL);
		dbus_g_proxy_add_signal(proxyt->proxy, "IndicatorModified",
								G_TYPE_UINT, G_TYPE_STRING, G_TYPE_INVALID);
		dbus_g_proxy_connect_signal(proxyt->proxy, "IndicatorModified",
									G_CALLBACK(proxy_indicator_modified), proxyt, NULL);

		if (type != NULL) {
			if (proxyt->type != NULL) {
				g_free(proxyt->type);
			}
			proxyt->type = g_strdup(type);
		}

		g_signal_emit(proxyt->listener, signals[SERVER_ADDED], 0, &proxyt->server, proxyt->type, TRUE);
	}

	return;
}

static void
proxy_indicator_added (DBusGProxy * proxy, guint id, const gchar * type, proxy_t * proxyt)
{
	if (proxyt->indicators == NULL) {
		proxy_server_added (proxy, NULL, proxyt);
	}

	GHashTable * indicators = g_hash_table_lookup(proxyt->indicators, type);

	if (indicators == NULL) {
		indicators = g_hash_table_new(g_direct_hash, g_direct_equal);
		g_hash_table_insert(proxyt->indicators, g_strdup(type), indicators);
	}

	if (!g_hash_table_lookup(indicators, (gpointer)id)) {
		g_hash_table_insert(indicators, (gpointer)id, (gpointer)TRUE);
		g_signal_emit(proxyt->listener, signals[INDICATOR_ADDED], 0, &proxyt->server, GUINT_TO_POINTER(id), type, TRUE);
	}

	return;
}

static void
proxy_indicator_removed (DBusGProxy * proxy, guint id, const gchar * type, proxy_t * proxyt)
{
	if (proxyt->indicators == NULL) {
		g_warning("Oddly we had an indicator removed from an interface that we didn't think had indicators.");
		return;
	}

	GHashTable * indicators = g_hash_table_lookup(proxyt->indicators, type);
	if (indicators == NULL) {
		g_warning("Can not remove indicator %d of type '%s' as there are no indicators of that type on %s.", id, type, proxyt->name);
		return;
	}

	if (!g_hash_table_lookup(indicators, (gpointer)id)) {
		g_warning("No indicator %d of type '%s' on '%s'.", id, type, proxyt->name);
		return;
	}

	g_hash_table_remove(indicators, (gpointer)id);
	g_signal_emit(proxyt->listener, signals[INDICATOR_REMOVED], 0, &proxyt->server, GUINT_TO_POINTER(id), type, TRUE);

	return;
}

static void
proxy_indicator_modified (DBusGProxy * proxy, guint id, const gchar * property, proxy_t * proxyt)
{
	if (proxyt->indicators == NULL) {
		g_warning("Oddly we had an indicator modified from an interface that we didn't think had indicators.");
		return;
	}

	GList * keys = g_hash_table_get_keys(proxyt->indicators);
	GList * inc = NULL;
	gchar * type;

	for (inc = g_list_first(keys); inc != NULL; inc = g_list_next(inc)) {
		type = (gchar *)inc->data;

		GHashTable * indicators = g_hash_table_lookup(proxyt->indicators, type);
		if (indicators == NULL) continue; /* no indicators for this type?  Odd, but not an error */

		if (g_hash_table_lookup(indicators, (gpointer)id)) {
			break;
		}
	}

	if (inc == NULL) {
		g_warning("Can not modify indicator %d with property '%s' as there are no indicators with that id on %s.", id, property, proxyt->name);
		return;
	}

	g_signal_emit(proxyt->listener, signals[INDICATOR_MODIFIED], 0, &proxyt->server, GUINT_TO_POINTER(id), type, property, TRUE);

	return;
}

static void
proxy_indicators_free (gpointer data)
{
	GHashTable * table = (GHashTable *)data;

	if (g_hash_table_size(table) != 0) {
		g_warning("Clearning a set of indicators that wasn't signaled!");
	}

	g_hash_table_unref(table);
	return;
}

typedef enum _get_property_type get_property_type;
enum _get_property_type {
	PROPERTY_TYPE_STRING,
	PROPERTY_TYPE_TIME
};

typedef struct _get_property_t get_property_t;
struct _get_property_t {
	GCallback cb;
	gpointer data;
	IndicateListener * listener;
	IndicateListenerServer * server;
	IndicateListenerIndicator * indicator;
	gchar * property;
	get_property_type type;
};

static void
get_property_cb (DBusGProxy *proxy, char * OUT_value, GError *error, gpointer userdata)
{
	get_property_t * get_property_data = (get_property_t *)userdata;

	if (error != NULL) {
		g_warning("Unable to get property data: %s", error->message);
		g_error_free(error);
		return;
	}

	switch (get_property_data->type) {
	case PROPERTY_TYPE_STRING: {
		indicate_listener_get_property_cb cb = (indicate_listener_get_property_cb)get_property_data->cb;
		cb(get_property_data->listener, get_property_data->server, get_property_data->indicator, get_property_data->property, OUT_value, get_property_data->data);
		break;
	}
	case PROPERTY_TYPE_TIME: {
		indicate_listener_get_property_time_cb cb = (indicate_listener_get_property_time_cb)get_property_data->cb;
		GTimeVal time;
		if (g_time_val_from_iso8601(OUT_value, &time)) {
			cb(get_property_data->listener, get_property_data->server, get_property_data->indicator, get_property_data->property, &time, get_property_data->data);
		}
		break;
	}
	}

	g_free(get_property_data->property);
	g_free(get_property_data);

	return;
};

static void
get_property_helper (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, GCallback callback, gpointer data, get_property_type prop_type)
{
	/* g_debug("get_property_helper: %s %d", property, prop_type); */
	/* TODO: Do we need to somehow refcount the server/indicator while we're waiting on this? */
	get_property_t * get_property_data = g_new(get_property_t, 1);
	get_property_data->cb = callback;
	get_property_data->data = data;
	get_property_data->listener = listener;
	get_property_data->server = server;
	get_property_data->indicator = indicator;
	get_property_data->property = g_strdup(property);
	get_property_data->type = prop_type;
	
	org_freedesktop_indicator_get_indicator_property_async (server->proxy , INDICATE_LISTENER_INDICATOR_ID(indicator), property, get_property_cb, get_property_data);
	return;
}

void
indicate_listener_get_property (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, indicate_listener_get_property_cb callback, gpointer data)
{
	return get_property_helper(listener, server, indicator, property, G_CALLBACK(callback), data, PROPERTY_TYPE_STRING);
}

void
indicate_listener_get_property_time (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, indicate_listener_get_property_time_cb callback, gpointer data)
{
	return get_property_helper(listener, server, indicator, property, G_CALLBACK(callback), data, PROPERTY_TYPE_TIME);
}

gboolean
_indicate_listener_get_indicator_servers (IndicateListener * listener, GList * servers)
{

	return TRUE;
}

static void 
listener_display_cb (DBusGProxy *proxy, GError *error, gpointer userdata)
{
	if (error != NULL) {
		g_warning("Listener display caused an error: %s", error->message);
	}
	return;
}

void
indicate_listener_display (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator)
{
	org_freedesktop_indicator_show_indicator_to_user_async (server->proxy, INDICATE_LISTENER_INDICATOR_ID(indicator), listener_display_cb, NULL);

	return;
}

typedef struct {
	IndicateListener * listener;
	IndicateListenerServer * server;
	indicate_listener_get_server_property_cb cb;
	gpointer data;
} property_cb_t;

static void
property_cb (DBusGProxy * proxy, DBusGProxyCall * call, void * data)
{
	/* g_debug("Callback for property %s %s %s", dbus_g_proxy_get_bus_name(proxy), dbus_g_proxy_get_path(proxy), dbus_g_proxy_get_interface(proxy)); */
	property_cb_t * propertyt = data;
	GError * error = NULL;

	GValue property = {0};

	dbus_g_proxy_end_call(proxy, call, &error, G_TYPE_VALUE, &property, G_TYPE_INVALID);
	if (error != NULL) {
		/* g_warning("Unable to get property: %s", error->message); */
		g_error_free(error);
		g_free(propertyt);
		return;
	}

	if (!G_VALUE_HOLDS_STRING(&property)) {
		g_warning("Property returned is not a string!");
		g_free(propertyt);
		return;
	}

	IndicateListener * listener = propertyt->listener;
	IndicateListenerServer * server = propertyt->server;
	indicate_listener_get_server_property_cb cb = propertyt->cb;
	gpointer cb_data = propertyt->data;

	g_free(propertyt);

	gchar * propstr = g_value_dup_string(&property);

	/* g_debug("\tProperty value: %s", propstr); */

	return cb(listener, server, propstr, cb_data);
}

static void
get_server_property (IndicateListener * listener, IndicateListenerServer * server, indicate_listener_get_server_property_cb callback, const gchar * property_name, gpointer data)
{
	/* g_debug("Setting up callback for property %s on %s", property_name, INDICATE_LISTENER_SERVER_DBUS_NAME(server)); */
	IndicateListenerPrivate * priv = INDICATE_LISTENER_GET_PRIVATE(listener);

	proxy_t searchitem;
	searchitem.name = server->name;
	searchitem.connection = server->connection;

	GList * proxyitem = g_list_find_custom(priv->proxies_possible, &searchitem, proxy_t_equal);
	if (proxyitem == NULL) {
		proxyitem = g_list_find_custom(priv->proxies_working, &searchitem, proxy_t_equal);
	}

	if (proxyitem == NULL) {
		g_warning("Can not find a proxy for the server at all.");
		return;
	}

	proxy_t * proxyt = (proxy_t *)proxyitem->data;

	if (proxyt->property_proxy == NULL) {
		proxyt->property_proxy = dbus_g_proxy_new_for_name(proxyt->connection,
		                                                   proxyt->name,
		                                                   "/org/freedesktop/indicate",
		                                                   DBUS_INTERFACE_PROPERTIES);
	}

	property_cb_t * localdata = g_new(property_cb_t, 1);
	localdata->listener = listener;
	localdata->server = server;
	localdata->cb = callback;
	localdata->data = data;

	dbus_g_proxy_begin_call (proxyt->property_proxy,
	                         "Get",
	                         property_cb,
	                         localdata,
	                         NULL,
	                         G_TYPE_STRING, "org.freedesktop.indicator",
	                         G_TYPE_STRING, property_name,
	                         G_TYPE_INVALID, G_TYPE_VALUE, G_TYPE_INVALID);

	return;
}

void
indicate_listener_server_get_type (IndicateListener * listener, IndicateListenerServer * server, indicate_listener_get_server_property_cb callback, gpointer data)
{
	return get_server_property(listener, server, callback, "type", data);
}

void
indicate_listener_server_get_desktop (IndicateListener * listener, IndicateListenerServer * server, indicate_listener_get_server_property_cb callback, gpointer data)
{
	return get_server_property(listener, server, callback, "desktop", data);
}

const gchar *
indicate_listener_server_get_dbusname (IndicateListenerServer * server)
{
	if (server == NULL) return NULL;
	return server->name;
}

guint
indicate_listener_indicator_get_id (IndicateListenerIndicator * indicator)
{
	return GPOINTER_TO_UINT(indicator);
}

static const gchar *
interest_to_string (IndicateInterests interest)
{
	switch (interest) {
	case INDICATE_INTEREST_SERVER_DISPLAY:
		return INDICATE_INTEREST_STRING_SERVER_DISPLAY;
	case INDICATE_INTEREST_SERVER_SIGNAL:
		return INDICATE_INTEREST_STRING_SERVER_SIGNAL;
	case INDICATE_INTEREST_INDICATOR_DISPLAY:
		return INDICATE_INTEREST_STRING_INDICATOR_DISPLAY;
	case INDICATE_INTEREST_INDICATOR_SIGNAL:
		return INDICATE_INTEREST_STRING_INDICATOR_SIGNAL;
	case INDICATE_INTEREST_INDICATOR_COUNT:
		return INDICATE_INTEREST_STRING_INDICATOR_COUNT;
	default:
		return "";
	}
}

static void
interest_cb (DBusGProxy *proxy, GError *error, gpointer userdata)
{
	if (error != NULL) {
		g_warning("Unable to configure interest.");
	}

	return;
}

void
indicate_listener_server_show_interest (IndicateListener * listener, IndicateListenerServer * server, IndicateInterests interest)
{
	if (!(interest > INDICATE_INTEREST_NONE && interest < INDICATE_INTEREST_LAST)) {
		return;
	}

	if (!server->interests[interest]) {
		org_freedesktop_indicator_show_interest_async (server->proxy, interest_to_string(interest), interest_cb, server);
		server->interests[interest] = TRUE;
	}
	return;
}

void
indicate_listener_server_remove_interest (IndicateListener * listener, IndicateListenerServer * server, IndicateInterests interest)
{
	if (server->interests[interest]) {
		org_freedesktop_indicator_remove_interest_async (server->proxy, interest_to_string(interest), interest_cb, server);
		server->interests[interest] = FALSE;
	}
	return;
}

gboolean
indicate_listener_server_check_interest (IndicateListener * listener, IndicateListenerServer * server, IndicateInterests interest)
{
	return server->interests[interest];
}

GType
indicate_listener_server_get_gtype (void)
{
  static GType our_type = 0;
  
  if (our_type == 0)
    our_type = g_pointer_type_register_static ("IndicateListenerServer");

  return our_type;
}

GType
indicate_listener_indicator_get_gtype (void)
{
  static GType our_type = 0;
  
  if (our_type == 0)
    our_type = g_pointer_type_register_static ("IndicateListenerIndicator");

  return our_type;
}

static const gchar * _introspector_path[] = {"", "org", "freedesktop", "indicate", NULL};
static const gchar * _introspector_fullpath[] = {"/", "/org", "/org/freedesktop", "/org/freedesktop/indicate", NULL};
static const gchar * _introspector_interface = "org.freedesktop.indicator";

static void
introspect_this (DBusGProxy * proxy, char * OUT_data, GError * error, gpointer data)
{
	/* g_debug("Introspect this:\n%s", OUT_data); */
	proxy_t * server = (proxy_t *)data;
	if (proxy != NULL) {
		g_object_unref(proxy);
	}
	if (error != NULL) {
		/* We probably couldn't introspect that far up.  That's
		   life, it happens. Or there's a timeout, that happens
		   too, I guess some apps are too busy for us. */
		/* g_debug("Introspection error on %s object %s: %s", server->name, _introspector_fullpath[server->introspect_level], error->message); */
		return;
	}

	if (OUT_data != NULL) {
		xmlDocPtr xmldoc;
		/* Parse the XML */
		xmldoc = xmlReadMemory(OUT_data, g_utf8_strlen(OUT_data, 16*1024), "introspection.xml", NULL, 0);

		/* Check for root being "node" */
		xmlNodePtr root = xmlDocGetRootElement(xmldoc);
		if (g_strcmp0(root->name, "node") != 0) {
			xmlFreeDoc(xmldoc);
			g_warning("Introspection data from %s is not valid: %s", server->name, OUT_data);
			return;
		}

		server->introspect_level += 1;
		const gchar * nodename = NULL;
		const gchar * nameval = NULL;
		if (_introspector_path[server->introspect_level] == NULL) {
			/* We're looking for our interface */
			nodename = "interface";
			nameval = _introspector_interface;
		} else {
			/* We're looking for our next node */
			nodename = "node";
			nameval = _introspector_path[server->introspect_level];
		}

		gboolean found = FALSE;
		xmlNodePtr children;
		for (children = root->children; children != NULL; children = children->next) {
			gchar * xmlnameval = NULL;
			if (g_strcmp0(children->name, nodename) == 0) {
				xmlAttrPtr attrib;
				for (attrib = children->properties; attrib != NULL; attrib = attrib->next) {
					if (g_strcmp0(attrib->name, "name") == 0) {
						if (attrib->children != NULL) {
							xmlnameval = attrib->children->content;
						}
						break;
					}
				}

				if (!g_strcmp0(nameval, xmlnameval)) {
					found = TRUE;
					break;
				}
			}
		}

		xmlFreeDoc(xmldoc);

		if (!found) {
			/* Ah, nothing we're interested in */
			return;
		}

		if (_introspector_path[server->introspect_level] == NULL) {
			/* If we've found the interface at the end of the tree, whoo! hoo! */
			/* Now we know it's safe to get the type on it */
			indicate_listener_server_get_type(server->listener, &server->server, get_type_cb, server);
			return;
		}
	} else {
		server->introspect_level = 0;
	}

	DBusGProxy * newproxy = dbus_g_proxy_new_for_name(server->connection,
	                                                  server->name,
	                                                  _introspector_fullpath[server->introspect_level],
	                                                  DBUS_INTERFACE_INTROSPECTABLE);

	org_freedesktop_DBus_Introspectable_introspect_async(newproxy, introspect_this, server);

	return;
}
