/*
 * Album (Buddy Icon Archiver)
 *
 * Copyright (C) 2005-2008, Sadrul Habib Chowdhury <imadil@gmail.com>
 * Copyright (C) 2005-2008, Richard Laager <rlaager@pidgin.im>
 * Copyright (C) 2006, Jérôme Poulin (TiCPU) <jeromepoulin@gmail.com>
 *
 * 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.
 */

#include "album-ui.h"

/* We want to use the gstdio functions when possible so that non-ASCII
 * filenames are handled properly on Windows. */
#include <glib/gstdio.h>

#include <gtk/gtk.h>
#include <gtk/gtkwidget.h>

#include <sys/stat.h>
#include <string.h>

#include <account.h>
#include <debug.h>
#include <gtkblist.h>
#include <pidgin.h>
#include <plugin.h>
#include <request.h>
#include <util.h>

/* XXX: For DATADIR... There must be a better way. */
#ifdef _WIN32
#include <win32/win32dep.h>
#endif

/* The increment between sizes. */
#ifndef ICON_SIZE_MULTIPLIER
#define ICON_SIZE_MULTIPLIER 32
#endif

/* The size of the icon in the icon viewer's label. */
#ifndef LABEL_ICON_SIZE
#define LABEL_ICON_SIZE 24
#endif

/* How much to pad between two rows. */
#ifndef ROW_PADDING
#define ROW_PADDING 0
#endif

/* Padding around the vbox containing the image and text. */
#ifndef VBOX_BORDER
#define VBOX_BORDER 10
#endif

/* Spacing between children in the vbox containing the image and text. */
#ifndef VBOX_SPACING
#define VBOX_SPACING 5
#endif

/* Padding around the text label for an icon. */
#ifndef LABEL_PADDING
#define LABEL_PADDING 3
#endif

/* Padding around the image. */
#ifndef IMAGE_PADDING
#define IMAGE_PADDING 3
#endif

typedef struct _BuddyIcon BuddyIcon;
typedef struct _BuddyWindow BuddyWindow;
typedef struct _icon_viewer_key icon_viewer_key;

struct _BuddyIcon
{
	char *full_path;
	time_t timestamp;

	/* For suggesting a filename on image save. */
	char *buddy_name;
};

struct _BuddyWindow
{
	GtkWidget *window;
	GtkWidget *vbox;
	GtkWidget *iconview;
	GtkTextBuffer *text_buffer;
	int text_height;
	int text_width;
	GtkRequisition requisition;
};

/* contact or ((account and screenname) and possibly buddy) will be set.*/
struct _icon_viewer_key
{
	PurpleContact *contact;

	PurpleBuddy *buddy;

	PurpleAccount *account;
	char *screenname;
	GList *list;
};

static void update_icon_view(icon_viewer_key *key);
static void show_buddy_icon_window(icon_viewer_key *key, const char *name);

void icon_viewer_key_free(void *data)
{
	icon_viewer_key *key = (icon_viewer_key *)data;
	g_free(key->screenname);
	g_free(key);
}

guint icon_viewer_hash(gconstpointer data)
{
	const icon_viewer_key *key = data;

	if (key->contact != NULL)
		return g_direct_hash(key->contact);

	return g_str_hash(key->screenname) +
		g_str_hash(purple_account_get_username(key->account));
}

gboolean icon_viewer_equal(gconstpointer y, gconstpointer z)
{
	const icon_viewer_key *a, *b;

	a = y;
	b = z;

	if (a->contact != NULL)
	{
		if (b->contact != NULL)
			return (a->contact == b->contact);
		else
			return FALSE;
	}
	else if (b->contact != NULL)
		return FALSE;

	if (a->account == b->account)
	{
		char *normal = g_strdup(purple_normalize(a->account, a->screenname));
		if (!strcmp(normal, purple_normalize(b->account, b->screenname)))
		{
			g_free(normal);
			return TRUE;
		}
		g_free(normal);
	}

	return FALSE;
}

static void set_window_geometry(BuddyWindow *bw, int buddy_icon_size)
{
	GdkGeometry geom;

	g_return_if_fail(bw != NULL);

	/* Set the window geometry.  This controls window resizing. */
	geom.base_width  = bw->requisition.width  + 40; /* XXX: Where is the hardcoded value coming from? */
	geom.base_height = bw->requisition.height + 18; /* XXX: Where is the hardcoded value coming from? */
	geom.width_inc   = MAX(buddy_icon_size, bw->text_width) + 2 * VBOX_BORDER;
	geom.height_inc  = ROW_PADDING + 2 * VBOX_BORDER + buddy_icon_size + 2 * IMAGE_PADDING + VBOX_SPACING + bw->text_height + 2 * LABEL_PADDING;
	geom.min_width   = geom.base_width  + 3 * geom.width_inc;  /* Minimum size: 3 wide */
	geom.min_height  = geom.base_height +     geom.height_inc; /* Minimum size: 1 high */
	gtk_window_set_geometry_hints(GTK_WINDOW(bw->window), bw->vbox, &geom,
	                              GDK_HINT_MIN_SIZE | GDK_HINT_RESIZE_INC | GDK_HINT_BASE_SIZE);
}

static gboolean resize_icons(GtkWidget *combo, icon_viewer_key *key)
{
	int sel = gtk_combo_box_get_active(GTK_COMBO_BOX(combo));
	BuddyWindow *bw;

	switch(sel)
	{
		case 0: /* Small */
		case 1: /* Medium */
		case 2: /* Large */
			purple_prefs_set_int(PREF_ICON_SIZE, sel);
			break;
		default:
			g_return_val_if_reached(FALSE);
	}
	update_icon_view(key);

	bw = g_hash_table_lookup(buddy_windows, key);
	g_return_val_if_fail(bw != NULL, FALSE);
	set_window_geometry(bw, ICON_SIZE_MULTIPLIER * (sel + 1));

	return FALSE;
}

/* Save the size of the window. */
static gboolean update_size(GtkWidget *win, GdkEventConfigure *event, gpointer data)
{
	int w;
	int h;

	gtk_window_get_size(GTK_WINDOW(win), &w, &h);

	purple_prefs_set_int(PREF_WINDOW_WIDTH, w);
	purple_prefs_set_int(PREF_WINDOW_HEIGHT, h);

	/* We want the normal handling to continue. */
	return FALSE;
}

static gboolean window_close(GtkWidget *win, gint resp, icon_viewer_key *key)
{
	g_hash_table_remove(buddy_windows, key);
	gtk_widget_destroy(win);
	return TRUE;
}

/* Returns a scroller window which contains widget. */
static GtkWidget *wrap_in_scroller(GtkWidget *widget)
{
	GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);

	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller),
	                               GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroller),
	                                    GTK_SHADOW_IN);

	gtk_container_add(GTK_CONTAINER(scroller), widget);

	return scroller;
}

/* Based on Purple's gtkimhtml.c, image_save_yes_cb(). */
static void convert_image(GtkImage *image, const char *filename)
{
	gchar *type = NULL;
	GError *error = NULL;
	GSList *formats = gdk_pixbuf_get_formats();

	while (formats)
	{
		GdkPixbufFormat *format = formats->data;
		gchar **extensions = gdk_pixbuf_format_get_extensions(format);
		gpointer p = extensions;

		while (gdk_pixbuf_format_is_writable(format) && extensions && extensions[0])
		{
			gchar *fmt_ext = extensions[0];
			const gchar *file_ext = filename + strlen(filename) - strlen(fmt_ext);

			if (!strcmp(fmt_ext, file_ext))
			{
				type = gdk_pixbuf_format_get_name(format);
				break;
			}

			extensions++;
		}

		g_strfreev(p);

		if (type)
			break;

		formats = formats->next;
	}

	g_slist_free(formats);

	if (!type)
	{
		GtkWidget *dialog;

		/* Present an error dialog. */
		dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
		                                            _("<span size='larger' weight='bold'>Unrecognized file type</span>\n\nDefaulting to PNG."));
		g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
		gtk_widget_show(dialog);

		/* Assume the user wants a PNG. */
		type = g_strdup("png");
	}

	gdk_pixbuf_save(gtk_image_get_pixbuf(image), filename, type, &error, NULL);

	if (error)
	{
		GtkWidget *dialog;

		/* Present an error dialog. */
		dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
		                                            _("<span size='larger' weight='bold'>Error saving image</span>\n\n%s"),
		                                            error->message);
		g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
		gtk_widget_show(dialog);

		g_error_free(error);
	}

	g_free(type);
}

static void
image_save_cb(GtkWidget *widget, gint response, GtkImage *image)
{
	char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
	char *image_name = g_object_get_data(G_OBJECT(image), "filename");

	gtk_widget_destroy(widget);

	if (response != GTK_RESPONSE_ACCEPT)
		return;

	purple_debug_misc(PLUGIN_STATIC_NAME, "Saving image %s as: %s\n", image_name, filename);

	/* Keeping this crud out of this function is a Good Thing (TM). */
	convert_image(image, filename);

	g_free(filename);
}

static void save_dialog(GtkWidget *widget, GtkImage *image)
{
	GtkWidget *dialog;
	const char *ext;
	char *filename;

	dialog = gtk_file_chooser_dialog_new(_("Save Image"),
	                                     NULL,
	                                     GTK_FILE_CHOOSER_ACTION_SAVE,
	                                     GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
	                                     GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
	                                     NULL);
	gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);

	/* Determine the extension. */
	ext = g_object_get_data(G_OBJECT(image), "filename");
	if (ext)
		ext = strrchr(ext, '.');
	if (ext == NULL)
		ext = "";

	filename = g_strdup_printf("%s%s", purple_escape_filename(g_object_get_data(G_OBJECT(image), "buddy_name")), ext);
	gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), filename);
	g_free(filename);

	g_signal_connect(G_OBJECT(GTK_FILE_CHOOSER(dialog)), "response",
					 G_CALLBACK(image_save_cb), image);

	gtk_widget_show(dialog);
}

static gboolean save_menu(GtkWidget *event_box, GdkEventButton *event, GtkImage *image)
{
	GtkWidget *menu = gtk_menu_new();
	GtkWidget *item = gtk_image_menu_item_new_with_mnemonic("_Save Icon");

	gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
	                              gtk_image_new_from_stock(GTK_STOCK_SAVE, GTK_ICON_SIZE_MENU));

	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
	g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(save_dialog), image);

	gtk_widget_show_all(menu);

	gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, event_box, 3, event->time);

	return FALSE;
}

static gint buddy_icon_compare(BuddyIcon *b1, BuddyIcon *b2)
{
	gint ret;

	/* NOTE: This sorts by timestamp DESCENDING. */
	if ((ret = b2->timestamp - b1->timestamp) != 0)
		return ret;

	/* This isn't strictly necessary, but it
	 * ensures the icon order is stable. */
	return strcmp(b1->full_path, b2->full_path);
}

/* Returns an unsorted list of available icons for buddy (as BuddyIcon *).
 * The items need to be freed.
 */
static GList *retrieve_icons(PurpleAccount *account, const char *name)
{
	char *path;
	GDir *dir;
	const char *filename;
	GList *list = NULL;

	path = album_buddy_icon_get_dir(account, name);
	if (path == NULL)
	{
		purple_debug_warning(PLUGIN_STATIC_NAME, "Path for buddy %s not found.\n", name);
		return NULL;
	}

	if (!(dir = g_dir_open(path, 0, NULL)))
	{
		purple_debug_warning(PLUGIN_STATIC_NAME, "Could not open path: %s\n", path);
		g_free(path);
		return NULL;
	}

	while ((filename = g_dir_read_name(dir)))
	{
		char *full_path = g_build_filename(path, filename, NULL);
		struct stat st;
		BuddyIcon *icon;

		/* We need the file's timestamp. */
		if (stat(full_path, &st) != 0)
		{
			g_free(full_path);
			continue;
		}

		icon = g_new0(BuddyIcon, 1);
		icon->full_path = full_path;
		icon->timestamp = st.st_mtime;
		icon->buddy_name = g_strdup(name);

		list = g_list_prepend(list, icon);
	}

	g_dir_close(dir);

	g_free(path);
	return list;
}

static gboolean add_icon_from_list_cb(gpointer data)
{
	GtkTextIter text_iter;
	int buddy_icon_pref = purple_prefs_get_int(PREF_ICON_SIZE);
	int buddy_icon_size;
	BuddyWindow *bw;
	GtkTextBuffer *text_buffer;
	GtkTextIter start, end;
	GtkWidget *iconview;
	icon_viewer_key *key = data;
	BuddyIcon *icon;
	GdkPixbuf *pixbuf;
	int width;
	int height;
	int xpad = 0;
	int ypad = 0;
	GdkPixbuf *scaled;
	GtkWidget *image;
	GtkWidget *event_box;
	GtkWidget *alignment;
	GtkWidget *widget;
	GtkWidget *label;
	const char *timestamp;
	GtkTextChildAnchor *anchor;
	BuddyIcon *prev_icon;
	char *prev_icon_filename;
	GList *l;


	/* If we're out of icons, kill this idle callback. */
	if (key->list == NULL)
		return FALSE;

	bw = g_hash_table_lookup(buddy_windows, key);
	g_return_val_if_fail(bw != NULL, FALSE);

	text_buffer = bw->text_buffer;
	iconview = bw->iconview;

	/* Clamp the pref value to the allowable range. */
	buddy_icon_pref = CLAMP(buddy_icon_pref, 0, 2);

	buddy_icon_size = ICON_SIZE_MULTIPLIER * (buddy_icon_pref + 1);

	gtk_text_buffer_get_end_iter(text_buffer, &text_iter);


	/* Duplicate Removal */

	prev_icon = key->list->data;
	/* Technically, this will yield "/filename.ext", but the
	 * code below will get the same thing, so it'll compare fine. */
	prev_icon_filename = strrchr(prev_icon->full_path, '/');
	if (prev_icon_filename == NULL)
	{
		/* This should never happen. */
		prev_icon_filename = prev_icon->full_path;
	}

	for (l = key->list->next ; l != NULL ; l = l->next)
	{
		BuddyIcon *this_icon = l->data;
		char *this_icon_filename = strrchr(this_icon->full_path, '/');

		if (this_icon_filename == NULL)
		{
			/* This should never happen. */
			this_icon_filename = this_icon->full_path;
		}

		/* The files are named by hash, so if they have the
		 * same basename, we can assume they have the same
		 * contents.  This happens when someone uses the
		 * same icon on multiple accounts.  We only want to
		 * show each icon once.
		 */
		if (!strcmp(this_icon_filename, prev_icon_filename))
		{
			key->list = g_list_delete_link(key->list, l);
		}
	}


	/* Pop off one icon to add. */
	icon = key->list->data;
	key->list = g_list_delete_link(key->list, key->list);

	pixbuf = gdk_pixbuf_new_from_file(icon->full_path, NULL);
	if (pixbuf == NULL)
	{
		purple_debug_warning(PLUGIN_STATIC_NAME, "Invalid image file: %s\n", icon->full_path);
		g_free(icon->full_path);
		g_free(icon->buddy_name);
		g_free(icon);
		return TRUE;
	}

	width = gdk_pixbuf_get_width(pixbuf);
	height = gdk_pixbuf_get_height(pixbuf);

	/* Never scale the image up. */
	if (height > buddy_icon_size || width > buddy_icon_size)
	{
		/* Scale the image proportionally to fit with a square of size buddy_icon_size. */
		if (width > height)
		{
				int new_height = (int)(buddy_icon_size / (double)width * height);
				ypad = buddy_icon_size - new_height;
				scaled = gdk_pixbuf_scale_simple(pixbuf, buddy_icon_size, new_height, GDK_INTERP_BILINEAR);
		}
		else
		{
				int new_width = (int)(buddy_icon_size / (double)height * width);
				xpad = buddy_icon_size - new_width;
				scaled = gdk_pixbuf_scale_simple(pixbuf, new_width, buddy_icon_size, GDK_INTERP_BILINEAR);
		}
		g_object_unref(G_OBJECT(pixbuf));
	}
	else
	{
		ypad = buddy_icon_size - height;
		xpad = buddy_icon_size - width;
		scaled = pixbuf;
	}


	/* Now we're ready to add the icon. */

	/* Create the image and an event box to which to attach the right-click menu. */
	image = gtk_image_new_from_pixbuf(scaled);
	g_object_unref(G_OBJECT(scaled));
	event_box = gtk_event_box_new();
	gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
	gtk_container_add(GTK_CONTAINER(event_box), image);

	/* Save the filename and create a right-click handler. */
	g_object_set_data_full(G_OBJECT(image), "buddy_name", icon->buddy_name, g_free);
	g_object_set_data_full(G_OBJECT(image), "filename", icon->full_path, g_free);
	g_signal_connect(G_OBJECT(event_box), "button-press-event",
					 G_CALLBACK(save_menu), image);

	/* Add padding as required. */
	alignment = gtk_alignment_new(0.5, 0.5, 0, 0);
	/* The + 1 is in case the padding is odd. It ensures that one side will get the extra one pixel so that
	 * the padding is always correct. Without it, we'd be one pixel short whenever xpad or ypad was odd. */
	gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), ypad / 2, (ypad + 1) / 2, xpad / 2, (xpad + 1) / 2);
	gtk_container_add(GTK_CONTAINER(alignment), event_box);

	widget = gtk_vbox_new(FALSE, VBOX_SPACING);
	gtk_container_set_border_width(GTK_CONTAINER(widget), VBOX_BORDER);
	gtk_box_pack_start(GTK_BOX(widget), alignment, FALSE, FALSE, IMAGE_PADDING);

	/* Label */
	timestamp = purple_utf8_strftime(_("%x\n%X"),  localtime(&icon->timestamp));
	label = gtk_label_new(NULL);
	gtk_label_set_text(GTK_LABEL(label), timestamp);
	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
	gtk_box_pack_start(GTK_BOX(widget), label, TRUE, TRUE, LABEL_PADDING);

	anchor = gtk_text_buffer_create_child_anchor(text_buffer, &text_iter);
	gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(iconview), widget, anchor);

	gtk_widget_show_all(widget);
	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
	gtk_text_buffer_apply_tag_by_name(text_buffer, "word_wrap", &start, &end);

	g_free(icon);
	return TRUE;
}

static void update_icon_view(icon_viewer_key *key)
{
	GtkTextIter start;
	GtkTextIter end;
	GList *list = NULL;
	BuddyWindow *bw;
	GtkWidget *iconview;
	GtkTextBuffer *text_buffer;
	gboolean success = FALSE;

	bw = g_hash_table_lookup(buddy_windows, key);
	g_return_if_fail(bw != NULL);
	iconview = bw->iconview;
	text_buffer = bw->text_buffer;

	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
	gtk_text_buffer_delete(text_buffer, &start, &end);

	if (key->contact != NULL)
	{
		PurpleBlistNode *bnode;

		/* Get the list of all the accounts of the contact and sort it. */
		for (bnode = ((PurpleBlistNode *)key->contact)->child; bnode ; bnode = bnode->next)
		{
			list = g_list_concat(retrieve_icons(purple_buddy_get_account((PurpleBuddy *)bnode),
			                                    purple_buddy_get_name((PurpleBuddy *)bnode)), list);
		}
	}
	else if (key->buddy != NULL)
	{
		list = retrieve_icons(purple_buddy_get_account(key->buddy),
		                      purple_buddy_get_name(key->buddy));
	}
	else
	{
		list = retrieve_icons(key->account, key->screenname);
	}

	/* Show icons for all the accounts of the contact. */
	if (list != NULL)
	{
		int id;

		list = g_list_sort(list, (GCompareFunc)buddy_icon_compare);
		success = (list != NULL);
		key->list = list;

		/* It's possible we already have one of these loops running, but
		 * having two won't harm anything, so we don't do anything about it. */
		id = g_idle_add(add_icon_from_list_cb, key);
		g_object_set_data_full(G_OBJECT(iconview), "update-idle-callback",
					GINT_TO_POINTER(id), (GDestroyNotify)g_source_remove);
	}

	if (!success)
	{
		/* No icons were found.  Display an appropriate message. */
		GtkWidget *hbox;
		char *filename;
		GdkPixbuf *pixbuf;
		GdkPixbuf *scaled;
		GtkWidget *image;
		char *str;
		GtkWidget *label;
		GtkTextIter text_iter;
		GtkTextChildAnchor *anchor;

		/* Hbox */
		/* Reuse spacing information that's used for the vbox which contains the image and text for each icon. */
		hbox = gtk_hbox_new(FALSE, VBOX_SPACING);
		gtk_container_set_border_width(GTK_CONTAINER(hbox), VBOX_BORDER);

		/* Image */
#ifndef _WIN32
		filename = g_build_filename(PIXMAPSDIR, "dialogs", "purple_info.png", NULL);
#else
		filename = g_build_filename(wpurple_install_dir(), "pixmaps", "pidgin", "dialogs", "purple_info.png", NULL);
#endif

		pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
		g_free(filename);

		scaled = gdk_pixbuf_scale_simple(pixbuf, 48, 48, GDK_INTERP_BILINEAR);
		g_object_unref(G_OBJECT(pixbuf));

		image = gtk_image_new_from_pixbuf(scaled);
		g_object_unref(G_OBJECT(scaled));
		gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);


		/* Label */
		str = g_strdup_printf("<span size='larger' weight='bold'>%s</span>", _("No icons were found."));
		label = gtk_label_new(NULL);
		gtk_label_set_markup(GTK_LABEL(label), str);
		g_free(str);

		gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
		gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);


		/* Add the hbox to the text view. */
		gtk_text_buffer_get_iter_at_offset (text_buffer, &text_iter, 0);
		anchor = gtk_text_buffer_create_child_anchor(text_buffer, &text_iter);
		gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(iconview), hbox, anchor);
	}

	gtk_widget_show_all(iconview);
	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(iconview), FALSE);
}

static void update_runtime(icon_viewer_key *key, gpointer value, PurpleBuddy *buddy)
{
	PurpleAccount *account = purple_buddy_get_account(buddy);

	if (key->contact != NULL)
	{
		char *name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
		PurpleBlistNode *node;

		for (node = ((PurpleBlistNode *)key->contact)->child; node; node = node->next)
		{
			if (account == purple_buddy_get_account((PurpleBuddy *)node) &&
			    !strcmp(name, purple_normalize(account, purple_buddy_get_name((PurpleBuddy *)node))))
			{
				g_free(name);
				update_icon_view(key);
				return;
			}
		}
		g_free(name);
	}
	else if (account == key->account &&
	         !strcmp(key->screenname, purple_normalize(account, purple_buddy_get_name(buddy))))
	{
		update_icon_view(key);
	}
}

void album_update_runtime(PurpleBuddy *buddy, gpointer data)
{
	g_hash_table_foreach(buddy_windows, (GHFunc)update_runtime, buddy);
}

static GtkWidget *get_viewer_icon()
{
	char *filename = NULL;
	GdkPixbuf *pixbuf;
	int width;
	int height;
	GdkPixbuf *scaled;
	GtkWidget *image;

	if (filename == NULL)
#ifndef _WIN32
		filename = g_build_filename(PIXMAPSDIR, "icons", "online.png", NULL);
#else
		filename = g_build_filename(wpurple_install_dir(), "pixmaps", "pidgin", "icons", "online.png", NULL);
#endif

	pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
	g_free(filename);

	width = gdk_pixbuf_get_width(pixbuf);
	height = gdk_pixbuf_get_height(pixbuf);

	/* Never scale the image up. */
	if (height > LABEL_ICON_SIZE || width > LABEL_ICON_SIZE)
	{
		/* Scale the image proportionally to fit with a square of size buddy_icon_size. */
		if (width > height)
		{
				int new_height = (int)(LABEL_ICON_SIZE / (double)width * height);
				scaled = gdk_pixbuf_scale_simple(pixbuf, LABEL_ICON_SIZE, new_height, GDK_INTERP_BILINEAR);
		}
		else
		{
				int new_width = (int)(LABEL_ICON_SIZE / (double)height * width);
				scaled = gdk_pixbuf_scale_simple(pixbuf, new_width, LABEL_ICON_SIZE, GDK_INTERP_BILINEAR);
		}
		g_object_unref(G_OBJECT(pixbuf));
	}
	else
	{
		scaled = pixbuf;
	}

	image = gtk_image_new_from_pixbuf(scaled);
	g_object_unref(G_OBJECT(scaled));
	return image;
}

static void view_buddy_icons_cb(PurpleBlistNode *node, gpointer data)
{
	gboolean contact_expanded;
	icon_viewer_key *key = g_new0(icon_viewer_key, 1);
	const char *name;

	g_return_if_fail(node != NULL);

	if(PURPLE_BLIST_NODE_HAS_FLAG(node, PURPLE_BLIST_NODE_FLAG_NO_SAVE))
		return;

	contact_expanded = pidgin_blist_node_is_contact_expanded(node);

	if (PURPLE_BLIST_NODE_IS_BUDDY(node))
	{
		if (!contact_expanded)
		{
			/* Work on the contact, since it's collapsed. */

			name = purple_contact_get_alias((PurpleContact*)node->parent);
			if (name == NULL)
				name = purple_buddy_get_name(((PurpleContact *)node->parent)->priority);

			if (node->next != NULL)
			{
				/* The contact has at least two buddies.  */
				key->contact = (PurpleContact *)node->parent;
			}
			else
			{
				/* Treat one-buddy contacts similar to buddies. */
				key->account = purple_buddy_get_account((PurpleBuddy *)node);
				key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node)));
				key->buddy = (PurpleBuddy *)node;
			}
		}
		else
		{
			/* The contact is expanded and the user has chosen a buddy. */

			key->account = purple_buddy_get_account((PurpleBuddy *)node);
			key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node)));
			key->buddy = (PurpleBuddy *)node;

			name = purple_buddy_get_alias_only((PurpleBuddy *)node);
			if (name == NULL)
			{
				name = purple_buddy_get_name((PurpleBuddy *)node);
			}
		}
	}
	else if (PURPLE_BLIST_NODE_IS_CONTACT(node))
	{
		/* The contact is expanded and the user has chosen the contact. */

		if (node->child != NULL && node->child->next != NULL)
		{
			/* The contact has at least two buddies.  */
			key->contact = (PurpleContact *)node;
		}
		else
		{
			/* Treat one-buddy contacts similar to buddies. */
			key->account = purple_buddy_get_account((PurpleBuddy *)node->child);
			key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node->child)));
			key->buddy = (PurpleBuddy *)node->child;
		}

		name = purple_contact_get_alias((PurpleContact*)node);
		if (name == NULL)
			name = purple_buddy_get_name(((PurpleContact *)node)->priority);
	}
	else
		g_return_if_reached();

	show_buddy_icon_window(key, name);
}

static gboolean compare_buddy_keys(icon_viewer_key *key1, BuddyWindow *bw, icon_viewer_key *key2)
{
	g_return_val_if_fail(key2->contact == NULL, FALSE);

	if (key1->contact == NULL)
	{
		if (key1->account == key2->account)
		{
			char *normal = g_strdup(purple_normalize(key1->account, key1->screenname));
			if (!strcmp(normal, purple_normalize(key2->account, key2->screenname)))
			{
				g_free(normal);
				return TRUE;
			}
			g_free(normal);
		}
	}

	return FALSE;
}

static void show_buddy_icon_window(icon_viewer_key *key, const char *name)
{
	char *title;
	GtkWidget *win;
	GtkWidget *vbox;
	char *str;
	GtkTextIter start;
	GtkTextIter end;
	GtkWidget *title_box;
	GtkWidget *label;
	GtkWidget *combo;
	GtkWidget *iconview;
	GtkTextBuffer *text_buffer;
	time_t now;
	const char *timestamp;
	PangoLayout *layout;
	int text_width;
	int text_height;
	int buddy_icon_pref = purple_prefs_get_int(PREF_ICON_SIZE);
	int buddy_icon_size;
	BuddyWindow *bw;

	/* Return if a window is already opened for the buddy. */
	if ((bw = g_hash_table_lookup(buddy_windows, key)) != NULL)
	{
		icon_viewer_key_free(key);
		gtk_window_present(GTK_WINDOW(bw->window));
		return;
	}

	/* If it was a contact, it would've matched above.
	 * Compare screennames...
	 */
	if (key->contact == NULL &&
	    (bw = g_hash_table_find(buddy_windows, (GHRFunc)compare_buddy_keys, key)) != NULL)
	{
		icon_viewer_key_free(key);
		gtk_window_present(GTK_WINDOW(bw->window));
		return;
	}

	/* Clamp the pref value to the allowable range. */
	buddy_icon_pref = MAX(0, buddy_icon_pref);
	buddy_icon_pref = MIN(2, buddy_icon_pref);

	buddy_icon_size = ICON_SIZE_MULTIPLIER * (buddy_icon_pref + 1);

	title = g_strdup_printf(_("Buddy Icons used by %s"), name);

	win = gtk_dialog_new_with_buttons(title, NULL, 0,
	                                  GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
	gtk_window_set_role(GTK_WINDOW(win), "buddy_icon_viewer");
	gtk_container_set_border_width(GTK_CONTAINER(win), 12);

	vbox = gtk_vbox_new(FALSE, 5);
	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(win)->vbox), vbox, TRUE, TRUE, 0);

	iconview = gtk_text_view_new();
	text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(iconview));
	gtk_text_view_set_editable(GTK_TEXT_VIEW(iconview), FALSE);
#if ROW_PADDING != 0
	gtk_text_view_set_pixels_inside_wrap(GTK_TEXT_VIEW(iconview), ROW_PADDING);
#endif
	gtk_text_buffer_create_tag (text_buffer, "word_wrap",
	                            "wrap_mode", GTK_WRAP_WORD, NULL);
	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
	gtk_text_buffer_apply_tag_by_name(text_buffer, "word_wrap", &start, &end);

	/* Get the size of the text of a sample timestamp. */
	now = time(NULL);
	timestamp = purple_utf8_strftime("%x\n%X",  localtime(&now));
	layout = gtk_widget_create_pango_layout(iconview, timestamp);
	pango_layout_get_pixel_size(layout, &text_width, &text_height);


	/* Title Box */
	title_box = gtk_hbox_new(FALSE, 6);
	gtk_container_set_border_width(GTK_CONTAINER(title_box), 6);
	gtk_box_pack_start(GTK_BOX(vbox), title_box, FALSE, FALSE, 0);


	/* Icon */
	gtk_box_pack_start(GTK_BOX(title_box), get_viewer_icon(), FALSE, FALSE, 0);


	/* Label */
	str = g_strdup_printf("<span size='larger' weight='bold'>%s</span>", title);
	g_free(title);

	label = gtk_label_new(NULL);
	gtk_label_set_markup(GTK_LABEL(label), str);
	g_free(str);

	gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
	gtk_box_pack_start(GTK_BOX(title_box), label, FALSE, FALSE, 0);


	/* Icon View */
	gtk_box_pack_start(GTK_BOX(vbox), wrap_in_scroller(iconview), TRUE, TRUE, 0);


	/* Size Selector */
	combo = gtk_combo_box_new_text();

	str = g_strdup_printf(_("Small (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER);
	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
	g_free(str);

	str = g_strdup_printf(_("Medium (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER * 2);
	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
	g_free(str);

	str = g_strdup_printf(_("Large (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER * 3);
	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
	g_free(str);

	gtk_combo_box_set_active(GTK_COMBO_BOX(combo), buddy_icon_pref);

	gtk_widget_show_all(combo);
	gtk_signal_connect(GTK_OBJECT(combo), "changed", GTK_SIGNAL_FUNC(resize_icons), key);

	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(win)->action_area), combo, FALSE, FALSE, 0);
	gtk_box_reorder_child(GTK_BOX(GTK_DIALOG(win)->action_area), combo, 0);

	/* Add the stuff in the hashtable. */
	bw = g_new0(BuddyWindow, 1);
	bw->window = win;
	bw->vbox = vbox;
	bw->iconview = iconview;
	bw->text_buffer = text_buffer;
	bw->text_height = text_height;
	bw->text_width = text_width;
	g_hash_table_insert(buddy_windows, key, bw);

	update_icon_view(key);

	gtk_widget_size_request(bw->iconview, &bw->requisition);
	set_window_geometry(bw, buddy_icon_size);

	gtk_window_set_default_size(GTK_WINDOW(win),
	                            purple_prefs_get_int(PREF_WINDOW_WIDTH),
	                            purple_prefs_get_int(PREF_WINDOW_HEIGHT));

	gtk_window_set_policy(GTK_WINDOW(win), FALSE, TRUE, FALSE);
	gtk_widget_show_all(win);

#if 0
	/* TODO: We need a way to disconnect this signal handler when the window is closed.
	 * TODO: Otherwise, it segfaults in update_runtime. */

	/* Register a signal handler to update the viewer if a new buddy icon is cached. */
	purple_signal_connect_priority(purple_buddy_icons_get_handle(), "buddy-icon-cached",
	                             purple_plugins_find_with_id(PLUGIN_ID), PURPLE_CALLBACK(update_runtime),
	                             key, PURPLE_SIGNAL_PRIORITY_DEFAULT + 1);
#endif

	gtk_signal_connect(GTK_OBJECT(win), "configure_event",
	                   GTK_SIGNAL_FUNC(update_size), NULL);
	g_signal_connect(G_OBJECT(win), "response",
	                 G_CALLBACK(window_close), key);
}

static gboolean has_stored_icons(PurpleBuddy *buddy)
{
	char *path = album_buddy_icon_get_dir(purple_buddy_get_account(buddy),
	                                      purple_buddy_get_name(buddy));
	GDir *dir = g_dir_open(path, 0, NULL);

	g_free(path);

	if (dir)
	{
		if (g_dir_read_name(dir))
		{
			g_dir_close(dir);
			return TRUE;
		}
		g_dir_close(dir);
	}

	return FALSE;
}

static void
album_select_dialog_cb(gpointer data, PurpleRequestFields *fields)
{
	char *username;
	PurpleAccount *account;

	account  = purple_request_fields_get_account(fields, "account");

	username = g_strdup(purple_normalize(account,
		purple_request_fields_get_string(fields,  "screenname")));

	if (username != NULL && *username != '\0' && account != NULL )
	{
		icon_viewer_key *key = g_new0(icon_viewer_key, 1);
		key->account = account;
		key->screenname = username;

		show_buddy_icon_window(key, username);
	}
}

/* Based on Pidgin's gtkdialogs.c, pidgindialogs_log(). */
static void album_select_dialog(PurplePluginAction *action)
{
	PurpleRequestFields *fields;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;

	fields = purple_request_fields_new();

	group = purple_request_field_group_new(NULL);
	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("screenname", _("_Name"), NULL, FALSE);
	purple_request_field_set_type_hint(field, "screenname-all");
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_account_new("account", _("_Account"), NULL);
	purple_request_field_set_type_hint(field, "account");
	purple_request_field_account_set_show_all(field, TRUE);
	purple_request_field_set_visible(field, (purple_accounts_get_all() != NULL &&
	                                       purple_accounts_get_all()->next != NULL));
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	purple_request_fields(purple_get_blist(), _("View Buddy Icons..."),
	                    NULL,
	                    _("Please enter the screen name or alias of the person whose icon album you want to view."),
	                    fields,
	                    _("OK"), G_CALLBACK(album_select_dialog_cb),
	                    _("Cancel"), NULL,
	                    NULL, NULL, NULL,
	                    NULL);
}

GList *album_get_plugin_actions(PurplePlugin *plugin, gpointer data)
{
	GList *actions = NULL;

	actions = g_list_append(actions, purple_plugin_action_new(_("View Buddy Icons"), album_select_dialog));

	return actions;
}

void album_blist_node_menu_cb(PurpleBlistNode *node, GList **menu)
{
	gboolean contact_expanded;
	PurpleMenuAction *action;
	void (*callback)() = view_buddy_icons_cb;

	if (!(PURPLE_BLIST_NODE_IS_CONTACT(node) || PURPLE_BLIST_NODE_IS_BUDDY(node)))
		return;

	contact_expanded = pidgin_blist_node_is_contact_expanded(node);

	if (PURPLE_BLIST_NODE_IS_BUDDY(node))
	{
		if (contact_expanded)
		{
			if (!has_stored_icons((PurpleBuddy *)node))
			{
				callback = NULL;
			}
		}
		else if (PURPLE_BLIST_NODE_IS_CONTACT(node))
		{
			/* We don't want to show this option in buddy submenus. */
			if ((PurpleBlistNode *)((PurpleContact *)node->parent)->priority != node)
				return;

			/* Find the contact and fall through to the contact handling code. */
			node = node->parent;
		}
	}

	if (PURPLE_BLIST_NODE_IS_CONTACT(node))
	{
		PurpleBlistNode *bnode;

		for (bnode = node->child; bnode; bnode = bnode->next)
		{
			if (has_stored_icons((PurpleBuddy *)bnode))
				break;
		}

		/* bnode == NULL when the for loop made it all the way through. */
		if (bnode == NULL)
		{
			/* No icons found. */
			callback = NULL;
		}
	}

	/* Separator */
	(*menu) = g_list_append(*menu, NULL);

	action = purple_menu_action_new(_("_View Buddy Icons"), PURPLE_CALLBACK(callback), NULL, NULL);
	(*menu) = g_list_append(*menu, action);
}
