#import <ObjXMPP/namespaces.h>
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#import "JubGtkRosterUI.h"
#import "JubGObjectMap.h"
#import "JubGtkChatUI.h"
#import "JubGtkHelper.h"

static void roster_row_activated(GtkTreeView *tree_view, GtkTreePath *path,
    GtkTreeViewColumn *column, gpointer data)
{
	OFAutoreleasePool *pool;
	XMPPContact *contact;
	JubChatClient *client = data;
	GtkTreeIter row_iter;
	GtkTreeModel *tree_model;
	gchar *jid_s;
	OFString *jid;

	tree_model = gtk_tree_view_get_model(tree_view);
	gtk_tree_model_get_iter(tree_model, &row_iter, path);
	gtk_tree_model_get(tree_model, &row_iter, 1, &jid_s, -1);

	// This was a group row
	if (!jid_s) return;

	pool = [OFAutoreleasePool new];

	jid = [OFString stringWithUTF8String: jid_s];
	contact = [client.contactManager.contacts objectForKey: jid];

	[client performSelectorOnMainThread: @selector(chatForContact:)
				 withObject: contact
			      waitUntilDone: false];
	[pool release];
}

static void presence_changed(GtkComboBox *combo_box, gpointer data)
{
	JubChatClient *client = data;
	OFAutoreleasePool *pool = [OFAutoreleasePool new];
	const char *status = gtk_combo_box_get_active_id(combo_box);

	[client sendPresenceWithStatus: @(status)];

	[pool release];
}

static gboolean filter_roster_by_presence(GtkTreeModel *model,
    GtkTreeIter *iter, gpointer data)
{
	bool *showOffline = data;
	char *status;
	gtk_tree_model_get(model, iter, 2, &status, -1);

	// Groups have no status
	if (!status)
		return TRUE;

	if (!*showOffline && !strcmp(status, "unavailable"))
		return FALSE;

	return TRUE;
}

static void menu_show_offline(GtkCheckMenuItem *checkmenuitem, gpointer data)
{
	JubGtkRosterUI *ui = data;
	ui.showOffline = gtk_check_menu_item_get_active(checkmenuitem);
}

static void dialog_response_callback(GtkDialog *dialog, gint response_id,
    gpointer user_data)
{
	void (^block)(gint) = user_data;
	block(response_id);
	[block release];
}

@implementation JubGtkRosterUI
- initWithClient: (JubChatClient*)client
{
	self = [super init];

	@try {
		GtkCheckMenuItem *show_offline_menu_item;
		GtkTreeView *roster_view;
		GtkBuilder *builder;

		_groupMap = [[OFMapTable alloc]
		    initWithKeyFunctions: keyFunctions
			  valueFunctions: rowRefFunctions];
		_subDialogMap = [[OFMapTable alloc]
		    initWithKeyFunctions: keyFunctions
		    valueFunctions: gObjectFunctions];
		_contactMap = [[OFMutableDictionary alloc] init];
		_client = [client retain];

		[_client.contactManager addDelegate: self];
		[_client.avatarManager setDelegate: self];

		builder = gtk_builder_new();
		gtk_builder_add_from_file(builder, "data/gtk/roster.ui", NULL);
		gtk_builder_connect_signals(builder, NULL);

		_roster_window =
		    GTK_WIDGET(gtk_builder_get_object(builder, "RosterWindow"));

		gtk_widget_show(_roster_window);

		_roster_model = GTK_TREE_STORE(gtk_builder_get_object(builder,
		    "RosterTreeStore"));

		_roster_filter = GTK_TREE_MODEL_FILTER(
		    gtk_builder_get_object(builder, "RosterTreeModelFilter"));

		gtk_tree_model_filter_set_visible_func(_roster_filter,
		    filter_roster_by_presence, &_showOffline, NULL);

		roster_view = GTK_TREE_VIEW(gtk_builder_get_object(builder,
			"RosterTreeView"));

		g_signal_connect(roster_view, "row_activated",
		    G_CALLBACK(roster_row_activated), client);

		_presence_combo = GTK_COMBO_BOX(gtk_builder_get_object(builder,
			"PresenceComboBox"));

		_presence_combo_changed_handler_id =
		    g_signal_connect(_presence_combo, "changed",
			G_CALLBACK(presence_changed), client);

		show_offline_menu_item =
		    GTK_CHECK_MENU_ITEM(gtk_builder_get_object(builder,
			"showOfflineCheckMenuItem"));

		g_signal_connect(show_offline_menu_item, "toggled",
		    G_CALLBACK(menu_show_offline), self);

		g_object_unref(G_OBJECT(builder));
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_client.avatarManager setDelegate: nil];
	[_client.contactManager removeDelegate: self];
	[_subDialogMap release];
	[_groupMap release];
	[_contactMap release];
	[_client release];

	gtk_widget_destroy(_roster_window);

	[super dealloc];
}

- (void)Jub_closeSubscribeDialogForRosterItem: (XMPPRosterItem*)rosterItem
{
	// Close subscripton dialogs, answered on another client
	GtkDialog *dialog;
	OFString *subscription = rosterItem.subscription;
	OFString *bareJID = [rosterItem.JID bareJID];

	if (([subscription isEqual: @"from"] ||
	     [subscription isEqual: @"both"]) &&
	    (dialog = [_subDialogMap valueForKey: bareJID]) != NULL)
		gtk_dialog_response(dialog, GTK_RESPONSE_NONE);
}

/* Roster Delegate methods */
- (void)Jub_addRosterItem: (XMPPRosterItem*)item
		    group: (OFString*)group
{
	GtkTreeIter group_iter, contact_iter;
	GtkTreeRowReference *group_ref, *contact_ref;
	GtkTreePath *group_path, *contact_path;
	OFString *bareJID = [item.JID bareJID];
	OFMapTable *contactRows;

	if (!(contactRows = [_contactMap objectForKey: bareJID])) {
		contactRows = [OFMapTable
		    mapTableWithKeyFunctions: keyFunctions
			      valueFunctions: rowRefFunctions];

		[_contactMap setObject: contactRows
				forKey: bareJID];
	}

	group_ref = [_groupMap valueForKey: group];

	if (!group_ref) {
		// Create new group row
		gtk_tree_store_append(_roster_model, &group_iter, NULL);
		gtk_tree_store_set(_roster_model, &group_iter,
		    0, [group UTF8String], -1);

		group_path = gtk_tree_model_get_path(GTK_TREE_MODEL(
		    _roster_model), &group_iter);

		group_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(
		    _roster_model), group_path);

		[_groupMap setValue: group_ref
			     forKey: group];
	} else {
		// Get iter for existing group row
		group_path = gtk_tree_row_reference_get_path(group_ref);

		gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
		    &group_iter, group_path);
	}
	gtk_tree_path_free(group_path);

	// Create new contact row
	gtk_tree_store_append(_roster_model, &contact_iter, &group_iter);
	if (item.name)
		gtk_tree_store_set(_roster_model, &contact_iter,
		    0, [item.name UTF8String],
		    1, [bareJID UTF8String],
		    2, "unavailable", -1);
	else if (item.JID.node)
		gtk_tree_store_set(_roster_model, &contact_iter,
		    0, [item.JID.node UTF8String],
		    1, [bareJID UTF8String],
		    2, "unavailable", -1);
	else
		gtk_tree_store_set(_roster_model, &contact_iter,
		    0, [item.JID.domain UTF8String],
		    1, [bareJID UTF8String],
		    2, "unavailable", -1);

	contact_path = gtk_tree_model_get_path(GTK_TREE_MODEL(_roster_model),
	    &contact_iter);

	contact_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(_roster_model),
	    contact_path);

	gtk_tree_path_free(contact_path);

	[contactRows setValue: contact_ref
		       forKey: group];
}

- (void)Jub_removeRosterItem: (XMPPRosterItem*)item
		       group: (OFString*)group
{
	GtkTreeIter contact_iter, group_iter;
	GtkTreePath *contact_path, *group_path;
	GtkTreeRowReference *contact_ref, *group_ref;
	OFString *bareJID = [item.JID bareJID];
	OFMapTable *contactRows = [_contactMap objectForKey: bareJID];

	contact_ref = [contactRows valueForKey: group];
	contact_path = gtk_tree_row_reference_get_path(contact_ref);
	gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &contact_iter,
	    contact_path);
	gtk_tree_path_free(contact_path);

	gtk_tree_store_remove(_roster_model, &contact_iter);

	group_ref = [_groupMap valueForKey: group];
	group_path = gtk_tree_row_reference_get_path(group_ref);
	gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &group_iter,
	    group_path);

	if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(_roster_model),
	    &group_iter)) {
		gtk_tree_store_remove(_roster_model, &group_iter);
		[_groupMap removeValueForKey: group];
	}

	gtk_tree_path_free(group_path);

	[contactRows removeValueForKey: group];
	if([contactRows count] == 0)
		[_contactMap removeObjectForKey: bareJID];
}

- (void)contactManager: (XMPPContactManager*)manager
	 didAddContact: (XMPPContact*)contact
{
	XMPPRosterItem *rosterItem = contact.rosterItem;
	OFArray *groups = rosterItem.groups;;

	if (groups == nil)
		groups = @[ @"General" ];

	for (OFString *group in groups)
		[self performSelectorOnGLibThread: @selector(
		    Jub_addRosterItem:group:)
				       withObject: rosterItem
				       withObject: group];

	[self Jub_closeSubscribeDialogForRosterItem: rosterItem];
}

- (void)contactManager: (XMPPContactManager*)manager
      didRemoveContact: (XMPPContact*)contact
{
	XMPPRosterItem *rosterItem = contact.rosterItem;
	OFArray *groups = rosterItem.groups;

	if (groups == nil)
		groups = @[ @"General" ];

	for (OFString *group in groups)
		[self performSelectorOnGLibThread: @selector(
		    Jub_removeRosterItem:group:)
				       withObject: rosterItem
				       withObject: group];
}

-          (void)contactManager: (XMPPContactManager*)manager
  didReceiveSubscriptionRequest: (XMPPPresence*)presence
{
	XMPPJID *JID = presence.from;
	OFString *bareJID = [JID bareJID];
	OFString *message = [OFString stringWithFormat: @"<b>%@</b> would like "
	    @"to subscribe to your presence.", bareJID];
	g_idle_add_block(^{
		GtkWidget *dialog, *content_area, *label;

	        if ([_subDialogMap valueForKey: JID] != NULL)
			return;

		dialog = gtk_dialog_new_with_buttons("Subscription Request",
		    GTK_WINDOW(_roster_window), GTK_DIALOG_DESTROY_WITH_PARENT,
		    "Accept", GTK_RESPONSE_ACCEPT,
		    "Deny", GTK_RESPONSE_REJECT, NULL);

		content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
		label = gtk_label_new(NULL);
		gtk_label_set_markup(GTK_LABEL(label), [message UTF8String]);
		gtk_container_add(GTK_CONTAINER(content_area), label);

		g_signal_connect(dialog, "response",
		    G_CALLBACK(dialog_response_callback),
		    [^(gint response_id) {
			if (response_id == GTK_RESPONSE_ACCEPT)
				[manager sendSubscribedToJID: JID];
			else if (response_id == GTK_RESPONSE_REJECT)
				[manager sendUnsubscribedToJID: JID];
			[_subDialogMap removeValueForKey: bareJID];
			gtk_widget_destroy(GTK_WIDGET(dialog));
		} copy]);

		[_subDialogMap setValue: dialog
				 forKey: bareJID];

		gtk_widget_show_all(dialog);
	});
}

-	     (void)contact: (XMPPContact*)contact
  willUpdateWithRosterItem: (XMPPRosterItem*)rosterItem;
{
	[self Jub_closeSubscribeDialogForRosterItem: rosterItem];

	// Remove contact from old set of groups
	XMPPRosterItem *oldItem = contact.rosterItem;
	OFArray *groups = oldItem.groups;

	if (groups == nil)
		groups = @[ @"General" ];

	for (OFString *group in groups)
		[self performSelectorOnGLibThread: @selector(
		    Jub_removeRosterItem:group:)
				       withObject: oldItem
				       withObject: group];

	// Add contact to new set of groups
	groups = rosterItem.groups;

	if (groups == nil)
		groups = @[ @"General" ];

	for (OFString *group in groups)
		[self performSelectorOnGLibThread: @selector(
		    Jub_addRosterItem:group:)
				       withObject: rosterItem
				       withObject: group];
}

-   (void)contact: (XMPPContact*)contact
  didSendPresence: (XMPPPresence*)presence
{
	OFDictionary *allPresences = [contact presences];
	XMPPPresence *highPresence = [[[allPresences allObjects] sortedArray]
					 lastObject];
	OFMutableString *tooltip =
	    [OFMutableString stringWithString: @"<b>Resources:</b>"];

	[allPresences enumerateKeysAndObjectsUsingBlock:
	    ^(OFString *resource, XMPPPresence *pres, bool *stop) {
		[tooltip appendString: @"\n"];
		[tooltip appendString: resource];
		if ([pres.type isEqual: @"available"]) {
			if (pres.show != nil)
				[tooltip appendFormat: @" (%@)", pres.show];
			else
				[tooltip appendString: @" (available)"];
		} else
			[tooltip appendString: @" (unavailable)"];

		if (pres.status)
		        [tooltip appendFormat: @": <i>%@</i>", pres.status];
	}];

	g_idle_add_block(^{
		GtkTreeIter iter;
		GtkTreePath *path;
		GtkTreeRowReference *ref;
		OFString *bareJID = [contact.rosterItem.JID bareJID];
		OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
		OFArray *groups = contact.rosterItem.groups;;

		if (groups == nil)
			groups = @[ @"General" ];

		for (OFString *group in groups) {
			ref = [contactRows valueForKey: group];
			path = gtk_tree_row_reference_get_path(ref);
			gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
			    &iter, path);
			gtk_tree_path_free(path);

			if ([highPresence.type isEqual: @"available"]) {
				if (highPresence.show != nil)
					gtk_tree_store_set(_roster_model, &iter,
					    2, [highPresence.show UTF8String],
					    -1);
				else
					gtk_tree_store_set(_roster_model, &iter,
					    2, "available", -1);
			} else
				gtk_tree_store_set(_roster_model, &iter,
				    2, "unavailable", -1);

			gtk_tree_store_set(_roster_model, &iter,
			    3, [tooltip UTF8String], -1);
		}

		gtk_tree_model_filter_refilter(_roster_filter);
	});
}

- (void)contact: (XMPPContact*)contact
   didSetAvatar: (OFString*)avatarFile
{
	of_log(@"Got an avatar from %@", contact.rosterItem.JID);
	g_idle_add_block(^{
		GtkTreeIter iter;
		GtkTreePath *path;
		GtkTreeRowReference *ref;
		OFString *bareJID = [contact.rosterItem.JID bareJID];
		OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
		OFArray *groups = contact.rosterItem.groups;;

		GdkPixbuf *avatar =
		    gdk_pixbuf_new_from_file_at_size([avatarFile UTF8String],
			32, 32, NULL);

		if (groups == nil)
			groups = @[ @"General" ];

		for (OFString *group in groups) {
			ref = [contactRows valueForKey: group];
			path = gtk_tree_row_reference_get_path(ref);
			gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
			    &iter, path);
			gtk_tree_path_free(path);

			gtk_tree_store_set(_roster_model, &iter,
			    4, avatar, -1);
		}
		g_object_unref(G_OBJECT(avatar));
	});
}

-      (void)client: (JubChatClient*)client_
  didChangePresence: (XMPPPresence*)presence
{
	OFString *tooltip = @"";
	OFString *show =
	    [[presence elementForName: @"show"
			    namespace: XMPP_NS_CLIENT] stringValue];
	OFString *status =
	    [[presence elementForName: @"status"
			    namespace: XMPP_NS_CLIENT] stringValue];

	if (status != nil)
		tooltip = [@"<b>Status:</b> " stringByAppendingString: status];

	g_idle_add_block(^{
		// Block the PresenceComboBox's changed handler, so it doesn't
		// fire and resend presence
		g_signal_handler_block(_presence_combo,
		    _presence_combo_changed_handler_id);

		if ([presence.type isEqual: @"unavailable"])
			gtk_combo_box_set_active_id(_presence_combo,
			    "unavailable");
		else if (show == nil)
			gtk_combo_box_set_active_id(_presence_combo,
			    "available");
		else
			gtk_combo_box_set_active_id(_presence_combo,
			    [show UTF8String]);

		// Unblock the changed handler
		g_signal_handler_unblock(_presence_combo,
		    _presence_combo_changed_handler_id);

		gtk_widget_set_tooltip_markup(GTK_WIDGET(_presence_combo),
		    [tooltip UTF8String]);
	});
}

- (bool)showOffline
{
	OF_GETTER(_showOffline, true);
}

- (void)setShowOffline: (bool)showOffline
{
	_showOffline = showOffline;
	g_idle_add_block(^{
		gtk_tree_model_filter_refilter(_roster_filter);
	});
}
@end
