/*************************************************************
 * gnc-module.c -- loadable plugin/module system for gnucash
 * Copyright 2001 Linux Developers Group, Inc.
 *************************************************************/
/********************************************************************\
 * This program 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 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program 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, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
 * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
 *                                                                  *
\********************************************************************/


#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gmodule.h>
#include <sys/types.h>
#ifdef HAVE_DIRENT_H
# include <dirent.h>
#endif

#include "gnc-module.h"

static GHashTable * loaded_modules = NULL;
static GList      * module_info = NULL;

typedef struct
{
    char * module_path;
    char * module_description;
    char * module_filepath;
    int    module_interface;
    int    module_age;
    int    module_revision;
} GNCModuleInfo;

typedef struct
{
    GModule       * gmodule;
    gchar         * filename;
    int           load_count;
    GNCModuleInfo * info;
    int           (* init_func)(int refcount);
} GNCLoadedModule;

static GNCModuleInfo * gnc_module_get_info(const char * lib_path);

/*************************************************************
 * gnc_module_system_search_dirs
 * return a list of dirs to look in for gnc_module libraries
 *************************************************************/

static GList *
gnc_module_system_search_dirs(void)
{
    const char *spath = g_getenv("GNC_MODULE_PATH");
    GList * list    = NULL;
    GString * token = g_string_new(NULL);
    int   escchar   = 0;
    const char *cpos;

    if (!spath)
    {
        spath = DEFAULT_MODULE_PATH;
    }

    for (cpos = spath; *cpos; cpos++)
    {
        switch (*cpos)
        {
#ifndef G_OS_WIN32
            /* On windows, with '\' as the directory separator character,
               this additional de-quoting will make every path processing
               fail miserably. Anyway this should probably be thrown out
               altogether, because this additional level of de-quoting
               (after shell quoting) is completely unexpected and
               uncommon. */
        case '\\':
            if (!escchar)
            {
                escchar = TRUE;
            }
            else
            {
                g_string_append_c(token, *cpos);
                escchar = FALSE;
            }
            break;
#endif

            /* This is ':' on UNIX machines and ';' under Windows. */
        case G_SEARCHPATH_SEPARATOR:
            if (!escchar)
            {
                char *token_str = g_string_free (token, FALSE);
                list = g_list_append (list, token_str);
                token = g_string_new(NULL);
            }
            else
            {
                g_string_append_c(token, *cpos);
                escchar = FALSE;
            }
            break;

        default:
            g_string_append_c(token, *cpos);
            escchar = FALSE;
            break;
        }
    }
    if (token->len)
    {
        char *token_str = g_string_free (token, FALSE);
        list = g_list_append(list, token_str);
    }
    else
    {
        g_string_free(token, TRUE);
    }
    return list;
}

/*************************************************************
 * gnc_module_system_init
 * initialize the module system
 *************************************************************/
void
gnc_module_system_init(void)
{
    if (loaded_modules)
        return;

    loaded_modules = g_hash_table_new(g_direct_hash, g_direct_equal);

    /* now crawl the GNC_MODULE_PATH to find likely libraries */
    gnc_module_system_refresh();
}

static inline gboolean
exclude_module (const char* module)
{
     /* These modules were converted to shared libraries and removed
      * from GnuCash 4. Simply dlopening them will introduce duplicate
      * symbols and cause unwanted initializations that may lead to a
      * crash. See https://bugs.gnucash.org/show_bug.cgi?id=798229.
      */
   static const char* excluded_modules[] =
    {
        "libgncmod-app-utils",
        "libgncmod-bi-import",
        "libgncmod-csv-export",
        "libgncmod-csv-import",
        "libgncmod-customer-import",
        "libgncmod-engine",
        "libgncmod-generic-import",
        "libgncmod-gnome-search",
        "libgncmod-gnome-utils",
        "libgncmod-html",
        "libgncmod-ledger-core",
        "libgncmod-locale-reports-us",
        "libgncmod-log-replay",
        "libgncmod-qif-import",
        "libgncmod-register-core",
        "libgncmod-register-gnome",
        "libgncmod-report-gnome",
        "libgncmod-report-system",
        "libgncmod-stylesheets",
        "libgncmod-tax-us"
    };
    static const unsigned len = G_N_ELEMENTS (excluded_modules);
    unsigned namelen = strchr(module, '.') ?
        strchr(module, '.') - module : strlen (module);
    for (unsigned i = 0; i < len; ++i)
        if (strncmp(excluded_modules[i], module, MIN(namelen, strlen(excluded_modules[i]))) == 0)
            return TRUE;
    return FALSE;
}

/*************************************************************
 * gnc_module_system_refresh
 * build the database of modules by looking through the
 * GNC_MODULE_PATH
 *************************************************************/

void
gnc_module_system_refresh(void)
{
    GList * search_dirs;
    GList * current;

    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    /* get the GNC_MODULE_PATH and split it into directories */
    search_dirs = gnc_module_system_search_dirs();

    /* look in each search directory */
    for (current = search_dirs; current; current = current->next)
    {
        GDir *d = g_dir_open(current->data, 0, NULL);
        const gchar *dent = NULL;
        char * fullpath = NULL;
        GNCModuleInfo * info;

        if (!d) continue;

        while ((dent = g_dir_read_name(d)) != NULL)
        {
            /* is the file a loadable module? */

            /* Gotcha: On MacOS, G_MODULE_SUFFIX is defined as "so",
             * but if we do not build clean libtool modules with
             * "-module", we get dynamic libraries ending in .dylib
             * On Windows, all modules will move to bin/, so they will
             * be mixed with other libraries, such as gtk+. Adding a
             * prefix "libgncmod" filter will prevent the module loader
             * from loading other libraries. The filter should work on
             * other platforms.
             */
            if ((g_str_has_suffix(dent, "." G_MODULE_SUFFIX)
                 || g_str_has_suffix(dent, ".dylib"))
                && g_str_has_prefix(dent, GNC_MODULE_PREFIX)
                && !exclude_module(dent))
            {
                /* get the full path name, then dlopen the library and see
                 * if it has the appropriate symbols to be a gnc_module */
                fullpath = g_build_filename((const gchar *)(current->data),
                                            dent, (char*)NULL);
                info     = gnc_module_get_info(fullpath);

                if (info)
                {
                    module_info = g_list_prepend(module_info, info);
                }
                g_free(fullpath);
            }
        }
        g_dir_close(d);

    }
    /* free the search dir strings */
    g_list_free_full (search_dirs, g_free);
}


/*************************************************************
 *  gnc_module_system_modinfo
 *  return the list of module information
 *************************************************************/

GList *
gnc_module_system_modinfo(void)
{
    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    return module_info;
}


/*
 *  gnc_module_get_symbol
 *  gets the munged symbol from the file
 */
static gboolean
gnc_module_get_symbol(GModule* gmodule, const char* symbol, gpointer res)
{
    gchar** strs;
    gchar* munged_symbol;
    gchar *basename;
    gboolean ret;

    g_return_val_if_fail(gmodule, FALSE);
    g_return_val_if_fail(symbol, FALSE);

    /* Separate the file from its extension */
    /* Note: This currently does not work with versioned libtool dlls,
     * as they are named like libgncmodbaz-0.dll */
    basename = g_path_get_basename(g_module_name(gmodule));
    strs = g_strsplit(basename, ".", 2);
    g_free(basename);

    /* Translate any dashes to underscores */
    g_strdelimit(strs[0], "-", '_');

    /* Create the symbol <filename>_<symbol> and retrieve that symbol */
    munged_symbol = g_strdup_printf("%s_%s", strs[0], symbol);
    ret = g_module_symbol(gmodule, munged_symbol, res);

    /* printf("(%d) Looking for symbol %s\n", ret, munged_symbol); */

    /* Free everything */
    g_strfreev(strs);
    g_free(munged_symbol);
    return ret;
}

/*************************************************************
 *  gnc_module_get_info
 *  check a proposed gnc_module by looking for specific symbols in it;
 *  if it's a gnc_module, return a struct describing it.
 *************************************************************/

static GNCModuleInfo *
gnc_module_get_info(const char * fullpath)
{
    GModule *gmodule;
    gpointer modsysver;
    GNCModuleInfo *info = NULL;
    gpointer initfunc, pathfunc, descripfunc, iface, revision, age;
    gchar * (* f_path)(void);
    gchar * (* f_descrip)(void);

    /*   g_debug("(init) dlopening '%s'\n", fullpath); */
    gmodule = g_module_open(fullpath, G_MODULE_BIND_LAZY);
    if (gmodule == NULL)
    {
        g_debug("Failed to dlopen() '%s': %s\n", fullpath, g_module_error());
        return NULL;
    }

    /* the modsysver tells us what the expected symbols and their
     * types are */
    if (!gnc_module_get_symbol(gmodule, "gnc_module_system_interface", &modsysver))
    {
        /*       g_debug("Module '%s' does not contain 'gnc_module_system_interface'\n", */
        /*                 fullpath); */
        goto get_info_close;
    }

    if (*(int *)modsysver != 0)
    {
        g_warning("Module '%s' requires newer module system\n", fullpath);
        goto get_info_close;
    }

    if (!gnc_module_get_symbol(gmodule, "gnc_module_init", &initfunc) ||
            !gnc_module_get_symbol(gmodule, "gnc_module_path", &pathfunc) ||
            !gnc_module_get_symbol(gmodule, "gnc_module_description", &descripfunc) ||
            !gnc_module_get_symbol(gmodule, "gnc_module_current", &iface) ||
            !gnc_module_get_symbol(gmodule, "gnc_module_revision", &revision) ||
            !gnc_module_get_symbol(gmodule, "gnc_module_age", &age))
    {
        g_warning("Module '%s' does not match module signature\n", fullpath);
        goto get_info_close;
    }

    /* we have found a gnc_module. */
    info = g_new0(GNCModuleInfo, 1);
    f_path                   = pathfunc;
    f_descrip                = descripfunc;
    info->module_path        = f_path();
    info->module_description = f_descrip();
    info->module_filepath    = g_strdup(fullpath);
    info->module_interface   = *(int *)iface;
    info->module_age         = *(int *)age;
    info->module_revision    = *(int *)revision;

    g_module_make_resident(gmodule);
get_info_close:
    /*   g_debug("(init) closing '%s'\n", fullpath); */
    g_module_close(gmodule);

    return info;
}


/*************************************************************
 * gnc_module_locate
 * find the best matching module for the name, interface pair
 *************************************************************/

static GNCModuleInfo *
gnc_module_locate(const gchar * module_name, int iface)
{
    GNCModuleInfo * best    = NULL;
    GNCModuleInfo * current = NULL;
    GList * lptr;

    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    for (lptr = module_info; lptr; lptr = lptr->next)
    {
        current = lptr->data;
        if (!strcmp(module_name, current->module_path) &&
                (iface >= (current->module_interface - current->module_age)) &&
                (iface <= current->module_interface))
        {
            if (best)
            {
                if ((current->module_interface > best->module_interface) ||
                        ((current->module_interface == best->module_interface) &&
                         (current->module_age > best->module_age)) ||
                        ((current->module_interface == best->module_interface) &&
                         (current->module_age == best->module_age) &&
                         (current->module_revision > best->module_revision)))
                {
                    best = current;
                }
            }
            else
            {
                best = current;
            }
        }
    }
    return best;
}

static void
list_loaded (gpointer k, gpointer v, gpointer data)
{
    GList ** l = data;
    *l = g_list_prepend(*l, v);
}

static GNCLoadedModule *
gnc_module_check_loaded(const char * module_name, gint iface)
{
    GNCModuleInfo * modinfo = gnc_module_locate(module_name, iface);
    GList * modules = NULL;
    GList * p = NULL;
    GNCLoadedModule * rv = NULL;

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

    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    /* turn the loaded-modules table into a list */
    g_hash_table_foreach(loaded_modules, list_loaded, &modules);

    /* walk the list to see if the file we want is already open */
    for (p = modules; p; p = p->next)
    {
        GNCLoadedModule * lm = p->data;
        if (!strcmp(lm->filename, modinfo->module_filepath))
        {
            rv = lm;
            break;
        }
    }
    g_list_free(modules);
    return rv;
}


/*************************************************************
 * gnc_module_load
 * Ensure that the module named by "module_name" is loaded.
 *************************************************************/

static GNCModule
gnc_module_load_common(const char * module_name, gint iface, gboolean optional)
{

    GNCLoadedModule * info;
    GModule         * gmodule;
    GNCModuleInfo   * modinfo;

    g_debug ("module_name: %s", module_name);

    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    info = gnc_module_check_loaded(module_name, iface);

    /* if the module's already loaded, just increment its use count.
     * otherwise, load it and check for the initializer
     * "gnc_module_init".  if we find that, assume it's a gnucash module
     * and run the function. */

    if (info)
    {
        /* module already loaded ... call the init thunk */
        if (info->init_func)
        {
            if (info->init_func(info->load_count))
            {
                info->load_count++;
                g_debug ("module %s already loaded", module_name);
                return info;
            }
            else
            {
                g_warning ("module init failed: %s", module_name);
                return NULL;
            }
        }
        else
        {
            g_warning ("module has no init func: %s", module_name);
            return NULL;
        }
        /* NOTREACHED */
        g_error("internal error");
        return NULL;
    }

    modinfo = gnc_module_locate(module_name, iface);
    if (!modinfo)
    {
        if (optional)
        {
            g_message ("Could not locate optional module %s interface v.%d",
                       module_name, iface);
        }
        else
        {
            g_warning ("Could not locate module %s interface v.%d",
                       module_name, iface);
        }
        return NULL;
    }

    /*     if (modinfo) */
    /*       g_debug("(init) loading '%s' from '%s'\n", module_name, */
    /*               modinfo->module_filepath); */

    if ((gmodule = g_module_open(modinfo->module_filepath, 0)) != NULL)
    {
        gpointer initfunc;

        if (gnc_module_get_symbol(gmodule, "gnc_module_init", &initfunc))
        {
            /* stick it in the hash table */
            info = g_new0(GNCLoadedModule, 1);
            info->gmodule    = gmodule;
            info->filename   = g_strdup(modinfo->module_filepath);
            info->load_count = 1;
            info->init_func  = initfunc;
            g_hash_table_insert(loaded_modules, info, info);

            /* now call its init function.  this should load any dependent
             * modules, too.  If it doesn't return TRUE unload the module. */
            if (!info->init_func(0))
            {
                /* init failed. unload the module. */
                g_warning ("Initialization failed for module %s", module_name);
                g_hash_table_remove(loaded_modules, info);
                g_free(info->filename);
                g_free(info);
                /* g_module_close(module); */
                return NULL;
            }

            return info;
        }
        else
        {
            g_warning ("Module %s (%s) is not a gnc-module.\n", module_name,
                       modinfo->module_filepath);
            //lt_dlclose(handle);
        }
        return info;
    }

    g_warning ("Failed to open module %s: %s\n", module_name, g_module_error());

    return NULL;
}


GNCModule
gnc_module_load(const char * module_name, gint iface)
{
    return gnc_module_load_common(module_name, iface, FALSE);
}

GNCModule
gnc_module_load_optional(const char * module_name, gint iface)
{
    return gnc_module_load_common(module_name, iface, TRUE);
}

/*************************************************************
 * gnc_module_unload
 * unload a module (only actually unload it if the use count goes to 0)
 *************************************************************/

int
gnc_module_unload(GNCModule module)
{
    GNCLoadedModule * info;

    if (!loaded_modules)
    {
        gnc_module_system_init();
    }

    if ((info = g_hash_table_lookup(loaded_modules, module)) != NULL)
    {
        gpointer unload_thunk;
        int unload_val = TRUE;

        info->load_count--;
        if (gnc_module_get_symbol(info->gmodule, "gnc_module_end", &unload_thunk))
        {
            int (* thunk)(int) = unload_thunk;
            unload_val = thunk(info->load_count);
        }

        /* actually unload the module if necessary */
        if (info->load_count == 0)
        {
            /* now close the module and free the struct */
            /* g_debug("(unload) closing %s\n", info->filename); */
            /* g_module_close(info->gmodule); */
            g_hash_table_remove(loaded_modules, module);
            g_free(info);
        }
        return unload_val;
    }
    else
    {
        g_warning ("Failed to unload module %p (it is not loaded)\n", module);
        return 0;
    }
}

