/*
 * Copyright (C) 2010, 2011 Igalia S.L.
 * Copyright (C) 2011 Intel Corporation.
 *
 * Contact: Iago Toral Quiroga <itoral@igalia.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; version 2.1 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */

/**
 * SECTION:grl-registry
 * @short_description: Grilo plugins loader and manager
 * @see_also: #GrlPlugin, #GrlSource
 *
 * The registry holds the metadata of a set of plugins.
 *
 * The #GrlRegistry object is a list of plugins and some functions
 * for dealing with them. Each #GrlPlugin is matched 1-1 with a file
 * on disk, and may or may not be loaded a given time. There only can be
 * a single instance of #GrlRegistry (singleton pattern).
 *
 * A #GrlPlugin can hold several data #GrlSource sources, and #GrlRegistry
 * shall register each one of them.
 */

#include "grl-registry.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "grl-registry-priv.h"
#include "grl-plugin-priv.h"
#include "grl-log.h"
#include "grl-error.h"

#include <glib/gi18n-lib.h>
#include <string.h>
#include <gmodule.h>
#include <libxml/parser.h>

#define GRL_LOG_DOMAIN_DEFAULT  registry_log_domain
GRL_LOG_DOMAIN(registry_log_domain);

#define XML_ROOT_ELEMENT_NAME "plugin"

#define GRL_PLUGIN_INFO_SUFFIX "xml"

#define GRL_PLUGIN_INFO_MODULE "module"

#define LOCAL_NET_TAG      "net:local"
#define INTERNET_NET_TAG   "net:internet"

#ifdef HAVE_GIO_WITH_NETMON
#define SET_INVISIBLE_SOURCE(src, val)                          \
  g_object_set_data(G_OBJECT(src), "invisible", GINT_TO_POINTER(val))
#define SOURCE_IS_INVISIBLE(src)                                \
  GPOINTER_TO_INT(g_object_get_data(G_OBJECT(src), "invisible"))
#else
#define SET_INVISIBLE_SOURCE(src, val)
#define SOURCE_IS_INVISIBLE(src)                                \
  FALSE
#endif /* HAVE_GIO_WITH_NETMON */

#define GRL_REGISTRY_GET_PRIVATE(object)                        \
  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
                               GRL_TYPE_REGISTRY,               \
                               GrlRegistryPrivate))

/* GQuark-like implementation, where we manually assign the first IDs. */
struct KeyIDHandler {
  GHashTable *string_to_id;
  GArray *id_to_string;
  gint last_id;
};

struct _GrlRegistryPrivate {
  GHashTable *configs;
  GHashTable *plugins;
  GHashTable *sources;
  GHashTable *related_keys;
  GHashTable *system_keys;
  GHashTable *ranks;
  GSList *plugins_dir;
  GSList *allowed_plugins;
  gboolean all_plugins_preloaded;
  struct KeyIDHandler key_id_handler;
#ifdef HAVE_GIO_WITH_NETMON
  GNetworkMonitor *netmon;
#endif
};

static void grl_registry_setup_ranks (GrlRegistry *registry);

static void grl_registry_preload_plugins (GrlRegistry *registry,
                                          GList **plugins_loaded);

static void key_id_handler_init (struct KeyIDHandler *handler);

static void key_id_handler_free (struct KeyIDHandler *handler);

static GrlKeyID key_id_handler_get_key (struct KeyIDHandler *handler,
                                        const gchar *key_name);

static const gchar *key_id_handler_get_name (struct KeyIDHandler *handler,
                                             GrlKeyID key);

static GrlKeyID key_id_handler_add (struct KeyIDHandler *handler,
                                    GrlKeyID key, const gchar *key_name);

static gboolean param_spec_is_equal (GParamSpec *curr, GParamSpec *new);

static void shutdown_plugin (GrlPlugin *plugin);

static void configs_free (GList *configs);

static GrlPlugin *grl_registry_prepare_plugin (GrlRegistry *registry,
                                               const gchar *library_filename,
                                               GError **error);

/* ================ GrlRegistry GObject ================ */

enum {
  SIG_SOURCE_ADDED,
  SIG_SOURCE_REMOVED,
  SIG_METADATA_KEY_ADDED,
  SIG_LAST
};
static gint registry_signals[SIG_LAST];

G_DEFINE_TYPE (GrlRegistry, grl_registry, G_TYPE_OBJECT);

static void
grl_registry_class_init (GrlRegistryClass *klass)
{
  g_type_class_add_private (klass, sizeof (GrlRegistryPrivate));

  /**
   * GrlRegistry::source-added:
   * @registry: the registry
   * @source: the source that has been added
   *
   * Signals that a source has been added to the registry.
   *
   * Since: 0.2.0
   */
  registry_signals[SIG_SOURCE_ADDED] =
    g_signal_new("source-added",
		 G_TYPE_FROM_CLASS(klass),
		 G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
		 0,
		 NULL,
		 NULL,
		 g_cclosure_marshal_VOID__OBJECT,
		 G_TYPE_NONE, 1, GRL_TYPE_SOURCE);

  /**
   * GrlRegistry::source-removed:
   * @registry: the registry
   * @source: the source that has been removed
   *
   * Signals that a source has been removed from the registry.
   *
   * Since: 0.2.0
   */
  registry_signals[SIG_SOURCE_REMOVED] =
    g_signal_new("source-removed",
		 G_TYPE_FROM_CLASS(klass),
		 G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
		 0,
		 NULL,
		 NULL,
		 g_cclosure_marshal_VOID__OBJECT,
		 G_TYPE_NONE, 1, GRL_TYPE_SOURCE);

  /**
   * GrlRegistry::metadata-key-added:
   * @registry: the registry
   * @key: the name of the new key added
   *
   * Signals that a new metadata key has been registered.
   *
   * Since: 0.2.10
   */
  registry_signals[SIG_METADATA_KEY_ADDED] =
    g_signal_new("metadata-key-added",
                 G_TYPE_FROM_CLASS(klass),
                 G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                 0,
                 NULL,
                 NULL,
                 g_cclosure_marshal_VOID__STRING,
                 G_TYPE_NONE, 1, G_TYPE_STRING);
}

#ifdef HAVE_GIO_WITH_NETMON
static void
get_connectivity (GrlRegistry          *registry,
                  GNetworkConnectivity *connectivity,
                  gboolean             *network_available)
{
  g_assert (connectivity != NULL);
  g_assert (network_available != NULL);

  if (g_getenv("GRL_NET_MOCKED") != NULL) {
    GRL_DEBUG ("Mocked network, assuming network is available and connectivity "
               "level is FULL");
    *connectivity = G_NETWORK_CONNECTIVITY_FULL;
    *network_available = TRUE;
  } else {
    g_object_get (G_OBJECT (registry->priv->netmon),
                  "connectivity", connectivity,
                  "network-available", network_available,
                  NULL);

    GRL_DEBUG ("Connectivity level is %d, Network is %s",
               *connectivity, *network_available ? "available" : "unavailable");
  }
}

static void
network_changed_cb (GObject     *gobject,
                    GParamSpec  *pspec,
                    GrlRegistry *registry)
{
  GNetworkConnectivity connectivity;
  gboolean network_available;
  GHashTableIter iter;
  GrlSource *current_source;

  GRL_DEBUG ("Network availability changed");
  get_connectivity (registry, &connectivity, &network_available);

  if (!network_available) {
    g_hash_table_iter_init (&iter, registry->priv->sources);

    while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &current_source)) {
      const char **tags = grl_source_get_tags (current_source);

      if (!tags)
        continue;

      if ((g_strv_contains (tags, LOCAL_NET_TAG) ||
           g_strv_contains (tags, INTERNET_NET_TAG)) &&
          !SOURCE_IS_INVISIBLE(current_source)) {
        GRL_DEBUG ("Network isn't available for '%s', hiding",
                   grl_source_get_id (current_source));
        SET_INVISIBLE_SOURCE(current_source, TRUE);
        g_signal_emit (registry, registry_signals[SIG_SOURCE_REMOVED], 0, current_source);
      }
    }
  } else {
    g_hash_table_iter_init (&iter, registry->priv->sources);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &current_source)) {
      const char **tags = grl_source_get_tags (current_source);

      if (!tags)
        continue;

      if (g_strv_contains (tags, LOCAL_NET_TAG) &&
          SOURCE_IS_INVISIBLE(current_source)) {
        GRL_DEBUG ("Local network became available for '%s', showing",
                   grl_source_get_id (current_source));
        SET_INVISIBLE_SOURCE(current_source, FALSE);
        g_signal_emit (registry, registry_signals[SIG_SOURCE_ADDED], 0, current_source);
      }

      if (g_strv_contains (tags, INTERNET_NET_TAG) &&
          connectivity == G_NETWORK_CONNECTIVITY_FULL &&
          SOURCE_IS_INVISIBLE(current_source)) {
        GRL_DEBUG ("Internet became available for '%s', showing",
                   grl_source_get_id (current_source));
        SET_INVISIBLE_SOURCE(current_source, FALSE);
        g_signal_emit (registry, registry_signals[SIG_SOURCE_ADDED], 0, current_source);
      }

      if (g_strv_contains (tags, INTERNET_NET_TAG) &&
          connectivity != G_NETWORK_CONNECTIVITY_FULL &&
          !SOURCE_IS_INVISIBLE(current_source)) {
        GRL_DEBUG ("Internet became unavailable for '%s', hiding",
                   grl_source_get_id (current_source));
        SET_INVISIBLE_SOURCE(current_source, TRUE);
        g_signal_emit (registry, registry_signals[SIG_SOURCE_REMOVED], 0, current_source);
      }
    }
  }
}
#endif /* HAVE_GIO_WITH_NETMON */

static void
grl_registry_init (GrlRegistry *registry)
{
  registry->priv = GRL_REGISTRY_GET_PRIVATE (registry);

  registry->priv->configs =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) configs_free);
  registry->priv->plugins =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
  registry->priv->sources =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  registry->priv->related_keys =
    g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
  registry->priv->system_keys =
    g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_param_spec_unref);

#ifdef HAVE_GIO_WITH_NETMON
  registry->priv->netmon = g_network_monitor_get_default ();
  g_signal_connect (G_OBJECT (registry->priv->netmon), "notify::connectivity",
                    G_CALLBACK (network_changed_cb), registry);
  g_signal_connect (G_OBJECT (registry->priv->netmon), "notify::network-available",
                    G_CALLBACK (network_changed_cb), registry);
#endif

  key_id_handler_init (&registry->priv->key_id_handler);

  grl_registry_setup_ranks (registry);
}

/* ================ Utitilies ================ */

static void
configs_free (GList *configs)
{
  g_list_free_full (configs, g_object_unref);
}

#ifdef HAVE_GIO_WITH_NETMON
static void
update_source_visibility (GrlRegistry *registry,
                          GrlSource   *source)
{
  GNetworkConnectivity connectivity;
  gboolean network_available;
  const char **tags;
  gboolean needs_local, needs_inet;

  tags = grl_source_get_tags (source);
  if (!tags)
    return;

  needs_local = g_strv_contains (tags, LOCAL_NET_TAG);
  needs_inet = g_strv_contains (tags, INTERNET_NET_TAG);

  if (!needs_local &&
      !needs_inet)
    return;

  get_connectivity (registry, &connectivity, &network_available);

  GRL_DEBUG ("Source %s needs %s %s%s",
             grl_source_get_id (source),
             needs_local ? "local network" : "",
             needs_inet && needs_local ? " and " : "",
             needs_inet ? "Internet" : "");

  if (!network_available) {
    if (needs_local || needs_inet) {
      GRL_DEBUG ("Network isn't available for '%s', hiding",
                 grl_source_get_id (source));
      SET_INVISIBLE_SOURCE(source, TRUE);
    }
  } else {
    if (connectivity != G_NETWORK_CONNECTIVITY_FULL) {
      if (needs_inet) {
        GRL_DEBUG ("Internet isn't available for '%s', hiding",
                   grl_source_get_id (source));
        SET_INVISIBLE_SOURCE(source, TRUE);
      }
    }
  }
}
#endif /* HAVE_GIO_WITH_NETMON */

static void
config_source_rank (GrlRegistry *registry,
                    const gchar *source_id,
                    gint rank)
{
  GRL_DEBUG ("Rank configuration, '%s:%d'", source_id, rank);
  g_hash_table_insert (registry->priv->ranks,
                       g_strdup (source_id),
                       GINT_TO_POINTER (rank));
}

static void
set_source_rank (GrlRegistry *registry, GrlSource *source)
{
  gint rank;

  rank =
    GPOINTER_TO_INT (g_hash_table_lookup (registry->priv->ranks,
                                          grl_source_get_id (source)));
  if (!rank) {
    rank = GRL_RANK_DEFAULT;
  }
  g_object_set (source, "rank", rank, NULL);
  GRL_DEBUG ("Source rank '%s' : %d", grl_source_get_id (source), rank);
}

static void
grl_registry_setup_ranks (GrlRegistry *registry)
{
  const gchar *ranks_env;
  gchar **rank_specs;
  gchar **iter;

  registry->priv->ranks = g_hash_table_new_full (g_str_hash, g_str_equal,
						 g_free, NULL);

  ranks_env = g_getenv (GRL_PLUGIN_RANKS_VAR);
  if (!ranks_env) {
    GRL_DEBUG ("$%s is not set, using default ranks.", GRL_PLUGIN_RANKS_VAR);
    return;
  }

  rank_specs = g_strsplit (ranks_env, ",", 0);
  iter = rank_specs;

  while (*iter) {
    gchar **rank_info = g_strsplit (*iter, ":", 2);
    if (rank_info[0] && rank_info[1]) {
      gchar *tmp;
      gchar *id = rank_info[0];
      gchar *srank = rank_info[1];
      gint rank = (gint) g_ascii_strtoll (srank, &tmp, 10);
      if (*tmp != '\0') {
        GRL_WARNING ("Incorrect ranking definition: '%s'. Skipping...", *iter);
      } else {
        config_source_rank (registry, id, rank);
      }
    } else {
      GRL_WARNING ("Incorrect ranking definition: '%s'. Skipping...", *iter);
    }
    g_strfreev (rank_info);
    iter++;
  }

  g_strfreev (rank_specs);
}

static gint
compare_by_rank (gconstpointer a,
                 gconstpointer b) {
  gint rank_a;
  gint rank_b;

  rank_a = grl_source_get_rank (GRL_SOURCE (a));
  rank_b = grl_source_get_rank (GRL_SOURCE (b));

  return (rank_a < rank_b) - (rank_a > rank_b);
}

static GHashTable *
get_info_from_plugin_xml (const gchar *xml_path)
{
  GHashTable *hash_table;
  xmlNodePtr node, info_node, child;
  xmlDocPtr doc_ptr = xmlReadFile (xml_path,
				   NULL,
				   XML_PARSE_RECOVER | XML_PARSE_NOBLANKS |
				   XML_PARSE_NOWARNING | XML_PARSE_NOERROR);
  if (!doc_ptr) {
    GRL_MESSAGE ("Could not read XML file under the location: %s", xml_path);
    return NULL;
  }

  node = xmlDocGetRootElement (doc_ptr);
  if (!node || g_strcmp0 ((gchar *) node->name, XML_ROOT_ELEMENT_NAME)) {
    GRL_WARNING ("%s did not have a %s root element.",
                 xml_path,
                 XML_ROOT_ELEMENT_NAME);
    xmlFreeDoc (doc_ptr);
    return NULL;
  }

  /* Get the info node */
  info_node = node->children;
  while (info_node) {
    if (g_strcmp0 ((gchar *) info_node->name, "info") == 0) {
      break;
    }
    info_node = info_node->next;
  }
  if (!info_node) {
    xmlFreeDoc (doc_ptr);
    return NULL;
  }

  /* Populate the hash table with the XML's info*/
  hash_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  child = info_node->children;
  while (child) {
    g_hash_table_insert (hash_table,
			 g_strdup ((gchar *) child->name),
			 (gchar *) xmlNodeGetContent (child));
    child = child->next;
  }
  xmlFreeDoc (doc_ptr);

  return hash_table;
}

static gboolean
register_keys_plugin (GrlRegistry *registry,
                      GrlPlugin *plugin,
                      GError **error)
{
  gboolean is_loaded;

  /* Check if plugin is already loaded */
  g_object_get (plugin, "loaded", &is_loaded, NULL);
  if (is_loaded) {
    GRL_WARNING ("Plugin is already loaded: '%s'", grl_plugin_get_id (plugin));
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Plugin '%s' is already loaded"), grl_plugin_get_id (plugin));
    return FALSE;
  }

  grl_plugin_register_keys (plugin);

  return TRUE;
}

static gboolean
activate_plugin (GrlRegistry *registry,
                 GrlPlugin *plugin,
                 GError **error)
{
  GList *plugin_configs;

  plugin_configs = g_hash_table_lookup (registry->priv->configs,
                                        grl_plugin_get_id (plugin));

  if (!grl_plugin_load (plugin, plugin_configs)) {
    GRL_DEBUG ("Failed to initialize plugin from %s. Check if plugin is well configured", grl_plugin_get_filename (plugin));
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Failed to initialize plugin from %s"), grl_plugin_get_filename (plugin));
    shutdown_plugin (plugin);
    return FALSE;
  }

  GRL_DEBUG ("Loaded plugin '%s' from '%s'",
             grl_plugin_get_id (plugin),
             grl_plugin_get_filename (plugin));

  return TRUE;
}

static GrlPlugin *
grl_registry_preload_plugin (GrlRegistry *registry,
                             const gchar *dirname,
                             const gchar *plugin_info_filename)
{
  GHashTable *info;
  GrlPlugin *plugin = NULL;
  gchar *file;
  gchar *suffix;
  gchar *id;
  gchar *module_name;
  gchar *module_filename;
  gchar *module_fullpathname;

  if ((suffix = g_strrstr (plugin_info_filename, "." GRL_PLUGIN_INFO_SUFFIX)) == NULL) {
    return NULL;
  }

  id = g_strndup (plugin_info_filename, suffix - plugin_info_filename);
  /* Check if plugin is already preloaded */
  if (g_hash_table_lookup (registry->priv->plugins, id)) {
    GRL_DEBUG ("Plugin '%s' already preloaded; skipping", id);
    g_free (id);
    return NULL;
  }

  /* Check if plugin is allowed */
  if (registry->priv->allowed_plugins &&
      !g_slist_find_custom (registry->priv->allowed_plugins,
                            id,
                            (GCompareFunc) g_strcmp0)) {
    GRL_DEBUG ("Plugin '%s' not allowed; skipping", id);
    g_free (id);
    return NULL;
  }

  file = g_build_filename (dirname, plugin_info_filename, NULL);
  info = get_info_from_plugin_xml (file);
  g_free (file);

  if (info) {
    plugin = g_object_new (GRL_TYPE_PLUGIN, NULL);
    grl_plugin_set_id (plugin, id);
    grl_plugin_set_optional_info (plugin, info);
    module_name = grl_plugin_get_info (plugin, GRL_PLUGIN_INFO_MODULE);
    if (!module_name) {
      GRL_WARNING ("Unknown module file for plugin with id '%s'", id);
      g_object_unref (plugin);
      g_free (id);
      return NULL;
    }

    if (g_path_is_absolute (module_name)) {
      grl_plugin_set_filename (plugin, module_name);
    } else {
#ifdef __CYGWIN__
      if (g_str_has_prefix (module_name, "lib")) {
        module_name[0] = 'c';
        module_name[1] = 'y';
        module_name[2] = 'g';
      }
#endif
      if (g_str_has_suffix (module_name, "." G_MODULE_SUFFIX)) {
        module_fullpathname = g_build_filename (dirname, module_name, NULL);
      } else {
        module_filename = g_strconcat (module_name, "." G_MODULE_SUFFIX, NULL);
        module_fullpathname = g_build_filename (dirname, module_filename, NULL);
        g_free (module_filename);
      }
      grl_plugin_set_filename (plugin, module_fullpathname);
      g_free (module_fullpathname);
    }

    g_hash_table_insert (registry->priv->plugins,
                         id,
                         plugin);
  }
  return plugin;
}

static void
grl_registry_preload_plugins_directory (GrlRegistry *registry,
                                        const gchar *directory,
                                        GList **plugins_loaded)
{
  GDir *dir;
  GError *error = NULL;
  const gchar *entry;
  GrlPlugin *plugin;

  dir = g_dir_open (directory, 0, &error);
  if (!dir) {
    GRL_WARNING ("Could not open plugins' info directory '%s': %s",
                 directory,
                 error->message);
    g_error_free (error);
    return;
  }

  while ((entry = g_dir_read_name (dir)) != NULL) {
    plugin = grl_registry_preload_plugin (registry, directory, entry);
    if (plugins_loaded && plugin) {
      *plugins_loaded = g_list_prepend (*plugins_loaded, plugin);
    }
  }

  g_dir_close (dir);
}

static void
grl_registry_preload_plugins (GrlRegistry *registry,
                              GList **plugins_loaded)
{
  GSList *plugin_dir;
  GList *plugins_directory_loaded = NULL;

  for (plugin_dir = registry->priv->plugins_dir;
       plugin_dir;
       plugin_dir = g_slist_next (plugin_dir)) {
    if (plugins_loaded) {
      grl_registry_preload_plugins_directory (registry,
                                              plugin_dir->data,
                                              &plugins_directory_loaded);
      *plugins_loaded = g_list_concat (*plugins_loaded, plugins_directory_loaded);
      plugins_directory_loaded = NULL;
    } else {
      grl_registry_preload_plugins_directory (registry,
                                              plugin_dir->data,
                                              NULL);
    }
  }
}

static gboolean
grl_registry_load_plugin_list (GrlRegistry *registry,
                               GList *plugin_list)
{
  GList *l;
  gboolean loaded_one = FALSE;

  for (l = plugin_list; l ; l = l->next) {
    GrlPlugin *plugin = l->data;
    if (grl_plugin_get_module (plugin)) {
      loaded_one |= register_keys_plugin (registry, plugin, NULL);
    } else {
      loaded_one |= (grl_registry_prepare_plugin (registry,
                                                  grl_plugin_get_filename (plugin),
                                                  NULL) != NULL);
      loaded_one |= register_keys_plugin (registry, plugin, NULL);
    }
  }

  for (l = plugin_list; l ; l = l->next) {
    GrlPlugin *plugin = l->data;
    loaded_one |= activate_plugin (registry, plugin, NULL);
  }

  return loaded_one;
}

static GrlKeyID
grl_registry_register_metadata_key_full (GrlRegistry *registry,
                                         GParamSpec *param_spec,
                                         GrlKeyID key,
                                         GError **error)
{
  const gchar *key_name;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), 0);
  g_return_val_if_fail (G_IS_PARAM_SPEC (param_spec), 0);
  GrlKeyID registered_key;

  key_name = g_param_spec_get_name (param_spec);
  registered_key = key_id_handler_get_key (&registry->priv->key_id_handler, key_name);
  if (registered_key != GRL_METADATA_KEY_INVALID) {
    GParamSpec *key_spec = g_hash_table_lookup (registry->priv->system_keys,
                                                (gpointer) key_name);
    if (param_spec_is_equal (key_spec, param_spec)) {
      /* Key registered */
      GRL_DEBUG ("metadata key '%s' already registered with same spec", key_name);
      g_param_spec_unref (param_spec);
      return registered_key;
    } else {
      GRL_WARNING ("metadata key '%s' already exists", key_name);
      g_set_error (error,
                   GRL_CORE_ERROR,
                   GRL_CORE_ERROR_REGISTER_METADATA_KEY_FAILED,
                   _("Metadata key '%s' already registered in different format"),
                   key_name);
      return GRL_METADATA_KEY_INVALID;
    }
  }

  registered_key = key_id_handler_add (&registry->priv->key_id_handler, key, key_name);

  if (registered_key == GRL_METADATA_KEY_INVALID) {
    GRL_WARNING ("metadata key '%s' cannot be registered", key_name);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_REGISTER_METADATA_KEY_FAILED,
                 _("Metadata key '%s' cannot be registered"),
                 key_name);

    return GRL_METADATA_KEY_INVALID;
  }

  g_hash_table_insert (registry->priv->system_keys,
                       (gpointer) key_name,
                       param_spec);

  /* Each key is related to itself */
  g_hash_table_insert (registry->priv->related_keys,
                       GRLKEYID_TO_POINTER (registered_key),
                       g_list_prepend (NULL,
                                       GRLKEYID_TO_POINTER (registered_key)));

  return registered_key;
}

static void
key_id_handler_init (struct KeyIDHandler *handler)
{
  const gchar *null_string = NULL;
  handler->string_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  handler->id_to_string = g_array_new (FALSE, /* zero terminated */
                                       TRUE,  /* zero-initialised */
                                       sizeof (const gchar *));
  /* We want indices in ->id_to_string to start from 1, so we add a NULL entry
   * for GRL_METADATA_KEY_INVALID (i.e. 0) */
  g_array_insert_val (handler->id_to_string,
                      GRL_METADATA_KEY_INVALID,
                      null_string);
}

static void
key_id_handler_free (struct KeyIDHandler *handler)
{
  g_hash_table_unref (handler->string_to_id);
  g_array_unref (handler->id_to_string);
}

static
GrlKeyID key_id_handler_get_key (struct KeyIDHandler *handler, const gchar *key_name)
{
  gpointer val = g_hash_table_lookup (handler->string_to_id, key_name);
  if (val == NULL)
    return GRL_METADATA_KEY_INVALID;

  return GRLPOINTER_TO_KEYID (val);
}

static const gchar *
key_id_handler_get_name (struct KeyIDHandler *handler, GrlKeyID key)
{
  if (key < handler->id_to_string->len)
    return g_array_index (handler->id_to_string, const gchar *, key);

  return NULL;
}

/*
 * key_id_handler_add:
 * @handler: the handler
 * @key: a specific key for system keys, or GRL_METADATA_KEY_INVALID for it to
 * be assigned
 * @name: the name of the key.
 *
 * Add a new key<->name correspondence.
 *
 * Returns: the new key number, or GRL_METADATA_KEY_INVALID if the key could
 * not be created (typically if @key or @name is already registered).
 */
static GrlKeyID
key_id_handler_add (struct KeyIDHandler *handler, GrlKeyID key, const gchar *name)
{
  GrlKeyID _key = key;

  if (_key == GRL_METADATA_KEY_INVALID) {
    /* existing keys go from 1 to (id_to_string->len - 1), so the next
     * available key is id_to_string->len, which will be incremented by
     * g_array_insert_val() */
    _key = handler->id_to_string->len;
  }

  if (NULL != key_id_handler_get_name (handler, _key)) {
    GRL_WARNING ("Cannot register %d:%s because key is already defined as %s",
                 _key, name, key_id_handler_get_name (handler, _key));
    return GRL_METADATA_KEY_INVALID;
  } else if ( GRL_METADATA_KEY_INVALID != key_id_handler_get_key (handler, name)) {
    /* _key or name is already in use! */
    GRL_WARNING ("Cannot register %d:%s because name is already registered with key %d",
                 _key, name, key_id_handler_get_key (handler, name));
    return GRL_METADATA_KEY_INVALID;
  } else {
    /* name_copy is shared between handler->id_to_string and
     * handler->string_to_id */
    gchar *name_copy = g_strdup (name);

    if (_key >= handler->id_to_string->len)
      g_array_set_size (handler->id_to_string, _key + 1);

    /* yes, g_array_index() is a macro that give you an lvalue */
    g_array_index (handler->id_to_string, const gchar *, _key) = name_copy;
    g_hash_table_insert (handler->string_to_id,
                         name_copy, GRLKEYID_TO_POINTER (_key));
  }

  return _key;

}

static GList *
key_id_handler_get_all_keys (struct KeyIDHandler *handler)
{
  return g_hash_table_get_values (handler->string_to_id);
}

#define CHECK_NUMERIC_PARAM_SPEC_LIMITS(is_type, cast_type, a, b) {     \
  if (is_type) {                                                        \
    if ((cast_type(a))->maximum != (cast_type(b))->maximum ||           \
        (cast_type(a))->minimum != (cast_type(b))->minimum ||           \
        (cast_type(a))->default_value != (cast_type(b))->default_value) \
      return FALSE;                                                     \
    return TRUE;                                                        \
  }                                                                     \
}

/* @curr: The current spec we have
 * @new: The spec to match
 *
 * Returns: true if specs are the same, false otherwise.
 */
static gboolean
param_spec_is_equal (GParamSpec *cur,
                     GParamSpec *new)
{
  GType ctype = G_PARAM_SPEC_TYPE (cur);

  if (ctype != G_PARAM_SPEC_TYPE (new))
    return FALSE;

  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_INT),
                                   G_PARAM_SPEC_INT, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_LONG),
                                   G_PARAM_SPEC_LONG, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_INT64),
                                   G_PARAM_SPEC_INT64, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_CHAR),
                                   G_PARAM_SPEC_CHAR, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_UINT),
                                   G_PARAM_SPEC_UINT, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_ULONG),
                                   G_PARAM_SPEC_ULONG, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_UINT64),
                                   G_PARAM_SPEC_UINT64, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_UCHAR),
                                   G_PARAM_SPEC_UCHAR, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_FLOAT),
                                   G_PARAM_SPEC_FLOAT, cur, new);
  CHECK_NUMERIC_PARAM_SPEC_LIMITS ((ctype == G_TYPE_PARAM_DOUBLE),
                                   G_PARAM_SPEC_DOUBLE, cur, new);
  if (ctype == G_TYPE_PARAM_STRING) {
    GParamSpecString *c = G_PARAM_SPEC_STRING (cur);
    GParamSpecString *n = G_PARAM_SPEC_STRING (new);
    return (g_strcmp0 (c->default_value, n->default_value) == 0);
  } else if (ctype == G_TYPE_PARAM_ENUM) {
    GParamSpecEnum *c = G_PARAM_SPEC_ENUM (cur);
    GParamSpecEnum *n = G_PARAM_SPEC_ENUM (new);
    if (c->default_value != n->default_value ||
        cur->value_type != new->value_type) {
      GRL_DEBUG ("%s differ (values: %d and %d) (types: %s and %s)",
                 g_type_name (ctype), c->default_value, n->default_value,
                 g_type_name (cur->value_type), g_type_name (new->value_type));
      return FALSE;
    }
  } else if (ctype == G_TYPE_PARAM_FLAGS) {
    GParamSpecFlags *c = G_PARAM_SPEC_FLAGS (cur);
    GParamSpecFlags *n = G_PARAM_SPEC_FLAGS (new);
    if (c->default_value != n->default_value ||
        cur->value_type != new->value_type) {
      GRL_DEBUG ("%s differ (values: %d and %d) (types: %s and %s)",
                 g_type_name (ctype), c->default_value, n->default_value,
                 g_type_name (cur->value_type), g_type_name (new->value_type));
      return FALSE;
    }
  } else if (ctype == G_TYPE_PARAM_BOOLEAN) {
    GParamSpecBoolean *c = G_PARAM_SPEC_BOOLEAN (cur);
    GParamSpecBoolean *n = G_PARAM_SPEC_BOOLEAN (new);
    if (c->default_value != n->default_value) {
      GRL_DEBUG ("%s type differ: %s != %s", g_type_name (ctype),
                 g_type_name (cur->value_type), g_type_name (new->value_type));
      return FALSE;
    }
  } else if (ctype == G_TYPE_PARAM_BOXED || ctype == G_TYPE_PARAM_OBJECT) {
    if (cur->value_type != new->value_type) {
      GRL_DEBUG ("%s type differ: %s != %s", g_type_name (ctype),
                 g_type_name (cur->value_type), g_type_name (new->value_type));
      return FALSE;
    }
  } else {
    g_warn_if_reached();
    return FALSE;
  }

  return TRUE;
}

#undef CHECK_NUMERIC_PARAM_SPEC_LIMITS

static void
shutdown_plugin (GrlPlugin *plugin)
{
  GRL_DEBUG ("Unloading plugin '%s'", grl_plugin_get_id (plugin));
  grl_plugin_unload (plugin);

  if (grl_plugin_get_module (plugin)) {
    g_module_close (grl_plugin_get_module (plugin));
    grl_plugin_set_module (plugin, NULL);
  }
}

/* ================ PRIVATE API ================ */

/*
 * grl_registry_restrict_plugins:
 * @registry: the registry instance
 * @plugins: a @NULL-terminated array of plugins identifiers
 *
 * Restrict the plugins that application sees to this list.
 *
 * Other plugins will not be available for the application, unless it uses
 * directly #grl_registry_load_plugin() function.
 **/
void
grl_registry_restrict_plugins (GrlRegistry *registry,
                               gchar **plugins)
{
  g_return_if_fail (GRL_IS_REGISTRY (registry));
  g_return_if_fail (plugins);

  /* Free previous list */
  if (registry->priv->allowed_plugins) {
    g_slist_free_full (registry->priv->allowed_plugins, g_free);
    registry->priv->allowed_plugins = NULL;
  }

  while (*plugins) {
    registry->priv->allowed_plugins = g_slist_prepend (registry->priv->allowed_plugins,
                                                       g_strdup (*plugins));
    plugins++;
  }
}

/*
 * grl_registry_shutdown:
 * @registry: the registry instance
 *
 * Frees all the resources in the registry and the registry itself.
 **/
void
grl_registry_shutdown (GrlRegistry *registry)
{
  GHashTableIter iter;
  GList *each_key;
  GList *related_keys = NULL;
  GrlPlugin *plugin = NULL;
  GrlSource *source = NULL;

  if (registry->priv->plugins) {
    g_hash_table_iter_init (&iter, registry->priv->plugins);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &plugin)) {
      shutdown_plugin (plugin);
    }
    g_clear_pointer (&registry->priv->plugins, g_hash_table_unref);
  }

  if (registry->priv->sources) {
    g_hash_table_iter_init (&iter, registry->priv->sources);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &source)) {
      g_object_unref (source);
    }
    g_clear_pointer (&registry->priv->sources, g_hash_table_unref);
  }

  g_clear_pointer (&registry->priv->ranks, g_hash_table_unref);
  g_clear_pointer (&registry->priv->configs, g_hash_table_unref);

  /* We need to free this table with care. Several keys can be pointing to the
     same value, so we need to ensure that we only free the value once */
  if (registry->priv->related_keys) {
    while (TRUE) {
      g_hash_table_iter_init (&iter, registry->priv->related_keys);
      if (!g_hash_table_iter_next (&iter, NULL, (gpointer *) &related_keys)) {
        break;
      }
      /* This will invalidate the iterator */
      for (each_key = related_keys; each_key; each_key = g_list_next (each_key)) {
        g_hash_table_remove (registry->priv->related_keys, GRLKEYID_TO_POINTER (each_key->data));
      }
      g_list_free (related_keys);
    }
    g_clear_pointer (&registry->priv->related_keys, g_hash_table_unref);
  }

  g_slist_free_full (registry->priv->plugins_dir, (GDestroyNotify) g_free);
  g_slist_free_full (registry->priv->allowed_plugins, (GDestroyNotify) g_free);

  key_id_handler_free (&registry->priv->key_id_handler);
  g_clear_pointer (&registry->priv->system_keys, g_hash_table_unref);

  g_object_unref (registry);
}

/* ================ PUBLIC API ================ */

/**
 * grl_registry_get_default:
 *
 * As the registry is designed to work as a singleton, this
 * method is in charge of creating the only instance or
 * returned it if it is already in memory.
 *
 * Returns: (transfer none): a new or an already created instance of the registry.
 *
 * It is NOT MT-safe
 *
 * Since: 0.2.0
 */
GrlRegistry *
grl_registry_get_default (void)
{
  static GrlRegistry *registry = NULL;

  if (!registry) {
    registry = g_object_new (GRL_TYPE_REGISTRY, NULL);
    g_object_add_weak_pointer (G_OBJECT (registry), (gpointer *) &registry);
  }

  return registry;
}

/**
 * grl_registry_register_source:
 * @registry: the registry instance
 * @plugin: the plugin which owns the source
 * @source: (transfer full): the source to register
 * @error: error return location or @NULL to ignore
 *
 * Register a @source in the @registry with the given @plugin information
 *
 * Returns: %TRUE if success, %FALSE% otherwise.
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_register_source (GrlRegistry *registry,
                              GrlPlugin *plugin,
                              GrlSource *source,
                              GError **error)
{
  gchar *id;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (GRL_IS_PLUGIN (plugin), FALSE);
  g_return_val_if_fail (GRL_IS_SOURCE (source), FALSE);

  g_object_get (source, "source-id", &id, NULL);
  GRL_DEBUG ("New source available: '%s'", id);

  /* Take ownership of the source */
  g_object_ref_sink (source);
  g_object_unref (source);

  /* Do not free id, since g_hash_table_insert does not copy,
     it will be freed when removed from the hash table */
  g_hash_table_insert (registry->priv->sources, id, source);

  /* Set the plugin as owner of source */
  g_object_set (source, "plugin", plugin, NULL);

  /* Set source rank */
  set_source_rank (registry, source);

#ifdef HAVE_GIO_WITH_NETMON
  /* Update whether it should be invisible */
  update_source_visibility (registry, source);
#endif

  if (!SOURCE_IS_INVISIBLE(source))
    g_signal_emit (registry, registry_signals[SIG_SOURCE_ADDED], 0, source);

  return TRUE;
}

/**
 * grl_registry_unregister_source:
 * @registry: the registry instance
 * @source: the source to unregister
 * @error: error return location or @NULL to ignore
 *
 * Removes the @source from the @registry hash table
 *
 * Returns: %TRUE if success, %FALSE% otherwise.
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_unregister_source (GrlRegistry *registry,
                                GrlSource *source,
                                GError **error)
{
  gchar *id;
  gboolean ret = TRUE;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (GRL_IS_SOURCE (source), FALSE);

  g_object_get (source, "source-id", &id, NULL);
  GRL_DEBUG ("Unregistering source '%s'", id);

  if (g_hash_table_remove (registry->priv->sources, id)) {
    GRL_DEBUG ("source '%s' is no longer available", id);
    g_signal_emit (registry, registry_signals[SIG_SOURCE_REMOVED], 0, source);
    g_object_unref (source);
  } else {
    GRL_WARNING ("source '%s' not found", id);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_UNREGISTER_SOURCE_FAILED,
                 _("Source with id '%s' was not found"), id);
    ret = FALSE;
  }

  g_free (id);
  return ret;
}

/**
 * grl_registry_add_directory:
 * @registry: the registry instance
 * @path: a path with plugins
 *
 * Set this path as part of default paths to load plugins.
 *
 * Since: 0.2.0
 **/
void
grl_registry_add_directory (GrlRegistry *registry,
                            const gchar *path)
{
  g_return_if_fail (GRL_IS_REGISTRY (registry));
  g_return_if_fail (path);

  /* Use append instead of prepend so plugins are loaded in the same order as
     they were added */
  registry->priv->plugins_dir = g_slist_append (registry->priv->plugins_dir,
                                                g_strdup (path));
  registry->priv->all_plugins_preloaded = FALSE;
}

static GrlPlugin *
grl_registry_prepare_plugin (GrlRegistry *registry,
                             const gchar *library_filename,
                             GError **error)
{
  GModule *module;
  GrlPluginDescriptor *plugin_desc;
  GrlPlugin *plugin;
  gchar *module_name;
  gchar *info_dirname;
  gchar *info_filename;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);

  module = g_module_open (library_filename, G_MODULE_BIND_LOCAL);
  if (!module) {
    GRL_WARNING ("Failed to open module: %s", g_module_error ());
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Failed to load plugin from %s"), library_filename);
    return NULL;
  }

  if (!g_module_symbol (module, "GRL_PLUGIN_DESCRIPTOR", (gpointer) &plugin_desc)) {
    GRL_WARNING ("Plugin descriptor not found in '%s'", library_filename);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Invalid plugin file %s"), library_filename);
    g_module_close (module);
    return NULL;
  }

  if (!plugin_desc->plugin_init ||
      !plugin_desc->plugin_id) {
    GRL_WARNING ("Plugin descriptor is not valid: '%s'", library_filename);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("'%s' is not a valid plugin file"), library_filename);
    g_module_close (module);
    return NULL;
  }

  /* Check if plugin is preloaded; if not, then create one */
  plugin = g_hash_table_lookup (registry->priv->plugins,
                                plugin_desc->plugin_id);

  if (!plugin) {
    info_dirname = g_path_get_dirname (library_filename);
    info_filename = g_strconcat (plugin_desc->plugin_id, "." GRL_PLUGIN_INFO_SUFFIX, NULL);
    plugin = grl_registry_preload_plugin (registry, info_dirname, info_filename);
    g_free (info_dirname);
    g_free (info_filename);
    if (!plugin) {
      g_set_error (error,
                   GRL_CORE_ERROR,
                   GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                   _("Unable to load plugin '%s'"), plugin_desc->plugin_id);
      g_module_close (module);
      return NULL;
    }
  } else {
    /* Check if the existent plugin is for a different module */
    if (g_strcmp0 (grl_plugin_get_filename (plugin), library_filename) != 0) {
      GRL_WARNING ("Plugin '%s' already exists", library_filename);
      g_set_error (error,
                   GRL_CORE_ERROR,
                   GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                   _("Plugin '%s' already exists"), library_filename);
      return NULL;
    }
  }

  if (!grl_plugin_get_module (plugin)) {
    grl_plugin_set_load_func (plugin, plugin_desc->plugin_init);
    grl_plugin_set_unload_func (plugin, plugin_desc->plugin_deinit);
    grl_plugin_set_register_keys_func (plugin, plugin_desc->plugin_register_keys);

    /* Insert module name as part of plugin information */
    module_name = g_path_get_basename (library_filename);
    grl_plugin_set_info (plugin, GRL_PLUGIN_INFO_MODULE, module_name);
    g_free (module_name);

    grl_plugin_set_module (plugin, module);

    /* Make plugin resident */
    g_module_make_resident (module);
  }

  return plugin;
}

/**
 * grl_registry_load_plugin:
 * @registry: the registry instance
 * @library_filename: the path to the so file
 * @error: error return location or @NULL to ignore
 *
 * Loads a module from shared object file stored in @path
 *
 * Returns: %TRUE if the module is loaded correctly
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_load_plugin (GrlRegistry *registry,
                          const gchar *library_filename,
                          GError **error)
{
  GrlPlugin *plugin;

  plugin = grl_registry_prepare_plugin (registry, library_filename, error);
  if (!plugin)
    return FALSE;

  return register_keys_plugin (registry, plugin, error) &&
         activate_plugin (registry, plugin, error);
}

/**
 * grl_registry_load_plugin_directory:
 * @registry: the registry instance
 * @path: the path to the directory
 * @error: error return location or @NULL to ignore
 *
 * Loads a set of modules from directory in @path which contains
 * a group shared object files.
 *
 * Returns: %TRUE if the directory is valid.
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_load_plugin_directory (GrlRegistry *registry,
                                    const gchar *path,
                                    GError **error)
{
  GList *preloaded_plugins = NULL;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);

  /* Preload plugins */
  grl_registry_preload_plugins_directory (registry, path, &preloaded_plugins);

  /* Load the plugins */
  if (!grl_registry_load_plugin_list (registry, preloaded_plugins)) {
    GRL_WARNING ("No plugins loaded from directory '%s'", path);
  }
  g_list_free (preloaded_plugins);

  return TRUE;
}

/**
 * grl_registry_load_all_plugins:
 * @registry: the registry instance
 * @error: error return location or @NULL to ignore
 *
 * Load all the modules available in the default directory path.
 *
 * The default directory path can be changed through the environment
 * variable %GRL_PLUGIN_PATH and it can contain several paths separated
 * by ":"
 *
 * Returns: %FALSE% is all the configured plugin paths are invalid,
 * %TRUE% otherwise.
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_load_all_plugins (GrlRegistry *registry, GError **error)
{
  GList *all_plugins;
  gboolean loaded_one;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), TRUE);

  /* Preload all plugins */
  if (!registry->priv->all_plugins_preloaded) {
    grl_registry_preload_plugins (registry, NULL);
    registry->priv->all_plugins_preloaded = TRUE;
  }

  /* Now load all plugins */
  all_plugins = g_hash_table_get_values (registry->priv->plugins);
  loaded_one = grl_registry_load_plugin_list (registry, all_plugins);

  g_list_free (all_plugins);

  if (!loaded_one) {
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("All configured plugin paths are invalid"));
  }

  return loaded_one;
}

/**
 * grl_registry_load_plugin_by_id:
 * @registry: the registry instance
 * @plugin_id: plugin identifier
 * @error: error return location or @NULL to ignore
 *
 * Loads plugin identified by @plugin_id.
 *
 * This requires the XML plugin information file to define a "module" key with
 * the name of the module that provides the plugin or the absolute path of the
 * actual module file.
 *
 * Returns: %TRUE if the plugin is loaded correctly
 *
 * Since: 0.2.0
 **/
gboolean
grl_registry_load_plugin_by_id (GrlRegistry *registry,
                                const gchar *plugin_id,
                                GError **error)
{
  GrlPlugin *plugin;
  gboolean is_loaded;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (plugin_id, FALSE);


  /* Preload all plugins */
  if (!registry->priv->all_plugins_preloaded) {
    grl_registry_preload_plugins (registry, NULL);
    registry->priv->all_plugins_preloaded = FALSE;
  }

  /* Check if plugin is available */
  plugin = g_hash_table_lookup (registry->priv->plugins, plugin_id);
  if (!plugin) {
    GRL_WARNING ("Plugin '%s' not available", plugin_id);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Plugin '%s' not available"), plugin_id);
    return FALSE;
  }

  /* Check if plugin is already loaded */
  g_object_get (plugin, "loaded", &is_loaded, NULL);
  if (is_loaded) {
    GRL_WARNING ("Plugin '%s' is already loaded", plugin_id);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_LOAD_PLUGIN_FAILED,
                 _("Plugin '%s' is already loaded"), plugin_id);
    return FALSE;
  }

  /* Load plugin */
  return grl_registry_load_plugin (registry, grl_plugin_get_filename (plugin), error);
}

/**
 * grl_registry_lookup_source:
 * @registry: the registry instance
 * @source_id: the id of a source
 *
 * This function will search and retrieve a source given its identifier.
 *
 * Returns: (transfer none): The source found.
 *
 * Since: 0.2.0
 */
GrlSource *
grl_registry_lookup_source (GrlRegistry *registry,
                            const gchar *source_id)
{
  GrlSource *source;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);
  g_return_val_if_fail (source_id != NULL, NULL);

  source = (GrlSource *) g_hash_table_lookup (registry->priv->sources,
                                              source_id);
  if (source && !SOURCE_IS_INVISIBLE(source))
    return source;
  return NULL;
}

/**
 * grl_registry_get_sources:
 * @registry: the registry instance
 * @ranked: whether the returned list shall be returned ordered by rank
 *
 * This function will return all the available sources in the @registry.
 *
 * If @ranked is %TRUE, the source list will be ordered by rank.
 *
 * Returns: (element-type GrlSource) (transfer container): a #GList of
 * available #GrlSource<!-- -->s. The content of the list should not be
 * modified or freed. Use g_list_free() when done using the list.
 *
 * Since: 0.2.0
 */
GList *
grl_registry_get_sources (GrlRegistry *registry,
                          gboolean ranked)
{
  GHashTableIter iter;
  GList *source_list = NULL;
  GrlSource *current_source;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);

  g_hash_table_iter_init (&iter, registry->priv->sources);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &current_source)) {
    if (!SOURCE_IS_INVISIBLE(current_source))
      source_list = g_list_prepend (source_list, current_source);
  }

  if (ranked) {
    source_list = g_list_sort (source_list, (GCompareFunc) compare_by_rank);
  }

  return source_list;
}

/**
 * grl_registry_get_sources_by_operations:
 * @registry: the registry instance
 * @ops: a bitwise mangle of the requested operations.
 * @ranked: whether the returned list shall be returned ordered by rank
 *
 * Give an array of all the available sources in the @registry capable of
 * perform the operations requested in @ops.
 *
 * If @ranked is %TRUE, the source list will be ordered by rank.
 *
 * Returns: (element-type GrlSource) (transfer container): a #GList of
 * available #GrlSource<!-- -->s. The content of the list should not be
 * modified or freed. Use g_list_free() when done using the list.
 *
 * Since: 0.2.0
 */
GList *
grl_registry_get_sources_by_operations (GrlRegistry *registry,
                                        GrlSupportedOps ops,
                                        gboolean ranked)
{
  GHashTableIter iter;
  GList *source_list = NULL;
  GrlSource *source;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);

  g_hash_table_iter_init (&iter, registry->priv->sources);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &source)) {
    GrlSupportedOps source_ops;
    source_ops =
      grl_source_supported_operations (source);
    if ((source_ops & ops) == ops &&
        !SOURCE_IS_INVISIBLE(source)) {
      source_list = g_list_prepend (source_list, source);
    }
  }

  if (ranked) {
    source_list = g_list_sort (source_list, compare_by_rank);
  }

  return source_list;
}

/**
 * grl_registry_lookup_plugin:
 * @registry: the registry instance
 * @plugin_id: the id of a plugin
 *
 * This function will search and retrieve a plugin given its identifier.
 *
 * Returns: (transfer none): The plugin found
 *
 * Since: 0.2.0
 **/
GrlPlugin *
grl_registry_lookup_plugin (GrlRegistry *registry,
                            const gchar *plugin_id)
{
  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);
  g_return_val_if_fail (plugin_id, NULL);

  return (GrlPlugin *) g_hash_table_lookup (registry->priv->plugins,
                                            plugin_id);
}

/**
 * grl_registry_get_plugins:
 * @registry: the registry instance
 * @only_loaded: whether the returned list shall include only loaded plugins
 *
 * This function will return all the available plugins in the @registry.
 *
 * If @only_loaded is %TRUE, the plugin list will contain only plugins that are
 * loaded.
 *
 * Returns: (element-type GrlPlugin) (transfer container): a #GList of
 * available #GrlPlugin<!-- -->s. The content of the list should not be modified
 * or freed. Use g_list_free() when done using the list.
 *
 * Since: 0.2.0
 **/
GList *
grl_registry_get_plugins (GrlRegistry *registry,
                          gboolean only_loaded)
{
  GList *plugin_list = NULL;
  GHashTableIter iter;
  GrlPlugin *current_plugin;
  gboolean is_loaded;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);

  if (only_loaded) {
    g_hash_table_iter_init (&iter, registry->priv->plugins);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &current_plugin)) {
      g_object_get (current_plugin, "loaded", &is_loaded, NULL);
      if (is_loaded) {
        plugin_list = g_list_prepend (plugin_list, current_plugin);
      }
    }
  } else {
    plugin_list = g_hash_table_get_keys (registry->priv->plugins);
  }

  return plugin_list;
}

/**
 * grl_registry_unload_plugin:
 * @registry: the registry instance
 * @plugin_id: the identifier of the plugin
 * @error: error return location or @NULL to ignore
 *
 * Unload from memory a module identified by @plugin_id. This means call the
 * module's deinit function.
 *
 * Returns: %TRUE% on success.
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_unload_plugin (GrlRegistry *registry,
                            const gchar *plugin_id,
                            GError **error)
{
  GrlPlugin *plugin;
  GList *sources = NULL;
  GList *sources_iter;

  GRL_DEBUG ("%s: %s", __FUNCTION__, plugin_id);

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (plugin_id != NULL, FALSE);

  /* First check the plugin is valid  */
  plugin = g_hash_table_lookup (registry->priv->plugins, plugin_id);
  if (!plugin) {
    GRL_WARNING ("Could not deinit plugin '%s'. Plugin not found.", plugin_id);
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_UNLOAD_PLUGIN_FAILED,
                 _("Plugin not found: '%s'"), plugin_id);
    return FALSE;
  }

  /* Second, shut down any sources spawned by this plugin */
  GRL_DEBUG ("Shutting down sources spawned by '%s'", plugin_id);
  sources = grl_registry_get_sources (registry, FALSE);

  for (sources_iter = sources; sources_iter;
      sources_iter = g_list_next (sources_iter)) {
    GrlSource *source = GRL_SOURCE (sources_iter->data);
    if (grl_source_get_plugin (source) == plugin) {
      grl_registry_unregister_source (registry, source, NULL);
    }
  }
  g_list_free (sources);

  /* Third, shut down the plugin */
  shutdown_plugin (plugin);

  return TRUE;
}

/**
 * grl_registry_register_metadata_key:
 * @registry: The plugin registry
 * @param_spec: (transfer full): The definition of the key to register
 * @error: error return location or @NULL to ignore
 *
 * Registers a metadata key
 *
 * Returns: The #GrlKeyID registered.
 *
 * Since: 0.2.0
 */
GrlKeyID
grl_registry_register_metadata_key (GrlRegistry *registry,
                                    GParamSpec *param_spec,
                                    GError **error)
{
  GrlKeyID key;

  key = grl_registry_register_metadata_key_full (registry,
                                                 param_spec,
                                                 GRL_METADATA_KEY_INVALID,
                                                 error);

  if (key != GRL_METADATA_KEY_INVALID) {
    g_signal_emit (registry, registry_signals[SIG_METADATA_KEY_ADDED],
                   0,
                   grl_metadata_key_get_name (key));
  }

  return key;
}

/*
 * grl_registry_register_metadata_key_system:
 *
 * This is an internal method only meant to be used to register core
 * keys.
 *
 * For internal use. Plugin developers should use
 * grl_registry_register_metadata_key().
 */
GrlKeyID
grl_registry_register_metadata_key_system (GrlRegistry *registry,
                                           GParamSpec *param_spec,
                                           GrlKeyID key,
                                           GError **error)
{
  GrlKeyID registered_key;

  registered_key = grl_registry_register_metadata_key_full (registry,
                                                            param_spec,
                                                            key,
                                                            error);

  return registered_key;
}

/**
 * grl_registry_register_metadata_key_relation:
 * @registry: the plugin registry
 * @key1: key involved in relationship
 * @key2: key involved in relationship
 *
 * Creates a relation between @key1 and @key2, meaning that the values of both
 * keys are somehow related.
 *
 * One example of a relation would be the one between the URI of a media
 * resource and its mime-type: they are both tied together and one does not make
 * sense without the other.
 *
 * Relations between keys allow the framework to provide all the data that is
 * somehow related when any of the related keys are requested.
 *
 * Since: 0.2.0
 */
void
grl_registry_register_metadata_key_relation (GrlRegistry *registry,
                                             GrlKeyID key1,
                                             GrlKeyID key2)
{
  GList *key1_partners, *key1_peer;
  GList *key2_partners;

  g_return_if_fail (GRL_IS_REGISTRY (registry));
  g_return_if_fail (key1);
  g_return_if_fail (key2);

  if (key1 == key2) {
    return;
  }

  /* Search for keys related with each key */
  key1_partners = g_hash_table_lookup (registry->priv->related_keys, GRLKEYID_TO_POINTER (key1));
  key2_partners = g_hash_table_lookup (registry->priv->related_keys, GRLKEYID_TO_POINTER (key2));

  /* Check if they are already related */
  if (!key1_partners || !key2_partners || key1_partners == key2_partners) {
    return;
  }

  /* Merge both relations [related(key1), related(key2)] */
  key1_partners = g_list_concat(key1_partners, key2_partners);

  for (key1_peer = key1_partners;
       key1_peer;
       key1_peer = g_list_next (key1_peer)) {
    g_hash_table_insert (registry->priv->related_keys, key1_peer->data, key1_partners);
  }
}

/**
 * grl_registry_lookup_metadata_key:
 * @registry: the registry instance
 * @key_name: the key name
 *
 * Look up for the metadata key with name @key_name.
 *
 * Returns: The metadata key, or GRL_METADATA_KEY_INVALID if not found
 *
 * Since: 0.2.0
 */
GrlKeyID
grl_registry_lookup_metadata_key (GrlRegistry *registry,
                                  const gchar *key_name)
{
  g_return_val_if_fail (GRL_IS_REGISTRY (registry), 0);
  g_return_val_if_fail (key_name, 0);

  return key_id_handler_get_key (&registry->priv->key_id_handler, key_name);
}

/**
 * grl_registry_lookup_metadata_key_name:
 * @registry: the registry instance
 * @key: a metadata key
 *
 * Returns @key name.
 *
 * Returns: metadata key name, or @NULL if not found
 *
 * Since: 0.2.0
 */
const gchar *
grl_registry_lookup_metadata_key_name (GrlRegistry *registry,
                                       GrlKeyID key)
{
  g_return_val_if_fail (GRL_IS_REGISTRY (registry), 0);

  return key_id_handler_get_name (&registry->priv->key_id_handler, key);
}

/**
 * grl_registry_lookup_metadata_key_desc:
 * @registry: the registry instance
 * @key: a metadata key
 *
 * Returns @key description.
 *
 * Returns: metadata key description, or @NULL if not found
 *
 * Since: 0.2.0
 */
const gchar *
grl_registry_lookup_metadata_key_desc (GrlRegistry *registry,
                                       GrlKeyID key)
{
  const gchar *key_name;
  GParamSpec *key_pspec;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), 0);

  key_name = key_id_handler_get_name (&registry->priv->key_id_handler, key);
  if (!key_name) {
    return NULL;
  }
  key_pspec = g_hash_table_lookup (registry->priv->system_keys, key_name);

  if (key_pspec) {
    return g_param_spec_get_blurb (key_pspec);
  } else {
    return NULL;
  }
}

/**
 * grl_registry_lookup_metadata_key_type:
 * @registry: the registry instance
 * @key: a metadata key
 *
 * Returns @key expected value type.
 *
 * Returns: metadata key type, or @G_TYPE_INVALID if not found
 *
 * Since: 0.2.0
 */
GType
grl_registry_lookup_metadata_key_type (GrlRegistry *registry,
                                       GrlKeyID key)
{
  const gchar *key_name;
  GParamSpec *key_pspec;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), 0);

  key_name = key_id_handler_get_name (&registry->priv->key_id_handler, key);
  if (!key_name) {
    return G_TYPE_INVALID;
  }
  key_pspec = g_hash_table_lookup (registry->priv->system_keys, key_name);

  if (key_pspec) {
    return G_PARAM_SPEC_VALUE_TYPE (key_pspec);
  } else {
    return G_TYPE_INVALID;
  }
}

/**
 * grl_registry_metadata_key_validate:
 * @registry: the registry instance
 * @key: a metadata key
 * @value: value to be validate
 *
 * Validates @value content complies with the key specification. That is, it has
 * the expected type, and value are within the range specified in key (for
 * integer values).
 *
 * Returns: %TRUE if complies
 *
 * Since: 0.2.0
 **/
gboolean
grl_registry_metadata_key_validate (GrlRegistry *registry,
                                    GrlKeyID key,
                                    GValue *value)
{
  const gchar *key_name;
  GParamSpec *key_pspec;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (G_IS_VALUE (value), FALSE);

  key_name = key_id_handler_get_name (&registry->priv->key_id_handler, key);
  if (!key_name) {
    return FALSE;
  }
  key_pspec = g_hash_table_lookup (registry->priv->system_keys, key_name);

  if (key_pspec) {
    return !g_param_value_validate (key_pspec, value);
  } else {
    return FALSE;
  }
}

/**
 * grl_registry_lookup_metadata_key_relation:
 * @registry: the registry instance
 * @key: a metadata key
 *
 * Look up the list of keys that have a relation with @key.
 *
 * @key is included in that list.
 *
 * Returns: (element-type GrlKeyID) (transfer none): a #GList of
 * related keys, or @NULL if key is invalid.
 *
 * Since: 0.2.0
 **/
const GList *
grl_registry_lookup_metadata_key_relation (GrlRegistry *registry,
                                           GrlKeyID key)
{
  g_return_val_if_fail (GRL_IS_REGISTRY (registry), NULL);

  return g_hash_table_lookup (registry->priv->related_keys, GRLKEYID_TO_POINTER (key));
}

/**
 * grl_registry_get_metadata_keys:
 * @registry: the registry instance
 *
 * Returns a list with all registered keys in system.
 *
 * Returns: (transfer container) (element-type GrlKeyID): a #GList with all the available
 * #GrlKeyID<!-- -->s. The content of the list should not be modified or freed.
 * Use g_list_free() when done using the list.
 *
 * Since: 0.2.0
 **/
GList *
grl_registry_get_metadata_keys (GrlRegistry *registry)
{
  return key_id_handler_get_all_keys (&registry->priv->key_id_handler);
}

/**
 * grl_registry_add_config:
 * @registry: the registry instance
 * @config: (transfer full): a configuration set
 * @error: error return location or @NULL to ignore
 *
 * Add a configuration for a plugin/source.
 *
 * Returns: %TRUE on success
 *
 * Since: 0.2.0
 */
gboolean
grl_registry_add_config (GrlRegistry *registry,
                         GrlConfig *config,
                         GError **error)
{
  gchar *plugin_id;
  GList *configs = NULL;

  g_return_val_if_fail (config != NULL, FALSE);
  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);

  plugin_id = grl_config_get_plugin (config);
  if (!plugin_id) {
    GRL_WARNING ("Plugin configuration missed plugin information, ignoring...");
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_CONFIG_FAILED,
                 _("Plugin configuration does not contain 'plugin-id' reference"));
    return FALSE;
  }

  configs = g_hash_table_lookup (registry->priv->configs, plugin_id);
  if (configs) {
    /* Notice that we are using g_list_append on purpose to avoid
       having to insert again in the hash table */
    configs = g_list_append (configs, config);
    g_free (plugin_id);
  } else {
    configs = g_list_prepend (configs, config);
    g_hash_table_insert (registry->priv->configs,
			 (gpointer) plugin_id,
			 configs);
  }

  return TRUE;
}

static void
add_config_from_keyfile (GKeyFile    *keyfile,
			 GrlRegistry *registry)
{
  GrlConfig *config;
  gchar **key;
  gchar **keys;
  gchar **plugin;
  gchar **plugins;
  gchar *value;

  /* Look up for defined plugins */
  plugins = g_key_file_get_groups (keyfile, NULL);
  for (plugin = plugins; *plugin; plugin++) {
    config = grl_config_new (*plugin, NULL);

    /* Look up configuration keys for this plugin */
    keys = g_key_file_get_keys (keyfile, *plugin, NULL, NULL);
    for (key = keys; *key; key++) {
      value = g_key_file_get_string (keyfile, *plugin, *key, NULL);
      if (value) {
        grl_config_set_string (config, *key, value);
        g_free (value);
      }
    }
    grl_registry_add_config (registry, config, NULL);
    g_strfreev (keys);
  }
  g_strfreev (plugins);
}

/**
 * grl_registry_add_config_from_file:
 * @registry: the registry instance
 * @config_file: a key-value file containing the configuration
 * @error: error return location or @NULL to ignore
 *
 * Load plugin configurations from a .ini-like config file.
 *
 * Returns: %TRUE on success
 *
 * Since: 0.2.0
 **/
gboolean
grl_registry_add_config_from_file (GrlRegistry *registry,
                                   const gchar *config_file,
                                   GError **error)
{
  GError *load_error = NULL;
  GKeyFile *keyfile;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (config_file, FALSE);

  keyfile = g_key_file_new ();

  if (g_key_file_load_from_file (keyfile,
                                 config_file,
                                 G_KEY_FILE_NONE,
                                 &load_error)) {
    add_config_from_keyfile (keyfile, registry);
    g_key_file_free (keyfile);
    return TRUE;
  } else {
    GRL_WARNING ("Unable to load configuration. %s", load_error->message);
    g_set_error_literal (error,
                         GRL_CORE_ERROR,
                         GRL_CORE_ERROR_CONFIG_LOAD_FAILED,
                         load_error->message);
    g_error_free (load_error);
    g_key_file_free (keyfile);
    return FALSE;
  }
}

/**
 * grl_registry_add_config_from_resource:
 * @registry: the registry instance
 * @resource_path: a key-value file containing the configuration
 * @error: error return location or @NULL to ignore
 *
 * Load plugin configurations from a .ini-like resource file.
 *
 * Returns: %TRUE on success
 *
 * Since: 0.2.8
 **/
gboolean
grl_registry_add_config_from_resource (GrlRegistry *registry,
                                       const gchar *resource_path,
                                       GError **error)
{
  GError *load_error = NULL;
  GKeyFile *keyfile = NULL;
  GBytes *bytes;
  gboolean ret = FALSE;

  g_return_val_if_fail (GRL_IS_REGISTRY (registry), FALSE);
  g_return_val_if_fail (resource_path, FALSE);

  bytes = g_resources_lookup_data (resource_path, 0, error);
  if (bytes == NULL)
    goto bail;

  keyfile = g_key_file_new ();

  if (g_key_file_load_from_data (keyfile,
                                 g_bytes_get_data (bytes, NULL),
                                 g_bytes_get_size (bytes),
                                 G_KEY_FILE_NONE,
                                 &load_error)) {
    add_config_from_keyfile (keyfile, registry);
    ret = TRUE;
  } else {
    GRL_WARNING ("Unable to load configuration. %s", load_error->message);
    g_set_error_literal (error,
                         GRL_CORE_ERROR,
                         GRL_CORE_ERROR_CONFIG_LOAD_FAILED,
                         load_error->message);
    g_error_free (load_error);
  }

bail:
  g_clear_pointer (&keyfile, g_key_file_free);
  g_clear_pointer (&bytes, g_bytes_unref);

  return ret;
}
