#include <dlfcn.h>
#include <blockdev/utils.h>
#include "blockdev.h"
#include "plugins.h"

#include "plugin_apis/lvm.h"
#include "plugin_apis/lvm.c"
#include "plugin_apis/btrfs.h"
#include "plugin_apis/btrfs.c"
#include "plugin_apis/swap.h"
#include "plugin_apis/swap.c"
#include "plugin_apis/loop.h"
#include "plugin_apis/loop.c"
#include "plugin_apis/crypto.h"
#include "plugin_apis/crypto.c"
#include "plugin_apis/mpath.h"
#include "plugin_apis/mpath.c"
#include "plugin_apis/dm.h"
#include "plugin_apis/dm.c"
#include "plugin_apis/mdraid.h"
#include "plugin_apis/mdraid.c"
#include "plugin_apis/part.h"
#include "plugin_apis/part.c"
#include "plugin_apis/fs.h"
#include "plugin_apis/fs.c"
#include "plugin_apis/nvdimm.h"
#include "plugin_apis/nvdimm.c"
#include "plugin_apis/nvme.h"
#include "plugin_apis/nvme.c"
#include "plugin_apis/smart.h"
#include "plugin_apis/smart.c"

#if defined(__s390__) || defined(__s390x__)
#include "plugin_apis/s390.h"
#include "plugin_apis/s390.c"
#endif

/**
 * SECTION: blockdev
 * @short_description: a library for doing low-level operations with block devices
 * @title: blockdev library
 * @include: blockdev.h
 *
 */

#define DEFAULT_CONF_DIR_PATH "/etc/libblockdev/3/conf.d/"

static GMutex init_lock;
static gboolean initialized = FALSE;

typedef struct BDPluginStatus {
    BDPluginSpec spec;
    gpointer handle;
} BDPluginStatus;

typedef void* (*LoadFunc) (const gchar *so_name);

/* KEEP THE ORDERING OF THESE ARRAYS MATCHING THE BDPluginName ENUM! */
static gchar * default_plugin_so[BD_PLUGIN_UNDEF] = {
    "libbd_lvm.so.3", "libbd_btrfs.so.3",
    "libbd_swap.so.3", "libbd_loop.so.3",
    "libbd_crypto.so.3", "libbd_mpath.so.3",
    "libbd_dm.so.3", "libbd_mdraid.so.3",
    "libbd_s390.so.3", "libbd_part.so.3",
    "libbd_fs.so.3", "libbd_nvdimm.so.3",
    "libbd_nvme.so.3",
#if defined(HAVE_SMART) || !defined(HAVE_SMARTMONTOOLS)
     "libbd_smart.so.3",
#else
     "libbd_smartmontools.so.3",
#endif
};
static BDPluginStatus plugins[BD_PLUGIN_UNDEF] = {
    {{BD_PLUGIN_LVM, NULL}, NULL},
    {{BD_PLUGIN_BTRFS, NULL}, NULL},
    {{BD_PLUGIN_SWAP, NULL}, NULL},
    {{BD_PLUGIN_LOOP, NULL}, NULL},
    {{BD_PLUGIN_CRYPTO, NULL}, NULL},
    {{BD_PLUGIN_MPATH, NULL}, NULL},
    {{BD_PLUGIN_DM, NULL}, NULL},
    {{BD_PLUGIN_MDRAID, NULL}, NULL},
    {{BD_PLUGIN_S390, NULL}, NULL},
    {{BD_PLUGIN_PART, NULL}, NULL},
    {{BD_PLUGIN_FS, NULL}, NULL},
    {{BD_PLUGIN_NVDIMM, NULL}, NULL},
    {{BD_PLUGIN_NVME, NULL}, NULL},
    {{BD_PLUGIN_SMART, NULL}, NULL},
};
static gchar* plugin_names[BD_PLUGIN_UNDEF] = {
    "lvm", "btrfs", "swap", "loop", "crypto", "mpath", "dm", "mdraid", "s390", "part", "fs", "nvdimm", "nvme", "smart"
};

static void set_plugin_so_name (BDPlugin name, const gchar *so_name) {
    plugins[name].spec.so_name = so_name;
}

static gint config_file_cmp (gconstpointer a, gconstpointer b, gpointer user_data G_GNUC_UNUSED) {
    const gchar *name1 = (const gchar *) a;
    const gchar *name2 = (const gchar *) b;

    return g_strcmp0 (name1, name2);
}

static GSequence* get_config_files (GError **error) {
    GDir *dir = NULL;
    GSequence *ret = NULL;
    gchar *conf_dir_path = NULL;
    const gchar *dirent = NULL;

    conf_dir_path = g_strdup (g_getenv("LIBBLOCKDEV_CONFIG_DIR"));
    if (!conf_dir_path)
        conf_dir_path = g_strdup (DEFAULT_CONF_DIR_PATH);

    dir = g_dir_open (conf_dir_path, 0, error);
    if (!dir) {
        g_prefix_error (error, "Failed to get contents of the config dir (%s)", conf_dir_path);
        g_free (conf_dir_path);
        return NULL;
    }

    ret = g_sequence_new ((GDestroyNotify) g_free);

    dirent = g_dir_read_name(dir);
    while (dirent) {
        /* only process .cfg files from the directory */
        if (g_str_has_suffix (dirent, ".cfg")) {
            dirent = g_build_filename (conf_dir_path, dirent, NULL);
            g_sequence_insert_sorted (ret, (gpointer) dirent, config_file_cmp, NULL);
        }
        dirent = g_dir_read_name(dir);
    }

    g_free (conf_dir_path);
    g_dir_close (dir);
    return ret;
}

static gboolean process_config_file (const gchar *config_file, GSList **plugins_sonames, GError **error) {
    GKeyFile *config = NULL;
    BDPlugin i = 0;
    gchar **sonames = NULL;
    gsize n_sonames = 0;

    config = g_key_file_new ();
    if (!g_key_file_load_from_file (config, config_file, G_KEY_FILE_NONE, error)) {
        g_key_file_free (config);
        return FALSE;
    }

    /* get sonames for each plugin (if specified) */
    for (i=0; (i < BD_PLUGIN_UNDEF); i++) {
        sonames = g_key_file_get_string_list (config, plugin_names[i], "sonames", &n_sonames, error);
        if (!sonames) {
            /* no sonames specified or an error occurred */
            if (*error)
                g_clear_error (error);
        } else {
            /* go through the sonames in the reversed order (because we prepend
               them to the list) */
            for (; n_sonames > 0; n_sonames--) {
                plugins_sonames[i] = g_slist_prepend (plugins_sonames[i], sonames[n_sonames-1]);
            }
            /* we need to free only the array here not its items because those
               were put into the list above as pointers */
            g_free (sonames);
        }
    }

    g_key_file_free (config);

    return TRUE;
}

static gboolean load_config (GSequence *config_files, GSList **plugins_sonames, GError **error) {
    GSequenceIter *config_file_iter = NULL;
    gchar *config_file = NULL;

    /* process config files one after another in order */
    config_file_iter = g_sequence_get_begin_iter (config_files);
    while (!g_sequence_iter_is_end (config_file_iter)) {
        config_file = (gchar *) g_sequence_get (config_file_iter);
        if (!process_config_file (config_file, plugins_sonames, error)) {
            bd_utils_log_format (BD_UTILS_LOG_WARNING, "Cannot process the config file '%s': %s. Skipping.",
                                 config_file, (*error)->message);
            g_clear_error (error);
        }
        config_file_iter = g_sequence_iter_next (config_file_iter);
    }

    return TRUE;
}

static void unload_plugins (void) {
    if (plugins[BD_PLUGIN_LVM].handle && !unload_lvm (plugins[BD_PLUGIN_LVM].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the lvm plugin");
    plugins[BD_PLUGIN_LVM].handle = NULL;

    if (plugins[BD_PLUGIN_BTRFS].handle && !unload_btrfs (plugins[BD_PLUGIN_BTRFS].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the btrfs plugin");
    plugins[BD_PLUGIN_BTRFS].handle = NULL;

    if (plugins[BD_PLUGIN_SWAP].handle && !unload_swap (plugins[BD_PLUGIN_SWAP].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the swap plugin");
    plugins[BD_PLUGIN_SWAP].handle = NULL;

    if (plugins[BD_PLUGIN_LOOP].handle && !unload_loop (plugins[BD_PLUGIN_LOOP].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the loop plugin");
    plugins[BD_PLUGIN_LOOP].handle = NULL;

    if (plugins[BD_PLUGIN_CRYPTO].handle && !unload_crypto (plugins[BD_PLUGIN_CRYPTO].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the crypto plugin");
    plugins[BD_PLUGIN_CRYPTO].handle = NULL;

    if (plugins[BD_PLUGIN_MPATH].handle && !unload_mpath (plugins[BD_PLUGIN_MPATH].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the mpath plugin");
    plugins[BD_PLUGIN_MPATH].handle = NULL;

    if (plugins[BD_PLUGIN_DM].handle && !unload_dm (plugins[BD_PLUGIN_DM].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the dm plugin");
    plugins[BD_PLUGIN_DM].handle = NULL;

    if (plugins[BD_PLUGIN_MDRAID].handle && !unload_mdraid (plugins[BD_PLUGIN_MDRAID].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the mdraid plugin");
    plugins[BD_PLUGIN_MDRAID].handle = NULL;

#if defined(__s390__) || defined(__s390x__)
    if (plugins[BD_PLUGIN_S390].handle && !unload_s390 (plugins[BD_PLUGIN_S390].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the s390 plugin");
    plugins[BD_PLUGIN_S390].handle = NULL;
#endif

    if (plugins[BD_PLUGIN_PART].handle && !unload_part (plugins[BD_PLUGIN_PART].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the part plugin");
    plugins[BD_PLUGIN_PART].handle = NULL;

    if (plugins[BD_PLUGIN_FS].handle && !unload_fs (plugins[BD_PLUGIN_FS].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the fs plugin");
    plugins[BD_PLUGIN_FS].handle = NULL;

    if (plugins[BD_PLUGIN_NVDIMM].handle && !unload_nvdimm (plugins[BD_PLUGIN_NVDIMM].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the nvdimm plugin");
    plugins[BD_PLUGIN_NVDIMM].handle = NULL;

    if (plugins[BD_PLUGIN_NVME].handle && !unload_nvme (plugins[BD_PLUGIN_NVME].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the nvme plugin");
    plugins[BD_PLUGIN_NVME].handle = NULL;

    if (plugins[BD_PLUGIN_SMART].handle && !unload_smart (plugins[BD_PLUGIN_SMART].handle))
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to close the smart plugin");
    plugins[BD_PLUGIN_SMART].handle = NULL;
}

static void load_plugin_from_sonames (BDPlugin plugin, LoadFunc load_fn, void **handle, GSList *sonames) {
    while (!(*handle) && sonames) {
        *handle = load_fn (sonames->data);
        if (*handle)
            set_plugin_so_name(plugin, g_strdup (sonames->data));
        sonames = g_slist_next (sonames);
    }
}

static void do_load (GSList **plugins_sonames) {
    if (!plugins[BD_PLUGIN_LVM].handle && plugins_sonames[BD_PLUGIN_LVM])
        load_plugin_from_sonames (BD_PLUGIN_LVM, load_lvm_from_plugin, &(plugins[BD_PLUGIN_LVM].handle), plugins_sonames[BD_PLUGIN_LVM]);
    if (!plugins[BD_PLUGIN_BTRFS].handle && plugins_sonames[BD_PLUGIN_BTRFS])
        load_plugin_from_sonames (BD_PLUGIN_BTRFS, load_btrfs_from_plugin, &(plugins[BD_PLUGIN_BTRFS].handle), plugins_sonames[BD_PLUGIN_BTRFS]);
    if (!plugins[BD_PLUGIN_SWAP].handle && plugins_sonames[BD_PLUGIN_SWAP])
        load_plugin_from_sonames (BD_PLUGIN_SWAP,load_swap_from_plugin, &(plugins[BD_PLUGIN_SWAP].handle), plugins_sonames[BD_PLUGIN_SWAP]);
    if (!plugins[BD_PLUGIN_LOOP].handle && plugins_sonames[BD_PLUGIN_LOOP])
        load_plugin_from_sonames (BD_PLUGIN_LOOP, load_loop_from_plugin, &(plugins[BD_PLUGIN_LOOP].handle), plugins_sonames[BD_PLUGIN_LOOP]);
    if (!plugins[BD_PLUGIN_CRYPTO].handle && plugins_sonames[BD_PLUGIN_CRYPTO])
        load_plugin_from_sonames (BD_PLUGIN_CRYPTO, load_crypto_from_plugin, &(plugins[BD_PLUGIN_CRYPTO].handle), plugins_sonames[BD_PLUGIN_CRYPTO]);
    if (!plugins[BD_PLUGIN_MPATH].handle && plugins_sonames[BD_PLUGIN_MPATH])
        load_plugin_from_sonames (BD_PLUGIN_MPATH, load_mpath_from_plugin, &(plugins[BD_PLUGIN_MPATH].handle), plugins_sonames[BD_PLUGIN_MPATH]);
    if (!plugins[BD_PLUGIN_DM].handle && plugins_sonames[BD_PLUGIN_DM])
        load_plugin_from_sonames (BD_PLUGIN_DM, load_dm_from_plugin, &(plugins[BD_PLUGIN_DM].handle), plugins_sonames[BD_PLUGIN_DM]);
    if (!plugins[BD_PLUGIN_MDRAID].handle && plugins_sonames[BD_PLUGIN_MDRAID])
        load_plugin_from_sonames (BD_PLUGIN_MDRAID, load_mdraid_from_plugin, &(plugins[BD_PLUGIN_MDRAID].handle), plugins_sonames[BD_PLUGIN_MDRAID]);
#if defined(__s390__) || defined(__s390x__)
    if (!plugins[BD_PLUGIN_S390].handle && plugins_sonames[BD_PLUGIN_S390])
        load_plugin_from_sonames (BD_PLUGIN_S390, load_s390_from_plugin, &(plugins[BD_PLUGIN_S390].handle), plugins_sonames[BD_PLUGIN_S390]);
#endif
    if (!plugins[BD_PLUGIN_PART].handle && plugins_sonames[BD_PLUGIN_PART])
        load_plugin_from_sonames (BD_PLUGIN_PART, load_part_from_plugin, &(plugins[BD_PLUGIN_PART].handle), plugins_sonames[BD_PLUGIN_PART]);
    if (!plugins[BD_PLUGIN_FS].handle && plugins_sonames[BD_PLUGIN_FS])
        load_plugin_from_sonames (BD_PLUGIN_FS, load_fs_from_plugin, &(plugins[BD_PLUGIN_FS].handle), plugins_sonames[BD_PLUGIN_FS]);
    if (!plugins[BD_PLUGIN_NVDIMM].handle && plugins_sonames[BD_PLUGIN_NVDIMM])
        load_plugin_from_sonames (BD_PLUGIN_NVDIMM, load_nvdimm_from_plugin, &(plugins[BD_PLUGIN_NVDIMM].handle), plugins_sonames[BD_PLUGIN_NVDIMM]);
    if (!plugins[BD_PLUGIN_NVME].handle && plugins_sonames[BD_PLUGIN_NVME])
        load_plugin_from_sonames (BD_PLUGIN_NVME, load_nvme_from_plugin, &(plugins[BD_PLUGIN_NVME].handle), plugins_sonames[BD_PLUGIN_NVME]);
    if (!plugins[BD_PLUGIN_SMART].handle && plugins_sonames[BD_PLUGIN_SMART])
        load_plugin_from_sonames (BD_PLUGIN_SMART, load_smart_from_plugin, &(plugins[BD_PLUGIN_SMART].handle), plugins_sonames[BD_PLUGIN_SMART]);
}

static gboolean load_plugins (BDPluginSpec **require_plugins, gboolean reload, guint64 *num_loaded) {
    guint8 i = 0;
    gboolean requested_loaded = TRUE;
    GError *error = NULL;
    GSequence *config_files = NULL;
    GSList *plugins_sonames[BD_PLUGIN_UNDEF] = {0};
    BDPlugin plugin_name = BD_PLUGIN_UNDEF;
    guint64 required_plugins_mask = 0;

    /* load config files first */
    config_files = get_config_files (&error);
    if (config_files) {
        if (!load_config (config_files, plugins_sonames, &error))
            bd_utils_log_format (BD_UTILS_LOG_WARNING, "Failed to load config files: %s. Using the built-in config",
                                 error->message);

        g_sequence_free (config_files);
    } else
        bd_utils_log_format (BD_UTILS_LOG_INFO, "Failed to load config files: %s. Using the built-in config",
                             error->message);
    g_clear_error (&error);

    /* populate missing items with the built-in defaults */
    for (i=0; i < BD_PLUGIN_UNDEF; i++)
        if (!plugins_sonames[i])
            plugins_sonames[i] = g_slist_prepend (plugins_sonames[i], g_strdup (default_plugin_so[i]));

#if !defined(__s390__) && !defined(__s390x__)
    /* do not load the s390 plugin by default if not on s390(x) */
    g_slist_free_full (plugins_sonames[BD_PLUGIN_S390], (GDestroyNotify) g_free);
    plugins_sonames[BD_PLUGIN_S390] = NULL;
#endif

    /* unload the previously loaded plugins if requested */
    if (reload) {
        unload_plugins ();
        /* clean all so names and populate back those that are requested or the
           defaults */
        for (i=0; i < BD_PLUGIN_UNDEF; i++) {
            g_free ((gchar *) plugins[i].spec.so_name);
            plugins[i].spec.so_name = NULL;
        }
    }

    if (require_plugins) {
        /* set requested sonames */
        for (i=0; *(require_plugins + i); i++) {
            plugin_name = require_plugins[i]->name;
            if (plugin_name >= BD_PLUGIN_UNDEF)
                continue;
            required_plugins_mask |= ((guint64) 1 << plugin_name);
            if (require_plugins[i]->so_name) {
                g_slist_free_full (plugins_sonames[plugin_name], (GDestroyNotify) g_free);
                plugins_sonames[plugin_name] = NULL;
                plugins_sonames[plugin_name] = g_slist_prepend(plugins_sonames[plugin_name], g_strdup (require_plugins[i]->so_name));
            }
        }

        /* now remove the defaults for plugins that are not required */
        for (i=0; (i < BD_PLUGIN_UNDEF); i++)
            if (!(required_plugins_mask & ((guint64) 1 << i))) {
                /* plugin not required */
                g_slist_free_full (plugins_sonames[i], (GDestroyNotify) g_free);
                plugins_sonames[i] = NULL;
            }
    }

    do_load (plugins_sonames);

    *num_loaded = 0;
    for (i=0; (i < BD_PLUGIN_UNDEF); i++) {
        /* if this plugin was required or all plugins were required, check if it
           was successfully loaded or not */
        if (!require_plugins || (required_plugins_mask & ((guint64) 1 << i))) {
#if !defined(__s390__) && !defined(__s390x__)
            if (!require_plugins && (i == BD_PLUGIN_S390))
                /* do not check the s390 plugin on different archs unless
                   explicitly required */
                continue;
#endif
            if (plugins[i].handle)
                (*num_loaded)++;
            else
                requested_loaded = FALSE;
        }
    }

    /* clear/free the config */
    for (i=0; (i < BD_PLUGIN_UNDEF); i++) {
        if (plugins_sonames[i]) {
            g_slist_free_full (plugins_sonames[i], (GDestroyNotify) g_free);
            plugins_sonames[i] = NULL;
        }
    }

    return requested_loaded;
}

GQuark bd_init_error_quark (void)
{
    return g_quark_from_static_string ("g-bd-init-error-quark");
}

/**
 * bd_init:
 * @require_plugins: (nullable) (array zero-terminated=1): %NULL-terminated list
 *                 of plugins that should be loaded (if no so_name is specified
 *                 for the plugin, the default is used) or %NULL to load all
 *                 plugins
 * @log_func: (nullable) (scope notified): logging function to use
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether the library was successfully initialized with all the
 *          required or default (see @require_plugins) plugins or not
 *
 * Example of libblockdev initialization with 'fs' and 'lvm' plugins. Specific
 * version of the lvm plugin is required:
 *
 * |[<!-- language="C" -->
 * GError *error = NULL;
 * gboolean ret = FALSE;
 * BDPluginSpec fs_plugin = {BD_PLUGIN_FS, NULL};
 * BDPluginSpec lvm_plugin = {BD_PLUGIN_LVM, "libbd_lvm.so.3"};
 *
 * BDPluginSpec *plugins[] = {&fs_plugin, &lvm_plugin, NULL};
 *
 * ret = bd_init (plugins, NULL, &error);
 * ]|
 */
gboolean bd_init (BDPluginSpec **require_plugins, BDUtilsLogFunc log_func, GError **error) {
    gboolean success = TRUE;
    guint64 num_loaded = 0;

    g_mutex_lock (&init_lock);
    if (initialized) {
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "bd_init() called more than once! Use bd_reinit() to reinitialize "
                             "or bd_is_initialized() to get the current state.");
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    if (log_func && !bd_utils_init_logging (log_func, error)) {
        /* the error is already populated */
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    if (!load_plugins (require_plugins, FALSE, &num_loaded)) {
        g_set_error_literal (error, BD_INIT_ERROR, BD_INIT_ERROR_PLUGINS_FAILED,
                             "Failed to load plugins");
        success = FALSE;
    }

    if (num_loaded == 0) {
        if (require_plugins && (*require_plugins == NULL))
            /* requested to load no plugins (NULL is the first item in the
               array), none loaded -> OK */
            initialized = TRUE;
        else
            initialized = FALSE;
    } else
        initialized = TRUE;

    g_mutex_unlock (&init_lock);

    return success;
}

/**
 * bd_ensure_init:
 * @require_plugins: (nullable) (array zero-terminated=1): %NULL-terminated list
 *                 of plugins that should be loaded (if no so_name is specified
 *                 for the plugin, the default is used) or %NULL to load all
 *                 plugins
 * @log_func: (nullable) (scope notified): logging function to use
 * @error: (out) (optional): place to store error (if any)
 *
 * Checks the state of the library and if it is uninitialized or not all the
 * @require_plugins plugins are available, tries to (re)initialize it. Otherwise
 * just returns early. The difference between:
 *
 * |[<!-- language="C" -->
 * if (!bd_is_initialized())
 *     bd_init(None, None, &error);
 * ]|
 *
 * and this function is that this function does the check and init in an atomic
 * way (holding the lock preventing other threads from doing changes in
 * between).
 *
 * Returns: whether the library was successfully initialized with all the
 *          required or default (see @require_plugins) plugins or not either
 *          before or by this call
 */
gboolean bd_ensure_init (BDPluginSpec **require_plugins, BDUtilsLogFunc log_func, GError **error) {
    gboolean success = TRUE;
    BDPluginSpec **check_plugin = NULL;
    gboolean missing = FALSE;
    guint64 num_loaded = 0;
    BDPlugin plugin = BD_PLUGIN_UNDEF;

    g_mutex_lock (&init_lock);
    if (initialized) {
        if (require_plugins)
            for (check_plugin=require_plugins; !missing && *check_plugin; check_plugin++)
                missing = !bd_is_plugin_available((*check_plugin)->name);
        else
            /* all plugins requested */
            for (plugin=BD_PLUGIN_LVM; !missing && plugin != BD_PLUGIN_UNDEF; plugin++)
                missing = !bd_is_plugin_available(plugin);

        if (!missing) {
            g_mutex_unlock (&init_lock);
            return TRUE;
        }
    }

    if (log_func && !bd_utils_init_logging (log_func, error)) {
        /* the error is already populated */
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    if (!load_plugins (require_plugins, FALSE, &num_loaded)) {
        g_set_error_literal (error, BD_INIT_ERROR, BD_INIT_ERROR_PLUGINS_FAILED,
                             "Failed to load plugins");
        success = FALSE;
    }

    if (num_loaded == 0) {
        if (require_plugins && (*require_plugins == NULL))
            /* requested to load no plugins (NULL is the first item in the
               array), none loaded -> OK */
            initialized = TRUE;
        else
            initialized = FALSE;
    } else
        initialized = TRUE;

    g_mutex_unlock (&init_lock);

    return success;
}

/**
 * bd_try_init:
 * @request_plugins: (nullable) (array zero-terminated=1): %NULL-terminated list
 *                   of plugins that should be loaded (if no so_name is specified
 *                   for the plugin, the default is used) or %NULL to load all
 *                   plugins
 * @log_func: (nullable) (scope notified): logging function to use
 * @loaded_plugin_names: (optional) (out) (transfer container) (array zero-terminated=1): names
 *                       of the successfully loaded plugins
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether the library was successfully initialized with all the
 *          required or default (see @require_plugins) plugins or not
 *
 * *UNLIKE IN CASE OF bd_init() AND bd_ensure_init(), FAILURE TO LOAD A PLUGIN
 *  IS NOT CONSIDERED ERROR*
 */
gboolean bd_try_init(BDPluginSpec **request_plugins, BDUtilsLogFunc log_func,
                     gchar ***loaded_plugin_names, GError **error) {
    gboolean success = TRUE;
    guint64 num_loaded = 0;

    g_mutex_lock (&init_lock);
    if (initialized) {
        bd_utils_log_format (BD_UTILS_LOG_WARNING, "bd_try_init() called more than once! Use bd_reinit() "
                             "to reinitialize or bd_is_initialized() to get the current state.");
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    if (log_func && !bd_utils_init_logging (log_func, error)) {
        /* the error is already populated */
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    success = load_plugins (request_plugins, FALSE, &num_loaded);

    if (num_loaded == 0) {
        if (request_plugins && (*request_plugins == NULL))
            /* requested to load no plugins (NULL is the first item in the
               array), none loaded -> OK */
            initialized = TRUE;
        else
            initialized = FALSE;
    } else
        initialized = TRUE;

    if (loaded_plugin_names)
        *loaded_plugin_names = bd_get_available_plugin_names ();

    g_mutex_unlock (&init_lock);

    return success;
}

/**
 * bd_reinit:
 * @require_plugins: (nullable) (array zero-terminated=1): %NULL-terminated list
 *                 of plugins that should be loaded (if no so_name is specified
 *                 for the plugin, the default is used) or %NULL to load all
 *                 plugins
 * @reload: whether to reload the already loaded plugins or not
 * @log_func: (nullable) (scope notified): logging function to use or %NULL
 *                                           to keep the old one
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether the library was successfully initialized or not
 *
 * If @reload is %TRUE all the plugins are closed and reloaded otherwise only
 * the missing plugins are loaded.
 */
gboolean bd_reinit (BDPluginSpec **require_plugins, gboolean reload, BDUtilsLogFunc log_func, GError **error) {
    gboolean success = TRUE;
    guint64 num_loaded = 0;

    g_mutex_lock (&init_lock);
    if (log_func && !bd_utils_init_logging (log_func, error)) {
        /* the error is already populated */
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    if (!load_plugins (require_plugins, reload, &num_loaded)) {
        g_set_error_literal (error, BD_INIT_ERROR, BD_INIT_ERROR_PLUGINS_FAILED,
                             "Failed to load plugins");
        success = FALSE;
    } else
        if (require_plugins && (*require_plugins == NULL) && reload)
            /* requested to just unload all plugins */
            success = (num_loaded == 0);

    if (num_loaded == 0) {
        if (require_plugins && (*require_plugins == NULL))
            /* requested to load no plugins (NULL is the first item in the
               array), none loaded -> OK */
            initialized = TRUE;
        else
            initialized = FALSE;
    } else
        initialized = TRUE;

    g_mutex_unlock (&init_lock);
    return success;
}

/**
 * bd_try_reinit:
 * @require_plugins: (nullable) (array zero-terminated=1): %NULL-terminated list
 *                 of plugins that should be loaded (if no so_name is specified
 *                 for the plugin, the default is used) or %NULL to load all
 *                 plugins
 * @reload: whether to reload the already loaded plugins or not
 * @log_func: (nullable) (scope notified): logging function to use or %NULL
 *                                           to keep the old one
 * @loaded_plugin_names: (optional) (out) (transfer container) (array zero-terminated=1): names of the successfully
 *                                                                loaded plugins
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether the library was successfully initialized or not
 *
 * If @reload is %TRUE all the plugins are closed and reloaded otherwise only
 * the missing plugins are loaded.
 *
 * *UNLIKE IN CASE OF bd_init() AND bd_ensure_init(), FAILURE TO LOAD A PLUGIN
 *  IS NOT CONSIDERED ERROR*
 */
gboolean bd_try_reinit (BDPluginSpec **require_plugins, gboolean reload, BDUtilsLogFunc log_func,
                        gchar ***loaded_plugin_names, GError **error) {
    gboolean success = TRUE;
    guint64 num_loaded = 0;

    g_mutex_lock (&init_lock);
    if (log_func && !bd_utils_init_logging (log_func, error)) {
        /* the error is already populated */
        g_mutex_unlock (&init_lock);
        return FALSE;
    }

    success = load_plugins (require_plugins, reload, &num_loaded);
    if (success && require_plugins && (*require_plugins == NULL) && reload)
        /* requested to just unload all plugins */
        success = (num_loaded == 0);

    if (num_loaded == 0) {
        if (require_plugins && (*require_plugins == NULL))
            /* requested to load no plugins (NULL is the first item in the
               array), none loaded -> OK */
            initialized = TRUE;
        else
            initialized = FALSE;
    } else
        initialized = TRUE;

    if (loaded_plugin_names)
        *loaded_plugin_names = bd_get_available_plugin_names ();

    g_mutex_unlock (&init_lock);
    return success;
}

/**
 * bd_is_initialized:
 *
 * Returns: whether the library is initialized or not
 *
 * The library is considered initialized if some of the *init*() functions
 * was/were called and either at least one plugin is loaded or 0 plugins are
 * loaded after an explicit call that requested 0 plugins to be loaded.
 */
gboolean bd_is_initialized (void) {
    gboolean is = FALSE;
    g_mutex_lock (&init_lock);
    is = initialized;
    g_mutex_unlock (&init_lock);
    return is;
}

/**
 * bd_get_available_plugin_names:
 *
 * Returns: (transfer container) (array zero-terminated=1): an array of string
 * names of plugins that are available
 */
gchar** bd_get_available_plugin_names (void) {
    guint8 i = 0;
    guint8 num_loaded = 0;
    guint8 next = 0;

    for (i=0; i < BD_PLUGIN_UNDEF; i++)
        if (plugins[i].handle)
            num_loaded++;

    gchar **ret_plugin_names = g_new0 (gchar*, num_loaded + 1);
    for (i=0; i < BD_PLUGIN_UNDEF; i++)
        if (plugins[i].handle) {
            ret_plugin_names[next] = plugin_names[i];
            next++;
        }
    ret_plugin_names[next] = NULL;

    return ret_plugin_names;
}

/**
 * bd_is_plugin_available:
 * @plugin: the queried plugin
 *
 * Returns: whether the given plugin is available or not
 */
gboolean bd_is_plugin_available (BDPlugin plugin) {
    if (plugin < BD_PLUGIN_UNDEF)
        return plugins[plugin].handle != NULL;
    else
        return FALSE;
}

/**
 * bd_get_plugin_soname:
 * @plugin: the queried plugin
 *
 * Returns: (transfer full): name of the shared object loaded for the plugin or
 * %NULL if none is loaded
 */
gchar* bd_get_plugin_soname (BDPlugin plugin) {
    if (plugin >= BD_PLUGIN_UNDEF)
        return NULL;

    if (plugins[plugin].handle)
        return g_strdup (plugins[plugin].spec.so_name);

    return NULL;
}

/**
 * bd_get_plugin_name:
 * @plugin: the queried plugin
 *
 * Returns: (transfer none): name of the plugin
 */
gchar* bd_get_plugin_name (BDPlugin plugin) {
    if (plugin >= BD_PLUGIN_UNDEF)
        return NULL;

    return plugin_names[plugin];
}
