/* Chrysalide - Outil d'analyse de fichiers binaires
 * pglist.c - gestion de l'ensemble des greffons
 *
 * Copyright (C) 2009-2019 Cyrille Bagard
 *
 *  This file is part of Chrysalide.
 *
 *  Chrysalide is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Chrysalide 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


#include "pglist.h"


#include <assert.h>
#include <dirent.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>


#include <i18n.h>


#include "manager.h"
#include "native.h"
#include "plugin-int.h"
#include "../common/cpp.h"
#include "../common/extstr.h"
#include "../core/logs.h"
#include "../core/nox.h"
#include "../core/paths.h"



/**
 * Prototype de la fonction de création, à garder synchronisé avec
 * NATIVE_PLUGIN_ENTRYPOINT() (cf. native-int.h).
 */
typedef GPluginModule * (* get_plugin_instance_cb) (GModule *);

/* Liste de l'ensemble des greffons */
static GPluginModule **_pg_list = NULL;
static size_t _pg_count = 0;

/* Accès à cette liste */
static GRWLock _pg_lock;


/* Filtre les répertoire et les modules de greffons pootentels. */
static int filter_dirs_or_mods(const struct dirent *);

/* Part à la recherche de greffons sous forme de modules. */
static void browse_directory_for_plugins(const char *);

/* Suit les variations du compteur de références d'un greffon. */
static void on_plugin_ref_toggle(gpointer, GPluginModule *, gboolean);

/* Fournit le greffon répondant à un nom donné. */
static GPluginModule *_find_plugin_by_name(const char *, size_t *);



/******************************************************************************
*                                                                             *
*  Paramètres  : load = procéde à un chargement dans la foulée ?              *
*                                                                             *
*  Description : Procède au chargement des différents greffons trouvés.       *
*                                                                             *
*  Retour      : Toujours true (même s'il y a des erreurs de chargement).     *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

bool init_all_plugins(bool load)
{
    bool result;                            /* Bilan à retourner           */
    char *edir;                             /* Répertoire de base effectif */
    char *env;                              /* Contenu environnemental     */
    char *saveptr;                          /* Sauvegarde pour parcours    */
    char *udir;                             /* Répertoire supplémentaire ? */

    result = true;

    g_rw_lock_init(&_pg_lock);

    edir = get_effective_directory_new(TDT_PLUGINS_LIB);
    browse_directory_for_plugins(edir);
    free(edir);

    env = getenv("CHRYSALIDE_PLUGINS_PATH");

    if (env != NULL)
    {
        env = strdup(env);

        for (udir = strtok_r(env, ":", &saveptr); udir != NULL; udir = strtok_r(NULL, ":", &saveptr))
            browse_directory_for_plugins(udir);

        free(env);

    }

    if (load)
        load_remaning_plugins();

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Procède au déchargement des différents greffons présents.    *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void exit_all_plugins(void)
{
    size_t i;                               /* Boucle de parcours          */

    lock_plugin_list_for_reading();

    for (i = 0; i < _pg_count; i++)
    {
        assert(_pg_list[i] != NULL);
        unref_object(_pg_list[i]);
    }

    if (_pg_list != NULL)
        free(_pg_list);

    unlock_plugin_list_for_reading();

    g_rw_lock_clear(&_pg_lock);

}


/******************************************************************************
*                                                                             *
*  Paramètres  : lock = type d'action à mener.                                *
*                                                                             *
*  Description : Verrouille ou déverrouille l'accès en lecture à la liste.    *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void _lock_unlock_plugin_list_for_reading(bool lock)
{
    if (lock)
        g_rw_lock_reader_lock(&_pg_lock);
    else
        g_rw_lock_reader_unlock(&_pg_lock);

}


/******************************************************************************
*                                                                             *
*  Paramètres  : entry = entrée de répertoire à analyser.                     *
*                                                                             *
*  Description : Filtre les répertoire et les modules de greffons pootentels. *
*                                                                             *
*  Retour      : Valeur non nulle pour garder l'élément.                      *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static int filter_dirs_or_mods(const struct dirent *entry)
{
    int result;                             /* Conclusion à remonter       */

    if (entry->d_type == DT_DIR)
        result = strcmp(entry->d_name, ".") * strcmp(entry->d_name, "..");

    else
        result = (strrcmp(entry->d_name, "." G_MODULE_SUFFIX) == 0 ? 1 : 0);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : dir = répertoire à parcourir en quête de greffons (sans /).  *
*                                                                             *
*  Description : Indique la version (NOX/UI) associée à un nom de fichier.    *
*                                                                             *
*  Retour      : true si la version complémentaire existe ou false.           *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool check_for_plugin_versions(const char *dir, const char *filename, bool *is_nox, bool *is_ui)
{
    bool result;                            /* Bilan à renvoyer            */
    size_t length;                          /* Taille du nom de fichier    */
    char *alt_path;                         /* Autre chemin complet testé  */
    int ret;                                /* Bilan d'une impression      */

#ifdef _WIN32
#   define SHARED_SUFFIX ".dll"
#else
#   define SHARED_SUFFIX ".so"
#endif
#define UI_SHARED_SUFFIX "ui" SHARED_SUFFIX

    result = false;

    /* Propriétés du fichier courant */

    length = strlen(filename);

    if (length < STATIC_STR_SIZE(UI_SHARED_SUFFIX))
        *is_ui = false;

    else
        *is_ui = (strcmp(filename + length - STATIC_STR_SIZE(UI_SHARED_SUFFIX), UI_SHARED_SUFFIX) == 0);

    if (*is_ui)
        *is_nox = false;

    else
    {
        if (length < STATIC_STR_SIZE(SHARED_SUFFIX))
            *is_nox = false;

        else
            *is_nox = (strcmp(filename + length - STATIC_STR_SIZE(SHARED_SUFFIX), SHARED_SUFFIX) == 0);

    }

    /* Recherche d'une version alternative */

    if (*is_nox || *is_ui)
    {

        if (*is_nox)
            ret = asprintf(&alt_path, "%s%s%.*s%s",
                           dir, G_DIR_SEPARATOR_S,
                           (int)(length - STATIC_STR_SIZE(SHARED_SUFFIX)), filename,
                           UI_SHARED_SUFFIX);
        else
            ret = asprintf(&alt_path, "%s%s%.*s%s",
                           dir, G_DIR_SEPARATOR_S,
                           (int)(length - STATIC_STR_SIZE(SHARED_SUFFIX)), filename,
                           SHARED_SUFFIX);

        if (ret <= 0)
        {
            LOG_ERROR_N("asprintf");
            goto exit;
        }

        ret = access(alt_path, R_OK | X_OK);

        result = (ret == 0);

        free(alt_path);

    }

 exit:

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : dir = répertoire à parcourir en quête de greffons (sans /).  *
*                                                                             *
*  Description : Part à la recherche de greffons sous forme de modules.       *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void browse_directory_for_plugins(const char *dir)
{
    struct dirent **namelist;               /* Eléments trouvés            */
    int ret;                                /* Bilan d'un appel            */
    int k;                                  /* Boucle de parcours          */
    bool nox_mode;                          /* Absence de support graphique*/
    char *filename;                         /* Elément à ausculter         */
    bool is_nox;                            /* Chemin de version basique ? */
    bool is_ui;                             /* Chemin de version graphique */
    bool has_alt;                           /* Existence d'une alternative */
    GModule *module;                        /* Abstration de manipulation  */
    get_plugin_instance_cb get_instance;    /* Point d'entrée exporté      */
    GPluginModule *plugin;                  /* Greffon à intégrer ou pas   */

    ret = scandir(dir, &namelist, filter_dirs_or_mods, alphasort);
    if (ret < 0)
    {
        LOG_ERROR_N("scandir");
        return;
    }

    nox_mode = run_in_nox_mode();

    for (k = ret; k--; )
    {
        ret = asprintf(&filename, "%s%s%s", dir, G_DIR_SEPARATOR_S, namelist[k]->d_name);
        if (ret <= 0)
        {
            LOG_ERROR_N("asprintf");
            continue;
        }

        if (namelist[k]->d_type == DT_DIR)
            browse_directory_for_plugins(filename);

        else
        {

            printf("// Candidate // %s\n", filename);

            has_alt = check_for_plugin_versions(dir, namelist[k]->d_name, &is_nox, &is_ui);

            printf("  -> nox=%d  ui=%d  -> alt? %d\n", is_nox, is_ui, has_alt);


            if ((nox_mode && is_nox) || (!nox_mode && ((is_nox && !has_alt) || is_ui)))
            {


                printf(" ---> load!\n");



                module = g_module_open(filename, G_MODULE_BIND_LAZY);
                if (module == NULL)
                {
                    log_variadic_message(LMT_ERROR, 
                                         _("Error while loading the plugin candidate '%s' : %s"),
                                         filename, g_module_error());
                    goto next_file;
                }


                printf(" (main) module=%p '%s'\n", module, g_module_name(module));


                if (!g_module_symbol(module, "get_chrysalide_plugin_instance", (gpointer *)&get_instance))
                {
                    log_variadic_message(LMT_ERROR,
                                         _("No '%s' entry in plugin candidate '%s'"),
                                         "<sym>", filename);



                }


                if (get_instance == NULL)
                    plugin = NULL;
                else
                    plugin = get_instance(module);



                printf(" ===> plugin: %p\n", plugin);



                if (plugin != NULL)
                {
                    register_plugin(plugin);
                    unref_object(plugin);
                }

                else
                    g_module_close(module);

            }
            else
                log_variadic_message(LMT_INFO, _("Skipping unsuitable file for plugin: %s"), filename);

        }

 next_file:

        free(filename);
        free(namelist[k]);

    }

    free(namelist);

}


/******************************************************************************
*                                                                             *
*  Paramètres  : unused = adresse non utilisée ici.                           *
*                plugin = greffon àà venir effacer de la liste au besoin.     *
*                last   = indication sur la valeur du compteur de références. *
*                                                                             *
*  Description : Suit les variations du compteur de références d'un greffon.  *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void on_plugin_ref_toggle(gpointer unused, GPluginModule *plugin, gboolean last)
{
    const char *name;                       /* Désignation du greffon      */
    size_t index;                           /* Indice du greffon           */
    GPluginModule *same;                    /* Juste pour la récupération  */
    GModule *module;                        /* Structure de chargement GLib*/

    if (last)
    {
        assert(g_rw_lock_writer_trylock(&_pg_lock) == FALSE);

        name = g_plugin_module_get_name(plugin);

        /**
         * Les mécanismes de g_object_unref() prennent en compte la bascule d'un
         * compteur de références initialement à 2 avant appel pour déclencher
         * cet appel à on_plugin_ref_toggle() mis en place par g_object_add_toggle_ref().
         *
         * Incrémenter ce compteur à nouveau, via get_plugin_by_name(), puis le
         * décrémenter ensuite via unref_object() va conduire à une nouvelle
         * bascule des statuts de suivi dans g_object_unref().
         *
         * Il est ainsi impératif de rechercher une instance du greffon dans
         * la liste des extensions sans toucher au compteur de références.
         */

        same = _find_plugin_by_name(name, &index);

        assert(same != NULL);
        assert(same == plugin);

        _pg_list[index] = NULL;

        /**
         * Suppression de la dernière référence.
         */

        if (G_IS_NATIVE_PLUGIN(plugin))
            module = g_native_plugin_get_module(G_NATIVE_PLUGIN(plugin));
        else
            module = NULL;

        g_object_remove_toggle_ref(G_OBJECT(same), (GToggleNotify)on_plugin_ref_toggle, NULL);

        /**
         * Plus aucun code issu du greffon n'est désormais utile. Le module associé peut
         * être libéré de la mémoire.
         */

        if (module != NULL)
            g_module_close(module);

    }

}


/******************************************************************************
*                                                                             *
*  Paramètres  : plugin = greffon à ajouter aux autres disponibles.           *
*                                                                             *
*  Description : Ajoute un greffon à la liste principale de greffons.         *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void register_plugin(GPluginModule *plugin)
{
    size_t i;                               /* Boucle de parcours          */
    const char *name;                       /* Désignation du greffon      */
    const char *existing;                   /* Nom d'un greffon en place   */

    g_rw_lock_writer_lock(&_pg_lock);

    /* Recherche d'un éventuel doublon */

    name = g_plugin_module_get_name(plugin);

    for (i = 0; i < _pg_count; i++)
    {
        existing = g_plugin_module_get_name(_pg_list[i]);

        if (strcmp(name, existing) == 0)
        {
            log_variadic_message(LMT_ERROR, _("Plugin '%s' already registered!"), name);
            break;
        }

    }

    /* Ajout du greffon à la liste */

    if (i == _pg_count)
    {
        _pg_list = realloc(_pg_list, ++_pg_count * sizeof(GPluginModule));

        _pg_list[_pg_count - 1] = plugin;
        ref_object(plugin);

        g_object_add_toggle_ref(G_OBJECT(plugin), (GToggleNotify)on_plugin_ref_toggle, NULL);

    }

    g_rw_lock_writer_unlock(&_pg_lock);

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Charge tous les greffons restant à charger.                  *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void load_remaning_plugins(void)
{
    bool changed;                           /* Variation de dépendances    */
    size_t i;                               /* Boucle de parcours          */
    PluginStatusFlags flags;                /* Fanions de greffon          */

    g_rw_lock_reader_lock(&_pg_lock);

    /* Etablit la liste de toutes les dépendances */

    do
    {
        changed = false;

        for (i = 0; i < _pg_count; i++)
            changed |= g_plugin_module_resolve_dependencies(_pg_list[i], _pg_list, _pg_count);

    }
    while (changed);

    for (i = 0; i < _pg_count; i++)
    {
        flags = g_plugin_module_get_flags(_pg_list[i]);

        if (flags & PSF_UNKNOW_DEP)
            log_variadic_message(LMT_ERROR,
                                 _("There is (at least) one unknown dependency in the plugin '%s'"),
                                 g_plugin_module_get_filename(_pg_list[i]));

        else if (flags & PSF_DEP_LOOP)
            log_variadic_message(LMT_ERROR,
                                 _("There is a dependency loop in the plugin '%s'"),
                                 g_plugin_module_get_filename(_pg_list[i]));

    }

    /* Effectue les chargements possibles */

    for (i = 0; i < _pg_count; i++)
    {
        flags = g_plugin_module_get_flags(_pg_list[i]);

        if ((flags & (BROKEN_PLUGIN_STATUS | PSF_LOADED)) == 0)
            g_plugin_module_load(_pg_list[i], _pg_list, _pg_count);

    }

    /* Supprime les greffons non chargés */

    for (i = 0; i < _pg_count;)
    {
        flags = g_plugin_module_get_flags(_pg_list[i]);

        if (flags & PSF_LOADED)
            i++;

        else
        {
            unref_object(_pg_list[i]);

            assert(_pg_list[i] == NULL);

            memmove(&_pg_list[i], &_pg_list[i + 1], (_pg_count - i - 1) * sizeof(GPluginModule *));
            _pg_count--;

        }

    }

    g_rw_lock_reader_unlock(&_pg_lock);

    notify_native_plugins_loaded();

    notify_all_plugins_loaded();

}


/******************************************************************************
*                                                                             *
*  Paramètres  : name  = désignation du greffon recherché.                    *
*                index = indice du greffon trouvé. [OUT]                      *
*                                                                             *
*  Description : Fournit le greffon répondant à un nom donné.                 *
*                                                                             *
*  Retour      : Instance du greffon trouvé ou NULL si aucun.                 *
*                                                                             *
*  Remarques   : Le compteur de référence d'un greffon trouvé n'est pas       *
*                modifié.                                                     *
*                                                                             *
******************************************************************************/

static GPluginModule *_find_plugin_by_name(const char *name, size_t *index)
{
    GPluginModule *result;                  /* Greffon trouvé à renvoyer   */
    size_t i;                               /* Boucle de parcours          */
    const char *current;                    /* Nom du greffon courant      */

    result = NULL;

    /**
     * L'accès à la liste doit être encadré.
     */
    assert(g_rw_lock_writer_trylock(&_pg_lock) == FALSE);

    for (i = 0; i < _pg_count && result == NULL; i++)
    {
        /* Si on est en train de procéder à un nettoyage... */
        if (_pg_list[i] == NULL) continue;

        current = g_plugin_module_get_name(_pg_list[i]);

        if (strcmp(current, name) == 0)
        {
            result = _pg_list[i];

            if (index != NULL)
                *index = i;

        }

    }

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : name  = désignation du greffon recherché.                    *
*                index = indice du greffon trouvé. [OUT]                      *
*                                                                             *
*  Description : Fournit le greffon répondant à un nom donné.                 *
*                                                                             *
*  Retour      : Instance du greffon trouvé ou NULL si aucun.                 *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

GPluginModule *get_plugin_by_name(const char *name, size_t *index)
{
    GPluginModule *result;                  /* Greffon trouvé à renvoyer   */

    result = _find_plugin_by_name(name, index);

    if (result != NULL)
        ref_object(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : count = nombre de greffons trouvés. [OUT]                    *
*                                                                             *
*  Description : Fournit la liste de l'ensemble des greffons.                 *
*                                                                             *
*  Retour      : Liste de tous les greffons chargés.                          *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

GPluginModule **get_all_plugins(size_t *count)
{
    GPluginModule **result;                 /* Liste à retourner           */
    size_t i;                               /* Boucle de parcours          */

    g_rw_lock_reader_lock(&_pg_lock);

    result = malloc(_pg_count * sizeof(GPluginModule *));
    *count = _pg_count;

    for (i = 0; i < _pg_count; i++)
    {
        result[i] = _pg_list[i];
        ref_object(result[i]);
    }

    g_rw_lock_reader_unlock(&_pg_lock);

    return result;

}