/*
 * gnome-thumbnail.c: Utilities for handling thumbnails
 *
 * Copyright (C) 2002 Red Hat, Inc.
 * Copyright (C) 2010 Carlos Garcia Campos <carlosgc@gnome.org>
 *
 * This file is part of the Gnome Library.
 *
 * The Gnome Library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * The Gnome 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with the Gnome Library; see the file COPYING.LIB.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * Author: Alexander Larsson <alexl@redhat.com>
 */

/**
 * SECTION:scarecrow-desktop-thumbnail
 * @short_description: Generates and looks up thumbnails of files and
 * directories
 * @stability: Unstable
 * @include: libscarecrow-desktop/scarecrow-desktop-thumbnail.h
 *
 * #GnomeDesktopThumbnailFactory allows generation and loading of thumbnails for
 * local and remote files and directories. It uses a collection of programs
 * called <firstterm>thumbnailers</firstterm>, each one generating thumbnails
 * for a specific set of content-types of files. For example,
 * <application>totem-video-thumbnailer</application> generates thumbnails for
 * video files using GStreamer; <application>evince-thumbnailer</application>
 * generates thumbnails for PDFs and other document files. If no specific
 * thumbnailer exists for a file, or if the thumbnailer fails, gdk-pixbuf is
 * used as a fallback.
 *
 * To generate a thumbnail, an appropriate thumbnailer program is selected then
 * executed, passing it the URI of the file to thumbnail, plus a path to write
 * the thumbnail image to. If thumbnailing succeeds, the thumbnailer should have
 * written the image to disk before terminating; but if thumbnailing fails, no
 * image should be written, and the thumbnailer should return a non-zero exit
 * status. #GnomeDesktopThumbnailFactory will then fall back to using gdk-pixbuf
 * to generate a thumbnail, if possible.
 *
 * Thumbnailers are chosen by examining a series of
 * <filename>.thumbnailer</filename> files in
 * <filename><replaceable>$PREFIX</replaceable>/share/thumbnailers</filename>.
 * Each is in a simple key-file format:
 * <informalexample><programlisting>
 * [Thumbnailer Entry]
 * Exec=evince-thumbnailer -s %s %u %o
 * MimeType=application/pdf;application/x-bzpdf;application/x-gzpdf;
 * </programlisting></informalexample>
 *
 * The <filename>.thumbnailer</filename> format supports three keys:
 * <variablelist>
 * <varlistentry><term><code>Exec</code></term><listitem><para>
 * Required. The command to execute the thumbnailer. It supports a few different
 * parameters which are replaced before calling the thumbnailer:
 * <replaceable>%u</replaceable> is the URI of the file being thumbnailed;
 * <replaceable>%i</replaceable> is its path; <replaceable>%o</replaceable>
 * is the path of the image file to be written to;
 * <replaceable>%s</replaceable> is the maximum desired size of the thumbnail
 * image (the maximum width or height, in pixels); and
 * <replaceable>%%</replaceable> is a literal percent character.
 * </para></listitem></varlistentry>
 * <varlistentry><term><code>MimeType</code></term><listitem><para>
 * Required. A semicolon-separated list of MIME types which the thumbnailer
 * supports generating thumbnails for.
 * </para></listitem></varlistentry>
 * </variablelist>
 *
 * So in the example <filename>.thumbnailer</filename> file above, the command
 * passes the requested thumbnail size, then the input file’s URI, then the
 * path for the output image file to
 * <application>evince-thumbnailer</application>.
 *
 * The code to examine and call a thumbnailer is contained in
 * #GnomeDesktopThumbnailFactory, which handles looking up the right thumbnailer
 * script, building and executing the command for it, and loading the resulting
 * thumbnail image into a #GdkPixbuf.
 *
 * Thumbnail caching is also supported by #GnomeDesktopThumbnailFactory. When
 * calling a thumbnailer, the path passed for the output image file is in
 * <filename><envar>$XDG_CACHE_HOME</envar>/thumbnails/
 * <replaceable>$SIZE</replaceable>/</filename>. The cached image file is given
 * a (probably) unique filename, generated by hashing the original file’s URI,
 * so the thumbnail can be looked up in future. #GnomeDesktopThumbnailFactory
 * supports two sizes of thumbnails: %GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL and
 * %GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE. Normal thumbnails are up to 128×128
 * pixels, whereas large thumbnails are up to 256×256 pixels. Thumbnails which
 * are larger than this are scaled down before being cached, and non-square
 * thumbnails are scaled so their largest dimension is at most 128 or 256
 * pixels.
 *
 * #GnomeDesktopThumbnailFactory also handles failed thumbnails. If a
 * thumbnailer can’t generate a thumbnail for a file (e.g. because the file is
 * corrupt or because the right video codecs aren’t available), it returns a
 * non-zero exit status. The thumbnail factory then writes an entry to
 * <filename><envar>$XDG_CACHE_HOME</envar>/thumbnails/fail/
 * gnome-thumbnail-factory/</filename> which is named after the hash of the
 * input file URI (just like a successful cached thumbnail). For future queries
 * for thumbnails for that file, #GnomeDesktopThumbnailFactory can immediately
 * return an error after looking up the fail entry.
 *
 * If a file changes content, #GnomeDesktopThumbnailFactory will generate a new
 * thumbnail because each cached image has associated metadata (stored as PNG
 * tEXt keys) storing the full URI of the thumbnailed file (to check for hash
 * collisions) and its last modification time at the point of thumbnailing. If
 * the stored modification time doesn’t match the file’s current one, a new
 * thumbnail is generated.
 *
 * Since: 2.2
 */

#include <config.h>

#include <glib.h>
#include <glib/gstdio.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#define GNOME_DESKTOP_USE_UNSTABLE_API
#include "scarecrow-desktop-thumbnail.h"
#include "scarecrow-desktop-thumbnail-script.h"

static void
thumbnailers_directory_changed (GFileMonitor                 *monitor,
                                GFile                        *file,
                                GFile                        *other_file,
                                GFileMonitorEvent             event_type,
                                GnomeDesktopThumbnailFactory *factory);

struct _GnomeDesktopThumbnailFactoryPrivate {
  GnomeDesktopThumbnailSize size;

  GMutex lock;

  GList *thumbnailers;
  GHashTable *mime_types_map;
  GList *monitors;

  GSettings *settings;
  gboolean loaded : 1;
  gboolean disabled : 1;
  gchar **disabled_types;
};

static const char *appname = "gnome-thumbnail-factory";

G_DEFINE_TYPE_WITH_CODE (GnomeDesktopThumbnailFactory,
	       gnome_desktop_thumbnail_factory,
	       G_TYPE_OBJECT,
	       G_ADD_PRIVATE (GnomeDesktopThumbnailFactory))
#define parent_class gnome_desktop_thumbnail_factory_parent_class

#define THUMBNAILER_ENTRY_GROUP "Thumbnailer Entry"
#define THUMBNAILER_EXTENSION   ".thumbnailer"

typedef struct {
    volatile gint ref_count;

    gchar *path;

    gchar  *command;
    gchar **mime_types;
} Thumbnailer;

static Thumbnailer *
thumbnailer_ref (Thumbnailer *thumb)
{
  g_return_val_if_fail (thumb != NULL, NULL);
  g_return_val_if_fail (thumb->ref_count > 0, NULL);

  g_atomic_int_inc (&thumb->ref_count);
  return thumb;
}

static void
thumbnailer_unref (Thumbnailer *thumb)
{
  g_return_if_fail (thumb != NULL);
  g_return_if_fail (thumb->ref_count > 0);

  if (g_atomic_int_dec_and_test (&thumb->ref_count))
    {
      g_free (thumb->path);
      g_free (thumb->command);
      g_strfreev (thumb->mime_types);

      g_slice_free (Thumbnailer, thumb);
    }
}

static Thumbnailer *
thumbnailer_load (Thumbnailer *thumb)
{
  GKeyFile *key_file;
  GError *error = NULL;

  key_file = g_key_file_new ();
  if (!g_key_file_load_from_file (key_file, thumb->path, 0, &error))
    {
      g_warning ("Failed to load thumbnailer from \"%s\": %s\n", thumb->path, error->message);
      g_error_free (error);
      thumbnailer_unref (thumb);
      g_key_file_free (key_file);

      return NULL;
    }

  if (!g_key_file_has_group (key_file, THUMBNAILER_ENTRY_GROUP))
    {
      g_warning ("Invalid thumbnailer: missing group \"%s\"\n", THUMBNAILER_ENTRY_GROUP);
      thumbnailer_unref (thumb);
      g_key_file_free (key_file);

      return NULL;
    }

  thumb->command = g_key_file_get_string (key_file, THUMBNAILER_ENTRY_GROUP, "Exec", NULL);
  if (!thumb->command)
    {
      g_warning ("Invalid thumbnailer: missing Exec key\n");
      thumbnailer_unref (thumb);
      g_key_file_free (key_file);

      return NULL;
    }

  thumb->mime_types = g_key_file_get_string_list (key_file, THUMBNAILER_ENTRY_GROUP, "MimeType", NULL, NULL);
  if (!thumb->mime_types)
    {
      g_warning ("Invalid thumbnailer: missing MimeType key\n");
      thumbnailer_unref (thumb);
      g_key_file_free (key_file);

      return NULL;
    }

  g_key_file_free (key_file);

  return thumb;
}

static Thumbnailer *
thumbnailer_reload (Thumbnailer *thumb)
{
  g_return_val_if_fail (thumb != NULL, NULL);

  g_free (thumb->command);
  thumb->command = NULL;
  g_strfreev (thumb->mime_types);
  thumb->mime_types = NULL;

  return thumbnailer_load (thumb);
}

static Thumbnailer *
thumbnailer_new (const gchar *path)
{
  Thumbnailer *thumb;

  thumb = g_slice_new0 (Thumbnailer);
  thumb->ref_count = 1;
  thumb->path = g_strdup (path);

  return thumbnailer_load (thumb);
}

static gpointer
init_thumbnailers_dirs (gpointer data)
{
  const gchar * const *data_dirs;
  GPtrArray *thumbs_dirs;
  guint i;

  data_dirs = g_get_system_data_dirs ();
  thumbs_dirs = g_ptr_array_new ();

  g_ptr_array_add (thumbs_dirs, g_build_filename (g_get_user_data_dir (), "thumbnailers", NULL));
  for (i = 0; data_dirs[i] != NULL; i++)
    g_ptr_array_add (thumbs_dirs, g_build_filename (data_dirs[i], "thumbnailers", NULL));
  g_ptr_array_add (thumbs_dirs, NULL);

  return g_ptr_array_free (thumbs_dirs, FALSE);
}

static const gchar * const *
get_thumbnailers_dirs (void)
{
  static GOnce once_init = G_ONCE_INIT;
  return g_once (&once_init, init_thumbnailers_dirs, NULL);
}

/* These should be called with the lock held */
static void
gnome_desktop_thumbnail_factory_register_mime_types (GnomeDesktopThumbnailFactory *factory,
                                                     Thumbnailer                  *thumb)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  gint i;

  for (i = 0; thumb->mime_types[i]; i++)
    {
      if (!g_hash_table_lookup (priv->mime_types_map, thumb->mime_types[i]))
        g_hash_table_insert (priv->mime_types_map,
                             g_strdup (thumb->mime_types[i]),
                             thumbnailer_ref (thumb));
    }
}

static void
gnome_desktop_thumbnail_factory_add_thumbnailer (GnomeDesktopThumbnailFactory *factory,
                                                 Thumbnailer                  *thumb)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;

  gnome_desktop_thumbnail_factory_register_mime_types (factory, thumb);
  priv->thumbnailers = g_list_prepend (priv->thumbnailers, thumb);
}

static gboolean
gnome_desktop_thumbnail_factory_is_disabled (GnomeDesktopThumbnailFactory *factory,
                                             const gchar                  *mime_type)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  guint i;

  if (priv->disabled)
    return TRUE;

  if (!priv->disabled_types)
    return FALSE;

  for (i = 0; priv->disabled_types[i]; i++)
    {
      if (g_strcmp0 (priv->disabled_types[i], mime_type) == 0)
        return TRUE;
    }

  return FALSE;
}

static gboolean
remove_thumbnailer_from_mime_type_map (gchar       *key,
                                       Thumbnailer *value,
                                       gchar       *path)
{
  return (strcmp (value->path, path) == 0);
}

static void
update_or_create_thumbnailer (GnomeDesktopThumbnailFactory *factory,
                              const gchar                  *path)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  GList *l;
  Thumbnailer *thumb;
  gboolean found = FALSE;

  g_mutex_lock (&priv->lock);

  for (l = priv->thumbnailers; l && !found; l = g_list_next (l))
    {
      thumb = (Thumbnailer *)l->data;

      if (strcmp (thumb->path, path) == 0)
        {
          found = TRUE;

          /* First remove the mime_types associated to this thumbnailer */
          g_hash_table_foreach_remove (priv->mime_types_map,
                                       (GHRFunc)remove_thumbnailer_from_mime_type_map,
                                       (gpointer)path);
          if (!thumbnailer_reload (thumb))
              priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l);
          else
              gnome_desktop_thumbnail_factory_register_mime_types (factory, thumb);
        }
    }

  if (!found)
    {
      thumb = thumbnailer_new (path);
      if (thumb)
        gnome_desktop_thumbnail_factory_add_thumbnailer (factory, thumb);
    }

  g_mutex_unlock (&priv->lock);
}

static void
remove_thumbnailer (GnomeDesktopThumbnailFactory *factory,
                    const gchar                  *path)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  GList *l;
  Thumbnailer *thumb;

  g_mutex_lock (&priv->lock);

  for (l = priv->thumbnailers; l; l = g_list_next (l))
    {
      thumb = (Thumbnailer *)l->data;

      if (strcmp (thumb->path, path) == 0)
        {
          priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l);
          g_hash_table_foreach_remove (priv->mime_types_map,
                                       (GHRFunc)remove_thumbnailer_from_mime_type_map,
                                       (gpointer)path);
          thumbnailer_unref (thumb);

          break;
        }
    }

  g_mutex_unlock (&priv->lock);
}

static void
remove_thumbnailers_for_dir (GnomeDesktopThumbnailFactory *factory,
                             const gchar                  *thumbnailer_dir,
                             GFileMonitor                 *monitor)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  GList *l;
  Thumbnailer *thumb;

  g_mutex_lock (&priv->lock);

  /* Remove all the thumbnailers inside this @thumbnailer_dir. */
  for (l = priv->thumbnailers; l; l = g_list_next (l))
    {
      thumb = (Thumbnailer *)l->data;

      if (g_str_has_prefix (thumb->path, thumbnailer_dir) == TRUE)
        {
          priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l);
          g_hash_table_foreach_remove (priv->mime_types_map,
                                       (GHRFunc)remove_thumbnailer_from_mime_type_map,
                                       (gpointer)thumb->path);
          thumbnailer_unref (thumb);

          break;
        }
    }

  /* Remove the monitor for @thumbnailer_dir. */
  priv->monitors = g_list_remove (priv->monitors, monitor);
  g_signal_handlers_disconnect_by_func (monitor, thumbnailers_directory_changed, factory);

  g_mutex_unlock (&priv->lock);
}

static void
gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (GnomeDesktopThumbnailFactory *factory,
                                                           const gchar                  *path)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  GDir *dir;
  GFile *dir_file;
  GFileMonitor *monitor;
  const gchar *dirent;

  dir = g_dir_open (path, 0, NULL);
  if (!dir)
      return;

  /* Monitor dir */
  dir_file = g_file_new_for_path (path);
  monitor = g_file_monitor_directory (dir_file,
                                      G_FILE_MONITOR_NONE,
                                      NULL, NULL);
  if (monitor)
    {
      g_signal_connect (monitor, "changed",
                        G_CALLBACK (thumbnailers_directory_changed),
                        factory);
      priv->monitors = g_list_prepend (priv->monitors, monitor);
    }
  g_object_unref (dir_file);

  while ((dirent = g_dir_read_name (dir)))
    {
      Thumbnailer *thumb;
      gchar       *filename;

      if (!g_str_has_suffix (dirent, THUMBNAILER_EXTENSION))
          continue;

      filename = g_build_filename (path, dirent, NULL);
      thumb = thumbnailer_new (filename);
      g_free (filename);

      if (thumb)
          gnome_desktop_thumbnail_factory_add_thumbnailer (factory, thumb);
    }

  g_dir_close (dir);
}

static void
thumbnailers_directory_changed (GFileMonitor                 *monitor,
                                GFile                        *file,
                                GFile                        *other_file,
                                GFileMonitorEvent             event_type,
                                GnomeDesktopThumbnailFactory *factory)
{
  gchar *path;

  switch (event_type)
    {
    case G_FILE_MONITOR_EVENT_CREATED:
    case G_FILE_MONITOR_EVENT_CHANGED:
    case G_FILE_MONITOR_EVENT_DELETED:
      path = g_file_get_path (file);
      if (!g_str_has_suffix (path, THUMBNAILER_EXTENSION))
        {
          g_free (path);
          return;
        }

      if (event_type == G_FILE_MONITOR_EVENT_DELETED)
        remove_thumbnailer (factory, path);
      else
        update_or_create_thumbnailer (factory, path);

      g_free (path);
      break;
    case G_FILE_MONITOR_EVENT_UNMOUNTED:
    case G_FILE_MONITOR_EVENT_MOVED:
      path = g_file_get_path (file);
      remove_thumbnailers_for_dir (factory, path, monitor);

      if (event_type == G_FILE_MONITOR_EVENT_MOVED)
          gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (factory, path);

      g_free (path);
      break;
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
    case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
    case G_FILE_MONITOR_EVENT_RENAMED:
    case G_FILE_MONITOR_EVENT_MOVED_IN:
    case G_FILE_MONITOR_EVENT_MOVED_OUT:
    default:
      break;
    }
}

static void
gnome_desktop_thumbnail_factory_load_thumbnailers (GnomeDesktopThumbnailFactory *factory)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;
  const gchar * const *dirs;
  guint i;

  if (priv->loaded)
    return;

  dirs = get_thumbnailers_dirs ();
  for (i = 0; dirs[i]; i++)
    {
      gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (factory, dirs[i]);
    }

  priv->loaded = TRUE;
}

static void
external_thumbnailers_disabled_all_changed_cb (GSettings                    *settings,
                                               const gchar                  *key,
                                               GnomeDesktopThumbnailFactory *factory)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;

  g_mutex_lock (&priv->lock);

  priv->disabled = g_settings_get_boolean (priv->settings, "disable-all");
  if (priv->disabled)
    {
      g_strfreev (priv->disabled_types);
      priv->disabled_types = NULL;
    }
  else
    {
      priv->disabled_types = g_settings_get_strv (priv->settings, "disable");
      gnome_desktop_thumbnail_factory_load_thumbnailers (factory);
    }

  g_mutex_unlock (&priv->lock);
}

static void
external_thumbnailers_disabled_changed_cb (GSettings                    *settings,
                                           const gchar                  *key,
                                           GnomeDesktopThumbnailFactory *factory)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;

  g_mutex_lock (&priv->lock);

  if (!priv->disabled)
    {
      g_strfreev (priv->disabled_types);
      priv->disabled_types = g_settings_get_strv (priv->settings, "disable");
    }

  g_mutex_unlock (&priv->lock);
}

static void
gnome_desktop_thumbnail_factory_init (GnomeDesktopThumbnailFactory *factory)
{
  GnomeDesktopThumbnailFactoryPrivate *priv;

  factory->priv = gnome_desktop_thumbnail_factory_get_instance_private (factory);

  priv = factory->priv;

  priv->size = GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL;

  priv->mime_types_map = g_hash_table_new_full (g_str_hash,
                                                g_str_equal,
                                                (GDestroyNotify)g_free,
                                                (GDestroyNotify)thumbnailer_unref);

  g_mutex_init (&priv->lock);

  priv->settings = g_settings_new ("io.github.scarecrow_de.desktop.thumbnailers");
  priv->disabled = g_settings_get_boolean (priv->settings, "disable-all");
  if (!priv->disabled)
    priv->disabled_types = g_settings_get_strv (priv->settings, "disable");
  g_signal_connect (priv->settings, "changed::disable-all",
                    G_CALLBACK (external_thumbnailers_disabled_all_changed_cb),
                    factory);
  g_signal_connect (priv->settings, "changed::disable",
                    G_CALLBACK (external_thumbnailers_disabled_changed_cb),
                    factory);

  if (!priv->disabled)
    gnome_desktop_thumbnail_factory_load_thumbnailers (factory);
}

static void
gnome_desktop_thumbnail_factory_finalize (GObject *object)
{
  GnomeDesktopThumbnailFactory *factory;
  GnomeDesktopThumbnailFactoryPrivate *priv;

  factory = GNOME_DESKTOP_THUMBNAIL_FACTORY (object);

  priv = factory->priv;

  if (priv->thumbnailers)
    {
      g_list_free_full (priv->thumbnailers, (GDestroyNotify)thumbnailer_unref);
      priv->thumbnailers = NULL;
    }

  g_clear_pointer (&priv->mime_types_map, g_hash_table_destroy);

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

  g_mutex_clear (&priv->lock);

  g_clear_pointer (&priv->disabled_types, g_strfreev);

  if (priv->settings)
    {
      g_signal_handlers_disconnect_by_func (priv->settings,
                                            external_thumbnailers_disabled_all_changed_cb,
                                            factory);
      g_signal_handlers_disconnect_by_func (priv->settings,
                                            external_thumbnailers_disabled_changed_cb,
                                            factory);
      g_clear_object (&priv->settings);
    }

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

static void
gnome_desktop_thumbnail_factory_class_init (GnomeDesktopThumbnailFactoryClass *class)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (class);

  gobject_class->finalize = gnome_desktop_thumbnail_factory_finalize;
}

/**
 * gnome_desktop_thumbnail_factory_new:
 * @size: The thumbnail size to use
 *
 * Creates a new #GnomeDesktopThumbnailFactory.
 *
 * This function must be called on the main thread and is non-blocking.
 *
 * Return value: a new #GnomeDesktopThumbnailFactory
 *
 * Since: 2.2
 **/
GnomeDesktopThumbnailFactory *
gnome_desktop_thumbnail_factory_new (GnomeDesktopThumbnailSize size)
{
  GnomeDesktopThumbnailFactory *factory;

  factory = g_object_new (GNOME_DESKTOP_TYPE_THUMBNAIL_FACTORY, NULL);

  factory->priv->size = size;

  return factory;
}

static char *
thumbnail_filename (const char *uri)
{
  GChecksum *checksum;
  guint8 digest[16];
  gsize digest_len = sizeof (digest);
  char *file;

  checksum = g_checksum_new (G_CHECKSUM_MD5);
  g_checksum_update (checksum, (const guchar *) uri, strlen (uri));

  g_checksum_get_digest (checksum, digest, &digest_len);
  g_assert (digest_len == 16);

  file = g_strconcat (g_checksum_get_string (checksum), ".png", NULL);

  g_checksum_free (checksum);

  return file;
}

static char *
thumbnail_path (const char                *uri,
                GnomeDesktopThumbnailSize  size)
{
  char *path, *file;

  file = thumbnail_filename (uri);
  path = g_build_filename (g_get_user_cache_dir (),
                           "thumbnails",
                           size == GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE ? "large" : "normal",
                           file,
                           NULL);
  g_free (file);
  return path;
}

static char *
thumbnail_failed_path (const char *uri)
{
  char *path, *file;

  file = thumbnail_filename (uri);
  /* XXX: appname is only used for failed thumbnails. Is this a mistake? */
  path = g_build_filename (g_get_user_cache_dir (),
                           "thumbnails",
                           "fail",
                           appname,
                           file,
                           NULL);
  g_free (file);
  return path;
}

static char *
validate_thumbnail_path (char                      *path,
                         const char                *uri,
                         time_t                     mtime,
                         GnomeDesktopThumbnailSize  size)
{
  GdkPixbuf *pixbuf;

  pixbuf = gdk_pixbuf_new_from_file (path, NULL);
  if (pixbuf == NULL ||
      !gnome_desktop_thumbnail_is_valid (pixbuf, uri, mtime)) {
      g_free (path);
      return NULL;
  }

  g_clear_object (&pixbuf);

  return path;
}

static char *
lookup_thumbnail_path (const char                *uri,
                       time_t                     mtime,
                       GnomeDesktopThumbnailSize  size)
{
  char *path = thumbnail_path (uri, size);
  return validate_thumbnail_path (path, uri, mtime, size);
}

static char *
lookup_failed_thumbnail_path (const char                *uri,
                              time_t                     mtime,
                              GnomeDesktopThumbnailSize  size)
{
  char *path = thumbnail_failed_path (uri);
  return validate_thumbnail_path (path, uri, mtime, size);
}

/**
 * gnome_desktop_thumbnail_factory_lookup:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @uri: the uri of a file
 * @mtime: the mtime of the file
 *
 * Tries to locate an existing thumbnail for the file specified.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Return value: The absolute path of the thumbnail, or %NULL if none exist.
 *
 * Since: 2.2
 **/
char *
gnome_desktop_thumbnail_factory_lookup (GnomeDesktopThumbnailFactory *factory,
					const char            *uri,
					time_t                 mtime)
{
  GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv;

  g_return_val_if_fail (uri != NULL, NULL);

  return lookup_thumbnail_path (uri, mtime, priv->size);
}

/**
 * gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @uri: the uri of a file
 * @mtime: the mtime of the file
 *
 * Tries to locate an failed thumbnail for the file specified. Writing
 * and looking for failed thumbnails is important to avoid to try to
 * thumbnail e.g. broken images several times.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Return value: TRUE if there is a failed thumbnail for the file.
 *
 * Since: 2.2
 **/
gboolean
gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail (GnomeDesktopThumbnailFactory *factory,
							    const char            *uri,
							    time_t                 mtime)
{
  char *path;

  g_return_val_if_fail (uri != NULL, FALSE);

  path = lookup_failed_thumbnail_path (uri, mtime, factory->priv->size);
  if (path == NULL)
    return FALSE;

  g_free (path);

  return TRUE;
}

/**
 * gnome_desktop_thumbnail_factory_can_thumbnail:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @uri: the uri of a file
 * @mime_type: the mime type of the file
 * @mtime: the mtime of the file
 *
 * Returns TRUE if this GnomeDesktopThumbnailFactory can (at least try) to thumbnail
 * this file. Thumbnails or files with failed thumbnails won't be thumbnailed.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Return value: TRUE if the file can be thumbnailed.
 *
 * Since: 2.2
 **/
gboolean
gnome_desktop_thumbnail_factory_can_thumbnail (GnomeDesktopThumbnailFactory *factory,
					       const char            *uri,
					       const char            *mime_type,
					       time_t                 mtime)
{
  gboolean have_script = FALSE;

  /* Don't thumbnail thumbnails */
  if (uri &&
      strncmp (uri, "file:/", 6) == 0 &&
      strstr (uri, "/thumbnails/") != NULL)
    return FALSE;

  if (!mime_type)
    return FALSE;

  g_mutex_lock (&factory->priv->lock);
  if (!gnome_desktop_thumbnail_factory_is_disabled (factory, mime_type))
    {
      Thumbnailer *thumb;

      thumb = g_hash_table_lookup (factory->priv->mime_types_map, mime_type);
      have_script = (thumb != NULL);
    }
  g_mutex_unlock (&factory->priv->lock);

  if (have_script)
    {
      return !gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail (factory,
                                                                          uri,
                                                                          mtime);
    }

  return FALSE;
}

static GdkPixbuf *
get_preview_thumbnail (const char *uri,
                       int         size)
{
    GdkPixbuf *pixbuf;
    GFile *file;
    GFileInfo *file_info;
    GInputStream *input_stream;
    GObject *object;

    g_return_val_if_fail (uri != NULL, NULL);

    input_stream = NULL;

    file = g_file_new_for_uri (uri);

    /* First see if we can get an input stream via preview::icon  */
    file_info = g_file_query_info (file,
                                   G_FILE_ATTRIBUTE_PREVIEW_ICON,
                                   G_FILE_QUERY_INFO_NONE,
                                   NULL,  /* GCancellable */
                                   NULL); /* return location for GError */
    g_object_unref (file);

    if (file_info == NULL)
      return NULL;

    object = g_file_info_get_attribute_object (file_info,
                                               G_FILE_ATTRIBUTE_PREVIEW_ICON);
    if (object)
        g_object_ref (object);
    g_object_unref (file_info);

    if (!object)
      return NULL;
    if (!G_IS_LOADABLE_ICON (object)) {
      g_object_unref (object);
      return NULL;
    }

    input_stream = g_loadable_icon_load (G_LOADABLE_ICON (object),
                                         0,     /* size */
                                         NULL,  /* return location for type */
                                         NULL,  /* GCancellable */
                                         NULL); /* return location for GError */
    g_object_unref (object);

    if (!input_stream)
      return NULL;

    pixbuf = gdk_pixbuf_new_from_stream_at_scale (input_stream,
                                                  size, size,
                                                  TRUE, NULL, NULL);
    g_object_unref (input_stream);

    return pixbuf;
}

static GdkPixbuf *
pixbuf_new_from_bytes (GBytes  *bytes,
		       GError **error)
{
  g_autoptr(GdkPixbufLoader) loader = NULL;

  loader = gdk_pixbuf_loader_new_with_mime_type ("image/png", error);
  if (!loader)
    return NULL;

  if (!gdk_pixbuf_loader_write (loader,
				g_bytes_get_data (bytes, NULL),
				g_bytes_get_size (bytes),
				error))
    {
      return NULL;
    }

  if (!gdk_pixbuf_loader_close (loader, error))
    return NULL;

  return g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader));
}

/**
 * gnome_desktop_thumbnail_factory_generate_thumbnail:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @uri: the uri of a file
 * @mime_type: the mime type of the file
 *
 * Tries to generate a thumbnail for the specified file. If it succeeds
 * it returns a pixbuf that can be used as a thumbnail.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Return value: (transfer full): thumbnail pixbuf if thumbnailing succeeded, %NULL otherwise.
 *
 * Since: 2.2
 **/
GdkPixbuf *
gnome_desktop_thumbnail_factory_generate_thumbnail (GnomeDesktopThumbnailFactory *factory,
						    const char            *uri,
						    const char            *mime_type)
{
  GdkPixbuf *pixbuf;
  char *script;
  int size;

  g_return_val_if_fail (uri != NULL, NULL);
  g_return_val_if_fail (mime_type != NULL, NULL);

  /* Doesn't access any volatile fields in factory, so it's threadsafe */

  size = 128;
  if (factory->priv->size == GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE)
    size = 256;

  pixbuf = get_preview_thumbnail (uri, size);
  if (pixbuf != NULL)
    return pixbuf;

  script = NULL;
  g_mutex_lock (&factory->priv->lock);
  if (!gnome_desktop_thumbnail_factory_is_disabled (factory, mime_type))
    {
      Thumbnailer *thumb;

      thumb = g_hash_table_lookup (factory->priv->mime_types_map, mime_type);
      if (thumb)
        script = g_strdup (thumb->command);
    }
  g_mutex_unlock (&factory->priv->lock);

  if (script)
    {
      GBytes *data;
      GError *error = NULL;

      data = gnome_desktop_thumbnail_script_exec (script, size, uri, &error);
      if (data)
        {
          pixbuf = pixbuf_new_from_bytes (data, &error);
          if (!pixbuf)
            {
              g_debug ("Could not load thumbnail pixbuf: %s", error->message);
              g_error_free (error);
            }
          g_bytes_unref (data);
        }
      else
        {
          g_debug ("Thumbnail script ('%s') failed for '%s': %s",
                   script, uri, error ? error->message : "no details");
          g_clear_error (&error);
        }
    }
  else
    {
      g_debug ("Could not find thumbnailer for mime-type '%s'",
               mime_type);
    }

  g_free (script);

  return pixbuf;
}

static gboolean
save_thumbnail (GdkPixbuf  *pixbuf,
                char       *path,
                const char *uri,
                time_t      mtime)
{
  char *dirname;
  char *tmp_path = NULL;
  int tmp_fd;
  char mtime_str[21];
  gboolean ret = FALSE;
  GError *error = NULL;
  const char *width, *height;

  if (pixbuf == NULL)
    return FALSE;

  dirname = g_path_get_dirname (path);

  if (g_mkdir_with_parents (dirname, 0700) != 0)
    goto out;

  tmp_path = g_strconcat (path, ".XXXXXX", NULL);
  tmp_fd = g_mkstemp (tmp_path);

  if (tmp_fd == -1)
    goto out;
  close (tmp_fd);

  g_snprintf (mtime_str, 21, "%" G_GINT64_FORMAT, (gint64) mtime);
  width = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Width");
  height = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Height");

  error = NULL;
  if (width != NULL && height != NULL)
    ret = gdk_pixbuf_save (pixbuf,
			   tmp_path,
			   "png", &error,
			   "tEXt::Thumb::Image::Width", width,
			   "tEXt::Thumb::Image::Height", height,
			   "tEXt::Thumb::URI", uri,
			   "tEXt::Thumb::MTime", mtime_str,
			   "tEXt::Software", "GNOME::ThumbnailFactory",
			   NULL);
  else
    ret = gdk_pixbuf_save (pixbuf,
			   tmp_path,
			   "png", &error,
			   "tEXt::Thumb::URI", uri,
			   "tEXt::Thumb::MTime", mtime_str,
			   "tEXt::Software", "GNOME::ThumbnailFactory",
			   NULL);

  if (!ret)
    goto out;

  g_chmod (tmp_path, 0600);
  g_rename (tmp_path, path);

 out:
  if (error != NULL)
    {
      g_warning ("Failed to create thumbnail %s: %s", tmp_path, error->message);
      g_error_free (error);
    }
  g_unlink (tmp_path);
  g_free (tmp_path);
  g_free (dirname);
  return ret;
}

static GdkPixbuf *
make_failed_thumbnail (void)
{
  GdkPixbuf *pixbuf;

  pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, 1, 1);
  gdk_pixbuf_fill (pixbuf, 0x00000000);
  return pixbuf;
}

/**
 * gnome_desktop_thumbnail_factory_save_thumbnail:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @thumbnail: the thumbnail as a pixbuf
 * @uri: the uri of a file
 * @original_mtime: the modification time of the original file
 *
 * Saves @thumbnail at the right place. If the save fails a
 * failed thumbnail is written.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Since: 2.2
 **/
void
gnome_desktop_thumbnail_factory_save_thumbnail (GnomeDesktopThumbnailFactory *factory,
						GdkPixbuf             *thumbnail,
						const char            *uri,
						time_t                 original_mtime)
{
  char *path;

  path = thumbnail_path (uri, factory->priv->size);
  if (!save_thumbnail (thumbnail, path, uri, original_mtime))
    {
      thumbnail = make_failed_thumbnail ();
      g_free (path);
      path = thumbnail_failed_path (uri);
      save_thumbnail (thumbnail, path, uri, original_mtime);
      g_object_unref (thumbnail);
    }
  g_free (path);
}

/**
 * gnome_desktop_thumbnail_factory_create_failed_thumbnail:
 * @factory: a #GnomeDesktopThumbnailFactory
 * @uri: the uri of a file
 * @mtime: the modification time of the file
 *
 * Creates a failed thumbnail for the file so that we don't try
 * to re-thumbnail the file later.
 *
 * Usage of this function is threadsafe and does blocking I/O.
 *
 * Since: 2.2
 **/
void
gnome_desktop_thumbnail_factory_create_failed_thumbnail (GnomeDesktopThumbnailFactory *factory,
							 const char            *uri,
							 time_t                 mtime)
{
  char *path;
  GdkPixbuf *pixbuf;

  path = thumbnail_failed_path (uri);
  pixbuf = make_failed_thumbnail ();
  save_thumbnail (pixbuf, path, uri, mtime);
  g_free (path);
  g_object_unref (pixbuf);
}

/**
 * gnome_desktop_thumbnail_path_for_uri:
 * @uri: an uri
 * @size: a thumbnail size
 *
 * Returns the filename that a thumbnail of size @size for @uri would have.
 * This function is threadsafe and does no blocking I/O.
 *
 * Return value: an absolute filename
 *
 * Since: 2.2
 **/
char *
gnome_desktop_thumbnail_path_for_uri (const char         *uri,
				      GnomeDesktopThumbnailSize  size)
{
  return thumbnail_path (uri, size);
}

/**
 * gnome_desktop_thumbnail_is_valid:
 * @pixbuf: an loaded thumbnail #GdkPixbuf
 * @uri: a uri
 * @mtime: the mtime
 *
 * Returns whether the thumbnail has the correct uri and mtime embedded in the
 * png options. This function is threadsafe and does no blocking I/O.
 *
 * Return value: TRUE if the thumbnail has the right @uri and @mtime
 *
 * Since: 2.2
 **/
gboolean
gnome_desktop_thumbnail_is_valid (GdkPixbuf          *pixbuf,
				  const char         *uri,
				  time_t              mtime)
{
  const char *thumb_uri, *thumb_mtime_str;
  time_t thumb_mtime;

  thumb_uri = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::URI");
  if (g_strcmp0 (uri, thumb_uri) != 0)
    return FALSE;

  thumb_mtime_str = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::MTime");
  if (!thumb_mtime_str)
    return FALSE;
  thumb_mtime = atol (thumb_mtime_str);
  if (mtime != thumb_mtime)
    return FALSE;

  return TRUE;
}
