/* LIBGIMP - The GIMP Library
 * Copyright (C) 1995-2000 Peter Mattis and Spencer Kimball
 *
 * gimpimagemetadata-save.c
 *
 * 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 <string.h>
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#if defined(_WIN32) && !defined(HAVE_SYS_TIME_H)
#include <time.h>
#include <windows.h>
#if defined(_MSC_VER) || defined(_MSC_EXTENSIONS)
#define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64
#else
#define DELTA_EPOCH_IN_MICROSECS 11644473600000000ULL
#endif
#endif

#include <gexiv2/gexiv2.h>

#include "gimp.h"
#include "gimpimagemetadata.h"

#include "libgimp-intl.h"


static const gchar *  gimp_fix_xmp_tag                      (const gchar          *tag);

static void       gimp_image_metadata_copy_tag              (GExiv2Metadata       *src,
                                                             GExiv2Metadata       *dest,
                                                             const gchar          *tag);

static gint       gimp_natural_sort_compare                 (gconstpointer         left,
                                                             gconstpointer         right);

static GList*     gimp_image_metadata_convert_tags_to_list  (gchar               **xmp_tags);

static gboolean   gimp_image_metadata_get_xmp_struct_type   (const gchar          *tag,
                                                             GExiv2StructureType  *struct_type);

static gboolean   gimp_image_metadata_exclude_metadata      (GList                *exclude_list,
                                                             gchar                *tag);

static GList*     gimp_image_metadata_set_xmp_structs       (GList                *xmp_list,
                                                             GExiv2Metadata       *metadata);

/*  public functions  */

/**
 * gimp_image_metadata_save_prepare:
 * @image:           The original image
 * @mime_type:       The saved file's mime-type
 * @suggested_flags: Suggested default values for the metadata to export.
 *
 * Gets the image metadata for storing it in an exported file.
 *
 * *Note: There is normally no need to call this function because it's
 * already called by [class@ExportProcedure] at the start and the
 * metadata is passed to the `run()` callback.*
 *
 * *You may call it separately for instance if you set @export_metadata
 * to %NULL in [ctor@Gimp.ExportProcedure.new] to prevent `libgimp`
 * from trying to store the metadata in the exported file, yet you wish
 * to process and store the metadata yourself using custom API.*
 *
 * The @suggested_flags are determined from what kind of metadata (Exif,
 * XMP, ...) is actually present in the image and the preferences for
 * metadata exporting.
 * The calling application may still ignore @suggested_flags, for
 * instance to follow the settings from a previous export in the same
 * session, or a previous export of the same image. But it should not
 * override the preferences without a good reason since it is a data
 * leak.
 *
 * The suggested value for [flags@Gimp.MetadataSaveFlags.THUMBNAIL] is
 * determined by whether there was a thumbnail in the previously
 * imported image.
 *
 * Returns: (transfer full): The image's metadata, prepared for saving.
 *
 * Since: 2.10
 */
GimpMetadata *
gimp_image_metadata_save_prepare (GimpImage             *image,
                                  const gchar           *mime_type,
                                  GimpMetadataSaveFlags *suggested_flags)
{
  GimpMetadata *metadata;
  GError       *error = NULL;

  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
  g_return_val_if_fail (mime_type != NULL, NULL);
  g_return_val_if_fail (suggested_flags != NULL, NULL);

  *suggested_flags = GIMP_METADATA_SAVE_ALL;

  metadata = gimp_image_get_metadata (image);

  if (metadata)
    {
      GDateTime      *datetime;
      gint            image_width;
      gint            image_height;
      gdouble         xres;
      gdouble         yres;
      gchar           buffer[32];
      gchar          *datetime_buf = NULL;
      GExiv2Metadata *g2metadata = GEXIV2_METADATA (metadata);

      image_width  = gimp_image_get_width  (image);
      image_height = gimp_image_get_height (image);

      datetime = g_date_time_new_now_local ();

      if (! gimp_update_metadata ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_UPDATE;

      /* Exif */

      if (! gimp_export_exif () ||
          ! gexiv2_metadata_has_exif (g2metadata))
        *suggested_flags &= ~GIMP_METADATA_SAVE_EXIF;

      if (gimp_update_metadata ())
        {
          datetime_buf = g_date_time_format (datetime, "%Y:%m:%d %T");
          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Exif.Image.DateTime",
                                              datetime_buf, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Image.DateTime", error->message);
              g_clear_error (&error);
            }
          g_clear_pointer (&datetime_buf, g_free);

          /* Timezone is separate */
          datetime_buf = g_date_time_format (datetime, "\%:z");
          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Exif.Photo.OffsetTime",
                                              datetime_buf, NULL);
          g_clear_pointer (&datetime_buf, g_free);


          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Exif.Image.Software",
                                              PACKAGE_STRING, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Image.Software", error->message);
              g_clear_error (&error);
            }
        }

      gimp_metadata_set_pixel_size (metadata,
                                    image_width, image_height);

      gimp_image_get_resolution (image, &xres, &yres);
      gimp_metadata_set_resolution (metadata, xres, yres,
                                    gimp_image_get_unit (image));

      /* XMP */

      if (! gimp_export_xmp () ||
          ! gexiv2_metadata_has_xmp (g2metadata))
        *suggested_flags &= ~GIMP_METADATA_SAVE_XMP;

      gexiv2_metadata_try_set_tag_string (g2metadata,
                                          "Xmp.dc.Format",
                                          mime_type, &error);
      if (error)
        {
          g_warning ("%s: failed to set metadata '%s': %s\n",
                     G_STRFUNC, "Xmp.dc.Format", error->message);
          g_clear_error (&error);
        }

      if (gimp_update_metadata ())
        {
          /* XMP uses datetime in ISO 8601 format */
          datetime_buf = g_date_time_format (datetime, "%Y-%m-%dT%T\%:z");

          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Xmp.xmp.ModifyDate",
                                              datetime_buf, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                          G_STRFUNC, "Xmp.xmp.ModifyDate", error->message);
              g_clear_error (&error);
            }
          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Xmp.xmp.MetadataDate",
                                              datetime_buf, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                          G_STRFUNC, "Xmp.xmp.MetadataDate", error->message);
              g_clear_error (&error);
            }
        }

      if (! g_strcmp0 (mime_type, "image/tiff"))
        {
          /* TIFF specific XMP data */

          g_snprintf (buffer, sizeof (buffer), "%d", image_width);
          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Xmp.tiff.ImageWidth",
                                              buffer, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.tiff.ImageWidth", error->message);
              g_clear_error (&error);
            }

          g_snprintf (buffer, sizeof (buffer), "%d", image_height);
          gexiv2_metadata_try_set_tag_string (g2metadata,
                                              "Xmp.tiff.ImageLength",
                                              buffer, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.tiff.ImageLength", error->message);
              g_clear_error (&error);
            }

          if (gimp_update_metadata ())
            {
              gexiv2_metadata_try_set_tag_string (g2metadata,
                                                  "Xmp.tiff.DateTime",
                                                  datetime_buf, &error);
              if (error)
                {
                  g_warning ("%s: failed to set metadata '%s': %s\n",
                             G_STRFUNC, "Xmp.tiff.DateTime", error->message);
                  g_clear_error (&error);
                }
            }
        }

      /* IPTC */

      if (! gimp_export_iptc () ||
          ! gexiv2_metadata_has_iptc (g2metadata))
        *suggested_flags &= ~GIMP_METADATA_SAVE_IPTC;

      g_free (datetime_buf);
      g_date_time_unref (datetime);

      /* EXIF Thumbnail */

      if (gimp_export_thumbnail () && gexiv2_metadata_has_exif (g2metadata))
        {
          gchar *value;

          /* Check a required tag for a thumbnail to be present. */
          value = gexiv2_metadata_try_get_tag_string (g2metadata,
                                                      "Exif.Thumbnail.ImageLength",
                                                      NULL);
          if (! value)
            {
              *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;
            }
          else
            {
              g_free (value);
            }
        }
      else
        {
          *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;
        }
    }
  else
    {
      /* At least initialize the returned flags with preferences defaults */

      if (! gimp_export_exif ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_EXIF;

      if (! gimp_export_xmp ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_XMP;

      if (! gimp_export_iptc ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_IPTC;

      if (! gimp_export_thumbnail ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL;

      if (! gimp_update_metadata ())
        *suggested_flags &= ~GIMP_METADATA_SAVE_UPDATE;
    }

  /* Color profile */

  if (! gimp_export_color_profile ())
    *suggested_flags &= ~GIMP_METADATA_SAVE_COLOR_PROFILE;

  /* Comment */

  if (! gimp_export_comment ())
    *suggested_flags &= ~GIMP_METADATA_SAVE_COMMENT;

  return metadata;
}

static const gchar *
gimp_fix_xmp_tag (const gchar *tag)
{
  const gchar org[] = "/Iptc4xmpExt";
  const gchar new[] = "/iptcExt";
  gchar *substring, *tail;

  G_STATIC_ASSERT (sizeof new < sizeof org);

  /* Due to problems using /Iptc4xmpExt namespace (/iptcExt is used
   * instead by Exiv2) we replace all occurrences with /iptcExt which
   * is valid but less common. Not doing so would cause saving xmp
   * metadata to fail. This has to be done after getting the values
   * from the source metadata since that source uses the original
   * tag names and would otherwise return NULL as value.
   * Multiple occurrences are possible: e.g.:
   * Xmp.iptcExt.ImageRegion[3]/Iptc4xmpExt:RegionBoundary/Iptc4xmpExt:rbVertices[1]/Iptc4xmpExt:rbX
   */

  while ((substring = strstr (tag, org)))
    {
      strcpy (substring, new);

      tail = substring + strlen (org);
      substring += strlen (new);

      while ((*substring++ = *tail++))
        ;

      g_debug ("Fixed tag value: %s", tag);
    }
  return tag;
}

static void
gimp_image_metadata_copy_tag (GExiv2Metadata *src,
                              GExiv2Metadata *dest,
                              const gchar    *tag)
{
  gchar  **values = gexiv2_metadata_try_get_tag_multiple (src, tag, NULL);
  GError  *error  = NULL;

  if (values)
    {
      gchar *temp_tag;

      /* Xmp always seems to return multiple values */
      if (g_str_has_prefix (tag, "Xmp."))
        temp_tag = (gchar *) gimp_fix_xmp_tag (g_strdup (tag));
      else
        temp_tag = g_strdup (tag);

      g_debug ("Copy multi tag %s, first value: %s", temp_tag, values[0]);
      gexiv2_metadata_try_set_tag_multiple (dest, temp_tag,
                                            (const gchar **) values,
                                            &error);
      if (error)
        {
          g_warning ("%s: failed to set multiple metadata '%s': %s\n",
                     G_STRFUNC, tag, error->message);
          g_clear_error (&error);
        }

      g_free (temp_tag);
      g_strfreev (values);
    }
  else
    {
      gchar *value = gexiv2_metadata_try_get_tag_string (src, tag, &error);

      if (value)
        {
          g_debug ("Copy tag %s, value: %s", tag, value);
          gexiv2_metadata_try_set_tag_string (dest, tag, value, &error);
          if (error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, tag, error->message);
              g_clear_error (&error);
            }
          g_free (value);
        }
      else if (error)
        {
          g_warning ("%s: failed to get metadata '%s': %s\n",
                     G_STRFUNC, tag, error->message);
          g_clear_error (&error);
        }
    }
}

static gint
gimp_natural_sort_compare (gconstpointer left,
                           gconstpointer right)
{
  gint   compare;
  gchar *left_key  = g_utf8_collate_key_for_filename ((gchar *) left, -1);
  gchar *right_key = g_utf8_collate_key_for_filename ((gchar *) right, -1);

  compare = g_strcmp0 (left_key, right_key);
  g_free (left_key);
  g_free (right_key);

  return compare;
}

static GList*
gimp_image_metadata_convert_tags_to_list (gchar **xmp_tags)
{
  GList *list = NULL;
  gint   i;

  for (i = 0; xmp_tags[i] != NULL; i++)
    {
      g_debug ("Tag: %s, tag type: %s", xmp_tags[i], gexiv2_metadata_try_get_tag_type (xmp_tags[i], NULL));
      list = g_list_prepend (list, xmp_tags[i]);
    }
  return list;
}

static gboolean
gimp_image_metadata_get_xmp_struct_type (const gchar         *tag,
                                         GExiv2StructureType *struct_type)
{
  GError *error = NULL;

  g_debug ("Struct type for tag: %s, type: %s", tag, gexiv2_metadata_try_get_tag_type (tag, NULL));

  if (! g_strcmp0 (gexiv2_metadata_try_get_tag_type (tag, &error), "XmpSeq"))
    {
      *struct_type = GEXIV2_STRUCTURE_XA_SEQ;
      return TRUE;
    }

  if (error)
    {
      g_debug ("%s: failed to get type of tag '%s': %s\n",
               G_STRFUNC, tag, error->message);
      g_clear_error (&error);
      *struct_type = GEXIV2_STRUCTURE_XA_NONE;
      return FALSE;
    }

  *struct_type = GEXIV2_STRUCTURE_XA_BAG;
  return TRUE;
}

static gboolean
gimp_image_metadata_exclude_metadata (GList *exclude_list,
                                      gchar *tag)
{
  GList *list;

  for (list = exclude_list; list != NULL; list = list->next)
    {
      if (strstr (tag, (gchar *) list->data))
        {
          return TRUE;
        }
    }
  return FALSE;
}

static GList*
gimp_image_metadata_set_xmp_structs (GList          *xmp_list,
                                     GExiv2Metadata *metadata)
{
  GList *list;
  GList *exclude  = NULL;
  gchar *prev_one = NULL;
  gchar *prev_two = NULL;

  for (list = xmp_list; list != NULL; list = list->next)
    {
      gchar **tag_split;

      /*
       * Most tags with structs have only one struct part, like:
       * Xmp.xmpMM.History[1]...
       * However there are also Xmp tags that have two
       * structs in one tag, e.g.:
       * Xmp.crs.GradientBasedCorrections[1]/crs:CorrectionMasks[1]...
       */
      tag_split = g_strsplit ((gchar *) list->data, "[1]", 3);
      /* Check if there are at least two parts */
      if (tag_split && tag_split[1])
        {
          GError *error = NULL;

          if (strstr (tag_split[0], "["))
            {
              gchar **splits    = NULL;
              gint    last_part = -1;

              /* Handle things like:
               * Xmp.iptcExt.ImageRegion[3]/Iptc4xmpExt:RegionBoundary/Iptc4xmpExt:rbVertices[1]/Iptc4xmpExt:rbX,
               * where rbVertices only appears in ImageRegion[3].
               * We only want the part starting with the last '/'.
               */
              splits = g_strsplit (tag_split[0], "/", 0);
              if (splits)
                {
                  GExiv2StructureType  type;
                  gchar               *tag_inner = NULL;

                  for (int i = 0; splits[i] != NULL; i++)
                    {
                      last_part = i;
                    }
                  tag_inner = g_strconcat ("/", splits[last_part], NULL);

                  if (gimp_image_metadata_get_xmp_struct_type (gimp_fix_xmp_tag (tag_inner), &type))
                    {
                      gexiv2_metadata_try_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
                                                              tag_inner, type, &error);
                      if (error)
                        {
                          g_printerr ("%s: failed to set XMP struct '%s': %s\n",
                                      G_STRFUNC, tag_inner, error->message);
                          g_clear_error (&error);
                        }
                    }
                  else
                    {
                      /* Add to exclude list */
                      if (! g_list_find_custom (exclude, tag_inner, (GCompareFunc) g_strcmp0))
                        {
                          g_debug ("Adding unsupported tag to exclude list: %s", tag_inner);
                          exclude = g_list_prepend (exclude, g_strdup (tag_inner));
                        }
                    }
                  g_free (tag_inner);
                }
              g_strfreev (splits);
            }
          else if (! prev_one || strcmp (tag_split[0], prev_one) != 0)
            {
              GExiv2StructureType  type;

              g_free (prev_one);
              prev_one = g_strdup (tag_split[0]);

              if (gimp_image_metadata_get_xmp_struct_type (gimp_fix_xmp_tag (tag_split[0]), &type))
                {
                  gexiv2_metadata_try_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
                                                          prev_one, type, &error);
                  if (error)
                    {
                      g_printerr ("%s: failed to set XMP struct '%s': %s\n",
                                  G_STRFUNC, prev_one, error->message);
                      g_clear_error (&error);
                    }
                }
              else
                {
                  /* Add to exclude list */
                  if (! g_list_find_custom (exclude, tag_split[0], (GCompareFunc) g_strcmp0))
                    {
                      g_debug ("Adding unsupported tag to exclude list: %s", tag_split[0]);
                      exclude = g_list_prepend (exclude, g_strdup (tag_split[0]));
                    }
                }
            }
          if (tag_split[2] && (!prev_two || strcmp (tag_split[1], prev_two) != 0))
            {
              gchar               *second_struct;
              GExiv2StructureType  type;

              g_free (prev_two);
              prev_two = g_strdup (tag_split[1]);
              second_struct = g_strdup_printf ("%s[1]%s", prev_one, gimp_fix_xmp_tag(prev_two));

              if (gimp_image_metadata_get_xmp_struct_type (gimp_fix_xmp_tag (tag_split[1]), &type))
                {
                  gexiv2_metadata_try_set_xmp_tag_struct (GEXIV2_METADATA (metadata),
                                                          second_struct, type, &error);
                  if (error)
                    {
                      g_printerr ("%s: failed to set XMP struct '%s': %s\n",
                                   G_STRFUNC, second_struct, error->message);
                      g_clear_error (&error);
                    }
                }
              else
                {
                  /* Add to exclude list */
                  if (! g_list_find_custom (exclude, tag_split[1], (GCompareFunc) g_strcmp0))
                    {
                      g_debug ("Adding unsupported tag to exclude list: %s", tag_split[1]);
                      exclude = g_list_prepend (exclude, g_strdup (tag_split[1]));
                    }
                }
              g_free (second_struct);
            }
        }

      g_strfreev (tag_split);
    }
  g_free (prev_one);
  g_free (prev_two);

  return exclude;
}

#if defined(_WIN32) && ! defined(HAVE_SYS_TIME_H)
struct timezone
{
  int tz_minuteswest; /* minutes W of Greenwich */
  int tz_dsttime;     /* type of dst correction */
};

int
gettimeofday (struct timeval *tv, struct timezone *tz)
{
  FILETIME         ft;
  unsigned __int64 tmpres = 0;
  static int       tzflag = 0;

  if (NULL != tv)
    {
      GetSystemTimeAsFileTime (&ft);

      tmpres |= ft.dwHighDateTime;
      tmpres <<= 32;
      tmpres |= ft.dwLowDateTime;

      tmpres /= 10; /*convert into microseconds*/
      /*converting file time to unix epoch*/
      tmpres -= DELTA_EPOCH_IN_MICROSECS;
      tv->tv_sec  = (long) (tmpres / 1000000UL);
      tv->tv_usec = (long) (tmpres % 1000000UL);
    }

  if (NULL != tz)
    {
      if (! tzflag)
        {
          _tzset ();
          tzflag++;
        }
      tz->tz_minuteswest = _timezone / 60;
      tz->tz_dsttime     = _daylight;
    }

  return 0;
}
#endif

/**
 * gimp_image_metadata_save_filter:
 * @image:     The actually saved image
 * @mime_type: The saved file's mime-type
 * @metadata:  The metadata to export
 * @flags:     Flags to specify what of the metadata to save
 * @file:      The file @image was saved to or NULL if file was not saved yet
 * @error:     Return location for error message
 *
 * Filters the @metadata retrieved from the image with
 * [method@Gimp.Image.metadata_save_prepare], taking into account the
 * passed @flags.
 *
 * *Note: There is normally no need to call this function because it's
 * already called by [class@ExportProcedure] after the `run()`
 * callback.*
 *
 * Note that the @image passed to this function might be different
 * from the image passed to `gimp_image_metadata_save_prepare()`, due
 * to whatever file export conversion happened in the meantime
 *
 * This can be used as an alternative to core metadata handling when you
 * want to save metadata yourself and you need only filtering
 * processing.
 *
 * Returns: (transfer full): Filtered metadata or %NULL in case of failure.
 *          Use [GObject.Object.unref] when returned metadata are no
 *          longer needed
 *
 * Since: 3.0
 */
GimpMetadata *
gimp_image_metadata_save_filter (GimpImage            *image,
                                 const gchar          *mime_type,
                                 GimpMetadata         *metadata,
                                 GimpMetadataSaveFlags flags,
                                 GFile                *file,
                                 GError              **error)
{
  GimpMetadata   *new_metadata;
  GExiv2Metadata *new_g2metadata;
  /* Error for cases where we have full control, such as metadata tags
   * and contents. So we don't propagate these in @error (for things out
   * of our control, such as read or write errors), but we use them as
   * internal warning for bugs.
   */
  GError         *code_error = NULL;
  gboolean        support_exif;
  gboolean        support_xmp;
  gboolean        support_iptc;
  gint            i;

  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
  g_return_val_if_fail (mime_type != NULL, NULL);
  g_return_val_if_fail (GEXIV2_IS_METADATA (metadata), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  if (! (flags & (GIMP_METADATA_SAVE_EXIF |
                  GIMP_METADATA_SAVE_XMP  |
                  GIMP_METADATA_SAVE_IPTC |
                  GIMP_METADATA_SAVE_THUMBNAIL)))
    return NULL;

  if (file)
    {
      /* read metadata from saved file */
      new_metadata = gimp_metadata_load_from_file (file, error);
    }
  else
    {
      new_metadata = gimp_metadata_new ();
    }

  if (! new_metadata)
    return NULL;

  new_g2metadata = GEXIV2_METADATA (new_metadata);
  support_exif = gexiv2_metadata_get_supports_exif (new_g2metadata);
  support_xmp  = gexiv2_metadata_get_supports_xmp  (new_g2metadata);
  support_iptc = gexiv2_metadata_get_supports_iptc (new_g2metadata);

  if ((flags & GIMP_METADATA_SAVE_EXIF) && support_exif)
    {
      gchar **exif_data = NULL;

      if (gimp_update_metadata () && (flags & GIMP_METADATA_SAVE_COMMENT))
        {
          GimpParasite *comment_parasite;

          comment_parasite = gimp_image_get_parasite (image, "gimp-comment");
          if (comment_parasite)
            {
              guint32  parasite_size;
              gchar   *comment = NULL;

              comment = (gchar *) gimp_parasite_get_data (comment_parasite, &parasite_size);
              comment = g_strndup (comment, parasite_size);

              gimp_parasite_free (comment_parasite);

              if (comment)
                {
                  gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                                      "Exif.Photo.UserComment",
                                                      comment, &code_error);
                  if (code_error)
                    {
                      g_warning ("%s: failed to set metadata '%s': %s\n",
                                 G_STRFUNC, "Exif.Photo.UserComment", code_error->message);
                      g_clear_error (&code_error);
                    }

                  gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                                      "Exif.Image.ImageDescription",
                                                      comment, &code_error);
                  if (code_error)
                    {
                      g_warning ("%s: failed to set metadata '%s': %s\n",
                                 G_STRFUNC, "Exif.Image.ImageDescription", code_error->message);
                      g_clear_error (&code_error);
                    }

                  g_free (comment);
                }
            }
        }

      exif_data = gexiv2_metadata_get_exif_tags (GEXIV2_METADATA (metadata));

      for (i = 0; exif_data[i] != NULL; i++)
        {
          if (! gexiv2_metadata_try_has_tag (new_g2metadata, exif_data[i], NULL) &&
              gimp_metadata_is_tag_supported (exif_data[i], mime_type))
            {
              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
                                            new_g2metadata,
                                            exif_data[i]);
            }
        }

      g_strfreev (exif_data);
    }

  if ((flags & GIMP_METADATA_SAVE_XMP) && support_xmp)
    {
      gchar         **xmp_data;
      struct timeval  timer_usec;
      gint64          timestamp_usec;
      gchar           ts[128];
      GList          *xmp_list = NULL;
      GList          *exclude_list = NULL;
      GList          *list;

      if (gimp_update_metadata ())
        {
          gettimeofday (&timer_usec, NULL);
          timestamp_usec = ((gint64) timer_usec.tv_sec) * 1000000ll +
                            (gint64) timer_usec.tv_usec;
          g_snprintf (ts, sizeof (ts), "%" G_GINT64_FORMAT, timestamp_usec);

          gimp_metadata_add_xmp_history (metadata, "");

          gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                              "Xmp.GIMP.TimeStamp",
                                              ts, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.GIMP.TimeStamp", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                              "Xmp.xmp.CreatorTool",
                                              N_("GIMP"), &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.xmp.CreatorTool", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                              "Xmp.GIMP.Version",
                                              GIMP_VERSION, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.GIMP.Version", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                              "Xmp.GIMP.API",
                                              GIMP_API_VERSION, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.GIMP.API", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (GEXIV2_METADATA (metadata),
                                              "Xmp.GIMP.Platform",
#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
                                              "Windows",
#elif defined(__linux__)
                                              "Linux",
#elif defined(__APPLE__) && defined(__MACH__)
                                              "Mac OS",
#elif defined(unix) || defined(__unix__) || defined(__unix)
                                              "Unix",
#else
                                              "Unknown",
#endif
                                              &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Xmp.GIMP.Platform", code_error->message);
              g_clear_error (&code_error);
            }
        }

      xmp_data = gexiv2_metadata_get_xmp_tags (GEXIV2_METADATA (metadata));

      xmp_list = gimp_image_metadata_convert_tags_to_list (xmp_data);
      xmp_list = g_list_sort (xmp_list, (GCompareFunc) gimp_natural_sort_compare);
      exclude_list = gimp_image_metadata_set_xmp_structs (xmp_list, new_g2metadata);

      for (list = xmp_list; list != NULL; list = list->next)
        {
          gchar *fixed = NULL;

          /* Certain XMP metadata tags currently can't be saved by us until
           * we get support in gexiv2 for adding new structs.
           * We remove these from the exported metadata because the XMPSDK
           * in exiv2 would otherwise fail to write all XMP metadata. */

          fixed = (gchar *) gimp_fix_xmp_tag (g_strdup ((gchar *) list->data));
          if (gimp_image_metadata_exclude_metadata (exclude_list, (gchar *) fixed))
            {
              g_warning ("Unsupported XMP metadata tag (not saved): %s", fixed);
            }
          else if (! gexiv2_metadata_try_has_tag (new_g2metadata, (gchar *) list->data, NULL) &&
              gimp_metadata_is_tag_supported ((gchar *) list->data, mime_type))
            {
              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
                                            new_g2metadata,
                                            (gchar *) list->data);
            }
          else
            g_debug ("Ignored tag: %s", (gchar *) list->data);
          g_free (fixed);
        }

      g_list_free_full (exclude_list, g_free);
      g_list_free (xmp_list);
      g_strfreev (xmp_data);
    }

  if ((flags & GIMP_METADATA_SAVE_IPTC) && support_iptc)
    {
      gchar **iptc_data = gexiv2_metadata_get_iptc_tags (GEXIV2_METADATA (metadata));

      for (i = 0; iptc_data[i] != NULL; i++)
        {
          if (! gexiv2_metadata_try_has_tag (new_g2metadata, iptc_data[i], NULL) &&
              gimp_metadata_is_tag_supported (iptc_data[i], mime_type))
            {
              gimp_image_metadata_copy_tag (GEXIV2_METADATA (metadata),
                                            new_g2metadata,
                                            iptc_data[i]);
            }
        }

      g_strfreev (iptc_data);
    }

  if (support_exif)
    {
      /* Remove all old thumbnail metadata: third-party software may have
       * written tags we don't expect. */
      gexiv2_metadata_try_erase_exif_thumbnail (new_g2metadata, &code_error);
      if (code_error)
        {
          g_warning ("%s: failed to erase EXIF thumbnail: %s",
                     G_STRFUNC, code_error->message);
          g_clear_error (&code_error);
        }
    }

  if (flags & GIMP_METADATA_SAVE_THUMBNAIL && support_exif)
    {
      GdkPixbuf *thumb_pixbuf;
      gchar     *thumb_buffer;
      gint       image_width;
      gint       image_height;
      gsize      count;
      gint       thumbw;
      gint       thumbh;

#define EXIF_THUMBNAIL_SIZE 256

      image_width  = gimp_image_get_width  (image);
      image_height = gimp_image_get_height (image);

      if (image_width > image_height)
        {
          thumbw = EXIF_THUMBNAIL_SIZE;
          thumbh = EXIF_THUMBNAIL_SIZE * image_height / image_width;
        }
      else
        {
          thumbh = EXIF_THUMBNAIL_SIZE;
          thumbw = EXIF_THUMBNAIL_SIZE * image_width / image_height;
        }

      /* TODO: currently our exported thumbnails are always RGB because
       * GdkPixbuf only supports RGB so far. While RGB thumbnail may make sense,
       * even for CMYK images or other color spaces, grayscale images would be
       * better served with a grayscale thumbnail too.
       */
      thumb_pixbuf = gimp_image_get_thumbnail (image, thumbw, thumbh,
                                               GIMP_PIXBUF_KEEP_ALPHA);

      if (gdk_pixbuf_save_to_buffer (thumb_pixbuf, &thumb_buffer, &count,
                                     "jpeg", NULL,
                                     "quality", "75",
                                     NULL))
        {
          gchar buffer[32];

          gexiv2_metadata_try_set_exif_thumbnail_from_buffer (new_g2metadata,
                                                              (guchar *) thumb_buffer,
                                                              count, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set Exif thumbnail: %s\n",
                         G_STRFUNC, code_error->message);
              g_clear_error (&code_error);
            }

          g_snprintf (buffer, sizeof (buffer), "%d", thumbw);
          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.ImageWidth",
                                              buffer, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.ImageWidth", code_error->message);
              g_clear_error (&code_error);
            }

          g_snprintf (buffer, sizeof (buffer), "%d", thumbh);
          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.ImageLength",
                                              buffer, &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.ImageLength", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.BitsPerSample",
                                              "8 8 8", &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.BitsPerSample", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.SamplesPerPixel",
                                              "3", &code_error);
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.SamplesPerPixel", code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.PhotometricInterpretation",
                                              "6", &code_error); /* old jpeg */
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.PhotometricInterpretation",
                         code_error->message);
              g_clear_error (&code_error);
            }

          gexiv2_metadata_try_set_tag_string (new_g2metadata,
                                              "Exif.Thumbnail.NewSubfileType",
                                              "1", &code_error); /* reduced resolution image */
          if (code_error)
            {
              g_warning ("%s: failed to set metadata '%s': %s\n",
                         G_STRFUNC, "Exif.Thumbnail.NewSubfileType", code_error->message);
              g_clear_error (&code_error);
            }

          g_free (thumb_buffer);
        }

      g_object_unref (thumb_pixbuf);
    }

  if (flags & GIMP_METADATA_SAVE_COLOR_PROFILE)
    {
      /* nothing to do, but if we ever need to modify metadata based
       * on the exported color profile, this is probably the place to
       * add it
       */
    }

  if (flags & GIMP_METADATA_SAVE_COMMENT)
    {
      /* nothing to do, blah blah */
    }

  return new_metadata;
}


/* Internal functions */

/**
 * _gimp_image_metadata_save_finish:
 * @image:     The actually saved image
 * @mime_type: The saved file's mime-type
 * @metadata:  The metadata to write to @file
 * @flags:     Flags to specify what of the metadata to save
 * @file:      The file @image was saved to
 * @error:     Return location for error message
 *
 * *Note: There is normally no need to call this function because it's
 * already called by [class@ExportProcedure] at the end of the `run()` callback.*
 *
 * Saves the @metadata retrieved from the image with
 * [method@Gimp.Image.metadata_save_prepare] to @file, taking into
 * account the passed @flags.
 *
 * Note that the @image passed to this function might be different
 * from the image passed to `gimp_image_metadata_save_prepare()`, due to
 * whatever file export conversion happened in the meantime
 *
 * Returns: Whether the save was successful.
 *
 * Since: 2.10
 */
gboolean
_gimp_image_metadata_save_finish (GimpImage            *image,
                                  const gchar          *mime_type,
                                  GimpMetadata         *metadata,
                                  GimpMetadataSaveFlags flags,
                                  GFile                *file,
                                  GError              **error)
{
  GimpMetadata *new_metadata;
  gboolean      success = FALSE;

  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (mime_type != NULL, FALSE);
  g_return_val_if_fail (GEXIV2_IS_METADATA (metadata), FALSE);
  g_return_val_if_fail (G_IS_FILE (file), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  if (! (flags & (GIMP_METADATA_SAVE_EXIF |
                  GIMP_METADATA_SAVE_XMP  |
                  GIMP_METADATA_SAVE_IPTC |
                  GIMP_METADATA_SAVE_THUMBNAIL)))
    return TRUE;

  new_metadata = gimp_image_metadata_save_filter (image, mime_type, metadata,
                                                  flags, file, error);
  if (! new_metadata)
    {
      return FALSE;
    }

  success = gimp_metadata_save_to_file (new_metadata, file, error);

  g_object_unref (new_metadata);

  return success;
}
