/**
 * @file buddyicon.c Buddy Icon API
 * @ingroup core
 */

/* purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 */
#define _PURPLE_BUDDYICON_C_

#include "internal.h"
#include "buddyicon.h"
#include "conversation.h"
#include "dbus-maybe.h"
#include "debug.h"
#include "imgstore.h"
#include "util.h"

/* NOTE: Instances of this struct are allocated without zeroing the memory, so
 * NOTE: be sure to update purple_buddy_icon_new() if you add members. */
struct _PurpleBuddyIcon
{
	PurpleAccount *account;    /**< The account the user is on.          */
	PurpleStoredImage *img;    /**< The stored image containing
	                                the icon data.                       */
	char *username;            /**< The username the icon belongs to.    */
	char *checksum;            /**< The protocol checksum.               */
	int ref_count;             /**< The buddy icon reference count.      */
};

/**
 * This is the big grand daddy hash table that contains references to
 * everybody's buddy icons.
 *
 * Key is a PurpleAccount.
 * Value is another hash table, usually referred to as "icon_cache."
 * For this inner hash table:
 *    Key is the username of the buddy whose icon is being stored.
 *    Value is the PurpleBuddyIcon for this buddy.
 */
static GHashTable *account_cache = NULL;

/**
 * This hash table contains a bunch of PurpleStoredImages that are
 * shared across all accounts.
 *
 * Key is the filename for this image as constructed by
 * purple_util_get_image_filename().  So it is the base16 encoded
 * sha-1 hash plus an appropriate file extension.  For example:
 *   "0f4972d17d1e70e751c43c90c948e72efbff9796.gif"
 *
 * The value is a PurpleStoredImage containing the icon data.  These
 * images are reference counted, and when the count reaches 0
 * imgstore.c emits the image-deleting signal and we remove the image
 * from the hash table (but it might still be saved on disk, if the
 * icon is being used by offline accounts or some such).
 */
static GHashTable *icon_data_cache = NULL;

/**
 * This hash table contains references counts for how many times each
 * icon in the ~/.purple/icons/ directory is being used.  It's pretty
 * crazy.  It maintains the reference count across sessions, too, so
 * if you exit Pidgin then this hash table is reconstructed the next
 * time Pidgin starts.
 *
 * Key is the filename for this image as constructed by
 * purple_util_get_image_filename().  So it is the base16 encoded
 * sha-1 hash plus an appropriate file extension.  For example:
 *   "0f4972d17d1e70e751c43c90c948e72efbff9796.gif"
 *
 * The value is a GINT_TO_POINTER count of the number of times this
 * icon is used.  So if four of your buddies are using an icon, and
 * you have the icon set for two of your accounts, then this number
 * will be six.  When this reference count reaches 0 the icon will
 * be deleted from disk.
 */
static GHashTable *icon_file_cache = NULL;

/**
 * This hash table is used for both custom buddy icons on PurpleBlistNodes and
 * account icons.
 */
static GHashTable *pointer_icon_cache = NULL;

static char       *cache_dir     = NULL;

/** "Should icons be cached to disk?" */
static gboolean    icon_caching  = TRUE;

/* For ~/.gaim to ~/.purple migration. */
static char *old_icons_dir = NULL;

static void delete_buddy_icon_settings(PurpleBlistNode *node, const char *setting_name);

/*
 * Begin functions for dealing with the on-disk icon cache
 */

static void
ref_filename(const char *filename)
{
	int refs;

	g_return_if_fail(filename != NULL);

	refs = GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename));

	g_hash_table_insert(icon_file_cache, g_strdup(filename),
	                    GINT_TO_POINTER(refs + 1));
}

static void
unref_filename(const char *filename)
{
	int refs;

	if (filename == NULL)
		return;

	refs = GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename));

	if (refs == 1)
	{
		g_hash_table_remove(icon_file_cache, filename);
	}
	else
	{
		g_hash_table_insert(icon_file_cache, g_strdup(filename),
		                    GINT_TO_POINTER(refs - 1));
	}
}

static void
purple_buddy_icon_data_cache(PurpleStoredImage *img)
{
	const char *dirname;
	char *path;

	g_return_if_fail(img != NULL);

	if (!purple_buddy_icons_is_caching())
		return;

	dirname  = purple_buddy_icons_get_cache_dir();
	path = g_build_filename(dirname, purple_imgstore_get_filename(img), NULL);

	if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
	{
		purple_debug_info("buddyicon", "Creating icon cache directory.\n");

		if (g_mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
		{
			purple_debug_error("buddyicon",
			                   "Unable to create directory %s: %s\n",
			                   dirname, g_strerror(errno));
		}
	}

	if (!g_file_test(path, G_FILE_TEST_IS_REGULAR))
	{
		purple_util_write_data_to_file_absolute(path, purple_imgstore_get_data(img),
							purple_imgstore_get_size(img));
	}

	g_free(path);
}

static void
purple_buddy_icon_data_uncache_file(const char *filename)
{
	const char *dirname;
	char *path;

	g_return_if_fail(filename != NULL);

	/* It's possible that there are other references to this icon
	 * cache file that are not currently loaded into memory. */
	if (GPOINTER_TO_INT(g_hash_table_lookup(icon_file_cache, filename)))
		return;

	dirname  = purple_buddy_icons_get_cache_dir();
	path = g_build_filename(dirname, filename, NULL);

	if (g_file_test(path, G_FILE_TEST_EXISTS))
	{
		if (g_unlink(path))
		{
			purple_debug_error("buddyicon", "Failed to delete %s: %s\n",
			                   path, g_strerror(errno));
		}
		else
		{
			purple_debug_info("buddyicon", "Deleted cache file: %s\n", path);
		}
	}

	g_free(path);
}

/*
 * End functions for dealing with the on-disk icon cache
 */

/*
 * Begin functions for dealing with the in-memory icon cache
 */

static gboolean
value_equals(gpointer key, gpointer value, gpointer user_data)
{
	return (value == user_data);
}

static void
image_deleting_cb(const PurpleStoredImage *img, gpointer data)
{
	const char *filename = purple_imgstore_get_filename(img);

	/* If there's no filename, it can't be one of our images. */
	if (filename == NULL)
		return;

	if (img == g_hash_table_lookup(icon_data_cache, filename))
	{
		purple_buddy_icon_data_uncache_file(filename);
		g_hash_table_remove(icon_data_cache, filename);

		/* We could make this O(1) by using another hash table, but
		 * this is probably good enough. */
		g_hash_table_foreach_remove(pointer_icon_cache, value_equals, (gpointer)img);
	}
}

static PurpleStoredImage *
purple_buddy_icon_data_new(guchar *icon_data, size_t icon_len, const char *filename)
{
	char *file;
	PurpleStoredImage *img;

	g_return_val_if_fail(icon_data != NULL, NULL);
	g_return_val_if_fail(icon_len  > 0,     NULL);

	if (filename == NULL)
	{
		file = purple_util_get_image_filename(icon_data, icon_len);
		if (file == NULL)
		{
			g_free(icon_data);
			return NULL;
		}
	}
	else
		file = g_strdup(filename);

	if ((img = g_hash_table_lookup(icon_data_cache, file)))
	{
		g_free(file);
		g_free(icon_data);
		return purple_imgstore_ref(img);
	}

	img = purple_imgstore_add(icon_data, icon_len, file);

	/* This will take ownership of file and g_free it either now or later. */
	g_hash_table_insert(icon_data_cache, file, img);

	purple_buddy_icon_data_cache(img);

	return img;
}

/*
 * End functions for dealing with the in-memory icon cache
 */

static PurpleBuddyIcon *
purple_buddy_icon_create(PurpleAccount *account, const char *username)
{
	PurpleBuddyIcon *icon;
	GHashTable *icon_cache;

	/* This does not zero.  See purple_buddy_icon_new() for
	 * information on which function allocates which member. */
	icon = g_slice_new(PurpleBuddyIcon);
	PURPLE_DBUS_REGISTER_POINTER(icon, PurpleBuddyIcon);

	icon->account = account;
	icon->username = g_strdup(username);
	icon->checksum = NULL;
	icon->ref_count = 1;

	icon_cache = g_hash_table_lookup(account_cache, account);

	if (icon_cache == NULL)
	{
		icon_cache = g_hash_table_new(g_str_hash, g_str_equal);

		g_hash_table_insert(account_cache, account, icon_cache);
	}

	g_hash_table_insert(icon_cache,
	                    (char *)purple_buddy_icon_get_username(icon), icon);
	return icon;
}

PurpleBuddyIcon *
purple_buddy_icon_new(PurpleAccount *account, const char *username,
                      void *icon_data, size_t icon_len,
                      const char *checksum)
{
	PurpleBuddyIcon *icon;

	g_return_val_if_fail(account   != NULL, NULL);
	g_return_val_if_fail(username  != NULL, NULL);
	g_return_val_if_fail(icon_data != NULL, NULL);
	g_return_val_if_fail(icon_len  > 0,    NULL);

	/* purple_buddy_icons_find() does allocation, so be
	 * sure to update it as well when members are added. */
	icon = purple_buddy_icons_find(account, username);

	/* purple_buddy_icon_create() sets account & username */
	if (icon == NULL)
		icon = purple_buddy_icon_create(account, username);

	/* purple_buddy_icon_set_data() sets img, but it
	 * references img first, so we need to initialize it */
	icon->img = NULL;
	purple_buddy_icon_set_data(icon, icon_data, icon_len, checksum);

	return icon;
}

PurpleBuddyIcon *
purple_buddy_icon_ref(PurpleBuddyIcon *icon)
{
	g_return_val_if_fail(icon != NULL, NULL);

	icon->ref_count++;

	return icon;
}

PurpleBuddyIcon *
purple_buddy_icon_unref(PurpleBuddyIcon *icon)
{
	if (icon == NULL)
		return NULL;

	g_return_val_if_fail(icon->ref_count > 0, NULL);

	icon->ref_count--;

	if (icon->ref_count == 0)
	{
		GHashTable *icon_cache = g_hash_table_lookup(account_cache, purple_buddy_icon_get_account(icon));

		if (icon_cache != NULL)
			g_hash_table_remove(icon_cache, purple_buddy_icon_get_username(icon));

		g_free(icon->username);
		g_free(icon->checksum);
		purple_imgstore_unref(icon->img);

		PURPLE_DBUS_UNREGISTER_POINTER(icon);
		g_slice_free(PurpleBuddyIcon, icon);

		return NULL;
	}

	return icon;
}

void
purple_buddy_icon_update(PurpleBuddyIcon *icon)
{
	PurpleConversation *conv;
	PurpleAccount *account;
	const char *username;
	PurpleBuddyIcon *icon_to_set;
	GSList *buddies;

	g_return_if_fail(icon != NULL);

	account  = purple_buddy_icon_get_account(icon);
	username = purple_buddy_icon_get_username(icon);

	/* If no data exists (icon->img == NULL), then call the functions below
	 * with NULL to unset the icon.  They will then unref the icon and it should
	 * be destroyed.  The only way it wouldn't be destroyed is if someone
	 * else is holding a reference to it, in which case they can kill
	 * the icon when they realize it has no data. */
	icon_to_set = icon->img ? icon : NULL;

	/* Ensure that icon remains valid throughout */
	purple_buddy_icon_ref(icon);

	buddies = purple_find_buddies(account, username);
	while (buddies != NULL)
	{
		PurpleBuddy *buddy = (PurpleBuddy *)buddies->data;
		char *old_icon;

		purple_buddy_set_icon(buddy, icon_to_set);
		old_icon = g_strdup(purple_blist_node_get_string((PurpleBlistNode *)buddy,
		                                                 "buddy_icon"));
		if (icon->img && purple_buddy_icons_is_caching())
		{
			const char *filename = purple_imgstore_get_filename(icon->img);
			purple_blist_node_set_string((PurpleBlistNode *)buddy,
			                             "buddy_icon",
			                             filename);

			if (icon->checksum && *icon->checksum)
			{
				purple_blist_node_set_string((PurpleBlistNode *)buddy,
				                             "icon_checksum",
				                             icon->checksum);
			}
			else
			{
				purple_blist_node_remove_setting((PurpleBlistNode *)buddy,
				                                 "icon_checksum");
			}
			ref_filename(filename);
		}
		else if (!icon->img)
		{
			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "buddy_icon");
			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "icon_checksum");
		}
		unref_filename(old_icon);
		g_free(old_icon);

		buddies = g_slist_delete_link(buddies, buddies);
	}

	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, username, account);

	if (conv != NULL)
		purple_conv_im_set_icon(PURPLE_CONV_IM(conv), icon_to_set);

	/* icon's refcount was incremented above */
	purple_buddy_icon_unref(icon);
}

void
purple_buddy_icon_set_data(PurpleBuddyIcon *icon, guchar *data,
                           size_t len, const char *checksum)
{
	PurpleStoredImage *old_img;

	g_return_if_fail(icon != NULL);

	old_img = icon->img;
	icon->img = NULL;

	if (data != NULL)
	{
		if (len > 0)
			icon->img = purple_buddy_icon_data_new(data, len, NULL);
		else
			g_free(data);
	}

	g_free(icon->checksum);
	icon->checksum = g_strdup(checksum);

	purple_buddy_icon_update(icon);

	purple_imgstore_unref(old_img);
}

PurpleAccount *
purple_buddy_icon_get_account(const PurpleBuddyIcon *icon)
{
	g_return_val_if_fail(icon != NULL, NULL);

	return icon->account;
}

const char *
purple_buddy_icon_get_username(const PurpleBuddyIcon *icon)
{
	g_return_val_if_fail(icon != NULL, NULL);

	return icon->username;
}

const char *
purple_buddy_icon_get_checksum(const PurpleBuddyIcon *icon)
{
	g_return_val_if_fail(icon != NULL, NULL);

	return icon->checksum;
}

gconstpointer
purple_buddy_icon_get_data(const PurpleBuddyIcon *icon, size_t *len)
{
	g_return_val_if_fail(icon != NULL, NULL);

	if (icon->img)
	{
		if (len != NULL)
			*len = purple_imgstore_get_size(icon->img);

		return purple_imgstore_get_data(icon->img);
	}

	return NULL;
}

const char *
purple_buddy_icon_get_extension(const PurpleBuddyIcon *icon)
{
	if (icon->img != NULL)
		return purple_imgstore_get_extension(icon->img);

	return NULL;
}

void
purple_buddy_icons_set_for_user(PurpleAccount *account, const char *username,
                                void *icon_data, size_t icon_len,
                                const char *checksum)
{
	GHashTable *icon_cache;
	PurpleBuddyIcon *icon = NULL;

	g_return_if_fail(account  != NULL);
	g_return_if_fail(username != NULL);

	icon_cache = g_hash_table_lookup(account_cache, account);

	if (icon_cache != NULL)
		icon = g_hash_table_lookup(icon_cache, username);

	if (icon != NULL)
		purple_buddy_icon_set_data(icon, icon_data, icon_len, checksum);
	else if (icon_data && icon_len > 0)
	{
		PurpleBuddyIcon *icon = purple_buddy_icon_new(account, username, icon_data, icon_len, checksum);

		/* purple_buddy_icon_new() calls
		 * purple_buddy_icon_set_data(), which calls
		 * purple_buddy_icon_update(), which has the buddy list
		 * and conversations take references as appropriate.
		 * This function doesn't return icon, so we can't
		 * leave a reference dangling. */
		purple_buddy_icon_unref(icon);
	}
	else
	{
		/* If the buddy list or a conversation was holding a
		 * reference, we'd have found the icon in the cache.
		 * Since we know we're deleting the icon, we only
		 * need a subset of purple_buddy_icon_update(). */

		GSList *buddies = purple_find_buddies(account, username);
		while (buddies != NULL)
		{
			PurpleBuddy *buddy = (PurpleBuddy *)buddies->data;

			unref_filename(purple_blist_node_get_string((PurpleBlistNode *)buddy, "buddy_icon"));
			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "buddy_icon");
			purple_blist_node_remove_setting((PurpleBlistNode *)buddy, "icon_checksum");

			buddies = g_slist_delete_link(buddies, buddies);
		}
	}
}

char *purple_buddy_icon_get_full_path(PurpleBuddyIcon *icon)
{
	char *path;

	g_return_val_if_fail(icon != NULL, NULL);

	if (icon->img == NULL)
		return NULL;

	path = g_build_filename(purple_buddy_icons_get_cache_dir(),
	                        purple_imgstore_get_filename(icon->img), NULL);
	if (!g_file_test(path, G_FILE_TEST_EXISTS))
	{
		g_free(path);
		return NULL;
	}
	return path;
}

const char *
purple_buddy_icons_get_checksum_for_user(PurpleBuddy *buddy)
{
	return purple_blist_node_get_string((PurpleBlistNode*)buddy,
	                                    "icon_checksum");
}

static gboolean
read_icon_file(const char *path, guchar **data, size_t *len)
{
	GError *err = NULL;

	if (!g_file_get_contents(path, (gchar **)data, len, &err))
	{
		purple_debug_error("buddyicon", "Error reading %s: %s\n",
		                   path, err->message);
		g_error_free(err);

		return FALSE;
	}

	return TRUE;
}

PurpleBuddyIcon *
purple_buddy_icons_find(PurpleAccount *account, const char *username)
{
	GHashTable *icon_cache;
	PurpleBuddyIcon *icon = NULL;

	g_return_val_if_fail(account  != NULL, NULL);
	g_return_val_if_fail(username != NULL, NULL);

	icon_cache = g_hash_table_lookup(account_cache, account);

	if ((icon_cache == NULL) || ((icon = g_hash_table_lookup(icon_cache, username)) == NULL))
	{
		PurpleBuddy *b = purple_find_buddy(account, username);
		const char *protocol_icon_file;
		const char *dirname;
		gboolean caching;
		guchar *data;
		size_t len;

		if (!b)
			return NULL;

		protocol_icon_file = purple_blist_node_get_string((PurpleBlistNode*)b, "buddy_icon");

		if (protocol_icon_file == NULL)
			return NULL;

		dirname = purple_buddy_icons_get_cache_dir();

		caching = purple_buddy_icons_is_caching();
		/* By disabling caching temporarily, we avoid a loop
		 * and don't have to add special code through several
		 * functions. */
		purple_buddy_icons_set_caching(FALSE);

		if (protocol_icon_file != NULL)
		{
			char *path = g_build_filename(dirname, protocol_icon_file, NULL);
			if (read_icon_file(path, &data, &len))
			{
				const char *checksum;

				icon = purple_buddy_icon_create(account, username);
				icon->img = NULL;
				checksum = purple_blist_node_get_string((PurpleBlistNode*)b, "icon_checksum");
				purple_buddy_icon_set_data(icon, data, len, checksum);
			}
			else
				delete_buddy_icon_settings((PurpleBlistNode*)b, "buddy_icon");

			g_free(path);
		}

		purple_buddy_icons_set_caching(caching);
	}

	return (icon ? purple_buddy_icon_ref(icon) : NULL);
}

PurpleStoredImage *
purple_buddy_icons_find_account_icon(PurpleAccount *account)
{
	PurpleStoredImage *img;
	const char *account_icon_file;
	const char *dirname;
	char *path;
	guchar *data;
	size_t len;

	g_return_val_if_fail(account != NULL, NULL);

	if ((img = g_hash_table_lookup(pointer_icon_cache, account)))
	{
		return purple_imgstore_ref(img);
	}

	account_icon_file = purple_account_get_string(account, "buddy_icon", NULL);

	if (account_icon_file == NULL)
		return NULL;

	dirname = purple_buddy_icons_get_cache_dir();
	path = g_build_filename(dirname, account_icon_file, NULL);

	if (read_icon_file(path, &data, &len))
	{
		g_free(path);
		img = purple_buddy_icons_set_account_icon(account, data, len);
		return purple_imgstore_ref(img);
	}
	g_free(path);

	return NULL;
}

PurpleStoredImage *
purple_buddy_icons_set_account_icon(PurpleAccount *account,
                                    guchar *icon_data, size_t icon_len)
{
	PurpleStoredImage *old_img;
	PurpleStoredImage *img = NULL;
	char *old_icon;

	if (icon_data != NULL && icon_len > 0)
	{
		img = purple_buddy_icon_data_new(icon_data, icon_len, NULL);
	}

	old_icon = g_strdup(purple_account_get_string(account, "buddy_icon", NULL));
	if (img && purple_buddy_icons_is_caching())
	{
		const char *filename = purple_imgstore_get_filename(img);
		purple_account_set_string(account, "buddy_icon", filename);
		purple_account_set_int(account, "buddy_icon_timestamp", time(NULL));
		ref_filename(filename);
	}
	else
	{
		purple_account_set_string(account, "buddy_icon", NULL);
		purple_account_set_int(account, "buddy_icon_timestamp", 0);
	}
	unref_filename(old_icon);

	old_img = g_hash_table_lookup(pointer_icon_cache, account);

	if (img)
		g_hash_table_insert(pointer_icon_cache, account, img);
	else
		g_hash_table_remove(pointer_icon_cache, account);

	if (purple_account_is_connected(account))
	{
		PurpleConnection *gc;
		PurplePluginProtocolInfo *prpl_info;

		gc = purple_account_get_connection(account);
		prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(purple_connection_get_prpl(gc));

		if (prpl_info && prpl_info->set_buddy_icon)
			prpl_info->set_buddy_icon(gc, img);
	}

	if (old_img)
		purple_imgstore_unref(old_img);
	else if (old_icon)
	{
		/* The old icon may not have been loaded into memory.  In that
		 * case, we'll need to uncache the filename.  The filenames
		 * are ref-counted, so this is safe. */
		purple_buddy_icon_data_uncache_file(old_icon);
	}
	g_free(old_icon);

	return img;
}

time_t
purple_buddy_icons_get_account_icon_timestamp(PurpleAccount *account)
{
	time_t ret;

	g_return_val_if_fail(account != NULL, 0);

	ret = purple_account_get_int(account, "buddy_icon_timestamp", 0);

	/* This deals with migration cases. */
	if (ret == 0 && purple_account_get_string(account, "buddy_icon", NULL) != NULL)
	{
		ret = time(NULL);
		purple_account_set_int(account, "buddy_icon_timestamp", ret);
	}

	return ret;
}

gboolean
purple_buddy_icons_node_has_custom_icon(PurpleBlistNode *node)
{
	g_return_val_if_fail(node != NULL, FALSE);

	return (purple_blist_node_get_string(node, "custom_buddy_icon") != NULL);
}

PurpleStoredImage *
purple_buddy_icons_node_find_custom_icon(PurpleBlistNode *node)
{
	char *path;
	size_t len;
	guchar *data;
	PurpleStoredImage *img;
	const char *custom_icon_file, *dirname;

	g_return_val_if_fail(node != NULL, NULL);

	if ((img = g_hash_table_lookup(pointer_icon_cache, node)))
	{
		return purple_imgstore_ref(img);
	}

	custom_icon_file = purple_blist_node_get_string(node,
	                                                "custom_buddy_icon");

	if (custom_icon_file == NULL)
		return NULL;

	dirname = purple_buddy_icons_get_cache_dir();
	path = g_build_filename(dirname, custom_icon_file, NULL);

	if (read_icon_file(path, &data, &len))
	{
		g_free(path);
		img = purple_buddy_icons_node_set_custom_icon(node, data, len);
		return purple_imgstore_ref(img);
	}
	g_free(path);

	return NULL;
}

PurpleStoredImage *
purple_buddy_icons_node_set_custom_icon(PurpleBlistNode *node,
                                        guchar *icon_data, size_t icon_len)
{
	char *old_icon;
	PurpleStoredImage *old_img;
	PurpleStoredImage *img = NULL;

	g_return_val_if_fail(node != NULL, NULL);

	if (!PURPLE_BLIST_NODE_IS_CONTACT(node) &&
	    !PURPLE_BLIST_NODE_IS_CHAT(node) &&
	    !PURPLE_BLIST_NODE_IS_GROUP(node)) {
		return NULL;
	}

	old_img = g_hash_table_lookup(pointer_icon_cache, node);

	if (icon_data != NULL && icon_len > 0) {
		img = purple_buddy_icon_data_new(icon_data, icon_len, NULL);
	}

	old_icon = g_strdup(purple_blist_node_get_string(node,
	                                                 "custom_buddy_icon"));
	if (img && purple_buddy_icons_is_caching()) {
		const char *filename = purple_imgstore_get_filename(img);
		purple_blist_node_set_string(node, "custom_buddy_icon",
		                             filename);
		ref_filename(filename);
	} else {
		purple_blist_node_remove_setting(node, "custom_buddy_icon");
	}
	unref_filename(old_icon);

	if (img)
		g_hash_table_insert(pointer_icon_cache, node, img);
	else
		g_hash_table_remove(pointer_icon_cache, node);

	if (PURPLE_BLIST_NODE_IS_CONTACT(node)) {
		PurpleBlistNode *child;
		for (child = purple_blist_node_get_first_child(node);
		     child;
			 child = purple_blist_node_get_sibling_next(child))
		{
			PurpleBuddy *buddy;
			PurpleConversation *conv;

			if (!PURPLE_BLIST_NODE_IS_BUDDY(child))
				continue;

			buddy = (PurpleBuddy *)child;

			conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
			if (conv)
				purple_conversation_update(conv, PURPLE_CONV_UPDATE_ICON);

			/* Is this call necessary anymore? Can the buddies
			 * themselves need updating when the custom buddy
			 * icon changes? */
			purple_blist_update_node_icon((PurpleBlistNode*)buddy);
		}
	} else if (PURPLE_BLIST_NODE_IS_CHAT(node)) {
		PurpleConversation *conv = NULL;

		conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, purple_chat_get_name((PurpleChat*)node), purple_chat_get_account((PurpleChat*)node));
		if (conv) {
			purple_conversation_update(conv, PURPLE_CONV_UPDATE_ICON);
		}
	}

	purple_blist_update_node_icon(node);

	if (old_img) {
		purple_imgstore_unref(old_img);
	} else if (old_icon) {
		/* The old icon may not have been loaded into memory.  In that
		 * case, we'll need to uncache the filename.  The filenames
		 * are ref-counted, so this is safe. */
		purple_buddy_icon_data_uncache_file(old_icon);
	}
	g_free(old_icon);

	return img;
}

PurpleStoredImage *
purple_buddy_icons_node_set_custom_icon_from_file(PurpleBlistNode *node,
                                                  const gchar *filename)
{
	size_t len = 0;
	guchar *data = NULL;

	g_return_val_if_fail(node != NULL, NULL);

	if (!PURPLE_BLIST_NODE_IS_CONTACT(node) &&
	    !PURPLE_BLIST_NODE_IS_CHAT(node) &&
	    !PURPLE_BLIST_NODE_IS_GROUP(node)) {
		return NULL;
	}

	if (filename != NULL) {
		if (!read_icon_file(filename, &data, &len)) {
			return NULL;
		}
	}

	return purple_buddy_icons_node_set_custom_icon(node, data, len);
}

gboolean
purple_buddy_icons_has_custom_icon(PurpleContact *contact)
{
	return purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact);
}

PurpleStoredImage *
purple_buddy_icons_find_custom_icon(PurpleContact *contact)
{
	return purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
}

PurpleStoredImage *
purple_buddy_icons_set_custom_icon(PurpleContact *contact, guchar *icon_data,
                                   size_t icon_len)
{
	return purple_buddy_icons_node_set_custom_icon((PurpleBlistNode*)contact, icon_data, icon_len);
}

void
_purple_buddy_icon_set_old_icons_dir(const char *dirname)
{
	old_icons_dir = g_strdup(dirname);
}

static void
delete_buddy_icon_settings(PurpleBlistNode *node, const char *setting_name)
{
	purple_blist_node_remove_setting(node, setting_name);

	if (purple_strequal(setting_name, "buddy_icon"))
	{
		purple_blist_node_remove_setting(node, "avatar_hash");
		purple_blist_node_remove_setting(node, "icon_checksum");
	}
}

static void
migrate_buddy_icon(PurpleBlistNode *node, const char *setting_name,
                   const char *dirname, const char *filename)
{
	char *path;

	if (filename[0] != '/')
	{
		path = g_build_filename(dirname, filename, NULL);
		if (g_file_test(path, G_FILE_TEST_EXISTS))
		{
			g_free(path);
			return;
		}
		g_free(path);

		path = g_build_filename(old_icons_dir, filename, NULL);
	}
	else
		path = g_strdup(filename);

	if (g_file_test(path, G_FILE_TEST_EXISTS))
	{
		guchar *icon_data;
		size_t icon_len;
		FILE *file;
		char *new_filename;

		if (!read_icon_file(path, &icon_data, &icon_len))
		{
			g_free(path);
			delete_buddy_icon_settings(node, setting_name);
			return;
		}

		if (icon_data == NULL || icon_len <= 0)
		{
			/* This really applies to the icon_len check.
			 * icon_data should never be NULL if
			 * read_icon_file() returns TRUE. */
			purple_debug_error("buddyicon", "Empty buddy icon file: %s\n", path);
			delete_buddy_icon_settings(node, setting_name);
			g_free(path);
			return;
		}

		new_filename = purple_util_get_image_filename(icon_data, icon_len);
		if (new_filename == NULL)
		{
			purple_debug_error("buddyicon",
				"New icon filename is NULL. This should never happen! "
				"The old filename was: %s\n", path);
			g_free(path);
			delete_buddy_icon_settings(node, setting_name);

			g_return_if_reached();
		}

		g_free(path);
		path = g_build_filename(dirname, new_filename, NULL);
		if ((file = g_fopen(path, "wb")) != NULL)
		{
			if (!fwrite(icon_data, icon_len, 1, file))
			{
				purple_debug_error("buddyicon", "Error writing %s: %s\n",
				                   path, g_strerror(errno));
			}
			else
				purple_debug_info("buddyicon", "Wrote migrated cache file: %s\n", path);

			fclose(file);
		}
		else
		{
			purple_debug_error("buddyicon", "Unable to create file %s: %s\n",
			                   path, g_strerror(errno));
			g_free(new_filename);
			g_free(path);

			delete_buddy_icon_settings(node, setting_name);
			return;
		}
		g_free(path);

		purple_blist_node_set_string(node,
		                             setting_name,
		                             new_filename);
		ref_filename(new_filename);

		g_free(new_filename);

		if (purple_strequal(setting_name, "buddy_icon"))
		{
			const char *hash;

			hash = purple_blist_node_get_string(node, "avatar_hash");
			if (hash != NULL)
			{
				purple_blist_node_set_string(node, "icon_checksum", hash);
				purple_blist_node_remove_setting(node, "avatar_hash");
			}
		}
	}
	else
	{
		purple_debug_error("buddyicon", "Old icon file doesn't exist: %s\n", path);
		delete_buddy_icon_settings(node, setting_name);
		g_free(path);
	}
}

void
_purple_buddy_icons_account_loaded_cb()
{
	const char *dirname = purple_buddy_icons_get_cache_dir();
	GList *cur;

	for (cur = purple_accounts_get_all(); cur != NULL; cur = cur->next)
	{
		PurpleAccount *account = cur->data;
		const char *account_icon_file = purple_account_get_string(account, "buddy_icon", NULL);

		if (account_icon_file != NULL)
		{
			char *path = g_build_filename(dirname, account_icon_file, NULL);
			if (!g_file_test(path, G_FILE_TEST_EXISTS))
			{
				purple_account_set_string(account, "buddy_icon", NULL);
			} else {
				ref_filename(account_icon_file);
			}
			g_free(path);
		}
	}
}

void
_purple_buddy_icons_blist_loaded_cb()
{
	PurpleBlistNode *node = purple_blist_get_root();
	const char *dirname = purple_buddy_icons_get_cache_dir();

	/* Doing this once here saves having to check it inside a loop. */
	if (old_icons_dir != NULL)
	{
		if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
		{
			purple_debug_info("buddyicon", "Creating icon cache directory.\n");

			if (g_mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
			{
				purple_debug_error("buddyicon",
				                   "Unable to create directory %s: %s\n",
				                   dirname, g_strerror(errno));
			}
		}
	}

	while (node != NULL)
	{
		if (PURPLE_BLIST_NODE_IS_BUDDY(node))
		{
			const char *filename;

			filename = purple_blist_node_get_string(node, "buddy_icon");
			if (filename != NULL)
			{
				if (old_icons_dir != NULL)
				{
					migrate_buddy_icon(node,
					                   "buddy_icon",
					                   dirname, filename);
				}
				else
				{
					char *path = g_build_filename(dirname, filename, NULL);
					if (!g_file_test(path, G_FILE_TEST_EXISTS))
					{
						purple_blist_node_remove_setting(node,
						                                 "buddy_icon");
						purple_blist_node_remove_setting(node,
						                                 "icon_checksum");
					}
					else
						ref_filename(filename);
					g_free(path);
				}
			}
		}
		else if (PURPLE_BLIST_NODE_IS_CONTACT(node) ||
		         PURPLE_BLIST_NODE_IS_CHAT(node) ||
		         PURPLE_BLIST_NODE_IS_GROUP(node))
		{
			const char *filename;

			filename = purple_blist_node_get_string(node, "custom_buddy_icon");
			if (filename != NULL)
			{
				if (old_icons_dir != NULL)
				{
					migrate_buddy_icon(node,
					                   "custom_buddy_icon",
					                   dirname, filename);
				}
				else
				{
					char *path = g_build_filename(dirname, filename, NULL);
					if (!g_file_test(path, G_FILE_TEST_EXISTS))
					{
						purple_blist_node_remove_setting(node,
						                                 "custom_buddy_icon");
					}
					else
						ref_filename(filename);
					g_free(path);
				}
			}
		}
		node = purple_blist_node_next(node, TRUE);
	}
}

void
purple_buddy_icons_set_caching(gboolean caching)
{
	icon_caching = caching;
}

gboolean
purple_buddy_icons_is_caching(void)
{
	return icon_caching;
}

void
purple_buddy_icons_set_cache_dir(const char *dir)
{
	g_return_if_fail(dir != NULL);

	g_free(cache_dir);
	cache_dir = g_strdup(dir);
}

const char *
purple_buddy_icons_get_cache_dir(void)
{
	return cache_dir;
}

void *
purple_buddy_icons_get_handle()
{
	static int handle;

	return &handle;
}

void
purple_buddy_icons_init()
{
	account_cache = g_hash_table_new_full(
		g_direct_hash, g_direct_equal,
		NULL, (GFreeFunc)g_hash_table_destroy);

	icon_data_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
	                                        g_free, NULL);
	icon_file_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
	                                        g_free, NULL);
	pointer_icon_cache = g_hash_table_new(g_direct_hash, g_direct_equal);

	if (!cache_dir)
		cache_dir = g_build_filename(purple_user_dir(), "icons", NULL);

	purple_signal_connect(purple_imgstore_get_handle(), "image-deleting",
	                      purple_buddy_icons_get_handle(),
	                      G_CALLBACK(image_deleting_cb), NULL);
}

void
purple_buddy_icons_uninit()
{
	purple_signals_disconnect_by_handle(purple_buddy_icons_get_handle());

	g_hash_table_destroy(account_cache);
	g_hash_table_destroy(icon_data_cache);
	g_hash_table_destroy(icon_file_cache);
	g_hash_table_destroy(pointer_icon_cache);
	g_free(old_icons_dir);
	g_free(cache_dir);

	cache_dir = NULL;
	old_icons_dir = NULL;
}

void purple_buddy_icon_get_scale_size(PurpleBuddyIconSpec *spec, int *width, int *height)
{
	int new_width, new_height;

	new_width = *width;
	new_height = *height;

	if (*width < spec->min_width)
		new_width = spec->min_width;
	else if (*width > spec->max_width)
		new_width = spec->max_width;

	if (*height < spec->min_height)
		new_height = spec->min_height;
	else if (*height > spec->max_height)
		new_height = spec->max_height;

	/* preserve aspect ratio */
	if ((double)*height * (double)new_width >
		(double)*width * (double)new_height) {
			new_width = 0.5 + (double)*width * (double)new_height / (double)*height;
	} else {
			new_height = 0.5 + (double)*height * (double)new_width / (double)*width;
	}

	*width = new_width;
	*height = new_height;
}
