/* nautilus-icon-info.c
 * Copyright (C) 2007  Red Hat, Inc.,  Alexander Larsson <alexl@redhat.com>
 *
 * This 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.
 *
 * 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>
#include <string.h>
#include "nautilus-icon-info.h"
#include "nautilus-icon-names.h"
#include "nautilus-default-file-icon.h"
#include <gtk/gtk.h>
#include <gio/gio.h>

struct _NautilusIconInfo
{
    GObject parent;

    gboolean sole_owner;
    gint64 last_use_time;
    GdkPixbuf *pixbuf;

    char *icon_name;

    gint orig_scale;
};

struct _NautilusIconInfoClass
{
    GObjectClass parent_class;
};

static void schedule_reap_cache (void);

G_DEFINE_TYPE (NautilusIconInfo,
               nautilus_icon_info,
               G_TYPE_OBJECT);

static void
nautilus_icon_info_init (NautilusIconInfo *icon)
{
    icon->last_use_time = g_get_monotonic_time ();
    icon->sole_owner = TRUE;
}

gboolean
nautilus_icon_info_is_fallback (NautilusIconInfo *icon)
{
    return icon->pixbuf == NULL;
}

static void
pixbuf_toggle_notify (gpointer  info,
                      GObject  *object,
                      gboolean  is_last_ref)
{
    NautilusIconInfo *icon = info;

    if (is_last_ref)
    {
        icon->sole_owner = TRUE;
        g_object_remove_toggle_ref (object,
                                    pixbuf_toggle_notify,
                                    info);
        icon->last_use_time = g_get_monotonic_time ();
        schedule_reap_cache ();
    }
}

static void
nautilus_icon_info_finalize (GObject *object)
{
    NautilusIconInfo *icon;

    icon = NAUTILUS_ICON_INFO (object);

    if (!icon->sole_owner && icon->pixbuf)
    {
        g_object_remove_toggle_ref (G_OBJECT (icon->pixbuf),
                                    pixbuf_toggle_notify,
                                    icon);
    }

    if (icon->pixbuf)
    {
        g_object_unref (icon->pixbuf);
    }
    g_free (icon->icon_name);

    G_OBJECT_CLASS (nautilus_icon_info_parent_class)->finalize (object);
}

static void
nautilus_icon_info_class_init (NautilusIconInfoClass *icon_info_class)
{
    GObjectClass *gobject_class;

    gobject_class = (GObjectClass *) icon_info_class;

    gobject_class->finalize = nautilus_icon_info_finalize;
}

NautilusIconInfo *
nautilus_icon_info_new_for_pixbuf (GdkPixbuf *pixbuf,
                                   gint       scale)
{
    NautilusIconInfo *icon;

    icon = g_object_new (NAUTILUS_TYPE_ICON_INFO, NULL);

    if (pixbuf)
    {
        icon->pixbuf = g_object_ref (pixbuf);
    }

    icon->orig_scale = scale;

    return icon;
}

static NautilusIconInfo *
nautilus_icon_info_new_for_icon_info (GtkIconInfo *icon_info,
                                      gint         scale)
{
    NautilusIconInfo *icon;
    const char *filename;
    char *basename, *p;

    icon = g_object_new (NAUTILUS_TYPE_ICON_INFO, NULL);

    icon->pixbuf = gtk_icon_info_load_icon (icon_info, NULL);

    filename = gtk_icon_info_get_filename (icon_info);
    if (filename != NULL)
    {
        basename = g_path_get_basename (filename);
        p = strrchr (basename, '.');
        if (p)
        {
            *p = 0;
        }
        icon->icon_name = basename;
    }

    icon->orig_scale = scale;

    return icon;
}


typedef struct
{
    GIcon *icon;
    int scale;
    int size;
} LoadableIconKey;

typedef struct
{
    char *filename;
    int scale;
    int size;
} ThemedIconKey;

static GHashTable *loadable_icon_cache = NULL;
static GHashTable *themed_icon_cache = NULL;
static guint reap_cache_timeout = 0;

#define MICROSEC_PER_SEC ((guint64) 1000000L)

static guint time_now;

static gboolean
reap_old_icon (gpointer key,
               gpointer value,
               gpointer user_info)
{
    NautilusIconInfo *icon = value;
    gboolean *reapable_icons_left = user_info;

    if (icon->sole_owner)
    {
        if (time_now - icon->last_use_time > 30 * MICROSEC_PER_SEC)
        {
            /* This went unused 30 secs ago. reap */
            return TRUE;
        }
        else
        {
            /* We can reap this soon */
            *reapable_icons_left = TRUE;
        }
    }

    return FALSE;
}

static gboolean
reap_cache (gpointer data)
{
    gboolean reapable_icons_left;

    reapable_icons_left = TRUE;

    time_now = g_get_monotonic_time ();

    if (loadable_icon_cache)
    {
        g_hash_table_foreach_remove (loadable_icon_cache,
                                     reap_old_icon,
                                     &reapable_icons_left);
    }

    if (themed_icon_cache)
    {
        g_hash_table_foreach_remove (themed_icon_cache,
                                     reap_old_icon,
                                     &reapable_icons_left);
    }

    if (reapable_icons_left)
    {
        return TRUE;
    }
    else
    {
        reap_cache_timeout = 0;
        return FALSE;
    }
}

static void
schedule_reap_cache (void)
{
    if (reap_cache_timeout == 0)
    {
        reap_cache_timeout = g_timeout_add_seconds_full (0, 5,
                                                         reap_cache,
                                                         NULL, NULL);
    }
}

void
nautilus_icon_info_clear_caches (void)
{
    if (loadable_icon_cache)
    {
        g_hash_table_remove_all (loadable_icon_cache);
    }

    if (themed_icon_cache)
    {
        g_hash_table_remove_all (themed_icon_cache);
    }
}

static guint
loadable_icon_key_hash (LoadableIconKey *key)
{
    return g_icon_hash (key->icon) ^ key->scale ^ key->size;
}

static gboolean
loadable_icon_key_equal (const LoadableIconKey *a,
                         const LoadableIconKey *b)
{
    return a->size == b->size &&
           a->scale == b->scale &&
           g_icon_equal (a->icon, b->icon);
}

static LoadableIconKey *
loadable_icon_key_new (GIcon *icon,
                       int    scale,
                       int    size)
{
    LoadableIconKey *key;

    key = g_slice_new (LoadableIconKey);
    key->icon = g_object_ref (icon);
    key->scale = scale;
    key->size = size;

    return key;
}

static void
loadable_icon_key_free (LoadableIconKey *key)
{
    g_object_unref (key->icon);
    g_slice_free (LoadableIconKey, key);
}

static guint
themed_icon_key_hash (ThemedIconKey *key)
{
    return g_str_hash (key->filename) ^ key->size;
}

static gboolean
themed_icon_key_equal (const ThemedIconKey *a,
                       const ThemedIconKey *b)
{
    return a->size == b->size &&
           a->scale == b->scale &&
           g_str_equal (a->filename, b->filename);
}

static ThemedIconKey *
themed_icon_key_new (const char *filename,
                     int         scale,
                     int         size)
{
    ThemedIconKey *key;

    key = g_slice_new (ThemedIconKey);
    key->filename = g_strdup (filename);
    key->scale = scale;
    key->size = size;

    return key;
}

static void
themed_icon_key_free (ThemedIconKey *key)
{
    g_free (key->filename);
    g_slice_free (ThemedIconKey, key);
}

NautilusIconInfo *
nautilus_icon_info_lookup (GIcon *icon,
                           int    size,
                           int    scale)
{
    NautilusIconInfo *icon_info;
    GdkPixbuf *pixbuf;

    if (G_IS_LOADABLE_ICON (icon))
    {
        LoadableIconKey lookup_key;
        LoadableIconKey *key;
        GInputStream *stream;

        if (loadable_icon_cache == NULL)
        {
            loadable_icon_cache =
                g_hash_table_new_full ((GHashFunc) loadable_icon_key_hash,
                                       (GEqualFunc) loadable_icon_key_equal,
                                       (GDestroyNotify) loadable_icon_key_free,
                                       (GDestroyNotify) g_object_unref);
        }

        lookup_key.icon = icon;
        lookup_key.scale = scale;
        lookup_key.size = size * scale;

        icon_info = g_hash_table_lookup (loadable_icon_cache, &lookup_key);
        if (icon_info)
        {
            return g_object_ref (icon_info);
        }

        pixbuf = NULL;
        stream = g_loadable_icon_load (G_LOADABLE_ICON (icon),
                                       size * scale,
                                       NULL, NULL, NULL);
        if (stream)
        {
            pixbuf = gdk_pixbuf_new_from_stream_at_scale (stream,
                                                          size * scale, size * scale,
                                                          TRUE,
                                                          NULL, NULL);
            g_input_stream_close (stream, NULL, NULL);
            g_object_unref (stream);
        }

        icon_info = nautilus_icon_info_new_for_pixbuf (pixbuf, scale);

        key = loadable_icon_key_new (icon, scale, size);
        g_hash_table_insert (loadable_icon_cache, key, icon_info);

        return g_object_ref (icon_info);
    }
    else if (G_IS_THEMED_ICON (icon))
    {
        const char * const *names;
        ThemedIconKey lookup_key;
        ThemedIconKey *key;
        GtkIconTheme *icon_theme;
        GtkIconInfo *gtkicon_info;
        const char *filename;

        if (themed_icon_cache == NULL)
        {
            themed_icon_cache =
                g_hash_table_new_full ((GHashFunc) themed_icon_key_hash,
                                       (GEqualFunc) themed_icon_key_equal,
                                       (GDestroyNotify) themed_icon_key_free,
                                       (GDestroyNotify) g_object_unref);
        }

        names = g_themed_icon_get_names (G_THEMED_ICON (icon));

        icon_theme = gtk_icon_theme_get_default ();
        gtkicon_info = gtk_icon_theme_choose_icon_for_scale (icon_theme, (const char **) names,
                                                             size, scale, GTK_ICON_LOOKUP_FORCE_SIZE);

        if (gtkicon_info == NULL)
        {
            return nautilus_icon_info_new_for_pixbuf (NULL, scale);
        }

        filename = gtk_icon_info_get_filename (gtkicon_info);
        if (filename == NULL)
        {
            g_object_unref (gtkicon_info);
            return nautilus_icon_info_new_for_pixbuf (NULL, scale);
        }

        lookup_key.filename = (char *) filename;
        lookup_key.scale = scale;
        lookup_key.size = size;

        icon_info = g_hash_table_lookup (themed_icon_cache, &lookup_key);
        if (icon_info)
        {
            g_object_unref (gtkicon_info);
            return g_object_ref (icon_info);
        }

        icon_info = nautilus_icon_info_new_for_icon_info (gtkicon_info, scale);

        key = themed_icon_key_new (filename, scale, size);
        g_hash_table_insert (themed_icon_cache, key, icon_info);

        g_object_unref (gtkicon_info);

        return g_object_ref (icon_info);
    }
    else
    {
        GdkPixbuf *pixbuf;
        GtkIconInfo *gtk_icon_info;

        gtk_icon_info = gtk_icon_theme_lookup_by_gicon_for_scale (gtk_icon_theme_get_default (),
                                                                  icon,
                                                                  size,
                                                                  scale,
                                                                  GTK_ICON_LOOKUP_FORCE_SIZE);
        if (gtk_icon_info != NULL)
        {
            pixbuf = gtk_icon_info_load_icon (gtk_icon_info, NULL);
            g_object_unref (gtk_icon_info);
        }
        else
        {
            pixbuf = NULL;
        }

        icon_info = nautilus_icon_info_new_for_pixbuf (pixbuf, scale);

        if (pixbuf != NULL)
        {
            g_object_unref (pixbuf);
        }

        return icon_info;
    }
}

NautilusIconInfo *
nautilus_icon_info_lookup_from_name (const char *name,
                                     int         size,
                                     int         scale)
{
    GIcon *icon;
    NautilusIconInfo *info;

    icon = g_themed_icon_new (name);
    info = nautilus_icon_info_lookup (icon, size, scale);
    g_object_unref (icon);
    return info;
}

NautilusIconInfo *
nautilus_icon_info_lookup_from_path (const char *path,
                                     int         size,
                                     int         scale)
{
    GFile *icon_file;
    GIcon *icon;
    NautilusIconInfo *info;

    icon_file = g_file_new_for_path (path);
    icon = g_file_icon_new (icon_file);
    info = nautilus_icon_info_lookup (icon, size, scale);
    g_object_unref (icon);
    g_object_unref (icon_file);
    return info;
}

GdkPixbuf *
nautilus_icon_info_get_pixbuf_nodefault (NautilusIconInfo *icon)
{
    GdkPixbuf *res;

    if (icon->pixbuf == NULL)
    {
        res = NULL;
    }
    else
    {
        res = g_object_ref (icon->pixbuf);

        if (icon->sole_owner)
        {
            icon->sole_owner = FALSE;
            g_object_add_toggle_ref (G_OBJECT (res),
                                     pixbuf_toggle_notify,
                                     icon);
        }
    }

    return res;
}


GdkPixbuf *
nautilus_icon_info_get_pixbuf (NautilusIconInfo *icon)
{
    GdkPixbuf *res;

    res = nautilus_icon_info_get_pixbuf_nodefault (icon);
    if (res == NULL)
    {
        res = gdk_pixbuf_new_from_data (nautilus_default_file_icon,
                                        GDK_COLORSPACE_RGB,
                                        TRUE,
                                        8,
                                        nautilus_default_file_icon_width,
                                        nautilus_default_file_icon_height,
                                        nautilus_default_file_icon_width * 4,         /* stride */
                                        NULL,         /* don't destroy info */
                                        NULL);
    }

    return res;
}

GdkPixbuf *
nautilus_icon_info_get_pixbuf_nodefault_at_size (NautilusIconInfo *icon,
                                                 gsize             forced_size)
{
    GdkPixbuf *pixbuf, *scaled_pixbuf;
    int w, h, s;
    double scale;

    pixbuf = nautilus_icon_info_get_pixbuf_nodefault (icon);

    if (pixbuf == NULL)
    {
        return NULL;
    }

    w = gdk_pixbuf_get_width (pixbuf) / icon->orig_scale;
    h = gdk_pixbuf_get_height (pixbuf) / icon->orig_scale;
    s = MAX (w, h);
    if (s == forced_size)
    {
        return pixbuf;
    }

    scale = (double) forced_size / s;
    scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf,
                                             w * scale, h * scale,
                                             GDK_INTERP_BILINEAR);
    g_object_unref (pixbuf);
    return scaled_pixbuf;
}


GdkPixbuf *
nautilus_icon_info_get_pixbuf_at_size (NautilusIconInfo *icon,
                                       gsize             forced_size)
{
    GdkPixbuf *pixbuf, *scaled_pixbuf;
    int w, h, s;
    double scale;

    pixbuf = nautilus_icon_info_get_pixbuf (icon);

    w = gdk_pixbuf_get_width (pixbuf) / icon->orig_scale;
    h = gdk_pixbuf_get_height (pixbuf) / icon->orig_scale;
    s = MAX (w, h);
    if (s == forced_size)
    {
        return pixbuf;
    }

    scale = (double) forced_size / s;

    /* Neither of these can be 0. */
    w = MAX (w * scale, 1);
    h = MAX (h * scale, 1);

    scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf,
                                             w, h,
                                             GDK_INTERP_BILINEAR);
    g_object_unref (pixbuf);
    return scaled_pixbuf;
}

const char *
nautilus_icon_info_get_used_name (NautilusIconInfo *icon)
{
    return icon->icon_name;
}

gint
nautilus_get_icon_size_for_stock_size (GtkIconSize size)
{
    gint w, h;

    if (gtk_icon_size_lookup (size, &w, &h))
    {
        return MAX (w, h);
    }
    return NAUTILUS_CANVAS_ICON_SIZE_SMALL;
}
