/*
 * Copyright (C) 2011-2020 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 <https://www.gnu.org/licenses/>.
 */

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

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

#include <gplugin/gplugin-core.h>
#include <gplugin/gplugin-file-tree.h>
#include <gplugin/gplugin-manager.h>
#include <gplugin/gplugin-native-loader.h>
#include <gplugin/gplugin-private.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.
 */

/**
 * GPluginManagerForeachFunc:
 * @id: The id of the plugin.
 * @plugins: A #GSList of each plugin that has the id @id.
 * @data: User data passed to gplugin_manager_foreach().
 *
 * A callback function for gplugin_manager_foreach().
 */

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

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

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

	GSList *loaders;
	GHashTable *loaders_by_extension;

	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);

	gboolean (
		*register_loader)(GPluginManager *manager, GType type, GError **error);
	gboolean (*unregister_loader)(
		GPluginManager *manager,
		GType type,
		GError **error);
	GSList *(*get_loaders)(GPluginManager *manager);

	void (*refresh)(GPluginManager *manager);

	void (*foreach)(
		GPluginManager *manager,
		GPluginManagerForeachFunc func,
		gpointer data);

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

	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);
	void (*unload_plugin_failed)(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;
}

/*
 * gplugin_manager_find_loader_by_type:
 * @manager: The #GPluginManager instance.
 * @type: The #GType of the loader to find.
 *
 * Looks up a #GPluginLoader instance by its type.
 *
 * Returns: (transfer none): The #GPluginLoader instance or %NULL.
 */
static GPluginLoader *
gplugin_manager_find_loader_by_type(GPluginManager *manager, GType type)
{
	GSList *l = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);
	g_return_val_if_fail(g_type_is_a(type, GPLUGIN_TYPE_LOADER), NULL);

	for(l = manager->loaders; l; l = l->next) {
		if(G_OBJECT_TYPE(l->data) == type) {
			return GPLUGIN_LOADER(l->data);
		}
	}

	return NULL;
}

static void
gplugin_manager_foreach_unload_plugin(
	gpointer key,
	gpointer value,
	G_GNUC_UNUSED gpointer data)
{
	GList *l = NULL;
	gchar *id = (gchar *)key;

	for(l = (GList *)value; l; l = l->next) {
		GPluginPlugin *plugin = GPLUGIN_PLUGIN(l->data);
		GPluginLoader *loader = NULL;
		GError *error = NULL;

		if(gplugin_plugin_get_state(plugin) != GPLUGIN_PLUGIN_STATE_LOADED) {
			continue;
		}

		loader = gplugin_plugin_get_loader(plugin);
		if(!gplugin_loader_unload_plugin(loader, plugin, &error)) {
			g_warning(
				"failed to unload plugin with id %s: %s",
				id,
				error ? error->message : "unknown");
			g_clear_error(&error);
		}
		g_object_unref(G_OBJECT(loader));
	}
}

static gchar *
gplugin_manager_normalize_path(const gchar *path)
{
	if(g_str_has_suffix(path, G_DIR_SEPARATOR_S)) {
		return g_strdup(path);
	}

	return g_strdup_printf("%s%s", path, G_DIR_SEPARATOR_S);
}

static gint
gplugin_manager_compare_paths(gconstpointer a, gconstpointer b)
{
	gchar *keya = NULL, *keyb = NULL;
	gint r = 0;

	keya = g_utf8_collate_key_for_filename((const gchar *)a, -1);
	keyb = g_utf8_collate_key_for_filename((const gchar *)b, -1);

	r = strcmp(keya, keyb);

	g_free(keya);
	g_free(keyb);

	return r;
}

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

	if(!path) {
		return;
	}

	normalized = gplugin_manager_normalize_path(path);

	l = g_queue_find_custom(
		manager->paths,
		normalized,
		gplugin_manager_compare_paths);
	if(l == NULL) {
		g_queue_push_tail(manager->paths, normalized);
	} else {
		g_free(normalized);
	}
}

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

	if(!path) {
		return;
	}

	normalized = gplugin_manager_normalize_path(path);

	l = g_queue_find_custom(
		manager->paths,
		normalized,
		gplugin_manager_compare_paths);
	if(l == NULL) {
		g_queue_push_head(manager->paths, normalized);
	} else {
		g_free(normalized);
	}
}

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

	g_return_if_fail(path != NULL);

	normalized = gplugin_manager_normalize_path(path);

	l = g_queue_find_custom(
		manager->paths,
		normalized,
		gplugin_manager_compare_paths);
	if(l != NULL) {
		g_free(l->data);
		g_queue_delete_link(manager->paths, l);
	}

	g_free(normalized);
}

static void
gplugin_manager_real_remove_paths(GPluginManager *manager)
{
	/* g_queue_clear_full was added in 2.60 but we require 2.40 */
	g_queue_foreach(manager->paths, (GFunc)g_free, NULL);
	g_queue_clear(manager->paths);
}

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

static gboolean
gplugin_manager_real_register_loader(
	GPluginManager *manager,
	GType type,
	GError **error)
{
	GPluginLoader *loader = NULL;
	GSList *l = NULL, *exts = NULL;

	g_return_val_if_fail(g_type_is_a(type, GPLUGIN_TYPE_LOADER), FALSE);

	loader = gplugin_manager_find_loader_by_type(manager, type);
	if(GPLUGIN_IS_LOADER(loader)) {
		g_set_error(
			error,
			GPLUGIN_DOMAIN,
			0,
			_("loader %s was already registered"),
			g_type_name(type));
		return FALSE;
	}

	/* 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_set_error(
			error,
			GPLUGIN_DOMAIN,
			0,
			_("failed to create loader instance for %s"),
			g_type_name(type));

		return FALSE;
	}

	manager->loaders =
		g_slist_prepend(manager->loaders, g_object_ref(G_OBJECT(loader)));

	exts = gplugin_loader_get_supported_extensions(loader);
	for(l = exts; 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_by_extension, 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_by_extension,
			g_strdup(ext),
			existing);
	}
	g_slist_free(exts);

	/* 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));

	return TRUE;
}

static gboolean
gplugin_manager_real_unregister_loader(
	GPluginManager *manager,
	GType type,
	GError **error)
{
	GPluginLoader *loader = NULL;
	GSList *l = NULL, *exts = NULL;

	g_return_val_if_fail(g_type_is_a(type, GPLUGIN_TYPE_LOADER), FALSE);

	loader = gplugin_manager_find_loader_by_type(manager, type);
	if(!GPLUGIN_IS_LOADER(loader)) {
		g_set_error(
			error,
			GPLUGIN_DOMAIN,
			0,
			_("loader %s is not registered"),
			g_type_name(type));

		return FALSE;
	}

	exts = gplugin_loader_get_supported_extensions(loader);
	for(l = exts; l; l = l->next) {
		GSList *los = NULL;
		GSList *ll = NULL;
		const gchar *ext = NULL;

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

		for(ll = los; ll; ll = ll->next) {
			GPluginLoader *lo = GPLUGIN_LOADER(ll->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_by_extension,
					g_strdup(ext),
					los);
			} else {
				g_hash_table_remove(manager->loaders_by_extension, ext);
			}

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

			/* now move to the next extension to check */
			break;
		}
	}
	g_slist_free(exts);

	manager->loaders = g_slist_remove(manager->loaders, loader);
	g_object_unref(G_OBJECT(loader));

	return TRUE;
}

static GSList *
gplugin_manager_real_get_loaders(GPluginManager *manager)
{
	return g_slist_copy_deep(manager->loaders, (GCopyFunc)g_object_ref, NULL);
}

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 its 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 */
				l = g_hash_table_lookup(
					manager->loaders_by_extension,
					e->extension);
				for(; l; l = l->next) {
					if(!GPLUGIN_IS_LOADER(l->data)) {
						continue;
					}

					loader = GPLUGIN_LOADER(l->data);

					/* 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 its message and
					 * try the next loader.
					 */
					if(error) {
						errors++;

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

						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);
						g_free(real_filename);
						g_object_unref(G_OBJECT(info));

						error_messages =
							g_list_prepend(error_messages, error_message);

						continue;
					}

					/* 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);
					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));

					/* since the plugin is now stored in our hash tables we
					 * need to remove this function's reference to it.
					 */
					g_object_unref(G_OBJECT(plugin));
				}

				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 void
gplugin_manager_real_foreach(
	GPluginManager *manager,
	GPluginManagerForeachFunc func,
	gpointer data)
{
	GHashTableIter iter;
	gpointer id = NULL, plugins = NULL;

	g_hash_table_iter_init(&iter, manager->plugins);
	while(g_hash_table_iter_next(&iter, &id, &plugins)) {
		func((gchar *)id, (GSList *)plugins, data);
	}
}

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);

	l = g_hash_table_lookup(manager->plugins, id);
	plugins_list = g_slist_copy_deep(l, (GCopyFunc)g_object_ref, NULL);

	return plugins_list;
}

static GSList *
gplugin_manager_real_find_plugins_with_state(
	GPluginManager *manager,
	GPluginPluginState state)
{
	GSList *plugins = NULL;
	GHashTableIter iter;
	gpointer value = NULL;

	g_hash_table_iter_init(&iter, manager->plugins);
	while(g_hash_table_iter_next(&iter, NULL, &value)) {
		GSList *l = NULL;

		for(l = (GSList *)value; l != NULL; l = l->next) {
			GPluginPlugin *plugin = GPLUGIN_PLUGIN(l->data);

			if(gplugin_plugin_get_state(plugin) == state) {
				plugins =
					g_slist_prepend(plugins, g_object_ref(G_OBJECT(plugin)));
			}
		}
	}

	return plugins;
}

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)
{
	GSList *dependencies = NULL, *l = NULL;
	GError *ourerror = NULL;
	gboolean all_loaded = TRUE;

	dependencies = gplugin_manager_get_plugin_dependencies(plugin, &ourerror);
	if(ourerror != NULL) {
		g_propagate_error(error, ourerror);

		return FALSE;
	}

	for(l = dependencies; l != NULL; l = l->next) {
		GPluginPlugin *dependency = GPLUGIN_PLUGIN(l->data);
		gboolean loaded = FALSE;

		loaded = gplugin_manager_load_plugin(dependency, &ourerror);

		if(!loaded || ourerror != NULL) {
			if(ourerror != NULL) {
				g_propagate_error(error, ourerror);
			}

			all_loaded = FALSE;
			break;
		}
	}

	g_slist_free_full(dependencies, g_object_unref);

	return all_loaded;
}

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 i = 0;

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

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

	for(i = 0; dependencies[i] != NULL; 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;
			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_with_version(oid, oop, over);
			g_free(oid);
			g_free(oop);
			g_free(over);

			if(matches == NULL) {
				continue;
			}

			/* prepend the first found match to our return value */
			ret = g_slist_prepend(ret, g_object_ref(matches->data));
			g_slist_free_full(matches, g_object_unref);

			found = TRUE;

			break;
		}
		g_strfreev(ors);

		if(!found) {
			g_set_error(
				error,
				GPLUGIN_DOMAIN,
				0,
				_("failed to find dependency %s for %s"),
				dependencies[i],
				gplugin_plugin_info_get_id(info));

			g_slist_free_full(ret, g_object_unref);

			return NULL;
		}
	}

	return ret;
}

static gboolean
gplugin_manager_real_load_plugin(
	GPluginManager *manager,
	GPluginPlugin *plugin,
	GError **ret_error)
{
	GPluginPluginInfo *info = NULL;
	GPluginLoader *loader = NULL;
	GError *error = 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) {
		gchar *filename = gplugin_plugin_get_filename(plugin);

		g_set_error(
			ret_error,
			GPLUGIN_DOMAIN,
			0,
			_("Plugin %s did not return value plugin info"),
			filename);
		g_free(filename);

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		return FALSE;
	}

	if(!gplugin_manager_load_dependencies(plugin, info, ret_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)) {
		gchar *filename = gplugin_plugin_get_filename(plugin);

		g_set_error(
			ret_error,
			GPLUGIN_DOMAIN,
			0,
			_("The loader for %s is not a loader.  This "
			  "should not happened!"),
			filename);
		g_free(filename);

		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) {
		/* Set the plugin's error. */
		g_object_set(G_OBJECT(plugin), "error", error, NULL);

		g_propagate_error(ret_error, error);

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		return ret;
	}

	ret = gplugin_loader_load_plugin(loader, plugin, &error);
	if(ret) {
		/* If the plugin successfully loaded but returned an error, ignore the
		 * error.
		 */
		g_clear_error(&error);

		/* Likewise, make sure the plugin's error is set to NULL. */
		g_object_set(G_OBJECT(plugin), "error", NULL, NULL);

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOADED);
		g_signal_emit(manager, signals[SIG_LOADED], 0, plugin);
	} else {
		/* Set the error on the plugin as well.  This  has to be before we
		 * propagate the error, because error is invalidate at that point.
		 */
		g_object_set(G_OBJECT(plugin), "error", error, NULL);

		/* Set the state after the error is set, because people might connect
		 * to the notify signal on the state property.
		 */
		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_LOAD_FAILED);

		g_signal_emit(manager, signals[SIG_LOAD_FAILED], 0, plugin);

		g_propagate_error(ret_error, error);
	}

	return ret;
}

static gboolean
gplugin_manager_real_unload_plugin(
	GPluginManager *manager,
	GPluginPlugin *plugin,
	GError **ret_error)
{
	GPluginLoader *loader = NULL;
	GError *error = 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(
			ret_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) {
		/* Set the plugin's error. */
		g_object_set(G_OBJECT(plugin), "error", error, NULL);

		g_propagate_error(ret_error, error);

		return ret;
	}

	ret = gplugin_loader_unload_plugin(loader, plugin, &error);
	if(ret) {
		/* If the plugin successfully loaded but returned an error, ignore the
		 * error.
		 */
		g_clear_error(&error);

		/* Like, make sure the plugin's error is set to NULL. */
		g_object_set(G_OBJECT(plugin), "error", NULL, NULL);

		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_QUERIED);
		g_signal_emit(manager, signals[SIG_UNLOADED], 0, plugin);
	} else {
		/* Set the error on the plugin as well.  This  has to be before we
		 * propagate the error, because error is invalidate at that point.
		 */
		g_object_set(G_OBJECT(plugin), "error", error, NULL);

		/* Set the state after the error is set, because people might connect
		 * to the notify signal on the state property.
		 */
		gplugin_plugin_set_state(plugin, GPLUGIN_PLUGIN_STATE_UNLOAD_FAILED);

		g_signal_emit(manager, signals[SIG_UNLOAD_FAILED], 0, plugin);

		g_propagate_error(ret_error, 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;
}

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

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

	/* unload all of the loaded plugins */
	g_hash_table_foreach(
		manager->plugins,
		gplugin_manager_foreach_unload_plugin,
		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);

	/* clean up our list of loaders */
	g_slist_free_full(manager->loaders, g_object_unref);
	manager->loaders = NULL;

	/* free all the data in the loaders hash table and destroy it */
	g_hash_table_foreach_remove(
		manager->loaders_by_extension,
		gplugin_manager_remove_list_value,
		NULL);
	g_clear_pointer(&manager->loaders_by_extension, 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->get_loaders = gplugin_manager_real_get_loaders;

	manager_class->refresh = gplugin_manager_real_refresh;

	manager_class->foreach = gplugin_manager_real_foreach;

	manager_class->find_plugins = gplugin_manager_real_find_plugins;
	manager_class->find_plugins_with_state =
		gplugin_manager_real_find_plugins_with_state;
	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-plugin-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-plugin-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 successfully 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);

	/**
	 * GPluginManager::unload-plugin-failed:
	 * @manager: The #GPluginManager instance.
	 * @plugin: The #GPluginPlugin instance that failed to unload.
	 * @error: A #GError instance.
	 *
	 * Emitted when @manager was asked to unload @plugin, but @plugin returned
	 * %FALSE when its unload function was called.
	 */
	signals[SIG_UNLOAD_FAILED] = g_signal_new(
		"unload-plugin-failed",
		G_OBJECT_CLASS_TYPE(manager_class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET(GPluginManagerClass, unload_plugin_failed),
		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_by_extension 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 us 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_by_extension = g_hash_table_new_full(
		gplugin_manager_str_hash,
		g_str_equal,
		g_free,
		NULL);
}

/******************************************************************************
 * Private API
 *****************************************************************************/
void
gplugin_manager_private_init(gboolean register_native_loader)
{
	GError *error = NULL;

	if(instance != NULL) {
		return;
	}

	instance = g_object_new(GPLUGIN_TYPE_MANAGER, NULL);

	if(register_native_loader) {
		if(!gplugin_manager_register_loader(
			   GPLUGIN_TYPE_NATIVE_LOADER,
			   &error)) {
			if(error != NULL) {
				g_error("failed to register loader: %s", error->message);
				g_error_free(error);
			} else {
				g_error("failed to register loader: unknown failure");
			}
		}
	}

	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` so users can install additional loaders
 * themselves.
 */
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 searched for plugins.
 *
 * Returns: (element-type utf8) (transfer none): The 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.
 * @error: (out) (nullable): The return address for a #GError.
 *
 * Registers @type as an available loader.
 *
 * Returns: %TRUE if the loader was successfully register, %FALSE otherwise
 *          with @error set.
 */
gboolean
gplugin_manager_register_loader(GType type, GError **error)
{
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);

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

	g_set_error(
		error,
		GPLUGIN_DOMAIN,
		0,
		"register_loader method not implemented");

	return FALSE;
}

/**
 * gplugin_manager_unregister_loader:
 * @type: #GType of a #GPluginLoader.
 * @error: (out) (nullable): The return address for a #GError.
 *
 * Unregisters @type as an available loader.
 *
 * Returns: %TRUE if the loader was successfully unregistered, %FALSE
 *          otherwise with @error set.
 */
gboolean
gplugin_manager_unregister_loader(GType type, GError **error)
{
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);

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

	g_set_error(
		error,
		GPLUGIN_DOMAIN,
		0,
		"unregister_loader method not implemented");

	return FALSE;
}

/**
 * gplugin_manager_get_loaders:
 *
 * Returns a list of all registered #GPluginLoader's.
 *
 * Returns: (element-type GPlugin.Loader) (transfer full): Returns a list of all
 *          registered loaders.
 */
GSList *
gplugin_manager_get_loaders(void)
{
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);

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

	return NULL;
}

/**
 * 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_foreach:
 * @func: (scope call): The #GPluginManagerForeachFunc to call.
 * @data: User data to pass to func.
 *
 * Calls @func for each plugin that is known.
 */
void
gplugin_manager_foreach(GPluginManagerForeachFunc func, gpointer data)
{
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GPluginManagerClass *klass = NULL;

	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));
	g_return_if_fail(func != NULL);

	klass = GPLUGIN_MANAGER_GET_CLASS(manager);
	if(klass && klass->foreach)
		klass->foreach(manager, func, data);
}

/**
 * gplugin_manager_find_plugins:
 * @id: id string of the plugin to find.
 *
 * Finds all plugins matching @id.
 *
 * Returns: (element-type GPlugin.Plugin) (transfer full): A #GSList of
 *          referenced #GPluginPlugin's matching @id.  Call
 *          g_slist_free_full() with a `DestroyNotify` of g_object_unref() 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_find_plugins_with_version:
 * @id: The ID of the plugin to find.
 * @op: one of <, <=, =, ==, >=, >.
 * @version: The version to compare against.
 *
 * Similar to gplugin_manager_find_plugins() but only returns plugins whose
 * versions match @op and @version.  This is primarily used for dependency
 * loading where a plugin may depend on a specific range of versions of another
 * plugin.
 *
 * Returns: (element-type GPlugin.Plugin) (transfer full): A #GSList of
 *          referenced #GPluginPlugin's matching @id.  Call
 *          g_slist_free_full() with a `DestroyNotify` of g_object_unref() on
 *          the returned value when you're done with it.
 */
GSList *
gplugin_manager_find_plugins_with_version(
	const gchar *id,
	const gchar *op,
	const gchar *version)
{
	GPluginManager *manager = GPLUGIN_MANAGER_INSTANCE;
	GSList *plugins = NULL, *filtered = NULL, *l = NULL;

	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);

	plugins = gplugin_manager_find_plugins(id);

	if(op == NULL && version == NULL) {
		/* we weren't actually passed an operator and a version so just return
		 * the list we have based on the id.
		 */
		return plugins;
	}

	for(l = plugins; l; l = l->next) {
		GPluginPlugin *plugin = GPLUGIN_PLUGIN(l->data);
		GPluginPluginInfo *info = NULL;
		const gchar *found_version = NULL;
		gint result = 0;
		gboolean keep = FALSE;

		/* get the plugin's version from it's info */
		info = gplugin_plugin_get_info(plugin);
		found_version = gplugin_plugin_info_get_version(info);

		/* now compare the version of the plugin to passed in version.  This
		 * should be done in this order so it's easier to track the operators.
		 * IE: we want to keep the inequality the same.
		 */
		result = gplugin_version_compare(found_version, version);

		/* we need to keep info around until we're done using found_version */
		g_object_unref(G_OBJECT(info));

		if(result < 0) {
			keep = (g_strcmp0(op, "<") == 0 || g_strcmp0(op, "<=") == 0);
		} else if(result == 0) {
			keep =
				(g_strcmp0(op, "=") == 0 || g_strcmp0(op, "==") == 0 ||
				 g_strcmp0(op, "<=") == 0 || g_strcmp0(op, ">=") == 0);
		} else if(result > 0) {
			keep = (g_strcmp0(op, ">") == 0 || g_strcmp0(op, ">=") == 0);
		}

		if(keep) {
			filtered =
				g_slist_prepend(filtered, g_object_ref(G_OBJECT(plugin)));
		}
	}

	g_slist_free_full(plugins, g_object_unref);

	return g_slist_reverse(filtered);
}

/**
 * gplugin_manager_find_plugins_with_state:
 * @state: The #GPluginPluginState to look for.
 *
 * Finds all plugins that currently have a state of @state.
 *
 * Returns: (element-type GPlugin.Plugin) (transfer full): A #GSList of
 *          referenced #GPluginPlugin's whose state is @state.  Call
 *          g_slist_free_full() with a `DestroyNotify` of g_object_unref() on
 *          the returned value when you're done with it.
 */
GSList *
gplugin_manager_find_plugins_with_state(GPluginPluginState state)
{
	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_with_state)
		return klass->find_plugins_with_state(manager, state);

	return NULL;
}

/**
 * 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)));

	g_slist_free_full(plugins_list, g_object_unref);

	return plugin;
}

/**
 * gplugin_manager_find_plugin_with_newest_version:
 * @id: The id of the plugin to find.
 *
 * Calls gplugin_manager_find_plugins() with @id, and then returns the plugins
 * with the highest version number or %NULL if no plugins with @id are found.
 *
 * Returns: (transfer full): The #GPluginPlugin with an id of @id that has the
 *          highest version number, or %NULL if no plugins were found with @id.
 */
GPluginPlugin *
gplugin_manager_find_plugin_with_newest_version(const gchar *id)
{
	GPluginPlugin *plugin_a = NULL;
	GPluginPluginInfo *info_a = NULL;
	const gchar *version_a = NULL;
	GSList *l = NULL;

	g_return_val_if_fail(id != NULL, NULL);

	l = gplugin_manager_find_plugins(id);
	for(; l != NULL; l = g_slist_delete_link(l, l)) {
		GPluginPlugin *plugin_b = NULL;
		GPluginPluginInfo *info_b = NULL;
		const gchar *version_b = NULL;
		gint cmp = 0;

		if(!GPLUGIN_IS_PLUGIN(l->data)) {
			continue;
		}

		plugin_b = GPLUGIN_PLUGIN(l->data);
		info_b = gplugin_plugin_get_info(plugin_b);

		/* If this is the first plugin we've found, set the plugin_a values and
		 * continue.
		 */
		if(!GPLUGIN_IS_PLUGIN(plugin_a)) {
			plugin_a = plugin_b;
			info_a = info_b;

			version_a = gplugin_plugin_info_get_version(info_a);

			continue;
		}

		/* At this point, we've seen another plugin, so we need to compare
		 * their versions.
		 */
		version_b = gplugin_plugin_info_get_version(info_b);

		cmp = gplugin_version_compare(version_a, version_b);
		if(cmp < 0) {
			/* plugin_b has a newer version, so set the plugin_a pointers to
			 * the plugin_b pointers as well as the version pointers.
			 */
			g_set_object(&plugin_a, plugin_b);
			g_set_object(&info_a, info_b);

			version_a = version_b;
		}

		/* Clean up the plugin_b pointers. */
		g_clear_object(&plugin_b);
		g_clear_object(&info_b);
	}

	g_clear_object(&info_a);

	return plugin_a;
}

/**
 * gplugin_manager_get_plugin_dependencies:
 * @plugin: The #GPluginPlugin whose dependencies to get.
 * @error: (out) (nullable): 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.  Call g_slist_free_full() with a
 *               `DestroyNotify` of g_object_unref() on the returned value when
 *               you're done with it.
 */
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) (nullable): return location for a #GError or %NULL.
 *
 * Loads @plugin and all of its 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) (nullable): Return location for a #GError or %NULL.
 *
 * Unloads @plugin.  If @plugin has dependencies, they are not unloaded.
 *
 * Returns: %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 container): 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.
 *
 * Returns: (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;
}
