/*
 * Copyright (C) 2011-2014 Gary Kramlich <grim@reaperworld.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <string.h>

#include <glib.h>
#include <glib/gi18n.h>

#include <gplugin/gplugin-manager.h>
#include <gplugin/gplugin-core.h>

#include <gplugin/gplugin-private.h>

#include <gplugin/gplugin-native.h>

#include <gplugin/gplugin-file-tree.h>

/**
 * SECTION:gplugin-manager
 * @Title: Manager API
 * @Short_description: API for managing plugins
 *
 * The manager is used to manager all plugins in GPlugin.  This includes
 * loading, unloading, querying, checking for new plugins, and so on.
 */

/******************************************************************************
 * Enums
 *****************************************************************************/
enum {
	SIG_LOADING,
	SIG_LOADED,
	SIG_LOAD_FAILED,
	SIG_UNLOADING,
	SIG_UNLOADED,
	N_SIGNALS,
};

/******************************************************************************
 * Structs
 *****************************************************************************/
typedef struct {
	GObject parent;

	GQueue *paths;
	GHashTable *plugins;
	GHashTable *plugins_filename_view;

	GHashTable *loaders;

	gboolean refresh_needed;
} GPluginManager;

typedef struct {
	GObjectClass parent;

	void (*append_path)(GPluginManager *manager, const gchar *path);
	void (*prepend_path)(GPluginManager *manager, const gchar *path);
	void (*remove_path)(GPluginManager *manager, const gchar *path);
	void (*remove_paths)(GPluginManager *manager);

	GList *(*get_paths)(GPluginManager *manager);

	void (*register_loader)(GPluginManager *manager, GType type);
	void (*unregister_loader)(GPluginManager *manager, GType type);

	void (*refresh)(GPluginManager *manager);

	GSList *(*find_plugins)(GPluginManager *manager, const gchar *id);

	GList *(*list_plugins)(GPluginManager *manager);

	GSList *(*get_plugin_dependencies)(GPluginManager *manager, GPluginPlugin *plugin, GError **error);

	gboolean (*load_plugin)(GPluginManager *manager,
	                        GPluginPlugin *plugin,
	                        GError **error);
	gboolean (*unload_plugin)(GPluginManager *manager,
	                          GPluginPlugin *plugin,
	                          GError **error);

	/* signals */
	gboolean (*loading_plugin)(GObject *manager,
	                           GPluginPlugin *plugin,
	                           GError **error);
	void (*loaded_plugin)(GObject *manager,
	                      GPluginPlugin *plugin);
	void (*load_failed)(GObject *manager, GPluginPlugin *plugin);
	gboolean (*unloading_plugin)(GObject *manager,
	                             GPluginPlugin *plugin,
	                             GError **error);
	void (*unloaded_plugin)(GObject *manager,
	                        GPluginPlugin *plugin);

} GPluginManagerClass;

#define GPLUGIN_TYPE_MANAGER            (gplugin_manager_get_type())
#define GPLUGIN_MANAGER(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GPLUGIN_TYPE_MANAGER, GPluginManager))
#define GPLUGIN_MANAGER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), GPLUGIN_TYPE_MANAGER, GPluginManagerClass))
#define GPLUGIN_IS_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GPLUGIN_TYPE_MANAGER))
#define GPLUGIN_IS_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GPLUGIN_TYPE_MANAGER))
#define GPLUGIN_MANAGER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GPLUGIN_TYPE_MANAGER, GPluginManagerClass))

#define GPLUGIN_MANAGER_INSTANCE        (GPLUGIN_MANAGER(gplugin_manager_get_instance()))

G_DEFINE_TYPE(GPluginManager, gplugin_manager, G_TYPE_OBJECT);

/******************************************************************************
 * Globals
 *****************************************************************************/
GPluginManager *instance = NULL;
static guint signals[N_SIGNALS] = {0, };
const gchar *dependency_pattern = "^(?P<id>.+?)((?P<op>\\<=|\\<|==|=|\\>=|\\>)(?P<version>.+))?$";
GRegex *dependency_regex = NULL;

/******************************************************************************
 * Helpers
 *****************************************************************************/
static guint
gplugin_manager_str_hash(gconstpointer v) {
	if(v == NULL)
		return g_str_hash("");

	return g_str_hash(v);
}

static gboolean
gplugin_manager_remove_list_value(G_GNUC_UNUSED gpointer k,
                                  gpointer v,
                                  G_GNUC_UNUSED gpointer d)
{
	GSList *l = NULL;

	for(l = (GSList *)v; l; l = l->next) {
		if(l->data && G_IS_OBJECT(l->data))
			g_object_unref(G_OBJECT(l->data));
	}

	g_slist_free((GSList *)v);

	return TRUE;
}

/******************************************************************************
 * Manager implementation
 *****************************************************************************/
static void
gplugin_manager_real_append_path(GPluginManager *manager,
                                 const gchar *path)
{
	GList *l = NULL;

	if(!path)
		return;

	for(l = manager->paths->head; l; l = l->next)
		if(strcmp(l->data, path) == 0)
			return;

	g_queue_push_tail(manager->paths, g_strdup(path));
}

static void
gplugin_manager_real_prepend_path(GPluginManager *manager,
                                  const gchar *path)
{
	GList *l = NULL;

	if(!path)
		return;

	for(l = manager->paths->head; l; l = l->next)
		if(strcmp(l->data, path) == 0)
			return;

	g_queue_push_head(manager->paths, g_strdup(path));
}

static void
gplugin_manager_real_remove_path(GPluginManager *manager,
                                 const gchar *path)
{
	GList *l = NULL, *link = NULL;

	g_return_if_fail(path != NULL);

	for(l = manager->paths->head; l; l = l->next)
		if(strcmp(l->data, path) == 0) {
			g_free(l->data);
			link = l;
		}

	if(link)
		g_queue_delete_link(manager->paths, link);
}

static void
gplugin_manager_real_remove_paths(GPluginManager *manager) {
	g_queue_clear(manager->paths);
}

static GList *
gplugin_manager_real_get_paths(GPluginManager *manager) {
	return manager->paths->head;
}

static void
gplugin_manager_real_register_loader(GPluginManager *manager,
                                     GType type)
{
	GPluginLoader *loader = NULL;
	GPluginLoaderClass *lo_class = NULL;
	GSList *l = NULL;

	g_return_if_fail(g_type_is_a(type, GPLUGIN_TYPE_LOADER));

	/* Create the loader instance first.  If we can't create it, we bail */
	loader = g_object_new(type, NULL);
	if(!GPLUGIN_IS_LOADER(loader)) {
		g_warning(_("failed to create loader instance for %s"),
		          g_type_name(type));

		return;
	}

	/* grab the class of the loader */
	lo_class = GPLUGIN_LOADER_GET_CLASS(loader);
	if(!lo_class) {
		g_warning(_("failed to get the loader class for %s"), g_type_name(type));
		g_object_unref(G_OBJECT(loader));

		return;
	}

	for(l = gplugin_loader_class_get_supported_extensions(lo_class);
	    l; l = l->next)
	{
		GSList *existing = NULL, *ll = NULL;
		const gchar *ext = (const gchar *)l->data;

		/* grab any existing loaders that are registered for this type so that
		 * we can prepend our loader.  But before we add ours, we remove any
		 * old copies we might have of ours.
		 */
		existing = g_hash_table_lookup(manager->loaders, ext);
		for(ll = existing; ll; ll = ll->next) {
			if(G_OBJECT_TYPE(ll->data) == type) {
				GPluginLoader *old = GPLUGIN_LOADER(ll->data);

				existing = g_slist_remove(existing, old);

				g_object_unref(G_OBJECT(old));

				break;
			}
		}

		existing = g_slist_prepend(existing, g_object_ref(G_OBJECT(loader)));

		/* Now insert the updated slist back into the hash table */
		g_hash_table_insert(manager->loaders, g_strdup(ext), existing);
	}

	/* make a note that we need to refresh */
	manager->refresh_needed = TRUE;

	/* we remove our initial reference from the loader now to avoid a leak */
	g_object_unref(G_OBJECT(loader));
}

static void
gplugin_manager_real_unregister_loader(GPluginManager *manager,
                                       GType type)
{
	GPluginLoaderClass *klass = NULL;
	GSList *exts = NULL;

	g_return_if_fail(g_type_is_a(type, GPLUGIN_TYPE_LOADER));

	klass = g_type_class_ref(type);
	if(!klass)
		return;

	for(exts = gplugin_loader_class_get_supported_extensions(klass);
	    exts; exts = exts->next) {
		GSList *los = NULL;
		GSList *l = NULL;
		const gchar *ext = NULL;

		ext = (const gchar *)exts->data;
		los = g_hash_table_lookup(manager->loaders, ext);

		for(l = los; l; l = l->next) {
			GPluginLoader *lo = GPLUGIN_LOADER(l->data);

			/* check if this is not the loader we're looking for */
			if(G_OBJECT_TYPE(lo) != type)
				continue;

			/* at this point, the loader we're at is of the type we're
			 * removing.  So we'll remove it from the los SList.  Then if the
			 * SList is empty, we remove it from the hash table, otherwise we
			 * just update it.
			 */
			los = g_slist_remove(los, lo);
			if(los)
				g_hash_table_insert(manager->loaders, g_strdup(ext), los);
			else
				g_hash_table_remove(manager->loaders, ext);

			/* kill our ref to the loader */
			g_object_unref(G_OBJECT(lo));

			/* now move to the next extension to check */
			break;
		}
	}

	g_type_class_unref(klass);
}

static void
gplugin_manager_real_refresh(GPluginManager *manager) {
	GNode *root = NULL;
	GList *error_messages = NULL, *l = NULL;
	gchar *error_message = NULL;
	guint errors = 0;

	/* build a tree of all possible plugins */
	root = gplugin_file_tree_new(manager->paths->head);

	manager->refresh_needed = TRUE;

	while(manager->refresh_needed) {
		GNode *dir = NULL;

		if(error_messages) {
			for(l = error_messages; l; l = l->next)
				g_free(l->data);
			g_list_free(error_messages);
			error_messages = NULL;
		}

		manager->refresh_needed = FALSE;

		for(dir = root->children; dir; dir = dir->next) {
			GPluginFileTreeEntry *e = dir->data;
			GNode *file = NULL;
			const gchar *path = e->filename;

			for(file = dir->children; file; file = file->next) {
				GPluginPlugin *plugin = NULL;
				GPluginLoader *loader = NULL;
				GError *error = NULL;
				GSList *l = NULL;
				gchar *filename = NULL;

				e = (GPluginFileTreeEntry *)file->data;

				/* Build the path and see if we need to probe it! */
				filename = g_build_filename(path, e->filename, NULL);
				plugin = g_hash_table_lookup(manager->plugins_filename_view, filename);

				if(plugin && GPLUGIN_IS_PLUGIN(plugin)) {
					GPluginPluginState state =
						gplugin_plugin_get_state(plugin);

					/* The plugin is in our "view", check it's state.  If it's
					 * queried or loaded, move on to the next one.
					 */
					if(state == GPLUGIN_PLUGIN_STATE_QUERIED ||
					   state == GPLUGIN_PLUGIN_STATE_LOADED)
					{
						g_free(filename);
						continue;
					}
				}

				/* grab the list of loaders for this extension */
				for(l = g_hash_table_lookup(manager->loaders, e->extension); l;
				    l = l->next)
				{
					if(!l->data)
						continue;

					loader = GPLUGIN_LOADER(l->data);
					if(!GPLUGIN_IS_LOADER(loader)) {
						loader = NULL;
						continue;
					}

					/* Try to probe the plugin with the current loader */
					plugin = gplugin_loader_query_plugin(loader,
					                                     filename,
				                                         &error);

					/* Check the GError, if it's set, output it's message and
					 * try the next loader.
					 */
					if(plugin == NULL || error) {
						errors++;

						error_message =
							g_strdup_printf(_("failed to query '%s' with " \
							                  "loader '%s': %s"), filename,
							                  G_OBJECT_TYPE_NAME(loader),
							                  (error) ? error->message : _("Unknown"));
						error_messages = g_list_prepend(error_messages, error_message);

						if(error)
							g_error_free(error);

						error = NULL;

						loader = NULL;

						continue;
					}

					/* if the plugin instance is good, then break out of this
					 * loop.
					 */
					if(GPLUGIN_IS_PLUGIN(plugin)) {
						break;
					}

					g_object_unref(G_OBJECT(plugin));

					loader = NULL;
				}

				/* check if our plugin instance is good.  If it's not good we
				 * don't need to do anything but free the filename which we'll
				 * do later.
				 */
				if(GPLUGIN_IS_PLUGIN(plugin)) {
					/* we have a good plugin, huzzah!  We need to add it to our
					 * "view" as well as the main plugin hash table.
					 */

					/* we want the internal filename from the plugin to avoid
					 * duplicate memory, so we need to grab it for the "view".
					 */
					gchar *real_filename = gplugin_plugin_get_filename(plugin);

					/* we also need the GPluginPluginInfo to get the plugin's
					 * ID for the key in our main hash table.
					 */
					GPluginPluginInfo *info = gplugin_plugin_get_info(plugin);

					const gchar *id = gplugin_plugin_info_get_id(info);
					GSList *l = NULL, *ll = NULL;
					gboolean seen = FALSE;

					/* throw a warning if the info->id is NULL */
					if(id == NULL) {
						error_message =
							g_strdup_printf(_("Plugin %s has a NULL id."),
							                real_filename);
						error_messages = g_list_prepend(error_messages, error_message);
					}

					/* now insert into our view */
					g_hash_table_replace(manager->plugins_filename_view,
					                     real_filename,
					                     g_object_ref(G_OBJECT(plugin)));

					/* Grab the list of plugins with our id and prepend the new
					 * plugin to it before updating it.
					 */
					l = g_hash_table_lookup(manager->plugins, id); //-V1004
					for(ll = l; ll; ll = ll->next) {
						GPluginPlugin *splugin = GPLUGIN_PLUGIN(ll->data);
						gchar *sfilename = gplugin_plugin_get_filename(splugin);

						if(!g_strcmp0(real_filename, sfilename))
							seen = TRUE;

						g_free(sfilename);
					}
					if(!seen) {
						l = g_slist_prepend(l, g_object_ref(plugin));
						g_hash_table_insert(manager->plugins, g_strdup(id), l);
					}

					/* check if the plugin is supposed to be loaded on query,
					 * and if so, load it.
					 */
					if(gplugin_plugin_info_get_load_on_query(info)) {
						GError *error = NULL;
						gboolean loaded;

						loaded = gplugin_loader_load_plugin(loader,
						                                    plugin,
						                                    &error);

						if(!loaded) {
							error_message =
								g_strdup_printf(_("failed to load %s during query: %s"),
								                filename,
								                (error) ? error->message : _("Unknown"));
							error_messages = g_list_prepend(error_messages, error_message);

							errors++;

							g_error_free(error);
						} else {
							gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOADED);
						}
					} else {
						/* finally set the plugin state queried */
						gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_QUERIED);

						/* if errors is greater than 0 set
						 * manager->refresh_needed to TRUE.
						 */
						if(errors > 0) {
							errors = 0;
							manager->refresh_needed = TRUE;
						}
					}

					g_object_unref(G_OBJECT(info));

				}

				g_free(filename);
			}
		}
	}

	if(error_messages) {
		error_messages = g_list_reverse(error_messages);
		for(l = error_messages; l; l = l->next) {
			g_warning("%s", (gchar *)l->data);
			g_free(l->data);
		}

		g_list_free(error_messages);
	}

	/* free the file tree */
	gplugin_file_tree_free(root);
}

static GSList *
gplugin_manager_real_find_plugins(GPluginManager *manager,
                                  const gchar *id)
{
	GSList *plugins_list = NULL, *l;

	g_return_val_if_fail(id != NULL, NULL);

	for(l = g_hash_table_lookup(manager->plugins, id); l; l = l->next) {
		GPluginPlugin *plugin = NULL;

		if(l->data == NULL)
			continue;

		plugin = GPLUGIN_PLUGIN(l->data);

		plugins_list = g_slist_prepend(plugins_list,
		                               g_object_ref(G_OBJECT(plugin)));
	}

	return plugins_list;
}

static GList *
gplugin_manager_real_list_plugins(GPluginManager *manager) {
	GQueue *queue = g_queue_new();
	GList *ret = NULL;
	GHashTableIter iter;
	gpointer key = NULL;

	g_hash_table_iter_init(&iter, manager->plugins);
	while(g_hash_table_iter_next(&iter, &key, NULL)) {
		g_queue_push_tail(queue, (gchar *)key);
	}

	ret = g_list_copy(queue->head);

	g_queue_free(queue);

	return ret;
}

static gboolean
gplugin_manager_load_dependencies(GPluginPlugin *plugin,
                                  GPluginPluginInfo *info,
                                  GError **error)
{
	const gchar * const *dependencies = NULL;
	gint i = 0;

	/* now walk through any dependencies the plugin has and load them.  If they
	 * fail to load we need to fail as well.
	 */
	dependencies = gplugin_plugin_info_get_dependencies(info);
	if(dependencies != NULL) {
		gboolean all_found = TRUE;

		for(i = 0; dependencies[i]; i++) {
			gboolean found = FALSE;
			gchar **ors = NULL;
			gint o = 0;

			ors = g_strsplit(dependencies[i], "|", 0);
			for(o = 0; ors[o]; o++) {
				GMatchInfo *match = NULL;
				GSList *matches = NULL, *m = NULL;
				gchar *oid = NULL, *oop = NULL, *over = NULL;

				if(!g_regex_match(dependency_regex, ors[o], 0, &match)) {
					continue;
				}

				/* grab the or'd id, op, and version */
				oid = g_match_info_fetch_named(match, "id");
				oop = g_match_info_fetch_named(match, "op");
				over = g_match_info_fetch_named(match, "version");

				/* free the match info */
				g_match_info_free(match);

				/* now look for a plugin matching the id */
				matches = gplugin_manager_find_plugins(oid);
				if(matches == NULL) {
					g_free(oid);
					g_free(oop);
					g_free(over);
					continue;
				}

				/* now iterate the matches and check if we need to check their
				 * version.
				 */
				for(m = matches; m; m = m->next) {
					GPluginPlugin *dplugin = GPLUGIN_PLUGIN(g_object_ref(G_OBJECT(m->data)));
					gboolean ret = FALSE;

					if(oop && over) {
						/* we need to check the version, so grab the info to
						 * get the version and check it.
						 */
						GPluginPluginInfo *dinfo = NULL;
						const gchar *dver = NULL;
						gboolean satisfied = FALSE;
						gint res = 0;

						dinfo = gplugin_plugin_get_info(dplugin);
						dver = gplugin_plugin_info_get_version(dinfo);

						res = gplugin_version_compare(dver, over, error);
						g_object_unref(G_OBJECT(dinfo));

						if(res < 0) {
							/* dver is greather than over */
							if(g_strcmp0(oop, ">") == 0)
								satisfied = TRUE;
						} else if(res == 0) {
							/* dver is equal to over */
							if(g_strcmp0(oop, ">=") == 0 ||
							   g_strcmp0(oop, "<=") == 0 ||
							   g_strcmp0(oop, "=") == 0 ||
							   g_strcmp0(oop, "==") == 0)
							{
								satisfied = TRUE;
							}
						} else if(res > 0) {
							if(g_strcmp0(oop, "<") == 0)
								satisfied = TRUE;
						}

						if(satisfied)
							found = TRUE;
					}

					ret = gplugin_manager_load_plugin(dplugin, error);

# warning need to figure out dependencies
//					gplugin_plugin_add_dependent_plugin(dplugin, plugin);

					g_object_unref(G_OBJECT(dplugin));

					if(ret) {
						found = TRUE;
						break;
					}
				}

				g_free(oid);
				g_free(oop);
				g_free(over);

				if(found)
					break;
			}
			g_strfreev(ors);

			if(!found)
				all_found = FALSE;
		}

		if(!all_found) {
			return FALSE;
		}
	}

	return TRUE;
}

static GSList *
gplugin_manager_real_get_plugin_dependencies(G_GNUC_UNUSED GPluginManager *manager,
                                             GPluginPlugin *plugin,
                                             GError **error)
{
	GPluginPluginInfo *info = NULL;
	GSList *ret = NULL;
	const gchar * const *dependencies = NULL;
	gint idx = 0;

	info = gplugin_plugin_get_info(plugin);
	dependencies = gplugin_plugin_info_get_dependencies(info);

	if(dependencies == NULL) {
		return NULL;
	}

	for(idx = 0; dependencies[idx] != NULL; idx++) {
		GPluginPlugin *dep = gplugin_manager_find_plugin(dependencies[idx]);

		if(!GPLUGIN_IS_PLUGIN(dep)) {
			if(error) {
				*error = g_error_new(
					GPLUGIN_DOMAIN,
					0,
					"failed to find plugin dependency '%s'",
					dependencies[idx]
				);

				g_slist_free_full(ret, g_object_unref);

				return NULL;
			}
		}

		ret = g_slist_prepend(ret, dep);
	}

	return ret;
}

static gboolean
gplugin_manager_real_load_plugin(GPluginManager *manager,
                                 GPluginPlugin *plugin,
                                 GError **error)
{
	GPluginPluginInfo *info = NULL;
	GPluginLoader *loader = NULL;
	gboolean ret = TRUE;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	/* if the plugin is already loaded there's nothing for us to do */
	if(gplugin_plugin_get_state(plugin) == GPLUGIN_PLUGIN_STATE_LOADED)
		return TRUE;

	/* now try to get the plugin info from the plugin */
	info = gplugin_plugin_get_info(plugin);
	if(info == NULL) {
		g_set_error(error, GPLUGIN_DOMAIN, 0,
		            _("Plugin %s did not return value plugin info"),
		            gplugin_plugin_get_filename(plugin));

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		return FALSE;
	}

	if(!gplugin_manager_load_dependencies(plugin, info, error)) {
		g_object_unref(G_OBJECT(info));

		return FALSE;
	}

	g_object_unref(G_OBJECT(info));

	/* now load the actual plugin */
	loader = gplugin_plugin_get_loader(plugin);

	if(!GPLUGIN_IS_LOADER(loader)) {
		g_set_error(error, GPLUGIN_DOMAIN, 0,
		            _("The loader for %s is not a loader.  This "
		              "should not happened!"),
		            gplugin_plugin_get_filename(plugin));

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		return FALSE;
	}

	g_signal_emit(manager, signals[SIG_LOADING], 0, plugin, error, &ret);
	if(!ret) {
		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		return ret;
	}

	ret = gplugin_loader_load_plugin(loader, plugin, error);
	gplugin_plugin_set_state(plugin, (ret) ? GPLUGIN_PLUGIN_STATE_LOADED :
	                                         GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

	if(ret)
		g_signal_emit(manager, signals[SIG_LOADED], 0, plugin);
	else
		g_signal_emit(manager, signals[SIG_LOAD_FAILED], 0, plugin);

	return ret;
}

static gboolean
gplugin_manager_real_unload_plugin(GPluginManager *manager,
                                   GPluginPlugin *plugin,
                                   GError **error)
{
	GPluginLoader *loader = NULL;
	gboolean ret = TRUE;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	if(gplugin_plugin_get_state(plugin) != GPLUGIN_PLUGIN_STATE_LOADED)
		return TRUE;

	loader = gplugin_plugin_get_loader(plugin);
	if(!GPLUGIN_IS_LOADER(loader)) {
		g_set_error_literal(error, GPLUGIN_DOMAIN, 0,
		                    _("Plugin loader is not a loader"));

		return FALSE;
	}

	g_signal_emit(manager, signals[SIG_UNLOADING], 0, plugin, error, &ret);
	if(!ret)
		return ret;

	ret = gplugin_loader_unload_plugin(loader, plugin, error);
	if(ret) {
		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_QUERIED);

		g_signal_emit(manager, signals[SIG_UNLOADED], 0, plugin, error);
	}

	return ret;
}

static gboolean
gplugin_manager_loading_cb(G_GNUC_UNUSED GObject *manager,
                           G_GNUC_UNUSED GPluginPlugin *plugin,
                           G_GNUC_UNUSED GError **error)
{
	return TRUE;
}

static gboolean
gplugin_manager_unloading_cb(G_GNUC_UNUSED GObject *manager,
                             G_GNUC_UNUSED GPluginPlugin *plugin,
                             G_GNUC_UNUSED GError **error)
{
	return TRUE;
}

/******************************************************************************
 * Object Stuff
 *****************************************************************************/
static void
gplugin_manager_finalize(GObject *obj) {
	GPluginManager *manager = GPLUGIN_MANAGER(obj);

	g_queue_free_full(manager->paths, g_free);
	manager->paths = NULL;

	/* free all the data in the plugins hash table and destroy it */
	g_hash_table_foreach_remove(manager->plugins,
	                            gplugin_manager_remove_list_value,
	                            NULL);
	g_clear_pointer(&manager->plugins, g_hash_table_destroy);

	/* destroy the filename view */
	g_clear_pointer(&manager->plugins_filename_view, g_hash_table_destroy);

	/* free all the data in the loaders hash table and destroy it */
	g_hash_table_foreach_remove(manager->loaders,
	                            gplugin_manager_remove_list_value,
	                            NULL);
	g_clear_pointer(&manager->loaders, g_hash_table_destroy);

	/* call the base class's destructor */
	G_OBJECT_CLASS(gplugin_manager_parent_class)->finalize(obj);
}

static void
gplugin_manager_class_init(GPluginManagerClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	GPluginManagerClass *manager_class =
		GPLUGIN_MANAGER_CLASS(klass);

	obj_class->finalize = gplugin_manager_finalize;

	manager_class->append_path = gplugin_manager_real_append_path;
	manager_class->prepend_path = gplugin_manager_real_prepend_path;
	manager_class->remove_path = gplugin_manager_real_remove_path;
	manager_class->remove_paths = gplugin_manager_real_remove_paths;
	manager_class->get_paths = gplugin_manager_real_get_paths;

	manager_class->register_loader =
		gplugin_manager_real_register_loader;
	manager_class->unregister_loader =
		gplugin_manager_real_unregister_loader;

	manager_class->refresh = gplugin_manager_real_refresh;

	manager_class->find_plugins = gplugin_manager_real_find_plugins;
	manager_class->list_plugins = gplugin_manager_real_list_plugins;

	manager_class->get_plugin_dependencies = gplugin_manager_real_get_plugin_dependencies;

	manager_class->load_plugin = gplugin_manager_real_load_plugin;
	manager_class->unload_plugin = gplugin_manager_real_unload_plugin;

	manager_class->loading_plugin = gplugin_manager_loading_cb;
	manager_class->unloading_plugin = gplugin_manager_unloading_cb;

	/* signals */

	/**
	 * GPluginManager::loading-plugin:
	 * @manager: The #GPluginManager instance.  Treat as a #GObject.
	 * @plugin: The #GPluginPlugin that's about to be loaded.
	 * @error: Return address for a #GError.
	 *
	 * Emitted before @plugin is loaded.
	 *
	 * Return FALSE to stop loading
	 */
	signals[SIG_LOADING] =
		g_signal_new("loading-plugin",
		             G_OBJECT_CLASS_TYPE(manager_class),
		             G_SIGNAL_RUN_LAST,
		             G_STRUCT_OFFSET(GPluginManagerClass,
		                             loading_plugin),
		             gplugin_boolean_accumulator, NULL,
		             NULL,
		             G_TYPE_BOOLEAN,
		             2,
		             G_TYPE_OBJECT, G_TYPE_POINTER);

	/**
	 * GPluginManager::loaded-plugin:
	 * @manager: the #gpluginpluginmanager instance.  treat as a #gobject.
	 * @plugin: the #gpluginplugin that's about to be loaded.
	 *
	 * emitted after a plugin is loaded.
	 */
	signals[SIG_LOADED] =
		g_signal_new("loaded-plugin",
		             G_OBJECT_CLASS_TYPE(manager_class),
		             G_SIGNAL_RUN_LAST,
		             G_STRUCT_OFFSET(GPluginManagerClass,
		                             loaded_plugin),
		             NULL, NULL,
		             NULL,
		             G_TYPE_NONE,
		             1,
		             G_TYPE_OBJECT);

	/**
	 * GPluginManager::load-failed:
	 * @manager: The #GPluginPluginManager instance.
	 * @plugin: The #GPluginPlugin that failed to load.
	 *
	 * emitted after a plugin fails to load.
	 */
	signals[SIG_LOAD_FAILED] =
		g_signal_new("load-failed",
		             G_OBJECT_CLASS_TYPE(manager_class),
		             G_SIGNAL_RUN_LAST,
		             G_STRUCT_OFFSET(GPluginManagerClass, load_failed),
		             NULL,
		             NULL,
		             NULL,
		             G_TYPE_NONE,
		             1,
		             G_TYPE_OBJECT);

	/**
	 * GPluginManager::unloading-plugin:
	 * @manager: the #GPluginPluginManager instance.  treat as a #GObject.
	 * @plugin: the #GPluginPlugin that's about to be loaded.
	 *
	 * emitted before a plugin is unloaded.
	 *
	 * Return FALSE to stop unloading
	 */
	signals[SIG_UNLOADING] =
		g_signal_new("unloading-plugin",
		             G_OBJECT_CLASS_TYPE(manager_class),
		             G_SIGNAL_RUN_LAST,
		             G_STRUCT_OFFSET(GPluginManagerClass,
		                             unloading_plugin),
		             gplugin_boolean_accumulator, NULL,
		             NULL,
		             G_TYPE_BOOLEAN,
		             2,
		             G_TYPE_OBJECT, G_TYPE_POINTER);

	/**
	 * GPluginManager::unloaded-plugin:
	 * @manager: the #gpluginpluginmanager instance.  treat as a #gobject.
	 * @plugin: the #gpluginplugin that's about to be loaded.
	 *
	 * emitted after a plugin is unloaded.
	 */
	signals[SIG_UNLOADED] =
		g_signal_new("unloaded-plugin",
		             G_OBJECT_CLASS_TYPE(manager_class),
		             G_SIGNAL_RUN_LAST,
		             G_STRUCT_OFFSET(GPluginManagerClass,
		                             unloaded_plugin),
		             NULL, NULL,
		             NULL,
		             G_TYPE_NONE,
		             1,
		             G_TYPE_OBJECT);
}

static void
gplugin_manager_init(GPluginManager *manager) {
	manager->paths = g_queue_new();

	/* the plugins hashtable is keyed on a plugin id and holds a GSList of all
	 * plugins that share that id.
	 */
	manager->plugins =
		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

	/* the filename view is hash table keyed on the filename of the plugin with
	 * a value of the plugin itself.
	 */
	manager->plugins_filename_view =
		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);

	/* The loaders hash table is keyed on the supported extensions of the
	 * loader.  Which means that a loader that supports multiple extensions
	 * will be in the table multiple times.
	 *
	 * We deal with collisions by using a GSList for the value which will hold
	 * references to instances of the actual loaders.
	 *
	 * Storing this in this method allows up to quickly figure out which loader
	 * to use by the filename and helps us to avoid iterating the loaders table
	 * again and again.
	 */
	manager->loaders =
		g_hash_table_new_full(gplugin_manager_str_hash, g_str_equal,
		g_free, NULL);
}

/******************************************************************************
 * Private API
 *****************************************************************************/
void
gplugin_manager_private_init(void) {
	if (instance != NULL) {
		return;
	}

	instance = g_object_new(GPLUGIN_TYPE_MANAGER, NULL);

	gplugin_manager_register_loader(GPLUGIN_TYPE_NATIVE_LOADER);

	dependency_regex = g_regex_new(dependency_pattern, 0, 0, NULL);
}

void
gplugin_manager_private_uninit(void) {
	g_regex_unref(dependency_regex);

	g_clear_object(&instance);
}

/******************************************************************************
 * API
 *****************************************************************************/

/**
 * gplugin_manager_append_path:
 * @path: A path to add to the end of the plugin search paths
 *
 * Adds @path to the end of the list of paths to search for plugins.
 */
void
gplugin_manager_append_path(const gchar *path) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);

	if(klass && klass->append_path)
		klass->append_path(manager, path);
}

/**
 * gplugin_manager_prepend_path:
 * @path: A path to add to the beginning of the plugin search paths
 *
 * Adds @path to the beginning of the list of paths to search for plugins.
 */
void
gplugin_manager_prepend_path(const gchar *path) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);

	if(klass && klass->prepend_path)
		klass->prepend_path(manager, path);
}

/**
 * gplugin_manager_remove_path:
 * @path: A path to remove from the plugin search paths
 *
 * Removes @path from the list of paths to search for plugins.
 */
void
gplugin_manager_remove_path(const gchar *path) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);

	if(klass && klass->remove_path)
		klass->remove_path(manager, path);
}

/**
 * gplugin_manager_remove_paths:
 *
 * Clears all paths that are set to search for plugins.
 */
void
gplugin_manager_remove_paths(void) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);

	if(klass && klass->remove_paths)
		klass->remove_paths(manager);
}

/**
 * gplugin_manager_add_default_paths:
 *
 * Adds the path that GPlugin was installed to to the plugin search path, as
 * well as ${XDG_CONFIG_HOME}/gplugin.
 */
void
gplugin_manager_add_default_paths(void) {
	gchar *path;

	path = g_build_filename(PREFIX, LIBDIR, "gplugin", NULL);
	gplugin_manager_prepend_path(path);
	g_free(path);

	path = g_build_filename(g_get_user_config_dir(), "gplugin", NULL);
	gplugin_manager_prepend_path(path);
	g_free(path);
}


/**
 * gplugin_manager_add_app_paths:
 * @prefix: The installation prefix for the application.
 * @appname: The name of the application whose paths to add.
 *
 * Adds the application installation path for @appname.  This will add
 * $prefix/@appname/plugins to the list as well as
 * ${XDG_CONFIG_HOME}/@appname/plugins.
 */
void
gplugin_manager_add_app_paths(const gchar *prefix,
                              const gchar *appname)
{
	gchar *path;

	g_return_if_fail(appname != NULL);

	path = g_build_filename(prefix, LIBDIR, appname, NULL);
	gplugin_manager_prepend_path(path);
	g_free(path);

	path = g_build_filename(g_get_user_config_dir(), appname, "plugins", NULL);
	gplugin_manager_prepend_path(path);
	g_free(path);
}

/**
 * gplugin_manager_get_paths:
 *
 * Gets the list of paths which will be search for plugins.
 *
 * Return value: (element-type utf8) (transfer none): list of paths which will
 *               be searched for plugins.
 */
GList *
gplugin_manager_get_paths(void) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->get_paths)
		return klass->get_paths(manager);

	return NULL;
}

/**
 * gplugin_manager_register_loader:
 * @type: #GType of a #GPluginLoader
 *
 * Registers @type as an available loader.
 */
void
gplugin_manager_register_loader(GType type) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->register_loader)
		klass->register_loader(manager, type);
}

/**
 * gplugin_manager_unregister_loader:
 * @type: #GType of a #GPluginLoader
 *
 * Unregisters @type as an available loader.
 */
void
gplugin_manager_unregister_loader(GType type) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->unregister_loader)
		klass->unregister_loader(manager, type);
}

/**
 * gplugin_manager_refresh:
 *
 * Forces a refresh of all plugins found in the search paths.
 */
void
gplugin_manager_refresh(void) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->refresh)
		klass->refresh(manager);
}

/**
 * gplugin_manager_find_plugins:
 * @id: id string of the plugin to find
 *
 * Finds all plugins matching @id.
 *
 * Return value: (element-type GPlugin.Plugin) (transfer full): A #GSList of
 *               referenced #GPluginPlugin's matching @id.  Call
 *               #gplugin_manager_free_plugin_list on the returned value
 *               when you're done with it.
 */
GSList *
gplugin_manager_find_plugins(const gchar *id) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->find_plugins)
		return klass->find_plugins(manager, id);

	return NULL;
}

/**
 * gplugin_manager_free_plugin_list:
 * @plugins_list: (element-type GPlugin.Plugin): Returned value from
 *                #gplugin_manager_find_plugins
 *
 * Frees the return value of #gplugin_manager_find_plugins.
 */
void
gplugin_manager_free_plugin_list(GSList *plugins_list) {
	GSList *l = NULL;

	g_return_if_fail(plugins_list != NULL);

	for(l = plugins_list; l; l = l->next) {
		GPluginPlugin *plugin = NULL;

		if(l->data == NULL)
			continue;

		plugin = GPLUGIN_PLUGIN(l->data);

		g_object_unref(G_OBJECT(plugin));
	}

	g_slist_free(plugins_list);
}

/**
 * gplugin_manager_find_plugin:
 * @id: The id of the plugin to find.
 *
 * Finds the first plugin matching @id.  This function uses
 * #gplugin_manager_find_plugins and returns the first plugin in the
 * list.
 *
 * Return value: (transfer full): A referenced #GPluginPlugin instance or NULL
 *               if no plugin matching @id was found.
 */
GPluginPlugin *
gplugin_manager_find_plugin(const gchar *id) {
	GSList *plugins_list = NULL;
	GPluginPlugin *plugin = NULL;

	g_return_val_if_fail(id != NULL, NULL);

	plugins_list = gplugin_manager_find_plugins(id);
	if(plugins_list == NULL)
		return NULL;

	plugin = GPLUGIN_PLUGIN(g_object_ref(G_OBJECT(plugins_list->data)));

	gplugin_manager_free_plugin_list(plugins_list);

	return plugin;
}

/**
 * gplugin_manager_get_plugin_dependencies:
 * @plugin: The #GPluginPlugin whose dependencies to get.
 * @error: Return address for a #GError.
 *
 * Returns a list of all the #GPluginPlugin's that @plugin depends on.
 *
 * Return value: (element-type GPlugin.Plugin) (transfer full): A #GSList of
 *               #GPluginPlugin's that @plugin depends on, or NULL on error
 *               with @error set.
 */
GSList *
gplugin_manager_get_plugin_dependencies(GPluginPlugin *plugin, GError **error) {
	GPluginManager *manager = NULL;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), NULL);

	manager = GPLUGIN_MANAGER_INSTANCE;
	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->get_plugin_dependencies) {
		return klass->get_plugin_dependencies(manager, plugin, error);
	}

	return NULL;
}

/**
 * gplugin_manager_load_plugin:
 * @plugin: #GPluginPlugin instance
 * @error: (out): return location for a #GError or null
 *
 * Loads @plugin and all of it's dependencies.  If a dependency can not be
 * loaded, @plugin will not be loaded either.  However, any other plugins that
 * @plugin depends on that were loaded from this call, will not be unloaded.
 *
 * Return value: TRUE if @plugin was loaded successfully or already loaded,
 *               FALSE otherwise.
 */
gboolean
gplugin_manager_load_plugin(GPluginPlugin *plugin, GError **error) {
	GPluginManager *manager = NULL;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	manager = GPLUGIN_MANAGER_INSTANCE;
	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->load_plugin)
		return klass->load_plugin(manager, plugin, error);

	return FALSE;
}

/**
 * gplugin_manager_unload_plugin:
 * @plugin: #GPluginPlugin instance
 * @error: (out): return location for a #GError or null
 *
 * Unloads @plugin.  If @plugin has dependencies, they are not unloaded.
 *
 * Return value: TRUE if @plugin was unloaded successfully or not loaded,
 *               FALSE otherwise.
 */
gboolean
gplugin_manager_unload_plugin(GPluginPlugin *plugin, GError **error) {
	GPluginManager *manager = NULL;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_PLUGIN(plugin), FALSE);

	manager = GPLUGIN_MANAGER_INSTANCE;
	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->unload_plugin)
		return klass->unload_plugin(manager, plugin, error);

	return FALSE;
}

/**
 * gplugin_manager_list_plugins:
 *
 * Returns a #GList of all plugin id's.  Each id should be queried directly
 * for more information.
 *
 * Return value: (element-type utf8) (transfer full): A #GList of each unique
 *               plugin id.
 */
GList *
gplugin_manager_list_plugins(void) {
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->list_plugins)
		return klass->list_plugins(manager);

	return NULL;
}


/**
 * gplugin_manager_get_instance:
 *
 * Returns a #GObject that is the instance of the plugin manager that is being
 * used.
 *
 * This is provided so that signals can be connected and should not be tinkered
 * with in any way.
 *
 * Return Value: (transfer none): The #GObject that is the instance of the
 *                                plugin manager.
 */
GObject *
gplugin_manager_get_instance(void) {
	if(instance)
		return G_OBJECT(instance);

	return NULL;
}

