/* LIBPIKA - The PIKA Library
 * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
 *
 * pikamodule.c
 * (C) 1999 Austin Donnelly <austin@gimp.org>
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <gio/gio.h>

#include "libpikabase/pikabase.h"

#include "pikamodule.h"

#include "libpika/libpika-intl.h"


/**
 * SECTION: pikamodule
 * @title: PikaModule
 * @short_description: A #GTypeModule subclass which implements module
 *                     loading using #GModule.
 * @see_also: #GModule, #GTypeModule
 *
 * #PikaModule is a generic mechanism to dynamically load modules into
 * PIKA. It is a #GTypeModule subclass, implementing module loading
 * using #GModule.  #PikaModule does not know which functionality is
 * implemented by the modules, it just provides a framework to get
 * arbitrary #GType implementations loaded from disk.
 **/


enum
{
  PROP_0,
  PROP_AUTO_LOAD,
  PROP_ON_DISK,
  N_PROPS
};
static GParamSpec *obj_props[N_PROPS] = { NULL, };

struct _PikaModulePrivate
{
  GFile           *file;       /* path to the module                       */
  gboolean         auto_load;  /* auto-load the module on creation         */
  gboolean         verbose;    /* verbose error reporting                  */

  PikaModuleInfo  *info;       /* returned values from module_query        */
  PikaModuleState  state;      /* what's happened to the module            */
  gchar           *last_error;

  gboolean         on_disk;    /* TRUE if file still exists                */

  GModule         *module;     /* handle on the module                     */

  PikaModuleQueryFunc     query_module;
  PikaModuleRegisterFunc  register_module;
};


static void       pika_module_get_property   (GObject      *object,
                                              guint         property_id,
                                              GValue       *value,
                                              GParamSpec   *pspec);
static void       pika_module_set_property   (GObject      *object,
                                              guint         property_id,
                                              const GValue *value,
                                              GParamSpec   *pspec);
static void       pika_module_finalize       (GObject      *object);

static gboolean   pika_module_load           (GTypeModule  *module);
static void       pika_module_unload         (GTypeModule  *module);

static gboolean   pika_module_open           (PikaModule   *module);
static gboolean   pika_module_close          (PikaModule   *module);
static void       pika_module_set_last_error (PikaModule   *module,
                                              const gchar  *error_str);

static PikaModuleInfo * pika_module_info_new  (guint32               abi_version,
                                               const gchar          *purpose,
                                               const gchar          *author,
                                               const gchar          *version,
                                               const gchar          *copyright,
                                               const gchar          *date);
static PikaModuleInfo * pika_module_info_copy (const PikaModuleInfo *info);
static void             pika_module_info_free (PikaModuleInfo       *info);


G_DEFINE_TYPE_WITH_PRIVATE (PikaModule, pika_module, G_TYPE_TYPE_MODULE)

#define parent_class pika_module_parent_class


static void
pika_module_class_init (PikaModuleClass *klass)
{
  GObjectClass     *object_class = G_OBJECT_CLASS (klass);
  GTypeModuleClass *module_class = G_TYPE_MODULE_CLASS (klass);

  object_class->set_property = pika_module_set_property;
  object_class->get_property = pika_module_get_property;
  object_class->finalize = pika_module_finalize;

  obj_props[PROP_AUTO_LOAD] = g_param_spec_boolean ("auto-load", "auto-load", "auto-load",
                                                    FALSE, PIKA_PARAM_READWRITE);
  obj_props[PROP_ON_DISK] = g_param_spec_boolean ("on-disk", "on-disk", "on-disk",
                                                   FALSE, PIKA_PARAM_READABLE);
  g_object_class_install_properties (object_class, N_PROPS, obj_props);

  module_class->load     = pika_module_load;
  module_class->unload   = pika_module_unload;
}

static void
pika_module_init (PikaModule *module)
{
  module->priv = pika_module_get_instance_private (module);

  module->priv->state = PIKA_MODULE_STATE_ERROR;
}

static void
pika_module_set_property (GObject      *object,
                          guint         property_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  PikaModule *module = PIKA_MODULE (object);

  switch (property_id)
    {
    case PROP_AUTO_LOAD:
      pika_module_set_auto_load (module, g_value_get_boolean (value));
      break;

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

static void
pika_module_get_property (GObject    *object,
                          guint       property_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  PikaModule *module = PIKA_MODULE (object);

  switch (property_id)
    {
    case PROP_AUTO_LOAD:
      g_value_set_boolean (value, pika_module_get_auto_load (module));
      break;

    case PROP_ON_DISK:
      g_value_set_boolean (value, pika_module_is_on_disk (module));
      break;

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

static void
pika_module_finalize (GObject *object)
{
  PikaModule *module = PIKA_MODULE (object);

  g_clear_object  (&module->priv->file);
  g_clear_pointer (&module->priv->info, pika_module_info_free);
  g_clear_pointer (&module->priv->last_error, g_free);

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

static gboolean
pika_module_load (GTypeModule *module)
{
  PikaModule *pika_module = PIKA_MODULE (module);
  gpointer    func;

  g_return_val_if_fail (pika_module->priv->file != NULL, FALSE);
  g_return_val_if_fail (pika_module->priv->module == NULL, FALSE);

  if (pika_module->priv->verbose)
    g_print ("Loading module '%s'\n",
             pika_file_get_utf8_name (pika_module->priv->file));

  if (! pika_module_open (pika_module))
    return FALSE;

  if (! pika_module_query_module (pika_module))
    return FALSE;

  /* find the pika_module_register symbol */
  if (! g_module_symbol (pika_module->priv->module, "pika_module_register",
                         &func))
    {
      pika_module_set_last_error (pika_module,
                                  "Missing pika_module_register() symbol");

      g_message (_("Module '%s' load error: %s"),
                 pika_file_get_utf8_name (pika_module->priv->file),
                 pika_module->priv->last_error);

      pika_module_close (pika_module);

      pika_module->priv->state = PIKA_MODULE_STATE_ERROR;

      return FALSE;
    }

  pika_module->priv->register_module = func;

  if (! pika_module->priv->register_module (module))
    {
      pika_module_set_last_error (pika_module,
                                  "pika_module_register() returned FALSE");

      g_message (_("Module '%s' load error: %s"),
                 pika_file_get_utf8_name (pika_module->priv->file),
                 pika_module->priv->last_error);

      pika_module_close (pika_module);

      pika_module->priv->state = PIKA_MODULE_STATE_LOAD_FAILED;

      return FALSE;
    }

  pika_module->priv->state = PIKA_MODULE_STATE_LOADED;

  return TRUE;
}

static void
pika_module_unload (GTypeModule *module)
{
  PikaModule *pika_module = PIKA_MODULE (module);

  g_return_if_fail (pika_module->priv->module != NULL);

  if (pika_module->priv->verbose)
    g_print ("Unloading module '%s'\n",
             pika_file_get_utf8_name (pika_module->priv->file));

  pika_module_close (pika_module);
}


/*  public functions  */

/**
 * pika_module_new:
 * @file:      A #GFile pointing to a loadable module.
 * @auto_load: Pass %TRUE to exclude this module from auto-loading.
 * @verbose:   Pass %TRUE to enable debugging output.
 *
 * Creates a new #PikaModule instance.
 *
 * Returns: The new #PikaModule object.
 **/
PikaModule *
pika_module_new (GFile    *file,
                 gboolean  auto_load,
                 gboolean  verbose)
{
  PikaModule *module;

  g_return_val_if_fail (G_IS_FILE (file), NULL);
  g_return_val_if_fail (g_file_is_native (file), NULL);

  module = g_object_new (PIKA_TYPE_MODULE, NULL);

  module->priv->file      = g_object_ref (file);
  module->priv->auto_load = auto_load ? TRUE : FALSE;
  module->priv->verbose   = verbose ? TRUE : FALSE;

  if (module->priv->auto_load)
    {
      if (pika_module_load (G_TYPE_MODULE (module)))
        pika_module_unload (G_TYPE_MODULE (module));
    }
  else
    {
      if (verbose)
        g_print ("Skipping module '%s'\n",
                 pika_file_get_utf8_name (file));

      module->priv->state = PIKA_MODULE_STATE_NOT_LOADED;
    }

  return module;
}

/**
 * pika_module_get_file:
 * @module: A #PikaModule
 *
 * Returns #GFile of the @module,
 *
 * Returns: (transfer none): The @module's #GFile.
 *
 * Since: 3.0
 **/
GFile *
pika_module_get_file (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), NULL);

  return module->priv->file;
}

/**
 * pika_module_set_auto_load:
 * @module:    A #PikaModule
 * @auto_load: Pass %FALSE to exclude this module from auto-loading
 *
 * Sets the @auto_load property of the module
 *
 * Since: 3.0
 **/
void
pika_module_set_auto_load (PikaModule *module,
                           gboolean    auto_load)
{
  g_return_if_fail (PIKA_IS_MODULE (module));

  if (auto_load != module->priv->auto_load)
    {
      module->priv->auto_load = auto_load;

      g_object_notify_by_pspec (G_OBJECT (module), obj_props[PROP_AUTO_LOAD]);
    }
}

/**
 * pika_module_get_auto_load:
 * @module: A #PikaModule
 *
 * Returns whether this @module in automatically loaded at startup.
 *
 * Returns: The @module's 'auto_load' property.
 *
 * Since: 3.0
 **/
gboolean
pika_module_get_auto_load (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), FALSE);

  return module->priv->auto_load;
}

/**
 * pika_module_is_on_disk:
 * @module: A #PikaModule
 *
 * Returns: Whether the @module is present on diak.
 *
 * Since: 3.0
 **/
gboolean
pika_module_is_on_disk (PikaModule *module)
{
  gboolean old_on_disk;

  g_return_val_if_fail (PIKA_IS_MODULE (module), FALSE);

  old_on_disk = module->priv->on_disk;

  module->priv->on_disk =
    (g_file_query_file_type (module->priv->file,
                             G_FILE_QUERY_INFO_NONE, NULL) ==
     G_FILE_TYPE_REGULAR);

  if (module->priv->on_disk != old_on_disk)
    g_object_notify_by_pspec (G_OBJECT (module), obj_props[PROP_ON_DISK]);

  return module->priv->on_disk;
}

/**
 * pika_module_is_loaded:
 * @module: A #PikaModule
 *
 * Returns: Whether the @module is currently loaded.
 *
 * Since: 3.0
 **/
gboolean
pika_module_is_loaded (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), FALSE);

  return module->priv->module != NULL;
}

/**
 * pika_module_get_info:
 * @module: A #PikaModule
 *
 * Returns: (transfer none): The @module's #PikaModuleInfo as provided
 *          by the actual module, or %NULL.
 *
 * Since: 3.0
 **/
const PikaModuleInfo *
pika_module_get_info (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), NULL);

  return module->priv->info;
}

/**
 * pika_module_get_state:
 * @module: A #PikaModule
 *
 * Returns: The @module's state.
 *
 * Since: 3.0
 **/
PikaModuleState
pika_module_get_state (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), PIKA_MODULE_STATE_ERROR);

  return module->priv->state;
}

/**
 * pika_module_get_last_error:
 * @module: A #PikaModule
 *
 * Returns: The @module's last error message.
 *
 * Since: 3.0
 **/
const gchar *
pika_module_get_last_error (PikaModule *module)
{
  g_return_val_if_fail (PIKA_IS_MODULE (module), NULL);

  return module->priv->last_error;
}

/**
 * pika_module_query_module:
 * @module: A #PikaModule.
 *
 * Queries the module without actually registering any of the types it
 * may implement. After successful query, pika_module_get_info() can be
 * used to get further about the module.
 *
 * Returns: %TRUE on success.
 **/
gboolean
pika_module_query_module (PikaModule *module)
{
  const PikaModuleInfo *info;
  gboolean              close_module = FALSE;
  gpointer              func;

  g_return_val_if_fail (PIKA_IS_MODULE (module), FALSE);

  if (! module->priv->module)
    {
      if (! pika_module_open (module))
        return FALSE;

      close_module = TRUE;
    }

  /* find the pika_module_query symbol */
  if (! g_module_symbol (module->priv->module, "pika_module_query", &func))
    {
      pika_module_set_last_error (module,
                                  "Missing pika_module_query() symbol");

      g_message (_("Module '%s' load error: %s"),
                 pika_file_get_utf8_name (module->priv->file),
                 module->priv->last_error);

      pika_module_close (module);

      module->priv->state = PIKA_MODULE_STATE_ERROR;
      return FALSE;
    }

  module->priv->query_module = func;

  g_clear_pointer (&module->priv->info, pika_module_info_free);

  info = module->priv->query_module (G_TYPE_MODULE (module));

  if (! info || info->abi_version != PIKA_MODULE_ABI_VERSION)
    {
      pika_module_set_last_error (module,
                                  info ?
                                  "module ABI version does not match" :
                                  "pika_module_query() returned NULL");

      g_message (_("Module '%s' load error: %s"),
                 pika_file_get_utf8_name (module->priv->file),
                 module->priv->last_error);

      pika_module_close (module);

      module->priv->state = PIKA_MODULE_STATE_ERROR;
      return FALSE;
    }

  module->priv->info = pika_module_info_copy (info);

  if (close_module)
    return pika_module_close (module);

  return TRUE;
}

/**
 * pika_module_error_quark:
 *
 * This function is never called directly. Use PIKA_MODULE_ERROR() instead.
 *
 * Returns: the #GQuark that defines the PIKA module error domain.
 *
 * Since: 2.8
 **/
GQuark
pika_module_error_quark (void)
{
  return g_quark_from_static_string ("pika-module-error-quark");
}


/*  private functions  */

static gboolean
pika_module_open (PikaModule *module)
{
  gchar *path = g_file_get_path (module->priv->file);

  module->priv->module = g_module_open (path, 0);

  g_free (path);

  if (! module->priv->module)
    {
      module->priv->state = PIKA_MODULE_STATE_ERROR;
      pika_module_set_last_error (module, g_module_error ());

      g_message (_("Module '%s' load error: %s"),
                 pika_file_get_utf8_name (module->priv->file),
                 module->priv->last_error);

      return FALSE;
    }

  return TRUE;
}

static gboolean
pika_module_close (PikaModule *module)
{
  g_module_close (module->priv->module); /* FIXME: error handling */
  module->priv->module          = NULL;
  module->priv->query_module    = NULL;
  module->priv->register_module = NULL;

  module->priv->state = PIKA_MODULE_STATE_NOT_LOADED;

  return TRUE;
}

static void
pika_module_set_last_error (PikaModule  *module,
                            const gchar *error_str)
{
  if (module->priv->last_error)
    g_free (module->priv->last_error);

  module->priv->last_error = g_strdup (error_str);
}


/*  PikaModuleInfo functions  */

/**
 * pika_module_info_new:
 * @abi_version: The #PIKA_MODULE_ABI_VERSION the module was compiled against.
 * @purpose:     The module's general purpose.
 * @author:      The module's author.
 * @version:     The module's version.
 * @copyright:   The module's copyright.
 * @date:        The module's release date.
 *
 * Creates a newly allocated #PikaModuleInfo struct.
 *
 * Returns: The new #PikaModuleInfo struct.
 **/
static PikaModuleInfo *
pika_module_info_new (guint32      abi_version,
                      const gchar *purpose,
                      const gchar *author,
                      const gchar *version,
                      const gchar *copyright,
                      const gchar *date)
{
  PikaModuleInfo *info = g_slice_new0 (PikaModuleInfo);

  info->abi_version = abi_version;
  info->purpose     = g_strdup (purpose);
  info->author      = g_strdup (author);
  info->version     = g_strdup (version);
  info->copyright   = g_strdup (copyright);
  info->date        = g_strdup (date);

  return info;
}

/**
 * pika_module_info_copy:
 * @info: The #PikaModuleInfo struct to copy.
 *
 * Copies a #PikaModuleInfo struct.
 *
 * Returns: The new copy.
 **/
static PikaModuleInfo *
pika_module_info_copy (const PikaModuleInfo *info)
{
  g_return_val_if_fail (info != NULL, NULL);

  return pika_module_info_new (info->abi_version,
                               info->purpose,
                               info->author,
                               info->version,
                               info->copyright,
                               info->date);
}

/**
 * pika_module_info_free:
 * @info: The #PikaModuleInfo struct to free
 *
 * Frees the passed #PikaModuleInfo.
 **/
static void
pika_module_info_free (PikaModuleInfo *info)
{
  g_return_if_fail (info != NULL);

  g_free (info->purpose);
  g_free (info->author);
  g_free (info->version);
  g_free (info->copyright);
  g_free (info->date);

  g_slice_free (PikaModuleInfo, info);
}
