/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-2002 Spencer Kimball, Peter Mattis, and others
 *
 * gimpextensionmanager.c
 * Copyright (C) 2018 Jehan <jehan@gimp.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpconfig/gimpconfig.h"

#include "core-types.h"

#include "config/gimpcoreconfig.h"

#include "gimp.h"
#include "gimpextension.h"
#include "gimpextension-error.h"
#include "gimpobject.h"
#include "gimpmarshal.h"
#include "gimp-utils.h"

#include "gimpextensionmanager.h"

#include "gimp-intl.h"


enum
{
  PROP_0,
  PROP_GIMP,
  PROP_BRUSH_PATHS,
  PROP_DYNAMICS_PATHS,
  PROP_MYPAINT_BRUSH_PATHS,
  PROP_PATTERN_PATHS,
  PROP_GRADIENT_PATHS,
  PROP_PALETTE_PATHS,
  PROP_TOOL_PRESET_PATHS,
  PROP_SPLASH_PATHS,
  PROP_THEME_PATHS,
  PROP_PLUG_IN_PATHS,
};

enum
{
  EXTENSION_INSTALLED,
  EXTENSION_REMOVED,
  LAST_SIGNAL
};

struct _GimpExtensionManagerPrivate
{
  Gimp       *gimp;

  /* Installed system (read-only) extensions. */
  GList      *sys_extensions;
  /* Self-installed (read-write) extensions. */
  GList      *extensions;
  /* Uninstalled extensions (cached to allow undo). */
  GList      *uninstalled_extensions;

  /* Running extensions */
  GHashTable *running_extensions;

  /* Metadata properties */
  GList      *brush_paths;
  GList      *dynamics_paths;
  GList      *mypaint_brush_paths;
  GList      *pattern_paths;
  GList      *gradient_paths;
  GList      *palette_paths;
  GList      *tool_preset_paths;
  GList      *splash_paths;
  GList      *theme_paths;
  GList      *plug_in_paths;
};

static void     gimp_extension_manager_config_iface_init   (GimpConfigInterface  *iface);
static gboolean gimp_extension_manager_serialize           (GimpConfig           *config,
                                                            GimpConfigWriter     *writer,
                                                            gpointer              data);
static gboolean gimp_extension_manager_deserialize         (GimpConfig           *config,
                                                            GScanner             *scanner,
                                                            gint                  nest_level,
                                                            gpointer              data);

static void     gimp_extension_manager_finalize            (GObject              *object);
static void     gimp_extension_manager_set_property        (GObject              *object,
                                                            guint                 property_id,
                                                            const GValue         *value,
                                                            GParamSpec           *pspec);
static void     gimp_extension_manager_get_property        (GObject              *object,
                                                            guint                 property_id,
                                                            GValue               *value,
                                                            GParamSpec           *pspec);

static void     gimp_extension_manager_serialize_extension (GimpExtensionManager *manager,
                                                            GimpExtension        *extension,
                                                            GimpConfigWriter     *writer);

static void     gimp_extension_manager_refresh             (GimpExtensionManager *manager);
static void     gimp_extension_manager_search_directory    (GimpExtensionManager *manager,
                                                            GFile                *directory,
                                                            gboolean              system_dir);

static void     gimp_extension_manager_extension_running   (GimpExtension        *extension,
                                                            GParamSpec           *pspec,
                                                            GimpExtensionManager *manager);


G_DEFINE_TYPE_WITH_CODE (GimpExtensionManager, gimp_extension_manager,
                         GIMP_TYPE_OBJECT,
                         G_ADD_PRIVATE (GimpExtensionManager)
                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_CONFIG,
                                                gimp_extension_manager_config_iface_init))

#define parent_class gimp_extension_manager_parent_class

static guint signals[LAST_SIGNAL] = { 0, };

static void
gimp_extension_manager_class_init (GimpExtensionManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize     = gimp_extension_manager_finalize;
  object_class->set_property = gimp_extension_manager_set_property;
  object_class->get_property = gimp_extension_manager_get_property;

  g_object_class_install_property (object_class, PROP_GIMP,
                                   g_param_spec_object ("gimp", NULL, NULL,
                                                        GIMP_TYPE_GIMP,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_BRUSH_PATHS,
                                   g_param_spec_pointer ("brush-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_DYNAMICS_PATHS,
                                   g_param_spec_pointer ("dynamics-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_MYPAINT_BRUSH_PATHS,
                                   g_param_spec_pointer ("mypaint-brush-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_PATTERN_PATHS,
                                   g_param_spec_pointer ("pattern-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_GRADIENT_PATHS,
                                   g_param_spec_pointer ("gradient-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_PALETTE_PATHS,
                                   g_param_spec_pointer ("palette-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_TOOL_PRESET_PATHS,
                                   g_param_spec_pointer ("tool-preset-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_SPLASH_PATHS,
                                   g_param_spec_pointer ("splash-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_THEME_PATHS,
                                   g_param_spec_pointer ("theme-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_PLUG_IN_PATHS,
                                   g_param_spec_pointer ("plug-in-paths",
                                                         NULL, NULL,
                                                         GIMP_PARAM_READWRITE));

  signals[EXTENSION_INSTALLED] =
    g_signal_new ("extension-installed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpExtensionManagerClass, extension_installed),
                  NULL, NULL,
                  gimp_marshal_VOID__OBJECT_BOOLEAN,
                  G_TYPE_NONE, 2,
                  GIMP_TYPE_EXTENSION,
                  G_TYPE_BOOLEAN);
  signals[EXTENSION_REMOVED] =
    g_signal_new ("extension-removed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpExtensionManagerClass, extension_removed),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1,
                  G_TYPE_STRING);
}

static void
gimp_extension_manager_init (GimpExtensionManager *manager)
{
  manager->p = gimp_extension_manager_get_instance_private (manager);
  manager->p->extensions     = NULL;
  manager->p->sys_extensions = NULL;
}

static void
gimp_extension_manager_config_iface_init (GimpConfigInterface *iface)
{
  iface->serialize   = gimp_extension_manager_serialize;
  iface->deserialize = gimp_extension_manager_deserialize;
}

static gboolean
gimp_extension_manager_serialize (GimpConfig       *config,
                                  GimpConfigWriter *writer,
                                  gpointer          data G_GNUC_UNUSED)
{
  GimpExtensionManager *manager = GIMP_EXTENSION_MANAGER (config);
  GList                *iter;

  /* TODO: another information we will want to add will be the last
   * extension list update to allow ourselves to regularly check for
   * updates while not doing it too often.
   */
  for (iter = manager->p->extensions; iter; iter = iter->next)
    gimp_extension_manager_serialize_extension (manager, iter->data, writer);
  for (iter = manager->p->sys_extensions; iter; iter = iter->next)
    {
      if (g_list_find_custom (manager->p->extensions, iter->data,
                              (GCompareFunc) gimp_extension_cmp))
        continue;
      gimp_extension_manager_serialize_extension (manager, iter->data, writer);
    }

  return TRUE;
}

static gboolean
gimp_extension_manager_deserialize (GimpConfig *config,
                                    GScanner   *scanner,
                                    gint        nest_level,
                                    gpointer    data)
{
  GimpExtensionManager  *manager  = GIMP_EXTENSION_MANAGER (config);
  GList                **processed = (GList**) data;
  GTokenType             token;

  token = G_TOKEN_LEFT_PAREN;

  while (g_scanner_peek_next_token (scanner) == token)
    {
      token = g_scanner_get_next_token (scanner);

      switch (token)
        {
        case G_TOKEN_LEFT_PAREN:
          token = G_TOKEN_IDENTIFIER;
          break;

        case G_TOKEN_IDENTIFIER:
          {
            gchar    *name      = NULL;
            gboolean  is_active = FALSE;
            GType     type;

            type = g_type_from_name (scanner->value.v_identifier);

            if (! type)
              {
                g_scanner_error (scanner,
                                 "unable to determine type of '%s'",
                                 scanner->value.v_identifier);
                return FALSE;
              }

            if (! g_type_is_a (type, GIMP_TYPE_EXTENSION))
              {
                g_scanner_error (scanner,
                                 "'%s' is not a subclass of '%s'",
                                 scanner->value.v_identifier,
                                 g_type_name (GIMP_TYPE_EXTENSION));
                return FALSE;
              }

            if (! gimp_scanner_parse_string (scanner, &name))
              {
                g_scanner_error (scanner,
                                 "Expected extension id not found after GimpExtension.");
                return FALSE;
              }

            if (! name || strlen (name) == 0)
              {
                g_scanner_error (scanner,
                                 "NULL or empty strings are not valid extension IDs.");
                if (name)
                  g_free (name);
                return FALSE;
              }

            if (! gimp_scanner_parse_token (scanner, G_TOKEN_LEFT_PAREN))
              {
                g_scanner_error (scanner,
                                 "Left paren expected after extension ID.");
                g_free (name);
                return FALSE;
              }
            if (! gimp_scanner_parse_identifier (scanner, "active"))
              {
                g_scanner_error (scanner,
                                 "Expected identifier \"active\" after extension ID.");
                g_free (name);
                return FALSE;
              }
            if (gimp_scanner_parse_boolean (scanner, &is_active))
              {
                if (is_active)
                  {
                    GList *list;

                    list = g_list_find_custom (manager->p->extensions, name,
                                               (GCompareFunc) gimp_extension_id_cmp);
                    if (! list)
                      list = g_list_find_custom (manager->p->sys_extensions, name,
                                                 (GCompareFunc) gimp_extension_id_cmp);
                    if (list)
                      {
                        GError *error = NULL;

                        if (gimp_extension_run (list->data, &error))
                          {
                            g_hash_table_insert (manager->p->running_extensions,
                                                 (gpointer) gimp_object_get_name (list->data),
                                                 list->data);
                          }
                        else
                          {
                            g_printerr ("Extension '%s' failed to run: %s\n",
                                        gimp_object_get_name (list->data),
                                        error->message);
                            g_error_free (error);
                          }
                      }
                  }

                /* Save the list of processed extension IDs, whether
                 * active or not.
                 */
                *processed = g_list_prepend (*processed, name);
              }
            else
              {
                g_scanner_error (scanner,
                                 "Expected boolean after \"active\" identifier.");
                g_free (name);
                return FALSE;
              }

            if (! gimp_scanner_parse_token (scanner, G_TOKEN_RIGHT_PAREN))
              {
                g_scanner_error (scanner,
                                 "Right paren expected after \"active\" identifier.");
                return FALSE;
              }
          }
          token = G_TOKEN_RIGHT_PAREN;
          break;

        case G_TOKEN_RIGHT_PAREN:
          token = G_TOKEN_LEFT_PAREN;
          break;

        default: /* do nothing */
          break;
        }
    }

  return gimp_config_deserialize_return (scanner, token, nest_level);
}

static void
gimp_extension_manager_finalize (GObject *object)
{
  GList *iter;

  GimpExtensionManager *manager = GIMP_EXTENSION_MANAGER (object);

  g_list_free_full (manager->p->sys_extensions, g_object_unref);
  g_list_free_full (manager->p->extensions, g_object_unref);
  g_hash_table_unref (manager->p->running_extensions);

  for (iter = manager->p->uninstalled_extensions; iter; iter = iter->next)
    {
      /* Recursively delete folders of uninstalled extensions. */
      GError *error = NULL;
      GFile  *file;

      file = g_file_new_for_path (gimp_extension_get_path (iter->data));
      if (! gimp_file_delete_recursive (file, &error))
        g_warning ("%s: %s\n", G_STRFUNC, error->message);
      g_object_unref (file);
    }
  g_list_free_full (manager->p->uninstalled_extensions, g_object_unref);

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

static void
gimp_extension_manager_set_property (GObject      *object,
                                     guint         property_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  GimpExtensionManager *manager = GIMP_EXTENSION_MANAGER (object);

  switch (property_id)
    {
    case PROP_GIMP:
      manager->p->gimp = g_value_get_object (value);
      break;
    case PROP_BRUSH_PATHS:
      manager->p->brush_paths = g_value_get_pointer (value);
      break;
    case PROP_DYNAMICS_PATHS:
      manager->p->dynamics_paths = g_value_get_pointer (value);
      break;
    case PROP_MYPAINT_BRUSH_PATHS:
      manager->p->mypaint_brush_paths = g_value_get_pointer (value);
      break;
    case PROP_PATTERN_PATHS:
      manager->p->pattern_paths = g_value_get_pointer (value);
      break;
    case PROP_GRADIENT_PATHS:
      manager->p->gradient_paths = g_value_get_pointer (value);
      break;
    case PROP_PALETTE_PATHS:
      manager->p->palette_paths = g_value_get_pointer (value);
      break;
    case PROP_TOOL_PRESET_PATHS:
      manager->p->tool_preset_paths = g_value_get_pointer (value);
      break;
    case PROP_SPLASH_PATHS:
      manager->p->splash_paths = g_value_get_pointer (value);
      break;
    case PROP_THEME_PATHS:
      manager->p->theme_paths = g_value_get_pointer (value);
      break;
    case PROP_PLUG_IN_PATHS:
      manager->p->plug_in_paths = g_value_get_pointer (value);
      break;

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

static void
gimp_extension_manager_get_property (GObject      *object,
                                     guint         property_id,
                                     GValue       *value,
                                     GParamSpec   *pspec)
{
  GimpExtensionManager *manager = GIMP_EXTENSION_MANAGER (object);

  switch (property_id)
    {
    case PROP_GIMP:
      g_value_set_object (value, manager->p->gimp);
      break;
    case PROP_BRUSH_PATHS:
      g_value_set_pointer (value, manager->p->brush_paths);
      break;
    case PROP_DYNAMICS_PATHS:
      g_value_set_pointer (value, manager->p->dynamics_paths);
      break;
    case PROP_MYPAINT_BRUSH_PATHS:
      g_value_set_pointer (value, manager->p->mypaint_brush_paths);
      break;
    case PROP_PATTERN_PATHS:
      g_value_set_pointer (value, manager->p->pattern_paths);
      break;
    case PROP_GRADIENT_PATHS:
      g_value_set_pointer (value, manager->p->gradient_paths);
      break;
    case PROP_PALETTE_PATHS:
      g_value_set_pointer (value, manager->p->palette_paths);
      break;
    case PROP_TOOL_PRESET_PATHS:
      g_value_set_pointer (value, manager->p->tool_preset_paths);
      break;
    case PROP_SPLASH_PATHS:
      g_value_set_pointer (value, manager->p->splash_paths);
      break;
    case PROP_THEME_PATHS:
      g_value_set_pointer (value, manager->p->theme_paths);
      break;
    case PROP_PLUG_IN_PATHS:
      g_value_set_pointer (value, manager->p->plug_in_paths);
      break;

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

/* Public functions. */

GimpExtensionManager *
gimp_extension_manager_new (Gimp *gimp)
{
  GimpExtensionManager *manager;

  manager = g_object_new (GIMP_TYPE_EXTENSION_MANAGER,
                          "gimp", gimp,
                          NULL);

  return manager;
}

void
gimp_extension_manager_initialize (GimpExtensionManager *manager)
{
  GFile  *file;
  GError *error = NULL;
  gchar  *path_str;
  GList  *path;
  GList  *list;
  GList  *processed_ids;

  g_return_if_fail (GIMP_IS_EXTENSION_MANAGER (manager));

  /* List user-installed extensions. */
  path_str = gimp_config_build_writable_path ("extensions");
  path = gimp_config_path_expand_to_files (path_str, NULL);
  g_free (path_str);
  for (list = path; list; list = g_list_next (list))
    gimp_extension_manager_search_directory (manager, list->data, FALSE);
  g_list_free_full (path, (GDestroyNotify) g_object_unref);

  /* List system extensions. */
  path_str = gimp_config_build_system_path ("extensions");
  path = gimp_config_path_expand_to_files (path_str, NULL);
  g_free (path_str);
  for (list = path; list; list = g_list_next (list))
    gimp_extension_manager_search_directory (manager, list->data, TRUE);
  g_list_free_full (path, (GDestroyNotify) g_object_unref);

  /* Actually load the extensions. */
  if (manager->p->running_extensions)
    g_hash_table_unref (manager->p->running_extensions);
  manager->p->running_extensions = g_hash_table_new (g_str_hash, g_str_equal);

  file = gimp_directory_file ("extensionrc", NULL);

  processed_ids = NULL;
  if (g_file_query_exists (file, NULL))
    {
      if (manager->p->gimp->be_verbose)
        g_print ("Parsing '%s'\n", gimp_file_get_utf8_name (file));

      gimp_config_deserialize_file (GIMP_CONFIG (manager),
                                    file, &processed_ids, &error);
      if (error)
        {
          g_printerr ("Failed to parse '%s': %s\n",
                      gimp_file_get_utf8_name (file),
                      error->message);
          g_error_free (error);
        }
    }
  g_object_unref (file);

  /* System extensions are on by default and user ones are off by
   * default */
  for (list = manager->p->extensions; list; list = list->next)
    {
      /* Directly flag user-installed extensions as processed. We do not
       * load them unless they were explicitly loaded (i.e. they must be
       * set in extensionrc.
       */
      if (! g_list_find_custom (processed_ids,
                                gimp_object_get_name (list->data),
                                (GCompareFunc) g_strcmp0))
        processed_ids = g_list_prepend (processed_ids,
                                        g_strdup (gimp_object_get_name (list->data)));
      g_signal_connect (list->data, "notify::running",
                        G_CALLBACK (gimp_extension_manager_extension_running),
                        manager);
    }
  for (list = manager->p->sys_extensions; list; list = g_list_next (list))
    {
      /* Unlike user-installed extensions, system extensions are loaded
       * by default if they were not set in the extensionrc (so that new
       * extensions installed with GIMP updates get loaded) and if they
       * were not overridden by a user-installed extension (same ID).
       */
      if (! g_list_find_custom (processed_ids,
                                gimp_object_get_name (list->data),
                                (GCompareFunc) g_strcmp0))

        {
          error = NULL;
          if (gimp_extension_run (list->data, &error))
            {
              g_hash_table_insert (manager->p->running_extensions,
                                   (gpointer) gimp_object_get_name (list->data),
                                   list->data);
            }
          else
            {
              g_printerr ("Extension '%s' failed to run: %s\n",
                          gimp_object_get_name (list->data),
                          error->message);
              g_error_free (error);
            }
        }
      g_signal_connect (list->data, "notify::running",
                        G_CALLBACK (gimp_extension_manager_extension_running),
                        manager);
    }

  gimp_extension_manager_refresh (manager);
  g_list_free_full (processed_ids, g_free);
}

void
gimp_extension_manager_exit (GimpExtensionManager *manager)
{
  GFile  *file;
  GError *error = NULL;

  g_return_if_fail (GIMP_IS_EXTENSION_MANAGER (manager));

  file = gimp_directory_file ("extensionrc", NULL);

  if (manager->p->gimp->be_verbose)
    g_print ("Writing '%s'\n", gimp_file_get_utf8_name (file));

  if (! gimp_config_serialize_to_file (GIMP_CONFIG (manager),
                                       file,
                                       "GIMP extensionrc",
                                       "end of extensionrc",
                                       NULL,
                                       &error))
    {
      gimp_message_literal (manager->p->gimp, NULL, GIMP_MESSAGE_ERROR, error->message);
      g_error_free (error);
    }

  g_object_unref (file);
}

const GList *
gimp_extension_manager_get_system_extensions (GimpExtensionManager *manager)
{
  return manager->p->sys_extensions;
}

const GList *
gimp_extension_manager_get_user_extensions (GimpExtensionManager *manager)
{
  return manager->p->extensions;
}

/**
 * gimp_extension_manager_is_running:
 * @extension:
 *
 * Returns: %TRUE if @extension is ON.
 */
gboolean
gimp_extension_manager_is_running (GimpExtensionManager *manager,
                                   GimpExtension        *extension)
{
  GimpExtension *ext;

  ext = g_hash_table_lookup (manager->p->running_extensions,
                             gimp_object_get_name (extension));

  return (ext && ext == extension);
}

/**
 * gimp_extension_manager_can_run:
 * @extension:
 *
 * Returns: %TRUE is @extension can be run.
 */
gboolean
gimp_extension_manager_can_run (GimpExtensionManager *manager,
                                GimpExtension        *extension)
{
  /* System extension overridden by another extension. */
  if (g_list_find (manager->p->sys_extensions, extension) &&
      g_list_find_custom (manager->p->extensions, extension,
                          (GCompareFunc) gimp_extension_cmp))
    return FALSE;

  /* TODO: should return FALSE if required GIMP version or other
   * requirements are not filled as well.
   */

  return TRUE;
}

/**
 * gimp_extension_manager_is_removed:
 * @manager:
 * @extension:
 *
 * Returns: %TRUE is @extension was installed and has been removed
 * (hence gimp_extension_manager_undo_remove() can be used on it).
 */
gboolean
gimp_extension_manager_is_removed (GimpExtensionManager *manager,
                                   GimpExtension        *extension)
{
  GList *iter;

  g_return_val_if_fail (GIMP_IS_EXTENSION_MANAGER (manager), FALSE);
  g_return_val_if_fail (GIMP_IS_EXTENSION (extension), FALSE);

  iter = manager->p->uninstalled_extensions;
  for (; iter; iter = iter->next)
    if (gimp_extension_cmp (iter->data, extension) == 0)
      break;

  return (iter != NULL);
}

/**
 * gimp_extension_manager_install:
 * @manager:
 * @extension:
 * @error:
 *
 * Install @extension. This merely adds the extension in the known
 * extension list to make the manager aware of it at runtime, and to
 * emit a signal for GUI update.
 */
gboolean
gimp_extension_manager_install (GimpExtensionManager *manager,
                                GimpExtension        *extension,
                                GError              **error)
{
  gboolean success = FALSE;

  if ((success = gimp_extension_load (extension, error)))
    {
      manager->p->extensions = g_list_prepend (manager->p->extensions,
                                               extension);
      g_signal_connect (extension, "notify::running",
                        G_CALLBACK (gimp_extension_manager_extension_running),
                        manager);
      g_signal_emit (manager, signals[EXTENSION_INSTALLED], 0, extension, FALSE);
    }

  return success;
}

/**
 * gimp_extension_manager_remove:
 * @manager:
 * @extension:
 * @error:
 *
 * Uninstall @extension. Technically this only move the object to a
 * temporary list. The extension folder will be really deleted when GIMP
 * will stop.
 * This allows to undo a deletion for as long as the session runs.
 */
gboolean
gimp_extension_manager_remove (GimpExtensionManager  *manager,
                               GimpExtension         *extension,
                               GError               **error)
{
  GList *iter;

  g_return_val_if_fail (GIMP_IS_EXTENSION_MANAGER (manager), FALSE);
  g_return_val_if_fail (GIMP_IS_EXTENSION (extension), FALSE);

  iter = (GList *) gimp_extension_manager_get_system_extensions (manager);
  for (; iter; iter = iter->next)
    if (iter->data == extension)
      {
        /* System extensions cannot be uninstalled. */
        if (error)
          *error = g_error_new (GIMP_EXTENSION_ERROR,
                                GIMP_EXTENSION_FAILED,
                                _("System extensions cannot be uninstalled."));
        return FALSE;
      }

  iter = (GList *) gimp_extension_manager_get_user_extensions (manager);
  for (; iter; iter = iter->next)
    if (gimp_extension_cmp (iter->data, extension) == 0)
      break;

  /* The extension has to be in the extension list. */
  g_return_val_if_fail (iter != NULL, FALSE);

  gimp_extension_stop (extension);

  manager->p->extensions = g_list_remove_link (manager->p->extensions,
                                               iter);
  manager->p->uninstalled_extensions = g_list_concat (manager->p->uninstalled_extensions,
                                                      iter);
  g_signal_emit (manager, signals[EXTENSION_REMOVED], 0,
                 gimp_object_get_name (extension));

  return TRUE;
}

gboolean
gimp_extension_manager_undo_remove (GimpExtensionManager *manager,
                                    GimpExtension        *extension,
                                    GError              **error)
{
  GList *iter;

  g_return_val_if_fail (GIMP_IS_EXTENSION_MANAGER (manager), FALSE);
  g_return_val_if_fail (GIMP_IS_EXTENSION (extension), FALSE);

  iter = manager->p->uninstalled_extensions;
  for (; iter; iter = iter->next)
    if (gimp_extension_cmp (iter->data, extension) == 0)
      break;

  /* The extension has to be in the uninstalled extension list. */
  g_return_val_if_fail (iter != NULL, FALSE);

  manager->p->uninstalled_extensions = g_list_remove (manager->p->uninstalled_extensions,
                                                      extension);
  gimp_extension_manager_install (manager, extension, error);

  return TRUE;
}

/* Private functions. */

static void
gimp_extension_manager_serialize_extension (GimpExtensionManager *manager,
                                            GimpExtension        *extension,
                                            GimpConfigWriter     *writer)
{
  const gchar *name = gimp_object_get_name (extension);

  g_return_if_fail (name != NULL);

  gimp_config_writer_open (writer, g_type_name (G_TYPE_FROM_INSTANCE (extension)));
  gimp_config_writer_string (writer, name);

  /* The extensionrc does not need to save any information about an
   * extension. This is all saved in the metadata. The only thing we
   * care to store is the state of extensions, i.e. which one is loaded
   * or not (since you can install an extension but leave it unloaded),
   * named by its unique ID.
   * As a consequence, GimpExtension does not need to implement
   * GimpConfigInterface.
   */
  gimp_config_writer_open (writer, "active");
  if (g_hash_table_contains (manager->p->running_extensions, name))
    gimp_config_writer_identifier (writer, "yes");
  else
    gimp_config_writer_identifier (writer, "no");
  gimp_config_writer_close (writer);

  gimp_config_writer_close (writer);
}

static void
gimp_extension_manager_refresh (GimpExtensionManager *manager)
{
  GHashTableIter iter;
  gpointer       key;
  gpointer       value;
  GList         *brush_paths         = NULL;
  GList         *dynamics_paths      = NULL;
  GList         *mypaint_brush_paths = NULL;
  GList         *pattern_paths       = NULL;
  GList         *gradient_paths      = NULL;
  GList         *palette_paths       = NULL;
  GList         *tool_preset_paths   = NULL;
  GList         *splash_paths        = NULL;
  GList         *theme_paths         = NULL;
  GList         *plug_in_paths       = NULL;

  g_hash_table_iter_init (&iter, manager->p->running_extensions);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      GimpExtension *extension = value;
      GList         *new_paths;

      new_paths = g_list_copy_deep (gimp_extension_get_brush_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      brush_paths = g_list_concat (brush_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_dynamics_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      dynamics_paths = g_list_concat (dynamics_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_mypaint_brush_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      mypaint_brush_paths = g_list_concat (mypaint_brush_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_pattern_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      pattern_paths = g_list_concat (pattern_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_gradient_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      gradient_paths = g_list_concat (gradient_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_palette_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      palette_paths = g_list_concat (palette_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_tool_preset_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      tool_preset_paths = g_list_concat (tool_preset_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_splash_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      splash_paths = g_list_concat (splash_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_theme_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      theme_paths = g_list_concat (theme_paths, new_paths);

      new_paths = g_list_copy_deep (gimp_extension_get_plug_in_paths (extension),
                                    (GCopyFunc) g_object_ref, NULL);
      plug_in_paths = g_list_concat (plug_in_paths, new_paths);
    }

  g_object_set (manager,
                "brush-paths",         brush_paths,
                "dynamics-paths",      dynamics_paths,
                "mypaint-brush-paths", brush_paths,
                "pattern-paths",       pattern_paths,
                "gradient-paths",      gradient_paths,
                "palette-paths",       palette_paths,
                "tool-preset-paths",   tool_preset_paths,
                "plug-in-paths",       plug_in_paths,
                "splash-paths",        splash_paths,
                "theme-paths",         theme_paths,
                NULL);
}

static void
gimp_extension_manager_search_directory (GimpExtensionManager *manager,
                                         GFile                *directory,
                                         gboolean              system_dir)
{
  GFileEnumerator *enumerator;

  enumerator = g_file_enumerate_children (directory,
                                          G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                          G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                          G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                          G_FILE_QUERY_INFO_NONE,
                                          NULL, NULL);
  if (enumerator)
    {
      GFileInfo *info;

      while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)))
        {
          GFile *subdir;

          if (g_file_info_get_is_hidden (info))
            {
              g_object_unref (info);
              continue;
            }

          subdir = g_file_enumerator_get_child (enumerator, info);

          if (g_file_query_file_type (subdir,
                                      G_FILE_QUERY_INFO_NONE,
                                      NULL) == G_FILE_TYPE_DIRECTORY)
            {
              GimpExtension *extension;
              GError        *error = NULL;

              extension = gimp_extension_new (g_file_peek_path (subdir),
                                              ! system_dir);

              if (gimp_extension_load (extension, &error))
                {
                  if (system_dir)
                    manager->p->sys_extensions = g_list_prepend (manager->p->sys_extensions,
                                                                 extension);
                  else
                    manager->p->extensions = g_list_prepend (manager->p->extensions,
                                                             extension);
                }
              else
                {
                  g_object_unref (extension);
                  if (error)
                    {
                      g_printerr (_("Skipping extension '%s': %s\n"),
                                  g_file_peek_path (subdir), error->message);
                      g_error_free (error);
                    }
                }
            }
          else
            {
              g_printerr (_("Skipping unknown file '%s' in extension directory.\n"),
                          g_file_peek_path (subdir));
            }

          g_object_unref (subdir);
          g_object_unref (info);
        }

      g_object_unref (enumerator);
    }
}

static void
gimp_extension_manager_extension_running (GimpExtension        *extension,
                                          GParamSpec           *pspec,
                                          GimpExtensionManager *manager)
{
  gboolean running;

  g_object_get (extension,
                "running", &running,
                NULL);
  if (running)
    g_hash_table_insert (manager->p->running_extensions,
                         (gpointer) gimp_object_get_name (extension),
                         extension);
  else
    g_hash_table_remove (manager->p->running_extensions,
                         (gpointer) gimp_object_get_name (extension));

  gimp_extension_manager_refresh (manager);
}
