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

#include "config.h"

#include <errno.h>
#include <libintl.h>
#include <string.h>

#include "gimp.h"

#include "libgimpbase/gimpprotocol.h"
#include "libgimpbase/gimpwire.h"

#include "gimp-private.h"
#include "gimp-shm.h"
#include "gimpgpparams.h"
#include "gimpplugin-private.h"
#include "gimpplugin_pdb.h"


/**
 * GimpPlugIn:
 *
 * The base class for plug-ins to derive from.
 *
 * GimpPlugIn manages the plug-in's [class@Procedure] objects. The procedures a
 * plug-in implements are registered with GIMP by returning a list of their
 * names from either [vfunc@GimpPlugIn.query_procedures] or
 * [vfunc@GimpPlugIn.init_procedures].
 *
 * Every GIMP plug-in has to be implemented as a subclass and make it known to
 * the libgimp infrastructure and the main GIMP application by passing its
 * `GType` to [func@MAIN].
 *
 * [func@MAIN] passes the 'argc' and 'argv' of the platform's main() function,
 * along with the `GType`, to [func@main], which creates an instance of the
 * plug-in's `GimpPlugIn` subclass and calls its virtual functions, depending
 * on how the plug-in was called by GIMP.
 *
 * There are 3 different ways GIMP calls a plug-in: "query", "init" and "run".
 *
 * The plug-in is called in "query" mode once after it was installed, or when
 * the cached plug-in information in the config file "pluginrc" needs to be
 * recreated. In "query" mode, [vfunc@GimpPlugIn.query_procedures] is called
 * and returns a list of procedure names the plug-in implements. This is the
 * "normal" place to register procedures, because the existence of most
 * procedures doesn't depend on things that change between GIMP sessions.
 *
 * The plug-in is called in "init" mode at each GIMP startup, and
 * [vfunc@PlugIn.init_procedures] is called and returns a list of procedure
 * names this plug-in implements. This only happens if the plug-in actually
 * implements [vfunc@GimpPlugIn.init_procedures]. A plug-in only needs to
 * implement init_procedures if the existence of its procedures can change
 * between GIMP sessions, for example if they depend on the presence of
 * external tools, or hardware like scanners, or online services, or whatever
 * variable circumstances.
 *
 * In order to register the plug-in's procedures with the main GIMP application
 * in the plug-in's "query" and "init" modes, [class@PlugIn] calls
 * [vfunc@PlugIn.create_procedure] on all procedure names in the exact order of
 * the list returned by [vfunc@PlugIn.query_procedures] or
 * [vfunc@PlugIn.init_procedures] and then registers the returned
 * [class@Procedure].
 *
 * The plug-in is called in "run" mode whenever one of the procedures it
 * implements is called by either the main GIMP application or any other
 * plug-in. In "run" mode, one of the procedure names returned by
 * [vfunc@PlugIn.query_procedures] or [vfunc@PlugIn.init_procedures] is passed
 * to [vfunc@PlugIn.create_procedure] which must return a [class@Procedure] for
 * the passed name. The procedure is then executed by calling
 * [method@Procedure.run].
 *
 * In any of the three modes, [vfunc@PlugIn.quit] is called before the plug-in
 * process exits, so the plug-in can perform whatever cleanup necessary.
 *
 * Since: 3.0
 */


#define WRITE_BUFFER_SIZE 1024

/**
 * gimp_plug_in_error_quark:
 *
 * Generic #GQuark error domain for plug-ins. Plug-ins are welcome to
 * create their own domain when they want to handle advanced error
 * handling. Often, you just want to pass an error message to the core.
 * This domain can be used for such simple usage.
 *
 * See #GError for information on error domains.
 */
G_DEFINE_QUARK (gimp-plug-in-error-quark, gimp_plug_in_error)

enum
{
  PROP_0,
  PROP_PROGRAM_NAME,
  PROP_READ_CHANNEL,
  PROP_WRITE_CHANNEL,
  N_PROPS
};


typedef struct _GimpPlugInMenuBranch GimpPlugInMenuBranch;

struct _GimpPlugInMenuBranch
{
  gchar *menu_path;
  gchar *menu_label;
};

typedef struct _GimpPlugInPrivate
{
  gchar      *program_name;

  GIOChannel *read_channel;
  GIOChannel *write_channel;

  gchar       write_buffer[WRITE_BUFFER_SIZE];
  gulong      write_buffer_index;

  guint       persistent_source_id;

  gchar      *translation_domain_name;
  GFile      *translation_domain_path;

  gchar      *help_domain_name;
  GFile      *help_domain_uri;

  GList      *menu_branches;

  GList      *temp_procedures;

  GList      *procedure_stack;
  GList      *ran_procedure_stack;
  GHashTable *displays;
  GHashTable *images;
  GHashTable *items;
  GHashTable *filters;
  GHashTable *resources;
} GimpPlugInPrivate;


static void       gimp_plug_in_constructed       (GObject         *object);
static void       gimp_plug_in_dispose           (GObject         *object);
static void       gimp_plug_in_finalize          (GObject         *object);
static void       gimp_plug_in_set_property      (GObject         *object,
                                                  guint            property_id,
                                                  const GValue    *value,
                                                  GParamSpec      *pspec);
static void       gimp_plug_in_get_property      (GObject         *object,
                                                  guint            property_id,
                                                  GValue          *value,
                                                  GParamSpec      *pspec);

static gboolean   gimp_plug_in_real_set_i18n     (GimpPlugIn      *plug_in,
                                                  const gchar     *procedure_name,
                                                  gchar          **gettext_domain,
                                                  gchar          **catalog_dir);

static void       gimp_plug_in_register          (GimpPlugIn      *plug_in,
                                                  GList           *procedures);

static gboolean   gimp_plug_in_write             (GIOChannel      *channel,
                                                  const guint8    *buf,
                                                  gulong           count,
                                                  gpointer         user_data);
static gboolean   gimp_plug_in_flush             (GIOChannel      *channel,
                                                  gpointer         user_data);
static gboolean   gimp_plug_in_io_error_handler  (GIOChannel      *channel,
                                                  GIOCondition     cond,
                                                  gpointer         data);

static void       gimp_plug_in_loop              (GimpPlugIn      *plug_in);
static void       gimp_plug_in_single_message    (GimpPlugIn      *plug_in);
static void       gimp_plug_in_process_message   (GimpPlugIn      *plug_in,
                                                  GimpWireMessage *msg);
static void       gimp_plug_in_main_proc_run     (GimpPlugIn      *plug_in,
                                                  GPProcRun       *proc_run);
static void       gimp_plug_in_temp_proc_run     (GimpPlugIn      *plug_in,
                                                  GPProcRun       *proc_run);
static void       gimp_plug_in_proc_run          (GPProcRun       *proc_run,
                                                  GimpProcedure   *procedure,
                                                  GPProcReturn    *proc_return);
static void       gimp_plug_in_proc_run_internal (GimpPlugIn      *plug_in,
                                                  GPProcRun       *proc_run,
                                                  GimpProcedure   *procedure,
                                                  GPProcReturn    *proc_return);
static gboolean   gimp_plug_in_persistent_read   (GIOChannel      *channel,
                                                  GIOCondition     condition,
                                                  gpointer         data);

static void       gimp_plug_in_push_procedure    (GimpPlugIn      *plug_in,
                                                  GimpProcedure   *procedure);
static void       gimp_plug_in_pop_procedure     (GimpPlugIn      *plug_in,
                                                  GimpProcedure   *procedure);
static gboolean   gimp_plug_in_is_procedure_stack_empty (
                                                  GimpPlugIn      *plug_in);

static void       gimp_plug_in_main_run_cleanup  (GimpPlugIn      *plug_in);
static void       gimp_plug_in_temp_run_cleanup  (GimpPlugIn      *plug_in);
static void       gimp_plug_in_destroy_hashes    (GimpPlugIn      *plug_in);
static void       gimp_plug_in_destroy_all_proxies
                                                 (GimpPlugIn      *plug_in);
static void       gimp_plug_in_destroy_proxies   (GimpPlugIn      *plug_in,
                                                  GHashTable      *hash_table,
                                                  const gchar     *type,
                                                  gboolean         destroy_all);

static void       gimp_plug_in_init_i18n         (GimpPlugIn      *plug_in);


G_DEFINE_TYPE_WITH_PRIVATE (GimpPlugIn, gimp_plug_in, G_TYPE_OBJECT)

#define parent_class gimp_plug_in_parent_class

static GParamSpec *props[N_PROPS] = { NULL, };


static void
gimp_plug_in_class_init (GimpPlugInClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed  = gimp_plug_in_constructed;
  object_class->dispose      = gimp_plug_in_dispose;
  object_class->finalize     = gimp_plug_in_finalize;
  object_class->set_property = gimp_plug_in_set_property;
  object_class->get_property = gimp_plug_in_get_property;

  klass->set_i18n            = gimp_plug_in_real_set_i18n;

  /**
   * GimpPlugIn:program-name:
   *
   * The program name as usually found on argv[0]
   */
  props[PROP_PROGRAM_NAME] =
    g_param_spec_string ("program-name",
                         "The plug-in executable",
                         "The executable name as usually found on argv[0]",
                         NULL,
                         GIMP_PARAM_WRITABLE |
                         G_PARAM_CONSTRUCT_ONLY);

  /**
   * GimpPlugIn:read-channel:
   *
   * The [struct@GLib.IOChannel] to read from GIMP
   */
  props[PROP_READ_CHANNEL] =
    g_param_spec_boxed ("read-channel",
                        "Read channel",
                        "The GIOChanel to read from GIMP",
                        G_TYPE_IO_CHANNEL,
                        GIMP_PARAM_READWRITE |
                        G_PARAM_CONSTRUCT_ONLY);

  /**
   * GimpPlugIn:write-channel:
   *
   * The [struct@GLib.IOChannel] to write to GIMP
   */
  props[PROP_WRITE_CHANNEL] =
    g_param_spec_boxed ("write-channel",
                        "Write channel",
                        "The GIOChanel to write to GIMP",
                        G_TYPE_IO_CHANNEL,
                        GIMP_PARAM_READWRITE |
                        G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, props);
}

static void
gimp_plug_in_init (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  priv->procedure_stack     = NULL;
  priv->ran_procedure_stack = NULL;
  priv->temp_procedures     = NULL;
}

static void
gimp_plug_in_constructed (GObject *object)
{
  GimpPlugIn        *plug_in = GIMP_PLUG_IN (object);
  GimpPlugInPrivate *priv;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_assert (priv->read_channel != NULL);
  g_assert (priv->write_channel != NULL);

  gp_init ();

  gimp_wire_set_writer (gimp_plug_in_write);
  gimp_wire_set_flusher (gimp_plug_in_flush);
}

static void
gimp_plug_in_dispose (GObject *object)
{
  GimpPlugIn        *plug_in = GIMP_PLUG_IN (object);
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (priv->persistent_source_id)
    {
      g_source_remove (priv->persistent_source_id);
      priv->persistent_source_id = 0;
    }

  if (priv->temp_procedures)
    {
      g_list_free_full (priv->temp_procedures, g_object_unref);
      priv->temp_procedures = NULL;
    }

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

static void
gimp_plug_in_finalize (GObject *object)
{
  GimpPlugIn        *plug_in = GIMP_PLUG_IN (object);
  GimpPlugInPrivate *priv;
  GList             *list;

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_clear_pointer (&priv->program_name, g_free);
  g_clear_pointer (&priv->translation_domain_name, g_free);
  g_clear_object  (&priv->translation_domain_path);

  g_clear_pointer (&priv->help_domain_name, g_free);
  g_clear_object  (&priv->help_domain_uri);

  for (list = priv->menu_branches; list; list = g_list_next (list))
    {
      GimpPlugInMenuBranch *branch = list->data;

      g_free (branch->menu_path);
      g_free (branch->menu_label);
      g_slice_free (GimpPlugInMenuBranch, branch);
    }

  g_clear_pointer (&priv->menu_branches, g_list_free);

  gimp_plug_in_destroy_proxies (plug_in, priv->displays,  "display",  TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->images,    "image",    TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->items,     "item",     TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->filters,   "filters",  TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->resources, "resource", TRUE);

  gimp_plug_in_destroy_hashes (plug_in);
  g_clear_list (&priv->ran_procedure_stack, g_object_unref);

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

static void
gimp_plug_in_set_property (GObject      *object,
                           guint         property_id,
                           const GValue *value,
                           GParamSpec   *pspec)
{
  GimpPlugIn        *plug_in = GIMP_PLUG_IN (object);
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  switch (property_id)
    {
    case PROP_PROGRAM_NAME:
      priv->program_name = g_value_dup_string (value);
      break;

    case PROP_READ_CHANNEL:
      priv->read_channel = g_value_get_boxed (value);
      break;

    case PROP_WRITE_CHANNEL:
      priv->write_channel = g_value_get_boxed (value);
      break;

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

static void
gimp_plug_in_get_property (GObject    *object,
                           guint       property_id,
                           GValue     *value,
                           GParamSpec *pspec)
{
  GimpPlugIn        *plug_in = GIMP_PLUG_IN (object);
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  switch (property_id)
    {
    case PROP_READ_CHANNEL:
      g_value_set_boxed (value, priv->read_channel);
      break;

    case PROP_WRITE_CHANNEL:
      g_value_set_boxed (value, priv->write_channel);
      break;

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

static gboolean
gimp_plug_in_real_set_i18n (GimpPlugIn   *plug_in,
                            const gchar  *procedure_name,
                            gchar       **gettext_domain,
                            gchar       **catalog_dir)
{
  /* Default to enabling localization by gettext. It will have the good
   * side-effect of warning plug-in developers of the existence of the
   * ability through stderr if the catalog directory is missing.
   */
  return TRUE;
}


/*  public functions  */

/**
 * gimp_plug_in_set_help_domain:
 * @plug_in:     A #GimpPlugIn.
 * @domain_name: The XML namespace of the plug-in's help pages.
 * @domain_uri:  The root URI of the plug-in's help pages.
 *
 * Set a help domain and path for the @plug_in.
 *
 * This function registers user documentation for the calling plug-in
 * with the GIMP help system. The @domain_uri parameter points to the
 * root directory where the plug-in help is installed. For each
 * supported language there should be a file called 'gimp-help.xml'
 * that maps the help IDs to the actual help files.
 *
 * This function can only be called in the
 * [vfunc@PlugIn.query_procedures] function of a plug-in.
 *
 * Since: 3.0
 **/
void
gimp_plug_in_set_help_domain (GimpPlugIn  *plug_in,
                              const gchar *domain_name,
                              GFile       *domain_uri)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
  g_return_if_fail (domain_name != NULL);
  g_return_if_fail (G_IS_FILE (domain_uri));

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_free (priv->help_domain_name);
  priv->help_domain_name = g_strdup (domain_name);

  g_set_object (&priv->help_domain_uri, domain_uri);
}

/**
 * gimp_plug_in_add_menu_branch:
 * @plug_in:    A #GimpPlugIn
 * @menu_path:  The sub-menu's menu path.
 * @menu_label: The menu label of the sub-menu.
 *
 * Add a new sub-menu to the GIMP menus.
 *
 * This function installs a sub-menu which does not belong to any
 * procedure at the location @menu_path.
 *
 * For translations of @menu_label to work properly, @menu_label
 * should only be marked for translation but passed to this function
 * untranslated, for example using N_("Submenu"). GIMP will look up
 * the translation in the textdomain registered for the plug-in.
 *
 * See also: gimp_procedure_add_menu_path().
 *
 * Since: 3.0
 **/
void
gimp_plug_in_add_menu_branch (GimpPlugIn  *plug_in,
                              const gchar *menu_path,
                              const gchar *menu_label)
{
  GimpPlugInPrivate    *priv;
  GimpPlugInMenuBranch *branch;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
  g_return_if_fail (menu_path != NULL);
  g_return_if_fail (menu_label != NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  branch = g_slice_new (GimpPlugInMenuBranch);

  branch->menu_path  = g_strdup (menu_path);
  branch->menu_label = g_strdup (menu_label);

  priv->menu_branches = g_list_append (priv->menu_branches, branch);
}

/**
 * gimp_plug_in_add_temp_procedure:
 * @plug_in:   A #GimpPlugIn
 * @procedure: A #GimpProcedure of type %GIMP_PDB_PROC_TYPE_TEMPORARY.
 *
 * This function adds a temporary procedure to @plug_in. It is usually
 * called from a %GIMP_PDB_PROC_TYPE_PERSISTENT procedure's
 * [vfunc@Procedure.run].
 *
 * A temporary procedure is a procedure which is only available while
 * one of your plug-in's "real" procedures is running.
 *
 * The procedure's type _must_ be
 * %GIMP_PDB_PROC_TYPE_TEMPORARY or the function will fail.
 *
 * NOTE: Normally, plug-in communication is triggered by the plug-in
 * and the GIMP core only responds to the plug-in's requests. You must
 * explicitly enable receiving of temporary procedure run requests
 * using either [method@PlugIn.persistent_enable] or
 * [method@PlugIn.persistent_process]. See their respective
 * documentation for details.
 *
 * Since: 3.0
 **/
void
gimp_plug_in_add_temp_procedure (GimpPlugIn    *plug_in,
                                 GimpProcedure *procedure)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
  g_return_if_fail (GIMP_IS_PROCEDURE (procedure));
  g_return_if_fail (gimp_procedure_get_proc_type (procedure) ==
                    GIMP_PDB_PROC_TYPE_TEMPORARY);

  priv = gimp_plug_in_get_instance_private (plug_in);

  priv->temp_procedures =
    g_list_prepend (priv->temp_procedures,
                    g_object_ref (procedure));

  GIMP_PROCEDURE_GET_CLASS (procedure)->install (procedure);
}

/**
 * gimp_plug_in_remove_temp_procedure:
 * @plug_in:        A #GimpPlugIn
 * @procedure_name: The name of a [class@Procedure] added to @plug_in.
 *
 * This function removes a temporary procedure from @plug_in by the
 * procedure's @procedure_name.
 *
 * Since: 3.0
 **/
void
gimp_plug_in_remove_temp_procedure (GimpPlugIn  *plug_in,
                                    const gchar *procedure_name)
{
  GimpProcedure *procedure;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
  g_return_if_fail (gimp_is_canonical_identifier (procedure_name));

  procedure = gimp_plug_in_get_temp_procedure (plug_in, procedure_name);

  if (procedure)
    {
      GimpPlugInPrivate *priv;

      GIMP_PROCEDURE_GET_CLASS (procedure)->uninstall (procedure);

      priv = gimp_plug_in_get_instance_private (plug_in);

      priv->temp_procedures = g_list_remove (priv->temp_procedures, procedure);
      if (! g_list_find (priv->ran_procedure_stack, procedure))
        g_object_unref (procedure);
    }
}

/**
 * gimp_plug_in_get_temp_procedures:
 * @plug_in: A plug-in
 *
 * This function retrieves the list of temporary procedure of @plug_in as
 * added with [method@PlugIn.add_temp_procedure].
 *
 * Returns: (transfer none) (element-type GimpProcedure): The list of
 *          procedures.
 *
 * Since: 3.0
 **/
GList *
gimp_plug_in_get_temp_procedures (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  return priv->temp_procedures;
}

/**
 * gimp_plug_in_get_temp_procedure:
 * @plug_in:        A #GimpPlugIn
 * @procedure_name: The name of a [class@Procedure] added to @plug_in.
 *
 * This function retrieves a temporary procedure from @plug_in by the
 * procedure's @procedure_name.
 *
 * Returns: (nullable) (transfer none): The procedure if registered, or %NULL.
 *
 * Since: 3.0
 **/
GimpProcedure *
gimp_plug_in_get_temp_procedure (GimpPlugIn  *plug_in,
                                 const gchar *procedure_name)
{
  GimpPlugInPrivate *priv;
  GList             *list;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
  g_return_val_if_fail (gimp_is_canonical_identifier (procedure_name), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  for (list = priv->temp_procedures; list; list = g_list_next (list))
    {
      GimpProcedure *procedure = list->data;

      if (! strcmp (procedure_name, gimp_procedure_get_name (procedure)))
        return procedure;
    }

  return NULL;
}

/**
 * gimp_plug_in_persistent_enable:
 * @plug_in: A plug-in
 *
 * Enables asynchronous processing of messages from the main GIMP
 * application.
 *
 * Normally, a plug-in is not called by GIMP except for the call to
 * the procedure it implements. All subsequent communication is
 * triggered by the plug-in and all messages sent from GIMP to the
 * plug-in are just answers to requests the plug-in made.
 *
 * If the plug-in however registered temporary procedures using
 * [method@PlugIn.add_temp_procedure], it needs to be able to receive
 * requests to execute them. Usually this will be done by running
 * [method@PlugIn.persistent_process] in an endless loop.
 *
 * If the plug-in cannot use [method@PlugIn.persistent_process], i.e. if
 * it has a GUI and is hanging around in a [struct@GLib.MainLoop], it
 * must call [method@PlugIn.persistent_enable].
 *
 * Note that the plug-in does not need to be a
 * [enum@Gimp.PDBProcType.PERSISTENT] to register temporary procedures.
 *
 * See also: [method@PlugIn.add_temp_procedure].
 *
 * Since: 3.0
 **/
void
gimp_plug_in_persistent_enable (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (! priv->persistent_source_id)
    {
      priv->persistent_source_id =
        g_io_add_watch (priv->read_channel, G_IO_IN | G_IO_PRI,
                        gimp_plug_in_persistent_read,
                        plug_in);

      /* #12631
       * Similar code in app/plug-in/gimpplugin.c.
       *
       * The sequence of events and calls which requires set can_recurse
       * on the IO source of events:
       *
       * - A user choosing a menu item implemented by persistent plugin script-fu
       *   writes a "run temp proc" msg to the pipe to the plugin.
       * - On the plugin side, Glib generates an event which invokes the
       *   handler plug_in_persistent_read, of type GIOFunc .
       * - The handler ultimately shows a dialog having its own event loop.
       * - When the dialog has a resource select widget and the user clicks one,
       *   the plugin creates a temporary PDB procedure for a callback,
       *   and calls a PDB procedure to open a remote resource chooser widget
       *   in the app, passing the name of the callback.
       * - The user choosing a resource in the remote chooser widget
       *   invokes the callback by writing a second "run temp proc" msg
       *   to the pipe to the plugin.
       * - Unless can_recurse is set, the second write (the callback) by the app
       *   is blocked and does not generate an event in the plugin.
       * - The dialog event loop receives no event, doesn't read the pipe,
       *   and fails to update its resource select widget with the user's choice.
       *   The message in the pipe is then unexpected by the plugin
       *   for example when the user OKs the dialog.
       */
      g_source_set_can_recurse (
        g_main_context_find_source_by_id (NULL, priv->persistent_source_id),
        TRUE);
    }
}

/**
 * gimp_plug_in_persistent_process:
 * @plug_in: A plug-in.
 * @timeout: The timeout (in ms) to use for the select() call.
 *
 * Processes one message sent by GIMP and returns.
 *
 * Call this function in an endless loop after calling
 * [method@Gimp.Procedure.persistent_ready] to process requests for
 * running temporary procedures.
 *
 * See [method@PlugIn.persistent_enable] for an asynchronous way of
 * doing the same if running an endless loop is not an option.
 *
 * See also: [method@PlugIn.add_temp_procedure].
 *
 * Since: 3.0
 **/
void
gimp_plug_in_persistent_process (GimpPlugIn *plug_in,
                                 guint       timeout)
{
  GimpPlugInPrivate *priv;
#ifndef G_OS_WIN32
  gint               select_val;
#else
  /* Zero means infinite wait for us, but g_poll and
   * g_io_channel_win32_poll use -1 to indicate
   * infinite wait.
   */
  GPollFD            pollfd;
#endif

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

#ifndef G_OS_WIN32
  do
    {
      fd_set readfds;
      struct timeval  tv;
      struct timeval *tvp;

      if (timeout)
        {
          tv.tv_sec  = timeout / 1000;
          tv.tv_usec = (timeout % 1000) * 1000;
          tvp = &tv;
        }
      else
        tvp = NULL;

      FD_ZERO (&readfds);
      FD_SET (g_io_channel_unix_get_fd (priv->read_channel),
              &readfds);

      if ((select_val = select (FD_SETSIZE, &readfds, NULL, NULL, tvp)) > 0)
        {
          gimp_plug_in_single_message (plug_in);
        }
      else if (select_val == -1 && errno != EINTR)
        {
          perror ("gimp_plug_in_persistent_process");
          gimp_quit ();
        }
    }
  while (select_val == -1 && errno == EINTR);

#else
  if (timeout == 0)
    timeout = -1;

  g_io_channel_win32_make_pollfd (priv->read_channel, G_IO_IN,
                                  &pollfd);

  if (g_io_channel_win32_poll (&pollfd, 1, timeout) == 1)
    {
      gimp_plug_in_single_message (plug_in);
    }

#endif
}

/**
 * gimp_plug_in_set_pdb_error_handler:
 * @plug_in: A plug-in
 * @handler: Who is responsible for handling procedure call errors.
 *
 * Sets an error handler for procedure calls.
 *
 * This procedure changes the way that errors in procedure calls are
 * handled. By default GIMP will raise an error dialog if a procedure
 * call made by a plug-in fails. Using this procedure the plug-in can
 * change this behavior. If the error handler is set to
 * %GIMP_PDB_ERROR_HANDLER_PLUGIN, then the plug-in is responsible for
 * calling gimp_pdb_get_last_error() and handling the error whenever
 * one if its procedure calls fails. It can do this by displaying the
 * error message or by forwarding it in its own return values.
 *
 * Since: 3.0
 **/
void
gimp_plug_in_set_pdb_error_handler (GimpPlugIn          *plug_in,
                                    GimpPDBErrorHandler  handler)
{
  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  _gimp_plug_in_set_pdb_error_handler (handler);
}

/**
 * gimp_plug_in_get_pdb_error_handler:
 * @plug_in: A plug-in
 *
 * Retrieves the active error handler for procedure calls.
 *
 * This procedure retrieves the currently active error handler for
 * procedure calls made by the calling plug-in. See
 * gimp_plugin_set_pdb_error_handler() for details.
 *
 * Returns: Who is responsible for handling procedure call errors.
 *
 * Since: 3.0
 **/
GimpPDBErrorHandler
gimp_plug_in_get_pdb_error_handler (GimpPlugIn *plug_in)
{
  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in),
                        GIMP_PDB_ERROR_HANDLER_INTERNAL);

  return _gimp_plug_in_get_pdb_error_handler ();
}


/*  internal functions  */

void
_gimp_plug_in_query (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures)
    gp_has_init_write (priv->write_channel, plug_in);

  if (GIMP_PLUG_IN_GET_CLASS (plug_in)->query_procedures)
    {
      GList *procedures =
        GIMP_PLUG_IN_GET_CLASS (plug_in)->query_procedures (plug_in);

      gimp_plug_in_register (plug_in, procedures);
    }
}

void
_gimp_plug_in_init (GimpPlugIn *plug_in)
{
  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  if (GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures)
    {
      GList *procedures =
        GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures (plug_in);

      gimp_plug_in_register (plug_in, procedures);
    }
}

void
_gimp_plug_in_run (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_io_add_watch (priv->read_channel,
                  G_IO_ERR | G_IO_HUP,
                  gimp_plug_in_io_error_handler,
                  NULL);

  gimp_plug_in_loop (plug_in);
}

void
_gimp_plug_in_quit (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (GIMP_PLUG_IN_GET_CLASS (plug_in)->quit)
    GIMP_PLUG_IN_GET_CLASS (plug_in)->quit (plug_in);

  _gimp_shm_close ();

  gp_quit_write (priv->write_channel, plug_in);
}

GIOChannel *
_gimp_plug_in_get_read_channel (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  return priv->read_channel;
}

GIOChannel *
_gimp_plug_in_get_write_channel (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  return priv->write_channel;
}

void
_gimp_plug_in_read_expect_msg (GimpPlugIn      *plug_in,
                               GimpWireMessage *msg,
                               gint             type)
{
  GimpPlugInPrivate *priv;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  while (TRUE)
    {
      if (! gimp_wire_read_msg (priv->read_channel, msg, NULL))
        gimp_quit ();

      if (msg->type == type)
        return; /* up to the caller to call wire_destroy() */

      if (msg->type == GP_TEMP_PROC_RUN || msg->type == GP_QUIT)
        {
          gimp_plug_in_process_message (plug_in, msg);
        }
      else
        {
          g_error ("unexpected message: %d", msg->type);
        }

      gimp_wire_destroy (msg);
    }
}

gboolean
_gimp_plug_in_set_i18n (GimpPlugIn   *plug_in,
                        const gchar  *procedure_name,
                        gchar       **gettext_domain,
                        gchar       **catalog_dir)
{
  GimpPlugInPrivate *priv;
  gboolean           use_gettext;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), FALSE);
  g_return_val_if_fail (gettext_domain && *gettext_domain == NULL, FALSE);
  g_return_val_if_fail (catalog_dir && *catalog_dir == NULL, FALSE);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (! priv->translation_domain_path ||
      ! priv->translation_domain_name)
    gimp_plug_in_init_i18n (plug_in);

  if (! GIMP_PLUG_IN_GET_CLASS (plug_in)->set_i18n)
    {
      use_gettext = FALSE;
    }
  else
    {
      gchar *utf8_catalog_dir = NULL;

      use_gettext = GIMP_PLUG_IN_GET_CLASS (plug_in)->set_i18n (plug_in,
                                                                procedure_name,
                                                                gettext_domain,
                                                                &utf8_catalog_dir);
      if (use_gettext)
        {
          gboolean reserved = FALSE;

          if (*gettext_domain == NULL)
            {
              *gettext_domain = g_strdup (priv->translation_domain_name);
            }
          else if (g_strcmp0 (*gettext_domain, GETTEXT_PACKAGE "-std-plug-ins") == 0 ||
                   g_strcmp0 (*gettext_domain, GETTEXT_PACKAGE "-script-fu") == 0 ||
                   g_strcmp0 (*gettext_domain, GETTEXT_PACKAGE "-python") == 0)
            {
              /* We special-case these 3 domains as the only ones where
               * it is allowed to set an absolute system dir (actually
               * set by the lib itself; devs must set NULL). See docs of
               * set_i18n() method.
               */
              if (utf8_catalog_dir != NULL)
                g_printerr ("[%s] Do not set a catalog directory with set_i18n() with reserved domain: %s\n",
                            procedure_name, *gettext_domain);

              *catalog_dir = g_strdup (gimp_locale_directory ());
              reserved = TRUE;
            }

          if (utf8_catalog_dir != NULL && *catalog_dir == NULL)
            {
              GError *error = NULL;

              /* The passed-on catalog directory is in UTF-8 because this is
               * usually hardcoded (in which case it's easier to request a
               * specific encoding, chosen at development time, rather than the
               * "OS encoding", depending on runtime).
               * But now we want to transform it to the encoding used for
               * filenames by GLib.
               */
              *catalog_dir = g_filename_from_utf8 (utf8_catalog_dir, -1, NULL, NULL, &error);

              if (*catalog_dir == NULL)
                g_printerr ("[%s] provided catalog directory is not proper UTF-8: %s\n",
                            procedure_name, error ? error->message : "(N/A)");

              g_clear_error (&error);
            }

          if (*catalog_dir && ! reserved)
            {
              if (g_path_is_absolute (*catalog_dir))
                {
                  g_printerr ("[%s] The catalog directory set by set_i18n() is not relative: %s\n",
                              procedure_name, *catalog_dir);
                  g_printerr ("[%s] Localization disabled\n", procedure_name);

                  use_gettext = FALSE;
                }
              else
                {
                  gchar *rootdir   = g_path_get_dirname (gimp_get_progname ());
                  GFile *root_file = g_file_new_for_path (rootdir);
                  GFile *catalog_file;
                  GFile *parent_p  = NULL;
                  GFile *parent;

                  catalog_file = g_file_resolve_relative_path (root_file, *catalog_dir);

                  /* Verify that the catalog is a subdir of the plug-in folder.
                   * We do not want to allow plug-ins to look outside their own
                   * realm.
                   */
                  parent = g_file_dup (catalog_file);
                  do
                    {
                      g_clear_object (&parent_p);
                      if (g_file_equal (parent, root_file))
                        break;
                      parent_p = parent;
                    }
                  while ((parent = g_file_get_parent (parent)));

                  if (parent == NULL)
                    {
                      g_printerr ("[%s] The catalog directory set by set_i18n() is not a subdirectory: %s\n",
                                  procedure_name, *catalog_dir);
                      g_printerr ("[%s] Localization disabled\n", procedure_name);

                      use_gettext = FALSE;
                    }

                  g_free (*catalog_dir);
                  *catalog_dir = g_file_get_path (catalog_file);

                  g_free (rootdir);
                  g_object_unref (root_file);
                  g_clear_object (&parent);
                  g_clear_object (&parent_p);
                  g_object_unref (catalog_file);
                }
            }
          else if (! *catalog_dir)
            {
              *catalog_dir = g_file_get_path (priv->translation_domain_path);
            }
        }

      g_clear_pointer (&utf8_catalog_dir, g_free);
    }

  if (use_gettext && ! g_file_test (*catalog_dir, G_FILE_TEST_IS_DIR))
    {
      g_printerr ("[%s] The catalog directory does not exist: %s\n",
                  procedure_name, *catalog_dir);
      g_printerr ("[%s] Override method set_i18n() for the plug-in to customize or disable localization.\n",
                  procedure_name);
      g_printerr ("[%s] Localization disabled\n", procedure_name);

      use_gettext = FALSE;
    }

  if (! use_gettext)
    {
      g_clear_pointer (gettext_domain, g_free);
      g_clear_pointer (catalog_dir, g_free);
    }

  return use_gettext;
}

GimpProcedure *
_gimp_plug_in_create_procedure (GimpPlugIn  *plug_in,
                                const gchar *procedure_name)
{
  gchar *gettext_domain = NULL;
  gchar *catalog_dir    = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
  g_return_val_if_fail (gimp_is_canonical_identifier (procedure_name), NULL);

  if (_gimp_plug_in_set_i18n (plug_in, procedure_name, &gettext_domain, &catalog_dir))
    {
      gimp_bind_text_domain (gettext_domain, catalog_dir);
#ifdef HAVE_BIND_TEXTDOMAIN_CODESET
      bind_textdomain_codeset (gettext_domain, "UTF-8");
#endif
      textdomain (gettext_domain);

      g_free (gettext_domain);
      g_free (catalog_dir);
    }

  if (GIMP_PLUG_IN_GET_CLASS (plug_in)->create_procedure)
    return GIMP_PLUG_IN_GET_CLASS (plug_in)->create_procedure (plug_in,
                                                               procedure_name);

  return NULL;
}


/*  private functions  */

static void
gimp_plug_in_register (GimpPlugIn *plug_in,
                       GList      *procedures)
{
  GimpPlugInPrivate *priv;
  GList             *list;

  for (list = procedures; list; list = g_list_next (list))
    {
      const gchar   *name = list->data;
      GimpProcedure *procedure;

      procedure = _gimp_plug_in_create_procedure (plug_in, name);
      if (procedure)
        {
          GIMP_PROCEDURE_GET_CLASS (procedure)->install (procedure);
          g_object_unref (procedure);
        }
      else
        {
          g_warning ("Plug-in failed to create procedure '%s'\n", name);
        }
    }

  g_list_free_full (procedures, g_free);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (priv->help_domain_name)
    {
      _gimp_plug_in_help_register (priv->help_domain_name,
                                   priv->help_domain_uri);
    }

  for (list = priv->menu_branches; list; list = g_list_next (list))
    {
      GimpPlugInMenuBranch *branch = list->data;

      _gimp_plug_in_menu_branch_register (branch->menu_path,
                                          branch->menu_label);
    }
}

static gboolean
gimp_plug_in_write (GIOChannel   *channel,
                    const guint8 *buf,
                    gulong        count,
                    gpointer      user_data)
{
  GimpPlugIn        *plug_in = user_data;
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  while (count > 0)
    {
      gulong bytes;

      if ((priv->write_buffer_index + count) >= WRITE_BUFFER_SIZE)
        {
          bytes = WRITE_BUFFER_SIZE - priv->write_buffer_index;
          memcpy (&priv->write_buffer[priv->write_buffer_index],
                  buf, bytes);
          priv->write_buffer_index += bytes;

          if (! gimp_wire_flush (channel, plug_in))
            return FALSE;
        }
      else
        {
          bytes = count;
          memcpy (&priv->write_buffer[priv->write_buffer_index],
                  buf, bytes);
          priv->write_buffer_index += bytes;
        }

      buf   += bytes;
      count -= bytes;
    }

  return TRUE;
}

static gboolean
gimp_plug_in_flush (GIOChannel *channel,
                    gpointer    user_data)
{
  GimpPlugIn        *plug_in = user_data;
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (priv->write_buffer_index > 0)
    {
      gsize count = 0;

      while (count != priv->write_buffer_index)
        {
          GIOStatus status;
          gsize     bytes;
          GError   *error = NULL;

          do
            {
              bytes = 0;
              status = g_io_channel_write_chars (channel,
                                                 &priv->write_buffer[count],
                                                 (priv->write_buffer_index - count),
                                                 &bytes,
                                                 &error);
            }
          while (status == G_IO_STATUS_AGAIN);

          if (status != G_IO_STATUS_NORMAL)
            {
              if (error)
                {
                  g_warning ("%s: gimp_flush(): error: %s",
                             g_get_prgname (), error->message);
                  g_error_free (error);
                }
              else
                {
                  g_warning ("%s: gimp_flush(): error", g_get_prgname ());
                }

              return FALSE;
            }

          count += bytes;
        }

      priv->write_buffer_index = 0;
    }

  return TRUE;
}

static gboolean
gimp_plug_in_io_error_handler (GIOChannel   *channel,
                               GIOCondition  cond,
                               gpointer      data)
{
  g_printerr ("%s: fatal error: GIMP crashed\n", gimp_get_progname ());
  gimp_quit ();

  /* never reached */
  return TRUE;
}

static void
gimp_plug_in_loop (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  priv = gimp_plug_in_get_instance_private (plug_in);

  while (TRUE)
    {
      GimpWireMessage msg;

      if (! gimp_wire_read_msg (priv->read_channel, &msg, NULL))
        return;

      switch (msg.type)
        {
        case GP_QUIT:
          gimp_wire_destroy (&msg);
          return;

        case GP_CONFIG:
          _gimp_config (msg.data);
          break;

        case GP_TILE_REQ:
        case GP_TILE_ACK:
        case GP_TILE_DATA:
          g_warning ("unexpected tile message received (should not happen)");
          break;

        case GP_PROC_RUN:
          gimp_plug_in_main_proc_run (plug_in, msg.data);
          gimp_wire_destroy (&msg);
          return;

        case GP_PROC_RETURN:
          g_warning ("unexpected proc return message received (should not happen)");
          break;

        case GP_TEMP_PROC_RUN:
          g_warning ("unexpected temp proc run message received (should not happen");
          break;

        case GP_TEMP_PROC_RETURN:
          g_warning ("unexpected temp proc return message received (should not happen");
          break;

        case GP_PROC_INSTALL:
          g_warning ("unexpected proc install message received (should not happen)");
          break;

        case GP_HAS_INIT:
          g_warning ("unexpected has init message received (should not happen)");
          break;
        }

      gimp_wire_destroy (&msg);
    }
}

static void
gimp_plug_in_single_message (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;
  GimpWireMessage    msg;

  priv = gimp_plug_in_get_instance_private (plug_in);

  /* Run a temp function */
  if (! gimp_wire_read_msg (priv->read_channel, &msg, NULL))
    gimp_quit ();

  gimp_plug_in_process_message (plug_in, &msg);

  gimp_wire_destroy (&msg);
}

static void
gimp_plug_in_process_message (GimpPlugIn      *plug_in,
                              GimpWireMessage *msg)
{
  switch (msg->type)
    {
    case GP_QUIT:
      gimp_quit ();
      break;
    case GP_CONFIG:
      _gimp_config (msg->data);
      break;
    case GP_TILE_REQ:
    case GP_TILE_ACK:
    case GP_TILE_DATA:
      g_warning ("unexpected tile message received (should not happen)");
      break;
    case GP_PROC_RUN:
      g_warning ("unexpected proc run message received (should not happen)");
      break;
    case GP_PROC_RETURN:
      g_warning ("unexpected proc return message received (should not happen)");
      break;
    case GP_TEMP_PROC_RUN:
      gimp_plug_in_temp_proc_run (plug_in, msg->data);
      break;
    case GP_TEMP_PROC_RETURN:
      g_warning ("unexpected temp proc return message received (should not happen)");
      break;
    case GP_PROC_INSTALL:
      g_warning ("unexpected proc install message received (should not happen)");
      break;
    case GP_HAS_INIT:
      g_warning ("unexpected has init message received (should not happen)");
      break;
    }
}

/* Run a proc that is main, i.e. root of a plugin call stack. */
static void
gimp_plug_in_main_proc_run (GimpPlugIn *plug_in,
                            GPProcRun  *proc_run)
{
  GimpPlugInPrivate *priv;
  GPProcReturn       proc_return;
  GimpProcedure     *procedure;

  procedure = _gimp_plug_in_create_procedure (plug_in, proc_run->name);
  priv      = gimp_plug_in_get_instance_private (plug_in);

  if (procedure)
    gimp_plug_in_proc_run_internal (plug_in,
                                    proc_run, procedure,
                                    &proc_return);

  gimp_plug_in_main_run_cleanup (plug_in);

  if (! gp_proc_return_write (priv->write_channel,
                              &proc_return, plug_in))
    gimp_quit ();

  _gimp_gp_params_free (proc_return.params, proc_return.n_params, TRUE);
}

static void
gimp_plug_in_temp_proc_run (GimpPlugIn *plug_in,
                            GPProcRun  *proc_run)
{
  GimpPlugInPrivate *priv;
  GPProcReturn       proc_return;
  GimpProcedure     *procedure;

  procedure = gimp_plug_in_get_temp_procedure (plug_in, proc_run->name);
  priv      = gimp_plug_in_get_instance_private (plug_in);

  if (procedure)
    gimp_plug_in_proc_run_internal (plug_in,
                                    proc_run, procedure,
                                    &proc_return);

  gimp_plug_in_temp_run_cleanup (plug_in);

  if (! gp_temp_proc_return_write (priv->write_channel,
                                   &proc_return, plug_in))
    gimp_quit ();

  _gimp_gp_params_free (proc_return.params, proc_return.n_params, TRUE);
}

/* Run the proc, passing args from proc_run
 * and returning values in proc_return.
 * This does not alter the state of the GimpPlugin.
 */
static void
gimp_plug_in_proc_run (GPProcRun     *proc_run,
                       GimpProcedure *procedure,
                       GPProcReturn  *proc_return)
{
  GimpValueArray *arguments;
  GimpValueArray *return_values  = NULL;

  arguments = _gimp_gp_params_to_value_array (NULL,
                                              NULL, 0,
                                              proc_run->params,
                                              proc_run->n_params,
                                              FALSE);

  return_values = _gimp_procedure_run_array (procedure, arguments);

  gimp_value_array_unref (arguments);

  proc_return->name     = proc_run->name;
  proc_return->n_params = gimp_value_array_length (return_values);
  proc_return->params   = _gimp_value_array_to_gp_params (return_values, TRUE);

  gimp_value_array_unref (return_values);
}


/* Setup translation, maintain proc stack, and run proc.
 * Proc is a main or temp proc.
 * "internal" means private, not that the proc is type INTERNAL.
 */
static void
gimp_plug_in_proc_run_internal (GimpPlugIn    *plug_in,
                                GPProcRun     *proc_run,
                                GimpProcedure *procedure,
                                GPProcReturn  *proc_return)
{
  gchar *gettext_domain = NULL;
  gchar *catalog_dir    = NULL;

  if (_gimp_plug_in_set_i18n (plug_in, gimp_procedure_get_name (procedure),
                              &gettext_domain, &catalog_dir))
    {
      gimp_bind_text_domain (gettext_domain, catalog_dir);
#ifdef HAVE_BIND_TEXTDOMAIN_CODESET
      bind_textdomain_codeset (gettext_domain, "UTF-8");
#endif
      textdomain (gettext_domain);

      g_free (gettext_domain);
      g_free (catalog_dir);
    }

  gimp_plug_in_push_procedure (plug_in, procedure);

  gimp_plug_in_proc_run (proc_run, procedure, proc_return);

  gimp_plug_in_pop_procedure (plug_in, procedure);
}

static gboolean
gimp_plug_in_persistent_read (GIOChannel  *channel,
                              GIOCondition condition,
                              gpointer     data)
{
  GimpPlugIn *plug_in = data;

  gimp_plug_in_single_message (plug_in);

  return G_SOURCE_CONTINUE;
}


/*  procedure stack / display-, image-, item-cache  */


GimpProcedure *
_gimp_plug_in_get_procedure (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_return_val_if_fail (priv->procedure_stack != NULL, NULL);

  return priv->procedure_stack->data;
}

static gboolean
gimp_plug_in_is_procedure_stack_empty (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  return (priv->procedure_stack == NULL);
}

static void
gimp_plug_in_push_procedure (GimpPlugIn    *plug_in,
                             GimpProcedure *procedure)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  priv->procedure_stack =
    g_list_prepend (priv->procedure_stack, procedure);
}

/* After a run of a main proc, cleanup.
 * We are about to return to GIMP or another calling plugin process.
 * This plugin process will soon terminate.
 *
 * Expect the proc stack is empty: don't destroy proxies
 * when there are still calling procs that might have a reference.
 */
static void
gimp_plug_in_main_run_cleanup (GimpPlugIn *plug_in)
{
  if (gimp_plug_in_is_procedure_stack_empty (plug_in))
    {
      g_debug ("%s proc stack empty, destroy proxies.", G_STRFUNC);
      gimp_plug_in_destroy_all_proxies (plug_in);
      gimp_plug_in_destroy_hashes (plug_in);
    }
  else
    {
      /* Unexpected. */
      g_warning ("%s proc stack not empty when main proc returns.", G_STRFUNC);
    }
}

/* After a run of a temp proc, cleanup.
 * We are about to return to another calling proc.
 *
 * When the just-run temp proc is returning to top proc on stack and it
 * is a persistent plug-in, cleanup is destroy the plugin's proxies.
 * The proc stack is never empty for a persistent plug-in: the top is
 * e.g. extension-script-fu, which must not reference proxies.
 */
static void
gimp_plug_in_temp_run_cleanup (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  /* When at top of proc stack and top is a persistent plug-in, destroy proxies. */
  if ((g_list_length (priv->procedure_stack) == 1) &&
      (gimp_procedure_get_proc_type (priv->procedure_stack->data) == GIMP_PDB_PROC_TYPE_PERSISTENT))
    {
      g_debug ("%s top of proc stack is a persistent plug-in, destroy proxies.", G_STRFUNC);
      gimp_plug_in_destroy_all_proxies (plug_in);
      gimp_plug_in_destroy_hashes (plug_in);
    }
  else
    {
      /* Normal.  The temp proc just run was called by a calling proc
       * which is not a main proc of a persistent plug-in.
       * We can't destroy proxies because the calling proc may retain a reference.
       */
      g_debug ("%s Not destroy proxies for temp proc.", G_STRFUNC);
    }
}

static void
gimp_plug_in_pop_procedure (GimpPlugIn    *plug_in,
                            GimpProcedure *procedure)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  priv->procedure_stack     = g_list_remove (priv->procedure_stack, procedure);
  if (! g_list_find (priv->ran_procedure_stack, procedure))
    priv->ran_procedure_stack = g_list_prepend (priv->ran_procedure_stack, procedure);

  /* Don't destroy proxies now because any proc, especially temporary procs,
   * may have passed a reference to a proc higher in the stack e.g. the main procedure.
   * We don't have separate proxy hashes for each pushed procedure,
   * only a hash table for the run.
   */
}

GimpDisplay *
_gimp_plug_in_get_display (GimpPlugIn *plug_in,
                           gint32      display_id)
{
  GimpPlugInPrivate *priv;

  GimpDisplay *display = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (G_UNLIKELY (! priv->displays))
    priv->displays =
      g_hash_table_new_full (g_direct_hash,
                             g_direct_equal,
                             NULL,
                             (GDestroyNotify) g_object_unref);

  display = g_hash_table_lookup (priv->displays,
                                 GINT_TO_POINTER (display_id));

  if (! display)
    {
      display = g_object_new (GIMP_TYPE_DISPLAY,
                              "id", display_id,
                              NULL);

      g_hash_table_insert (priv->displays,
                           GINT_TO_POINTER (display_id),
                           display);
    }

  return display;
}

GimpImage *
_gimp_plug_in_get_image (GimpPlugIn *plug_in,
                         gint32      image_id)
{
  GimpPlugInPrivate *priv;
  GimpImage         *image = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (G_UNLIKELY (! priv->images))
    priv->images =
      g_hash_table_new_full (g_direct_hash,
                             g_direct_equal,
                             NULL,
                             (GDestroyNotify) g_object_unref);

  image = g_hash_table_lookup (priv->images,
                               GINT_TO_POINTER (image_id));

  if (! image)
    {
      image = g_object_new (GIMP_TYPE_IMAGE,
                            "id", image_id,
                            NULL);

      g_hash_table_insert (priv->images,
                           GINT_TO_POINTER (image_id),
                           image);
    }

  return image;
}

GimpItem *
_gimp_plug_in_get_item (GimpPlugIn *plug_in,
                        gint32      item_id)
{
  GimpPlugInPrivate *priv;
  GimpItem          *item = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (G_UNLIKELY (! priv->items))
    priv->items =
      g_hash_table_new_full (g_direct_hash,
                             g_direct_equal,
                             NULL,
                             (GDestroyNotify) g_object_unref);

  item = g_hash_table_lookup (priv->items,
                              GINT_TO_POINTER (item_id));

  if (! item)
    {
      if (gimp_item_id_is_text_layer (item_id))
        {
          item = g_object_new (GIMP_TYPE_TEXT_LAYER,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_group_layer (item_id))
        {
          item = g_object_new (GIMP_TYPE_GROUP_LAYER,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_layer (item_id))
        {
          item = g_object_new (GIMP_TYPE_LAYER,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_layer_mask (item_id))
        {
          item = g_object_new (GIMP_TYPE_LAYER_MASK,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_selection (item_id))
        {
          item = g_object_new (GIMP_TYPE_SELECTION,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_channel (item_id))
        {
          item = g_object_new (GIMP_TYPE_CHANNEL,
                               "id", item_id,
                               NULL);
        }
      else if (gimp_item_id_is_path (item_id))
        {
          item = g_object_new (GIMP_TYPE_PATH,
                               "id", item_id,
                               NULL);
        }

      if (item)
        g_hash_table_insert (priv->items,
                             GINT_TO_POINTER (item_id),
                             item);
    }

  return item;
}

GimpDrawableFilter *
_gimp_plug_in_get_filter (GimpPlugIn *plug_in,
                          gint32      filter_id)
{
  GimpPlugInPrivate  *priv;
  GimpDrawableFilter *filter = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (G_UNLIKELY (! priv->filters))
    priv->filters =
      g_hash_table_new_full (g_direct_hash,
                             g_direct_equal,
                             NULL,
                             (GDestroyNotify) g_object_unref);

  filter = g_hash_table_lookup (priv->filters, GINT_TO_POINTER (filter_id));

  if (! filter)
    {
      filter = g_object_new (GIMP_TYPE_DRAWABLE_FILTER,
                             "id", filter_id,
                             NULL);

      g_hash_table_insert (priv->filters,
                           GINT_TO_POINTER (filter_id),
                           filter);
    }

  return filter;
}

GimpResource *
_gimp_plug_in_get_resource (GimpPlugIn *plug_in,
                            gint32      resource_id)
{
  GimpPlugInPrivate *priv;
  GimpResource      *resource = NULL;

  g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (G_UNLIKELY (! priv->resources))
    priv->resources =
      g_hash_table_new_full (g_direct_hash,
                             g_direct_equal,
                             NULL,
                             (GDestroyNotify) g_object_unref);

  resource = g_hash_table_lookup (priv->resources,
                                  GINT_TO_POINTER (resource_id));

  if (! resource)
    {
      if (gimp_resource_id_is_brush (resource_id))
        {
          resource = g_object_new (GIMP_TYPE_BRUSH,
                                   "id", resource_id,
                                   NULL);
        }
      else if (gimp_resource_id_is_pattern (resource_id))
        {
          resource = g_object_new (GIMP_TYPE_PATTERN,
                                   "id", resource_id,
                                   NULL);
        }
      else if (gimp_resource_id_is_gradient (resource_id))
        {
          resource = g_object_new (GIMP_TYPE_GRADIENT,
                                   "id", resource_id,
                                   NULL);
        }
      else if (gimp_resource_id_is_palette (resource_id))
        {
          resource = g_object_new (GIMP_TYPE_PALETTE,
                                   "id", resource_id,
                                   NULL);
        }
      else if (gimp_resource_id_is_font (resource_id))
        {
          resource = g_object_new (GIMP_TYPE_FONT,
                                   "id", resource_id,
                                   NULL);
        }

      if (resource)
        g_hash_table_insert (priv->resources,
                             GINT_TO_POINTER (resource_id),
                             resource);
    }

  return resource;
}

gboolean
_gimp_plug_in_manage_memory_manually (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;
  gboolean           manual_management = TRUE;

  priv = gimp_plug_in_get_instance_private (plug_in);

  if (priv->program_name)
    {
      GFile *file = g_file_new_for_path (priv->program_name);

      /* Limitations:
       * 1. Checking a file extension (and trusting argv[0] in general) is not
       *    the best idea. I thought about the possibility of passing through
       *    the name of the used interpreter from core to plug-in, but even this
       *    should not be trusted because it is configurable.
       * 2. An alternative could be to pass the information if a plug-in is
       *    interpreted (i.e. it has an interp file). Though I'm unsure if there
       *    might be some interpreted language out there with manual memory
       *    management.
       * 3. This list only contains the bindings we actually tested with (and
       *    have demo plug-ins for). Much more bindings are possible.
       *
       * So in the end, this test is just worth so far, but it's
       * acceptable for the officially supported bindings at least.
       */
      if (gimp_file_has_extension (file, ".lua") ||
          gimp_file_has_extension (file, ".py")  ||
          gimp_file_has_extension (file, ".js")  ||
          gimp_file_has_extension (file, ".gjs"))
        manual_management = FALSE;

      g_object_unref (file);
    }

  return manual_management;
}

static void
gimp_plug_in_destroy_hashes (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  g_clear_pointer (&priv->displays,  g_hash_table_unref);
  g_clear_pointer (&priv->images,    g_hash_table_unref);
  g_clear_pointer (&priv->items,     g_hash_table_unref);
  g_clear_pointer (&priv->resources, g_hash_table_unref);
}

/* Destroy proxies of all kinds.
 * This destroys with prejudice, i.e. destroy_all==TRUE.
 * All procedures, main and temporary, of the plugin must not be run subsequently,
 * especially to reference a proxy we are destroying.
 */
static void
gimp_plug_in_destroy_all_proxies (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv = gimp_plug_in_get_instance_private (plug_in);

  gimp_plug_in_destroy_proxies (plug_in, priv->displays,  "display",  TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->images,    "image",    TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->items,     "item",     TRUE);
  gimp_plug_in_destroy_proxies (plug_in, priv->resources, "resource", TRUE);
}

static void
gimp_plug_in_destroy_proxies (GimpPlugIn  *plug_in,
                              GHashTable  *hash_table,
                              const gchar *type,
                              gboolean     destroy_all)
{
  GimpPlugInPrivate *priv;
  GHashTableIter     iter;
  gpointer           key, value;

  if (! hash_table)
    return;

  priv = gimp_plug_in_get_instance_private (plug_in);

  g_hash_table_iter_init (&iter, hash_table);

  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      GObject *object = value;
      gint     ref_count = 0;

      for (GList *list = priv->ran_procedure_stack; list; list = list->next)
        ref_count += _gimp_procedure_get_ref_count (list->data, object);

      if (object->ref_count == 1 + ref_count * 2)
        {
          /* There may be additional references as argument defaults (we
           * multiply by 2, because there would be one copy of the spec
           * in the config type too).
           */
          g_hash_table_iter_remove (&iter);
        }
      else if (! G_IS_OBJECT (object) || object->ref_count < 1 + ref_count * 2)
        {
          /* this is debug code, a plug-in MUST NOT unref a proxy. To be nice,
           * we steal the object from the table, as removing it normally would
           * crash, since the object is not valid anymore.
           */
          g_printerr ("%s: ERROR: %s proxy was unrefed "
                      "by plug-in, it MUST NOT do that!\n",
                      G_STRFUNC, type);

          g_hash_table_iter_steal (&iter);
        }
      else if (destroy_all)
        {
          /* this is debug code, a plug-in MUST NOT ref a proxy */
          gint id;

          g_object_get (object, "id", &id, NULL);

          /* Some bindings are always keeping a reference to objects, in
           * particular this is the behavior of lua and gjs bindings where
           * object->ref_count == 2 at this point.
           * As for the Python binding, it is only true for global
           * variables (scoped variables are properly freed before the
           * plug-in ends), which anyway is not the best practice.
           * In any case, it's not something which the plug-in developer can do
           * anything about so this error is only confusing.
           * See #3912.
           */
          if (_gimp_plug_in_manage_memory_manually (plug_in))
            /* This only MIGHT be a programming error.
             * Because a plugin keeps temporary procedure instances,
             * which keep formal args with defaults that can be proxy objects,
             * and persistent plugins don't destroy their temporary procedures,
             * such proxy objects can have any refcount, often two.
             */
            g_debug ("%s: %s proxy with ID %d has refcount %d.",
                     G_STRFUNC, G_OBJECT_TYPE_NAME (object), id, object->ref_count);

#if 0
          /* The code used to do this, which is only meaningful when the bug is
           * really in the plug-in code. If it's one of the cases where the
           * reference is actually held by a bindings, it's unwise and will
           * likely end up in double-free crashes.
           */
          while (object->ref_count > 1)
            g_object_unref (object);
#endif
          g_hash_table_iter_remove (&iter);
        }
    }
}

static void
gimp_plug_in_init_i18n (GimpPlugIn *plug_in)
{
  GimpPlugInPrivate *priv;
  gchar             *rootdir      = g_path_get_dirname (gimp_get_progname ());
  GFile             *root_file    = g_file_new_for_path (rootdir);
  GFile             *catalog_file = NULL;

  g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));

  priv = gimp_plug_in_get_instance_private (plug_in);

  /* Default domain name is the program directory name. */
  g_free (priv->translation_domain_name);
  priv->translation_domain_name = g_path_get_basename (rootdir);

  /* Default catalog path is the locale/ directory under the root
   * directory.
   */
  catalog_file = g_file_resolve_relative_path (root_file, "locale");
  g_set_object (&priv->translation_domain_path, catalog_file);

  g_free (rootdir);
  g_object_unref (root_file);
  g_object_unref (catalog_file);
}
