/* LIBGIMP - The GIMP Library
 * Copyright (C) 1995-2003 Peter Mattis and Spencer Kimball
 *
 * gimpprocedureconfig.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 "gimp.h"
#include "gimpimagemetadata.h"

#include "gimpprocedureconfig-private.h"


/**
 * GimpProcedureConfig:
 *
 * The base class for [class@Procedure] specific config objects and the main
 * interface to manage aspects of [class@Procedure]'s arguments such as
 * persistency of the last used arguments across GIMP sessions.
 *
 * A procedure config is created by a [class@Procedure] using
 * [method@Procedure.create_config] and its properties match the
 * procedure's arguments and auxiliary arguments in number, order and
 * type.
 *
 * It implements the [struct@Config] interface and therefore has all its
 * serialization and deserialization features.
 *
 * Since: 3.0
 **/


enum
{
  PROP_0,
  PROP_PROCEDURE,
  N_PROPS
};


typedef struct _GimpProcedureConfigPrivate
{
  GimpProcedure         *procedure;

  GimpImage             *image;
  GimpRunMode            run_mode;

  GimpMetadata          *metadata;
  gchar                 *mime_type;
  GimpMetadataSaveFlags  metadata_flags;
  gboolean               metadata_saved;
} GimpProcedureConfigPrivate;

#define GET_PRIVATE(obj) ((GimpProcedureConfigPrivate *) gimp_procedure_config_get_instance_private ((GimpProcedureConfig *) (obj)))


static void       gimp_procedure_config_constructed   (GObject             *object);
static void       gimp_procedure_config_dispose       (GObject             *object);
static void       gimp_procedure_config_set_property  (GObject             *object,
                                                       guint                property_id,
                                                       const GValue        *value,
                                                       GParamSpec          *pspec);
static void       gimp_procedure_config_get_property  (GObject             *object,
                                                       guint                property_id,
                                                       GValue              *value,
                                                       GParamSpec          *pspec);


static void       gimp_procedure_config_set_values    (GimpProcedureConfig  *config,
                                                       const GimpValueArray *values);

static gboolean   gimp_procedure_config_load_last     (GimpProcedureConfig  *config,
                                                        GError              **error);
static gboolean   gimp_procedure_config_save_last     (GimpProcedureConfig  *config,
                                                       GError              **error);

static gboolean   gimp_procedure_config_load_parasite (GimpProcedureConfig  *config,
                                                       GimpImage            *image,
                                                       GError              **error);
static gboolean   gimp_procedure_config_save_parasite (GimpProcedureConfig  *config,
                                                       GimpImage            *image,
                                                       GError              **error);

static GFile    * gimp_procedure_config_get_file      (GimpProcedureConfig *config,
                                                       const gchar         *extension);
static gchar    * gimp_procedure_config_parasite_name (GimpProcedureConfig *config,
                                                       const gchar         *suffix);


G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GimpProcedureConfig, gimp_procedure_config,
                                     G_TYPE_OBJECT)

#define parent_class gimp_procedure_config_parent_class

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

static const struct
{
  const gchar           *name;
  GimpMetadataSaveFlags  flag;
}
metadata_properties[] =
{
  { "include-exif",          GIMP_METADATA_SAVE_EXIF          },
  { "include-xmp",           GIMP_METADATA_SAVE_XMP           },
  { "include-iptc",          GIMP_METADATA_SAVE_IPTC          },
  { "include-thumbnail",     GIMP_METADATA_SAVE_THUMBNAIL     },
  { "include-color-profile", GIMP_METADATA_SAVE_COLOR_PROFILE },
  { "include-comment",       GIMP_METADATA_SAVE_COMMENT       }
};


static void
gimp_procedure_config_class_init (GimpProcedureConfigClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed  = gimp_procedure_config_constructed;
  object_class->dispose      = gimp_procedure_config_dispose;
  object_class->set_property = gimp_procedure_config_set_property;
  object_class->get_property = gimp_procedure_config_get_property;

  props[PROP_PROCEDURE] =
    g_param_spec_object ("procedure",
                         "Procedure",
                         "The procedure this config object is used for",
                         GIMP_TYPE_PROCEDURE,
                         GIMP_PARAM_READWRITE |
                         G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, props);
}

static void
gimp_procedure_config_init (GimpProcedureConfig *config)
{
  GET_PRIVATE (config)->run_mode = -1;
}

static void
gimp_procedure_config_constructed (GObject *object)
{
  GimpProcedureConfig *config = GIMP_PROCEDURE_CONFIG (object);

  G_OBJECT_CLASS (parent_class)->constructed (object);

  g_assert (GIMP_IS_PROCEDURE (GET_PRIVATE (config)->procedure));
}

static void
gimp_procedure_config_dispose (GObject *object)
{
  GimpProcedureConfig        *config = GIMP_PROCEDURE_CONFIG (object);
  GimpProcedureConfigPrivate *priv = GET_PRIVATE (config);

  g_clear_object (&priv->procedure);
  g_clear_object (&priv->metadata);
  g_clear_pointer (&priv->mime_type, g_free);

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

static void
gimp_procedure_config_set_property (GObject      *object,
                                    guint         property_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  GimpProcedureConfig *config = GIMP_PROCEDURE_CONFIG (object);

  switch (property_id)
    {
    case PROP_PROCEDURE:
      GET_PRIVATE (config)->procedure = g_value_dup_object (value);
      break;

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

static void
gimp_procedure_config_get_property (GObject    *object,
                                    guint       property_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  GimpProcedureConfig *config = GIMP_PROCEDURE_CONFIG (object);

  switch (property_id)
    {
    case PROP_PROCEDURE:
      g_value_set_object (value, GET_PRIVATE (config)->procedure);
      break;

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


/*  public functions  */

/**
 * gimp_procedure_config_get_procedure:
 * @config: a procedure config
 *
 * This function returns the [class@Procedure] which created @config, see
 * [method@Procedure.create_config].
 *
 * Returns: (transfer none): The procedure which created this config.
 *
 * Since: 3.0
 **/
GimpProcedure *
gimp_procedure_config_get_procedure (GimpProcedureConfig *config)
{
  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), NULL);

  return GET_PRIVATE (config)->procedure;
}

static void
gimp_procedure_config_get_parasite (GimpProcedureConfig *config,
                                    GParamSpec          *pspec)
{
  GimpParasite *parasite;
  gchar        *value = NULL;

  /*  for now we only support strings  */
  if (! GET_PRIVATE (config)->image ||
      ! G_IS_PARAM_SPEC_STRING (pspec))
    return;

  parasite = gimp_image_get_parasite (GET_PRIVATE (config)->image,
                                      pspec->name);

  if (parasite)
    {
      guint32 parasite_size;

      value = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
      value = g_strndup (value, parasite_size);
      gimp_parasite_free (parasite);

      if (value && ! strlen (value))
        g_clear_pointer (&value, g_free);
    }

  if (! value)
    {
      /*  special case "gimp-comment" here, yes this is bad hack  */
      if (! strcmp (pspec->name, "gimp-comment"))
        {
          value = gimp_get_default_comment ();
        }
    }

  if (value && strlen (value))
    g_object_set (config,
                  pspec->name, value,
                  NULL);

  g_free (value);
}

static void
gimp_procedure_config_set_parasite (GimpProcedureConfig *config,
                                    GParamSpec          *pspec)
{
  GimpProcedureConfigPrivate *priv = GET_PRIVATE (config);
  GimpParasite *parasite;
  gchar        *value;

  /*  for now we only support strings  */
  if (! priv->image || ! G_IS_PARAM_SPEC_STRING (pspec))
    return;

  g_object_get (config,
                pspec->name, &value,
                NULL);

  parasite = gimp_image_get_parasite (priv->image, pspec->name);

  if (parasite)
    {
      /*  it there is a parasite, always override it if its value was
       *  changed
       */
      gchar   *image_value;
      guint32  parasite_size;

      image_value = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
      image_value = g_strndup (image_value, parasite_size);
      gimp_parasite_free (parasite);

      if (g_strcmp0 (value, image_value))
        {
          if (value && strlen (value))
            {
              parasite = gimp_parasite_new (pspec->name,
                                            GIMP_PARASITE_PERSISTENT,
                                            strlen (value) + 1,
                                            value);
              gimp_image_attach_parasite (priv->image, parasite);
              gimp_parasite_free (parasite);
            }
          else
            {
              gimp_image_detach_parasite (priv->image, pspec->name);
            }
        }

      g_free (image_value);
    }
  else
    {
      /*  otherwise, set the parasite if the value was changed from
       *  the default value
       */
      gchar *default_value = NULL;

      /*  special case "gimp-comment" here, yes this is bad hack  */
      if (! strcmp (pspec->name, "gimp-comment"))
        {
          default_value = gimp_get_default_comment ();
        }

      if (g_strcmp0 (value, default_value) &&
          value && strlen (value))
        {
          parasite = gimp_parasite_new (pspec->name,
                                        GIMP_PARASITE_PERSISTENT,
                                        strlen (value) + 1,
                                        value);
          gimp_image_attach_parasite (priv->image, parasite);
          gimp_parasite_free (parasite);
        }

      g_free (default_value);
    }

  g_free (value);
}

/**
 * gimp_procedure_config_save_metadata:
 * @config:         a #GimpProcedureConfig
 * @exported_image: the image that was actually exported
 * @file:           the file @exported_image was written to
 *
 * *Note: There is normally no need to call this function because it's
 * already called by [class@ExportProcedure] after the `run()` callback.*
 *
 * *Only use this function if the [class@Metadata] passed as argument of a
 * [class@ExportProcedure]'s run() method needs to be written at a specific
 * point of the export, other than its end.*
 *
 * This function syncs back @config's export properties to the
 * metadata's [flags@MetadataSaveFlags] and writes the metadata to
 * @file.
 *
 * The metadata is only ever written once. If this function has been
 * called explicitly, it will do nothing when called a second time at the end of
 * the `run()` callback.
 *
 * Since: 3.0
 **/
void
gimp_procedure_config_save_metadata (GimpProcedureConfig *config,
                                     GimpImage           *exported_image,
                                     GFile               *file)
{
  GimpProcedureConfigPrivate *priv = GET_PRIVATE (config);

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (GIMP_IS_IMAGE (exported_image));
  g_return_if_fail (G_IS_FILE (file));

  if (priv->metadata && ! priv->metadata_saved)
    {
      GObjectClass *object_class = G_OBJECT_GET_CLASS (config);
      GError       *error        = NULL;
      gint          i;

      for (i = 0; i < G_N_ELEMENTS (metadata_properties); i++)
        {
          const gchar           *prop_name = metadata_properties[i].name;
          GimpMetadataSaveFlags  prop_flag = metadata_properties[i].flag;
          GParamSpec            *pspec;
          gboolean               value;

          pspec = g_object_class_find_property (object_class, prop_name);
          if (pspec)
            {
              g_object_get (config,
                            prop_name, &value,
                            NULL);

              if (value)
                priv->metadata_flags |= prop_flag;
              else
                priv->metadata_flags &= ~prop_flag;
            }
          else
            {
              priv->metadata_flags &= ~prop_flag;
            }
        }

      if (! _gimp_image_metadata_save_finish (exported_image,
                                              priv->mime_type,
                                              priv->metadata,
                                              priv->metadata_flags,
                                              file, &error))
        {
          if (error)
            {
              /* Even though a failure to write metadata is not enough
                 reason to say we failed to save the image, we should
                 still notify the user about the problem. */
              g_message ("%s: saving metadata failed: %s",
                         G_STRFUNC, error->message);
              g_error_free (error);
            }
        }

      priv->metadata_saved = TRUE;
    }
}

/**
 * gimp_procedure_config_get_core_object_array:
 * @config:        a #GimpProcedureConfig
 * @property_name: the name of a #GimpParamSpecCoreObjectArray param spec.
 *
 * A function for bindings to get a [type@CoreObjectArray] property. Getting
 * these with [method@GObject.Object.get] or [method@GObject.Object.get_property] won't
 * [work for the time being](https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/492)
 * so all our boxed array types must be set and get using alternative
 * functions instead.
 *
 * C plug-ins should just use [method@GObject.Object.get].
 *
 * Returns: (array zero-terminated) (transfer container): an array of #GObjects.
 *
 * Since: 3.0
 **/
GObject **
gimp_procedure_config_get_core_object_array (GimpProcedureConfig  *config,
                                             const gchar          *property_name)
{
  GObject    **objects = NULL;
  GParamSpec  *param_spec;

  param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config), property_name);

  if (! param_spec)
    {
      g_warning ("%s: %s has no property named '%s'",
                 G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (config)),
                 property_name);
      return objects;
    }

  if (! g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), GIMP_TYPE_PARAM_CORE_OBJECT_ARRAY))
    {
      g_warning ("%s: property '%s' of %s is not a GimpParamCoreObjectArray.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (param_spec->owner_type));
      return objects;
    }

  g_object_get (config,
                property_name, &objects,
                NULL);

  return objects;
}

/**
 * gimp_procedure_config_set_core_object_array:
 * @config:        a #GimpProcedureConfig
 * @property_name: the name of a #GimpParamSpecCoreObjectArray param spec.
 * @objects: (array length=n_objects) (transfer none): an array of #GObjects.
 * @n_objects: the numbers of @objects.
 *
 * A function for bindings to set a [type@CoreObjectArray] property. Setting
 * these with [method@GObject.Object.set] or [method@GObject.Object.set_property] won't
 * [work for the time being](https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/492)
 * so all our boxed array types must be set and get using alternative
 * functions instead.
 *
 * C plug-ins should just use [method@GObject.Object.set].
 *
 * Since: 3.0
 **/
void
gimp_procedure_config_set_core_object_array (GimpProcedureConfig  *config,
                                             const gchar          *property_name,
                                             GObject             **objects,
                                             gsize                 n_objects)
{
  GObject    **null_objects;
  GParamSpec  *param_spec;

  param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                             property_name);

  if (! param_spec)
    {
      g_warning ("%s: %s has no property named '%s'",
                 G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (config)),
                 property_name);
      return;
    }

  if (! g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), GIMP_TYPE_PARAM_CORE_OBJECT_ARRAY))
    {
      g_warning ("%s: property '%s' of %s is not a GimpParamCoreObjectArray.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (param_spec->owner_type));
      return;
    }

  null_objects = g_new0 (GObject *, n_objects + 1);

  for (gint i = 0; i < n_objects; i++)
    null_objects[i] = objects[i];

  g_object_set (config,
                property_name, null_objects,
                NULL);

  g_free (null_objects);
}

/**
 * gimp_procedure_config_get_color_array:
 * @config:        a #GimpProcedureConfig
 * @property_name: the name of a #GParamSpecBoxed param spec with [type@ColorArray] value type.
 *
 * A function for bindings to get a [type@ColorArray] property. Getting
 * these with [method@GObject.Object.get] or [method@GObject.Object.get_property] won't
 * [work for the time being](https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/492)
 * so all our boxed array types must be set and get using these
 * alternative functions instead.
 *
 * C plug-ins should just use [method@GObject.Object.get].
 *
 * Returns: (array zero-terminated) (transfer full): an array of #GObjects.
 *
 * Since: 3.0
 **/
GeglColor **
gimp_procedure_config_get_color_array (GimpProcedureConfig  *config,
                                       const gchar          *property_name)
{
  GeglColor  **colors = NULL;
  GParamSpec  *param_spec;

  param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config), property_name);

  if (! param_spec)
    {
      g_warning ("%s: %s has no property named '%s'",
                 G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (config)),
                 property_name);
      return colors;
    }

  if (! g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_BOXED) ||
      ! g_type_is_a (param_spec->value_type, GIMP_TYPE_COLOR_ARRAY))
    {
      g_warning ("%s: property '%s' of %s is not a GimpColorArray boxed type.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (param_spec->owner_type));
      return colors;
    }

  g_object_get (config,
                property_name, &colors,
                NULL);

  return colors;
}

/**
 * gimp_procedure_config_set_color_array:
 * @config:        a #GimpProcedureConfig
 * @property_name: the name of a #GParamSpecBoxed param spec with [type@ColorArray] value type.
 * @colors: (array length=n_colors) (transfer none): an array of [class@Gegl.Color].
 * @n_colors: the numbers of @colors.
 *
 * A function for bindings to set a [type@ColorArray] property. Setting
 * these with [method@GObject.Object.set] or [method@GObject.Object.set_property] won't
 * [work for the time being](https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/492)
 * so all our boxed array types must be set and get using these
 * alternative functions instead.
 *
 * C plug-ins should just use [method@GObject.Object.set].
 *
 * Since: 3.0
 **/
void
gimp_procedure_config_set_color_array (GimpProcedureConfig  *config,
                                       const gchar          *property_name,
                                       GeglColor           **colors,
                                       gsize                 n_colors)
{
  GeglColor  **null_colors;
  GParamSpec  *param_spec;

  param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config), property_name);

  if (! param_spec)
    {
      g_warning ("%s: %s has no property named '%s'",
                 G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (config)),
                 property_name);
      return;
    }

  if (! g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_BOXED) ||
      ! g_type_is_a (param_spec->value_type, GIMP_TYPE_COLOR_ARRAY))
    {
      g_warning ("%s: property '%s' of %s is not a GimpColorArray boxed type.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (param_spec->owner_type));
      return;
    }

  null_colors = g_new0 (GeglColor *, n_colors + 1);

  for (gint i = 0; i < n_colors; i++)
    null_colors[i] = colors[i];

  g_object_set (config,
                property_name, null_colors,
                NULL);

  g_free (null_colors);
}

/**
 * gimp_procedure_config_get_choice_id:
 * @config:        a #GimpProcedureConfig
 * @property_name: the name of a #GimpParamSpecChoice property.
 *
 * A utility function which will get the current string value of a
 * #GimpParamSpecChoice property in @config and convert it to the
 * integer ID mapped to this value.
 * This makes it easy to work with an Enum type locally, within a plug-in code.
 *
 * Since: 3.0
 **/
gint
gimp_procedure_config_get_choice_id (GimpProcedureConfig *config,
                                     const gchar         *property_name)
{
  GParamSpec *param_spec;
  GimpChoice *choice;
  gchar      *value = NULL;
  gint        id;

  param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                             property_name);

  if (! param_spec)
    {
      g_warning ("%s: %s has no property named '%s'",
                 G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (config)),
                 property_name);
      return 0;
    }

  if (! g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), GIMP_TYPE_PARAM_CHOICE))
    {
      g_warning ("%s: property '%s' of %s is a %s, not a GimpParamSpecChoice.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (G_TYPE_FROM_INSTANCE (param_spec)),
                 g_type_name (param_spec->owner_type));
      return 0;
    }

  choice = gimp_param_spec_choice_get_choice (param_spec);
  g_object_get (config,
                property_name, &value,
                NULL);
  id = gimp_choice_get_id (choice, value);

  g_free (value);

  return id;
}


/*  Functions only used by GimpProcedure classes  */

/**
 * _gimp_procedure_config_get_values:
 * @config: a #GimpProcedureConfig
 * @values: a #GimpValueArray
 *
 * Gets the values from @config's properties and stores them in
 * @values.
 *
 * See [method@ProcedureConfig.set_values].
 *
 * Since: 3.0
 **/
void
_gimp_procedure_config_get_values (GimpProcedureConfig  *config,
                                   GimpValueArray       *values)
{
  GParamSpec **pspecs;
  guint        n_pspecs;
  gint         n_aux_args;
  gint         n_values;
  gint         i;

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (values != NULL);

  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                           &n_pspecs);
  gimp_procedure_get_aux_arguments (GET_PRIVATE (config)->procedure, &n_aux_args);
  n_values = gimp_value_array_length (values);

  /* The config will have 1 additional property: "procedure". */
  g_return_if_fail (n_pspecs == n_values + n_aux_args + 1);

  for (i = 0; i < n_values; i++)
    {
      GParamSpec *pspec = pspecs[i + 1];
      GValue     *value = gimp_value_array_index (values, i);

      g_object_get_property (G_OBJECT (config), pspec->name, value);
    }

  g_free (pspecs);
}

/**
 * _gimp_procedure_config_begin_run:
 * @config:   a #GimpProcedureConfig
 * @image: (nullable): a #GimpImage or %NULL
 * @run_mode: the #GimpRunMode passed to a [class@Procedure]'s run()
 * @args:     the #GimpValueArray passed to a [class@Procedure]'s run()
 * @editable_properties: whether you may tweak the properties for
 *                       metadata export.
 *
 * Populates @config with values for a [class@Procedure]'s run(),
 * depending on @run_mode.
 *
 * If @run_mode is %GIMP_RUN_INTERACTIVE or %GIMP_RUN_WITH_LAST_VALS,
 * the saved values from the procedure's last run() are loaded and set
 * on @config. If @image is not %NULL, the last used values for this
 * image are tried first, and if no image-specific values are found
 * the globally saved last used values are used. If no saved last used
 * values are found, the procedure's default argument values are used.
 *
 * If @run_mode is %GIMP_RUN_NONINTERACTIVE, the contents of @args are
 * set on @config using [method@ProcedureConfig.set_values].
 *
 * After setting @config's properties like described above, arguments
 * and auxiliary arguments are automatically synced from image
 * parasites of the same name if they were set to
 * %GIMP_ARGUMENT_SYNC_PARASITE with [class@Procedure.set_argument_sync]:
 *
 * String properties are set to the value of the image parasite if
 * @run_mode is %GIMP_RUN_INTERACTIVE or %GIMP_RUN_WITH_LAST_VALS, or
 * if the corresponding argument is an auxiliary argument. As a
 * special case, a property named "gimp-comment" will default to
 * [func@get_default_comment] if there is no "gimp-comment" parasite.
 *
 * After calling this function, the @args passed to run() should be
 * left alone and @config be treated as the procedure's arguments, with
 * some exception, such as metadata export properties (which may be
 * overwritten for safety measure), and only if @editable_properties is
 * returned as %TRUE, which should only happen in interactive @run_mode
 * when this is the first run with @image in particular (i.e. there is
 * no config parasite on @image stored for this procedure). In
 * particular, in non-interactive, we will use exactly @args; and in
 * last-vals run, or if a config parasite was found on image, we will
 * use exactly the last-run arguments.
 *
 * It is possible to get @config's resulting values back into @args by
 * calling [method@ProcedureConfig.get_values], as long as modified
 * @args are written back to @config using [method@ProcedureConfig.set_values]
 * before the call to [method@ProcedureConfig.end_run].
 *
 * This function should be used at the beginning of a procedure's
 * run() and be paired with a call to [method@ProcedureConfig.end_run]
 * at the end of run().
 *
 * Since: 3.0
 **/
void
_gimp_procedure_config_begin_run (GimpProcedureConfig  *config,
                                  GimpImage            *image,
                                  GimpRunMode           run_mode,
                                  const GimpValueArray *args,
                                  gboolean             *editable_properties)
{
  GimpProcedureConfigPrivate  *priv;
  GParamSpec                  *run_mode_pspec;
  GParamSpec                 **pspecs;
  guint                        n_pspecs;
  gint                         i;
  gboolean                     loaded = FALSE;
  GError                      *error  = NULL;

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (image == NULL || GIMP_IS_IMAGE (image));
  g_return_if_fail (args != NULL);

  priv = GET_PRIVATE (config);

  priv->image    = image;
  priv->run_mode = run_mode;

  if (editable_properties)
    *editable_properties = FALSE;

  gimp_procedure_config_set_values (config, args);

  switch (run_mode)
    {
    case GIMP_RUN_INTERACTIVE :
    case GIMP_RUN_WITH_LAST_VALS:
      if (image)
        {
          loaded = gimp_procedure_config_load_parasite (config, image,
                                                        &error);
          if (! loaded && error)
            {
              g_printerr ("Loading last used values from parasite failed: %s\n",
                          error->message);
              g_clear_error (&error);
            }
        }

      if (! loaded)
        {
          if (! gimp_procedure_config_load_last (config, &error) && error)
            {
              g_printerr ("Loading last used values from disk failed: %s\n",
                          error->message);
              g_clear_error (&error);

              if (editable_properties)
                *editable_properties = TRUE;
            }

          if (run_mode == GIMP_RUN_INTERACTIVE && editable_properties)
            *editable_properties = TRUE;
        }
      break;

    case GIMP_RUN_NONINTERACTIVE:
      break;
    }

  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                           &n_pspecs);

  for (i = 0; i < n_pspecs; i++)
    {
      GParamSpec *pspec = pspecs[i];

      /*  skip our own properties  */
      if (pspec->owner_type == GIMP_TYPE_PROCEDURE_CONFIG)
        continue;

      switch (gimp_procedure_get_argument_sync (priv->procedure, pspec->name))
        {
        case GIMP_ARGUMENT_SYNC_PARASITE:
          /*  we sync the property from the image parasite if it is an
           *  aux argument, or if we run interactively, because the
           *  parasite should be global to the image and not depend on
           *  whatever parasite another image had when last using this
           *  procedure
           */
          if (gimp_procedure_find_aux_argument (priv->procedure,
                                                pspec->name) ||
              (run_mode != GIMP_RUN_NONINTERACTIVE))
            {
              gimp_procedure_config_get_parasite (config, pspec);
            }
          break;

        default:
          break;
        }
    }

  /* Always set the run-mode in the end, which should be influenced neither by
   * the default property value, nor last run's value.
   */
  run_mode_pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config), "run-mode");
  if (run_mode_pspec != NULL && G_IS_PARAM_SPEC_ENUM (run_mode_pspec))
    g_object_set (config, "run-mode", run_mode, NULL);

  g_free (pspecs);
}

/**
 * _gimp_procedure_config_end_run:
 * @config: a #GimpProcedureConfig
 * @status: the return status of the [class@Procedure]'s run()
 *
 * This function is the counterpart of
 * [method@ProcedureConfig.begin_run] and must always be called in
 * pairs in a procedure's run(), before returning return values.
 *
 * If the @run_mode passed to [method@ProcedureConfig.end_run] was
 * %GIMP_RUN_INTERACTIVE, @config is saved as last used values to be
 * used when the procedure runs again. Additionally, if the #GimpImage
 * passed to [method@ProcedureConfig.begin_run] was not %NULL, @config is
 * attached to @image as last used values for this image using a
 * #GimpParasite and [method@Image.attach_parasite].
 *
 * If @run_mode was not %GIMP_RUN_NONINTERACTIVE, this function also
 * conveniently calls [func@displays_flush], which is what most
 * procedures want and doesn't do any harm if called redundantly.
 *
 * After a %GIMP_RUN_INTERACTIVE run, %GIMP_ARGUMENT_SYNC_PARASITE
 * values that have been changed are written back to their
 * corresponding image parasite.
 *
 * See [method@ProcedureConfig.begin_run].
 *
 * Since: 3.0
 **/
void
_gimp_procedure_config_end_run (GimpProcedureConfig *config,
                                GimpPDBStatusType    status)
{
  GimpProcedureConfigPrivate *priv;

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));

  priv = GET_PRIVATE (config);

  if (priv->run_mode != GIMP_RUN_NONINTERACTIVE)
    gimp_displays_flush ();

  if (status == GIMP_PDB_SUCCESS &&
      priv->run_mode == GIMP_RUN_INTERACTIVE)
    {
      GParamSpec **pspecs;
      guint        n_pspecs;
      gint         i;
      GError      *error = NULL;

      if (priv->image)
        gimp_procedure_config_save_parasite (config, priv->image,
                                             NULL);

      if (! gimp_procedure_config_save_last (config, &error))
        {
          g_printerr ("Saving last used values to disk failed: %s\n",
                      error->message);
          g_clear_error (&error);
        }

      pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                               &n_pspecs);

      for (i = 0; i < n_pspecs; i++)
        {
          GParamSpec *pspec = pspecs[i];

          /*  skip our own properties  */
          if (pspec->owner_type == GIMP_TYPE_PROCEDURE_CONFIG)
            continue;

          switch (gimp_procedure_get_argument_sync (priv->procedure,
                                                    pspec->name))
            {
            case GIMP_ARGUMENT_SYNC_PARASITE:
              gimp_procedure_config_set_parasite (config, pspec);
              break;

            default:
              break;
            }
        }

      g_free (pspecs);
    }

  priv->image    = NULL;
  priv->run_mode = -1;
}

/**
 * _gimp_procedure_config_begin_export:
 * @config:         a #GimpProcedureConfig
 * @original_image: the image passed to run()
 * @run_mode:       the #GimpRunMode passed to a [class@Procedure]'s run()
 * @args:           the value array passed to a [class@Procedure]'s run()
 * @mime_type: (nullable):  exported file format's mime type, or %NULL.
 *
 * This is a variant of [method@ProcedureConfig.begin_run] to be used
 * by file export procedures using [class@ExportProcedure]. It must be
 * paired with a call to [method@ProcedureConfig.end_export] at the end
 * of run().
 *
 * It does everything [method@ProcedureConfig.begin_run] does but
 * provides additional features to automate file export:
 *
 * If @mime_type is non-%NULL, exporting metadata is handled
 * automatically, by calling [method@Image.metadata_save_prepare] and
 * syncing its returned [flags@MetadataSaveFlags] with @config's
 * properties. (The corresponding [method@Image.metadata_save_finish]
 * will be called by [method@ProcedureConfig.end_export]).
 *
 * The following boolean arguments of the used [class@ExportProcedure] are
 * synced. The procedure can but must not provide these arguments.
 *
 * - "include-exif" for %GIMP_METADATA_SAVE_EXIF.
 * - "include-xmp" for %GIMP_METADATA_SAVE_XMP.
 * - "include-iptc" for %GIMP_METADATA_SAVE_IPTC.
 * - "include-thumbnail" for %GIMP_METADATA_SAVE_THUMBNAIL.
 * - "include-color-profile" for %GIMP_METADATA_SAVE_COLOR_PROFILE.
 * - "include-comment" for %GIMP_METADATA_SAVE_COMMENT.
 *
 * The values from the [flags@MetadataSaveFlags] will only ever be used
 * to set these properties to %FALSE, overriding the user's saved
 * default values for the procedure, but NOT overriding the last used
 * values from exporting @original_image or the last used values from
 * exporting any other image using this procedure.
 *
 * If @mime_type is %NULL, [class@Metadata] handling is skipped. The
 * procedure can still have all of the above listed boolean arguments,
 * but must take care of their default values itself. The easiest way
 * to do this is by simply using [func@export_comment], [func@export_exif] etc.
 * as default values for these arguments when adding them using
 * gimp_procedure_add_boolean_argument() or
 * gimp_procedure_add_boolean_aux_argument().
 *
 * Returns: (transfer none) (nullable): The metadata to be used
 *          for this export, or %NULL if @original_image doesn't have
 *          metadata.
 *
 * Since: 3.0
 **/
GimpMetadata *
_gimp_procedure_config_begin_export (GimpProcedureConfig  *config,
                                     GimpImage            *original_image,
                                     GimpRunMode           run_mode,
                                     const GimpValueArray *args,
                                     const gchar          *mime_type)
{
  GimpProcedureConfigPrivate *priv;
  GObjectClass               *object_class;
  gboolean                    editable_properties;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), NULL);
  g_return_val_if_fail (GIMP_IS_IMAGE (original_image), NULL);
  g_return_val_if_fail (args != NULL, NULL);

  priv = GET_PRIVATE (config);

  object_class = G_OBJECT_GET_CLASS (config);

  /* This must be run before we process the metadata, because we may
   * overwrite various metadata export properties when they are globally
   * disabled in Preferences, for security reasons.
   */
  _gimp_procedure_config_begin_run (config, original_image, run_mode, args,
                                    &editable_properties);

  if (mime_type)
    {
      GimpMetadataSaveFlags metadata_flags;
      gint                  i;

      priv->metadata =
        gimp_image_metadata_save_prepare (original_image,
                                          mime_type,
                                          &metadata_flags);

      if (priv->metadata)
        {
          priv->mime_type      = g_strdup (mime_type);
          priv->metadata_flags = metadata_flags;
        }

      if (editable_properties)
        for (i = 0; i < G_N_ELEMENTS (metadata_properties); i++)
          {
            /*  we only disable properties based on metadata flags here
             *  and never enable them, so we don't override the user's
             *  saved default values that are passed to us via "args"
             */
            if (! (metadata_flags & metadata_properties[i].flag))
              {
                const gchar *prop_name = metadata_properties[i].name;
                GParamSpec  *pspec;

                pspec = g_object_class_find_property (object_class, prop_name);
                if (pspec)
                  g_object_set (config,
                                prop_name, FALSE,
                                NULL);
              }
          }
    }

  return priv->metadata;
}

/**
 * _gimp_procedure_config_end_export:
 * @config:         a #GimpProcedureConfig
 * @exported_image: the image that was actually exported
 * @file:           the #GFile @exported_image was written to
 * @status:         the return status of the [class@Procedure]'s run()
 *
 * This is a variant of [method@ProcedureConfig.end_run] to be used by
 * file export procedures using [class@ExportProcedure]. It must be paired
 * with a call to [method@ProcedureConfig.begin_export] at the
 * beginning of run().
 *
 * It does everything [method@ProcedureConfig.begin_run] does but
 * provides additional features to automate file export:
 *
 * If @status is %GIMP_PDB_SUCCESS, and
 * [method@ProcedureConfig.begin_export] returned a [class@Metadata], this
 * function calls [method@ProcedureConfig.save_metadata], which syncs
 * back @config's export properties to the metadata's
 * [flags@MetadataSaveFlags] and writes metadata to @file using
 * [method@Image.metadata_save_finish].
 *
 * Since: 3.0
 **/
void
_gimp_procedure_config_end_export (GimpProcedureConfig *config,
                                   GimpImage           *exported_image,
                                   GFile               *file,
                                   GimpPDBStatusType    status)
{
  GimpProcedureConfigPrivate *priv;

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (GIMP_IS_IMAGE (exported_image));
  g_return_if_fail (G_IS_FILE (file));

  priv = GET_PRIVATE (config);

  if (status == GIMP_PDB_SUCCESS)
    {
      gimp_procedure_config_save_metadata (config, exported_image, file);
    }

  g_clear_object (&priv->metadata);
  g_clear_pointer (&priv->mime_type, g_free);
  priv->metadata_flags = 0;
  priv->metadata_saved = FALSE;

  _gimp_procedure_config_end_run (config, status);
}

gboolean
gimp_procedure_config_has_default (GimpProcedureConfig *config)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);

  file = gimp_procedure_config_get_file (config, ".default");

  success = g_file_query_exists (file, NULL);
  g_object_unref (file);

  return success;
}

gboolean
gimp_procedure_config_load_default (GimpProcedureConfig  *config,
                                    GError              **error)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  file = gimp_procedure_config_get_file (config, ".default");

  success = gimp_config_deserialize_file (GIMP_CONFIG (config),
                                          file,
                                          NULL, error);

  if (! success && error && (*error)->code == GIMP_CONFIG_ERROR_OPEN_ENOENT)
    {
      g_clear_error (error);
    }

  g_object_unref (file);

  return success;
}

gboolean
gimp_procedure_config_save_default (GimpProcedureConfig  *config,
                                    GError              **error)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  file = gimp_procedure_config_get_file (config, ".default");

  success = gimp_config_serialize_to_file (GIMP_CONFIG (config),
                                           file,
                                           "settings",
                                           "end of settings",
                                           NULL, error);

  g_object_unref (file);

  return success;
}


/*  private functions  */

/**
 * gimp_procedure_config_set_values:
 * @config: a #GimpProcedureConfig
 * @values: a #GimpValueArray
 *
 * Sets the values from @values on @config's properties.
 *
 * The number, order and types of values in @values must match the
 * number, order and types of @config's properties.
 *
 * This function is meant to be used on @values which are passed as
 * arguments to the run() function of the [class@Procedure] which created
 * this @config. See [method@Procedure.create_config].
 *
 * Since: 3.0
 **/
static void
gimp_procedure_config_set_values (GimpProcedureConfig  *config,
                                  const GimpValueArray *values)
{
  GParamSpec **pspecs;
  guint        n_pspecs;
  gint         n_aux_args;
  gint         n_values;
  gint         i;

  g_return_if_fail (GIMP_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (values != NULL);

  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                           &n_pspecs);
  gimp_procedure_get_aux_arguments (GET_PRIVATE (config)->procedure,
                                    &n_aux_args);
  n_values = gimp_value_array_length (values);

  /* The first property is the procedure, all others are arguments. */
  g_return_if_fail (n_pspecs == n_values + n_aux_args + 1);

  for (i = 0; i < n_values; i++)
    {
      GParamSpec *pspec = pspecs[i + 1];
      GValue     *value = gimp_value_array_index (values, i);

      g_object_set_property (G_OBJECT (config), pspec->name, value);
    }

  g_free (pspecs);
}

static gboolean
gimp_procedure_config_load_last (GimpProcedureConfig  *config,
                                 GError              **error)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  file = gimp_procedure_config_get_file (config, ".last");

  success = gimp_config_deserialize_file (GIMP_CONFIG (config),
                                          file,
                                          NULL, error);

  if (! success && (*error)->code == GIMP_CONFIG_ERROR_OPEN_ENOENT)
    {
      g_clear_error (error);
    }

  g_object_unref (file);

  return success;
}

static gboolean
gimp_procedure_config_save_last (GimpProcedureConfig  *config,
                                 GError              **error)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  file = gimp_procedure_config_get_file (config, ".last");

  success = gimp_config_serialize_to_file (GIMP_CONFIG (config),
                                           file,
                                           "settings",
                                           "end of settings",
                                           NULL, error);

  g_object_unref (file);

  return success;
}

static gboolean
gimp_procedure_config_load_parasite (GimpProcedureConfig  *config,
                                     GimpImage            *image,
                                     GError              **error)
{
  gchar        *name;
  GimpParasite *parasite;
  gboolean      success;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  name = gimp_procedure_config_parasite_name (config, "-last");
  parasite = gimp_image_get_parasite (image, name);
  g_free (name);

  if (! parasite)
    return FALSE;

  success = gimp_config_deserialize_parasite (GIMP_CONFIG (config),
                                              parasite,
                                              NULL, error);
  gimp_parasite_free (parasite);

  return success;
}

static gboolean
gimp_procedure_config_save_parasite (GimpProcedureConfig  *config,
                                     GimpImage            *image,
                                     GError              **error)
{
  gchar        *name;
  GimpParasite *parasite;

  g_return_val_if_fail (GIMP_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  name = gimp_procedure_config_parasite_name (config, "-last");
  parasite = gimp_config_serialize_to_parasite (GIMP_CONFIG (config),
                                                name,
                                                GIMP_PARASITE_PERSISTENT,
                                                NULL);
  g_free (name);

  if (! parasite)
    return FALSE;

  gimp_image_attach_parasite (image, parasite);
  gimp_parasite_free (parasite);

  return TRUE;
}

static GFile *
gimp_procedure_config_get_file (GimpProcedureConfig *config,
                                const gchar         *extension)
{
  GFile *file;
  gchar *basename;

  basename = g_strconcat (G_OBJECT_TYPE_NAME (config), extension, NULL);
  file = gimp_directory_file ("plug-in-settings", basename, NULL);
  g_free (basename);

  return file;
}

static gchar *
gimp_procedure_config_parasite_name (GimpProcedureConfig *config,
                                     const gchar         *suffix)
{
  return g_strconcat (G_OBJECT_TYPE_NAME (config), suffix, NULL);
}
