/* LIBPIKA - The PIKA Library
 * Copyright (C) 1995-2003 Peter Mattis and Spencer Kimball
 *
 * pikaprocedureconfig.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 "pika.h"
#include "pikaimagemetadata.h"

#include "pikaprocedureconfig-private.h"


/**
 * PikaProcedureConfig:
 *
 * 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 PIKA 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
};


struct _PikaProcedureConfigPrivate
{
  PikaProcedure         *procedure;

  PikaImage             *image;
  PikaRunMode            run_mode;

  PikaMetadata          *metadata;
  gchar                 *mime_type;
  PikaMetadataSaveFlags  metadata_flags;
  gboolean               metadata_saved;
};


static void       pika_procedure_config_constructed   (GObject             *object);
static void       pika_procedure_config_dispose       (GObject             *object);
static void       pika_procedure_config_set_property  (GObject             *object,
                                                       guint                property_id,
                                                       const GValue        *value,
                                                       GParamSpec          *pspec);
static void       pika_procedure_config_get_property  (GObject             *object,
                                                       guint                property_id,
                                                       GValue              *value,
                                                       GParamSpec          *pspec);


static void       pika_procedure_config_set_values    (PikaProcedureConfig  *config,
                                                       const PikaValueArray *values);

static gboolean   pika_procedure_config_load_last     (PikaProcedureConfig  *config,
                                                        GError              **error);
static gboolean   pika_procedure_config_save_last     (PikaProcedureConfig  *config,
                                                       GError              **error);

static gboolean   pika_procedure_config_load_parasite (PikaProcedureConfig  *config,
                                                       PikaImage            *image,
                                                       GError              **error);
static gboolean   pika_procedure_config_save_parasite (PikaProcedureConfig  *config,
                                                       PikaImage            *image,
                                                       GError              **error);

static GFile    * pika_procedure_config_get_file      (PikaProcedureConfig *config,
                                                       const gchar         *extension);
static gchar    * pika_procedure_config_parasite_name (PikaProcedureConfig *config,
                                                       const gchar         *suffix);


G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (PikaProcedureConfig, pika_procedure_config,
                                     G_TYPE_OBJECT)

#define parent_class pika_procedure_config_parent_class

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

static const struct
{
  const gchar           *name;
  PikaMetadataSaveFlags  flag;
}
metadata_properties[] =
{
  { "save-exif",          PIKA_METADATA_SAVE_EXIF          },
  { "save-xmp",           PIKA_METADATA_SAVE_XMP           },
  { "save-iptc",          PIKA_METADATA_SAVE_IPTC          },
  { "save-thumbnail",     PIKA_METADATA_SAVE_THUMBNAIL     },
  { "save-color-profile", PIKA_METADATA_SAVE_COLOR_PROFILE },
  { "save-comment",       PIKA_METADATA_SAVE_COMMENT       }
};


static void
pika_procedure_config_class_init (PikaProcedureConfigClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed  = pika_procedure_config_constructed;
  object_class->dispose      = pika_procedure_config_dispose;
  object_class->set_property = pika_procedure_config_set_property;
  object_class->get_property = pika_procedure_config_get_property;

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

  g_object_class_install_properties (object_class, N_PROPS, props);
}

static void
pika_procedure_config_init (PikaProcedureConfig *config)
{
  config->priv = pika_procedure_config_get_instance_private (config);

  config->priv->run_mode = -1;
}

static void
pika_procedure_config_constructed (GObject *object)
{
  PikaProcedureConfig *config = PIKA_PROCEDURE_CONFIG (object);

  G_OBJECT_CLASS (parent_class)->constructed (object);

  g_assert (PIKA_IS_PROCEDURE (config->priv->procedure));
}

static void
pika_procedure_config_dispose (GObject *object)
{
  PikaProcedureConfig *config = PIKA_PROCEDURE_CONFIG (object);

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

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

static void
pika_procedure_config_set_property (GObject      *object,
                                    guint         property_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  PikaProcedureConfig *config = PIKA_PROCEDURE_CONFIG (object);

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

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

static void
pika_procedure_config_get_property (GObject    *object,
                                    guint       property_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  PikaProcedureConfig *config = PIKA_PROCEDURE_CONFIG (object);

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

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


/*  public functions  */

/**
 * pika_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
 **/
PikaProcedure *
pika_procedure_config_get_procedure (PikaProcedureConfig *config)
{
  g_return_val_if_fail (PIKA_IS_PROCEDURE_CONFIG (config), NULL);

  return config->priv->procedure;
}

static void
pika_procedure_config_get_parasite (PikaProcedureConfig *config,
                                    GParamSpec          *pspec)
{
  PikaParasite *parasite;
  gchar        *value = NULL;

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

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

  if (parasite)
    {
      guint32 parasite_size;

      value = (gchar *) pika_parasite_get_data (parasite, &parasite_size);
      value = g_strndup (value, parasite_size);
      pika_parasite_free (parasite);

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

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

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

  g_free (value);
}

static void
pika_procedure_config_set_parasite (PikaProcedureConfig *config,
                                    GParamSpec          *pspec)
{
  PikaParasite *parasite;
  gchar        *value;

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

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

  parasite = pika_image_get_parasite (config->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 *) pika_parasite_get_data (parasite, &parasite_size);
      image_value = g_strndup (image_value, parasite_size);
      pika_parasite_free (parasite);

      if (g_strcmp0 (value, image_value))
        {
          if (value && strlen (value))
            {
              parasite = pika_parasite_new (pspec->name,
                                            PIKA_PARASITE_PERSISTENT,
                                            strlen (value) + 1,
                                            value);
              pika_image_attach_parasite (config->priv->image,
                                          parasite);
              pika_parasite_free (parasite);
            }
          else
            {
              pika_image_detach_parasite (config->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 "pika-comment" here, yes this is bad hack  */
      if (! strcmp (pspec->name, "pika-comment"))
        {
          default_value = pika_get_default_comment ();
        }

      if (g_strcmp0 (value, default_value) &&
          value && strlen (value))
        {
          parasite = pika_parasite_new (pspec->name,
                                        PIKA_PARASITE_PERSISTENT,
                                        strlen (value) + 1,
                                        value);
          pika_image_attach_parasite (config->priv->image,
                                      parasite);
          pika_parasite_free (parasite);
        }

      g_free (default_value);
    }

  g_free (value);
}

/**
 * pika_procedure_config_save_metadata:
 * @config:         a #PikaProcedureConfig
 * @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@SaveProcedure] at the end of the `run()` callback.
 *
 * Only use this function if the [class@Metadata] passed as argument of a
 * [class@SaveProcedure]'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
 * using [method@Image.metadata_save_finish].
 *
 * 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
pika_procedure_config_save_metadata (PikaProcedureConfig *config,
                                     PikaImage           *exported_image,
                                     GFile               *file)
{
  g_return_if_fail (PIKA_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (PIKA_IS_IMAGE (exported_image));
  g_return_if_fail (G_IS_FILE (file));

  if (config->priv->metadata && ! config->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;
          PikaMetadataSaveFlags  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)
                config->priv->metadata_flags |= prop_flag;
              else
                config->priv->metadata_flags &= ~prop_flag;
            }
          else
            {
              config->priv->metadata_flags &= ~prop_flag;
            }
        }

      if (! pika_image_metadata_save_finish (exported_image,
                                             config->priv->mime_type,
                                             config->priv->metadata,
                                             config->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);
            }
        }

      config->priv->metadata_saved = TRUE;
    }
}

/**
 * pika_procedure_config_get_choice_id:
 * @config:        a #PikaProcedureConfig
 * @property_name: the name of a [struct@ParamSpecChoice] property.
 *
 * A utility function which will get the current string value of a
 * [struct@ParamSpecChoice] 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
pika_procedure_config_get_choice_id (PikaProcedureConfig *config,
                                     const gchar         *property_name)
{
  GParamSpec          *param_spec;
  PikaParamSpecChoice *cspec;
  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), PIKA_TYPE_PARAM_CHOICE))
    {
      g_warning ("%s: property '%s' of %s is not a PikaParamSpecChoice.",
                 G_STRFUNC,
                 param_spec->name,
                 g_type_name (param_spec->owner_type));
      return 0;
    }

  cspec = PIKA_PARAM_SPEC_CHOICE (param_spec);
  g_object_get (config,
                property_name, &value,
                NULL);
  id = pika_choice_get_id (cspec->choice, value);

  g_free (value);

  return id;
}


/*  Functions only used by PikaProcedure classes  */

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

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

  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                           &n_pspecs);
  pika_procedure_get_aux_arguments (config->priv->procedure, &n_aux_args);
  n_values = pika_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 = 1; i < n_pspecs; i++)
    {
      GParamSpec *pspec = pspecs[i];
      GValue     *value = pika_value_array_index (values, i - 1);

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

  g_free (pspecs);
}

/**
 * _pika_procedure_config_begin_run:
 * @config:   a #PikaProcedureConfig
 * @image: (nullable): a #PikaImage or %NULL
 * @run_mode: the #PikaRunMode passed to a [class@Procedure]'s run()
 * @args:     the #PikaValueArray passed to a [class@Procedure]'s run()
 *
 * Populates @config with values for a [class@Procedure]'s run(),
 * depending on @run_mode.
 *
 * If @run_mode is %PIKA_RUN_INTERACTIVE or %PIKA_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 %PIKA_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
 * %PIKA_ARGUMENT_SYNC_PARASITE with [class@Procedure.set_argument_sync]:
 *
 * String properties are set to the value of the image parasite if
 * @run_mode is %PIKA_RUN_INTERACTIVE or %PIKA_RUN_WITH_LAST_VALS, or
 * if the corresponding argument is an auxiliary argument. As a
 * special case, a property named "pika-comment" will default to
 * [func@get_default_comment] if there is no "pika-comment" parasite.
 *
 * After calling this function, the @args passed to run() should be
 * left alone and @config be treated as the procedure's 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
_pika_procedure_config_begin_run (PikaProcedureConfig  *config,
                                  PikaImage            *image,
                                  PikaRunMode           run_mode,
                                  const PikaValueArray *args)
{
  GParamSpec  *run_mode_pspec;
  GParamSpec **pspecs;
  guint        n_pspecs;
  gint         i;
  gboolean     loaded = FALSE;
  GError      *error  = NULL;

  g_return_if_fail (PIKA_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (image == NULL || PIKA_IS_IMAGE (image));
  g_return_if_fail (args != NULL);

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

  pika_procedure_config_set_values (config, args);

  switch (run_mode)
    {
    case PIKA_RUN_INTERACTIVE :
    case PIKA_RUN_WITH_LAST_VALS:
      if (image)
        {
          loaded = pika_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 &&
          ! pika_procedure_config_load_last (config, &error) && error)
        {
          g_printerr ("Loading last used values from disk failed: %s\n",
                      error->message);
          g_clear_error (&error);
        }
      break;

    case PIKA_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 == PIKA_TYPE_PROCEDURE_CONFIG)
        continue;

      switch (pika_procedure_get_argument_sync (config->priv->procedure,
                                                pspec->name))
        {
        case PIKA_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 (pika_procedure_find_aux_argument (config->priv->procedure,
                                                pspec->name) ||
              (run_mode != PIKA_RUN_NONINTERACTIVE))
            {
              pika_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);
}

/**
 * _pika_procedure_config_end_run:
 * @config: a #PikaProcedureConfig
 * @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
 * %PIKA_RUN_INTERACTIVE, @config is saved as last used values to be
 * used when the procedure runs again. Additionally, if the #PikaImage
 * passed to [method@ProcedureConfig.begin_run] was not %NULL, @config is
 * attached to @image as last used values for this image using a
 * #PikaParasite and [method@Image.attach_parasite].
 *
 * If @run_mode was not %PIKA_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 %PIKA_RUN_INTERACTIVE run, %PIKA_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
_pika_procedure_config_end_run (PikaProcedureConfig *config,
                                PikaPDBStatusType    status)
{
  g_return_if_fail (PIKA_IS_PROCEDURE_CONFIG (config));

  if (config->priv->run_mode != PIKA_RUN_NONINTERACTIVE)
    pika_displays_flush ();

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

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

      if (! pika_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 == PIKA_TYPE_PROCEDURE_CONFIG)
            continue;

          switch (pika_procedure_get_argument_sync (config->priv->procedure,
                                                    pspec->name))
            {
            case PIKA_ARGUMENT_SYNC_PARASITE:
              pika_procedure_config_set_parasite (config, pspec);
              break;

            default:
              break;
            }
        }

      g_free (pspecs);
    }

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

/**
 * _pika_procedure_config_begin_export:
 * @config:         a #PikaProcedureConfig
 * @original_image: the image passed to run()
 * @run_mode:       the #PikaRunMode 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@SaveProcedure]. 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@SaveProcedure] are
 * synced. The procedure can but must not provide these arguments.
 *
 * - "save-exif" for %PIKA_METADATA_SAVE_EXIF.
 * - "save-xmp" for %PIKA_METADATA_SAVE_XMP.
 * - "save-iptc" for %PIKA_METADATA_SAVE_IPTC.
 * - "save-thumbnail" for %PIKA_METADATA_SAVE_THUMBNAIL.
 * - "save-color-profile" for %PIKA_METADATA_SAVE_COLOR_PROFILE.
 * - "save-comment" for %PIKA_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
 * PIKA_PROC_ARG_BOOLEAN() or PIKA_PROC_AUX_ARG_BOOLEAN().
 *
 * Returns: (transfer none) (nullable): The metadata to be used
 *          for this export, or %NULL if @original_image doesn't have
 *          metadata.
 *
 * Since: 3.0
 **/
PikaMetadata *
_pika_procedure_config_begin_export (PikaProcedureConfig  *config,
                                     PikaImage            *original_image,
                                     PikaRunMode           run_mode,
                                     const PikaValueArray *args,
                                     const gchar          *mime_type)
{
  GObjectClass *object_class;

  g_return_val_if_fail (PIKA_IS_PROCEDURE_CONFIG (config), NULL);
  g_return_val_if_fail (PIKA_IS_IMAGE (original_image), NULL);
  g_return_val_if_fail (args != NULL, NULL);

  object_class = G_OBJECT_GET_CLASS (config);

  if (mime_type)
    {
      PikaMetadataSaveFlags metadata_flags;
      gint                  i;

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

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

      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);
            }
        }
    }

  _pika_procedure_config_begin_run (config, original_image, run_mode, args);

  return config->priv->metadata;
}

/**
 * _pika_procedure_config_end_export:
 * @config:         a #PikaProcedureConfig
 * @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@SaveProcedure]. 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 %PIKA_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
_pika_procedure_config_end_export (PikaProcedureConfig *config,
                                   PikaImage           *exported_image,
                                   GFile               *file,
                                   PikaPDBStatusType    status)
{
  g_return_if_fail (PIKA_IS_PROCEDURE_CONFIG (config));
  g_return_if_fail (PIKA_IS_IMAGE (exported_image));
  g_return_if_fail (G_IS_FILE (file));

  if (status == PIKA_PDB_SUCCESS)
    {
      pika_procedure_config_save_metadata (config, exported_image, file);
    }

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

  _pika_procedure_config_end_run (config, status);
}

gboolean
pika_procedure_config_has_default (PikaProcedureConfig *config)
{
  GFile    *file;
  gboolean  success;

  g_return_val_if_fail (PIKA_IS_PROCEDURE_CONFIG (config), FALSE);

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

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

  return success;
}

gboolean
pika_procedure_config_load_default (PikaProcedureConfig  *config,
                                    GError              **error)
{
  GFile    *file;
  gboolean  success;

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

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

  success = pika_config_deserialize_file (PIKA_CONFIG (config),
                                          file,
                                          NULL, error);

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

  g_object_unref (file);

  return success;
}

gboolean
pika_procedure_config_save_default (PikaProcedureConfig  *config,
                                    GError              **error)
{
  GFile    *file;
  gboolean  success;

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

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

  success = pika_config_serialize_to_file (PIKA_CONFIG (config),
                                           file,
                                           "settings",
                                           "end of settings",
                                           NULL, error);

  g_object_unref (file);

  return success;
}


/*  private functions  */

/**
 * pika_procedure_config_set_values:
 * @config: a #PikaProcedureConfig
 * @values: a #PikaValueArray
 *
 * 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
pika_procedure_config_set_values (PikaProcedureConfig  *config,
                                  const PikaValueArray *values)
{
  GParamSpec **pspecs;
  guint        n_pspecs;
  gint         n_aux_args;
  gint         n_values;
  gint         i;

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

  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (config),
                                           &n_pspecs);
  pika_procedure_get_aux_arguments (config->priv->procedure, &n_aux_args);
  n_values = pika_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 = pika_value_array_index (values, i);

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

  g_free (pspecs);
}

static gboolean
pika_procedure_config_load_last (PikaProcedureConfig  *config,
                                 GError              **error)
{
  GFile    *file;
  gboolean  success;

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

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

  success = pika_config_deserialize_file (PIKA_CONFIG (config),
                                          file,
                                          NULL, error);

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

  g_object_unref (file);

  return success;
}

static gboolean
pika_procedure_config_save_last (PikaProcedureConfig  *config,
                                 GError              **error)
{
  GFile    *file;
  gboolean  success;

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

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

  success = pika_config_serialize_to_file (PIKA_CONFIG (config),
                                           file,
                                           "settings",
                                           "end of settings",
                                           NULL, error);

  g_object_unref (file);

  return success;
}

static gboolean
pika_procedure_config_load_parasite (PikaProcedureConfig  *config,
                                     PikaImage            *image,
                                     GError              **error)
{
  gchar        *name;
  PikaParasite *parasite;
  gboolean      success;

  g_return_val_if_fail (PIKA_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  name = pika_procedure_config_parasite_name (config, "-last");
  parasite = pika_image_get_parasite (image, name);
  g_free (name);

  if (! parasite)
    return FALSE;

  success = pika_config_deserialize_parasite (PIKA_CONFIG (config),
                                              parasite,
                                              NULL, error);
  pika_parasite_free (parasite);

  return success;
}

static gboolean
pika_procedure_config_save_parasite (PikaProcedureConfig  *config,
                                     PikaImage            *image,
                                     GError              **error)
{
  gchar        *name;
  PikaParasite *parasite;

  g_return_val_if_fail (PIKA_IS_PROCEDURE_CONFIG (config), FALSE);
  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  name = pika_procedure_config_parasite_name (config, "-last");
  parasite = pika_config_serialize_to_parasite (PIKA_CONFIG (config),
                                                name,
                                                PIKA_PARASITE_PERSISTENT,
                                                NULL);
  g_free (name);

  if (! parasite)
    return FALSE;

  pika_image_attach_parasite (image, parasite);
  pika_parasite_free (parasite);

  return TRUE;
}

static GFile *
pika_procedure_config_get_file (PikaProcedureConfig *config,
                                const gchar         *extension)
{
  GFile *file;
  gchar *basename;

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

  return file;
}

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