/*
 * SwamiPlugin.c - Swami plugin system
 *
 * Swami
 * Copyright (C) 1999-2014 Element Green <element@elementsofsound.org>
 *
 * Inspired by gstplugin from GStreamer (although re-coded)
 *
 * 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; version 2
 * of the License only.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or point your web browser to http://www.gnu.org.
 */
#include <string.h>

#include <libinstpatch/misc.h>	/* ipatch_gerror_message() */

#include "SwamiPlugin.h"
#include "SwamiLog.h"
#include "version.h"
#include "i18n.h"

#ifdef __APPLE__
#include <CoreFoundation.h>
#endif

enum
{
    PROP_0,
    PROP_MODULE,
    PROP_LOADED,
    PROP_NAME,
    PROP_FILENAME,
    PROP_VERSION,
    PROP_AUTHOR,
    PROP_COPYRIGHT,
    PROP_DESCR,
    PROP_LICENSE
};

static GList *swami_plugins = NULL; /* global list of plugins */
static int plugin_seqno = 0;	/* plugin counter (for stats) */
static GList *plugin_paths = NULL; /* plugin search paths */

static GObjectClass *parent_class = NULL;


static void swami_plugin_free_plugin_path(char *path, gpointer user_data);
static void swami_plugin_class_init(SwamiPluginClass *klass);
static void swami_plugin_finalize(GObject *object);
static void swami_plugin_set_property(GObject *object, guint property_id,
                                      const GValue *value, GParamSpec *pspec);
static void swami_plugin_get_property(GObject *object, guint property_id,
                                      GValue *value, GParamSpec *pspec);
static gboolean swami_plugin_type_module_load(GTypeModule *type_module);
static void swami_plugin_type_module_unload(GTypeModule *type_module);
static gboolean swami_plugin_load_recurse(char *file, char *name);

/* default plugins directory */
static const char * plugins_dir = PLUGINS_DIR;

/* initialize plugin system */
void
_swami_plugin_initialize(void)
{
    gchar *path = NULL;

#if defined (__APPLE__)
    CFURLRef ResDirURL;
    gchar buf[PATH_MAX];

    /* Use plugins within the bundle if we are in an app bundle */
    ResDirURL = CFBundleCopyBuiltInPlugInsURL(CFBundleGetMainBundle());

    if(CFURLGetFileSystemRepresentation(ResDirURL, TRUE, (UInt8 *)buf, PATH_MAX)
            && g_str_has_suffix(buf, ".app/Contents/PlugIns"))
    {
        path = g_strdup(buf);
    }

    CFRelease(ResDirURL);
#elif defined (G_OS_WIN32)
    gchar *appdir;

    appdir = g_win32_get_package_installation_directory_of_module(NULL);

    if(appdir)
    {
        path = g_build_filename(appdir, "plugins", NULL);   /* !! never freed */
        g_free(appdir);     /* -- free appdir */
    }

#endif

    if(path)
    {
        plugin_paths = g_list_append(plugin_paths, path);
    }
    else
    {
        plugin_paths = g_list_append (plugin_paths, (gpointer)plugins_dir);
    }
}

GType
swami_plugin_get_type(void)
{
    static GType obj_type = 0;

    if(!obj_type)
    {
        static GTypeInfo obj_info =
        {
            sizeof(SwamiPluginClass), NULL, NULL,
            (GClassInitFunc) swami_plugin_class_init, NULL, NULL,
            sizeof(SwamiPlugin), 0,
            (GInstanceInitFunc) NULL,
        };

        obj_type = g_type_register_static(G_TYPE_TYPE_MODULE, "SwamiPlugin",
                                          &obj_info, 0);
    }

    return (obj_type);
}

static void
swami_plugin_class_init(SwamiPluginClass *klass)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    GTypeModuleClass *module_class = G_TYPE_MODULE_CLASS(klass);

    parent_class = g_type_class_peek_parent(klass);

    obj_class->finalize = swami_plugin_finalize;
    obj_class->set_property = swami_plugin_set_property;
    obj_class->get_property = swami_plugin_get_property;

    module_class->load = swami_plugin_type_module_load;
    module_class->unload = swami_plugin_type_module_unload;

    g_object_class_install_property(obj_class, PROP_MODULE,
                                    g_param_spec_pointer("module", "Module",
                                            "GModule of plugin",
                                            G_PARAM_READABLE));
    g_object_class_install_property(obj_class, PROP_LOADED,
                                    g_param_spec_boolean("loaded", "Loaded",
                                            "Loaded state of plugin",
                                            FALSE,
                                            G_PARAM_READABLE));

    g_object_class_install_property(obj_class, PROP_NAME,
                                    g_param_spec_string("name", "Name",
                                            "Name of plugin",
                                            NULL,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_FILENAME,
                                    g_param_spec_string("file-name", "File Name",
                                            "Name of plugin file",
                                            NULL,
                                            G_PARAM_READABLE));
    g_object_class_install_property(obj_class, PROP_VERSION,
                                    g_param_spec_string("version", "Version",
                                            "Plugin specific version",
                                            NULL,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_AUTHOR,
                                    g_param_spec_string("author", "Author",
                                            "Author of the plugin",
                                            NULL,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_COPYRIGHT,
                                    g_param_spec_string("copyright", "Copyright",
                                            "Copyright string",
                                            NULL,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_DESCR,
                                    g_param_spec_string("descr", "Description",
                                            "Description of plugin",
                                            NULL,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_LICENSE,
                                    g_param_spec_string("license", "License",
                                            "Plugin license type",
                                            NULL,
                                            G_PARAM_READWRITE));
}

static void
swami_plugin_finalize(GObject *object)
{
    SwamiPlugin *plugin = SWAMI_PLUGIN(object);

    g_free(plugin->filename);
    g_free(plugin->version);
    g_free(plugin->author);
    g_free(plugin->copyright);
    g_free(plugin->descr);
    g_free(plugin->license);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
swami_plugin_set_property(GObject *object, guint property_id,
                          const GValue *value, GParamSpec *pspec)
{
    SwamiPlugin *plugin = SWAMI_PLUGIN(object);

    switch(property_id)
    {
    case PROP_NAME:
        g_type_module_set_name(G_TYPE_MODULE(plugin),
                               g_value_get_string(value));
        break;

    case PROP_VERSION:
        g_free(plugin->version);
        plugin->version = g_value_dup_string(value);
        break;

    case PROP_AUTHOR:
        g_free(plugin->author);
        plugin->author = g_value_dup_string(value);
        break;

    case PROP_COPYRIGHT:
        g_free(plugin->copyright);
        plugin->copyright = g_value_dup_string(value);
        break;

    case PROP_DESCR:
        g_free(plugin->descr);
        plugin->descr = g_value_dup_string(value);
        break;

    case PROP_LICENSE:
        g_free(plugin->license);
        plugin->license = g_value_dup_string(value);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void
swami_plugin_get_property(GObject *object, guint property_id, GValue *value,
                          GParamSpec *pspec)
{
    SwamiPlugin *plugin = SWAMI_PLUGIN(object);

    switch(property_id)
    {
    case PROP_MODULE:
        g_value_set_pointer(value, plugin->module);
        break;

    case PROP_LOADED:
        g_value_set_boolean(value, plugin->module != NULL);
        break;

    case PROP_NAME:
        g_value_set_string(value, G_TYPE_MODULE(plugin)->name);
        break;

    case PROP_FILENAME:
        g_value_set_string(value, plugin->filename);
        break;

    case PROP_VERSION:
        g_value_set_string(value, plugin->version);
        break;

    case PROP_AUTHOR:
        g_value_set_string(value, plugin->author);
        break;

    case PROP_COPYRIGHT:
        g_value_set_string(value, plugin->copyright);
        break;

    case PROP_DESCR:
        g_value_set_string(value, plugin->descr);
        break;

    case PROP_LICENSE:
        g_value_set_string(value, plugin->license);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static gboolean
swami_plugin_type_module_load(GTypeModule *type_module)
{
    SwamiPlugin *plugin = SWAMI_PLUGIN(type_module);
    SwamiPluginInfo *info;
    GError *err = NULL;

    g_return_val_if_fail(SWAMI_IS_PLUGIN(plugin), FALSE);
    g_return_val_if_fail(plugin->filename != NULL, FALSE);
    g_return_val_if_fail(plugin->module == NULL, FALSE);

    plugin->module = g_module_open(plugin->filename, G_MODULE_BIND_LAZY);

    if(!plugin->module)
    {
        g_critical(_("Failed to load plugin \"%s\": %s"), plugin->filename,
                   g_module_error());
        return (FALSE);
    }

    if(!g_module_symbol(plugin->module, "swami_plugin_info",
                        (gpointer *)&info))
    {
        g_critical(_("Symbol \"swami_plugin_info\" not found in plugin \"%s\""),
                   plugin->filename);
        goto fail;
    }

    if(strncmp(info->magic, SWAMI_PLUGIN_MAGIC, 4) != 0)
    {
        g_critical(_("Invalid Swami plugin magic number"));
        goto fail;
    }

    if(strcmp(info->swami_version, SWAMI_VERSION) != 0)
    {
        g_critical(_("Plugin compiled for Swami version %s but loading on %s"),
                   info->swami_version, SWAMI_VERSION);
        goto fail;
    }

    if(info->init && !(*info->init)(plugin, &err))
    {
        g_critical(_("Plugin init function failed: %s"),
                   ipatch_gerror_message(err));
        g_clear_error(&err);
        goto fail;
    }

    plugin->init = info->init;
    plugin->exit = info->exit;

    return (TRUE);

fail:
    g_module_close(plugin->module);
    plugin->module = NULL;

    return (FALSE);
}

static void
swami_plugin_type_module_unload(GTypeModule *type_module)
{
    SwamiPlugin *plugin = SWAMI_PLUGIN(type_module);

    g_return_if_fail(plugin->module != NULL);

    if(plugin->exit)    /* call the plugin's exit function (if any) */
    {
        (*plugin->exit)(plugin);
    }

    plugin->init = NULL;
    plugin->exit = NULL;

    g_module_close(plugin->module);
    plugin->module = NULL;
}

/**
 * swami_plugin_add_path:
 * @path: The directory to add to the search path
 *
 * Add a directory to the path searched for plugins.
 */
void
swami_plugin_add_path(const char *path)
{
    plugin_paths = g_list_append(plugin_paths, g_strdup(path));
}

/**
 * swami_plugin_load_all:
 *
 * Load all plugins in the plugin search path.
 */
void
swami_plugin_load_all(void)
{
    GList *path;

    path = plugin_paths;

    if(path == NULL)
    {
        g_warning("Plugin search path is empty");
    }

    while(path)
    {
        /* g_message (_("Loading plugins from %s"), (char *)path->data); */
        swami_plugin_load_recurse(path->data, NULL);
        path = g_list_next(path);
    }

    /* g_message (_("Loaded %d plugins"), plugin_seqno); */
}

static gboolean
swami_plugin_load_recurse(char *file, char *name)
{
    GDir *dir;
    const gchar *d_name;
    gboolean loaded = FALSE;
    char *dirname, *temp;

    dir = g_dir_open(file, 0, NULL);

    if(dir)			/* is "file" a directory or file? */
    {
        while((d_name = g_dir_read_name(dir)))   /* its a directory, open it */
        {
            /* don't want to recurse in place or backwards */
            if(strcmp(d_name, ".") && strcmp(d_name, ".."))
            {
                dirname = g_strjoin(G_DIR_SEPARATOR_S, file,
                                    d_name, NULL);
                loaded = swami_plugin_load_recurse(dirname, name);
                g_free(dirname);

                if(loaded && name)  /* done searching for a specific plugin? */
                {
                    g_dir_close(dir);
                    return TRUE;
                }
            }
        }

        g_dir_close(dir);
    }
    else
    {
        /* search for shared library extension */
        temp = g_strrstr(file, "." G_MODULE_SUFFIX);

        /* make sure file ends with shared library extension */
        if(temp && strcmp(temp, "." G_MODULE_SUFFIX) == 0)
        {
            /* if we aren't searching for a specific plugin or this file name
               matches the search, load the plugin */
            if(!name || ((temp = strstr(file, name))
                         && strcmp(temp, name) == 0))
            {
                loaded = swami_plugin_load_absolute(file);
            }
        }
    }

    return (loaded);
}

/**
 * swami_plugin_load:
 * @filename: File name of plugin to load
 *
 * Load the named plugin.  Name should be the plugin file name
 * (&quot;libplugin.so&quot; on Linux for example).
 *
 * Returns: Whether the plugin was loaded or not
 */
gboolean
swami_plugin_load(const char *filename)
{
    GList *path;
    char *pluginname;
    SwamiPlugin *plugin;

    g_return_val_if_fail(filename != NULL, FALSE);

    plugin = swami_plugin_find(filename);

    if(plugin && plugin->module)
    {
        return (TRUE);
    }

    path = plugin_paths;

    while(path)
    {
        pluginname = g_module_build_path(path->data, filename);

        if(swami_plugin_load_absolute(pluginname))
        {
            g_free(pluginname);
            return (TRUE);
        }

        g_free(pluginname);

        path = g_list_next(path);
    }

    return (FALSE);
}

/**
 * swami_plugin_load_absolute:
 * @filename: Name of plugin to load
 *
 * Load the named plugin.  Name should be the full path and file name of the
 * plugin to load.
 *
 * Returns: whether the plugin was loaded or not
 */
gboolean
swami_plugin_load_absolute(const char *filename)
{
    SwamiPlugin *plugin = NULL;
    GList *plugins = swami_plugins;

    g_return_val_if_fail(filename != NULL, FALSE);

    while(plugins)		/* see if plugin already exists */
    {
        SwamiPlugin *testplugin = (SwamiPlugin *)plugins->data;

        if(testplugin->filename && strcmp(testplugin->filename, filename) == 0)
        {
            plugin = testplugin;
            break;
        }

        plugins = g_list_next(plugins);
    }

    if(!plugin)
    {
        plugin = g_object_new(SWAMI_TYPE_PLUGIN, NULL);
        plugin->filename = g_strdup(filename);
        swami_plugins = g_list_prepend(swami_plugins, plugin);
        plugin_seqno++;
    }

    return (g_type_module_use(G_TYPE_MODULE(plugin)));
}

/**
 * swami_plugin_load_plugin:
 * @plugin: The plugin to load
 *
 * Load the given plugin.
 *
 * Returns: %TRUE on success (loaded or already loaded), %FALSE otherwise
 */
gboolean
swami_plugin_load_plugin(SwamiPlugin *plugin)
{
    g_return_val_if_fail(SWAMI_IS_PLUGIN(plugin), FALSE);

    if(plugin->module)
    {
        return (TRUE);
    }

    return (g_type_module_use(G_TYPE_MODULE(plugin)));
}

/**
 * swami_plugin_is_loaded:
 * @plugin: Plugin to query
 *
 * Queries if the plugin is loaded into memory
 *
 * Returns: %TRUE is loaded, %FALSE otherwise
 */
gboolean
swami_plugin_is_loaded(SwamiPlugin *plugin)
{
    g_return_val_if_fail(plugin != NULL, FALSE);

    return (plugin->module != NULL);
}

/**
 * swami_plugin_find:
 * @name: Name of plugin to find
 *
 * Search the list of registered plugins for one with the given plugin name
 * (not file name).
 *
 * Returns: Pointer to the #SwamiPlugin if found, %NULL otherwise
 */
SwamiPlugin *
swami_plugin_find(const char *name)
{
    GList *plugins = swami_plugins;

    g_return_val_if_fail(name != NULL, NULL);

    while(plugins)
    {
        SwamiPlugin *plugin = (SwamiPlugin *)plugins->data;

        if(G_TYPE_MODULE(plugin)->name
                && strcmp(G_TYPE_MODULE(plugin)->name, name) == 0)
        {
            return (plugin);
        }

        plugins = g_list_next(plugins);
    }

    return (NULL);
}

/**
 * swami_plugin_get_list:
 *
 * get the currently loaded plugins
 *
 * Returns: a GList of SwamiPlugin elements which should be freed with
 *   g_list_free when finished with.
 */
GList *
swami_plugin_get_list(void)
{
    return (g_list_copy(swami_plugins));
}

/**
 * swami_plugin_save_xml:
 * @plugin: Swami plugin
 * @xmlnode: XML node to save plugin state to
 * @err: Location to store error info or %NULL to ignore
 *
 * Save a plugin's preferences state to XML.  Not all plugins implement this
 * method.  Check if the xml_save field is set for @plugin before calling, just
 * returns %TRUE if not implemented.
 *
 * Returns: %TRUE on success, %FALSE otherwise (in which case @err may be set)
 */
gboolean
swami_plugin_save_xml(SwamiPlugin *plugin, GNode *xmlnode, GError **err)
{
    g_return_val_if_fail(SWAMI_IS_PLUGIN(plugin), FALSE);
    g_return_val_if_fail(xmlnode != NULL, FALSE);
    g_return_val_if_fail(!err || !*err, FALSE);

    if(!plugin->save_xml)
    {
        return (TRUE);
    }

    return (plugin->save_xml(plugin, xmlnode, err));
}

/**
 * swami_plugin_load_xml:
 * @plugin: Swami plugin
 * @xmlnode: XML node to load plugin state from
 * @err: Location to store error info or %NULL to ignore
 *
 * Load a plugin's preferences state from XML.  Not all plugins implement this
 * method.  Check if the xml_load field is set for @plugin before calling, just
 * returns %TRUE if not implemented.
 *
 * Returns: %TRUE on success, %FALSE otherwise (in which case @err may be set)
 */
gboolean
swami_plugin_load_xml(SwamiPlugin *plugin, GNode *xmlnode, GError **err)
{
    g_return_val_if_fail(SWAMI_IS_PLUGIN(plugin), FALSE);
    g_return_val_if_fail(xmlnode != NULL, FALSE);
    g_return_val_if_fail(!err || !*err, FALSE);

    if(!plugin->load_xml)
    {
        return (TRUE);
    }

    return (plugin->load_xml(plugin, xmlnode, err));
}

/*------------------ unloading plugins---------------------------------------*/
/**
 * swami_plugin_unload_all:
 *
 * Unload and free all plugins registered in swami_plugins list.
 */
void
swami_plugin_unload_all(void)
{
    GList *plugins = swami_plugins;
    /* Unload modules */
    while (plugins)
    {
        /* unload plugin module */
        g_type_module_unuse(G_TYPE_MODULE(plugins->data));

        /* free plugin */
        g_object_unref(plugins->data);

        plugins = g_list_next(plugins);
    }
}

/* deinitialize plugin system */
void
_swami_plugin_deinitialize(void)
{
    /* free plugings_path list */
    g_list_foreach(plugin_paths, (GFunc)swami_plugin_free_plugin_path, NULL);
    g_list_free(plugin_paths);

    /* free swami_plugins list */
    g_list_free(swami_plugins);
}

/* free path */
static void swami_plugin_free_plugin_path(char *path, gpointer user_data)
{
    if(path != plugins_dir)
    {
        g_free(path);
    }
}

