/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * st-texture-cache.h: Object for loading and caching images as textures
 *
 * Copyright 2009, 2010 Red Hat, Inc.
 * Copyright 2010, Maxim Ermilov
 *
 * This program 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope 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 program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "st-image-content-private.h"
#include "st-texture-cache.h"
#include "st-private.h"
#include "st-settings.h"
#include "st-icon-theme.h"
#include <math.h>
#include <string.h>
#include <glib.h>

#define CACHE_PREFIX_ICON "icon:"
#define CACHE_PREFIX_FILE "file:"
#define CACHE_PREFIX_FILE_FOR_CAIRO "file-for-cairo:"

typedef struct _StTextureCache
{
  GObject parent;

  StIconTheme *icon_theme;

  /* Things that were loaded with a cache policy != NONE */
  GHashTable *keyed_cache; /* char * -> StImageContent* */
  GHashTable *keyed_surface_cache; /* char * -> cairo_surface_t* */

  GHashTable *used_scales; /* Set: double */

  /* Presently this is used to de-duplicate requests for GIcons and async URIs. */
  GHashTable *outstanding_requests; /* char * -> AsyncTextureLoadData * */

  /* File monitors to evict cache data on changes */
  GHashTable *file_monitors; /* char * -> GFileMonitor * */

  GCancellable *cancellable;
} StTextureCache;

static void st_texture_cache_dispose (GObject *object);
static void st_texture_cache_finalize (GObject *object);

enum
{
  ICON_THEME_CHANGED,
  TEXTURE_FILE_CHANGED,

  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0, };
G_DEFINE_FINAL_TYPE (StTextureCache, st_texture_cache, G_TYPE_OBJECT);

/* We want to preserve the aspect ratio by default, also the default
 * pipeline for an empty texture is full opacity white, which we
 * definitely don't want.  Skip that by setting 0 opacity.
 */
static ClutterActor *
create_invisible_actor (void)
{
  return g_object_new (CLUTTER_TYPE_ACTOR,
                       "opacity", 0,
                       "request-mode", CLUTTER_REQUEST_CONTENT_SIZE,
                       NULL);
}

/* Reverse the opacity we added while loading */
static void
set_content_from_image (ClutterActor   *actor,
                        ClutterContent *image)
{
  g_assert (image && ST_IS_IMAGE_CONTENT (image));

  clutter_actor_set_content (actor, image);
  clutter_actor_set_opacity (actor, 255);
}

static void
st_texture_cache_class_init (StTextureCacheClass *klass)
{
  GObjectClass *gobject_class = (GObjectClass *)klass;

  gobject_class->dispose = st_texture_cache_dispose;
  gobject_class->finalize = st_texture_cache_finalize;

  /**
   * StTextureCache::icon-theme-changed:
   * @self: a #StTextureCache
   *
   * Emitted when the icon theme is changed.
   */
  signals[ICON_THEME_CHANGED] =
    g_signal_new ("icon-theme-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, /* no default handler slot */
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  /**
   * StTextureCache::texture-file-changed:
   * @self: a #StTextureCache
   * @file: a #GFile
   *
   * Emitted when the source file of a texture is changed.
   */
  signals[TEXTURE_FILE_CHANGED] =
    g_signal_new ("texture-file-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, /* no default handler slot */
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1, G_TYPE_FILE);
}

/* Evicts all cached textures for named icons */
static void
st_texture_cache_evict_icons (StTextureCache *cache)
{
  GHashTableIter iter;
  gpointer key;
  gpointer value;

  g_hash_table_iter_init (&iter, cache->keyed_cache);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      const char *cache_key = key;

      /* This is too conservative - it takes out all cached textures
       * for GIcons even when they aren't named icons, but it's not
       * worth the complexity of parsing the key and calling
       * g_icon_new_for_string(); icon theme changes aren't normal */
      if (g_str_has_prefix (cache_key, CACHE_PREFIX_ICON))
        g_hash_table_iter_remove (&iter);
    }
}

static void
on_icon_theme_changed (StIconTheme    *icon_theme,
                       StTextureCache *self)
{
  st_texture_cache_evict_icons (self);
  g_signal_emit (self, signals[ICON_THEME_CHANGED], 0);
}

static void
st_texture_cache_init (StTextureCache *self)
{
  self->icon_theme = st_icon_theme_new ();
  st_icon_theme_add_resource_path (self->icon_theme,
                                   "/org/gnome/shell/icons");
  g_signal_connect (self->icon_theme, "changed",
                    G_CALLBACK (on_icon_theme_changed), self);

  self->keyed_cache = g_hash_table_new_full (g_str_hash, g_str_equal,
                                             g_free, g_object_unref);
  self->keyed_surface_cache = g_hash_table_new_full (g_str_hash,
                                                     g_str_equal,
                                                     g_free,
                                                     (GDestroyNotify) cairo_surface_destroy);
  self->used_scales = g_hash_table_new_full (g_double_hash, g_double_equal,
                                             g_free, NULL);
  self->outstanding_requests = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                      g_free, NULL);
  self->file_monitors = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal,
                                               g_object_unref, g_object_unref);

  self->cancellable = g_cancellable_new ();
}

static void
st_texture_cache_dispose (GObject *object)
{
  StTextureCache *self = (StTextureCache*)object;

  g_cancellable_cancel (self->cancellable);

  g_clear_object (&self->icon_theme);
  g_clear_object (&self->cancellable);

  g_clear_pointer (&self->keyed_cache, g_hash_table_destroy);
  g_clear_pointer (&self->keyed_surface_cache, g_hash_table_destroy);
  g_clear_pointer (&self->used_scales, g_hash_table_destroy);
  g_clear_pointer (&self->outstanding_requests, g_hash_table_destroy);
  g_clear_pointer (&self->file_monitors, g_hash_table_destroy);

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

static void
st_texture_cache_finalize (GObject *object)
{
  G_OBJECT_CLASS (st_texture_cache_parent_class)->finalize (object);
}

static void
compute_pixbuf_scale (gint      width,
                      gint      height,
                      gint      available_width,
                      gint      available_height,
                      gint     *new_width,
                      gint     *new_height)
{
  int scaled_width, scaled_height;

  if (width == 0 || height == 0)
    {
      *new_width = *new_height = 0;
      return;
    }

  if (available_width >= 0 && available_height >= 0)
    {
      /* This should keep the aspect ratio of the image intact, because if
       * available_width < (available_height * width) / height
       * then
       * (available_width * height) / width < available_height
       * So we are guaranteed to either scale the image to have an available_width
       * for width and height scaled accordingly OR have the available_height
       * for height and width scaled accordingly, whichever scaling results
       * in the image that can fit both available dimensions.
       */
      scaled_width = MIN (available_width, (available_height * width) / height);
      scaled_height = MIN (available_height, (available_width * height) / width);
    }
  else if (available_width >= 0)
    {
      scaled_width = available_width;
      scaled_height = (available_width * height) / width;
    }
  else if (available_height >= 0)
    {
      scaled_width = (available_height * width) / height;
      scaled_height = available_height;
    }
  else
    {
      scaled_width = scaled_height = 0;
    }

  /* Scale the image only if that will not increase its original dimensions. */
  if (scaled_width > 0 && scaled_height > 0 && scaled_width < width && scaled_height < height)
    {
      *new_width = scaled_width;
      *new_height = scaled_height;
    }
  else
    {
      *new_width = width;
      *new_height = height;
    }
}

/* A private structure for keeping width, height and scale. */
typedef struct {
  int width;
  int height;
  int scale;
} Dimensions;

/* This struct corresponds to a request for an texture.
 * It's creasted when something needs a new texture,
 * and destroyed when the texture data is loaded. */
typedef struct {
  StTextureCache *cache;
  StTextureCachePolicy policy;
  char *key;

  guint width;
  guint height;
  guint paint_scale;
  gfloat resource_scale;
  GSList *actors;

  StIconInfo *icon_info;
  StIconColors *colors;
  GFile *file;
  CoglContext *cogl_context;
} AsyncTextureLoadData;

static void
texture_load_data_free (gpointer p)
{
  AsyncTextureLoadData *data = p;

  g_clear_object (&data->icon_info);
  g_clear_pointer (&data->colors, st_icon_colors_unref);
  g_clear_object (&data->file);
  g_clear_pointer (&data->key, g_free);

  if (data->actors)
    g_slist_free_full (data->actors, (GDestroyNotify) g_object_unref);

  g_free (data);
}

/**
 * on_image_size_prepared:
 * @pixbuf_loader: #GdkPixbufLoader loading the image
 * @width: the original width of the image
 * @height: the original height of the image
 * @data: pointer to the #Dimensions structure containing available width and height for the image,
 *        available width or height can be -1 if the dimension is not limited
 *
 * Private function.
 *
 * Sets the size of the image being loaded to fit the available width and height dimensions,
 * but never scales up the image beyond its actual size.
 * Intended to be used as a callback for #GdkPixbufLoader "size-prepared" signal.
 */
static void
on_image_size_prepared (GdkPixbufLoader *pixbuf_loader,
                        gint             width,
                        gint             height,
                        gpointer         data)
{
  Dimensions *available_dimensions = data;
  int available_width = available_dimensions->width;
  int available_height = available_dimensions->height;
  int scale_factor = available_dimensions->scale;
  int scaled_width;
  int scaled_height;

  compute_pixbuf_scale (width, height, available_width, available_height,
                        &scaled_width, &scaled_height);

  gdk_pixbuf_loader_set_size (pixbuf_loader,
                              scaled_width * scale_factor,
                              scaled_height * scale_factor);
}

static GdkPixbuf *
impl_load_pixbuf_data (const guchar   *data,
                       gsize           size,
                       int             available_width,
                       int             available_height,
                       int             scale,
                       GError        **error)
{
  GdkPixbufLoader *pixbuf_loader = NULL;
  GdkPixbuf *rotated_pixbuf = NULL;
  GdkPixbuf *pixbuf;
  gboolean success;
  Dimensions available_dimensions;
  int width_before_rotation, width_after_rotation;

  pixbuf_loader = gdk_pixbuf_loader_new ();

  available_dimensions.width = available_width;
  available_dimensions.height = available_height;
  available_dimensions.scale = scale;
  g_signal_connect (pixbuf_loader, "size-prepared",
                    G_CALLBACK (on_image_size_prepared), &available_dimensions);

  success = gdk_pixbuf_loader_write (pixbuf_loader, data, size, error);
  if (!success)
    goto out;
  success = gdk_pixbuf_loader_close (pixbuf_loader, error);
  if (!success)
    goto out;

  pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader);

  width_before_rotation = gdk_pixbuf_get_width (pixbuf);

  rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf);
  width_after_rotation = gdk_pixbuf_get_width (rotated_pixbuf);

  /* There is currently no way to tell if the pixbuf will need to be rotated before it is loaded,
   * so we only check that once it is loaded, and reload it again if it needs to be rotated in order
   * to use the available width and height correctly.
   * See http://bugzilla.gnome.org/show_bug.cgi?id=579003
   */
  if (width_before_rotation != width_after_rotation)
    {
      g_object_unref (pixbuf_loader);
      g_object_unref (rotated_pixbuf);
      rotated_pixbuf = NULL;

      pixbuf_loader = gdk_pixbuf_loader_new ();

      /* We know that the image will later be rotated, so we reverse the available dimensions. */
      available_dimensions.width = available_height;
      available_dimensions.height = available_width;
      available_dimensions.scale = scale;
      g_signal_connect (pixbuf_loader, "size-prepared",
                        G_CALLBACK (on_image_size_prepared), &available_dimensions);

      success = gdk_pixbuf_loader_write (pixbuf_loader, data, size, error);
      if (!success)
        goto out;

      success = gdk_pixbuf_loader_close (pixbuf_loader, error);
      if (!success)
        goto out;

      pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader);

      rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf);
    }

out:
  if (pixbuf_loader)
    g_object_unref (pixbuf_loader);
  return rotated_pixbuf;
}

static GdkPixbuf *
impl_load_pixbuf_file (GFile          *file,
                       int             available_width,
                       int             available_height,
                       int             paint_scale,
                       float           resource_scale,
                       GError        **error)
{
  GdkPixbuf *pixbuf = NULL;
  char *contents = NULL;
  gsize size;

  if (g_file_load_contents (file, NULL, &contents, &size, NULL, error))
    {
      int scale = ceilf (paint_scale * resource_scale);
      pixbuf = impl_load_pixbuf_data ((const guchar *) contents, size,
                                      available_width, available_height,
                                      scale,
                                      error);
    }

  g_free (contents);

  return pixbuf;
}

static void
load_pixbuf_thread (GTask        *result,
                    gpointer      source,
                    gpointer      task_data,
                    GCancellable *cancellable)
{
  GdkPixbuf *pixbuf;
  AsyncTextureLoadData *data = task_data;
  GError *error = NULL;

  g_assert (data != NULL);
  g_assert (data->file != NULL);

  pixbuf = impl_load_pixbuf_file (data->file, data->width, data->height,
                                  data->paint_scale, data->resource_scale,
                                  &error);

  if (error != NULL)
    g_task_return_error (result, error);
  else if (pixbuf)
    g_task_return_pointer (result, g_object_ref (pixbuf), g_object_unref);

  g_clear_object (&pixbuf);
}

static GdkPixbuf *
load_pixbuf_async_finish (StTextureCache *cache, GAsyncResult *result, GError **error)
{
  return g_task_propagate_pointer (G_TASK (result), error);
}

static ClutterContent *
pixbuf_to_st_content_image (GdkPixbuf    *pixbuf,
                            CoglContext  *context,
                            int           width,
                            int           height,
                            int           paint_scale,
                            float         resource_scale,
                            GError      **error)
{
  ClutterContent *image;
  g_autoptr(GError) local_error = NULL;

  float native_width, native_height;

  native_width = ceilf (gdk_pixbuf_get_width (pixbuf) / resource_scale);
  native_height = ceilf (gdk_pixbuf_get_height (pixbuf) / resource_scale);

  if (width < 0 && height < 0)
    {
      width = native_width;
      height = native_height;
    }
  else if (width < 0)
    {
      height *= paint_scale;
      width = native_width * (height / native_height);
    }
  else if (height < 0)
    {
      width *= paint_scale;
      height = native_height * (width / native_width);
    }
  else
    {
      width *= paint_scale;
      height *= paint_scale;
    }

  image = st_image_content_new_with_preferred_size (width, height);
  st_image_content_set_data (ST_IMAGE_CONTENT (image),
                             context,
                             gdk_pixbuf_get_pixels (pixbuf),
                             gdk_pixbuf_get_has_alpha (pixbuf) ?
                               COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888,
                             gdk_pixbuf_get_width (pixbuf),
                             gdk_pixbuf_get_height (pixbuf),
                             gdk_pixbuf_get_rowstride (pixbuf),
                             &local_error);

  if (local_error)
    {
      g_propagate_error (error, g_steal_pointer (&local_error));
      g_clear_object (&image);
    }

  return image;
}

static void
util_cairo_surface_paint_pixbuf (cairo_surface_t *surface,
                                 const GdkPixbuf *pixbuf)
{
  int width, height;
  guchar *gdk_pixels, *cairo_pixels;
  int gdk_rowstride, cairo_stride;
  int n_channels;
  int j;

  if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
    return;

  /* This function can't just copy any pixbuf to any surface, be
   * sure to read the invariants here before calling it */
  g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE);
  g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24 ||
            cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32);
  g_assert (cairo_image_surface_get_width (surface) == gdk_pixbuf_get_width (pixbuf));
  g_assert (cairo_image_surface_get_height (surface) == gdk_pixbuf_get_height (pixbuf));

  cairo_surface_flush (surface);

  width = gdk_pixbuf_get_width (pixbuf);
  height = gdk_pixbuf_get_height (pixbuf);
  gdk_pixels = gdk_pixbuf_get_pixels (pixbuf);
  gdk_rowstride = gdk_pixbuf_get_rowstride (pixbuf);
  n_channels = gdk_pixbuf_get_n_channels (pixbuf);
  cairo_stride = cairo_image_surface_get_stride (surface);
  cairo_pixels = cairo_image_surface_get_data (surface);

  for (j = height; j; j--)
    {
      guchar *p = gdk_pixels;
      guchar *q = cairo_pixels;

      if (n_channels == 3)
        {
          guchar *end = p + 3 * width;

          while (p < end)
            {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
              q[0] = p[2];
              q[1] = p[1];
              q[2] = p[0];
#else
              q[1] = p[0];
              q[2] = p[1];
              q[3] = p[2];
#endif
              p += 3;
              q += 4;
            }
        }
      else
        {
          guchar *end = p + 4 * width;
          guint t1,t2,t3;

#define MULT(d,c,a,t) G_STMT_START { t = c * a + 0x80; d = ((t >> 8) + t) >> 8; } G_STMT_END

          while (p < end)
            {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
              MULT(q[0], p[2], p[3], t1);
              MULT(q[1], p[1], p[3], t2);
              MULT(q[2], p[0], p[3], t3);
              q[3] = p[3];
#else
              q[0] = p[3];
              MULT(q[1], p[0], p[3], t1);
              MULT(q[2], p[1], p[3], t2);
              MULT(q[3], p[2], p[3], t3);
#endif

              p += 4;
              q += 4;
            }

#undef MULT
        }

      gdk_pixels += gdk_rowstride;
      cairo_pixels += cairo_stride;
    }

  cairo_surface_mark_dirty (surface);
}

static void
util_cairo_set_source_pixbuf (cairo_t         *cr,
                              const GdkPixbuf *pixbuf,
                              gdouble          pixbuf_x,
                              gdouble          pixbuf_y)
{
  cairo_format_t format;
  cairo_surface_t *surface;

  if (gdk_pixbuf_get_n_channels (pixbuf) == 3)
    format = CAIRO_FORMAT_RGB24;
  else
    format = CAIRO_FORMAT_ARGB32;

  surface = cairo_surface_create_similar_image (cairo_get_target (cr),
                                                format,
                                                gdk_pixbuf_get_width (pixbuf),
                                                gdk_pixbuf_get_height (pixbuf));

  util_cairo_surface_paint_pixbuf (surface, pixbuf);

  cairo_set_source_surface (cr, surface, pixbuf_x, pixbuf_y);
  cairo_surface_destroy (surface);
}

static cairo_surface_t *
pixbuf_to_cairo_surface (GdkPixbuf *pixbuf)
{
  cairo_surface_t *dummy_surface;
  cairo_pattern_t *pattern;
  cairo_surface_t *surface;
  cairo_t *cr;

  dummy_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 1, 1);

  cr = cairo_create (dummy_surface);
  util_cairo_set_source_pixbuf (cr, pixbuf, 0, 0);
  pattern = cairo_get_source (cr);
  cairo_pattern_get_surface (pattern, &surface);
  cairo_surface_reference (surface);
  cairo_destroy (cr);
  cairo_surface_destroy (dummy_surface);

  return surface;
}

static void
finish_texture_load (AsyncTextureLoadData *data,
                     GdkPixbuf            *pixbuf)
{
  g_autoptr(ClutterContent) image = NULL;
  GSList *iter;
  StTextureCache *cache;

  cache = data->cache;

  g_hash_table_remove (cache->outstanding_requests, data->key);

  if (pixbuf == NULL)
    goto out;

  if (data->policy != ST_TEXTURE_CACHE_POLICY_NONE)
    {
      gpointer orig_key = NULL, value = NULL;

      if (!g_hash_table_lookup_extended (cache->keyed_cache, data->key,
                                         &orig_key, &value))
        {
          g_autoptr (GError) error = NULL;

          image = pixbuf_to_st_content_image (pixbuf,
                                              data->cogl_context,
                                              data->width, data->height,
                                              data->paint_scale,
                                              data->resource_scale,
                                              &error);
          if (!image)
            {
              g_warning ("Failed to load pixbuf into a content: %s",
                         error->message);
              goto out;
            }

          g_hash_table_insert (cache->keyed_cache, g_strdup (data->key),
                               g_object_ref (image));
        }
      else
        {
          image = g_object_ref (value);
        }
    }
  else
    {
      g_autoptr (GError) error = NULL;

      image = pixbuf_to_st_content_image (pixbuf,
                                          data->cogl_context,
                                          data->width, data->height,
                                          data->paint_scale,
                                          data->resource_scale,
                                          &error);
      if (!image)
        {
          g_warning ("Failed to load pixbuf into a content: %s",
                     error->message);
          goto out;
        }
    }

  if (data->icon_info)
    st_image_content_set_is_symbolic (ST_IMAGE_CONTENT (image),
                                      st_icon_info_is_symbolic (data->icon_info));

  for (iter = data->actors; iter; iter = iter->next)
    {
      ClutterActor *actor = iter->data;
      set_content_from_image (actor, image);
    }

out:
  texture_load_data_free (data);
}

static void
on_symbolic_icon_loaded (GObject      *source,
                         GAsyncResult *result,
                         gpointer      user_data)
{
  GdkPixbuf *pixbuf;
  pixbuf = st_icon_info_load_symbolic_finish (ST_ICON_INFO (source), result, NULL, NULL);
  finish_texture_load (user_data, pixbuf);
  g_clear_object (&pixbuf);
}

static void
on_icon_loaded (GObject      *source,
                GAsyncResult *result,
                gpointer      user_data)
{
  GdkPixbuf *pixbuf;
  pixbuf = st_icon_info_load_icon_finish (ST_ICON_INFO (source), result, NULL);
  finish_texture_load (user_data, pixbuf);
  g_clear_object (&pixbuf);
}

static void
on_pixbuf_loaded (GObject      *source,
                  GAsyncResult *result,
                  gpointer      user_data)
{
  GdkPixbuf *pixbuf;
  pixbuf = load_pixbuf_async_finish (ST_TEXTURE_CACHE (source), result, NULL);
  finish_texture_load (user_data, pixbuf);
  g_clear_object (&pixbuf);
}

static void
load_texture_async (StTextureCache       *cache,
                    AsyncTextureLoadData *data)
{
  if (data->file)
    {
      GTask *task = g_task_new (cache, NULL, on_pixbuf_loaded, data);
      g_task_set_source_tag (task, load_texture_async);
      g_task_set_task_data (task, data, NULL);
      g_task_run_in_thread (task, load_pixbuf_thread);
      g_object_unref (task);
    }
  else if (data->icon_info)
    {
      StIconColors *colors = data->colors;
      if (colors)
        {
          st_icon_info_load_symbolic_async (data->icon_info,
                                            data->colors,
                                            cache->cancellable,
                                            on_symbolic_icon_loaded, data);
        }
      else
        {
          st_icon_info_load_icon_async (data->icon_info,
                                        cache->cancellable,
                                        on_icon_loaded, data);
        }
    }
  else
    g_assert_not_reached ();
}

/**
 * st_texture_cache_load: (skip)
 * @cache: A #StTextureCache
 * @key: Arbitrary string used to refer to item
 * @policy: Caching policy
 * @load: Function to create the texture, if not already cached
 * @data: User data passed to @load
 * @error: A #GError
 *
 * Load an arbitrary texture, caching it.  The string chosen for @key
 * should be of the form "type-prefix:type-uuid".  For example,
 * "url:file:///usr/share/icons/hicolor/48x48/apps/firefox.png", or
 * "stock-icon:gtk-ok".
 *
 * Returns: (transfer full): A newly-referenced handle to the texture
 */
CoglTexture *
st_texture_cache_load (StTextureCache       *cache,
                       const char           *key,
                       StTextureCachePolicy  policy,
                       StTextureCacheLoader  load,
                       void                 *data,
                       GError              **error)
{
  CoglTexture *texture;

  texture = g_hash_table_lookup (cache->keyed_cache, key);
  if (!texture)
    {
      texture = load (cache, key, data, error);
      if (texture && policy == ST_TEXTURE_CACHE_POLICY_FOREVER)
        g_hash_table_insert (cache->keyed_cache, g_strdup (key), texture);
    }

  if (texture && policy == ST_TEXTURE_CACHE_POLICY_FOREVER)
    g_object_ref (texture);

  return texture;
}

/**
 * ensure_request:
 * @cache: A #StTextureCache
 * @key: A cache key
 * @policy: Cache policy
 * @request: (out): If no request is outstanding, one will be created and returned here
 * @texture: A texture to be added to the request
 *
 * Check for any outstanding load for the data represented by @key.  If there
 * is already a request pending, append it to that request to avoid loading
 * the data multiple times.
 *
 * Returns: %TRUE if there is already a request pending
 */
static gboolean
ensure_request (StTextureCache        *cache,
                const char            *key,
                StTextureCachePolicy   policy,
                AsyncTextureLoadData **request,
                ClutterActor          *actor)
{
  ClutterContent *image;
  AsyncTextureLoadData *pending;
  gboolean had_pending;

  image = g_hash_table_lookup (cache->keyed_cache, key);

  if (image != NULL)
    {
      /* We had this cached already, just set the texture and we're done. */
      set_content_from_image (actor, image);
      return TRUE;
    }

  pending = g_hash_table_lookup (cache->outstanding_requests, key);
  had_pending = pending != NULL;

  if (pending == NULL)
    {
      /* Not cached and no pending request, create it */
      *request = g_new0 (AsyncTextureLoadData, 1);
      if (policy != ST_TEXTURE_CACHE_POLICY_NONE)
        g_hash_table_insert (cache->outstanding_requests, g_strdup (key), *request);
    }
  else
   *request = pending;

  /* Regardless of whether there was a pending request, prepend our texture here. */
  (*request)->actors = g_slist_prepend ((*request)->actors, g_object_ref (actor));

  return had_pending;
}

/**
 * st_texture_cache_load_gicon:
 * @cache: A #StTextureCache
 * @theme_node: (nullable): The #StThemeNode to use for colors, or %NULL
 *                            if the icon must not be recolored
 * @icon: the #GIcon to load
 * @size: Size of themed
 * @paint_scale: Scale factor of display
 * @resource_scale: Resource scale factor
 *
 * This method returns a new #ClutterActor for a given #GIcon. If the
 * icon isn't loaded already, the texture will be filled
 * asynchronously.
 *
 * Returns: (transfer none) (nullable): A new #ClutterActor for the icon, or %NULL if not found
 */
ClutterActor *
st_texture_cache_load_gicon (StTextureCache    *cache,
                             StThemeNode       *theme_node,
                             GIcon             *icon,
                             gint               size,
                             gint               paint_scale,
                             gfloat             resource_scale)
{
  AsyncTextureLoadData *request;
  ClutterActor *actor;
  gint scale;
  char *gicon_string;
  g_autofree char *key = NULL;
  float actor_size;
  StIconTheme *theme;
  StTextureCachePolicy policy;
  StIconColors *colors = NULL;
  StIconStyle icon_style = ST_ICON_STYLE_REQUESTED;
  StIconLookupFlags lookup_flags;

  actor_size = size * paint_scale;

  if (ST_IS_IMAGE_CONTENT (icon))
    {
      int width, height;

      g_object_get (G_OBJECT (icon),
                    "preferred-width", &width,
                    "preferred-height", &height,
                    NULL);
      if (width == 0 && height == 0)
        return NULL;

      return g_object_new (CLUTTER_TYPE_ACTOR,
                           "content-gravity", CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT,
                           "width", actor_size,
                           "height", actor_size,
                           "content", CLUTTER_CONTENT (icon),
                           NULL);
    }

  if (theme_node)
    {
      colors = st_theme_node_get_icon_colors (theme_node);
      icon_style = st_theme_node_get_icon_style (theme_node);
    }

  /* Do theme lookups in the main thread to avoid thread-unsafety */
  theme = cache->icon_theme;

  lookup_flags = 0;

  if (icon_style == ST_ICON_STYLE_REGULAR)
    lookup_flags |= ST_ICON_LOOKUP_FORCE_REGULAR;
  else if (icon_style == ST_ICON_STYLE_SYMBOLIC)
    lookup_flags |= ST_ICON_LOOKUP_FORCE_SYMBOLIC;

  if (clutter_get_default_text_direction () == CLUTTER_TEXT_DIRECTION_RTL)
    lookup_flags |= ST_ICON_LOOKUP_DIR_RTL;
  else
    lookup_flags |= ST_ICON_LOOKUP_DIR_LTR;

  scale = ceilf (paint_scale * resource_scale);

  gicon_string = g_icon_to_string (icon);
  /* A return value of NULL indicates that the icon can not be serialized,
   * so don't have a unique identifier for it as a cache key, and thus can't
   * be cached. If it is cacheable, we hardcode a policy of FOREVER here for
   * now; we should actually blow this away on icon theme changes probably */
  policy = gicon_string != NULL ? ST_TEXTURE_CACHE_POLICY_FOREVER
                                : ST_TEXTURE_CACHE_POLICY_NONE;
  if (colors)
    {
      /* This raises some doubts about the practice of using string keys */
      key = g_strdup_printf (CACHE_PREFIX_ICON "%s,size=%d,scale=%d,style=%d,colors=%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x",
                             gicon_string, size, scale, icon_style,
                             colors->foreground.red, colors->foreground.blue, colors->foreground.green, colors->foreground.alpha,
                             colors->warning.red, colors->warning.blue, colors->warning.green, colors->warning.alpha,
                             colors->error.red, colors->error.blue, colors->error.green, colors->error.alpha,
                             colors->success.red, colors->success.blue, colors->success.green, colors->success.alpha);
    }
  else
    {
      key = g_strdup_printf (CACHE_PREFIX_ICON "%s,size=%d,scale=%d,style=%d",
                             gicon_string, size, scale, icon_style);
    }
  g_free (gicon_string);

  actor = create_invisible_actor ();
  clutter_actor_set_content_gravity  (actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT);
  clutter_actor_set_size (actor, actor_size, actor_size);
  if (!ensure_request (cache, key, policy, &request, actor))
    {
      /* Else, make a new request */
      StIconInfo *info;
      ClutterContext *context = clutter_actor_get_context (actor);
      ClutterBackend *clutter_backend = clutter_context_get_backend (context);

      info = st_icon_theme_lookup_by_gicon_for_scale (theme, icon,
                                                      size, scale,
                                                      lookup_flags);
      if (info == NULL)
        {
          g_hash_table_remove (cache->outstanding_requests, key);
          texture_load_data_free (request);
          g_object_unref (actor);
          return NULL;
        }

      request->cache = cache;
      /* Transfer ownership of key */
      request->key = g_steal_pointer (&key);
      request->policy = policy;
      request->colors = colors ? st_icon_colors_ref (colors) : NULL;
      request->icon_info = info;
      request->width = request->height = size;
      request->paint_scale = paint_scale;
      request->resource_scale = resource_scale;
      request->cogl_context = clutter_backend_get_cogl_context (clutter_backend);

      load_texture_async (cache, request);
    }

  return actor;
}

static void
hash_table_remove_with_scales (GHashTable *hash,
                               GList      *scales,
                               const char *base_key)
{
  GList *l;

  for (l = scales; l; l = l->next)
    {
      double scale = *((double *)l->data);
      g_autofree char *key = NULL;
      key = g_strdup_printf ("%s%f", base_key, scale);
      g_hash_table_remove (hash, key);
    }
}

static void
hash_table_insert_scale (GHashTable *hash,
                         double      scale)
{
  double *saved_scale;

  if (g_hash_table_contains (hash, &scale))
    return;

  saved_scale = g_new (double, 1);
  *saved_scale = scale;

  g_hash_table_add (hash, saved_scale);
}

static void
file_changed_cb (GFileMonitor      *monitor,
                 GFile             *file,
                 GFile             *other,
                 GFileMonitorEvent  event_type,
                 gpointer           user_data)
{
  StTextureCache *cache = user_data;
  char *key;
  guint file_hash;
  g_autoptr (GList) scales = NULL;

  if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
    return;

  file_hash = g_file_hash (file);
  scales = g_hash_table_get_keys (cache->used_scales);

  key = g_strdup_printf (CACHE_PREFIX_FILE "%u", file_hash);
  g_hash_table_remove (cache->keyed_cache, key);
  hash_table_remove_with_scales (cache->keyed_cache, scales, key);
  g_free (key);

  key = g_strdup_printf (CACHE_PREFIX_FILE_FOR_CAIRO "%u", file_hash);
  g_hash_table_remove (cache->keyed_surface_cache, key);
  hash_table_remove_with_scales (cache->keyed_surface_cache, scales, key);
  g_free (key);

  g_signal_emit (cache, signals[TEXTURE_FILE_CHANGED], 0, file);
}

static void
ensure_monitor_for_file (StTextureCache *cache,
                         GFile          *file)
{
  /* No point in trying to monitor files that are part of a
   * GResource, since it does not support file monitoring.
   */
  if (g_file_has_uri_scheme (file, "resource"))
    return;

  if (g_hash_table_lookup (cache->file_monitors, file) == NULL)
    {
      GFileMonitor *monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE,
                                                   NULL, NULL);
      g_signal_connect (monitor, "changed",
                        G_CALLBACK (file_changed_cb), cache);
      g_hash_table_insert (cache->file_monitors, g_object_ref (file), monitor);
    }
}

/**
 * st_texture_cache_load_file_async:
 * @cache: A #StTextureCache
 * @file: a #GFile of the image file from which to create a pixbuf
 * @available_width: available width for the image, can be -1 if not limited
 * @available_height: available height for the image, can be -1 if not limited
 * @paint_scale: scale factor of the display
 * @resource_scale: Resource scale factor
 *
 * Asynchronously load an image.   Initially, the returned texture will have a natural
 * size of zero.  At some later point, either the image will be loaded successfully
 * and at that point size will be negotiated, or upon an error, no image will be set.
 *
 * Returns: (transfer none): A new #ClutterActor with no image loaded initially.
 */
ClutterActor *
st_texture_cache_load_file_async (StTextureCache *cache,
                                  GFile          *file,
                                  int             available_width,
                                  int             available_height,
                                  int             paint_scale,
                                  gfloat          resource_scale)
{
  ClutterActor *actor;
  AsyncTextureLoadData *request;
  StTextureCachePolicy policy;
  gchar *key;
  int scale;

  scale = ceilf (paint_scale * resource_scale);
  key = g_strdup_printf (CACHE_PREFIX_FILE "%u%d", g_file_hash (file), scale);

  policy = ST_TEXTURE_CACHE_POLICY_NONE; /* XXX */

  actor = create_invisible_actor ();

  if (ensure_request (cache, key, policy, &request, actor))
    {
      /* If there's an outstanding request, we've just added ourselves to it */
      g_free (key);
    }
  else
    {
      ClutterContext *context = clutter_actor_get_context (actor);
      ClutterBackend *clutter_backend = clutter_context_get_backend (context);
      /* Else, make a new request */

      request->cache = cache;
      /* Transfer ownership of key */
      request->key = key;
      request->file = g_object_ref (file);
      request->policy = policy;
      request->width = available_width;
      request->height = available_height;
      request->paint_scale = paint_scale;
      request->resource_scale = resource_scale;
      request->cogl_context = clutter_backend_get_cogl_context (clutter_backend);

      load_texture_async (cache, request);
    }

  ensure_monitor_for_file (cache, file);

  return actor;
}

static CoglTexture *
st_texture_cache_load_file_sync_to_cogl_texture (StTextureCache *cache,
                                                 StTextureCachePolicy policy,
                                                 CoglContext    *context,
                                                 GFile          *file,
                                                 int             available_width,
                                                 int             available_height,
                                                 int             paint_scale,
                                                 gfloat          resource_scale,
                                                 GError         **error)
{
  ClutterContent *image;
  CoglTexture *texdata;
  GdkPixbuf *pixbuf;
  char *key;

  key = g_strdup_printf (CACHE_PREFIX_FILE "%u%f", g_file_hash (file), resource_scale);

  texdata = NULL;
  image = g_hash_table_lookup (cache->keyed_cache, key);

  if (image == NULL)
    {
      pixbuf = impl_load_pixbuf_file (file, available_width, available_height,
                                      paint_scale, resource_scale, error);
      if (!pixbuf)
        goto out;

      image = pixbuf_to_st_content_image (pixbuf, context,
                                          available_height, available_width,
                                          paint_scale, resource_scale,
                                          error);
      g_object_unref (pixbuf);

      if (!image)
        goto out;

      if (policy == ST_TEXTURE_CACHE_POLICY_FOREVER)
        {
          g_hash_table_insert (cache->keyed_cache, g_strdup (key), image);
          hash_table_insert_scale (cache->used_scales, (double)resource_scale);
        }
    }

  /* Because the texture is loaded synchronously, we won't call
   * st_image_content_set_data(), so it's safe to use the texture
   * of StImageContent here. */
  texdata = st_image_content_get_texture (ST_IMAGE_CONTENT (image));
  g_object_ref (texdata);

  ensure_monitor_for_file (cache, file);

out:
  g_free (key);
  return texdata;
}

static cairo_surface_t *
st_texture_cache_load_file_sync_to_cairo_surface (StTextureCache        *cache,
                                                  StTextureCachePolicy   policy,
                                                  GFile                 *file,
                                                  int                    available_width,
                                                  int                    available_height,
                                                  int                    paint_scale,
                                                  gfloat                 resource_scale,
                                                  GError               **error)
{
  cairo_surface_t *surface;
  GdkPixbuf *pixbuf;
  char *key;

  key = g_strdup_printf (CACHE_PREFIX_FILE_FOR_CAIRO "%u%f", g_file_hash (file), resource_scale);

  surface = g_hash_table_lookup (cache->keyed_surface_cache, key);

  if (surface == NULL)
    {
      pixbuf = impl_load_pixbuf_file (file, available_width, available_height,
                                      paint_scale, resource_scale, error);
      if (!pixbuf)
        goto out;

      surface = pixbuf_to_cairo_surface (pixbuf);
      g_object_unref (pixbuf);

      if (policy == ST_TEXTURE_CACHE_POLICY_FOREVER)
        {
          cairo_surface_reference (surface);
          g_hash_table_insert (cache->keyed_surface_cache,
                               g_strdup (key), surface);
          hash_table_insert_scale (cache->used_scales, (double)resource_scale);
        }
    }
  else
    cairo_surface_reference (surface);

  ensure_monitor_for_file (cache, file);

out:
  g_free (key);
  return surface;
}

/**
 * st_texture_cache_load_file_to_cogl_texture: (skip)
 * @cache: A #StTextureCache
 * @context: A #CoglContext
 * @file: A #GFile in supported image format
 * @paint_scale: Scale factor of the display
 * @resource_scale: Resource scale factor
 *
 * This function synchronously loads the given file path
 * into a COGL texture.  On error, a warning is emitted
 * and %NULL is returned.
 *
 * Returns: (transfer full): a new #CoglTexture
 */
CoglTexture *
st_texture_cache_load_file_to_cogl_texture (StTextureCache *cache,
                                            CoglContext    *context,
                                            GFile          *file,
                                            gint            paint_scale,
                                            gfloat          resource_scale)
{
  CoglTexture *texture;
  GError *error = NULL;

  texture = st_texture_cache_load_file_sync_to_cogl_texture (cache, ST_TEXTURE_CACHE_POLICY_FOREVER,
                                                             context,
                                                             file, -1, -1, paint_scale, resource_scale,
                                                             &error);

  if (texture == NULL)
    {
      char *uri = g_file_get_uri (file);
      g_warning ("Failed to load %s: %s", uri, error->message);
      g_clear_error (&error);
      g_free (uri);
    }

  return texture;
}

/**
 * st_texture_cache_load_file_to_cairo_surface:
 * @cache: A #StTextureCache
 * @file: A #GFile in supported image format
 * @paint_scale: Scale factor of the display
 * @resource_scale: Resource scale factor
 *
 * This function synchronously loads the given file path
 * into a cairo surface.  On error, a warning is emitted
 * and %NULL is returned.
 *
 * Returns: (transfer full): a new #cairo_surface_t
 */
cairo_surface_t *
st_texture_cache_load_file_to_cairo_surface (StTextureCache *cache,
                                             GFile          *file,
                                             gint            paint_scale,
                                             gfloat          resource_scale)
{
  cairo_surface_t *surface;
  GError *error = NULL;

  surface = st_texture_cache_load_file_sync_to_cairo_surface (cache, ST_TEXTURE_CACHE_POLICY_FOREVER,
                                                              file, -1, -1, paint_scale, resource_scale,
                                                              &error);

  if (surface == NULL)
    {
      char *uri = g_file_get_uri (file);
      g_warning ("Failed to load %s: %s", uri, error->message);
      g_clear_error (&error);
      g_free (uri);
    }

  return surface;
}

static StTextureCache *instance = NULL;

/**
 * st_texture_cache_get_default:
 *
 * Returns: (transfer none): The global texture cache
 */
StTextureCache*
st_texture_cache_get_default (void)
{
  if (instance == NULL)
    instance = g_object_new (ST_TYPE_TEXTURE_CACHE, NULL);
  return instance;
}

/**
 * st_texture_cache_rescan_icon_theme:
 *
 * Rescan the current icon theme, if necessary.
 *
 * Returns: %TRUE if the icon theme has changed and needed to be reloaded.
 */
gboolean
st_texture_cache_rescan_icon_theme (StTextureCache *cache)
{
  return st_icon_theme_rescan_if_needed (cache->icon_theme);
}
