/**
 * @file ft.c File Transfer API
 */

/* purple
 *
 * 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 "dbus-maybe.h"
#include "ft.h"
#include "network.h"
#include "notify.h"
#include "prefs.h"
#include "proxy.h"
#include "request.h"
#include "util.h"
#include "debug.h"

#define FT_INITIAL_BUFFER_SIZE 4096
#define FT_MAX_BUFFER_SIZE     65535

static PurpleXferUiOps *xfer_ui_ops = NULL;
static GList *xfers;

/*
 * A hack to store more data since we can't extend the size of PurpleXfer
 * easily.
 */
static GHashTable *xfers_data = NULL;

typedef struct _PurpleXferPrivData {
	/*
	 * Used to moderate the file transfer when either the read/write ui_ops are
	 * set or fd is not set. In those cases, the UI/prpl call the respective
	 * function, which is somewhat akin to a fd watch being triggered.
	 */
	enum {
		PURPLE_XFER_READY_NONE = 0x0,
		PURPLE_XFER_READY_UI   = 0x1,
		PURPLE_XFER_READY_PRPL = 0x2,
	} ready;
} PurpleXferPrivData;

static int purple_xfer_choose_file(PurpleXfer *xfer);

static void
purple_xfer_priv_data_destroy(gpointer data)
{
	PurpleXferPrivData *priv = data;

	g_free(priv);
}

GList *
purple_xfers_get_all()
{
	return xfers;
}

PurpleXfer *
purple_xfer_new(PurpleAccount *account, PurpleXferType type, const char *who)
{
	PurpleXfer *xfer;
	PurpleXferUiOps *ui_ops;
	PurpleXferPrivData *priv;

	g_return_val_if_fail(type    != PURPLE_XFER_UNKNOWN, NULL);
	g_return_val_if_fail(account != NULL,              NULL);
	g_return_val_if_fail(who     != NULL,              NULL);

	xfer = g_new0(PurpleXfer, 1);
	PURPLE_DBUS_REGISTER_POINTER(xfer, PurpleXfer);

	xfer->ref = 1;
	xfer->type    = type;
	xfer->account = account;
	xfer->who     = g_strdup(who);
	xfer->ui_ops  = purple_xfers_get_ui_ops();
	xfer->message = NULL;
	xfer->current_buffer_size = FT_INITIAL_BUFFER_SIZE;
	xfer->fd = -1;

	priv = g_new0(PurpleXferPrivData, 1);
	priv->ready = PURPLE_XFER_READY_NONE;

	g_hash_table_insert(xfers_data, xfer, priv);

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->new_xfer != NULL)
		ui_ops->new_xfer(xfer);

	xfers = g_list_prepend(xfers, xfer);

	if (purple_debug_is_verbose())
		purple_debug_info("xfer", "new %p [%d]\n", xfer, xfer->ref);

	return xfer;
}

static void
purple_xfer_destroy(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;

	g_return_if_fail(xfer != NULL);

	if (purple_debug_is_verbose())
		purple_debug_info("xfer", "destroyed %p [%d]\n", xfer, xfer->ref);

	/* Close the file browser, if it's open */
	purple_request_close_with_handle(xfer);

	if (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_STARTED)
		purple_xfer_cancel_local(xfer);

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->destroy != NULL)
		ui_ops->destroy(xfer);

	g_free(xfer->who);
	g_free(xfer->filename);
	g_free(xfer->remote_ip);
	g_free(xfer->local_filename);

	g_hash_table_remove(xfers_data, xfer);

	PURPLE_DBUS_UNREGISTER_POINTER(xfer);
	xfers = g_list_remove(xfers, xfer);
	g_free(xfer);
}

void
purple_xfer_ref(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);

	xfer->ref++;

	if (purple_debug_is_verbose())
		purple_debug_info("xfer", "ref'd %p [%d]\n", xfer, xfer->ref);
}

void
purple_xfer_unref(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);
	g_return_if_fail(xfer->ref > 0);

	xfer->ref--;

	if (purple_debug_is_verbose())
		purple_debug_info("xfer", "unref'd %p [%d]\n", xfer, xfer->ref);

	if (xfer->ref == 0)
		purple_xfer_destroy(xfer);
}

static void
purple_xfer_set_status(PurpleXfer *xfer, PurpleXferStatusType status)
{
	g_return_if_fail(xfer != NULL);

	if (xfer->status == status)
		return;

	xfer->status = status;

	if(xfer->type == PURPLE_XFER_SEND) {
		switch(status) {
			case PURPLE_XFER_STATUS_ACCEPTED:
				purple_signal_emit(purple_xfers_get_handle(), "file-send-accept", xfer);
				break;
			case PURPLE_XFER_STATUS_STARTED:
				purple_signal_emit(purple_xfers_get_handle(), "file-send-start", xfer);
				break;
			case PURPLE_XFER_STATUS_DONE:
				purple_signal_emit(purple_xfers_get_handle(), "file-send-complete", xfer);
				break;
			case PURPLE_XFER_STATUS_CANCEL_LOCAL:
			case PURPLE_XFER_STATUS_CANCEL_REMOTE:
				purple_signal_emit(purple_xfers_get_handle(), "file-send-cancel", xfer);
				break;
			default:
				break;
		}
	} else if(xfer->type == PURPLE_XFER_RECEIVE) {
		switch(status) {
			case PURPLE_XFER_STATUS_ACCEPTED:
				purple_signal_emit(purple_xfers_get_handle(), "file-recv-accept", xfer);
				break;
			case PURPLE_XFER_STATUS_STARTED:
				purple_signal_emit(purple_xfers_get_handle(), "file-recv-start", xfer);
				break;
			case PURPLE_XFER_STATUS_DONE:
				purple_signal_emit(purple_xfers_get_handle(), "file-recv-complete", xfer);
				break;
			case PURPLE_XFER_STATUS_CANCEL_LOCAL:
			case PURPLE_XFER_STATUS_CANCEL_REMOTE:
				purple_signal_emit(purple_xfers_get_handle(), "file-recv-cancel", xfer);
				break;
			default:
				break;
		}
	}
}

void purple_xfer_conversation_write(PurpleXfer *xfer, char *message, gboolean is_error)
{
	PurpleConversation *conv = NULL;
	PurpleMessageFlags flags = PURPLE_MESSAGE_SYSTEM;
	char *escaped;

	g_return_if_fail(xfer != NULL);
	g_return_if_fail(message != NULL);

	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, xfer->who,
											   purple_xfer_get_account(xfer));

	if (conv == NULL)
		return;

	escaped = g_markup_escape_text(message, -1);

	if (is_error)
		flags |= PURPLE_MESSAGE_ERROR;

	purple_conversation_write(conv, NULL, escaped, flags, time(NULL));
	g_free(escaped);
}

static void purple_xfer_show_file_error(PurpleXfer *xfer, const char *filename)
{
	int err = errno;
	gchar *msg = NULL, *utf8;
	PurpleXferType xfer_type = purple_xfer_get_type(xfer);
	PurpleAccount *account = purple_xfer_get_account(xfer);

	utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
	switch(xfer_type) {
		case PURPLE_XFER_SEND:
			msg = g_strdup_printf(_("Error reading %s: \n%s.\n"),
								  utf8, g_strerror(err));
			break;
		case PURPLE_XFER_RECEIVE:
			msg = g_strdup_printf(_("Error writing %s: \n%s.\n"),
								  utf8, g_strerror(err));
			break;
		default:
			msg = g_strdup_printf(_("Error accessing %s: \n%s.\n"),
								  utf8, g_strerror(err));
			break;
	}
	g_free(utf8);

	purple_xfer_conversation_write(xfer, msg, TRUE);
	purple_xfer_error(xfer_type, account, xfer->who, msg);
	g_free(msg);
}

static void
purple_xfer_choose_file_ok_cb(void *user_data, const char *filename)
{
	PurpleXfer *xfer;
	PurpleXferType type;
	struct stat st;
	gchar *dir;

	xfer = (PurpleXfer *)user_data;
	type = purple_xfer_get_type(xfer);

	if (g_stat(filename, &st) != 0) {
		/* File not found. */
		if (type == PURPLE_XFER_RECEIVE) {
#ifndef _WIN32
			int mode = W_OK;
#else
			int mode = F_OK;
#endif
			dir = g_path_get_dirname(filename);

			if (g_access(dir, mode) == 0) {
				purple_xfer_request_accepted(xfer, filename);
			} else {
				purple_xfer_ref(xfer);
				purple_notify_message(
					NULL, PURPLE_NOTIFY_MSG_ERROR, NULL,
					_("Directory is not writable."), NULL,
					(PurpleNotifyCloseCallback)purple_xfer_choose_file, xfer);
			}

			g_free(dir);
		}
		else {
			purple_xfer_show_file_error(xfer, filename);
			purple_xfer_cancel_local(xfer);
		}
	}
	else if ((type == PURPLE_XFER_SEND) && (st.st_size == 0)) {

		purple_notify_error(NULL, NULL,
						  _("Cannot send a file of 0 bytes."), NULL);

		purple_xfer_cancel_local(xfer);
	}
	else if ((type == PURPLE_XFER_SEND) && S_ISDIR(st.st_mode)) {
		/*
		 * XXX - Sending a directory should be valid for some protocols.
		 */
		purple_notify_error(NULL, NULL,
						  _("Cannot send a directory."), NULL);

		purple_xfer_cancel_local(xfer);
	}
	else if ((type == PURPLE_XFER_RECEIVE) && S_ISDIR(st.st_mode)) {
		char *msg, *utf8;
		utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
		msg = g_strdup_printf(
					_("%s is not a regular file. Cowardly refusing to overwrite it.\n"), utf8);
		g_free(utf8);
		purple_notify_error(NULL, NULL, msg, NULL);
		g_free(msg);
		purple_xfer_request_denied(xfer);
	}
	else if (type == PURPLE_XFER_SEND) {
#ifndef _WIN32
		int mode = R_OK;
#else
		int mode = F_OK;
#endif

		if (g_access(filename, mode) == 0) {
			purple_xfer_request_accepted(xfer, filename);
		} else {
			purple_xfer_ref(xfer);
			purple_notify_message(
				NULL, PURPLE_NOTIFY_MSG_ERROR, NULL,
				_("File is not readable."), NULL,
				(PurpleNotifyCloseCallback)purple_xfer_choose_file, xfer);
		}
	}
	else {
		purple_xfer_request_accepted(xfer, filename);
	}

	purple_xfer_unref(xfer);
}

static void
purple_xfer_choose_file_cancel_cb(void *user_data, const char *filename)
{
	PurpleXfer *xfer = (PurpleXfer *)user_data;

	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_CANCEL_LOCAL);
	if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND)
		purple_xfer_cancel_local(xfer);
	else
		purple_xfer_request_denied(xfer);
	purple_xfer_unref(xfer);
}

static int
purple_xfer_choose_file(PurpleXfer *xfer)
{
	purple_request_file(xfer, NULL, purple_xfer_get_filename(xfer),
					  (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE),
					  G_CALLBACK(purple_xfer_choose_file_ok_cb),
					  G_CALLBACK(purple_xfer_choose_file_cancel_cb),
					  purple_xfer_get_account(xfer), xfer->who, NULL,
					  xfer);

	return 0;
}

static int
cancel_recv_cb(PurpleXfer *xfer)
{
	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_CANCEL_LOCAL);
	purple_xfer_request_denied(xfer);
	purple_xfer_unref(xfer);

	return 0;
}

static void
purple_xfer_ask_recv(PurpleXfer *xfer)
{
	char *buf, *size_buf;
	size_t size;

	/* If we have already accepted the request, ask the destination file
	   name directly */
	if (purple_xfer_get_status(xfer) != PURPLE_XFER_STATUS_ACCEPTED) {
		PurpleBuddy *buddy = purple_find_buddy(xfer->account, xfer->who);

		if (purple_xfer_get_filename(xfer) != NULL)
		{
			size = purple_xfer_get_size(xfer);
			size_buf = purple_str_size_to_units(size);
			buf = g_strdup_printf(_("%s wants to send you %s (%s)"),
						  buddy ? purple_buddy_get_alias(buddy) : xfer->who,
						  purple_xfer_get_filename(xfer), size_buf);
			g_free(size_buf);
		}
		else
		{
			buf = g_strdup_printf(_("%s wants to send you a file"),
						buddy ? purple_buddy_get_alias(buddy) : xfer->who);
		}

		if (xfer->message != NULL)
			serv_got_im(purple_account_get_connection(xfer->account),
								 xfer->who, xfer->message, 0, time(NULL));

		purple_request_accept_cancel(xfer, NULL, buf, NULL,
								  PURPLE_DEFAULT_ACTION_NONE,
								  xfer->account, xfer->who, NULL,
								  xfer,
								  G_CALLBACK(purple_xfer_choose_file),
								  G_CALLBACK(cancel_recv_cb));

		g_free(buf);
	} else
		purple_xfer_choose_file(xfer);
}

static int
ask_accept_ok(PurpleXfer *xfer)
{
	purple_xfer_request_accepted(xfer, NULL);

	return 0;
}

static int
ask_accept_cancel(PurpleXfer *xfer)
{
	purple_xfer_request_denied(xfer);
	purple_xfer_unref(xfer);

	return 0;
}

static void
purple_xfer_ask_accept(PurpleXfer *xfer)
{
	char *buf, *buf2 = NULL;
	PurpleBuddy *buddy = purple_find_buddy(xfer->account, xfer->who);

	buf = g_strdup_printf(_("Accept file transfer request from %s?"),
				  buddy ? purple_buddy_get_alias(buddy) : xfer->who);
	if (purple_xfer_get_remote_ip(xfer) &&
		purple_xfer_get_remote_port(xfer))
		buf2 = g_strdup_printf(_("A file is available for download from:\n"
					 "Remote host: %s\nRemote port: %d"),
					   purple_xfer_get_remote_ip(xfer),
					   purple_xfer_get_remote_port(xfer));
	purple_request_accept_cancel(xfer, NULL, buf, buf2,
							   PURPLE_DEFAULT_ACTION_NONE,
							   xfer->account, xfer->who, NULL,
							   xfer,
							   G_CALLBACK(ask_accept_ok),
							   G_CALLBACK(ask_accept_cancel));
	g_free(buf);
	g_free(buf2);
}

void
purple_xfer_request(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);
	g_return_if_fail(xfer->ops.init != NULL);

	purple_xfer_ref(xfer);

	if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE)
	{
		purple_signal_emit(purple_xfers_get_handle(), "file-recv-request", xfer);
		if (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_CANCEL_LOCAL)
		{
			/* The file-transfer was cancelled by a plugin */
			purple_xfer_cancel_local(xfer);
		}
		else if (purple_xfer_get_filename(xfer) ||
		           purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_ACCEPTED)
		{
			gchar* message = NULL;
			PurpleBuddy *buddy = purple_find_buddy(xfer->account, xfer->who);
			message = g_strdup_printf(_("%s is offering to send file %s"),
				buddy ? purple_buddy_get_alias(buddy) : xfer->who, purple_xfer_get_filename(xfer));
			purple_xfer_conversation_write(xfer, message, FALSE);
			g_free(message);
			/* Ask for a filename to save to if it's not already given by a plugin */
			if (xfer->local_filename == NULL)
				purple_xfer_ask_recv(xfer);
		}
		else
		{
			purple_xfer_ask_accept(xfer);
		}
	}
	else
	{
		purple_xfer_choose_file(xfer);
	}
}

void
purple_xfer_request_accepted(PurpleXfer *xfer, const char *filename)
{
	PurpleXferType type;
	struct stat st;
	char *msg, *utf8, *base;
	PurpleAccount *account;
	PurpleBuddy *buddy;

	if (xfer == NULL)
		return;

	type = purple_xfer_get_type(xfer);
	account = purple_xfer_get_account(xfer);

	if (!filename && type == PURPLE_XFER_RECEIVE) {
		xfer->status = PURPLE_XFER_STATUS_ACCEPTED;
		xfer->ops.init(xfer);
		return;
	}

	buddy = purple_find_buddy(account, xfer->who);

	if (type == PURPLE_XFER_SEND) {
		/* Sending a file */
		/* Check the filename. */
		PurpleXferUiOps *ui_ops;
		ui_ops = purple_xfer_get_ui_ops(xfer);

#ifdef _WIN32
		if (g_strrstr(filename, "../") || g_strrstr(filename, "..\\"))
#else
		if (g_strrstr(filename, "../"))
#endif
		{
			utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);

			msg = g_strdup_printf(_("%s is not a valid filename.\n"), utf8);
			purple_xfer_error(type, account, xfer->who, msg);
			g_free(utf8);
			g_free(msg);

			purple_xfer_unref(xfer);
			return;
		}

		if (ui_ops == NULL || (ui_ops->ui_read == NULL && ui_ops->ui_write == NULL)) {
			if (g_stat(filename, &st) == -1) {
				purple_xfer_show_file_error(xfer, filename);
				purple_xfer_unref(xfer);
				return;
			}

			purple_xfer_set_local_filename(xfer, filename);
			purple_xfer_set_size(xfer, st.st_size);
		} else {
			utf8 = g_strdup(filename);
			purple_xfer_set_local_filename(xfer, filename);
		}

		base = g_path_get_basename(filename);
		utf8 = g_filename_to_utf8(base, -1, NULL, NULL, NULL);
		g_free(base);
		purple_xfer_set_filename(xfer, utf8);

		msg = g_strdup_printf(_("Offering to send %s to %s"),
				utf8, buddy ? purple_buddy_get_alias(buddy) : xfer->who);
		g_free(utf8);
		purple_xfer_conversation_write(xfer, msg, FALSE);
		g_free(msg);
	}
	else {
		/* Receiving a file */
		xfer->status = PURPLE_XFER_STATUS_ACCEPTED;
		purple_xfer_set_local_filename(xfer, filename);

		msg = g_strdup_printf(_("Starting transfer of %s from %s"),
				xfer->filename, buddy ? purple_buddy_get_alias(buddy) : xfer->who);
		purple_xfer_conversation_write(xfer, msg, FALSE);
		g_free(msg);
	}

	purple_xfer_add(xfer);
	xfer->ops.init(xfer);

}

void
purple_xfer_request_denied(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);

	if (xfer->ops.request_denied != NULL)
		xfer->ops.request_denied(xfer);

	purple_xfer_unref(xfer);
}

PurpleXferType
purple_xfer_get_type(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, PURPLE_XFER_UNKNOWN);

	return xfer->type;
}

PurpleAccount *
purple_xfer_get_account(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);

	return xfer->account;
}

const char *
purple_xfer_get_remote_user(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);
	return xfer->who;
}

PurpleXferStatusType
purple_xfer_get_status(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, PURPLE_XFER_STATUS_UNKNOWN);

	return xfer->status;
}

gboolean
purple_xfer_is_canceled(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, TRUE);

	if ((purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_CANCEL_LOCAL) ||
	    (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_CANCEL_REMOTE))
		return TRUE;
	else
		return FALSE;
}

gboolean
purple_xfer_is_completed(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, TRUE);

	return (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_DONE);
}

const char *
purple_xfer_get_filename(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);

	return xfer->filename;
}

const char *
purple_xfer_get_local_filename(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);

	return xfer->local_filename;
}

size_t
purple_xfer_get_bytes_sent(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0);

	return xfer->bytes_sent;
}

size_t
purple_xfer_get_bytes_remaining(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0);

	return xfer->bytes_remaining;
}

size_t
purple_xfer_get_size(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0);

	return xfer->size;
}

double
purple_xfer_get_progress(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0.0);

	if (purple_xfer_get_size(xfer) == 0)
		return 0.0;

	return ((double)purple_xfer_get_bytes_sent(xfer) /
			(double)purple_xfer_get_size(xfer));
}

unsigned int
purple_xfer_get_local_port(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, -1);

	return xfer->local_port;
}

const char *
purple_xfer_get_remote_ip(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);

	return xfer->remote_ip;
}

unsigned int
purple_xfer_get_remote_port(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, -1);

	return xfer->remote_port;
}

time_t
purple_xfer_get_start_time(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0);

	return xfer->start_time;
}

time_t
purple_xfer_get_end_time(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, 0);

	return xfer->end_time;
}

void
purple_xfer_set_completed(PurpleXfer *xfer, gboolean completed)
{
	PurpleXferUiOps *ui_ops;

	g_return_if_fail(xfer != NULL);

	if (completed == TRUE) {
		char *msg = NULL;
		PurpleConversation *conv;

		purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_DONE);

		if (purple_xfer_get_filename(xfer) != NULL)
		{
			char *filename = g_markup_escape_text(purple_xfer_get_filename(xfer), -1);
			if (purple_xfer_get_local_filename(xfer)
			 && purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE)
			{
				char *local = g_markup_escape_text(purple_xfer_get_local_filename(xfer), -1);
				msg = g_strdup_printf(_("Transfer of file <A HREF=\"file://%s\">%s</A> complete"),
				                      local, filename);
				g_free(local);
			}
			else
				msg = g_strdup_printf(_("Transfer of file %s complete"),
				                      filename);
			g_free(filename);
		}
		else
			msg = g_strdup(_("File transfer complete"));

		conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, xfer->who,
		                                             purple_xfer_get_account(xfer));

		if (conv != NULL)
			purple_conversation_write(conv, NULL, msg, PURPLE_MESSAGE_SYSTEM, time(NULL));
		g_free(msg);
	}

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->update_progress != NULL)
		ui_ops->update_progress(xfer, purple_xfer_get_progress(xfer));
}

void
purple_xfer_set_message(PurpleXfer *xfer, const char *message)
{
	g_return_if_fail(xfer != NULL);

	g_free(xfer->message);
	xfer->message = g_strdup(message);
}

void
purple_xfer_set_filename(PurpleXfer *xfer, const char *filename)
{
	g_return_if_fail(xfer != NULL);

	g_free(xfer->filename);
	xfer->filename = g_strdup(filename);
}

void
purple_xfer_set_local_filename(PurpleXfer *xfer, const char *filename)
{
	g_return_if_fail(xfer != NULL);

	g_free(xfer->local_filename);
	xfer->local_filename = g_strdup(filename);
}

void
purple_xfer_set_size(PurpleXfer *xfer, size_t size)
{
	g_return_if_fail(xfer != NULL);

	xfer->size = size;
	xfer->bytes_remaining = xfer->size - purple_xfer_get_bytes_sent(xfer);
}

void
purple_xfer_set_bytes_sent(PurpleXfer *xfer, size_t bytes_sent)
{
	g_return_if_fail(xfer != NULL);

	xfer->bytes_sent = bytes_sent;
	xfer->bytes_remaining = purple_xfer_get_size(xfer) - bytes_sent;
}

PurpleXferUiOps *
purple_xfer_get_ui_ops(const PurpleXfer *xfer)
{
	g_return_val_if_fail(xfer != NULL, NULL);

	return xfer->ui_ops;
}

void
purple_xfer_set_init_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.init = fnc;
}

void purple_xfer_set_request_denied_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.request_denied = fnc;
}

void
purple_xfer_set_read_fnc(PurpleXfer *xfer, gssize (*fnc)(guchar **, PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.read = fnc;
}

void
purple_xfer_set_write_fnc(PurpleXfer *xfer,
						gssize (*fnc)(const guchar *, size_t, PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.write = fnc;
}

void
purple_xfer_set_ack_fnc(PurpleXfer *xfer,
			  void (*fnc)(PurpleXfer *, const guchar *, size_t))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.ack = fnc;
}

void
purple_xfer_set_start_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.start = fnc;
}

void
purple_xfer_set_end_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.end = fnc;
}

void
purple_xfer_set_cancel_send_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.cancel_send = fnc;
}

void
purple_xfer_set_cancel_recv_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *))
{
	g_return_if_fail(xfer != NULL);

	xfer->ops.cancel_recv = fnc;
}

static void
purple_xfer_increase_buffer_size(PurpleXfer *xfer)
{
	xfer->current_buffer_size = MIN(xfer->current_buffer_size * 1.5,
			FT_MAX_BUFFER_SIZE);
}

gssize
purple_xfer_read(PurpleXfer *xfer, guchar **buffer)
{
	gssize s, r;

	g_return_val_if_fail(xfer   != NULL, 0);
	g_return_val_if_fail(buffer != NULL, 0);

	if (purple_xfer_get_size(xfer) == 0)
		s = xfer->current_buffer_size;
	else
		s = MIN(purple_xfer_get_bytes_remaining(xfer), xfer->current_buffer_size);

	if (xfer->ops.read != NULL)	{
		r = (xfer->ops.read)(buffer, xfer);
	}
	else {
		*buffer = g_malloc0(s);

		r = read(xfer->fd, *buffer, s);
		if (r < 0 && errno == EAGAIN)
			r = 0;
		else if (r < 0)
			r = -1;
		else if (r == 0)
			r = -1;
	}

	if (r == xfer->current_buffer_size)
		/*
		 * We managed to read the entire buffer.  This means our this
		 * network is fast and our buffer is too small, so make it
		 * bigger.
		 */
		purple_xfer_increase_buffer_size(xfer);

	return r;
}

gssize
purple_xfer_write(PurpleXfer *xfer, const guchar *buffer, gsize size)
{
	gssize r, s;

	g_return_val_if_fail(xfer   != NULL, 0);
	g_return_val_if_fail(buffer != NULL, 0);
	g_return_val_if_fail(size   != 0,    0);

	s = MIN(purple_xfer_get_bytes_remaining(xfer), size);

	if (xfer->ops.write != NULL) {
		r = (xfer->ops.write)(buffer, s, xfer);
	} else {
		r = write(xfer->fd, buffer, s);
		if (r < 0 && errno == EAGAIN)
			r = 0;
		if ((purple_xfer_get_bytes_sent(xfer)+r) >= purple_xfer_get_size(xfer))
			purple_xfer_set_completed(xfer, TRUE);
	}

	return r;
}

static void
do_transfer(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;
	guchar *buffer = NULL;
	gssize r = 0;

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (xfer->type == PURPLE_XFER_RECEIVE) {
		r = purple_xfer_read(xfer, &buffer);
		if (r > 0) {
			size_t wc;
			if (ui_ops && ui_ops->ui_write)
				wc = ui_ops->ui_write(xfer, buffer, r);
			else
				wc = fwrite(buffer, 1, r, xfer->dest_fp);

			if (wc != r) {
				purple_debug_error("filetransfer", "Unable to write whole buffer.\n");
				purple_xfer_cancel_local(xfer);
				g_free(buffer);
				return;
			}

			if ((purple_xfer_get_size(xfer) > 0) &&
				((purple_xfer_get_bytes_sent(xfer)+r) >= purple_xfer_get_size(xfer)))
				purple_xfer_set_completed(xfer, TRUE);
		} else if(r < 0) {
			purple_xfer_cancel_remote(xfer);
			g_free(buffer);
			return;
		}
	} else if (xfer->type == PURPLE_XFER_SEND) {
		size_t result;
		size_t s = MIN(purple_xfer_get_bytes_remaining(xfer), xfer->current_buffer_size);

		/* this is so the prpl can keep the connection open
		   if it needs to for some odd reason. */
		if (s == 0) {
			if (xfer->watcher) {
				purple_input_remove(xfer->watcher);
				xfer->watcher = 0;
			}
			return;
		}

		if (ui_ops && ui_ops->ui_read) {
			gssize tmp = ui_ops->ui_read(xfer, &buffer, s);
			if (tmp == 0) {
				/*
				 * UI isn't ready to send data. It will call
				 * purple_xfer_ui_ready when ready, which sets back up this
				 * watcher.
				 */
				if (xfer->watcher != 0) {
					purple_input_remove(xfer->watcher);
					xfer->watcher = 0;
				}

				return;
			} else if (tmp < 0) {
				purple_debug_error("filetransfer", "Unable to read whole buffer.\n");
				purple_xfer_cancel_local(xfer);
				return;
			}

			result = tmp;
		} else {
			buffer = g_malloc0(s);
			result = fread(buffer, 1, s, xfer->dest_fp);
			if (result != s) {
				purple_debug_error("filetransfer", "Unable to read whole buffer.\n");
				purple_xfer_cancel_local(xfer);
				g_free(buffer);
				return;
			}
		}

		/* Write as much as we're allowed to. */
		r = purple_xfer_write(xfer, buffer, result);

		if (r == -1) {
			purple_xfer_cancel_remote(xfer);
			g_free(buffer);
			return;
		} else if (r < result) {
			if (ui_ops == NULL || (ui_ops->ui_read == NULL && ui_ops->ui_write == NULL)) {
				/* We have to seek back in the file now. */
				fseek(xfer->dest_fp, r - s, SEEK_CUR);
			}
			else {
				ui_ops->data_not_sent(xfer, buffer + r, result - r);
			}
		} else {
			/*
			 * We managed to write the entire buffer.  This means our
			 * network is fast and our buffer is too small, so make it
			 * bigger.
			 */
			purple_xfer_increase_buffer_size(xfer);
		}
	}

	if (r > 0) {
		if (purple_xfer_get_size(xfer) > 0)
			xfer->bytes_remaining -= r;

		xfer->bytes_sent += r;

		if (xfer->ops.ack != NULL)
			xfer->ops.ack(xfer, buffer, r);

		g_free(buffer);

		if (ui_ops != NULL && ui_ops->update_progress != NULL)
			ui_ops->update_progress(xfer,
				purple_xfer_get_progress(xfer));
	}

	if (purple_xfer_is_completed(xfer))
		purple_xfer_end(xfer);
}

static void
transfer_cb(gpointer data, gint source, PurpleInputCondition condition)
{
	PurpleXfer *xfer = data;

	if (xfer->dest_fp == NULL) {
		/* The UI is moderating its side manually */
		PurpleXferPrivData *priv = g_hash_table_lookup(xfers_data, xfer);
		if (0 == (priv->ready & PURPLE_XFER_READY_UI)) {
			priv->ready |= PURPLE_XFER_READY_PRPL;

			purple_input_remove(xfer->watcher);
			xfer->watcher = 0;
			return;
		}
	}

	do_transfer(xfer);
}

static void
begin_transfer(PurpleXfer *xfer, PurpleInputCondition cond)
{
	PurpleXferType type = purple_xfer_get_type(xfer);
	PurpleXferUiOps *ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops == NULL || (ui_ops->ui_read == NULL && ui_ops->ui_write == NULL)) {
		xfer->dest_fp = g_fopen(purple_xfer_get_local_filename(xfer),
		                        type == PURPLE_XFER_RECEIVE ? "wb" : "rb");

		if (xfer->dest_fp == NULL) {
			purple_xfer_show_file_error(xfer, purple_xfer_get_local_filename(xfer));
			purple_xfer_cancel_local(xfer);
			return;
		}

		fseek(xfer->dest_fp, xfer->bytes_sent, SEEK_SET);
	}

	if (xfer->fd != -1)
		xfer->watcher = purple_input_add(xfer->fd, cond, transfer_cb, xfer);

	xfer->start_time = time(NULL);

	if (xfer->ops.start != NULL)
		xfer->ops.start(xfer);
}

static void
connect_cb(gpointer data, gint source, const gchar *error_message)
{
	PurpleXfer *xfer = (PurpleXfer *)data;

	if (source < 0) {
		purple_xfer_cancel_local(xfer);
		return;
	}

	xfer->fd = source;

	begin_transfer(xfer, PURPLE_INPUT_READ);
}

void
purple_xfer_ui_ready(PurpleXfer *xfer)
{
	PurpleInputCondition cond;
	PurpleXferType type;
	PurpleXferPrivData *priv;

	g_return_if_fail(xfer != NULL);

	priv = g_hash_table_lookup(xfers_data, xfer);
	priv->ready |= PURPLE_XFER_READY_UI;

	if (0 == (priv->ready & PURPLE_XFER_READY_PRPL))
		return;

	type = purple_xfer_get_type(xfer);
	if (type == PURPLE_XFER_SEND)
		cond = PURPLE_INPUT_WRITE;
	else /* if (type == PURPLE_XFER_RECEIVE) */
		cond = PURPLE_INPUT_READ;

	if (xfer->watcher == 0 && xfer->fd != -1)
		xfer->watcher = purple_input_add(xfer->fd, cond, transfer_cb, xfer);

	priv->ready = PURPLE_XFER_READY_NONE;

	do_transfer(xfer);
}

void
purple_xfer_prpl_ready(PurpleXfer *xfer)
{
	PurpleXferPrivData *priv;

	g_return_if_fail(xfer != NULL);

	priv = g_hash_table_lookup(xfers_data, xfer);
	priv->ready |= PURPLE_XFER_READY_PRPL;

	/* I don't think fwrite/fread are ever *not* ready */
	if (xfer->dest_fp == NULL && 0 == (priv->ready & PURPLE_XFER_READY_UI))
		return;

	priv->ready = PURPLE_XFER_READY_NONE;

	do_transfer(xfer);
}

void
purple_xfer_start(PurpleXfer *xfer, int fd, const char *ip,
				unsigned int port)
{
	PurpleInputCondition cond;
	PurpleXferType type;

	g_return_if_fail(xfer != NULL);
	g_return_if_fail(purple_xfer_get_type(xfer) != PURPLE_XFER_UNKNOWN);

	type = purple_xfer_get_type(xfer);

	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_STARTED);

	/*
	 * FIXME 3.0.0 -- there's too much broken code depending on fd == 0
	 * meaning "don't use a real fd"
	 */
	if (fd == 0)
		fd = -1;

	if (type == PURPLE_XFER_RECEIVE) {
		cond = PURPLE_INPUT_READ;

		if (ip != NULL) {
			xfer->remote_ip   = g_strdup(ip);
			xfer->remote_port = port;

			/* Establish a file descriptor. */
			purple_proxy_connect(NULL, xfer->account, xfer->remote_ip,
							   xfer->remote_port, connect_cb, xfer);

			return;
		}
		else {
			xfer->fd = fd;
		}
	}
	else {
		cond = PURPLE_INPUT_WRITE;

		xfer->fd = fd;
	}

	begin_transfer(xfer, cond);
}

void
purple_xfer_end(PurpleXfer *xfer)
{
	g_return_if_fail(xfer != NULL);

	/* See if we are actually trying to cancel this. */
	if (!purple_xfer_is_completed(xfer)) {
		purple_xfer_cancel_local(xfer);
		return;
	}

	xfer->end_time = time(NULL);
	if (xfer->ops.end != NULL)
		xfer->ops.end(xfer);

	if (xfer->watcher != 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
	}

	if (xfer->fd != -1)
		close(xfer->fd);

	if (xfer->dest_fp != NULL) {
		fclose(xfer->dest_fp);
		xfer->dest_fp = NULL;
	}

	purple_xfer_unref(xfer);
}

void
purple_xfer_add(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;

	g_return_if_fail(xfer != NULL);

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->add_xfer != NULL)
		ui_ops->add_xfer(xfer);
}

void
purple_xfer_cancel_local(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;
	char *msg = NULL;

	g_return_if_fail(xfer != NULL);

	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_CANCEL_LOCAL);
	xfer->end_time = time(NULL);

	if (purple_xfer_get_filename(xfer) != NULL)
	{
		msg = g_strdup_printf(_("You cancelled the transfer of %s"),
							  purple_xfer_get_filename(xfer));
	}
	else
	{
		msg = g_strdup(_("File transfer cancelled"));
	}
	purple_xfer_conversation_write(xfer, msg, FALSE);
	g_free(msg);

	if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND)
	{
		if (xfer->ops.cancel_send != NULL)
			xfer->ops.cancel_send(xfer);
	}
	else
	{
		if (xfer->ops.cancel_recv != NULL)
			xfer->ops.cancel_recv(xfer);
	}

	if (xfer->watcher != 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
	}

	if (xfer->fd != -1)
		close(xfer->fd);

	if (xfer->dest_fp != NULL) {
		fclose(xfer->dest_fp);
		xfer->dest_fp = NULL;
	}

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->cancel_local != NULL)
		ui_ops->cancel_local(xfer);

	xfer->bytes_remaining = 0;

	purple_xfer_unref(xfer);
}

void
purple_xfer_cancel_remote(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;
	gchar *msg;
	PurpleAccount *account;
	PurpleBuddy *buddy;

	g_return_if_fail(xfer != NULL);

	purple_request_close_with_handle(xfer);
	purple_xfer_set_status(xfer, PURPLE_XFER_STATUS_CANCEL_REMOTE);
	xfer->end_time = time(NULL);

	account = purple_xfer_get_account(xfer);
	buddy = purple_find_buddy(account, xfer->who);

	if (purple_xfer_get_filename(xfer) != NULL)
	{
		msg = g_strdup_printf(_("%s cancelled the transfer of %s"),
				buddy ? purple_buddy_get_alias(buddy) : xfer->who, purple_xfer_get_filename(xfer));
	}
	else
	{
		msg = g_strdup_printf(_("%s cancelled the file transfer"),
				buddy ? purple_buddy_get_alias(buddy) : xfer->who);
	}
	purple_xfer_conversation_write(xfer, msg, TRUE);
	purple_xfer_error(purple_xfer_get_type(xfer), account, xfer->who, msg);
	g_free(msg);

	if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND)
	{
		if (xfer->ops.cancel_send != NULL)
			xfer->ops.cancel_send(xfer);
	}
	else
	{
		if (xfer->ops.cancel_recv != NULL)
			xfer->ops.cancel_recv(xfer);
	}

	if (xfer->watcher != 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
	}

	if (xfer->fd != -1)
		close(xfer->fd);

	if (xfer->dest_fp != NULL) {
		fclose(xfer->dest_fp);
		xfer->dest_fp = NULL;
	}

	ui_ops = purple_xfer_get_ui_ops(xfer);

	if (ui_ops != NULL && ui_ops->cancel_remote != NULL)
		ui_ops->cancel_remote(xfer);

	xfer->bytes_remaining = 0;

	purple_xfer_unref(xfer);
}

void
purple_xfer_error(PurpleXferType type, PurpleAccount *account, const char *who, const char *msg)
{
	char *title;

	g_return_if_fail(msg  != NULL);
	g_return_if_fail(type != PURPLE_XFER_UNKNOWN);

	if (account) {
		PurpleBuddy *buddy;
		buddy = purple_find_buddy(account, who);
		if (buddy)
			who = purple_buddy_get_alias(buddy);
	}

	if (type == PURPLE_XFER_SEND)
		title = g_strdup_printf(_("File transfer to %s failed."), who);
	else
		title = g_strdup_printf(_("File transfer from %s failed."), who);

	purple_notify_error(NULL, NULL, title, msg);

	g_free(title);
}

void
purple_xfer_update_progress(PurpleXfer *xfer)
{
	PurpleXferUiOps *ui_ops;

	g_return_if_fail(xfer != NULL);

	ui_ops = purple_xfer_get_ui_ops(xfer);
	if (ui_ops != NULL && ui_ops->update_progress != NULL)
		ui_ops->update_progress(xfer, purple_xfer_get_progress(xfer));
}


/**************************************************************************
 * File Transfer Subsystem API
 **************************************************************************/
void *
purple_xfers_get_handle(void) {
	static int handle = 0;

	return &handle;
}

void
purple_xfers_init(void) {
	void *handle = purple_xfers_get_handle();

	xfers_data = g_hash_table_new_full(g_direct_hash, g_direct_equal,
	                                   NULL, purple_xfer_priv_data_destroy);

	/* register signals */
	purple_signal_register(handle, "file-recv-accept",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-send-accept",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-recv-start",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-send-start",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-send-cancel",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-recv-cancel",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-send-complete",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-recv-complete",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
	purple_signal_register(handle, "file-recv-request",
	                     purple_marshal_VOID__POINTER, NULL, 1,
	                     purple_value_new(PURPLE_TYPE_SUBTYPE,
	                                    PURPLE_SUBTYPE_XFER));
}

void
purple_xfers_uninit(void)
{
	void *handle = purple_xfers_get_handle();

	purple_signals_disconnect_by_handle(handle);
	purple_signals_unregister_by_instance(handle);

	g_hash_table_destroy(xfers_data);
	xfers_data = NULL;
}

void
purple_xfers_set_ui_ops(PurpleXferUiOps *ops) {
	xfer_ui_ops = ops;
}

PurpleXferUiOps *
purple_xfers_get_ui_ops(void) {
	return xfer_ui_ops;
}
