#include "nateon.h"
#include "xfer.h"

/* Wait this many seconds before trying FR-mode transfer */
#define NATEON_P2P_TIMEOUT_SECONDS  (5)

#define NATEON_P2P_START_PORT   (5004)
#define NATEON_P2P_END_PORT	 (6004)

static void nateon_xfer_end(PurpleXfer *xfer);
static ssize_t nateon_xfer_sock_write(NateonXferConnection *conn, const char *buf, size_t len);
static void nateon_xfer_send_next(NateonXfer *nate_xfer);

static char*
generate_p2p_cookie(NateonSession *session)
{
	NateonUser *user;
	char *cookie;
	static guint p2p_index = 1000;

	user = session->user;

	if (p2p_index > 9999) {
		p2p_index = 1000;
	}
	cookie = g_strdup_printf("%s:%d", user->id, ++p2p_index);

	purple_debug_info("nateon", "generated p2p_cookie:%s\n", cookie);
	return cookie;
}

static void
nateon_xfer_cancel_recv_dummy(PurpleXfer *xfer)
{
	return;
}

static void
nateon_xfer_cancel_recv(PurpleXfer *xfer)
{
	purple_debug_info("nateon", "%s\n", __FUNCTION__);
	/* FIXME: send FILE NACK */
	
	/* free NateonXfer */
	nateon_xfer_end(xfer);
}

static void
nateon_xfer_cancel_send(PurpleXfer *xfer)
{
	purple_debug_info("nateon", "%s\n", __FUNCTION__);
	/* FIXME: send FILE NACK */
	
	/* free NateonXfer */
	nateon_xfer_end(xfer);
}

static void
nateon_xfer_request_denied(PurpleXfer *xfer)
{
	purple_debug_info("nateon", "%s\n", __FUNCTION__);
	nateon_xfer_end(xfer);
}

static void
nateon_xfer_process_p2p_cmd_receive(NateonXfer *xfer, char *cmd)
{
	gchar **split;
	gint n_split;
	gchar *buf;

	split = g_strsplit(cmd, " ", 0);
	for (n_split=0; split[n_split]; n_split++) { }

	if (!strncmp(split[0], "ATHC", 4) && strstr(cmd, "6004")) 
	{
		buf = g_strdup_printf("ATHC %d 100 6004 0\r\n", atoi(split[1]));
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);

		buf = g_strdup_printf("FILE 0 ACCEPT %s 0\r\n", xfer->file_cookie);
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);

	}

	else if ( (n_split == 4) && !strncmp(split[0], "ATHC", 4) && !strncmp(split[1], "0", 1) &&
			!strncmp(split[2], "100", 3) && !strncmp(split[3], "0", 1))
	{
		buf = g_strdup_printf("FILE 0 ACCEPT %s 0\r\n", xfer->file_cookie);
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);
	}

	else if (!strncmp(split[0], "FILE", 4) && !strncmp(split[2], "INFO", 4)) 
	{
		buf = g_strdup_printf("FILE 0 START 0 0\r\n");
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);
	}

	else if (!strncmp(split[0], "FILE", 4) && !strncmp(split[2], "DATA", 4)) 
	{
		xfer->chunk_processing = TRUE;
		xfer->chunk_len = atoi(split[3]);
		xfer->chunk_cur_len = 0;
	}

	/* FR commands */

	else if (!strncmp(split[0], "FRIN", 4) && !strncmp(split[2], "100", 3))
	{
		/* Send REQC FR packet to file sender */
		gchar *payload;
		gint payload_len;
		NateonTransaction *trans;

		if (!(xfer->p2p_cookie)) {
			xfer->p2p_cookie = generate_p2p_cookie(xfer->session);
		}
		payload = g_strdup_printf("REQC FR %s:%d %s %s\r\n", 
						xfer->fr_ip, xfer->fr_port, 
						xfer->p2p_cookie, xfer->fr_authkey);
		payload_len = strlen(payload);

		trans = nateon_transaction_new(xfer->session->notification->cmdproc, "CTOC", "%s N %d", \
				purple_xfer_get_remote_user(xfer->prpl_xfer), payload_len);
		nateon_transaction_set_payload(trans, payload, payload_len);

		g_free(payload);

		nateon_cmdproc_send_trans(xfer->session->notification->cmdproc, trans);

	}

	g_strfreev(split);
}

static void
nateon_xfer_process_p2p_cmd_send(NateonXfer *xfer, char *cmd)
{
	gchar **split;
	gint n_split;
	gchar *buf;

	split = g_strsplit(cmd, " ", 0);
	for (n_split=0; split[n_split]; n_split++) { }

	if (!strncmp(split[0], "ATHC", 4) && strstr(cmd, "6004")) 
	{
		buf = g_strdup_printf("ATHC %d 100 6004 0\r\n", atoi(split[1]));
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);
	}

	else if ( (n_split == 4) && !strncmp(split[0], "ATHC", 4) && !strncmp(split[1], "0", 1) &&
			!strncmp(split[2], "100", 3) && !strncmp(split[3], "0", 1))
	{
		/* do nothing */
	}

	else if (!strncmp(split[0], "FILE", 4) && !strncmp(split[2], "ACCEPT", 6))
	{
		if (strcmp(xfer->file_cookie, split[3]) != 0)
		{
			purple_debug_info("nateon", "%s:file cookie does not match!\n", __FUNCTION__);
			purple_xfer_cancel_remote(xfer->prpl_xfer);
			return;
		}
		buf = g_strdup_printf("FILE %s INFO FILENAME %d CHAT 0\n",
				split[1], purple_xfer_get_size(xfer->prpl_xfer));
		nateon_xfer_sock_write(&xfer->conn, buf, strlen(buf));
		g_free(buf);
	}

	else if (!strncmp(split[0], "FILE", 4) && !strncmp(split[2], "START", 5)) 
	{
		/* Start sending data */
		xfer->send_data_trid = atoi(split[1]);
		nateon_xfer_send_next(xfer);
	}

	else if (!strncmp(split[0], "FILE", 4) && !strncmp(split[2], "END", 3)) 
	{
		purple_debug_info("nateon", "%s: file transfer complete\n", __FUNCTION__);
		purple_xfer_set_completed(xfer->prpl_xfer, TRUE);
	}

	g_strfreev(split);
}

static gssize 
nateon_xfer_sock_read(NateonXferConnection *conn, guchar **buffer)
{
	NateonXfer *nate_xfer;
	int len, cur_len;
	char buf[NATEON_BUF_LEN];
	char *end, *cur, *old_rx_buf;
	int buffer_len;

	*buffer = NULL;
	buffer_len = 0;
	nate_xfer = conn->nate_xfer;

	if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_RECEIVE) {
		conn = &nate_xfer->conn;
	
		len = read(conn->fd, buf, sizeof(buf) - 1);

		if (len <= 0) {
			if (len < 0 && errno == EAGAIN) {
				return 0;
			} else {
				/* FIXME handle error */
				purple_debug_info("nateon", "%s:read_error\n", __FUNCTION__);
				return -1;
			}
		}

		buf[len] = '\0';
		conn->rx_buf = g_realloc(conn->rx_buf, len + conn->rx_len + 1);
		memcpy(conn->rx_buf + conn->rx_len, buf, len + 1);
		conn->rx_len += len;
		end = old_rx_buf = conn->rx_buf;

		do
		{
			if (nate_xfer->chunk_processing) {
				/* Receiving real file data */

				if (conn->rx_len < nate_xfer->chunk_len - nate_xfer->chunk_cur_len) {
					cur_len = conn->rx_len;
				} else {
					cur_len = nate_xfer->chunk_len - nate_xfer->chunk_cur_len;
				}

				*buffer = g_realloc(*buffer, buffer_len + cur_len);
				memcpy(*buffer + buffer_len, end, cur_len);
				buffer_len += cur_len;
				nate_xfer->recv_len += cur_len;

				end += cur_len;
				conn->rx_len -= cur_len;
				nate_xfer->chunk_cur_len += cur_len;

				if (nate_xfer->chunk_cur_len == nate_xfer->chunk_len) {
					nate_xfer->chunk_processing = FALSE;
					nate_xfer->chunk_cur_len = 0;
					nate_xfer->chunk_len = 0;
				}

				if (nate_xfer->recv_len == purple_xfer_get_size(nate_xfer->prpl_xfer)) {
					gchar *buf;

					purple_debug_info("nateon", "%s:%s\n", __FUNCTION__, "file transfer complete");
					purple_xfer_set_completed(nate_xfer->prpl_xfer, TRUE);

					/* Send FILE END to sender */
					buf = g_strdup_printf("ATHC 0 END N 0\r\n");
					nateon_xfer_sock_write(&nate_xfer->conn, buf, strlen(buf));
					g_free(buf);
				}
			} else {	/* processing p2p command */
				cur = end;
				end = strstr(cur, "\r\n");
				if (end == NULL)
				{
					/* The command is still not complete. */
					break;
				}
				*end = '\0';
				end += 2;
				cur_len = end - cur;

				conn->rx_len -= cur_len;

				nateon_xfer_process_p2p_cmd_receive(nate_xfer, cur);
			}
		} while (conn->rx_len > 0);

		if (conn->rx_len > 0)
			conn->rx_buf = g_memdup(cur, conn->rx_len);
		else
			conn->rx_buf = NULL;

		g_free(old_rx_buf);

		return buffer_len;
	}

	else if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_SEND)
	{
		conn = &nate_xfer->conn;
	
		len = read(conn->fd, buf, sizeof(buf) - 1);

		if (len <= 0) {
			if (len < 0 && errno == EAGAIN) {
				return 0;
			} else {
				/* FIXME handle error */
				purple_debug_info("nateon", "%s:read_error\n", __FUNCTION__);
				return -1;
			}
		}

		buf[len] = '\0';
		conn->rx_buf = g_realloc(conn->rx_buf, len + conn->rx_len + 1);
		memcpy(conn->rx_buf + conn->rx_len, buf, len + 1);
		conn->rx_len += len;
		end = old_rx_buf = conn->rx_buf;

		do
		{
			/* processing p2p command */
			cur = end;
			end = strstr(cur, "\r\n");
			if (end == NULL)
			{
				/* The command is still not complete. */
				break;
			}
			*end = '\0';
			end += 2;
			cur_len = end - cur;

			conn->rx_len -= cur_len;

			nateon_xfer_process_p2p_cmd_send(nate_xfer, cur);
		} while (conn->rx_len > 0);

		if (conn->rx_len > 0)
			conn->rx_buf = g_memdup(cur, conn->rx_len);
		else
			conn->rx_buf = NULL;

		g_free(old_rx_buf);

		return buffer_len;
	}
	return 0;
}

static void
nateon_xfer_sock_read_cb(gpointer data, gint source, PurpleInputCondition condition)
{
	NateonXfer *nate_xfer = data;
	gssize r = 0;
	guchar *buffer = NULL;

	if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_RECEIVE)
	{
		r = nateon_xfer_sock_read(&nate_xfer->conn, &buffer);
		if (r > 0)
		{
			const size_t wc = fwrite(buffer, 1, r, nate_xfer->dest_fp);
			if (wc != r)
			{
				purple_debug_error("nateon", "%s:Unable to write whole buffer.\n");
				if (nate_xfer->content_type == NATEON_XFER_CONTENT_FILE)
				{
					purple_xfer_cancel_remote(nate_xfer->prpl_xfer);
				}
				g_free(buffer);
				return;
			}
			if (nate_xfer->content_type == NATEON_XFER_CONTENT_FILE)
			{
				purple_xfer_set_bytes_sent(nate_xfer->prpl_xfer, nate_xfer->recv_len);
				purple_xfer_update_progress(nate_xfer->prpl_xfer);
			}
		}
		else if (r < 0)
		{
			if (nate_xfer->content_type == NATEON_XFER_CONTENT_FILE)
			{
				purple_xfer_cancel_remote(nate_xfer->prpl_xfer);
			}

		}
	}
	else if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_SEND)
	{
		r = nateon_xfer_sock_read(&nate_xfer->conn, &buffer);
	}

	if (r > 0)
	{
		g_free(buffer);
	}

	if (purple_xfer_is_completed(nate_xfer->prpl_xfer))
	{
		if (nate_xfer->dest_fp)
		{
			fclose(nate_xfer->dest_fp);
			nate_xfer->dest_fp = NULL;
		}
		if (nate_xfer->content_type == NATEON_XFER_CONTENT_BUDDYIMG)
		{
			const gchar *filepath;
			guchar *imgbuf;
			gint filesize;
			gint ret;
			FILE *fp;

			filesize = purple_xfer_get_size(nate_xfer->prpl_xfer);
			imgbuf = g_malloc(filesize);
			filepath = purple_xfer_get_local_filename(nate_xfer->prpl_xfer);
			fp = fopen(filepath, "rb");
			if (fp)
			{
				ret = fread(imgbuf, 1, filesize, fp);
				fclose(fp);
				if (ret == filesize)
				{
					purple_buddy_icons_set_for_user(nate_xfer->session->account,
							nate_xfer->who, imgbuf, filesize, 
							purple_xfer_get_filename(nate_xfer->prpl_xfer));
				}
			}
			g_unlink(filepath);
		}
		purple_xfer_end(nate_xfer->prpl_xfer);
	}

	return;
}

static void
nateon_xfer_sock_write_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	NateonXferConnection *conn = data;
	int ret, writelen;

	purple_debug_info("nateon", "write_cb\n");
	writelen = purple_circ_buffer_get_max_read(conn->tx_buf);

	if (writelen == 0) {
		purple_input_remove(conn->tx_handler);
		conn->tx_handler = -1;
		return;
	}

	ret = write(conn->fd, conn->tx_buf->outptr, writelen);

	if (ret < 0 && errno == EAGAIN)
		return;
	else if (ret <= 0) {
		purple_debug_info("nateon", "%s:%s\n", __FUNCTION__, "socket write error");
		return;
	}

	purple_circ_buffer_mark_read(conn->tx_buf, ret);
}

static ssize_t
nateon_xfer_sock_write(NateonXferConnection *conn, const char *buf, size_t len)
{
	ssize_t ret = 0;

	g_return_val_if_fail(conn != NULL, 0);

	if (conn->tx_handler == -1) {
		ret = write(conn->fd, buf, len);
	} else {
		ret = -1;
		errno = EAGAIN;
	}

	if (ret < 0 && errno == EAGAIN)
		ret = 0;
	if (ret < len) {
		if (conn->tx_handler == -1)
			conn->tx_handler = purple_input_add(
					conn->fd, PURPLE_INPUT_WRITE,
					nateon_xfer_sock_write_cb, conn);
		purple_circ_buffer_append(conn->tx_buf, buf + ret, len - ret);
	}

	if (ret == -1)
	{
		purple_debug_info("nateon", "%s:%s\n", __FUNCTION__, "socket write error");
	}

	return ret;
}

static ssize_t nateon_xfer_send_data(NateonXfer *nate_xfer, const char *buf, size_t len);

static void
nateon_xfer_send_next(NateonXfer *nate_xfer)
{
	NateonXferConnection *conn;
	gchar *buf;
	int cmd_len;

	conn = &nate_xfer->conn;

	if (nate_xfer->sent_len == purple_xfer_get_size(nate_xfer->prpl_xfer))
	{
		purple_input_remove(conn->tx_handler);
		conn->tx_handler = -1;
		return;
	}
	if (purple_xfer_get_size(nate_xfer->prpl_xfer)-nate_xfer->sent_len > NATEON_XFER_SEND_BUFFER_SIZE)
	{
		nate_xfer->chunk_len = NATEON_XFER_SEND_BUFFER_SIZE;
	}
	else
	{
		nate_xfer->chunk_len = purple_xfer_get_size(nate_xfer->prpl_xfer) - nate_xfer->sent_len;
	}

	buf = g_strdup_printf("FILE %d DATA %d\r\n",
			nate_xfer->send_data_trid, nate_xfer->chunk_len);
	cmd_len = strlen(buf);
	buf = g_realloc(buf, cmd_len + nate_xfer->chunk_len);
	fread(buf + cmd_len, 1, nate_xfer->chunk_len, nate_xfer->local_fp);
	nateon_xfer_send_data(nate_xfer, buf, cmd_len + nate_xfer->chunk_len);
	g_free(buf);
	nate_xfer->send_data_trid++;
}

static void
nateon_xfer_send_data_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	NateonXfer *nate_xfer = data;
	NateonXferConnection *conn = &nate_xfer->conn;
	int ret, writelen;

	writelen = purple_circ_buffer_get_max_read(conn->tx_buf);

	if (writelen == 0)
	{
		purple_input_remove(conn->tx_handler);
		conn->tx_handler = -1;
		nate_xfer->sent_len += nate_xfer->chunk_len;
		nateon_xfer_send_next(nate_xfer);
		purple_xfer_set_bytes_sent(nate_xfer->prpl_xfer, nate_xfer->sent_len);
		purple_xfer_update_progress(nate_xfer->prpl_xfer);
		return;
	}

	ret = write(conn->fd, conn->tx_buf->outptr, writelen);

	if (ret < 0 && errno == EAGAIN)
		return;
	else if (ret <= 0) {
		//nateon_servconn_got_error(servconn, NATEON_SERVCONN_ERROR_WRITE);
		purple_debug_info("nateon", "%s:%s\n", __FUNCTION__, "socket write error");
		purple_input_remove(conn->tx_handler);
		conn->tx_handler = -1;
		purple_xfer_cancel_remote(nate_xfer->prpl_xfer);
		return;
	}

	purple_circ_buffer_mark_read(conn->tx_buf, ret);
}

static ssize_t
nateon_xfer_send_data(NateonXfer *nate_xfer, const char *buf, size_t len)
{
	ssize_t ret = 0;
	NateonXferConnection *conn;

	conn = &nate_xfer->conn;
	if (conn->tx_handler == -1)
	{
		ret = write(conn->fd, buf, len);
	}
	else
	{
		ret = -1;
		errno = EAGAIN;
	}

	if (ret < 0 && errno == EAGAIN)
		ret = 0;
	if (ret < len)
	{
		if (conn->tx_handler == -1)
		{
			conn->tx_handler = purple_input_add( conn->fd, PURPLE_INPUT_WRITE,
					nateon_xfer_send_data_cb, nate_xfer);
		}
		purple_circ_buffer_append(conn->tx_buf, buf + ret, len - ret);
	} else if (ret == len)
	{
		nate_xfer->sent_len += nate_xfer->chunk_len;
		purple_xfer_set_bytes_sent(nate_xfer->prpl_xfer, nate_xfer->sent_len);
		purple_xfer_update_progress(nate_xfer->prpl_xfer);
		nateon_xfer_send_next(nate_xfer);
		return ret;
	}

	if (ret == -1)
	{
		purple_debug_info("nateon", "%s:%s\n", __FUNCTION__, "socket write error");
		purple_xfer_cancel_remote(nate_xfer->prpl_xfer);
	}

	return ret;

}

static NateonXfer*
nateon_xfer_find_by_p2pcookie(NateonSession *session, char *cookie)
{
	GList *l;

	for (l = session->xfers; l != NULL; l = l->next)
	{
		NateonXfer *xfer;

		xfer = l->data;

		if (xfer->p2p_cookie && !strcmp(xfer->p2p_cookie, cookie)) {
			return xfer;
		}
	}
	return NULL;
}

static void
p2p_accept_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	NateonXfer *nate_xfer = data;
	int fd;

	purple_input_remove(nate_xfer->p2p_listen_pa);
	nate_xfer->p2p_listen_pa = -1;

	if (nate_xfer->conntype != NATEON_XFER_CONN_NONE)
	{
		/* already connected p2p session exists. drop this connection */
		purple_debug_info("nateon", "%s:drop duplicate connection\n", __FUNCTION__);
		close(source);
		return;
	}

	fd = accept(source, NULL, NULL);
	if (fd < 0)
	{
		/* accept failed. wait for outgoing p2p or fr-conn to succeed */
		purple_debug_info("nateon", "%s:accept error\n", __FUNCTION__);
		close(source);
		return;
	}

	purple_debug_info("nateon", "%s:using incoming connection for p2p\n", __FUNCTION__);
	nate_xfer->conntype = NATEON_XFER_CONN_P2P;
	nate_xfer->conn.fd = fd;

	close(source);

	purple_xfer_start(nate_xfer->prpl_xfer, -1, NULL, 0);
	nate_xfer->conn.rx_pa = purple_input_add(fd, PURPLE_INPUT_READ, \
			nateon_xfer_sock_read_cb, nate_xfer);

	/* cancel other connecting processes */
	if (nate_xfer->p2p_connect_data) {
		purple_proxy_connect_cancel(nate_xfer->p2p_connect_data);
		nate_xfer->p2p_connect_data = NULL;
	}
	if (nate_xfer->p2p_timer) {
		purple_timeout_remove(nate_xfer->p2p_timer);
		nate_xfer->p2p_timer = 0;
	}
}

static void
p2p_listen_cb(int listenfd, gpointer data)
{
	NateonXfer *nate_xfer = data;

	nate_xfer->p2p_listen_data = NULL;

	if (listenfd == -1) {
		purple_debug_info("nateon", 
				"%s:Could not create listen socket. Wait for other connections.\n",
				__FUNCTION__);
		return;
	}

	nate_xfer->p2p_listen_port = purple_network_get_port_from_fd(listenfd);
	nate_xfer->my_ip = g_strdup(purple_network_get_my_ip(listenfd));
	nate_xfer->p2p_listen_fd = listenfd;
	nate_xfer->p2p_listen_pa = purple_input_add(listenfd, PURPLE_INPUT_READ,
			p2p_accept_cb, nate_xfer);

	if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_RECEIVE)
	{
		char *payload;
		size_t payload_len;
		NateonTransaction *trans;

		/* Send REQC packet to file sender */
		nate_xfer->p2p_cookie = generate_p2p_cookie(nate_xfer->session);
		payload = g_strdup_printf("REQC NEW %s:%d %s\r\n", 
						nate_xfer->my_ip, nate_xfer->p2p_listen_port, nate_xfer->p2p_cookie);
		payload_len = strlen(payload);

		trans = nateon_transaction_new(nate_xfer->session->notification->cmdproc, "CTOC", \
				"%s N %d", purple_xfer_get_remote_user(nate_xfer->prpl_xfer), payload_len);

		nateon_transaction_set_payload(trans, payload, payload_len);

		g_free(payload);

		nateon_cmdproc_send_trans(nate_xfer->session->notification->cmdproc, trans);
	}
	else
	{
		char *payload;
		size_t payload_len;
		NateonTransaction *trans;

		/* Send REQC packet to file sender */
		payload = g_strdup_printf("REQC RES %s:%d %s\r\n", 
						nate_xfer->my_ip, nate_xfer->p2p_listen_port, nate_xfer->p2p_cookie);
		payload_len = strlen(payload);

		trans = nateon_transaction_new(nate_xfer->session->notification->cmdproc, "CTOC", \
				"%s N %d", purple_xfer_get_remote_user(nate_xfer->prpl_xfer), payload_len);

		nateon_transaction_set_payload(trans, payload, payload_len);

		g_free(payload);

		nateon_cmdproc_send_trans(nate_xfer->session->notification->cmdproc, trans);
	}
}

static gboolean
p2p_timeout_cb(gpointer data)
{
	NateonXfer *xfer = data;

	if (xfer->conntype == NATEON_XFER_CONN_NONE)
	{
		NateonTransaction *trans;
		purple_debug_info("nateon", "p2p connection timed out. Connecting to FR server\n");

		/* Send REFR to initiate FR-mode xfer */
		trans = nateon_transaction_new(xfer->session->notification->cmdproc, "REFR", "%s", \
									xfer->who);
		nateon_cmdproc_send_trans(xfer->session->notification->cmdproc, trans);
		xfer->fr_initiate_trid = trans->trId;
	}

	return FALSE;
}

static void
p2p_connect_cb(gpointer data, gint source, const char *error_message)
{
	NateonXfer *nate_xfer = data;

	gchar *buf;

	nate_xfer->p2p_connect_data = NULL;

	if (source < 0)
	{
		/* Simply wait for FR to begin */
		purple_debug_info("nateon", "%s:Connect failed. err: [%s]\n", __FUNCTION__, error_message);
		return;
	}

	if (nate_xfer->conntype != NATEON_XFER_CONN_NONE)
	{
		/* already connected session exists. drop this connection */
		purple_debug_info("nateon", "%s:drop duplicate connection\n", __FUNCTION__);
		close(source);
		return;
	}

	purple_debug_info("nateon", "%s:using outgoing connection for p2p\n", __FUNCTION__);
	nate_xfer->conntype = NATEON_XFER_CONN_P2P;
	nate_xfer->conn.fd = source;

	/* register watcher for read callback */
	purple_xfer_start(nate_xfer->prpl_xfer, -1, NULL, 0);
	nate_xfer->conn.rx_pa = purple_input_add(nate_xfer->conn.fd, PURPLE_INPUT_READ, \
			nateon_xfer_sock_read_cb, nate_xfer);

	buf = g_strdup_printf("ATHC 0 %s %s %s 6004 0\r\n",
			nate_xfer->session->user->account_name,
			nate_xfer->who,
			nate_xfer->p2p_cookie);
	nateon_xfer_sock_write(&nate_xfer->conn, buf, strlen(buf));
	g_free(buf);

	/* cancel other connecting processes */
	if (nate_xfer->p2p_listen_data)
	{
		purple_network_listen_cancel(nate_xfer->p2p_listen_data);
		nate_xfer->p2p_listen_data = NULL;
	}
	if (nate_xfer->p2p_listen_pa)
	{
		purple_input_remove(nate_xfer->p2p_listen_pa);
		nate_xfer->p2p_listen_pa = -1;
	}
	if (nate_xfer->p2p_listen_fd)
	{
		close(nate_xfer->p2p_listen_fd);
		nate_xfer->p2p_listen_fd = 0;
	}
	if (nate_xfer->p2p_timer) {
		purple_timeout_remove(nate_xfer->p2p_timer);
		nate_xfer->p2p_timer = 0;
	}
	return;
}

static void
fr_connect_cb(gpointer data, gint source, const char *error_message)
{
	NateonXfer *nate_xfer = data;
	gchar *buf;

	nate_xfer->fr_connect_data = NULL;

	purple_debug_info("nateon", "%s\n", __FUNCTION__);
	if (source < 0)
	{
		/* FIXME: Handle error. This was our last try.. */
		purple_debug_info("nateon", "%s:Connect failed. err: [%s]\n", __FUNCTION__, error_message);
		return;
	}

	if (nate_xfer->conntype != NATEON_XFER_CONN_NONE)
	{
		/* already connected p2p session exists. drop this connection */
		purple_debug_info("nateon", "%s:drop duplicate connection\n", __FUNCTION__);
		close(source);
		return;
	}

	nate_xfer->conntype = NATEON_XFER_CONN_FR;

	/* cancel p2p connection tries */
	if (nate_xfer->p2p_connect_data)
	{
		purple_proxy_connect_cancel(nate_xfer->p2p_connect_data);
		nate_xfer->p2p_connect_data = NULL;
	}
	if (nate_xfer->p2p_listen_data)
	{
		purple_network_listen_cancel(nate_xfer->p2p_listen_data);
		nate_xfer->p2p_listen_data = NULL;
	}
	if (nate_xfer->p2p_listen_pa > 0)
	{
		purple_input_remove(nate_xfer->p2p_listen_pa);
		nate_xfer->p2p_listen_pa = -1;
	}
	if (nate_xfer->p2p_listen_fd)
	{
		close(nate_xfer->p2p_listen_fd);
		nate_xfer->p2p_listen_fd = 0;
	}

	purple_debug_info("nateon", "%s:using fr connection for file transfer\n", __FUNCTION__);
	nate_xfer->conn.fd = source;

	/* register watcher for read callback */
	nate_xfer->conn.rx_pa = purple_input_add(nate_xfer->conn.fd, PURPLE_INPUT_READ, \
			nateon_xfer_sock_read_cb, nate_xfer);
	purple_xfer_start(nate_xfer->prpl_xfer, -1, NULL, 0);

	if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_RECEIVE)
	{
		buf = g_strdup_printf("FRIN 0 %s %s\r\n", nate_xfer->session->user->account_name, 
				nate_xfer->fr_authkey);
		nateon_xfer_sock_write(&nate_xfer->conn, buf, strlen(buf));
		g_free(buf);
	}
	else if (purple_xfer_get_type(nate_xfer->prpl_xfer) == PURPLE_XFER_SEND)
	{
		buf = g_strdup_printf("ATHC 0 %s %s %s %s 6004 0\r\n", 
				nate_xfer->session->user->account_name, 
				nate_xfer->who,
				nate_xfer->p2p_cookie, nate_xfer->fr_authkey);
		nateon_xfer_sock_write(&nate_xfer->conn, buf, strlen(buf));
		g_free(buf);
	}
}

static void
nateon_xfer_init(PurpleXfer *xfer)
{
	NateonXfer *nate_xfer;

	purple_debug_info("nateon", "%s\n", __FUNCTION__);

	nate_xfer = xfer->data;

	nate_xfer->conntype = NATEON_XFER_CONN_NONE;

	if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE)
	{
		if (nate_xfer->content_type == NATEON_XFER_CONTENT_FILE)
		{
			nate_xfer->dest_fp = fopen(purple_xfer_get_local_filename(xfer), "wb");
		}
		else if (nate_xfer->content_type == NATEON_XFER_CONTENT_BUDDYIMG)
		{
			char *filepath;
			nate_xfer->dest_fp = purple_mkstemp(&filepath, TRUE);
			purple_xfer_set_local_filename(xfer, filepath);
			g_free(filepath);
		}

		if (nate_xfer->dest_fp == NULL) {
			purple_debug_info("nateon", "%s: Error Writing File %s\n", __FUNCTION__,
					purple_xfer_get_local_filename(xfer));
			purple_xfer_cancel_local(xfer);
			return;
		}

		nate_xfer->p2p_listen_data = purple_network_listen_range(NATEON_P2P_START_PORT,\
				NATEON_P2P_END_PORT, SOCK_STREAM, p2p_listen_cb, nate_xfer);

		/* Start timer for FR-mode connection */
		purple_debug_info("nateon", "starting timer for fr_connect\n");
		nate_xfer->p2p_timer = purple_timeout_add(NATEON_P2P_TIMEOUT_SECONDS*1000, \
				p2p_timeout_cb, nate_xfer);
	}
	else if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND)
	{
		NateonSwitchBoard *swboard;
		NateonTransaction *trans;
		gchar *new_filename;

		swboard = nate_xfer->swboard;

		nate_xfer->local_fp = fopen(purple_xfer_get_local_filename(nate_xfer->prpl_xfer),
				"rb");
		if (!(nate_xfer->local_fp))
		{
			purple_debug_error("nateon", "%s: can't open file\n", __FUNCTION__);
			return;
		}
		fseek(nate_xfer->local_fp, 0, SEEK_SET);

		nate_xfer->file_cookie = g_strdup_printf("%d:%s:%d",
				g_random_int_range(100,999), nate_xfer->session->user->id,
				g_random_int_range(100,999));

		new_filename = purple_strreplace(purple_xfer_get_filename(nate_xfer->prpl_xfer),
											" ", "%20");
		trans = nateon_transaction_new(swboard->cmdproc, "WHSP", \
				"%s FILE REQUEST%%09%d%%09%s|%d|%s",
				nate_xfer->who, 1, new_filename,
				purple_xfer_get_size(nate_xfer->prpl_xfer),
				nate_xfer->file_cookie);
		g_free(new_filename);

		if (swboard->ready)
		{
			purple_debug_info("nateon", "[%s] send_trans\n", __FUNCTION__);
			nateon_cmdproc_send_trans(swboard->cmdproc, trans);
		}
		else
		{
			purple_debug_info("nateon", "[%s] queue_trans\n", __FUNCTION__);
			nateon_cmdproc_queue_trans(swboard->cmdproc, trans);
		}
	}
}

static void
nateon_xfer_end(PurpleXfer *xfer)
{
	NateonXfer *nate_xfer;
	NateonSession *session;

	purple_debug_info("nateon", "[%s]\n", __FUNCTION__); 

	nate_xfer = xfer->data;

	if (nate_xfer) {
		session = nate_xfer->session;
		nate_xfer->session = NULL;
		if (nate_xfer->who)
			g_free(nate_xfer->who);
		if (nate_xfer->my_ip)
			g_free(nate_xfer->my_ip);
		if (nate_xfer->file_cookie)
			g_free(nate_xfer->file_cookie);

		if (nate_xfer->p2p_cookie)
			g_free(nate_xfer->p2p_cookie);
		if (nate_xfer->p2p_listen_data)
			purple_network_listen_cancel(nate_xfer->p2p_listen_data);
		if (nate_xfer->p2p_connect_data)
			purple_proxy_connect_cancel(nate_xfer->p2p_connect_data);
		if (nate_xfer->p2p_listen_pa > 0)
			purple_input_remove(nate_xfer->p2p_listen_pa);
		if (nate_xfer->p2p_timer)
			purple_timeout_remove(nate_xfer->p2p_timer);

		if (nate_xfer->fr_ip)
			g_free(nate_xfer->fr_ip);
		if (nate_xfer->fr_authkey)
			g_free(nate_xfer->fr_authkey);
		if (nate_xfer->fr_connect_data)
			purple_proxy_connect_cancel(nate_xfer->fr_connect_data);

		if (nate_xfer->conn.rx_pa > 0)
			purple_input_remove(nate_xfer->conn.rx_pa);
		if (nate_xfer->conn.rx_buf)
			g_free(nate_xfer->conn.rx_buf);
		if (nate_xfer->conn.tx_buf)
			purple_circ_buffer_destroy(nate_xfer->conn.tx_buf);
		if (nate_xfer->conn.tx_handler > 0)
			purple_input_remove(nate_xfer->conn.tx_handler);

		if (nate_xfer->conn.fd)
			close(nate_xfer->conn.fd);
		if (nate_xfer->local_fp)
			fclose(nate_xfer->local_fp);

		session->xfers = g_list_remove(session->xfers, nate_xfer);
		g_free(nate_xfer);
	}
	xfer->data = NULL;
	return;
}

static NateonXfer*
nateon_xfer_new(NateonSession *session, PurpleXferType type, const char *who)
{
	NateonXfer *xfer;

	xfer = g_new0(NateonXfer, 1);

	xfer->session = session;

	xfer->prpl_xfer = purple_xfer_new(session->account, type, who);

	xfer->prpl_xfer->data = xfer;

	xfer->who = g_strdup(who);

	session->xfers = g_list_append(session->xfers, xfer);

	xfer->conn.tx_handler = -1;
	xfer->conn.tx_buf = purple_circ_buffer_new(0);
	xfer->conn.nate_xfer = xfer;
	xfer->p2p_listen_pa = -1;

	return xfer;
}

void
nateon_xfer_parse_refr(NateonSession *session, char **params, int param_count)
{
	NateonXfer *xfer = NULL;
	GList *l;

	if (param_count == 4) {
		/* find xfer by fr trid */
		xfer = NULL;
		for (l = session->xfers; l != NULL; l = l->next)
		{
			NateonXfer *t_xfer;
			t_xfer = l->data;
			if (t_xfer->fr_initiate_trid == atoi(params[0])) {
				xfer = t_xfer;
				break;
			}
		}
		if (!xfer || xfer->conntype != NATEON_XFER_CONN_NONE)
		{
			return;
		}

		xfer->fr_ip = g_strdup(params[1]);
		xfer->fr_port = atoi(params[2]);
		xfer->fr_authkey = g_strdup(params[3]);

		xfer->fr_connect_data = purple_proxy_connect(NULL, session->account, 
				xfer->fr_ip, xfer->fr_port, fr_connect_cb, xfer);
	}
}

void
nateon_xfer_parse_reqc(NateonSession *session, NateonCmdProc *cmdproc, char **params, int param_count)
{
	NateonXfer *xfer = NULL;

	if (param_count >= 3 && !strncmp(params[0], "RES", 3)) {
		char **split;

		xfer = nateon_xfer_find_by_p2pcookie(session, params[2]);
		if (!xfer) {
			purple_debug_info("nateon", "no matching p2pcookie found for this xfer\n");
			return;
		}

		split = g_strsplit(params[1], ":", 2);
		xfer->p2p_connect_data = purple_proxy_connect(NULL, session->account, split[0], 
				atoi(split[1]), p2p_connect_cb, xfer);
		g_strfreev(split);

	}
	else if (param_count >= 3 && !strncmp(params[0], "NEW", 3))
	{
		char **split;
		char *peer_name;
		GList *l;
		NateonTransaction *trans;

		if (cmdproc->last_cmd && !strcmp(cmdproc->last_cmd->command, "CTOC"))
		{
			peer_name = cmdproc->last_cmd->params[1];
		}
		else
		{
			return;
		}

		for (l = session->xfers; l; l = l->next)
		{
			NateonXfer *tmp_xfer;
			tmp_xfer = l->data;
			if (purple_xfer_get_type(tmp_xfer->prpl_xfer) == PURPLE_XFER_SEND &&
					!strcmp(tmp_xfer->who, peer_name) )
			{
				xfer = tmp_xfer;
				break;
			}
		}
		if (!xfer) {
			purple_debug_info("nateon", "no matching peer name for this xfer\n");
			return;
		}

		/* reply with "PNAK" */
		trans = nateon_transaction_new(xfer->session->notification->cmdproc, "PNAK", "");
		nateon_cmdproc_send_trans(xfer->session->notification->cmdproc, trans);

		/* Open Listening P2P Port */
		xfer->p2p_listen_data = purple_network_listen_range(NATEON_P2P_START_PORT,\
				NATEON_P2P_END_PORT, SOCK_STREAM, p2p_listen_cb, xfer);

		xfer->p2p_cookie = g_strdup(params[2]);
		split = g_strsplit(params[1], ":", 2);
		xfer->p2p_connect_data = purple_proxy_connect(NULL, session->account, split[0], 
				atoi(split[1]), p2p_connect_cb, xfer);
		g_strfreev(split);
	}
	else if (param_count >= 4 && !strncmp(params[0], "FR", 2))
	{
		char **split;

		xfer = nateon_xfer_find_by_p2pcookie(session, params[2]);
		if (!xfer || xfer->conntype != NATEON_XFER_CONN_NONE)
		{
			return;
		}
		split = g_strsplit(params[1], ":", 2);
		xfer->fr_ip = g_strdup(split[0]);
		xfer->fr_port = atoi(split[1]);
		xfer->fr_authkey = g_strdup(params[3]);

		xfer->fr_connect_data = purple_proxy_connect(NULL, session->account, 
				xfer->fr_ip, xfer->fr_port, fr_connect_cb, xfer);

		g_strfreev(split);
	}
}

void
nateon_xfer_receive_file(NateonSession *session, NateonSwitchBoard *swboard, \
		const char *who, const char *filename, const int filesize, const char *cookie)
{
	NateonXfer *xfer;

	xfer = nateon_xfer_new(session, PURPLE_XFER_RECEIVE, who);

	xfer->content_type = NATEON_XFER_CONTENT_FILE;
	xfer->file_cookie = g_strdup(cookie);
	xfer->swboard = swboard;
	purple_xfer_set_filename(xfer->prpl_xfer, filename);
	purple_xfer_set_size(xfer->prpl_xfer, filesize);

	purple_xfer_set_init_fnc(xfer->prpl_xfer, nateon_xfer_init);
	purple_xfer_set_cancel_recv_fnc(xfer->prpl_xfer, nateon_xfer_cancel_recv);
	purple_xfer_set_request_denied_fnc(xfer->prpl_xfer, nateon_xfer_request_denied);
	purple_xfer_set_end_fnc(xfer->prpl_xfer, nateon_xfer_end);

	purple_xfer_request(xfer->prpl_xfer);
}

void
nateon_xfer_send_file(NateonSession *session, const char *who, const char *filename)
{
	NateonXfer *xfer;

	purple_debug_info("nateon", "%s: who:%s file:%s\n", __FUNCTION__, who, filename);

	xfer = nateon_xfer_new(session, PURPLE_XFER_SEND, who);
	xfer->content_type = NATEON_XFER_CONTENT_FILE;
	purple_xfer_set_init_fnc(xfer->prpl_xfer, nateon_xfer_init);
	purple_xfer_set_cancel_recv_fnc(xfer->prpl_xfer, nateon_xfer_cancel_recv);
	purple_xfer_set_request_denied_fnc(xfer->prpl_xfer, nateon_xfer_request_denied);
	purple_xfer_set_end_fnc(xfer->prpl_xfer, nateon_xfer_end);
	purple_xfer_set_cancel_send_fnc(xfer->prpl_xfer, nateon_xfer_cancel_send);

	xfer->swboard = nateon_session_get_swboard(xfer->session, xfer->who, \
												NATEON_SB_FLAG_FT);

	if (filename)
	{
		purple_xfer_request_accepted(xfer->prpl_xfer, filename);
	} 
	else
	{
		purple_xfer_request(xfer->prpl_xfer);
	}
}

void
nateon_xfer_cancel_transfer(NateonSession *session, const char *who, const char *filename, const char *cookie)
{
	NateonXfer *xfer = NULL;
	GList *l;

	for (l = session->xfers; l; l = l->next)
	{
		NateonXfer *tmp;
		tmp = l->data;
		if ( !strcmp(tmp->who, who) && \
				((filename == NULL) || !strcmp(purple_xfer_get_filename(tmp->prpl_xfer), filename)) && \
				tmp->file_cookie && !strcmp(tmp->file_cookie, cookie) )
		{
			xfer = tmp;
			break;
		}
	}
	if (!xfer)
	{
		purple_debug_info("nateon", "%s: no matching xfer found for deny request\n", __FUNCTION__);
		return;
	}

	purple_xfer_cancel_remote(xfer->prpl_xfer);
}

void
nateon_xfer_receive_buddyimage(NateonSession *session, NateonSwitchBoard *swboard, \
		const char *who, const char *uniq_name, const int filesize, const char *cookie)
{
	NateonXfer *xfer;

	xfer = nateon_xfer_new(session, PURPLE_XFER_RECEIVE, who);

	xfer->file_cookie = g_strdup(cookie);
	xfer->swboard = swboard;

	xfer->content_type = NATEON_XFER_CONTENT_BUDDYIMG;

	purple_xfer_set_filename(xfer->prpl_xfer, uniq_name);
	purple_xfer_set_size(xfer->prpl_xfer, filesize);
	purple_xfer_set_cancel_recv_fnc(xfer->prpl_xfer, nateon_xfer_cancel_recv_dummy);
	//purple_xfer_set_init_fnc(xfer->prpl_xfer, nateon_xfer_init);
	//purple_xfer_set_request_denied_fnc(xfer->prpl_xfer, nateon_xfer_request_denied);
	purple_xfer_set_end_fnc(xfer->prpl_xfer, nateon_xfer_end);

	nateon_xfer_init(xfer->prpl_xfer);
}
