/* LIBPIKA - The PIKA Library
 * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
 *
 * 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 "libpikaconfig/pikaconfig.h"

#include "pikamoduletypes.h"

#include "pikamodule.h"
#include "pikamoduledb.h"

#include "libpika/libpika-intl.h"


/**
 * SECTION: pikamoduledb
 * @title: PikaModuleDB
 * @short_description: Keeps a list of #PikaModule's found in a given
 *                     searchpath.
 *
 * Keeps a list of #PikaModule's found in a given searchpath.
 **/


struct _PikaModuleDB
{
  GObject    parent_instance;

  GPtrArray *modules;

  gchar     *load_inhibit;
  gboolean   verbose;
};


static void         pika_module_db_finalize            (GObject      *object);

static void         pika_module_db_load_directory      (PikaModuleDB *db,
                                                        GFile        *directory);
static void         pika_module_db_load_module         (PikaModuleDB *db,
                                                        GFile        *file);

static PikaModule * pika_module_db_module_find_by_file (PikaModuleDB *db,
                                                        GFile        *file);

static void         pika_module_db_module_dump_func    (gpointer      data,
                                                        gpointer      user_data);

static void         pika_module_db_list_model_iface_init (GListModelInterface *iface);


G_DEFINE_TYPE_WITH_CODE (PikaModuleDB, pika_module_db, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
                                                pika_module_db_list_model_iface_init))

#define parent_class pika_module_db_parent_class


static void
pika_module_db_class_init (PikaModuleDBClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = pika_module_db_finalize;
}

static void
pika_module_db_init (PikaModuleDB *db)
{
  db->modules      = g_ptr_array_new ();
  db->load_inhibit = NULL;
  db->verbose      = FALSE;
}

static void
pika_module_db_finalize (GObject *object)
{
  PikaModuleDB *db = PIKA_MODULE_DB (object);

  g_clear_pointer (&db->modules,      g_ptr_array_unref);
  g_clear_pointer (&db->load_inhibit, g_free);

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

static GType
pika_module_db_get_item_type (GListModel *list)
{
  return PIKA_TYPE_MODULE;
}

static guint
pika_module_db_get_n_items (GListModel *list)
{
  PikaModuleDB *self = PIKA_MODULE_DB (list);
  return self->modules->len;
}

static void *
pika_module_db_get_item (GListModel *list,
                         guint       index)
{
  PikaModuleDB *self = PIKA_MODULE_DB (list);

  if (index >= self->modules->len)
    return NULL;
  return g_object_ref (g_ptr_array_index (self->modules, index));
}

static void
pika_module_db_list_model_iface_init (GListModelInterface *iface)
{
  iface->get_item_type = pika_module_db_get_item_type;
  iface->get_n_items = pika_module_db_get_n_items;
  iface->get_item = pika_module_db_get_item;
}

/**
 * pika_module_db_new:
 * @verbose: Pass %TRUE to enable debugging output.
 *
 * Creates a new #PikaModuleDB instance. The @verbose parameter will be
 * passed to the created #PikaModule instances using pika_module_new().
 *
 * Returns: The new #PikaModuleDB instance.
 **/
PikaModuleDB *
pika_module_db_new (gboolean verbose)
{
  PikaModuleDB *db;

  db = g_object_new (PIKA_TYPE_MODULE_DB, NULL);

  db->verbose = verbose ? TRUE : FALSE;

  return db;
}

/**
 * pika_module_db_set_verbose:
 * @db:      A #PikaModuleDB.
 * @verbose: the new 'verbose' setting
 *
 * Sets the 'verbose' setting of @db.
 *
 * Since: 3.0
 **/
void
pika_module_db_set_verbose (PikaModuleDB *db,
                            gboolean      verbose)
{
  g_return_if_fail (PIKA_IS_MODULE_DB (db));

  db->verbose = verbose ? TRUE : FALSE;
}

/**
 * pika_module_db_get_verbose:
 * @db: A #PikaModuleDB.
 *
 * Returns the 'verbose' setting of @db.
 *
 * Returns: the 'verbose' setting.
 *
 * Since: 3.0
 **/
gboolean
pika_module_db_get_verbose (PikaModuleDB *db)
{
  g_return_val_if_fail (PIKA_IS_MODULE_DB (db), FALSE);

  return db->verbose;
}

static gboolean
is_in_inhibit_list (GFile       *file,
                    const gchar *inhibit_list)
{
  gchar       *filename;
  gchar       *p;
  gint         pathlen;
  const gchar *start;
  const gchar *end;

  if (! inhibit_list || ! strlen (inhibit_list))
    return FALSE;

  filename = g_file_get_path (file);

  p = strstr (inhibit_list, filename);
  if (! p)
    {
      g_free (filename);
      return FALSE;
    }

  /* we have a substring, but check for colons either side */
  start = p;
  while (start != inhibit_list && *start != G_SEARCHPATH_SEPARATOR)
    start--;

  if (*start == G_SEARCHPATH_SEPARATOR)
    start++;

  end = strchr (p, G_SEARCHPATH_SEPARATOR);
  if (! end)
    end = inhibit_list + strlen (inhibit_list);

  pathlen = strlen (filename);

  g_free (filename);

  if ((end - start) == pathlen)
    return TRUE;

  return FALSE;
}

/**
 * pika_module_db_set_load_inhibit:
 * @db:           A #PikaModuleDB.
 * @load_inhibit: A #G_SEARCHPATH_SEPARATOR delimited list of module
 *                filenames to exclude from auto-loading.
 *
 * Sets the @load_inhibit flag for all #PikaModule's which are kept
 * by @db (using pika_module_set_load_inhibit()).
 **/
void
pika_module_db_set_load_inhibit (PikaModuleDB *db,
                                 const gchar  *load_inhibit)
{
  guint i;

  g_return_if_fail (PIKA_IS_MODULE_DB (db));

  g_free (db->load_inhibit);

  db->load_inhibit = g_strdup (load_inhibit);

  for (i = 0; i < db->modules->len; i++)
    {
      PikaModule *module = g_ptr_array_index (db->modules, i);
      gboolean    inhibit;

      inhibit = is_in_inhibit_list (pika_module_get_file (module),
                                    load_inhibit);

      pika_module_set_auto_load (module, ! inhibit);
    }
}

/**
 * pika_module_db_get_load_inhibit:
 * @db: A #PikaModuleDB.
 *
 * Return the #G_SEARCHPATH_SEPARATOR delimited list of module filenames
 * which are excluded from auto-loading.
 *
 * Returns: the @db's @load_inhibit string.
 **/
const gchar *
pika_module_db_get_load_inhibit (PikaModuleDB *db)
{
  g_return_val_if_fail (PIKA_IS_MODULE_DB (db), NULL);

  return db->load_inhibit;
}

/**
 * pika_module_db_load:
 * @db:          A #PikaModuleDB.
 * @module_path: A #G_SEARCHPATH_SEPARATOR delimited list of directories
 *               to load modules from.
 *
 * Scans the directories contained in @module_path and creates a
 * #PikaModule instance for every loadable module contained in the
 * directories.
 **/
void
pika_module_db_load (PikaModuleDB *db,
                     const gchar  *module_path)
{
  g_return_if_fail (PIKA_IS_MODULE_DB (db));
  g_return_if_fail (module_path != NULL);

  if (g_module_supported ())
    {
      GList *path;
      GList *list;

      path = pika_config_path_expand_to_files (module_path, NULL);

      for (list = path; list; list = g_list_next (list))
        {
          pika_module_db_load_directory (db, list->data);
        }

      g_list_free_full (path, (GDestroyNotify) g_object_unref);
    }

  if (FALSE)
    g_ptr_array_foreach (db->modules, pika_module_db_module_dump_func, NULL);
}

/**
 * pika_module_db_refresh:
 * @db:          A #PikaModuleDB.
 * @module_path: A #G_SEARCHPATH_SEPARATOR delimited list of directories
 *               to load modules from.
 *
 * Does the same as pika_module_db_load(), plus removes all #PikaModule
 * instances whose modules have been deleted from disk.
 *
 * Note that the #PikaModule's will just be removed from the internal
 * list and not freed as this is not possible with #GTypeModule
 * instances which actually implement types.
 **/
void
pika_module_db_refresh (PikaModuleDB *db,
                        const gchar  *module_path)
{
  guint i;

  g_return_if_fail (PIKA_IS_MODULE_DB (db));
  g_return_if_fail (module_path != NULL);

  for (i = 0; i < db->modules->len; i++)
    {
      PikaModule *module = g_ptr_array_index (db->modules, i);

      if (! pika_module_is_on_disk (module) &&
          ! pika_module_is_loaded (module))
        {
          g_ptr_array_remove_index (db->modules, i);
          g_list_model_items_changed (G_LIST_MODEL (db), i, 1, 0);
          i--;
        }
    }

  /* walk filesystem and add new things we find */
  pika_module_db_load (db, module_path);
}

static void
pika_module_db_load_directory (PikaModuleDB *db,
                               GFile        *directory)
{
  GFileEnumerator *enumerator;

  enumerator = g_file_enumerate_children (directory,
                                          G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                          G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                          G_FILE_QUERY_INFO_NONE,
                                          NULL, NULL);

  if (enumerator)
    {
      GFileInfo *info;

      while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)))
        {
          GFileType file_type = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE);

         if (file_type == G_FILE_TYPE_REGULAR &&
             ! g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN))
            {
              GFile *child = g_file_enumerator_get_child (enumerator, info);

              pika_module_db_load_module (db, child);

              g_object_unref (child);
            }

          g_object_unref (info);
        }

      g_object_unref (enumerator);
    }
}

static void
pika_module_db_load_module (PikaModuleDB *db,
                            GFile        *file)
{
  PikaModule *module;
  gboolean   load_inhibit;

  if (! pika_file_has_extension (file, "." G_MODULE_SUFFIX))
    return;

  /* don't load if we already know about it */
  if (pika_module_db_module_find_by_file (db, file))
    return;

  load_inhibit = is_in_inhibit_list (file, db->load_inhibit);

  module = pika_module_new (file,
                            ! load_inhibit,
                            db->verbose);

  g_ptr_array_add (db->modules, module);
  g_list_model_items_changed (G_LIST_MODEL (db), db->modules->len - 1, 0, 1);
}

static PikaModule *
pika_module_db_module_find_by_file (PikaModuleDB *db,
                                    GFile        *file)
{
  guint i;

  for (i = 0; i < db->modules->len; i++)
    {
      PikaModule *module = g_ptr_array_index (db->modules, i);

      if (g_file_equal (file, pika_module_get_file (module)))
        return module;
    }

  return NULL;
}

static void
pika_module_db_module_dump_func (gpointer data,
                                 gpointer user_data)
{
  static const gchar * const statenames[] =
  {
    N_("Module error"),
    N_("Loaded"),
    N_("Load failed"),
    N_("Not loaded")
  };

  PikaModule           *module = data;
  const PikaModuleInfo *info   = pika_module_get_info (module);

  g_print ("\n%s: %s\n",
           pika_file_get_utf8_name (pika_module_get_file (module)),
           gettext (statenames[pika_module_get_state (module)]));

#if 0
  g_print ("  module: %p  lasterr: %s  query: %p register: %p\n",
           module->module,
           module->last_module_error ? module->last_module_error : "NONE",
           module->query_module,
           module->register_module);
#endif

  if (info)
    {
      g_print ("  purpose:   %s\n"
               "  author:    %s\n"
               "  version:   %s\n"
               "  copyright: %s\n"
               "  date:      %s\n",
               info->purpose   ? info->purpose   : "NONE",
               info->author    ? info->author    : "NONE",
               info->version   ? info->version   : "NONE",
               info->copyright ? info->copyright : "NONE",
               info->date      ? info->date      : "NONE");
    }
}
