/*
 * gaim
 *
 * Some code copyright (C) 1998-1999, Mark Spencer <markster@marko.net>
 * libfaim code copyright 1998, 1999 Adam Fritzler <afritz@auk.cx>
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif


#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <ctype.h>
#include <crypt.h>
#include "multi.h"
#include "prpl.h"
#include "gaim.h"
#include "proxy.h"

#include "pixmaps/status-away.xpm"
#include "pixmaps/status-here.xpm"
#include "pixmaps/status-idle.xpm"

#undef YAHOO_DEBUG

#define USEROPT_MAIL 0

#define USEROPT_PAGERHOST 3
#define YAHOO_PAGER_HOST "scs.yahoo.com"
#define USEROPT_PAGERPORT 4
#define YAHOO_PAGER_PORT 5050

enum yahoo_service { /* these are easier to see in hex */
	YAHOO_SERVICE_LOGON = 1,
	YAHOO_SERVICE_LOGOFF,
	YAHOO_SERVICE_ISAWAY,
	YAHOO_SERVICE_ISBACK,
	YAHOO_SERVICE_IDLE, /* 5 (placemarker) */
	YAHOO_SERVICE_MESSAGE,
	YAHOO_SERVICE_IDACT,
	YAHOO_SERVICE_IDDEACT,
	YAHOO_SERVICE_MAILSTAT,
	YAHOO_SERVICE_USERSTAT, /* 0xa */
	YAHOO_SERVICE_NEWMAIL,
	YAHOO_SERVICE_CHATINVITE,
	YAHOO_SERVICE_CALENDAR,
	YAHOO_SERVICE_NEWPERSONALMAIL,
	YAHOO_SERVICE_NEWCONTACT,
	YAHOO_SERVICE_ADDIDENT, /* 0x10 */
	YAHOO_SERVICE_ADDIGNORE,
	YAHOO_SERVICE_PING,
	YAHOO_SERVICE_GROUPRENAME,
	YAHOO_SERVICE_SYSMESSAGE = 0x14,
	YAHOO_SERVICE_PASSTHROUGH2 = 0x16,
	YAHOO_SERVICE_CONFINVITE = 0x18,
	YAHOO_SERVICE_CONFLOGON,
	YAHOO_SERVICE_CONFDECLINE,
	YAHOO_SERVICE_CONFLOGOFF,
	YAHOO_SERVICE_CONFADDINVITE,
	YAHOO_SERVICE_CONFMSG,
	YAHOO_SERVICE_CHATLOGON,
	YAHOO_SERVICE_CHATLOGOFF,
	YAHOO_SERVICE_CHATMSG = 0x20,
	YAHOO_SERVICE_GAMELOGON = 0x28,
	YAHOO_SERVICE_GAMELOGOFF = 0x29,
	YAHOO_SERVICE_FILETRANSFER = 0x46,
	YAHOO_SERVICE_LIST = 0x55,
	YAHOO_SERVICE_ADDBUDDY = 0x83,
	YAHOO_SERVICE_REMBUDDY = 0x84
};

enum yahoo_status {
	YAHOO_STATUS_AVAILABLE = 0,
	YAHOO_STATUS_BRB,
	YAHOO_STATUS_BUSY,
	YAHOO_STATUS_NOTATHOME,
	YAHOO_STATUS_NOTATDESK,
	YAHOO_STATUS_NOTINOFFICE,
	YAHOO_STATUS_ONPHONE,
	YAHOO_STATUS_ONVACATION,
	YAHOO_STATUS_OUTTOLUNCH,
	YAHOO_STATUS_STEPPEDOUT,
	YAHOO_STATUS_INVISIBLE = 12,
	YAHOO_STATUS_CUSTOM = 99,
	YAHOO_STATUS_IDLE = 999,
	YAHOO_STATUS_OFFLINE = 0x5a55aa56 /* don't ask */
};

struct yahoo_data {
	int fd;
	guchar *rxqueue;
	int rxlen;
	GHashTable *hash;
	GSList *login;
	int current_status;
	gboolean logged_in;
};

struct yahoo_pair {
	int key;
	char *value;
};

struct yahoo_packet {
	guint16 service;
	guint32 status;
	guint32 id;
	GSList *hash;
};

struct yahoo_buddy {
	char *name;
	int state;
	char *msg;
};

static char *yahoo_name() {
	return "Yahoo";
}

#define YAHOO_PACKET_HDRLEN (4 + 2 + 2 + 2 + 2 + 4 + 4)

static struct yahoo_packet *yahoo_packet_new(enum yahoo_service service, enum yahoo_status status, int id)
{
	struct yahoo_packet *pkt = g_new0(struct yahoo_packet, 1);

	pkt->service = service;
	pkt->status = status;
	pkt->id = id;

	return pkt;
}

static void yahoo_packet_hash(struct yahoo_packet *pkt, int key, char *value)
{
	struct yahoo_pair *pair = g_new0(struct yahoo_pair, 1);
	pair->key = key;
	pair->value = g_strdup(value);
	pkt->hash = g_slist_append(pkt->hash, pair);
}

static int yahoo_packet_length(struct yahoo_packet *pkt)
{
	GSList *l;

	int len = 0;

	l = pkt->hash;
	while (l) {
		struct yahoo_pair *pair = l->data;
		int tmp = pair->key;
		do {
			tmp /= 10;
			len++;
		} while (tmp);
		len += 2;
		len += strlen(pair->value);
		len += 2;
		l = l->next;
	}

	return len;
}

/* sometimes i wish prpls could #include things from other prpls. then i could just
 * use the routines from libfaim and not have to admit to knowing how they work. */
#define yahoo_put16(buf, data) ( \
		(*(buf) = (u_char)((data)>>8)&0xff), \
		(*((buf)+1) = (u_char)(data)&0xff),  \
		2)
#define yahoo_get16(buf) ((((*(buf))<<8)&0xff00) + ((*((buf)+1)) & 0xff))
#define yahoo_put32(buf, data) ( \
		(*((buf)) = (u_char)((data)>>24)&0xff), \
		(*((buf)+1) = (u_char)((data)>>16)&0xff), \
		(*((buf)+2) = (u_char)((data)>>8)&0xff), \
		(*((buf)+3) = (u_char)(data)&0xff), \
		4)
#define yahoo_get32(buf) ((((*(buf))<<24)&0xff000000) + \
		(((*((buf)+1))<<16)&0x00ff0000) + \
		(((*((buf)+2))<< 8)&0x0000ff00) + \
		(((*((buf)+3)    )&0x000000ff)))

static void yahoo_packet_read(struct yahoo_packet *pkt, guchar *data, int len)
{
	int pos = 0;

	while (pos + 1 < len) {
		char key[64], *value;
		int x;

		struct yahoo_pair *pair = g_new0(struct yahoo_pair, 1);
		pkt->hash = g_slist_append(pkt->hash, pair);

		x = 0;
		while (pos + 1 < len) {
			if (data[pos] == 0xc0 && data[pos + 1] == 0x80)
				break;
			key[x++] = data[pos++];
		}
		key[x] = 0;
		pos += 2;
		pair->key = strtol(key, NULL, 10);

		value = g_malloc(len - pos);
		x = 0;
		while (pos + 1 < len) {
			if (data[pos] == 0xc0 && data[pos + 1] == 0x80)
				break;
			value[x++] = data[pos++];
		}
		value[x] = 0;
		pos += 2;
		pair->value = g_strdup(value);
		g_free(value);
		debug_printf("Key: %d  \tValue: %s\n", pair->key, pair->value);
	}
}

static void yahoo_packet_write(struct yahoo_packet *pkt, guchar *data)
{
	GSList *l = pkt->hash;
	int pos = 0;

	while (l) {
		struct yahoo_pair *pair = l->data;
		guchar buf[100];

		g_snprintf(buf, sizeof(buf), "%d", pair->key);
		strcpy(data + pos, buf);
		pos += strlen(buf);
		data[pos++] = 0xc0;
		data[pos++] = 0x80;

		strcpy(data + pos, pair->value);
		pos += strlen(pair->value);
		data[pos++] = 0xc0;
		data[pos++] = 0x80;

		l = l->next;
	}
}

static void yahoo_packet_dump(guchar *data, int len)
{
#ifdef YAHOO_DEBUG
	int i;
	for (i = 0; i + 1 < len; i += 2) {
		if ((i % 16 == 0) && i)
			debug_printf("\n");
		debug_printf("%02x", data[i]);
		debug_printf("%02x ", data[i+1]);
	}
	if (i < len)
		debug_printf("%02x", data[i]);
	debug_printf("\n");
	for (i = 0; i < len; i++) {
		if ((i % 16 == 0) && i)
			debug_printf("\n");
		if (isprint(data[i]))
			debug_printf("%c ", data[i]);
		else
			debug_printf(". ");
	}
	debug_printf("\n");
#endif
}

static int yahoo_send_packet(struct yahoo_data *yd, struct yahoo_packet *pkt)
{
	int pktlen = yahoo_packet_length(pkt);
	int len = YAHOO_PACKET_HDRLEN + pktlen;
	int ret;

	guchar *data;
	int pos = 0;

	if (yd->fd < 0)
		return -1;

	data = g_malloc0(len + 1);

	memcpy(data + pos, "YMSG", 4); pos += 4;
	pos += yahoo_put16(data + pos, 0x0600);
	pos += yahoo_put16(data + pos, 0x0000);
	pos += yahoo_put16(data + pos, pktlen);
	pos += yahoo_put16(data + pos, pkt->service);
	pos += yahoo_put32(data + pos, pkt->status);
	pos += yahoo_put32(data + pos, pkt->id);

	yahoo_packet_write(pkt, data + pos);

	yahoo_packet_dump(data, len);
	ret = write(yd->fd, data, len);

	g_free(data);

	return ret;
}

static void yahoo_packet_free(struct yahoo_packet *pkt)
{
	while (pkt->hash) {
		struct yahoo_pair *pair = pkt->hash->data;
		g_free(pair->value);
		g_free(pair);
		pkt->hash = g_slist_remove(pkt->hash, pair);
	}
	g_free(pkt);
}

static void yahoo_process_logon(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	struct yahoo_data *yd = gc->proto_data;
	GSList *l = pkt->hash;
	struct yahoo_buddy *buddy = NULL;
	struct yahoo_packet *newpkt;
	char *name = NULL;
	int state = 0;
	char *msg = NULL;

	while (l) {
		struct yahoo_pair *pair = l->data;

		switch (pair->key) {
		case 0: /* we won't actually do anything with this */
			break;
		case 1: /* we don't get the full buddy list here. */
			account_online(gc);
			serv_finish_login(gc);
			g_snprintf(gc->displayname, sizeof(gc->displayname), "%s", pair->value);
			do_import(gc, NULL);
			yd->logged_in = TRUE;

			/* this requests the list. i have a feeling that this is very evil */
			newpkt = yahoo_packet_new(YAHOO_SERVICE_LIST, YAHOO_STATUS_OFFLINE, 0);
			yahoo_send_packet(yd, newpkt);
			yahoo_packet_free(newpkt);

			break;
		case 8: /* how many online buddies we have */
			break;
		case 7: /* the current buddy */
			name = pair->value;
			break;
		case 10: /* state */
			state = strtol(pair->value, NULL, 10);
			break;
		case 19: /* custom message */
			msg = pair->value;
			break;
		case 11: /* i didn't know what this was in the old protocol either */
			break;
		case 17: /* in chat? */
			break;
		case 13: /* in pager, i think this should always be 1 */
			/* we don't actually give notification here. we wait until after we've
			 * gotten the list, so that they get added to the right group */
			buddy = g_new0(struct yahoo_buddy, 1);
			buddy->name = g_strdup(name);
			buddy->state = state;
			buddy->msg = msg ? g_strdup(msg) : NULL;
			yd->login = g_slist_append(yd->login, buddy);
			break;
		default:
			debug_printf("unknown login key %d\n", pair->key);
			break;
		}

		l = l->next;
	}
}

static void yahoo_process_list(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	struct yahoo_data *yd = gc->proto_data;
	GSList *l = pkt->hash;
	gboolean export = FALSE;

	while (l) {
		char **lines;
		char **split;
		char **buddies;
		char **tmp, **bud;

		struct yahoo_pair *pair = l->data;
		l = l->next;

		if (pair->key != 87)
			continue;

		lines = g_strsplit(pair->value, "\n", -1);
		for (tmp = lines; *tmp; tmp++) {
			split = g_strsplit(*tmp, ":", 2);
			if (!split)
				continue;
			if (!split[0] || !split[1]) {
				g_strfreev(split);
				continue;
			}
			buddies = g_strsplit(split[1], ",", -1);
			for (bud = buddies; bud && *bud; bud++)
				if (!find_buddy(gc, *bud)) {
					add_buddy(gc, split[0], *bud, *bud);
					export = TRUE;
				}
			g_strfreev(buddies);
			g_strfreev(split);
		}
		g_strfreev(lines);
	}

	if (export)
		do_export(gc);

	while (yd->login) {
		struct yahoo_buddy *buddy = yd->login->data;
		int status = buddy->state;
		yd->login = g_slist_remove(yd->login, buddy);
		if (status == YAHOO_STATUS_AVAILABLE)
			serv_got_update(gc, buddy->name, 1, 0, 0, 0, 0, 0);
		else if (status == YAHOO_STATUS_IDLE)
			serv_got_update(gc, buddy->name, 1, 0, 0, time(NULL) - 600, (status << 1), 0);
		else
			serv_got_update(gc, buddy->name, 1, 0, 0, 0, (status << 1) | UC_UNAVAILABLE, 0);
		if (status == YAHOO_STATUS_CUSTOM) {
			gpointer val = g_hash_table_lookup(yd->hash, buddy->name);
			if (val) {
				g_free(val);
				g_hash_table_insert(yd->hash, buddy->name, g_strdup(buddy->msg));
			} else
				g_hash_table_insert(yd->hash, g_strdup(buddy->name), g_strdup(buddy->msg));
		}
		g_free(buddy->msg);
		g_free(buddy->name);
		g_free(buddy);
	}
}

static void yahoo_process_message(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	char *msg = NULL;
	char *from = NULL;
	time_t tm = time(NULL);
	GSList *l = pkt->hash;

	while (l) {
		struct yahoo_pair *pair = l->data;
		if (pair->key == 4)
			from = pair->value;
		if (pair->key == 14)
			msg = pair->value;
		if (pair->key == 15)
			tm = strtol(pair->value, NULL, 10);
		l = l->next;
	}

	if (pkt->status == 1) {
		strip_linefeed(msg);
		serv_got_im(gc, from, msg, 0, tm);
	} else if (pkt->status == 2) {
		do_error_dialog(_("Your message did not get sent."), _("Gaim - Error"));
	}
}

static void yahoo_process_status(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	struct yahoo_data *yd = gc->proto_data;
	GSList *l = pkt->hash;
	char *name = NULL;
	int state = 0;
	char *msg = NULL;

	while (l) {
		struct yahoo_pair *pair = l->data;

		switch (pair->key) {
		case 7:
			name = pair->value;
			break;
		case 10:
			state = strtol(pair->value, NULL, 10);
			break;
		case 19:
			msg = pair->value;
			break;
		case 11: /* i didn't know what this was in the old protocol either */
			break;
		case 17: /* in chat? */
			break;
		case 13:
			if (strtol(pair->value, NULL, 10) != 1) {
				serv_got_update(gc, name, 0, 0, 0, 0, 0, 0);
				break;
			}
			if (state == YAHOO_STATUS_AVAILABLE)
				serv_got_update(gc, name, 1, 0, 0, 0, 0, 0);
			else if (state == YAHOO_STATUS_IDLE)
				serv_got_update(gc, name, 1, 0, 0, time(NULL) - 600, (state << 1), 0);
			else
				serv_got_update(gc, name, 1, 0, 0, 0, (state << 1) | UC_UNAVAILABLE, 0);
			if (state == YAHOO_STATUS_CUSTOM) {
				gpointer val = g_hash_table_lookup(yd->hash, name);
				if (val) {
					g_free(val);
					g_hash_table_insert(yd->hash, name, g_strdup(msg));
				} else
					g_hash_table_insert(yd->hash, g_strdup(name), g_strdup(msg));
			}
			break;
		}

		l = l->next;
	}
}

static void yahoo_process_contact(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	struct yahoo_data *yd = gc->proto_data;
	char *id = NULL;
	char *who = NULL;
	char *msg = NULL;
	char *name = NULL;
	int state = YAHOO_STATUS_AVAILABLE;
	int online = FALSE;

	GSList *l = pkt->hash;

	while (l) {
		struct yahoo_pair *pair = l->data;
		if (pair->key == 1)
			id = pair->value;
		else if (pair->key == 3)
			who = pair->value;
		else if (pair->key == 14)
			msg = pair->value;
		else if (pair->key == 7)
			name = pair->value;
		else if (pair->key == 10)
			state = strtol(pair->value, NULL, 10);
		else if (pair->key == 13)
			online = strtol(pair->value, NULL, 10);
		l = l->next;
	}

	if (id)
		show_got_added(gc, id, who, NULL, msg);
	if (name) {
		if (state == YAHOO_STATUS_AVAILABLE)
			serv_got_update(gc, name, 1, 0, 0, 0, 0, 0);
		else if (state == YAHOO_STATUS_IDLE)
			serv_got_update(gc, name, 1, 0, 0, time(NULL) - 600, (state << 1), 0);
		else
			serv_got_update(gc, name, 1, 0, 0, 0, (state << 1) | UC_UNAVAILABLE, 0);
		if (state == YAHOO_STATUS_CUSTOM) {
			gpointer val = g_hash_table_lookup(yd->hash, name);
			if (val) {
				g_free(val);
				g_hash_table_insert(yd->hash, name, g_strdup(msg));
			} else
				g_hash_table_insert(yd->hash, g_strdup(name), g_strdup(msg));
		}
	}
}

static void yahoo_process_mail(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	char *who = NULL;
	char *email = NULL;
	char *subj = NULL;
	int count = 0;
	GSList *l = pkt->hash;

	while (l) {
		struct yahoo_pair *pair = l->data;
		if (pair->key == 9)
			count = strtol(pair->value, NULL, 10);
		else if (pair->key == 43)
			who = pair->value;
		else if (pair->key == 42)
			email = pair->value;
		else if (pair->key == 18)
			subj = pair->value;
		l = l->next;
	}

	connection_has_mail(gc, count, NULL, NULL, "http://mail.yahoo.com/");
}

static void yahoo_packet_process(struct gaim_connection *gc, struct yahoo_packet *pkt)
{
	switch (pkt->service)
	{
	case YAHOO_SERVICE_LOGON:
		yahoo_process_logon(gc, pkt);
		break;
	case YAHOO_SERVICE_ISAWAY:
		yahoo_process_status(gc, pkt);
		break;
	case YAHOO_SERVICE_MESSAGE:
		yahoo_process_message(gc, pkt);
		break;
	case YAHOO_SERVICE_NEWMAIL:
		yahoo_process_mail(gc, pkt);
		break;
	case YAHOO_SERVICE_NEWCONTACT:
		yahoo_process_contact(gc, pkt);
		break;
	case YAHOO_SERVICE_LIST:
		yahoo_process_list(gc, pkt);
		break;
	default:
		debug_printf("unhandled service %d\n", pkt->service);
		break;
	}
}

static void yahoo_pending(gpointer data, gint source, GaimInputCondition cond)
{
	struct gaim_connection *gc = data;
	struct yahoo_data *yd = gc->proto_data;
	char buf[1024];
	int len;

	len = read(yd->fd, buf, sizeof(buf));

	if (len <= 0) {
		hide_login_progress(gc, "Unable to read");
		signoff(gc);
		return;
	}

	yd->rxqueue = g_realloc(yd->rxqueue, len + yd->rxlen);
	memcpy(yd->rxqueue + yd->rxlen, buf, len);
	yd->rxlen += len;

	while (1) {
		struct yahoo_packet *pkt;
		int pos = 0;
		int pktlen;

		if (yd->rxlen < YAHOO_PACKET_HDRLEN)
			return;

		pos += 4; /* YMSG */
		pos += 2;
		pos += 2;

		pktlen = yahoo_get16(yd->rxqueue + pos); pos += 2;
		debug_printf("%d bytes to read, rxlen is %d\n", pktlen, yd->rxlen);

		if (yd->rxlen < (YAHOO_PACKET_HDRLEN + pktlen))
			return;

		yahoo_packet_dump(yd->rxqueue, YAHOO_PACKET_HDRLEN + pktlen);

		pkt = yahoo_packet_new(0, 0, 0);

		pkt->service = yahoo_get16(yd->rxqueue + pos); pos += 2;
		debug_printf("Yahoo Service: %d Status: %d\n", pkt->service, pkt->status);
		pkt->status = yahoo_get32(yd->rxqueue + pos); pos += 4;
		pkt->id = yahoo_get32(yd->rxqueue + pos); pos += 4;

		yahoo_packet_read(pkt, yd->rxqueue + pos, pktlen);

		yd->rxlen -= YAHOO_PACKET_HDRLEN + pktlen;
		if (yd->rxlen) {
			char *tmp = g_memdup(yd->rxqueue + YAHOO_PACKET_HDRLEN + pktlen, yd->rxlen);
			g_free(yd->rxqueue);
			yd->rxqueue = tmp;
		} else {
			g_free(yd->rxqueue);
			yd->rxqueue = NULL;
		}

		yahoo_packet_process(gc, pkt);

		yahoo_packet_free(pkt);
	}
}

static void yahoo_got_connected(gpointer data, gint source, GaimInputCondition cond)
{
	struct gaim_connection *gc = data;
	struct yahoo_data *yd;
	struct yahoo_packet *pkt;

	if (!g_slist_find(connections, gc)) {
		close(source);
		return;
	}

	if (source < 0) {
		hide_login_progress(gc, "Unable to connect");
		signoff(gc);
		return;
	}

	yd = gc->proto_data;
	yd->fd = source;

	pkt = yahoo_packet_new(YAHOO_SERVICE_LOGON, YAHOO_STATUS_AVAILABLE, 0);

	yahoo_packet_hash(pkt, 0, gc->username);
	yahoo_packet_hash(pkt, 1, gc->username);
	yahoo_packet_hash(pkt, 6, crypt(gc->password, "$1$_2S43d5f$"));

	yahoo_send_packet(yd, pkt);

	yahoo_packet_free(pkt);

	gc->inpa = gaim_input_add(yd->fd, GAIM_INPUT_READ, yahoo_pending, gc);
}

static void yahoo_login(struct aim_user *user) {
	struct gaim_connection *gc = new_gaim_conn(user);
	struct yahoo_data *yd = gc->proto_data = g_new0(struct yahoo_data, 1);

	set_login_progress(gc, 1, "Connecting");

	yd->fd = -1;
	yd->hash = g_hash_table_new(g_str_hash, g_str_equal);

	if (proxy_connect(user->proto_opt[USEROPT_PAGERHOST][0] ?
				user->proto_opt[USEROPT_PAGERHOST] : YAHOO_PAGER_HOST,
			   user->proto_opt[USEROPT_PAGERPORT][0] ?
				atoi(user->proto_opt[USEROPT_PAGERPORT]) : YAHOO_PAGER_PORT,
			   yahoo_got_connected, gc) < 0) {
		hide_login_progress(gc, "Connection problem");
		signoff(gc);
		return;
	}

}

static gboolean yahoo_destroy_hash(gpointer key, gpointer val, gpointer data)
{
	g_free(key);
	g_free(val);
	return TRUE;
}

static void yahoo_close(struct gaim_connection *gc) {
	struct yahoo_data *yd = (struct yahoo_data *)gc->proto_data;
	g_hash_table_foreach_remove(yd->hash, yahoo_destroy_hash, NULL);
	g_hash_table_destroy(yd->hash);
	if (yd->fd >= 0)
		close(yd->fd);
	if (yd->rxqueue)
		g_free(yd->rxqueue);
	yd->rxlen = 0;
	while (yd->login) {
		struct yahoo_buddy *buddy = yd->login->data;
		yd->login = g_slist_remove(yd->login, buddy);
		g_free(buddy->msg);
		g_free(buddy->name);
		g_free(buddy);
	}
	if (gc->inpa)
		gaim_input_remove(gc->inpa);
	g_free(yd);
}

static char **yahoo_list_icon(int uc)
{
	if ((uc >> 1) == YAHOO_STATUS_IDLE)
		return status_idle_xpm;
	else if (uc == 0)
		return status_here_xpm;
	return status_away_xpm;
}

static char *yahoo_get_status_string(enum yahoo_status a)
{
	switch (a) {
	case YAHOO_STATUS_BRB:
		return "Be Right Back";
	case YAHOO_STATUS_BUSY:
		return "Busy";
	case YAHOO_STATUS_NOTATHOME:
		return "Not At Home";
	case YAHOO_STATUS_NOTATDESK:
		return "Not At Desk";
	case YAHOO_STATUS_NOTINOFFICE:
		return "Not In Office";
	case YAHOO_STATUS_ONPHONE:
		return "On Phone";
	case YAHOO_STATUS_ONVACATION:
		return "On Vacation";
	case YAHOO_STATUS_OUTTOLUNCH:
		return "Out To Lunch";
	case YAHOO_STATUS_STEPPEDOUT:
		return "Stepped Out";
	default:
		return NULL;
	}
}

static GList *yahoo_buddy_menu(struct gaim_connection *gc, char *who)
{
	GList *m = NULL;
	struct proto_buddy_menu *pbm;
	struct yahoo_data *yd = (struct yahoo_data *)gc->proto_data;
	struct buddy *b = find_buddy(gc, who); /* this should never be null. if it is,
						  segfault and get the bug report. */
	static char buf[1024];

	if (!(b->uc & UC_UNAVAILABLE))
		return NULL;

	pbm = g_new0(struct proto_buddy_menu, 1);
	if ((b->uc >> 1) != YAHOO_STATUS_CUSTOM)
		g_snprintf(buf, sizeof buf, "Status: %s", yahoo_get_status_string(b->uc >> 1));
	else
		g_snprintf(buf, sizeof buf, "Custom Status: %s",
			   (char *)g_hash_table_lookup(yd->hash, b->name));
	pbm->label = buf;
	pbm->callback = NULL;
	pbm->gc = gc;
	m = g_list_append(m, pbm);

	return m;
}

static GList *yahoo_user_opts()
{
	GList *m = NULL;
	struct proto_user_opt *puo;

	puo = g_new0(struct proto_user_opt, 1);
	puo->label = "Pager Host:";
	puo->def = YAHOO_PAGER_HOST;
	puo->pos = USEROPT_PAGERHOST;
	m = g_list_append(m, puo);

	puo = g_new0(struct proto_user_opt, 1);
	puo->label = "Pager Port:";
	puo->def = "5050";
	puo->pos = USEROPT_PAGERPORT;
	m = g_list_append(m, puo);

	return m;
}

static void yahoo_act_id(gpointer data, char *entry)
{
	struct gaim_connection *gc = data;
	struct yahoo_data *yd = gc->proto_data;

	struct yahoo_packet *pkt = yahoo_packet_new(YAHOO_SERVICE_IDACT, YAHOO_STATUS_AVAILABLE, 0);
	yahoo_packet_hash(pkt, 3, entry);
	yahoo_send_packet(yd, pkt);
	yahoo_packet_free(pkt);

	g_snprintf(gc->displayname, sizeof(gc->displayname), "%s", entry);
}

static void yahoo_do_action(struct gaim_connection *gc, char *act)
{
	if (!strcmp(act, "Activate ID")) {
		do_prompt_dialog("Activate which ID:", gc->displayname, gc, yahoo_act_id, NULL);
	}
}

static GList *yahoo_actions() {
	GList *m = NULL;

	m = g_list_append(m, "Activate ID");

	return m;
}

static int yahoo_send_im(struct gaim_connection *gc, char *who, char *what, int flags)
{
	struct yahoo_data *yd = gc->proto_data;
	struct yahoo_packet *pkt = yahoo_packet_new(YAHOO_SERVICE_MESSAGE, YAHOO_STATUS_OFFLINE, 0);

	yahoo_packet_hash(pkt, 1, gc->displayname);
	yahoo_packet_hash(pkt, 5, who);
	yahoo_packet_hash(pkt, 14, what);

	yahoo_send_packet(yd, pkt);

	yahoo_packet_free(pkt);

	return 1;
}

static void yahoo_set_away(struct gaim_connection *gc, char *state, char *msg)
{
	struct yahoo_data *yd = (struct yahoo_data *)gc->proto_data;
	struct yahoo_packet *pkt;
	char s[4];

	gc->away = NULL;

	if (msg) {
		yd->current_status = YAHOO_STATUS_CUSTOM;
		gc->away = "";
	} else if (state) {
		gc->away = "";
		if (!strcmp(state, "Available")) {
			yd->current_status = YAHOO_STATUS_AVAILABLE;
			gc->away = NULL;
		} else if (!strcmp(state, "Be Right Back")) {
			yd->current_status = YAHOO_STATUS_BRB;
		} else if (!strcmp(state, "Busy")) {
			yd->current_status = YAHOO_STATUS_BUSY;
		} else if (!strcmp(state, "Not At Home")) {
			yd->current_status = YAHOO_STATUS_NOTATHOME;
		} else if (!strcmp(state, "Not At Desk")) {
			yd->current_status = YAHOO_STATUS_NOTATDESK;
		} else if (!strcmp(state, "Not In Office")) {
			yd->current_status = YAHOO_STATUS_NOTINOFFICE;
		} else if (!strcmp(state, "On Phone")) {
			yd->current_status = YAHOO_STATUS_ONPHONE;
		} else if (!strcmp(state, "On Vacation")) {
			yd->current_status = YAHOO_STATUS_ONVACATION;
		} else if (!strcmp(state, "Out To Lunch")) {
			yd->current_status = YAHOO_STATUS_OUTTOLUNCH;
		} else if (!strcmp(state, "Stepped Out")) {
			yd->current_status = YAHOO_STATUS_STEPPEDOUT;
		} else if (!strcmp(state, "Invisible")) {
			yd->current_status = YAHOO_STATUS_INVISIBLE;
		} else if (!strcmp(state, GAIM_AWAY_CUSTOM)) {
			if (gc->is_idle) {
				yd->current_status = YAHOO_STATUS_IDLE;
			} else {
				yd->current_status = YAHOO_STATUS_AVAILABLE;
			}
			gc->away = NULL;
		}
	} else if (gc->is_idle) {
		yd->current_status = YAHOO_STATUS_IDLE;
	} else {
		yd->current_status = YAHOO_STATUS_AVAILABLE;
	}

	pkt = yahoo_packet_new(YAHOO_SERVICE_ISAWAY, yd->current_status, 0);
	g_snprintf(s, sizeof(s), "%d", yd->current_status);
	yahoo_packet_hash(pkt, 10, s);
	if (yd->current_status == YAHOO_STATUS_CUSTOM)
		yahoo_packet_hash(pkt, 19, msg);

	yahoo_send_packet(yd, pkt);
	yahoo_packet_free(pkt);
}

static void yahoo_set_idle(struct gaim_connection *gc, int idle)
{
	struct yahoo_data *yd = gc->proto_data;
	struct yahoo_packet *pkt = NULL;

	if (idle && yd->current_status == YAHOO_STATUS_AVAILABLE) {
		pkt = yahoo_packet_new(YAHOO_SERVICE_ISAWAY, YAHOO_STATUS_IDLE, 0);
		yd->current_status = YAHOO_STATUS_IDLE;
	} else if (!idle && yd->current_status == YAHOO_STATUS_IDLE) {
		pkt = yahoo_packet_new(YAHOO_SERVICE_ISAWAY, YAHOO_STATUS_AVAILABLE, 0);
		yd->current_status = YAHOO_STATUS_AVAILABLE;
	}

	if (pkt) {
		char buf[4];
		g_snprintf(buf, sizeof(buf), "%d", yd->current_status);
		yahoo_packet_hash(pkt, 10, buf);
		yahoo_send_packet(yd, pkt);
		yahoo_packet_free(pkt);
	}
}

static GList *yahoo_away_states(struct gaim_connection *gc)
{
	GList *m = NULL;

	m = g_list_append(m, "Available");
	m = g_list_append(m, "Be Right Back");
	m = g_list_append(m, "Busy");
	m = g_list_append(m, "Not At Home");
	m = g_list_append(m, "Not At Desk");
	m = g_list_append(m, "Not In Office");
	m = g_list_append(m, "On Phone");
	m = g_list_append(m, "On Vacation");
	m = g_list_append(m, "Out To Lunch");
	m = g_list_append(m, "Stepped Out");
	m = g_list_append(m, "Invisible");
	m = g_list_append(m, GAIM_AWAY_CUSTOM);

	return m;
}

static void yahoo_keepalive(struct gaim_connection *gc)
{
	struct yahoo_data *yd = gc->proto_data;
	struct yahoo_packet *pkt = yahoo_packet_new(YAHOO_SERVICE_PING, YAHOO_STATUS_AVAILABLE, 0);
	yahoo_send_packet(yd, pkt);
	yahoo_packet_free(pkt);
}

static void yahoo_add_buddy(struct gaim_connection *gc, char *who)
{
	struct yahoo_data *yd = (struct yahoo_data *)gc->proto_data;
	struct yahoo_packet *pkt;
	struct group *g;
	char *group = NULL;

	if (!yd->logged_in)
		return;

	g = find_group_by_buddy(gc, who);
	if (g)
		group = g->name;
	else
		group = "Buddies";

	pkt = yahoo_packet_new(YAHOO_SERVICE_ADDBUDDY, YAHOO_STATUS_AVAILABLE, 0);
	yahoo_packet_hash(pkt, 1, gc->displayname);
	yahoo_packet_hash(pkt, 7, who);
	yahoo_packet_hash(pkt, 65, group);
	yahoo_send_packet(yd, pkt);
	yahoo_packet_free(pkt);
}

static void yahoo_remove_buddy(struct gaim_connection *gc, char *who, char *group)
{
	struct yahoo_data *yd = (struct yahoo_data *)gc->proto_data;

	struct yahoo_packet *pkt = yahoo_packet_new(YAHOO_SERVICE_REMBUDDY, YAHOO_STATUS_AVAILABLE, 0);
	yahoo_packet_hash(pkt, 1, gc->displayname);
	yahoo_packet_hash(pkt, 7, who);
	yahoo_packet_hash(pkt, 65, group);
	yahoo_send_packet(yd, pkt);
	yahoo_packet_free(pkt);
}

static struct prpl *my_protocol = NULL;

void yahoo_init(struct prpl *ret) {
	ret->protocol = PROTO_YAHOO;
	ret->options = OPT_PROTO_MAIL_CHECK;
	ret->name = yahoo_name;
	ret->user_opts = yahoo_user_opts;
	ret->login = yahoo_login;
	ret->close = yahoo_close;
	ret->buddy_menu = yahoo_buddy_menu;
	ret->list_icon = yahoo_list_icon;
	ret->actions = yahoo_actions;
	ret->do_action = yahoo_do_action;
	ret->send_im = yahoo_send_im;
	ret->away_states = yahoo_away_states;
	ret->set_away = yahoo_set_away;
	ret->set_idle = yahoo_set_idle;
	ret->keepalive = yahoo_keepalive;
	ret->add_buddy = yahoo_add_buddy;
	ret->remove_buddy = yahoo_remove_buddy;

	my_protocol = ret;
}

#ifndef STATIC

char *gaim_plugin_init(GModule *handle)
{
	load_protocol(yahoo_init, sizeof(struct prpl));
	return NULL;
}

void gaim_plugin_remove()
{
	struct prpl *p = find_prpl(PROTO_YAHOO);
	if (p == my_protocol)
		unload_protocol(p);
}

char *name()
{
	return "Yahoo";
}

char *description()
{
	return PRPL_DESC("Yahoo");
}

#endif
