/* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

/**
 * SECTION: WpModule
 *
 * A module is a shared library that can be loaded dynamically in the
 * WirePlumber daemon process, adding functionality to the daemon.
 *
 * For every module that is loaded, WirePlumber constructs a #WpModule object
 * that gets registered on the #WpCore and can be retrieved through the
 * #WpObjectManager API.
 *
 * Every module has to conform to a certain interface in order for WirePlumber
 * to know how to load it. This interface is called "ABI" in #WpModule.
 * Currently there is only one possible ABI, the "C" one.
 *
 * ### Writing modules in C
 *
 * In order to define a module in C, you need to implement a function in
 * your shared library that has this signature:
 *
 * |[
 * WP_PLUGIN_EXPORT void
 * wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
 * ]|
 *
 * This function will be called once at the time of loading the module. The
 * @args parameter is a dictionary ("a{sv}") #GVariant that contains arguments
 * for this module that were specified in WirePlumber's configuration file.
 * The @module parameter is useful for registering a destroy callback (using
 * wp_module_set_destroy_callback()), which will be called at the time the
 * module is destroyed (when WirePlumber quits) and allows you to free any
 * resources that the module has allocated.
 */

#define G_LOG_DOMAIN "wp-module"

#include "module.h"
#include "debug.h"
#include "error.h"
#include "private.h"
#include <gmodule.h>

#define WP_MODULE_INIT_SYMBOL "wireplumber__module_init"

typedef void (*WpModuleInitFunc) (WpModule *, WpCore *, GVariant *);

struct _WpModule
{
  GObject parent;

  GWeakRef core;
  GVariant *properties;
  GDestroyNotify destroy;
  gpointer destroy_data;
};

G_DEFINE_TYPE (WpModule, wp_module, G_TYPE_OBJECT)

static void
wp_module_init (WpModule * self)
{
}

static void
wp_module_finalize (GObject * object)
{
  WpModule *self = WP_MODULE (object);

  wp_trace_object (self, "unloading module");

  if (self->destroy)
    self->destroy (self->destroy_data);
  g_clear_pointer (&self->properties, g_variant_unref);
  g_weak_ref_clear (&self->core);

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

static void
wp_module_class_init (WpModuleClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  object_class->finalize = wp_module_finalize;
}

static const gchar *
get_module_dir (void)
{
  static const gchar *module_dir = NULL;
  if (!module_dir) {
    module_dir = g_getenv ("WIREPLUMBER_MODULE_DIR");
    if (!module_dir)
      module_dir = WIREPLUMBER_DEFAULT_MODULE_DIR;
  }
  return module_dir;
}

static gboolean
wp_module_load_c (WpModule * self, WpCore * core,
    const gchar * module_name, GVariant * args, GError ** error)
{
  g_autofree gchar *module_path = NULL;
  GModule *gmodule;
  gpointer module_init;
  GVariantDict properties;

  module_path = g_module_build_path (get_module_dir (), module_name);
  gmodule = g_module_open (module_path, G_MODULE_BIND_LOCAL);
  if (!gmodule) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to open module %s: %s", module_path, g_module_error ());
    return FALSE;
  }

  if (!g_module_symbol (gmodule, WP_MODULE_INIT_SYMBOL, &module_init)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to locate symbol " WP_MODULE_INIT_SYMBOL " in %s",
        module_path);
    g_module_close (gmodule);
    return FALSE;
  }

  g_variant_dict_init (&properties, NULL);
  g_variant_dict_insert (&properties, "module.name", "s", module_name);
  g_variant_dict_insert (&properties, "module.abi", "s", "C");
  g_variant_dict_insert (&properties, "module.path", "s", module_path);
  if (args) {
    g_variant_take_ref (args);
    g_variant_dict_insert_value (&properties, "module.args", args);
  }
  self->properties = g_variant_ref_sink (g_variant_dict_end (&properties));

  ((WpModuleInitFunc) module_init) (self, core, args);

  if (args)
    g_variant_unref (args);

  return TRUE;
}

/**
 * wp_module_load:
 * @core: the core
 * @abi: the abi name of the module
 * @module_name: the module name
 * @args: (transfer floating)(nullable): additional properties passed to the
 *     module ("a{sv}")
 * @error: (out) (optional): return location for errors, or NULL to ignore
 *
 * Returns: (transfer none): the loaded module
 */
WpModule *
wp_module_load (WpCore * core, const gchar * abi, const gchar * module_name,
    GVariant * args, GError ** error)
{
  g_autoptr (WpModule) module = NULL;

  module = g_object_new (WP_TYPE_MODULE, NULL);
  g_weak_ref_init (&module->core, core);

  wp_debug_object (module, "loading module %s (ABI: %s)", module_name, abi);

  if (!g_strcmp0 (abi, "C")) {
    if (!wp_module_load_c (module, core, module_name, args, error))
      return NULL;
  } else {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "unknown module ABI %s", abi);
    return NULL;
  }

  wp_registry_register_object (wp_core_get_registry (core),
      g_object_ref (module));

  return module;
}

/**
 * wp_module_get_properties:
 * @self: the module
 *
 * Returns: (transfer none): the properties of the module ("a{sv}")
 */
GVariant *
wp_module_get_properties (WpModule * self)
{
  return self->properties;
}

/**
 * wp_module_get_core:
 * @self: the module
 *
 * Returns: (transfer full): the core on which this module is registered
 */
WpCore *
wp_module_get_core (WpModule * self)
{
  return g_weak_ref_get (&self->core);
}

/**
 * wp_module_set_destroy_callback:
 * @self: the module
 * @callback: (scope async): a function to call when the module is destroyed
 * @data: (closure): data to pass to @callback
 *
 * Registers a @callback to call when the module object is destroyed
 */
void
wp_module_set_destroy_callback (WpModule * self, GDestroyNotify callback,
    gpointer data)
{
  g_return_if_fail (self->destroy == NULL);
  self->destroy = callback;
  self->destroy_data = data;
}