/*
 * purple - Jabber Protocol Plugin
 *
 * 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
 *
 */
#include "internal.h"

#include "account.h"
#include "conversation.h"
#include "debug.h"
#include "glibcompat.h"
#include "notify.h"
#include "request.h"
#include "server.h"
#include "status.h"
#include "util.h"
#include "xmlnode.h"

#include "buddy.h"
#include "chat.h"
#include "google/google.h"
#include "google/google_presence.h"
#include "presence.h"
#include "iq.h"
#include "jutil.h"
#include "adhoccommands.h"

#include "usermood.h"
#include "usertune.h"

static GHashTable *presence_handlers = NULL;

static const struct {
	const char *name;
	JabberPresenceType type;
} litxmpp_presence_types[] = {
	{ "error", JABBER_PRESENCE_ERROR },
	{ "probe", JABBER_PRESENCE_PROBE },
	{ "unavailable", JABBER_PRESENCE_UNAVAILABLE },
	{ "subscribe", JABBER_PRESENCE_SUBSCRIBE },
	{ "subscribed", JABBER_PRESENCE_SUBSCRIBED },
	{ "unsubscribe", JABBER_PRESENCE_UNSUBSCRIBE },
	{ "unsubscribed", JABBER_PRESENCE_UNSUBSCRIBED }
	/* { NULL, JABBER_PRESENCE_AVAILABLE } the default */
};

static JabberPresenceType
str_to_presence_type(const char *type)
{
	gsize i;

	if (type == NULL)
		return JABBER_PRESENCE_AVAILABLE;

	for (i = 0; i < G_N_ELEMENTS(litxmpp_presence_types); ++i)
		if (purple_strequal(type, litxmpp_presence_types[i].name))
			return litxmpp_presence_types[i].type;

	purple_debug_warning("jabber", "Unknown presence type '%s'\n", type);
	return JABBER_PRESENCE_AVAILABLE;
}

static void chats_send_presence_foreach(gpointer key, gpointer val,
		gpointer user_data)
{
	JabberChat *chat = val;
	xmlnode *presence = user_data;
	char *chat_full_jid;

	if(!chat->conv || chat->left)
		return;

	chat_full_jid = g_strdup_printf("%s@%s/%s", chat->room, chat->server,
			chat->handle);

	xmlnode_set_attrib(presence, "to", chat_full_jid);
	litxmpp_send(chat->js, presence);
	g_free(chat_full_jid);
}

void litxmpp_presence_fake_to_self(JabberStream *js, PurpleStatus *status)
{
	PurpleAccount *account;
	PurplePresence *presence;
	JabberBuddy *jb;
	JabberBuddyResource *jbr;
	const char *username;
	JabberBuddyState state;
	char *msg;
	int priority;

	g_return_if_fail(js->user != NULL);

	account = purple_connection_get_account(js->gc);
	username = purple_connection_get_display_name(js->gc);
	presence = purple_account_get_presence(account);
	if (status == NULL)
		status = purple_presence_get_active_status(presence);
	purple_status_to_jabber(status, &state, &msg, &priority);

	jb = js->user_jb;

	if (state == JABBER_BUDDY_STATE_UNAVAILABLE ||
			state == JABBER_BUDDY_STATE_UNKNOWN) {
		litxmpp_buddy_remove_resource(jb, js->user->resource);
	} else {
		jbr = litxmpp_buddy_track_resource(jb, js->user->resource, priority,
				state, msg);
		jbr->idle = purple_presence_is_idle(presence) ?
				purple_presence_get_idle_time(presence) : 0;
	}

	/*
	 * While we need to track the status of this resource, the core
	 * only cares if we're on our own buddy list.
	 */
	if (purple_find_buddy(account, username)) {
		jbr = litxmpp_buddy_find_resource(jb, NULL);
		if (jbr) {
			purple_prpl_got_user_status(account, username,
					litxmpp_buddy_state_get_status_id(jbr->state),
					"priority", jbr->priority,
					jbr->status ? "message" : NULL, jbr->status,
					NULL);
			purple_prpl_got_user_idle(account, username, jbr->idle, jbr->idle);
		} else {
			purple_prpl_got_user_status(account, username, "offline",
					msg ? "message" : NULL, msg,
					NULL);
		}
	}
	g_free(msg);
}

void litxmpp_set_status(PurpleAccount *account, PurpleStatus *status)
{
	PurpleConnection *gc;
	JabberStream *js;

	if (!purple_account_is_connected(account))
		return;

	if (purple_status_is_exclusive(status) && !purple_status_is_active(status)) {
		/* An exclusive status can't be deactivated. You should just
		 * activate some other exclusive status. */
		return;
	}

	gc = purple_account_get_connection(account);
	js = purple_connection_get_protocol_data(gc);

	/* it's a mood update */
	if (purple_status_type_get_primitive(purple_status_get_type(status)) == PURPLE_STATUS_MOOD) {
		const char *mood =
			purple_status_get_attr_string(status, PURPLE_MOOD_NAME);
		const char *mood_text =
			purple_status_get_attr_string(status, PURPLE_MOOD_COMMENT);
		litxmpp_mood_set(js, mood, mood_text);
		return;
	}

	litxmpp_presence_send(js, FALSE);
}

void litxmpp_presence_send(JabberStream *js, gboolean force)
{
	PurpleAccount *account;
	xmlnode *presence, *x, *photo;
	char *stripped = NULL;
	JabberBuddyState state;
	int priority;
	const char *artist = NULL, *title = NULL, *source = NULL, *uri = NULL, *track = NULL;
	int length = -1;
	gboolean allowBuzz;
	PurplePresence *p;
	PurpleStatus *status, *tune;

	account = purple_connection_get_account(js->gc);
	p = purple_account_get_presence(account);
	status = purple_presence_get_active_status(p);

	/* we don't want to send presence before we've gotten our roster */
	if (js->state != JABBER_STREAM_CONNECTED) {
		purple_debug_misc("jabber", "attempt to send presence before roster retrieved\n");
		return;
	}

	purple_status_to_jabber(status, &state, &stripped, &priority);

	/* check for buzz support */
	allowBuzz = purple_status_get_attr_boolean(status,"buzz");
	/* changing the buzz state has to trigger a re-broadcasting of the presence for caps */

	tune = purple_presence_get_status(p, "tune");
	if (js->googletalk && !stripped && purple_status_is_active(tune)) {
		stripped = litxmpp_google_presence_outgoing(tune);
	}

	/* check if there are any differences to the <presence> and send them in that case */
	if (force || allowBuzz != js->allowBuzz || js->old_state != state ||
		!purple_strequal(js->old_msg, stripped) || js->old_priority != priority ||
		!purple_strequal(js->old_avatarhash, js->avatar_hash) || js->old_idle != js->idle) {
		/* Need to update allowBuzz before creating the presence (with caps) */
		js->allowBuzz = allowBuzz;

		presence = litxmpp_presence_create_js(js, state, stripped, priority);

		/* Per XEP-0153 4.1, we must always send the <x> */
		x = xmlnode_new_child(presence, "x");
		xmlnode_set_namespace(x, "vcard-temp:x:update");
		/*
		 * FIXME: Per XEP-0153 4.3.2 bullet 2, we must not publish our
		 * image hash if another resource has logged in and updated the
		 * vcard avatar. Requires changes in litxmpp_presence_parse.
		 */
		if (js->vcard_fetched) {
			/* Always publish a <photo>; it's empty if we have no image. */
			photo = xmlnode_new_child(x, "photo");
			if (js->avatar_hash)
				xmlnode_insert_data(photo, js->avatar_hash, -1);
		}

		litxmpp_send(js, presence);

		g_hash_table_foreach(js->chats, chats_send_presence_foreach, presence);
		xmlnode_free(presence);

		/* update old values */

		if(js->old_msg)
			g_free(js->old_msg);
		if(js->old_avatarhash)
			g_free(js->old_avatarhash);
		js->old_msg = g_strdup(stripped);
		js->old_avatarhash = g_strdup(js->avatar_hash);
		js->old_state = state;
		js->old_priority = priority;
		js->old_idle = js->idle;
	}
	g_free(stripped);

	/* next, check if there are any changes to the tune values */
	if (purple_status_is_active(tune)) {
		artist = purple_status_get_attr_string(tune, PURPLE_TUNE_ARTIST);
		title = purple_status_get_attr_string(tune, PURPLE_TUNE_TITLE);
		source = purple_status_get_attr_string(tune, PURPLE_TUNE_ALBUM);
		uri = purple_status_get_attr_string(tune, PURPLE_TUNE_URL);
		track = purple_status_get_attr_string(tune, PURPLE_TUNE_TRACK);
		length = (!purple_status_get_attr_value(tune, PURPLE_TUNE_TIME)) ? -1 :
				purple_status_get_attr_int(tune, PURPLE_TUNE_TIME);
	}

	if(!purple_strequal(artist, js->old_artist) || !purple_strequal(title, js->old_title) ||
			!purple_strequal(source, js->old_source) || !purple_strequal(uri, js->old_uri) ||
			!purple_strequal(track, js->old_track) || (length != js->old_length)) {
		PurpleJabberTuneInfo tuneinfo = {
			(char*)artist,
			(char*)title,
			(char*)source,
			(char*)track,
			length,
			(char*)uri
		};
		litxmpp_tune_set(js->gc, &tuneinfo);

		/* update old values */
		g_free(js->old_artist);
		g_free(js->old_title);
		g_free(js->old_source);
		g_free(js->old_uri);
		g_free(js->old_track);
		js->old_artist = g_strdup(artist);
		js->old_title = g_strdup(title);
		js->old_source = g_strdup(source);
		js->old_uri = g_strdup(uri);
		js->old_length = length;
		js->old_track = g_strdup(track);
	}

#undef CHANGED

	litxmpp_presence_fake_to_self(js, status);
}

xmlnode *litxmpp_presence_create(JabberBuddyState state, const char *msg, int priority)
{
    return litxmpp_presence_create_js(NULL, state, msg, priority);
}

xmlnode *litxmpp_presence_create_js(JabberStream *js, JabberBuddyState state, const char *msg, int priority)
{
	xmlnode *show, *status, *presence, *pri, *c;
	const char *show_string = NULL;
#ifdef USE_VV
	gboolean audio_enabled, video_enabled;
#endif

	presence = xmlnode_new("presence");

	if(state == JABBER_BUDDY_STATE_UNAVAILABLE)
		xmlnode_set_attrib(presence, "type", "unavailable");
	else if(state != JABBER_BUDDY_STATE_ONLINE &&
			state != JABBER_BUDDY_STATE_UNKNOWN &&
			state != JABBER_BUDDY_STATE_ERROR)
		show_string = litxmpp_buddy_state_get_show(state);

	if(show_string) {
		show = xmlnode_new_child(presence, "show");
		xmlnode_insert_data(show, show_string, -1);
	}

	if(msg) {
		status = xmlnode_new_child(presence, "status");
		xmlnode_insert_data(status, msg, -1);
	}

	if(priority) {
		char *pstr = g_strdup_printf("%d", priority);
		pri = xmlnode_new_child(presence, "priority");
		xmlnode_insert_data(pri, pstr, -1);
		g_free(pstr);
	}

	/* if we are idle and not offline, include idle */
	if (js != NULL && js->idle && state != JABBER_BUDDY_STATE_UNAVAILABLE) {
		xmlnode *query = xmlnode_new_child(presence, "query");
		gchar seconds[10];
		g_snprintf(seconds, 10, "%d", (int) (time(NULL) - js->idle));

		xmlnode_set_namespace(query, NS_LAST_ACTIVITY);
		xmlnode_set_attrib(query, "seconds", seconds);
	}

	/* JEP-0115 */
	/* calculate hash */
	if(js != NULL) {
		litxmpp_caps_calculate_own_hash(js);
	}

	/* create xml */
	c = xmlnode_new_child(presence, "c");
	xmlnode_set_namespace(c, "http://jabber.org/protocol/caps");
	xmlnode_set_attrib(c, "node", CAPS0115_NODE);
	xmlnode_set_attrib(c, "hash", "sha-1");
	xmlnode_set_attrib(c, "ver", litxmpp_caps_get_own_hash(js));

#ifdef USE_VV
	/*
	 * MASSIVE HUGE DISGUSTING HACK
	 * This is a huge hack. As far as I can tell, Google Talk's gmail client
	 * doesn't bother to check the actual features we advertise; they
	 * just assume that if we specify a 'voice-v1' ext (ignoring that
	 * these are to be assigned no semantic value), we support receiving voice
	 * calls.
	 *
	 * Ditto for 'video-v1'.
	 */
	audio_enabled = litxmpp_audio_enabled(js, NULL /* unused */);
	video_enabled = litxmpp_video_enabled(js, NULL /* unused */);

	if (audio_enabled && video_enabled)
		xmlnode_set_attrib(c, "ext", "voice-v1 camera-v1 video-v1");
	else if (audio_enabled)
		xmlnode_set_attrib(c, "ext", "voice-v1");
	else if (video_enabled)
		xmlnode_set_attrib(c, "ext", "camera-v1 video-v1");
#endif

	return presence;
}

struct _litxmpp_add_permit {
	PurpleConnection *gc;
	JabberStream *js;
	char *who;
};

static void authorize_add_cb(gpointer data)
{
	struct _litxmpp_add_permit *jap = data;
	if(PURPLE_CONNECTION_IS_VALID(jap->gc))
		litxmpp_presence_subscription_set(jap->gc->proto_data,
			jap->who, "subscribed");
	g_free(jap->who);
	g_free(jap);
}

static void deny_add_cb(gpointer data)
{
	struct _litxmpp_add_permit *jap = data;
	if(PURPLE_CONNECTION_IS_VALID(jap->gc))
		litxmpp_presence_subscription_set(jap->gc->proto_data,
			jap->who, "unsubscribed");
	g_free(jap->who);
	g_free(jap);
}

static void
litxmpp_vcard_parse_avatar(JabberStream *js, const char *from,
                          JabberIqType type, const char *id,
                          xmlnode *packet, gpointer blah)
{
	JabberBuddy *jb = NULL;
	xmlnode *vcard, *photo, *binval, *fn, *nick;
	char *text;

	if(!from)
		return;

	jb = litxmpp_buddy_find(js, from, TRUE);

	js->pending_avatar_requests = g_slist_remove(js->pending_avatar_requests, jb);

	if((vcard = xmlnode_get_child(packet, "vCard")) ||
			(vcard = xmlnode_get_child_with_namespace(packet, "query", "vcard-temp"))) {
		/* The logic here regarding the nickname and full name is copied from
		 * buddy.c:litxmpp_vcard_parse. */
		gchar *nickname = NULL;
		if ((fn = xmlnode_get_child(vcard, "FN")))
			nickname = xmlnode_get_data(fn);

		if ((nick = xmlnode_get_child(vcard, "NICKNAME"))) {
			char *tmp = xmlnode_get_data(nick);
			char *bare_jid = litxmpp_get_bare_jid(from);
			if (tmp && strstr(bare_jid, tmp) == NULL) {
				g_free(nickname);
				nickname = tmp;
			} else if (tmp)
				g_free(tmp);

			g_free(bare_jid);
		}

		if (nickname) {
			serv_got_alias(js->gc, from, nickname);
			g_free(nickname);
		}

		if ((photo = xmlnode_get_child(vcard, "PHOTO"))) {
			guchar *data = NULL;
			gchar *hash = NULL;
			gsize size = 0;

			if ((binval = xmlnode_get_child(photo, "BINVAL")) &&
					(text = xmlnode_get_data(binval))) {
				data = purple_base64_decode(text, &size);
				g_free(text);

				if (data)
					hash = litxmpp_calculate_data_hash(data, size, "sha1");
			}

			purple_buddy_icons_set_for_user(js->gc->account, from, data, size, hash);

			g_free(hash);
		}
	}
}

typedef struct _JabberPresenceCapabilities {
	JabberStream *js;
	JabberBuddy *jb;
	char *from;
} JabberPresenceCapabilities;

static void
litxmpp_presence_set_capabilities(JabberCapsClientInfo *info, GList *exts,
                                 JabberPresenceCapabilities *userdata)
{
	JabberBuddyResource *jbr;
	char *resource = strchr(userdata->from, '/');

	if (resource)
		resource += 1;

	jbr = litxmpp_buddy_find_resource(userdata->jb, resource);
	if (!jbr) {
		g_free(userdata->from);
		g_free(userdata);
		if (exts) {
			g_list_free_full(exts, g_free);
		}
		return;
	}

	/* Any old jbr->caps.info is owned by the caps code */
	if (jbr->caps.exts) {
		g_list_free_full(jbr->caps.exts, g_free);
	}

	jbr->caps.info = info;
	jbr->caps.exts = exts;

	purple_prpl_got_media_caps(
			purple_connection_get_account(userdata->js->gc),
			userdata->from);
	if (info == NULL)
		goto out;

	if (!jbr->commands_fetched && litxmpp_resource_has_capability(jbr, "http://jabber.org/protocol/commands")) {
		JabberIq *iq = litxmpp_iq_new_query(userdata->js, JABBER_IQ_GET, NS_DISCO_ITEMS);
		xmlnode *query = xmlnode_get_child_with_namespace(iq->node, "query", NS_DISCO_ITEMS);
		xmlnode_set_attrib(iq->node, "to", userdata->from);
		xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands");
		litxmpp_iq_set_callback(iq, litxmpp_adhoc_disco_result_cb, NULL);
		litxmpp_iq_send(iq);

		jbr->commands_fetched = TRUE;
	}

#if 0
	/*
	 * Versions of libpurple before 2.6.0 didn't advertise this capability, so
	 * we can't yet use Entity Capabilities to determine whether or not the
	 * other client supports Chat States.
	 */
	if (litxmpp_resource_has_capability(jbr, "http://jabber.org/protocol/chatstates"))
		jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED;
	else
		jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED;
#endif

out:
	g_free(userdata->from);
	g_free(userdata);
}

static gboolean
handle_presence_chat(JabberStream *js, JabberPresence *presence, xmlnode *packet)
{
	static int i = 1;
	PurpleConvChatBuddyFlags flags = PURPLE_CBFLAGS_NONE;
	JabberChat *chat = presence->chat;

	if (presence->state == JABBER_BUDDY_STATE_ERROR) {
		char *title, *msg = litxmpp_parse_error(js, packet, NULL);

		if (!chat->conv) {
			title = g_strdup_printf(_("Error joining chat %s"), presence->from);
			purple_serv_got_join_chat_failed(js->gc, chat->components);
		} else {
			title = g_strdup_printf(_("Error in chat %s"), presence->from);
			if (g_hash_table_size(chat->members) == 0)
				serv_got_chat_left(js->gc, chat->id);
		}
		purple_notify_error(js->gc, title, title, msg);
		g_free(title);
		g_free(msg);

		if (g_hash_table_size(chat->members) == 0)
			/* Only destroy the chat if the error happened while joining */
			litxmpp_chat_destroy(chat);
		return FALSE;
	}

	if (presence->type == JABBER_PRESENCE_AVAILABLE) {
		const char *jid = NULL;
		const char *affiliation = NULL;
		const char *role = NULL;
		gboolean is_our_resource = FALSE; /* Is the presence about us? */
		JabberBuddyResource *jbr;

		/*
		 * XEP-0045 mandates the presence to include a resource (which is
		 * treated as the chat nick). Some non-compliant servers allow
		 * joining without a nick.
		 */
		if (!presence->jid_from->resource)
			return FALSE;

		if (presence->chat_info.item) {
			jid = xmlnode_get_attrib(presence->chat_info.item, "jid");
			affiliation = xmlnode_get_attrib(presence->chat_info.item, "affiliation");
			role = xmlnode_get_attrib(presence->chat_info.item, "role");
		}

		if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(110)) ||
				purple_strequal(presence->jid_from->resource, chat->handle) ||
				purple_strequal(presence->to, jid))
			is_our_resource = TRUE;

		if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(201))) {
			chat->config_dialog_type = PURPLE_REQUEST_ACTION;
			chat->config_dialog_handle =
				purple_request_action(js->gc,
						_("Create New Room"),
						_("Create New Room"),
						_("You are creating a new room.  Would"
							" you like to configure it, or"
							" accept the default settings?"),
						/* Default Action */ 1,
						purple_connection_get_account(js->gc), NULL, chat->conv,
						chat, 2,
						_("_Configure Room"), G_CALLBACK(litxmpp_chat_request_room_configure),
						_("_Accept Defaults"), G_CALLBACK(litxmpp_chat_create_instant_room));
		}

		if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(210))) {
			/* server rewrote room-nick */
			g_free(chat->handle);
			chat->handle = g_strdup(presence->jid_from->resource);
		}

		if (purple_strequal(affiliation, "owner"))
			flags |= PURPLE_CBFLAGS_FOUNDER;
		if (role) {
			if (purple_strequal(role, "moderator"))
				flags |= PURPLE_CBFLAGS_OP;
			else if (purple_strequal(role, "participant"))
				flags |= PURPLE_CBFLAGS_VOICE;
		}

		if(!chat->conv) {
			char *room_jid = g_strdup_printf("%s@%s", presence->jid_from->node, presence->jid_from->domain);
			chat->id = i++;
			chat->conv = serv_got_joined_chat(js->gc, chat->id, room_jid);
			purple_conv_chat_set_nick(PURPLE_CONV_CHAT(chat->conv), chat->handle);

			litxmpp_chat_disco_traffic(chat);
			g_free(room_jid);
		}

		jbr = litxmpp_buddy_track_resource(presence->jb, presence->jid_from->resource, presence->priority, presence->state, presence->status);
		jbr->commands_fetched = TRUE;

		litxmpp_chat_track_handle(chat, presence->jid_from->resource, jid, affiliation, role);

		if(!litxmpp_chat_find_buddy(chat->conv, presence->jid_from->resource))
			purple_conv_chat_add_user(PURPLE_CONV_CHAT(chat->conv), presence->jid_from->resource,
					jid, flags, chat->joined > 0 && ((!presence->delayed) || (presence->sent > chat->joined)));
		else
			purple_conv_chat_user_set_flags(PURPLE_CONV_CHAT(chat->conv), presence->jid_from->resource,
					flags);

		if (is_our_resource && chat->joined == 0)
			chat->joined = time(NULL);

	} else if (presence->type == JABBER_PRESENCE_UNAVAILABLE) {
		gboolean nick_change = FALSE;
		gboolean kick = FALSE;
		gboolean is_our_resource = FALSE; /* Is the presence about us? */

		const char *jid = NULL;

		/* If the chat nick is invalid, we haven't yet joined, or we've
		 * already left (it was probably us leaving after we closed the
		 * chat), we don't care.
		 */
		if (!presence->jid_from->resource || !chat->conv || chat->left) {
			if (chat->left &&
					presence->jid_from->resource && chat->handle && purple_strequal(presence->jid_from->resource, chat->handle))
				litxmpp_chat_destroy(chat);
			return FALSE;
		}

		is_our_resource = purple_strequal(presence->jid_from->resource, chat->handle);

		litxmpp_buddy_remove_resource(presence->jb, presence->jid_from->resource);

		if (presence->chat_info.item)
			jid = xmlnode_get_attrib(presence->chat_info.item, "jid");

		if (chat->muc) {
			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(110))) {
				is_our_resource = TRUE;
				chat->joined = 0;
			}

			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(301))) {
				/* XXX: We got banned.  YAY! (No GIR, that's bad) */
			}


			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(303))) {
				const char *nick = NULL;
				if (presence->chat_info.item)
					nick = xmlnode_get_attrib(presence->chat_info.item, "nick");

				/* nick change */
				if (!nick) {
					purple_debug_warning("jabber", "Chat presence indicating a nick change, but no new nickname!\n");
				} else {
					nick_change = TRUE;

					if (purple_strequal(presence->jid_from->resource, chat->handle)) {
						/* Changing our own nickname */
						g_free(chat->handle);
						/* TODO: This should be resourceprep'd */
						chat->handle = g_strdup(nick);
					}

					purple_conv_chat_rename_user(PURPLE_CONV_CHAT(chat->conv),
					                             presence->jid_from->resource,
					                             nick);
					litxmpp_chat_remove_handle(chat,
					                          presence->jid_from->resource);
				}
			}

			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(307))) {
				/* Someone was kicked from the room */
				const char *actor = NULL;
				char *reason = NULL;
				char *tmp;

				kick = TRUE;

				if (presence->chat_info.item) {
					xmlnode *node;

					node = xmlnode_get_child(presence->chat_info.item, "actor");
					if (node)
						actor = xmlnode_get_attrib(node, "jid");
					node = xmlnode_get_child(presence->chat_info.item, "reason");
					if (node)
						reason = xmlnode_get_data(node);
				}

				if (reason == NULL)
					reason = g_strdup(_("No reason"));

				if (is_our_resource) {
					if (actor)
						tmp = g_strdup_printf(_("You have been kicked by %s: (%s)"),
								actor, reason);
					else
						tmp = g_strdup_printf(_("You have been kicked: (%s)"),
								reason);
				} else {
					if (actor)
						tmp = g_strdup_printf(_("Kicked by %s (%s)"),
								actor, reason);
					else
						tmp = g_strdup_printf(_("Kicked (%s)"),
								reason);
				}

				g_free(presence->status);
				presence->status = tmp;

				g_free(reason);
			}

			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(321))) {
				/* XXX: removed due to an affiliation change */
			}

			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(322))) {
				/* XXX: removed because room is now members-only */
			}

			if (g_slist_find(presence->chat_info.codes, GINT_TO_POINTER(332))) {
				/* XXX: removed due to system shutdown */
			}
		}

		/*
		 * Possibly another connected resource of our JID (see XEP-0045
		 * v1.24 section 7.1.10) being disconnected. Should be
		 * distinguished by the item_jid.
		 * Also possibly works around bits of an Openfire bug. See
		 * #8319.
		 */
		if (is_our_resource && jid && !purple_strequal(presence->to, jid)) {
			/* TODO: When the above is a loop, this needs to still act
			 * sanely for all cases (this code is a little fragile). */
			if (!kick && !nick_change)
				/* Presumably, kicks and nick changes also affect us. */
				is_our_resource = FALSE;
		}

		if(!nick_change) {
			if (is_our_resource) {
				if (kick)
					purple_conv_chat_write(PURPLE_CONV_CHAT(chat->conv), presence->jid_from->resource,
							presence->status, PURPLE_MESSAGE_SYSTEM, time(NULL));

				serv_got_chat_left(js->gc, chat->id);
				litxmpp_chat_destroy(chat);
			} else {
				purple_conv_chat_remove_user(PURPLE_CONV_CHAT(chat->conv), presence->jid_from->resource,
						presence->status);
				litxmpp_chat_remove_handle(chat, presence->jid_from->resource);
			}
		}
	}

	return TRUE;
}

static gboolean
handle_presence_contact(JabberStream *js, JabberPresence *presence)
{
	JabberBuddyResource *jbr;
	PurpleAccount *account;
	PurpleBuddy *b;
	char *buddy_name;
	PurpleConversation *conv;

	buddy_name = litxmpp_id_get_bare_jid(presence->jid_from);

	account = purple_connection_get_account(js->gc);
	b = purple_find_buddy(account, buddy_name);

	/*
	 * Unbind/unlock from sending messages to a specific resource on
	 * presence changes.  This is locked to a specific resource when
	 * receiving a message (in message.c).
	 */
	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM,
			buddy_name, account);
	if (conv) {
		purple_debug_info("jabber", "Changed conversation binding from %s to %s\n",
				purple_conversation_get_name(conv), buddy_name);
		purple_conversation_set_name(conv, buddy_name);
	}

	if (b == NULL) {
		if (presence->jb != js->user_jb) {
			purple_debug_warning("jabber", "Got presence for unknown buddy %s on account %s (%p)\n",
					buddy_name, purple_account_get_username(account), account);
			g_free(buddy_name);
			return FALSE;
		} else {
			/* this is a different resource of our own account. Resume even when this account isn't on our blist */
		}
	}

	if (b && presence->vcard_avatar_hash) {
		const char *ah = presence->vcard_avatar_hash[0] != '\0' ?
				presence->vcard_avatar_hash : NULL;
		const char *ah2 = purple_buddy_icons_get_checksum_for_user(b);
		if (!purple_strequal(ah, ah2)) {
			/* XXX this is a crappy way of trying to prevent
			 * someone from spamming us with presence packets
			 * and causing us to DoS ourselves...what we really
			 * need is a queue system that can throttle itself,
			 * but i'm too tired to write that right now */
			if(!g_slist_find(js->pending_avatar_requests, presence->jb)) {
				JabberIq *iq;
				xmlnode *vcard;

				js->pending_avatar_requests =
					g_slist_prepend(js->pending_avatar_requests, presence->jb);

				iq = litxmpp_iq_new(js, JABBER_IQ_GET);
				xmlnode_set_attrib(iq->node, "to", buddy_name);
				vcard = xmlnode_new_child(iq->node, "vCard");
				xmlnode_set_namespace(vcard, "vcard-temp");

				litxmpp_iq_set_callback(iq, litxmpp_vcard_parse_avatar, NULL);
				litxmpp_iq_send(iq);
			}
		}
	}

	if (presence->state == JABBER_BUDDY_STATE_ERROR ||
			presence->type == JABBER_PRESENCE_UNAVAILABLE ||
			presence->type == JABBER_PRESENCE_UNSUBSCRIBED) {
		litxmpp_buddy_remove_resource(presence->jb, presence->jid_from->resource);
	} else {
		jbr = litxmpp_buddy_track_resource(presence->jb,
				presence->jid_from->resource, presence->priority,
				presence->state, presence->status);
		jbr->idle = presence->idle ? time(NULL) - presence->idle : 0;
	}

	jbr = litxmpp_buddy_find_resource(presence->jb, NULL);
	if (jbr) {
		litxmpp_google_presence_incoming(js, buddy_name, jbr);
		purple_prpl_got_user_status(account, buddy_name,
				litxmpp_buddy_state_get_status_id(jbr->state),
				"priority", jbr->priority,
				"message", jbr->status,
				NULL);
		purple_prpl_got_user_idle(account, buddy_name,
				jbr->idle, jbr->idle);
		if (presence->nickname)
			serv_got_alias(js->gc, buddy_name, presence->nickname);
	} else {
		purple_prpl_got_user_status(account, buddy_name,
				litxmpp_buddy_state_get_status_id(JABBER_BUDDY_STATE_UNAVAILABLE),
				presence->status ? "message" : NULL, presence->status,
				NULL);
	}
	g_free(buddy_name);

	return TRUE;
}

void litxmpp_presence_parse(JabberStream *js, xmlnode *packet)
{
	const char *type;
	JabberBuddyResource *jbr = NULL;
	gboolean signal_return, ret;
	JabberPresence presence;
	xmlnode *child;

	memset(&presence, 0, sizeof(presence));
	/* defaults */
	presence.state = JABBER_BUDDY_STATE_UNKNOWN;
	presence.sent = time(NULL);
	/* interesting values */
	presence.from = xmlnode_get_attrib(packet, "from");
	presence.to   = xmlnode_get_attrib(packet, "to");
	type = xmlnode_get_attrib(packet, "type");
	presence.type = str_to_presence_type(type);

	presence.jb = litxmpp_buddy_find(js, presence.from, TRUE);
	g_return_if_fail(presence.jb != NULL);

	presence.jid_from = litxmpp_id_new(presence.from);
	if (presence.jid_from == NULL) {
		purple_debug_error("jabber", "Ignoring presence with malformed 'from' "
		                   "JID: %s\n", presence.from);
		return;
	}

	signal_return = GPOINTER_TO_INT(purple_signal_emit_return_1(purple_connection_get_prpl(js->gc),
			"jabber-receiving-presence", js->gc, type, presence.from, packet));
	if (signal_return) {
		goto out;
	}

	if (presence.jid_from->node)
		presence.chat = litxmpp_chat_find(js, presence.jid_from->node,
		                                 presence.jid_from->domain);
	if(presence.jb->error_msg) {
		g_free(presence.jb->error_msg);
		presence.jb->error_msg = NULL;
	}

	if (presence.type == JABBER_PRESENCE_AVAILABLE) {
		presence.state = JABBER_BUDDY_STATE_ONLINE;
	} else if (presence.type == JABBER_PRESENCE_ERROR) {
		/* TODO: Is this handled properly?  Should it be treated as per-jbr? */
		char *msg = litxmpp_parse_error(js, packet, NULL);
		presence.state = JABBER_BUDDY_STATE_ERROR;
		presence.jb->error_msg = msg ? msg : g_strdup(_("Unknown Error in presence"));
	} else if (presence.type == JABBER_PRESENCE_SUBSCRIBE) {
		/* TODO: Move to handle_subscribe() (so nick is extracted by the
		 * PresenceHandler */
		struct _litxmpp_add_permit *jap = g_new0(struct _litxmpp_add_permit, 1);
		gboolean onlist = FALSE;
		PurpleAccount *account;
		PurpleBuddy *buddy;
		xmlnode *nick;

		account = purple_connection_get_account(js->gc);
		buddy = purple_find_buddy(account, presence.from);
		nick = xmlnode_get_child_with_namespace(packet, "nick", "http://jabber.org/protocol/nick");
		if (nick)
			presence.nickname = xmlnode_get_data(nick);

		if (buddy) {
			if ((presence.jb->subscription & (JABBER_SUB_TO | JABBER_SUB_PENDING)))
				onlist = TRUE;
		}

		jap->gc = js->gc;
		jap->who = g_strdup(presence.from);
		jap->js = js;

		purple_account_request_authorization(account, presence.from, NULL, presence.nickname,
				NULL, onlist, authorize_add_cb, deny_add_cb, jap);

		goto out;
	} else if (presence.type == JABBER_PRESENCE_SUBSCRIBED) {
		/* This case (someone has approved our subscribe request) is handled
		 * by the roster push the server sends along with this.
		 */
		goto out;
	} else if (presence.type == JABBER_PRESENCE_UNSUBSCRIBE) {
		/* XXX I'm not sure this is the right way to handle this, it
		 * might be better to add "unsubscribe" to the presence status
		 * if lower down, but I'm not sure. */
		/* they are unsubscribing from our presence, we don't care */
		/* Well, maybe just a little, we might want/need to start
		 * acknowledging this (and the others) at some point. */
		goto out;
	} else if (presence.type == JABBER_PRESENCE_PROBE) {
		purple_debug_warning("jabber", "Ignoring presence probe\n");
		goto out;
	} else if (presence.type == JABBER_PRESENCE_UNAVAILABLE) {
		presence.state = JABBER_BUDDY_STATE_UNAVAILABLE;
	} else if (presence.type == JABBER_PRESENCE_UNSUBSCRIBED) {
		presence.state = JABBER_BUDDY_STATE_UNKNOWN;
	} else {
		purple_debug_warning("jabber", "Ignoring presence with invalid type "
		                     "'%s'\n", type);
		goto out;
	}

	for (child = packet->child; child; child = child->next) {
		const char *xmlns;
		char *key;
		JabberPresenceHandler *pih;
		if (child->type != XMLNODE_TYPE_TAG)
			continue;

		xmlns = xmlnode_get_namespace(child);
		key = g_strdup_printf("%s %s", child->name, xmlns ? xmlns : "");
		pih = g_hash_table_lookup(presence_handlers, key);
		g_free(key);
		if (pih)
			pih(js, &presence, child);
	}

	if (presence.delayed && presence.idle) {
		/* Delayed and idle, so update idle time */
		presence.idle = presence.idle + (time(NULL) - presence.sent);
	}

	/* TODO: Handle tracking jb(r) here? */

	if (presence.chat)
		ret = handle_presence_chat(js, &presence, packet);
	else
		ret = handle_presence_contact(js, &presence);
	if (!ret)
		goto out;

	if (presence.caps && presence.type == JABBER_PRESENCE_AVAILABLE) {
		/* handle Entity Capabilities (XEP-0115) */
		const char *node = xmlnode_get_attrib(presence.caps, "node");
		const char *ver  = xmlnode_get_attrib(presence.caps, "ver");
		const char *hash = xmlnode_get_attrib(presence.caps, "hash");
		const char *ext  = xmlnode_get_attrib(presence.caps, "ext");

		/* v1.3 uses: node, ver, and optionally ext.
		 * v1.5 uses: node, ver, and hash. */
		if (node && *node && ver && *ver) {
			gchar **exts = ext && *ext ? g_strsplit(ext, " ", -1) : NULL;
			jbr = litxmpp_buddy_find_resource(presence.jb, presence.jid_from->resource);

			/* Look it up if we don't already have all this information */
			if (!jbr || !jbr->caps.info ||
					!purple_strequal(node, jbr->caps.info->tuple.node) ||
					!purple_strequal(ver, jbr->caps.info->tuple.ver) ||
					!purple_strequal(hash, jbr->caps.info->tuple.hash) ||
					!litxmpp_caps_exts_known(jbr->caps.info, (gchar **)exts)) {
				JabberPresenceCapabilities *userdata = g_new0(JabberPresenceCapabilities, 1);
				userdata->js = js;
				userdata->jb = presence.jb;
				userdata->from = g_strdup(presence.from);
				litxmpp_caps_get_info(js, presence.from, node, ver, hash, exts,
				    (litxmpp_caps_get_info_cb)litxmpp_presence_set_capabilities,
				    userdata);
			} else {
				if (exts)
					g_strfreev(exts);
			}
		}
	}

out:
	while (presence.chat_info.codes)
		presence.chat_info.codes =
			g_slist_delete_link(presence.chat_info.codes,
			                    presence.chat_info.codes);

	g_free(presence.status);
	g_free(presence.vcard_avatar_hash);
	g_free(presence.nickname);
	litxmpp_id_free(presence.jid_from);
}

void litxmpp_presence_subscription_set(JabberStream *js, const char *who, const char *type)
{
	xmlnode *presence = xmlnode_new("presence");

	xmlnode_set_attrib(presence, "to", who);
	xmlnode_set_attrib(presence, "type", type);

	litxmpp_send(js, presence);
	xmlnode_free(presence);
}

void purple_status_to_jabber(const PurpleStatus *status, JabberBuddyState *state, char **msg, int *priority)
{
	const char *status_id = NULL;
	const char *formatted_msg = NULL;

	if(state) *state = JABBER_BUDDY_STATE_UNKNOWN;
	if(msg) *msg = NULL;
	if(priority) *priority = 0;

	if(!status) {
		if(state) *state = JABBER_BUDDY_STATE_UNAVAILABLE;
	} else {
		if(state) {
			status_id = purple_status_get_id(status);
			*state = litxmpp_buddy_status_id_get_state(status_id);
		}

		if(msg) {
			formatted_msg = purple_status_get_attr_string(status, "message");

			/* if the message is blank, then there really isn't a message */
			if(formatted_msg && *formatted_msg)
				*msg = purple_markup_strip_html(formatted_msg);
		}

		if(priority)
			*priority = purple_status_get_attr_int(status, "priority");
	}
}

/* Incoming presence handlers */
static void
parse_priority(JabberStream *js, JabberPresence *presence, xmlnode *priority)
{
	char *p = xmlnode_get_data(priority);

	if (presence->priority != 0)
		purple_debug_warning("jabber", "presence stanza received with multiple "
		                     "priority children!?\n");

	if (p) {
		presence->priority = atoi(p);
		g_free(p);
	} else
		purple_debug_warning("jabber", "Empty <priority/> in presence!\n");
}

static void
parse_show(JabberStream *js, JabberPresence *presence, xmlnode *show)
{
	char *cdata;

	if (presence->type != JABBER_PRESENCE_AVAILABLE) {
		purple_debug_warning("jabber", "<show/> present on presence, but "
		                               "type is not default ('available')\n");
		return;
	}

	cdata = xmlnode_get_data(show);
	if (cdata) {
		presence->state = litxmpp_buddy_show_get_state(cdata);
		g_free(cdata);
	} else
		purple_debug_warning("jabber", "<show/> present on presence, but "
		                               "no contents!\n");
}

static void
parse_status(JabberStream *js, JabberPresence *presence, xmlnode *status)
{
	/* TODO: Check/track language attribute? */

	g_free(presence->status);
	presence->status = xmlnode_get_data(status);
}

static void
parse_delay(JabberStream *js, JabberPresence *presence, xmlnode *delay)
{
	const char *stamp = xmlnode_get_attrib(delay, "stamp");
	presence->delayed = TRUE;
	presence->sent = purple_str_to_time(stamp, TRUE, NULL, NULL, NULL);
}

static void
parse_idle(JabberStream *js, JabberPresence *presence, xmlnode *query)
{
	const gchar *seconds = xmlnode_get_attrib(query, "seconds");
	if (seconds) {
		presence->idle = atoi(seconds);
		if (presence->idle < 0) {
			purple_debug_warning("jabber", "Received bogus idle time %s\n", seconds);
			presence->idle = 0;
		}
	}
}

static void
parse_caps(JabberStream *js, JabberPresence *presence, xmlnode *c)
{
	/* TODO: Move the rest of the caps handling in here, after changing the
	 * the "do we have details about this (node, ver) and exts" to not
	 * require the jbr to be present (since that happens later).
	 */
	presence->caps = c;
}

static void
parse_nickname(JabberStream *js, JabberPresence *presence, xmlnode *nick)
{
	g_free(presence->nickname);
	presence->nickname = xmlnode_get_data(nick);
}

static void
parse_vcard_avatar(JabberStream *js, JabberPresence *presence, xmlnode *x)
{
	xmlnode *photo = xmlnode_get_child(x, "photo");

	if (photo) {
		char *hash_tmp = xmlnode_get_data(photo);
		g_free(presence->vcard_avatar_hash);
		presence->vcard_avatar_hash =
			hash_tmp ? hash_tmp : g_strdup("");
	}
}

static void
parse_muc_user(JabberStream *js, JabberPresence *presence, xmlnode *x)
{
	xmlnode *status;

	if (presence->chat == NULL) {
		purple_debug_warning("jabber", "Ignoring MUC gloop on non-MUC presence\n");
		return;
	}

	if (presence->chat->conv == NULL)
		presence->chat->muc = TRUE;

	for (status = xmlnode_get_child(x, "status"); status;
			status = xmlnode_get_next_twin(status)) {
		const char *code = xmlnode_get_attrib(status, "code");
		int val;
		if (!code)
			continue;

		val = atoi(code);
		if (val == 0 || val < 0) {
			purple_debug_warning("jabber", "Ignoring bogus status code '%s'\n",
			                               code);
			continue;
		}

		presence->chat_info.codes = g_slist_prepend(presence->chat_info.codes, GINT_TO_POINTER(val));
	}

	presence->chat_info.item = xmlnode_get_child(x, "item");
}

void litxmpp_presence_register_handler(const char *node, const char *xmlns,
                                      JabberPresenceHandler *handler)
{
	/*
	 * This is valid because nodes nor namespaces cannot have spaces in them
	 * (see http://www.w3.org/TR/2006/REC-xml-20060816/ and
	 * http://www.w3.org/TR/REC-xml-names/)
	 */
	char *key = g_strdup_printf("%s %s", node, xmlns);
	g_hash_table_replace(presence_handlers, key, handler);
}

void litxmpp_presence_init(void)
{
	presence_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

	/* Core RFC things */
	litxmpp_presence_register_handler("priority", "jabber:client", parse_priority);
	litxmpp_presence_register_handler("show", "jabber:client", parse_show);
	litxmpp_presence_register_handler("status", "jabber:client", parse_status);

	/* XEPs */
	litxmpp_presence_register_handler("c", "http://jabber.org/protocol/caps", parse_caps);
	litxmpp_presence_register_handler("delay", NS_DELAYED_DELIVERY, parse_delay);
	litxmpp_presence_register_handler("nick", "http://jabber.org/protocol/nick", parse_nickname);
	litxmpp_presence_register_handler("query", NS_LAST_ACTIVITY, parse_idle);
	litxmpp_presence_register_handler("x", NS_DELAYED_DELIVERY_LEGACY, parse_delay);
	litxmpp_presence_register_handler("x", "http://jabber.org/protocol/muc#user", parse_muc_user);
	litxmpp_presence_register_handler("x", "vcard-temp:x:update", parse_vcard_avatar);
}

void litxmpp_presence_uninit(void)
{
	g_hash_table_destroy(presence_handlers);
	presence_handlers = NULL;
}
