/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 * GObject introspection: Repository implementation
 *
 * Copyright (C) 2005 Matthias Clasen
 * Copyright (C) 2008 Colin Walters <walters@verbum.org>
 * Copyright (C) 2008 Red Hat, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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 2 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, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "config.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <glib.h>
#include <glib-private.h>
#include <glib/gprintf.h>
#include <gmodule.h>
#include "gibaseinfo-private.h"
#include "girepository.h"
#include "gitypelib-internal.h"
#include "girepository-private.h"

/**
 * GIRepository:
 *
 * `GIRepository` is used to manage repositories of namespaces. Namespaces
 * are represented on disk by type libraries (`.typelib` files).
 *
 * The individual pieces of API within a type library are represented by
 * subclasses of [class@GIRepository.BaseInfo]. These can be found using
 * methods like [method@GIRepository.Repository.find_by_name] or
 * [method@GIRepository.Repository.get_info].
 *
 * You are responsible for ensuring that the lifetime of the
 * [class@GIRepository.Repository] exceeds that of the lifetime of any of its
 * [class@GIRepository.BaseInfo]s. This cannot be guaranteed by using internal
 * references within libgirepository as that would affect performance.
 *
 * ### Discovery of type libraries
 *
 * `GIRepository` will typically look for a `girepository-1.0` directory
 * under the library directory used when compiling gobject-introspection. On a
 * standard Linux system this will end up being `/usr/lib/girepository-1.0`.
 *
 * It is possible to control the search paths programmatically, using
 * [method@GIRepository.Repository.prepend_search_path]. It is also possible to
 * modify the search paths by using the `GI_TYPELIB_PATH` environment variable.
 * The environment variable takes precedence over the default search path
 * and the [method@GIRepository.Repository.prepend_search_path] calls.
 *
 * ### Namespace ordering
 *
 * In situations where namespaces may be searched in order, or returned in a
 * list, the namespaces will be returned in alphabetical order, with all fully
 * loaded namespaces being returned before any lazily loaded ones (those loaded
 * with `GI_REPOSITORY_LOAD_FLAG_LAZY`). This allows for deterministic and
 * reproducible results.
 *
 * Similarly, if a symbol (such as a `GType` or error domain) is being searched
 * for in the set of loaded namespaces, the namespaces will be searched in that
 * order. In particular, this means that a symbol which exists in two namespaces
 * will always be returned from the alphabetically-higher namespace. This should
 * only happen in the case of `Gio` and `GioUnix`/`GioWin32`, which all refer to
 * the same `.so` file and expose overlapping sets of symbols. Symbols should
 * always end up being resolved to `GioUnix` or `GioWin32` if they are platform
 * dependent, rather than `Gio` itself.
 *
 * Since: 2.80
 */

/* The namespace and version corresponding to libgirepository itself, so
 * that we can refuse to load typelibs corresponding to the older,
 * incompatible version of this same library in gobject-introspection. */
#define GIREPOSITORY_TYPELIB_NAME "GIRepository"
#define GIREPOSITORY_TYPELIB_VERSION "3.0"
#define GIREPOSITORY_TYPELIB_FILENAME \
  GIREPOSITORY_TYPELIB_NAME "-" GIREPOSITORY_TYPELIB_VERSION ".typelib"

typedef struct {
  size_t n_interfaces;
  GIBaseInfo *interfaces[];
} GTypeInterfaceCache;

static void
gtype_interface_cache_free (gpointer data)
{
  GTypeInterfaceCache *cache = data;

  for (size_t i = 0; i < cache->n_interfaces; i++)
    gi_base_info_unref ((GIBaseInfo*) cache->interfaces[i]);
  g_free (cache);
}

struct _GIRepository
{
  GObject parent;

  GPtrArray *typelib_search_path;  /* (element-type filename) (owned) */
  GPtrArray *library_paths;  /* (element-type filename) (owned) */

  /* Certain operations require iterating over the typelibs and the iteration
   * order may affect the results. So keep an ordered list of the typelibs,
   * alongside the hash table which keep the canonical strong reference to them. */
  GHashTable *typelibs; /* (string) namespace -> GITypelib */
  GPtrArray *ordered_typelibs;  /* (element-type unowned GITypelib) (owned) (not nullable) */
  GHashTable *lazy_typelibs; /* (string) namespace-version -> GITypelib */
  GPtrArray *ordered_lazy_typelibs;  /* (element-type unowned GITypelib) (owned) (not nullable) */

  GHashTable *info_by_gtype; /* GType -> GIBaseInfo */
  GHashTable *info_by_error_domain; /* GQuark -> GIBaseInfo */
  GHashTable *interfaces_for_gtype; /* GType -> GTypeInterfaceCache */
  GHashTable *unknown_gtypes; /* hashset of GType */

  char **cached_shared_libraries;  /* (owned) (nullable) (array zero-terminated=1) */
  size_t cached_n_shared_libraries;  /* length of @cached_shared_libraries, not including NULL terminator */
};

G_DEFINE_TYPE (GIRepository, gi_repository, G_TYPE_OBJECT);

#ifdef G_PLATFORM_WIN32
#include <windows.h>

static HMODULE girepository_dll = NULL;

#ifdef DLL_EXPORT

BOOL WINAPI DllMain (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved);

BOOL WINAPI
DllMain (HINSTANCE hinstDLL,
         DWORD     fdwReason,
         LPVOID    lpvReserved)
{
  if (fdwReason == DLL_PROCESS_ATTACH)
      girepository_dll = hinstDLL;

  return TRUE;
}

#endif /* DLL_EXPORT */
#endif /* G_PLATFORM_WIN32 */

#ifdef __APPLE__
#include <mach-o/dyld.h>

/* This function returns the file path of the loaded libgirepository1.0.dylib.
 * It iterates over all the loaded images to find the one with the
 * gi_repository_init symbol and returns its file path.
  */
static const char *
gi_repository_get_library_path_macos (void)
{
  /*
   * Relevant documentation:
   * https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html
   * https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
   * https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
  */
  const void *ptr = gi_repository_init;
  const struct mach_header *header;
  intptr_t offset;
  uint32_t i, count;

  /* Iterate over all the loaded images */
  count = _dyld_image_count ();
  for (i = 0; i < count; i++)
    {
      header = _dyld_get_image_header (i);
      offset = _dyld_get_image_vmaddr_slide (i);

      /* Locate the first `load` command */
      struct load_command *cmd = (struct load_command *) ((char *) header + sizeof (struct mach_header));
      if (header->magic == MH_MAGIC_64)
        cmd = (struct load_command *) ((char *) header + sizeof (struct mach_header_64));

      /* Find the first `segment` command iterating over all the `load` commands.
       * Then, check if the gi_repository_init symbol is in this image by checking
       * if the pointer is in the segment's memory address range.
       */
      uint32_t j = 0;
      while (j < header->ncmds)
        {
          if (cmd->cmd == LC_SEGMENT)
            {
              struct segment_command *seg = (struct segment_command *) cmd;
              if (((intptr_t) ptr >= (seg->vmaddr + offset)) && ((intptr_t) ptr < (seg->vmaddr + offset + seg->vmsize)))
                return _dyld_get_image_name (i);
           }
          if (cmd->cmd == LC_SEGMENT_64)
            {
              struct segment_command_64 *seg = (struct segment_command_64 *) cmd;
              if (((uintptr_t ) ptr >= (seg->vmaddr + offset)) && ((uintptr_t ) ptr < (seg->vmaddr + offset + seg->vmsize)))
                return _dyld_get_image_name (i);
            }
          /* Jump to the next command */
          j++;
          cmd = (struct load_command *) ((char *) cmd + cmd->cmdsize);
        }
    }
  return NULL;
}
#endif /* __APPLE__ */

/*
 * gi_repository_get_libdir:
 *
 * Returns the directory where the typelib files are installed.
 *
 * In platforms without relocation support, this functions returns the
 * `GOBJECT_INTROSPECTION_LIBDIR` directory defined at build time .
 *
 * On Windows and macOS this function returns the directory
 * relative to the installation directory detected at runtime.
 *
 * On macOS, if the library is installed in
 * `/Applications/MyApp.app/Contents/Home/lib/libgirepository-1.0.dylib`, it returns
 * `/Applications/MyApp.app/Contents/Home/lib/girepository-1.0`
 *
 * On Windows, if the application is installed in
 * `C:/Program Files/MyApp/bin/MyApp.exe`, it returns
 * `C:/Program Files/MyApp/lib/girepository-1.0`
*/
static const gchar *
gi_repository_get_libdir (void)
{
  static gchar *static_libdir;

  if (g_once_init_enter_pointer (&static_libdir))
    {
      gchar *libdir;
#if defined(G_PLATFORM_WIN32)
      const char *toplevel = g_win32_get_package_installation_directory_of_module (girepository_dll);
      libdir = g_build_filename (toplevel, GOBJECT_INTROSPECTION_RELATIVE_LIBDIR, NULL);
      g_ignore_leak (libdir);
#elif defined(__APPLE__)
      const char *libpath = gi_repository_get_library_path_macos ();
      if (libpath != NULL)
        {
          libdir = g_path_get_dirname (libpath);
          g_ignore_leak (libdir);
        } else {
          libdir = GOBJECT_INTROSPECTION_LIBDIR;
        }
#else /* !G_PLATFORM_WIN32 && !__APPLE__ */
        libdir = GOBJECT_INTROSPECTION_LIBDIR;
#endif
      g_once_init_leave_pointer (&static_libdir, libdir);
    }
  return static_libdir;
}

static void
gi_repository_init (GIRepository *repository)
{
  /* typelib search path */
    {
      const char *libdir;
      char *typelib_dir;
      const char *type_lib_path_env;

      /* This variable is intended to take precedence over both:
       *   - the default search path;
       *   - all gi_repository_prepend_search_path() calls.
       */
      type_lib_path_env = g_getenv ("GI_TYPELIB_PATH");

      if (type_lib_path_env)
        {
          char **custom_dirs;

          custom_dirs = g_strsplit (type_lib_path_env, G_SEARCHPATH_SEPARATOR_S, 0);
          repository->typelib_search_path =
            g_ptr_array_new_take_null_terminated ((gpointer) g_steal_pointer (&custom_dirs), g_free);
        }
      else
        {
          repository->typelib_search_path = g_ptr_array_new_null_terminated (1, g_free, TRUE);
        }

      libdir = gi_repository_get_libdir ();

      typelib_dir = g_build_filename (libdir, "girepository-1.0", NULL);

      g_ptr_array_add (repository->typelib_search_path, g_steal_pointer (&typelib_dir));
    }

  repository->library_paths = g_ptr_array_new_null_terminated (1, g_free, TRUE);

  repository->typelibs
    = g_hash_table_new_full (g_str_hash, g_str_equal,
                             (GDestroyNotify) g_free,
                             (GDestroyNotify) gi_typelib_unref);
  repository->ordered_typelibs = g_ptr_array_new_with_free_func (NULL);
  repository->lazy_typelibs
    = g_hash_table_new_full (g_str_hash, g_str_equal,
                             (GDestroyNotify) g_free,
                             (GDestroyNotify) gi_typelib_unref);
  repository->ordered_lazy_typelibs = g_ptr_array_new_with_free_func (NULL);

  repository->info_by_gtype
    = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                             (GDestroyNotify) NULL,
                             (GDestroyNotify) gi_base_info_unref);
  repository->info_by_error_domain
    = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                             (GDestroyNotify) NULL,
                             (GDestroyNotify) gi_base_info_unref);
  repository->interfaces_for_gtype
    = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                             (GDestroyNotify) NULL,
                             (GDestroyNotify) gtype_interface_cache_free);
  repository->unknown_gtypes = g_hash_table_new (NULL, NULL);
}

static void
gi_repository_finalize (GObject *object)
{
  GIRepository *repository = GI_REPOSITORY (object);

  g_hash_table_destroy (repository->typelibs);
  g_ptr_array_unref (repository->ordered_typelibs);
  g_hash_table_destroy (repository->lazy_typelibs);
  g_ptr_array_unref (repository->ordered_lazy_typelibs);

  g_hash_table_destroy (repository->info_by_gtype);
  g_hash_table_destroy (repository->info_by_error_domain);
  g_hash_table_destroy (repository->interfaces_for_gtype);
  g_hash_table_destroy (repository->unknown_gtypes);

  g_clear_pointer (&repository->cached_shared_libraries, g_strfreev);

  g_clear_pointer (&repository->library_paths, g_ptr_array_unref);
  g_clear_pointer (&repository->typelib_search_path, g_ptr_array_unref);

  (* G_OBJECT_CLASS (gi_repository_parent_class)->finalize) (G_OBJECT (repository));
}

static void
gi_repository_class_init (GIRepositoryClass *class)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (class);

  gobject_class->finalize = gi_repository_finalize;
}

/**
 * gi_repository_prepend_search_path:
 * @repository: A #GIRepository
 * @directory: (type filename): directory name to prepend to the typelib
 *   search path
 *
 * Prepends @directory to the typelib search path.
 *
 * See also: gi_repository_get_search_path().
 *
 * Since: 2.80
 */
void
gi_repository_prepend_search_path (GIRepository *repository,
                                   const char   *directory)
{
  g_return_if_fail (GI_IS_REPOSITORY (repository));

  g_ptr_array_insert (repository->typelib_search_path, 0, g_strdup (directory));
}

/**
 * gi_repository_get_search_path:
 * @repository: A #GIRepository
 * @n_paths_out: (optional) (out): The number of search paths returned.
 *
 * Returns the current search path [class@GIRepository.Repository] will use when
 * loading typelib files.
 *
 * The list is internal to [class@GIRepository.Repository] and should not be
 * freed, nor should its string elements.
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_paths_out.
 *
 * Returns: (element-type filename) (transfer none) (array length=n_paths_out): list of search paths, most
 *   important first
 * Since: 2.80
 */
const char * const *
gi_repository_get_search_path (GIRepository *repository,
                               size_t       *n_paths_out)
{
  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  if G_UNLIKELY (!repository->typelib_search_path ||
                 !repository->typelib_search_path->pdata)
    {
      static const char * const empty_search_path[] = {NULL};

      if (n_paths_out)
        *n_paths_out = 0;

      return empty_search_path;
    }

  if (n_paths_out)
    *n_paths_out = repository->typelib_search_path->len;

  return (const char * const *) repository->typelib_search_path->pdata;
}

/**
 * gi_repository_prepend_library_path:
 * @repository: A #GIRepository
 * @directory: (type filename): a single directory to scan for shared libraries
 *
 * Prepends @directory to the search path that is used to
 * search shared libraries referenced by imported namespaces.
 *
 * Multiple calls to this function all contribute to the final
 * list of paths.
 *
 * The list of paths is unique to @repository. When a typelib is loaded by the
 * repository, the list of paths from the @repository at that instant is used
 * by the typelib for loading its modules.
 *
 * If the library is not found in the directories configured
 * in this way, loading will fall back to the system library
 * path (i.e. `LD_LIBRARY_PATH` and `DT_RPATH` in ELF systems).
 * See the documentation of your dynamic linker for full details.
 *
 * Since: 2.80
 */
void
gi_repository_prepend_library_path (GIRepository *repository,
                                    const char   *directory)
{
  g_return_if_fail (GI_IS_REPOSITORY (repository));

  g_ptr_array_insert (repository->library_paths, 0, g_strdup (directory));
}

/**
 * gi_repository_get_library_path:
 * @repository: A #GIRepository
 * @n_paths_out: (optional) (out): The number of library paths returned.
 *
 * Returns the current search path [class@GIRepository.Repository] will use when
 * loading shared libraries referenced by imported namespaces.
 *
 * The list is internal to [class@GIRepository.Repository] and should not be
 * freed, nor should its string elements.
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_paths_out.
 *
 * Returns: (element-type filename) (transfer none) (array length=n_paths_out): list of search paths, most
 *   important first
 * Since: 2.80
 */
const char * const *
gi_repository_get_library_path (GIRepository *repository,
                                size_t       *n_paths_out)
{
  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  if G_UNLIKELY (!repository->library_paths || !repository->library_paths->pdata)
    {
      static const char * const empty_search_path[] = {NULL};

      if (n_paths_out)
        *n_paths_out = 0;

      return empty_search_path;
    }

  if (n_paths_out)
    *n_paths_out = repository->library_paths->len;

  return (const char * const *) repository->library_paths->pdata;
}

static char *
build_typelib_key (const char *name, const char *source)
{
  GString *str = g_string_new (name);
  g_string_append_c (str, '\0');
  g_string_append (str, source);
  return g_string_free (str, FALSE);
}

/* Note: Returns %NULL (not an empty %NULL-terminated array) if there are no
 * dependencies. */
static char **
get_typelib_dependencies (GITypelib *typelib)
{
  Header *header;
  const char *dependencies_glob;

  header = (Header *)typelib->data;

  if (header->dependencies == 0)
    return NULL;

  dependencies_glob = gi_typelib_get_string (typelib, header->dependencies);
  return g_strsplit (dependencies_glob, "|", 0);
}

static GITypelib *
check_version_conflict (GITypelib *typelib,
                        const char  *namespace,
                        const char  *expected_version,
                        char       **version_conflict)
{
  Header *header;
  const char *loaded_version;

  if (expected_version == NULL)
    {
      if (version_conflict)
        *version_conflict = NULL;
      return typelib;
    }

  header = (Header*)typelib->data;
  loaded_version = gi_typelib_get_string (typelib, header->nsversion);
  g_assert (loaded_version != NULL);

  if (strcmp (expected_version, loaded_version) != 0)
    {
      if (version_conflict)
        *version_conflict = (char*)loaded_version;
      return NULL;
    }
  if (version_conflict)
    *version_conflict = NULL;
  return typelib;
}

static GITypelib *
get_registered_status (GIRepository *repository,
                       const char   *namespace,
                       const char   *version,
                       gboolean      allow_lazy,
                       gboolean     *lazy_status,
                       char        **version_conflict)
{
  GITypelib *typelib;

  if (lazy_status)
    *lazy_status = FALSE;
  typelib = g_hash_table_lookup (repository->typelibs, namespace);
  if (typelib)
    return check_version_conflict (typelib, namespace, version, version_conflict);
  typelib = g_hash_table_lookup (repository->lazy_typelibs, namespace);
  if (!typelib)
    return NULL;
  if (lazy_status)
    *lazy_status = TRUE;
  if (!allow_lazy)
    return NULL;
  return check_version_conflict (typelib, namespace, version, version_conflict);
}

static GITypelib *
get_registered (GIRepository *repository,
                const char   *namespace,
                const char   *version)
{
  return get_registered_status (repository, namespace, version, TRUE, NULL, NULL);
}

static gboolean
load_dependencies_recurse (GIRepository *repository,
                           GITypelib     *typelib,
                           GError      **error)
{
  char **dependencies;

  dependencies = get_typelib_dependencies (typelib);

  if (dependencies != NULL)
    {
      int i;

      for (i = 0; dependencies[i]; i++)
        {
          char *dependency = dependencies[i];
          const char *last_dash;
          char *dependency_namespace;
          const char *dependency_version;

          last_dash = strrchr (dependency, '-');
          dependency_namespace = g_strndup (dependency, last_dash - dependency);
          dependency_version = last_dash+1;

          if (!gi_repository_require (repository, dependency_namespace, dependency_version,
                                      0, error))
            {
              g_free (dependency_namespace);
              g_strfreev (dependencies);
              return FALSE;
            }
          g_free (dependency_namespace);
        }
      g_strfreev (dependencies);
    }
  return TRUE;
}

/* Sort typelibs by namespace. The main requirement here is just to make iteration
 * deterministic, otherwise results can change as a lot of the code here would
 * just iterate over a `GHashTable`.
 *
 * A sub-requirement of this is that namespaces are sorted such that if a GType
 * or symbol is found in multiple namespaces where one is a prefix of the other,
 * the longest namespace wins. In practice, this only happens in
 * Gio/GioUnix/GioWin32, as all three of those namespaces refer to the same
 * `.so` file and overlapping sets of the same symbols, but we want the platform
 * specific namespace to be returned in preference to anything else (even though
 * either namespace is valid).
 * See https://gitlab.gnome.org/GNOME/glib/-/issues/3303 */
static int
sort_typelibs_cb (const void *a,
                  const void *b)
{
  GITypelib *typelib_a = *(GITypelib **) a;
  GITypelib *typelib_b = *(GITypelib **) b;

  return strcmp (gi_typelib_get_namespace (typelib_a),
                 gi_typelib_get_namespace (typelib_b));
}

static const char *
register_internal (GIRepository *repository,
                   const char   *source,
                   gboolean      lazy,
                   GITypelib     *typelib,
                   GError      **error)
{
  Header *header;
  const char *namespace;

  g_return_val_if_fail (typelib != NULL, NULL);

  header = (Header *)typelib->data;

  g_return_val_if_fail (header != NULL, NULL);

  namespace = gi_typelib_get_string (typelib, header->namespace);

  if (lazy)
    {
      g_assert (!g_hash_table_lookup (repository->lazy_typelibs,
                                      namespace));
      g_hash_table_insert (repository->lazy_typelibs,
                           build_typelib_key (namespace, source), gi_typelib_ref (typelib));
      g_ptr_array_add (repository->ordered_lazy_typelibs, typelib);
      g_ptr_array_sort (repository->ordered_lazy_typelibs, sort_typelibs_cb);
    }
  else
    {
      gpointer value;
      char *key;

      /* First, try loading all the dependencies */
      if (!load_dependencies_recurse (repository, typelib, error))
        return NULL;

      /* Check if we are transitioning from lazily loaded state */
      if (g_hash_table_lookup_extended (repository->lazy_typelibs,
                                        namespace,
                                        (gpointer)&key, &value))
        {
          g_hash_table_remove (repository->lazy_typelibs, key);
          g_ptr_array_remove (repository->ordered_lazy_typelibs, typelib);
        }
      else
        {
          key = build_typelib_key (namespace, source);
        }

      g_hash_table_insert (repository->typelibs,
                           g_steal_pointer (&key),
                           gi_typelib_ref (typelib));
      g_ptr_array_add (repository->ordered_typelibs, typelib);
      g_ptr_array_sort (repository->ordered_typelibs, sort_typelibs_cb);
    }

  /* These types might be resolved now, clear the cache */
  g_hash_table_remove_all (repository->unknown_gtypes);

  return namespace;
}

/**
 * gi_repository_get_immediate_dependencies:
 * @repository: A #GIRepository
 * @namespace_: Namespace of interest
 * @n_dependencies_out: (optional) (out): Return location for the number of
 *   dependencies
 *
 * Return an array of the immediate versioned dependencies for @namespace_.
 * Returned strings are of the form `namespace-version`.
 *
 * Note: @namespace_ must have already been loaded using a function
 * such as [method@GIRepository.Repository.require] before calling this
 * function.
 *
 * To get the transitive closure of dependencies for @namespace_, use
 * [method@GIRepository.Repository.get_dependencies].
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_dependencies_out.
 *
 * Returns: (transfer full) (array length=n_dependencies_out): String array of
 *   immediate versioned dependencies
 * Since: 2.80
 */
char **
gi_repository_get_immediate_dependencies (GIRepository *repository,
                                          const char   *namespace,
                                          size_t       *n_dependencies_out)
{
  GITypelib *typelib;
  char **deps;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered (repository, namespace, NULL);
  g_return_val_if_fail (typelib != NULL, NULL);

  /* Ensure we always return a non-%NULL vector. */
  deps = get_typelib_dependencies (typelib);
  if (deps == NULL)
      deps = g_strsplit ("", "|", 0);

  if (n_dependencies_out != NULL)
    *n_dependencies_out = g_strv_length (deps);

  return deps;
}

/* Load the transitive closure of dependency namespace-version strings for the
 * given @typelib. @repository must be non-%NULL. @transitive_dependencies must
 * be a pre-existing GHashTable<owned utf8, owned utf8> set for storing the
 * dependencies. */
static void
get_typelib_dependencies_transitive (GIRepository *repository,
                                     GITypelib    *typelib,
                                     GHashTable   *transitive_dependencies)
{
  char **immediate_dependencies;

  immediate_dependencies = get_typelib_dependencies (typelib);

  for (size_t i = 0; immediate_dependencies != NULL && immediate_dependencies[i]; i++)
    {
      char *dependency;
      const char *last_dash;
      char *dependency_namespace;

      dependency = immediate_dependencies[i];

      /* Steal from the strv. */
      g_hash_table_add (transitive_dependencies, dependency);
      immediate_dependencies[i] = NULL;

      /* Recurse for this namespace. */
      last_dash = strrchr (dependency, '-');
      dependency_namespace = g_strndup (dependency, last_dash - dependency);

      typelib = get_registered (repository, dependency_namespace, NULL);
      g_return_if_fail (typelib != NULL);
      get_typelib_dependencies_transitive (repository, typelib,
                                           transitive_dependencies);

      g_free (dependency_namespace);
    }

  g_free (immediate_dependencies);
}

/**
 * gi_repository_get_dependencies:
 * @repository: A #GIRepository
 * @namespace_: Namespace of interest
 * @n_dependencies_out: (optional) (out): Return location for the number of
 *   dependencies
 *
 * Retrieves all (transitive) versioned dependencies for
 * @namespace_.
 *
 * The returned strings are of the form `namespace-version`.
 *
 * Note: @namespace_ must have already been loaded using a function
 * such as [method@GIRepository.Repository.require] before calling this
 * function.
 *
 * To get only the immediate dependencies for @namespace_, use
 * [method@GIRepository.Repository.get_immediate_dependencies].
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_dependencies_out.
 *
 * Returns: (transfer full) (array length=n_dependencies_out): String array of
 *   all versioned dependencies
 * Since: 2.80
 */
char **
gi_repository_get_dependencies (GIRepository *repository,
                                const char   *namespace,
                                size_t       *n_dependencies_out)
{
  GITypelib *typelib;
  GHashTable *transitive_dependencies;  /* set of owned utf8 */
  GHashTableIter iter;
  char *dependency;
  GPtrArray *out;  /* owned utf8 elements */

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered (repository, namespace, NULL);
  g_return_val_if_fail (typelib != NULL, NULL);

  /* Load the dependencies. */
  transitive_dependencies = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                   g_free, NULL);
  get_typelib_dependencies_transitive (repository, typelib,
                                       transitive_dependencies);

  /* Convert to a string array. */
  out = g_ptr_array_new_null_terminated (g_hash_table_size (transitive_dependencies),
                                         g_free, TRUE);
  g_hash_table_iter_init (&iter, transitive_dependencies);

  while (g_hash_table_iter_next (&iter, (gpointer) &dependency, NULL))
    {
      g_ptr_array_add (out, dependency);
      g_hash_table_iter_steal (&iter);
    }

  g_hash_table_unref (transitive_dependencies);

  if (n_dependencies_out != NULL)
    *n_dependencies_out = out->len;

  return (char **) g_ptr_array_free (out, FALSE);
}

/**
 * gi_repository_load_typelib:
 * @repository: A #GIRepository
 * @typelib: (transfer none): the typelib to load
 * @flags: flags affecting the loading operation
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Load the given @typelib into the repository.
 *
 * Returns: namespace of the loaded typelib
 * Since: 2.80
 */
const char *
gi_repository_load_typelib (GIRepository           *repository,
                            GITypelib              *typelib,
                            GIRepositoryLoadFlags   flags,
                            GError                **error)
{
  Header *header;
  const char *namespace;
  const char *nsversion;
  gboolean allow_lazy = flags & GI_REPOSITORY_LOAD_FLAG_LAZY;
  gboolean is_lazy;
  char *version_conflict;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  header = (Header *) typelib->data;
  namespace = gi_typelib_get_string (typelib, header->namespace);
  nsversion = gi_typelib_get_string (typelib, header->nsversion);

  if (get_registered_status (repository, namespace, nsversion, allow_lazy,
                             &is_lazy, &version_conflict))
    {
      if (version_conflict != NULL)
        {
          g_set_error (error, GI_REPOSITORY_ERROR,
                       GI_REPOSITORY_ERROR_NAMESPACE_VERSION_CONFLICT,
                       "Attempting to load namespace '%s', version '%s', but '%s' is already loaded",
                       namespace, nsversion, version_conflict);
          return NULL;
        }
      return namespace;
    }
  return register_internal (repository, "<builtin>",
                            allow_lazy, typelib, error);
}

/**
 * gi_repository_is_registered:
 * @repository: A #GIRepository
 * @namespace_: Namespace of interest
 * @version: (nullable): Required version, may be `NULL` for latest
 *
 * Check whether a particular namespace (and optionally, a specific
 * version thereof) is currently loaded.
 *
 * This function is likely to only be useful in unusual circumstances; in order
 * to act upon metadata in the namespace, you should call
 * [method@GIRepository.Repository.require] instead which will ensure the
 * namespace is loaded, and return as quickly as this function will if it has
 * already been loaded.
 *
 * Returns: `TRUE` if namespace-version is loaded, `FALSE` otherwise
 * Since: 2.80
 */
gboolean
gi_repository_is_registered (GIRepository *repository,
                             const char   *namespace,
                             const char   *version)
{
  g_return_val_if_fail (GI_IS_REPOSITORY (repository), FALSE);

  return get_registered (repository, namespace, version) != NULL;
}

/**
 * gi_repository_new:
 *
 * Create a new [class@GIRepository.Repository].
 *
 * Returns: (transfer full): a new [class@GIRepository.Repository]
 * Since: 2.80
 */
GIRepository *
gi_repository_new (void)
{
  return g_object_new (GI_TYPE_REPOSITORY, NULL);
}

/**
 * gi_repository_get_n_infos:
 * @repository: A #GIRepository
 * @namespace_: Namespace to inspect
 *
 * This function returns the number of metadata entries in
 * given namespace @namespace_.
 *
 * The namespace must have already been loaded before calling this function.
 *
 * Returns: number of metadata entries
 * Since: 2.80
 */
unsigned int
gi_repository_get_n_infos (GIRepository *repository,
                           const char   *namespace)
{
  GITypelib *typelib;
  unsigned int n_interfaces = 0;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), -1);
  g_return_val_if_fail (namespace != NULL, -1);

  typelib = get_registered (repository, namespace, NULL);

  g_return_val_if_fail (typelib != NULL, -1);

  n_interfaces = ((Header *)typelib->data)->n_local_entries;

  return n_interfaces;
}

/**
 * gi_repository_get_info:
 * @repository: A #GIRepository
 * @namespace_: Namespace to inspect
 * @idx: 0-based offset into namespace metadata for entry
 *
 * This function returns a particular metadata entry in the
 * given namespace @namespace_.
 *
 * The namespace must have already been loaded before calling this function.
 * See [method@GIRepository.Repository.get_n_infos] to find the maximum number
 * of entries. It is an error to pass an invalid @idx to this function.
 *
 * Returns: (transfer full) (not nullable): [class@GIRepository.BaseInfo]
 *   containing metadata
 * Since: 2.80
 */
GIBaseInfo *
gi_repository_get_info (GIRepository *repository,
                        const char   *namespace,
                        unsigned int  idx)
{
  GITypelib *typelib;
  DirEntry *entry;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);
  g_return_val_if_fail (idx < G_MAXUINT16, NULL);

  typelib = get_registered (repository, namespace, NULL);

  g_return_val_if_fail (typelib != NULL, NULL);

  entry = gi_typelib_get_dir_entry (typelib, idx + 1);
  g_return_val_if_fail (entry != NULL, NULL);

  return gi_info_new_full (gi_typelib_blob_type_to_info_type (entry->blob_type),
                           repository,
                           NULL, typelib, entry->offset);
}

static DirEntry *
find_by_gtype (GPtrArray   *ordered_table,
               const char  *gtype_name,
               gboolean     check_prefix,
               GITypelib  **out_result_typelib)
{
  /* Search in reverse order as the longest namespaces will be listed last, and
   * those are the ones we want to search first. */
  for (guint i = ordered_table->len; i > 0; i--)
    {
      GITypelib *typelib = g_ptr_array_index (ordered_table, i - 1);
      DirEntry *ret;

      if (check_prefix)
        {
          if (!gi_typelib_matches_gtype_name_prefix (typelib, gtype_name))
            continue;
        }

      ret = gi_typelib_get_dir_entry_by_gtype_name (typelib, gtype_name);
      if (ret)
        {
          *out_result_typelib = typelib;
          return ret;
        }
    }

  return NULL;
}

/**
 * gi_repository_find_by_gtype:
 * @repository: A #GIRepository
 * @gtype: [type@GObject.Type] to search for
 *
 * Searches all loaded namespaces for a particular [type@GObject.Type].
 *
 * Note that in order to locate the metadata, the namespace corresponding to
 * the type must first have been loaded.  There is currently no
 * mechanism for determining the namespace which corresponds to an
 * arbitrary [type@GObject.Type] — thus, this function will operate most
 * reliably when you know the [type@GObject.Type] is from a loaded namespace.
 *
 * Returns: (transfer full) (nullable): [class@GIRepository.BaseInfo]
 *   representing metadata about @type, or `NULL` if none found
 * Since: 2.80
 */
GIBaseInfo *
gi_repository_find_by_gtype (GIRepository *repository,
                             GType         gtype)
{
  const char *gtype_name;
  GITypelib *result_typelib = NULL;
  GIBaseInfo *cached;
  DirEntry *entry;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL);

  cached = g_hash_table_lookup (repository->info_by_gtype,
                                (gpointer)gtype);

  if (cached != NULL)
    return gi_base_info_ref (cached);

  if (g_hash_table_contains (repository->unknown_gtypes, (gpointer)gtype))
    return NULL;

  gtype_name = g_type_name (gtype);

  /* Inside each typelib, we include the "C prefix" which acts as
   * a namespace mechanism.  For GtkTreeView, the C prefix is Gtk.
   * Given the assumption that GTypes for a library also use the
   * C prefix, we know we can skip examining a typelib if our
   * target type does not have this typelib's C prefix. Use this
   * assumption as our first attempt at locating the DirEntry.
   */
  entry = find_by_gtype (repository->ordered_typelibs, gtype_name, TRUE, &result_typelib);
  if (entry == NULL)
    entry = find_by_gtype (repository->ordered_lazy_typelibs, gtype_name, TRUE, &result_typelib);

  /* Not every class library necessarily specifies a correct c_prefix,
   * so take a second pass. This time we will try a global lookup,
   * ignoring prefixes.
   * See http://bugzilla.gnome.org/show_bug.cgi?id=564016
   */
  if (entry == NULL)
    entry = find_by_gtype (repository->ordered_typelibs, gtype_name, FALSE, &result_typelib);
  if (entry == NULL)
    entry = find_by_gtype (repository->ordered_lazy_typelibs, gtype_name, FALSE, &result_typelib);

  if (entry != NULL)
    {
      cached = gi_info_new_full (gi_typelib_blob_type_to_info_type (entry->blob_type),
                                 repository,
                                 NULL, result_typelib, entry->offset);

      g_hash_table_insert (repository->info_by_gtype,
                           (gpointer) gtype,
                           gi_base_info_ref (cached));
      return cached;
    }
  else
    {
      g_hash_table_add (repository->unknown_gtypes, (gpointer) gtype);
      return NULL;
    }
}

/**
 * gi_repository_find_by_name:
 * @repository: A #GIRepository
 * @namespace_: Namespace which will be searched
 * @name: Entry name to find
 *
 * Searches for a particular entry in a namespace.
 *
 * Before calling this function for a particular namespace, you must call
 * [method@GIRepository.Repository.require] to load the namespace, or otherwise
 * ensure the namespace has already been loaded.
 *
 * Returns: (transfer full) (nullable): [class@GIRepository.BaseInfo]
 *   representing metadata about @name, or `NULL` if none found
 * Since: 2.80
 */
GIBaseInfo *
gi_repository_find_by_name (GIRepository *repository,
                            const char   *namespace,
                            const char   *name)
{
  GITypelib *typelib;
  DirEntry *entry;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered (repository, namespace, NULL);
  g_return_val_if_fail (typelib != NULL, NULL);

  entry = gi_typelib_get_dir_entry_by_name (typelib, name);
  if (entry == NULL)
    return NULL;
  return gi_info_new_full (gi_typelib_blob_type_to_info_type (entry->blob_type),
                           repository,
                           NULL, typelib, entry->offset);
}

static DirEntry *
find_by_error_domain (GPtrArray  *ordered_typelibs,
                      GQuark      target_domain,
                      GITypelib **out_typelib)
{
  /* Search in reverse order as the longest namespaces will be listed last, and
   * those are the ones we want to search first. */
  for (guint i = ordered_typelibs->len; i > 0; i--)
    {
      GITypelib *typelib = g_ptr_array_index (ordered_typelibs, i - 1);
      DirEntry *entry;

      entry = gi_typelib_get_dir_entry_by_error_domain (typelib, target_domain);
      if (entry != NULL)
        {
          *out_typelib = typelib;
          return entry;
        }
    }

  return NULL;
}

/**
 * gi_repository_find_by_error_domain:
 * @repository: A #GIRepository
 * @domain: a [type@GLib.Error] domain
 *
 * Searches for the enum type corresponding to the given [type@GLib.Error]
 * domain.
 *
 * Before calling this function for a particular namespace, you must call
 * [method@GIRepository.Repository.require] to load the namespace, or otherwise
 * ensure the namespace has already been loaded.
 *
 * Returns: (transfer full) (nullable): [class@GIRepository.EnumInfo]
 *   representing metadata about @domain’s enum type, or `NULL` if none found
 * Since: 2.80
 */
GIEnumInfo *
gi_repository_find_by_error_domain (GIRepository *repository,
                                    GQuark        domain)
{
  GIEnumInfo *cached;
  DirEntry *result = NULL;
  GITypelib *result_typelib = NULL;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  cached = g_hash_table_lookup (repository->info_by_error_domain,
                                GUINT_TO_POINTER (domain));

  if (cached != NULL)
    return (GIEnumInfo *) gi_base_info_ref ((GIBaseInfo *)cached);

  result = find_by_error_domain (repository->ordered_typelibs, domain, &result_typelib);
  if (result == NULL)
    result = find_by_error_domain (repository->ordered_lazy_typelibs, domain, &result_typelib);

  if (result != NULL)
    {
      cached = (GIEnumInfo *) gi_info_new_full (gi_typelib_blob_type_to_info_type (result->blob_type),
                                                repository,
                                                NULL, result_typelib, result->offset);

      g_hash_table_insert (repository->info_by_error_domain,
                           GUINT_TO_POINTER (domain),
                           gi_base_info_ref ((GIBaseInfo *) cached));
      return cached;
    }
  return NULL;
}

/**
 * gi_repository_get_object_gtype_interfaces:
 * @repository: a #GIRepository
 * @gtype: a [type@GObject.Type] whose fundamental type is `G_TYPE_OBJECT`
 * @n_interfaces_out: (out): Number of interfaces
 * @interfaces_out: (out) (transfer none) (array length=n_interfaces_out): Interfaces for @gtype
 *
 * Look up the implemented interfaces for @gtype.
 *
 * This function cannot fail per se; but for a totally ‘unknown’
 * [type@GObject.Type], it may return 0 implemented interfaces.
 *
 * The semantics of this function are designed for a dynamic binding,
 * where in certain cases (such as a function which returns an
 * interface which may have ‘hidden’ implementation classes), not all
 * data may be statically known, and will have to be determined from
 * the [type@GObject.Type] of the object.  An example is
 * [func@Gio.File.new_for_path] returning a concrete class of
 * `GLocalFile`, which is a [type@GObject.Type] we see at runtime, but
 * not statically.
 *
 * Since: 2.80
 */
void
gi_repository_get_object_gtype_interfaces (GIRepository      *repository,
                                           GType              gtype,
                                           size_t            *n_interfaces_out,
                                           GIInterfaceInfo ***interfaces_out)
{
  GTypeInterfaceCache *cache;

  g_return_if_fail (GI_IS_REPOSITORY (repository));
  g_return_if_fail (g_type_fundamental (gtype) == G_TYPE_OBJECT);

  cache = g_hash_table_lookup (repository->interfaces_for_gtype,
                               (void *) gtype);
  if (cache == NULL)
    {
      GType *interfaces;
      unsigned int i;
      unsigned int n_interfaces;
      GList *interface_infos = NULL, *iter;

      interfaces = g_type_interfaces (gtype, &n_interfaces);
      for (i = 0; i < n_interfaces; i++)
        {
          GIBaseInfo *base_info;

          base_info = gi_repository_find_by_gtype (repository, interfaces[i]);
          if (base_info == NULL)
            continue;

          if (gi_base_info_get_info_type (base_info) != GI_INFO_TYPE_INTERFACE)
            {
              /* FIXME - could this really happen? */
              gi_base_info_unref (base_info);
              continue;
            }

          if (!g_list_find (interface_infos, base_info))
            interface_infos = g_list_prepend (interface_infos, base_info);
        }

      cache = g_malloc (sizeof (GTypeInterfaceCache)
                        + sizeof (GIBaseInfo*) * g_list_length (interface_infos));
      cache->n_interfaces = g_list_length (interface_infos);
      for (iter = interface_infos, i = 0; iter; iter = iter->next, i++)
        cache->interfaces[i] = iter->data;
      g_list_free (interface_infos);

      g_hash_table_insert (repository->interfaces_for_gtype, (gpointer) gtype,
                           cache);

      g_free (interfaces);
    }

  *n_interfaces_out = cache->n_interfaces;
  *interfaces_out = (GIInterfaceInfo**)&cache->interfaces[0];
}

static void
collect_namespaces (GPtrArray  *ordered_typelibs,
                    char      **names,
                    size_t     *inout_i)
{
  for (guint j = 0; j < ordered_typelibs->len; j++)
    {
      GITypelib *typelib = g_ptr_array_index (ordered_typelibs, j);
      const char *namespace = gi_typelib_get_namespace (typelib);
      names[(*inout_i)++] = g_strdup (namespace);
    }
}

/**
 * gi_repository_get_loaded_namespaces:
 * @repository: A #GIRepository
 * @n_namespaces_out: (optional) (out): Return location for the number of
 *   namespaces
 *
 * Return the list of currently loaded namespaces.
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_namespaces_out.
 *
 * Returns: (element-type utf8) (transfer full) (array length=n_namespaces_out):
 *   list of namespaces
 * Since: 2.80
 */
char **
gi_repository_get_loaded_namespaces (GIRepository *repository,
                                     size_t       *n_namespaces_out)
{
  char **names;
  size_t i;
  size_t n_typelibs;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  n_typelibs = repository->ordered_typelibs->len + repository->ordered_lazy_typelibs->len;
  names = g_malloc0 (sizeof (char *) * (n_typelibs + 1));
  i = 0;

  collect_namespaces (repository->ordered_typelibs, names, &i);
  collect_namespaces (repository->ordered_lazy_typelibs, names, &i);

  if (n_namespaces_out != NULL)
    *n_namespaces_out = i;

  return names;
}

/**
 * gi_repository_get_version:
 * @repository: A #GIRepository
 * @namespace_: Namespace to inspect
 *
 * This function returns the loaded version associated with the given
 * namespace @namespace_.
 *
 * Note: The namespace must have already been loaded using a function
 * such as [method@GIRepository.Repository.require] before calling this
 * function.
 *
 * Returns: Loaded version
 * Since: 2.80
 */
const char *
gi_repository_get_version (GIRepository *repository,
                           const char   *namespace)
{
  GITypelib *typelib;
  Header *header;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered (repository, namespace, NULL);

  g_return_val_if_fail (typelib != NULL, NULL);

  header = (Header *) typelib->data;
  return gi_typelib_get_string (typelib, header->nsversion);
}

/**
 * gi_repository_get_shared_libraries:
 * @repository: A #GIRepository
 * @namespace_: Namespace to inspect
 * @out_n_elements: (out) (optional): Return location for the number of elements
 *   in the returned array
 *
 * This function returns an array of paths to the
 * shared C libraries associated with the given namespace @namespace_.
 *
 * There may be no shared library path associated, in which case this
 * function will return `NULL`.
 *
 * Note: The namespace must have already been loaded using a function
 * such as [method@GIRepository.Repository.require] before calling this
 * function.
 *
 * The list is internal to [class@GIRepository.Repository] and should not be
 * freed, nor should its string elements.
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @out_n_elements.
 *
 * Returns: (nullable) (array length=out_n_elements) (transfer none): Array of
 *   paths to shared libraries, or `NULL` if none are associated
 * Since: 2.80
 */
const char * const *
gi_repository_get_shared_libraries (GIRepository *repository,
                                    const char   *namespace,
                                    size_t       *out_n_elements)
{
  GITypelib *typelib;
  Header *header;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered (repository, namespace, NULL);

  g_return_val_if_fail (typelib != NULL, NULL);

  header = (Header *) typelib->data;
  if (!header->shared_library)
    {
      if (out_n_elements != NULL)
        *out_n_elements = 0;
      return NULL;
    }

  /* Populate the cache. */
  if (repository->cached_shared_libraries == NULL)
    {
      const char *comma_separated = gi_typelib_get_string (typelib, header->shared_library);

      if (comma_separated != NULL && *comma_separated != '\0')
        {
          repository->cached_shared_libraries = g_strsplit (comma_separated, ",", -1);
          repository->cached_n_shared_libraries = g_strv_length (repository->cached_shared_libraries);
        }
    }

  if (out_n_elements != NULL)
    *out_n_elements = repository->cached_n_shared_libraries;

  return (const char * const *) repository->cached_shared_libraries;
}

/**
 * gi_repository_get_c_prefix:
 * @repository: A #GIRepository
 * @namespace_: Namespace to inspect
 *
 * This function returns the ‘C prefix’, or the C level namespace
 * associated with the given introspection namespace.
 *
 * Each C symbol starts with this prefix, as well each [type@GObject.Type] in
 * the library.
 *
 * Note: The namespace must have already been loaded using a function
 * such as [method@GIRepository.Repository.require] before calling this
 * function.
 *
 * Returns: (nullable): C namespace prefix, or `NULL` if none associated
 * Since: 2.80
 */
const char *
gi_repository_get_c_prefix (GIRepository *repository,
                            const char   *namespace_)
{
  GITypelib *typelib;
  Header *header;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace_ != NULL, NULL);

  typelib = get_registered (repository, namespace_, NULL);

  g_return_val_if_fail (typelib != NULL, NULL);

  header = (Header *) typelib->data;
  if (header->c_prefix)
    return gi_typelib_get_string (typelib, header->c_prefix);
  else
    return NULL;
}

/**
 * gi_repository_get_typelib_path:
 * @repository: A #GIRepository
 * @namespace_: GI namespace to use, e.g. `Gtk`
 *
 * If namespace @namespace_ is loaded, return the full path to the
 * .typelib file it was loaded from.
 *
 * If the typelib for namespace @namespace_ was included in a shared library,
 * return the special string `<builtin>`.
 *
 * Returns: (type filename) (nullable): Filesystem path (or `<builtin>`) if
 *   successful, `NULL` if namespace is not loaded
 * Since: 2.80
 */
const char *
gi_repository_get_typelib_path (GIRepository *repository,
                                const char   *namespace)
{
  gpointer orig_key, value;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  if (!g_hash_table_lookup_extended (repository->typelibs, namespace,
                                     &orig_key, &value))
    {
      if (!g_hash_table_lookup_extended (repository->lazy_typelibs, namespace,
                                         &orig_key, &value))

        return NULL;
    }
  return ((char*)orig_key) + strlen ((char *) orig_key) + 1;
}

/* This simple search function looks for a specified namespace-version;
   it's faster than the full directory listing required for latest version. */
static GMappedFile *
find_namespace_version (const char          *namespace,
                        const char          *version,
                        const char * const  *search_paths,
                        size_t               n_search_paths,
                        char               **path_ret)
{
  GError *error = NULL;
  GMappedFile *mfile = NULL;
  char *fname;

  if (g_str_equal (namespace, GIREPOSITORY_TYPELIB_NAME) &&
      !g_str_equal (version, GIREPOSITORY_TYPELIB_VERSION))
    {
      g_debug ("Ignoring %s-%s.typelib because this libgirepository "
               "corresponds to %s-%s",
               namespace, version,
               namespace, GIREPOSITORY_TYPELIB_VERSION);
      return NULL;
    }

  fname = g_strdup_printf ("%s-%s.typelib", namespace, version);

  for (size_t i = 0; i < n_search_paths; ++i)
    {
      char *path = g_build_filename (search_paths[i], fname, NULL);

      mfile = g_mapped_file_new (path, FALSE, &error);
      if (error)
        {
          g_free (path);
          g_clear_error (&error);
          continue;
        }
      *path_ret = path;
      break;
    }
  g_free (fname);
  return mfile;
}

static gboolean
parse_version (const char *version,
               int *major,
               int *minor)
{
  const char *dot;
  char *end;

  *major = strtol (version, &end, 10);
  dot = strchr (version, '.');
  if (dot == NULL)
    {
      *minor = 0;
      return TRUE;
    }
  if (dot != end)
    return FALSE;
  *minor = strtol (dot+1, &end, 10);
  if (end != (version + strlen (version)))
    return FALSE;
  return TRUE;
}

static int
compare_version (const char *v1,
                 const char *v2)
{
  gboolean success;
  int v1_major, v1_minor;
  int v2_major, v2_minor;

  success = parse_version (v1, &v1_major, &v1_minor);
  g_assert (success);

  success = parse_version (v2, &v2_major, &v2_minor);
  g_assert (success);

  /* Avoid a compiler warning about `success` being unused with G_DISABLE_ASSERT */
  (void) success;

  if (v1_major > v2_major)
    return 1;
  else if (v2_major > v1_major)
    return -1;
  else if (v1_minor > v2_minor)
    return 1;
  else if (v2_minor > v1_minor)
    return -1;
  return 0;
}

struct NamespaceVersionCandidadate
{
  GMappedFile *mfile;
  int path_index;
  char *path;
  char *version;
};

static int
compare_candidate_reverse (struct NamespaceVersionCandidadate *c1,
                           struct NamespaceVersionCandidadate *c2)
{
  int result = compare_version (c1->version, c2->version);
  /* First, check the version */
  if (result > 0)
    return -1;
  else if (result < 0)
    return 1;
  else
    {
      /* Now check the path index, which says how early in the search path
       * we found it.  This ensures that of equal version targets, we
       * pick the earlier one.
       */
      if (c1->path_index == c2->path_index)
        return 0;
      else if (c1->path_index > c2->path_index)
        return 1;
      else
        return -1;
    }
}

static void
free_candidate (struct NamespaceVersionCandidadate *candidate)
{
  g_mapped_file_unref (candidate->mfile);
  g_free (candidate->path);
  g_free (candidate->version);
  g_slice_free (struct NamespaceVersionCandidadate, candidate);
}

static GSList *
enumerate_namespace_versions (const char         *namespace,
                              const char * const *search_paths,
                              size_t              n_search_paths)
{
  GSList *candidates = NULL;
  GHashTable *found_versions = g_hash_table_new (g_str_hash, g_str_equal);
  char *namespace_dash;
  char *namespace_typelib;
  GError *error = NULL;
  int index;

  namespace_dash = g_strdup_printf ("%s-", namespace);
  namespace_typelib = g_strdup_printf ("%s.typelib", namespace);

  index = 0;
  for (size_t i = 0; i < n_search_paths; ++i)
    {
      GDir *dir;
      const char *dirname;
      const char *entry;

      dirname = search_paths[i];
      dir = g_dir_open (dirname, 0, NULL);
      if (dir == NULL)
        continue;
      while ((entry = g_dir_read_name (dir)) != NULL)
        {
          GMappedFile *mfile;
          char *path, *version;
          struct NamespaceVersionCandidadate *candidate;

          if (!g_str_has_suffix (entry, ".typelib"))
            continue;

          if (g_str_has_prefix (entry, namespace_dash))
            {
              const char *last_dash;
              const char *name_end;
              int major, minor;

              if (g_str_equal (namespace, GIREPOSITORY_TYPELIB_NAME) &&
                  !g_str_equal (entry, GIREPOSITORY_TYPELIB_FILENAME))
                {
                  g_debug ("Ignoring %s because this libgirepository "
                           "corresponds to %s",
                           entry, GIREPOSITORY_TYPELIB_FILENAME);
                  continue;
                }

              name_end = strrchr (entry, '.');
              last_dash = strrchr (entry, '-');
              version = g_strndup (last_dash+1, name_end-(last_dash+1));
              if (!parse_version (version, &major, &minor))
                {
                  g_free (version);
                  continue;
                }
            }
          else
            continue;

          if (g_hash_table_lookup (found_versions, version) != NULL)
            {
              g_free (version);
              continue;
            }

          path = g_build_filename (dirname, entry, NULL);
          mfile = g_mapped_file_new (path, FALSE, &error);
          if (mfile == NULL)
            {
              g_free (path);
              g_free (version);
              g_clear_error (&error);
              continue;
            }
          candidate = g_slice_new0 (struct NamespaceVersionCandidadate);
          candidate->mfile = mfile;
          candidate->path_index = index;
          candidate->path = path;
          candidate->version = version;
          candidates = g_slist_prepend (candidates, candidate);
          g_hash_table_add (found_versions, version);
        }
      g_dir_close (dir);
      index++;
    }

  g_free (namespace_dash);
  g_free (namespace_typelib);
  g_hash_table_destroy (found_versions);

  return candidates;
}

static GMappedFile *
find_namespace_latest (const char          *namespace,
                       const char * const  *search_paths,
                       size_t               n_search_paths,
                       char               **version_ret,
                       char               **path_ret)
{
  GSList *candidates;
  GMappedFile *result = NULL;

  *version_ret = NULL;
  *path_ret = NULL;

  candidates = enumerate_namespace_versions (namespace, search_paths, n_search_paths);

  if (candidates != NULL)
    {
      struct NamespaceVersionCandidadate *elected;
      candidates = g_slist_sort (candidates, (GCompareFunc) compare_candidate_reverse);

      elected = (struct NamespaceVersionCandidadate *) candidates->data;
      /* Remove the elected one so we don't try to free its contents */
      candidates = g_slist_delete_link (candidates, candidates);

      result = elected->mfile;
      *path_ret = elected->path;
      *version_ret = elected->version;
      g_slice_free (struct NamespaceVersionCandidadate, elected); /* just free the container */
      g_slist_foreach (candidates, (GFunc) (void *) free_candidate, NULL);
      g_slist_free (candidates);
    }
  return result;
}

/**
 * gi_repository_enumerate_versions:
 * @repository: A #GIRepository
 * @namespace_: GI namespace, e.g. `Gtk`
 * @n_versions_out: (optional) (out): The number of versions returned.
 *
 * Obtain an unordered list of versions (either currently loaded or
 * available) for @namespace_ in this @repository.
 *
 * The list is guaranteed to be `NULL` terminated. The `NULL` terminator is not
 * counted in @n_versions_out.
 *
 * Returns: (element-type utf8) (transfer full) (array length=n_versions_out): the array of versions.
 * Since: 2.80
 */
char **
gi_repository_enumerate_versions (GIRepository *repository,
                                  const char   *namespace_,
                                  size_t       *n_versions_out)
{
  GPtrArray *versions;
  GSList *candidates, *link;
  const char *loaded_version;
  char **ret;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);

  candidates = enumerate_namespace_versions (namespace_,
                                             (const char * const *) repository->typelib_search_path->pdata,
                                             repository->typelib_search_path->len);

  if (!candidates)
    {
      if (n_versions_out)
        *n_versions_out = 0;
      return g_strdupv ((char *[]) {NULL});
    }

  versions = g_ptr_array_new_null_terminated (1, g_free, TRUE);
  for (link = candidates; link; link = link->next)
    {
      struct NamespaceVersionCandidadate *candidate = link->data;
      g_ptr_array_add (versions, g_steal_pointer (&candidate->version));
      free_candidate (candidate);
    }
  g_slist_free (candidates);

  /* The currently loaded version of a namespace is also part of the
   * available versions, as it could have been loaded using
   * require_private().
   */
  if (gi_repository_is_registered (repository, namespace_, NULL))
    {
      loaded_version = gi_repository_get_version (repository, namespace_);
      if (loaded_version &&
          !g_ptr_array_find_with_equal_func (versions, loaded_version, g_str_equal, NULL))
        g_ptr_array_add (versions, g_strdup (loaded_version));
    }

  ret = (char **) g_ptr_array_steal (versions, n_versions_out);
  g_ptr_array_unref (g_steal_pointer (&versions));

  return ret;
}

static GITypelib *
require_internal (GIRepository           *repository,
                  const char             *namespace,
                  const char             *version,
                  GIRepositoryLoadFlags   flags,
                  const char * const     *search_paths,
                  size_t                  n_search_paths,
                  GError                **error)
{
  GMappedFile *mfile;
  GITypelib *ret = NULL;
  Header *header;
  GITypelib *typelib = NULL;
  GITypelib *typelib_owned = NULL;
  const char *typelib_namespace, *typelib_version;
  gboolean allow_lazy = (flags & GI_REPOSITORY_LOAD_FLAG_LAZY) > 0;
  gboolean is_lazy;
  char *version_conflict = NULL;
  char *path = NULL;
  char *tmp_version = NULL;

  g_return_val_if_fail (GI_IS_REPOSITORY (repository), NULL);
  g_return_val_if_fail (namespace != NULL, NULL);

  typelib = get_registered_status (repository, namespace, version, allow_lazy,
                                   &is_lazy, &version_conflict);
  if (typelib)
    return typelib;

  if (version_conflict != NULL)
    {
      g_set_error (error, GI_REPOSITORY_ERROR,
                   GI_REPOSITORY_ERROR_NAMESPACE_VERSION_CONFLICT,
                   "Requiring namespace '%s' version '%s', but '%s' is already loaded",
                   namespace, version, version_conflict);
      return NULL;
    }

  if (version != NULL)
    {
      mfile = find_namespace_version (namespace, version, search_paths,
                                      n_search_paths, &path);
      tmp_version = g_strdup (version);
    }
  else
    {
      mfile = find_namespace_latest (namespace, search_paths, n_search_paths,
                                     &tmp_version, &path);
    }

  if (mfile == NULL)
    {
      if (version != NULL)
        g_set_error (error, GI_REPOSITORY_ERROR,
                     GI_REPOSITORY_ERROR_TYPELIB_NOT_FOUND,
                     "Typelib file for namespace '%s', version '%s' not found",
                     namespace, version);
      else
        g_set_error (error, GI_REPOSITORY_ERROR,
                     GI_REPOSITORY_ERROR_TYPELIB_NOT_FOUND,
                     "Typelib file for namespace '%s' (any version) not found",
                     namespace);
      goto out;
    }

  {
    GError *temp_error = NULL;
    GBytes *bytes = NULL;

    bytes = g_mapped_file_get_bytes (mfile);
    typelib_owned = typelib = gi_typelib_new_from_bytes (bytes, &temp_error);
    g_bytes_unref (bytes);
    g_clear_pointer (&mfile, g_mapped_file_unref);

    if (!typelib)
      {
        g_set_error (error, GI_REPOSITORY_ERROR,
                     GI_REPOSITORY_ERROR_TYPELIB_NOT_FOUND,
                     "Failed to load typelib file '%s' for namespace '%s': %s",
                     path, namespace, temp_error->message);
        g_clear_error (&temp_error);
        goto out;
      }

    typelib->library_paths = (repository->library_paths != NULL) ? g_ptr_array_ref (repository->library_paths) : NULL;
  }
  header = (Header *) typelib->data;
  typelib_namespace = gi_typelib_get_string (typelib, header->namespace);
  typelib_version = gi_typelib_get_string (typelib, header->nsversion);

  if (strcmp (typelib_namespace, namespace) != 0)
    {
      g_set_error (error, GI_REPOSITORY_ERROR,
                   GI_REPOSITORY_ERROR_NAMESPACE_MISMATCH,
                   "Typelib file %s for namespace '%s' contains "
                   "namespace '%s' which doesn't match the file name",
                   path, namespace, typelib_namespace);
      goto out;
    }
  if (version != NULL && strcmp (typelib_version, version) != 0)
    {
      g_set_error (error, GI_REPOSITORY_ERROR,
                   GI_REPOSITORY_ERROR_NAMESPACE_MISMATCH,
                   "Typelib file %s for namespace '%s' contains "
                   "version '%s' which doesn't match the expected version '%s'",
                   path, namespace, typelib_version, version);
      goto out;
    }

  if (!register_internal (repository, path, allow_lazy,
                          typelib, error))
    goto out;
  ret = typelib;
 out:
  g_clear_pointer (&typelib_owned, gi_typelib_unref);
  g_free (tmp_version);
  g_free (path);
  return ret;
}

/**
 * gi_repository_require:
 * @repository: A #GIRepository
 * @namespace_: GI namespace to use, e.g. `Gtk`
 * @version: (nullable): Version of namespace, may be `NULL` for latest
 * @flags: Set of [flags@GIRepository.RepositoryLoadFlags], may be 0
 * @error: a [type@GLib.Error].
 *
 * Force the namespace @namespace_ to be loaded if it isn’t already.
 *
 * If @namespace_ is not loaded, this function will search for a
 * `.typelib` file using the repository search path.  In addition, a
 * version @version of namespace may be specified.  If @version is
 * not specified, the latest will be used.
 *
 * Returns: (transfer none): a pointer to the [type@GIRepository.Typelib] if
 *   successful, `NULL` otherwise
 * Since: 2.80
 */
GITypelib *
gi_repository_require (GIRepository  *repository,
                       const char    *namespace,
                       const char    *version,
                       GIRepositoryLoadFlags flags,
                       GError       **error)
{
  GITypelib *typelib;

  typelib = require_internal (repository, namespace, version, flags,
                              (const char * const *) repository->typelib_search_path->pdata,
                              repository->typelib_search_path->len, error);

  return typelib;
}

/**
 * gi_repository_require_private:
 * @repository: A #GIRepository
 * @typelib_dir: (type filename): Private directory where to find the requested
 *   typelib
 * @namespace_: GI namespace to use, e.g. `Gtk`
 * @version: (nullable): Version of namespace, may be `NULL` for latest
 * @flags: Set of [flags@GIRepository.RepositoryLoadFlags], may be 0
 * @error: a [type@GLib.Error].
 *
 * Force the namespace @namespace_ to be loaded if it isn’t already.
 *
 * If @namespace_ is not loaded, this function will search for a
 * `.typelib` file within the private directory only. In addition, a
 * version @version of namespace should be specified.  If @version is
 * not specified, the latest will be used.
 *
 * Returns: (transfer none): a pointer to the [type@GIRepository.Typelib] if
 *   successful, `NULL` otherwise
 * Since: 2.80
 */
GITypelib *
gi_repository_require_private (GIRepository           *repository,
                               const char             *typelib_dir,
                               const char             *namespace,
                               const char             *version,
                               GIRepositoryLoadFlags   flags,
                               GError                **error)
{
  const char * const search_path[] = { typelib_dir, NULL };

  return require_internal (repository, namespace, version, flags,
                           search_path, 1, error);
}

static gboolean
gi_repository_introspect_cb (const char *option_name,
                             const char *value,
                             gpointer data,
                             GError **error)
{
  GError *tmp_error = NULL;
  char **args;

  args = g_strsplit (value, ",", 2);

  if (!gi_repository_dump (args[0], args[1], &tmp_error))
    {
      g_error ("Failed to extract GType data: %s",
               tmp_error->message);
      exit (1);
    }
  exit (0);
}

static const GOptionEntry introspection_args[] = {
  { "introspect-dump", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK,
    gi_repository_introspect_cb, "Dump introspection information",
    "infile.txt,outfile.xml" },
  G_OPTION_ENTRY_NULL
};

/**
 * gi_repository_get_option_group:
 *
 * Obtain the option group for girepository.
 *
 * It’s used by the dumper and for programs that want to provide introspection
 * information
 *
 * Returns: (transfer full): the option group
 * Since: 2.80
 */
GOptionGroup *
gi_repository_get_option_group (void)
{
  GOptionGroup *group;
  group = g_option_group_new ("girepository", "Introspection Options", "Show Introspection Options", NULL, NULL);

  g_option_group_add_entries (group, introspection_args);
  return group;
}

GQuark
gi_repository_error_quark (void)
{
  static GQuark quark = 0;
  if (quark == 0)
    quark = g_quark_from_static_string ("g-irepository-error-quark");
  return quark;
}

/**
 * gi_type_tag_to_string:
 * @type: the type_tag
 *
 * Obtain a string representation of @type
 *
 * Returns: the string
 * Since: 2.80
 */
const char *
gi_type_tag_to_string (GITypeTag type)
{
  switch (type)
    {
    case GI_TYPE_TAG_VOID:
      return "void";
    case GI_TYPE_TAG_BOOLEAN:
      return "gboolean";
    case GI_TYPE_TAG_INT8:
      return "gint8";
    case GI_TYPE_TAG_UINT8:
      return "guint8";
    case GI_TYPE_TAG_INT16:
      return "gint16";
    case GI_TYPE_TAG_UINT16:
      return "guint16";
    case GI_TYPE_TAG_INT32:
      return "gint32";
    case GI_TYPE_TAG_UINT32:
      return "guint32";
    case GI_TYPE_TAG_INT64:
      return "gint64";
    case GI_TYPE_TAG_UINT64:
      return "guint64";
    case GI_TYPE_TAG_FLOAT:
      return "gfloat";
    case GI_TYPE_TAG_DOUBLE:
      return "gdouble";
    case GI_TYPE_TAG_UNICHAR:
      return "gunichar";
    case GI_TYPE_TAG_GTYPE:
      return "GType";
    case GI_TYPE_TAG_UTF8:
      return "utf8";
    case GI_TYPE_TAG_FILENAME:
      return "filename";
    case GI_TYPE_TAG_ARRAY:
      return "array";
    case GI_TYPE_TAG_INTERFACE:
      return "interface";
    case GI_TYPE_TAG_GLIST:
      return "glist";
    case GI_TYPE_TAG_GSLIST:
      return "gslist";
    case GI_TYPE_TAG_GHASH:
      return "ghash";
    case GI_TYPE_TAG_ERROR:
      return "error";
    default:
      return "unknown";
    }
}

/**
 * gi_info_type_to_string:
 * @type: the info type
 *
 * Obtain a string representation of @type
 *
 * Returns: the string
 * Since: 2.80
 */
const char *
gi_info_type_to_string (GIInfoType type)
{
  switch (type)
    {
    case GI_INFO_TYPE_INVALID:
      return "invalid";
    case GI_INFO_TYPE_FUNCTION:
      return "function";
    case GI_INFO_TYPE_CALLBACK:
      return "callback";
    case GI_INFO_TYPE_STRUCT:
      return "struct";
    case GI_INFO_TYPE_ENUM:
      return "enum";
    case GI_INFO_TYPE_FLAGS:
      return "flags";
    case GI_INFO_TYPE_OBJECT:
      return "object";
    case GI_INFO_TYPE_INTERFACE:
      return "interface";
    case GI_INFO_TYPE_CONSTANT:
      return "constant";
    case GI_INFO_TYPE_UNION:
      return "union";
    case GI_INFO_TYPE_VALUE:
      return "value";
    case GI_INFO_TYPE_SIGNAL:
      return "signal";
    case GI_INFO_TYPE_VFUNC:
      return "vfunc";
    case GI_INFO_TYPE_PROPERTY:
      return "property";
    case GI_INFO_TYPE_FIELD:
      return "field";
    case GI_INFO_TYPE_ARG:
      return "arg";
    case GI_INFO_TYPE_TYPE:
      return "type";
    case GI_INFO_TYPE_UNRESOLVED:
      return "unresolved";
    default:
      return "unknown";
  }
}

GIInfoType
gi_typelib_blob_type_to_info_type (GITypelibBlobType blob_type)
{
  switch (blob_type)
    {
    case BLOB_TYPE_BOXED:
      /* `BLOB_TYPE_BOXED` now always refers to a `StructBlob`, and
       * `GIRegisteredTypeInfo` (the parent type of `GIStructInfo`) has a method
       * for distinguishing whether the struct is a boxed type. So presenting
       * `BLOB_TYPE_BOXED` as its own `GIBaseInfo` subclass is not helpful.
       * See commit e28078c70cbf4a57c7dbd39626f43f9bd2674145 and
       * https://gitlab.gnome.org/GNOME/glib/-/issues/3245. */
      return GI_INFO_TYPE_STRUCT;
    default:
      return (GIInfoType) blob_type;
    }
}
