/*
 *
 *  OBEX library with GLib integration
 *
 *  Copyright (C) 2011  Intel Corporation. All rights reserved.
 *
 *  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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

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

#include <unistd.h>
#include <string.h>
#include <errno.h>

#include "gobex.h"
#include "gobex-debug.h"

#define G_OBEX_DEFAULT_MTU	4096
#define G_OBEX_MINIMUM_MTU	255
#define G_OBEX_MAXIMUM_MTU	65535

#define G_OBEX_DEFAULT_TIMEOUT	10
#define G_OBEX_ABORT_TIMEOUT	5

#define G_OBEX_OP_NONE		0xff

#define FINAL_BIT		0x80

#define CONNID_INVALID		0xffffffff

guint gobex_debug = 0;

struct srm_config {
	guint8 op;
	gboolean enabled;
	guint8 srm;
	guint8 srmp;
	gboolean outgoing;
};

struct _GObex {
	int ref_count;
	GIOChannel *io;
	guint io_source;

	gboolean (*read) (GObex *obex, GError **err);
	gboolean (*write) (GObex *obex, GError **err);

	guint8 *rx_buf;
	size_t rx_data;
	guint16 rx_pkt_len;
	guint8 rx_last_op;

	guint8 *tx_buf;
	size_t tx_data;
	size_t tx_sent;

	gboolean suspended;
	gboolean use_srm;

	struct srm_config *srm;

	guint write_source;

	gssize io_rx_mtu;
	gssize io_tx_mtu;

	guint16 rx_mtu;
	guint16 tx_mtu;

	guint32 conn_id;

	GQueue *tx_queue;

	GSList *req_handlers;

	GObexFunc disconn_func;
	gpointer disconn_func_data;

	struct pending_pkt *pending_req;
};

struct pending_pkt {
	guint id;
	GObex *obex;
	GObexPacket *pkt;
	guint timeout;
	guint timeout_id;
	GObexResponseFunc rsp_func;
	gpointer rsp_data;
	gboolean cancelled;
};

struct req_handler {
	guint id;
	guint8 opcode;
	GObexRequestFunc func;
	gpointer user_data;
};

struct connect_data {
	guint8 version;
	guint8 flags;
	guint16 mtu;
} __attribute__ ((packed));

struct setpath_data {
	guint8 flags;
	guint8 constants;
} __attribute__ ((packed));

static struct error_code {
	guint8 code;
	const char *name;
} obex_errors[] = {
	{ G_OBEX_RSP_CONTINUE,			"Continue" },
	{ G_OBEX_RSP_SUCCESS,			"Success" },
	{ G_OBEX_RSP_CREATED,			"Created" },
	{ G_OBEX_RSP_ACCEPTED,			"Accepted" },
	{ G_OBEX_RSP_NON_AUTHORITATIVE,		"Non Authoritative" },
	{ G_OBEX_RSP_NO_CONTENT,		"No Content" },
	{ G_OBEX_RSP_RESET_CONTENT,		"Reset Content" },
	{ G_OBEX_RSP_PARTIAL_CONTENT,		"Partial Content" },
	{ G_OBEX_RSP_MULTIPLE_CHOICES,		"Multiple Choices" },
	{ G_OBEX_RSP_MOVED_PERMANENTLY,		"Moved Permanently" },
	{ G_OBEX_RSP_MOVED_TEMPORARILY,		"Moved Temporarily" },
	{ G_OBEX_RSP_SEE_OTHER,			"See Other" },
	{ G_OBEX_RSP_NOT_MODIFIED,		"Not Modified" },
	{ G_OBEX_RSP_USE_PROXY,			"Use Proxy" },
	{ G_OBEX_RSP_BAD_REQUEST,		"Bad Request" },
	{ G_OBEX_RSP_UNAUTHORIZED,		"Unauthorized" },
	{ G_OBEX_RSP_PAYMENT_REQUIRED,		"Payment Required" },
	{ G_OBEX_RSP_FORBIDDEN,			"Forbidden" },
	{ G_OBEX_RSP_NOT_FOUND,			"Not Found" },
	{ G_OBEX_RSP_METHOD_NOT_ALLOWED,	"Method Not Allowed" },
	{ G_OBEX_RSP_NOT_ACCEPTABLE,		"Not Acceptable" },
	{ G_OBEX_RSP_PROXY_AUTH_REQUIRED,	"Proxy Authentication Required" },
	{ G_OBEX_RSP_REQUEST_TIME_OUT,		"Request Time Out" },
	{ G_OBEX_RSP_CONFLICT,			"Conflict" },
	{ G_OBEX_RSP_GONE,			"Gone" },
	{ G_OBEX_RSP_LENGTH_REQUIRED,		"Length Required" },
	{ G_OBEX_RSP_PRECONDITION_FAILED,	"Precondition Failed" },
	{ G_OBEX_RSP_REQ_ENTITY_TOO_LARGE,	"Request Entity Too Large" },
	{ G_OBEX_RSP_REQ_URL_TOO_LARGE,		"Request URL Too Large" },
	{ G_OBEX_RSP_UNSUPPORTED_MEDIA_TYPE,	"Unsupported Media Type" },
	{ G_OBEX_RSP_INTERNAL_SERVER_ERROR,	"Internal Server Error" },
	{ G_OBEX_RSP_NOT_IMPLEMENTED,		"Not Implemented" },
	{ G_OBEX_RSP_BAD_GATEWAY,		"Bad Gateway" },
	{ G_OBEX_RSP_SERVICE_UNAVAILABLE,	"Service Unavailable" },
	{ G_OBEX_RSP_GATEWAY_TIMEOUT,		"Gateway Timeout" },
	{ G_OBEX_RSP_VERSION_NOT_SUPPORTED,	"Version Not Supported" },
	{ G_OBEX_RSP_DATABASE_FULL,		"Database Full" },
	{ G_OBEX_RSP_DATABASE_LOCKED,		"Database Locked" },
	{ 0x00,					NULL }
};

const char *g_obex_strerror(guint8 err_code)
{
	struct error_code *error;

	for (error = obex_errors; error->name != NULL; error++) {
		if (error->code == err_code)
			return error->name;
	}

	return "<unknown>";
}

static ssize_t req_header_offset(guint8 opcode)
{
	switch (opcode) {
	case G_OBEX_OP_CONNECT:
		return sizeof(struct connect_data);
	case G_OBEX_OP_SETPATH:
		return sizeof(struct setpath_data);
	case G_OBEX_OP_DISCONNECT:
	case G_OBEX_OP_PUT:
	case G_OBEX_OP_GET:
	case G_OBEX_OP_SESSION:
	case G_OBEX_OP_ABORT:
	case G_OBEX_OP_ACTION:
		return 0;
	default:
		return -1;
	}
}

static ssize_t rsp_header_offset(guint8 opcode)
{
	switch (opcode) {
	case G_OBEX_OP_CONNECT:
		return sizeof(struct connect_data);
	case G_OBEX_OP_SETPATH:
	case G_OBEX_OP_DISCONNECT:
	case G_OBEX_OP_PUT:
	case G_OBEX_OP_GET:
	case G_OBEX_OP_SESSION:
	case G_OBEX_OP_ABORT:
	case G_OBEX_OP_ACTION:
		return 0;
	default:
		return -1;
	}
}

static void pending_pkt_free(struct pending_pkt *p)
{
	if (p->obex != NULL)
		g_obex_unref(p->obex);

	if (p->timeout_id > 0)
		g_source_remove(p->timeout_id);

	g_obex_packet_free(p->pkt);

	g_free(p);
}

static gboolean req_timeout(gpointer user_data)
{
	GObex *obex = user_data;
	struct pending_pkt *p = obex->pending_req;
	GError *err;

	g_assert(p != NULL);

	obex->pending_req = NULL;

	err = g_error_new(G_OBEX_ERROR, G_OBEX_ERROR_TIMEOUT,
					"Timed out waiting for response");

	g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", err->message);

	if (p->rsp_func)
		p->rsp_func(obex, err, NULL, p->rsp_data);

	g_error_free(err);
	pending_pkt_free(p);

	return FALSE;
}

static gboolean write_stream(GObex *obex, GError **err)
{
	GIOStatus status;
	gsize bytes_written;
	char *buf;

	buf = (char *) &obex->tx_buf[obex->tx_sent];
	status = g_io_channel_write_chars(obex->io, buf, obex->tx_data,
							&bytes_written, err);
	if (status != G_IO_STATUS_NORMAL)
		return FALSE;

	g_obex_dump(G_OBEX_DEBUG_DATA, "<", buf, bytes_written);

	obex->tx_sent += bytes_written;
	obex->tx_data -= bytes_written;

	return TRUE;
}

static gboolean write_packet(GObex *obex, GError **err)
{
	GIOStatus status;
	gsize bytes_written;
	char *buf;

	buf = (char *) &obex->tx_buf[obex->tx_sent];
	status = g_io_channel_write_chars(obex->io, buf, obex->tx_data,
							&bytes_written, err);
	if (status != G_IO_STATUS_NORMAL)
		return FALSE;

	if (bytes_written != obex->tx_data)
		return FALSE;

	g_obex_dump(G_OBEX_DEBUG_DATA, "<", buf, bytes_written);

	obex->tx_sent += bytes_written;
	obex->tx_data -= bytes_written;

	return TRUE;
}

static void set_srmp(GObex *obex, guint8 srmp, gboolean outgoing)
{
	struct srm_config *config = obex->srm;

	if (config == NULL)
		return;

	/* Dont't reset if direction doesn't match */
	if (srmp > G_OBEX_SRMP_NEXT_WAIT && config->outgoing != outgoing)
		return;

	config->srmp = srmp;
	config->outgoing = outgoing;
}

static void set_srm(GObex *obex, guint8 op, guint8 srm)
{
	struct srm_config *config = obex->srm;
	gboolean enable;

	if (config == NULL) {
		if (srm == G_OBEX_SRM_DISABLE)
			return;

		config = g_new0(struct srm_config, 1);
		config->op = op;
		config->srm = srm;
		obex->srm = config;
		return;
	}

	/* Indicate response, treat it as request */
	if (config->srm == G_OBEX_SRM_INDICATE) {
		if (srm != G_OBEX_SRM_ENABLE)
			goto done;
		config->srm = srm;
		return;
	}

	enable = (srm == G_OBEX_SRM_ENABLE);
	if (config->enabled == enable)
		goto done;

	config->enabled = enable;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "SRM %s", config->enabled ?
						"Enabled" : "Disabled");

done:
	if (config->enabled)
		return;

	g_free(obex->srm);
	obex->srm = NULL;
}

static void check_srm_final(GObex *obex, guint8 op)
{
	if (obex->srm == NULL || !obex->srm->enabled)
		return;

	switch (obex->srm->op) {
	case G_OBEX_OP_CONNECT:
		return;
	default:
		if (op <= G_OBEX_RSP_CONTINUE)
			return;
	}

	set_srm(obex, op, G_OBEX_SRM_DISABLE);
}

static void setup_srm(GObex *obex, GObexPacket *pkt, gboolean outgoing)
{
	GObexHeader *hdr;
	guint8 op;
	gboolean final;

	if (!obex->use_srm)
		return;

	op = g_obex_packet_get_operation(pkt, &final);

	hdr = g_obex_packet_get_header(pkt, G_OBEX_HDR_SRM);
	if (hdr != NULL) {
		guint8 srm;
		g_obex_header_get_uint8(hdr, &srm);
		g_obex_debug(G_OBEX_DEBUG_COMMAND, "srm 0x%02x", srm);
		set_srm(obex, op, srm);
	}

	hdr = g_obex_packet_get_header(pkt, G_OBEX_HDR_SRMP);
	if (hdr != NULL) {
		guint8 srmp;
		g_obex_header_get_uint8(hdr, &srmp);
		g_obex_debug(G_OBEX_DEBUG_COMMAND, "srmp 0x%02x", srmp);
		set_srmp(obex, srmp, outgoing);
	} else
		set_srmp(obex, -1, outgoing);

	if (final)
		check_srm_final(obex, op);
}

static gboolean write_data(GIOChannel *io, GIOCondition cond,
							gpointer user_data)
{
	GObex *obex = user_data;

	if (cond & G_IO_NVAL)
		return FALSE;

	if (cond & (G_IO_HUP | G_IO_ERR))
		goto stop_tx;

	if (obex->tx_data == 0) {
		struct pending_pkt *p = g_queue_pop_head(obex->tx_queue);
		ssize_t len;

		if (p == NULL)
			goto stop_tx;

		setup_srm(obex, p->pkt, TRUE);

		if (g_obex_srm_active(obex))
			goto encode;

		/* Can't send a request while there's a pending one */
		if (obex->pending_req && p->id > 0) {
			g_queue_push_head(obex->tx_queue, p);
			goto stop_tx;
		}

encode:
		len = g_obex_packet_encode(p->pkt, obex->tx_buf, obex->tx_mtu);
		if (len == -EAGAIN) {
			g_queue_push_head(obex->tx_queue, p);
			g_obex_suspend(obex);
			goto stop_tx;
		}

		if (len < 0) {
			pending_pkt_free(p);
			goto done;
		}

		if (p->id > 0) {
			if (obex->pending_req != NULL)
				pending_pkt_free(obex->pending_req);
			obex->pending_req = p;
			p->timeout_id = g_timeout_add_seconds(p->timeout,
							req_timeout, obex);
		} else {
			/* During packet encode final bit can be set */
			if (obex->tx_buf[0] & FINAL_BIT)
				check_srm_final(obex,
						obex->tx_buf[0] & ~FINAL_BIT);
			pending_pkt_free(p);
		}

		obex->tx_data = len;
		obex->tx_sent = 0;
	}

	if (obex->suspended) {
		obex->write_source = 0;
		return FALSE;
	}

	if (!obex->write(obex, NULL))
		goto stop_tx;

done:
	if (obex->tx_data > 0 || g_queue_get_length(obex->tx_queue) > 0)
		return TRUE;

stop_tx:
	obex->rx_last_op = G_OBEX_OP_NONE;
	obex->tx_data = 0;
	obex->write_source = 0;
	return FALSE;
}

static void enable_tx(GObex *obex)
{
	GIOCondition cond;

	if (obex->suspended)
		return;

	if (obex->write_source > 0)
		return;

	cond = G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL;
	obex->write_source = g_io_add_watch(obex->io, cond, write_data, obex);
}

static gboolean g_obex_send_internal(GObex *obex, struct pending_pkt *p,
								GError **err)
{

	if (obex->io == NULL) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_DISCONNECTED,
					"The transport is not connected");
		g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", (*err)->message);
		return FALSE;
	}

	if (g_obex_packet_get_operation(p->pkt, NULL) == G_OBEX_OP_ABORT)
		g_queue_push_head(obex->tx_queue, p);
	else
		g_queue_push_tail(obex->tx_queue, p);

	if (obex->pending_req == NULL || p->id == 0)
		enable_tx(obex);

	return TRUE;
}

static void init_connect_data(GObex *obex, struct connect_data *data)
{
	guint16 u16;

	memset(data, 0, sizeof(*data));

	data->version = 0x10;
	data->flags = 0;

	u16 = g_htons(obex->rx_mtu);
	memcpy(&data->mtu, &u16, sizeof(u16));
}

static void prepare_connect_rsp(GObex *obex, GObexPacket *rsp)
{
	GObexHeader *connid;
	struct connect_data data;
	static guint32 next_connid = 1;

	init_connect_data(obex, &data);
	g_obex_packet_set_data(rsp, &data, sizeof(data), G_OBEX_DATA_COPY);

	connid = g_obex_packet_get_header(rsp, G_OBEX_HDR_CONNECTION);
	if (connid != NULL) {
		g_obex_header_get_uint32(connid, &obex->conn_id);
		return;
	}

	obex->conn_id = next_connid++;

	connid = g_obex_header_new_uint32(G_OBEX_HDR_CONNECTION,
							obex->conn_id);
	g_obex_packet_prepend_header(rsp, connid);
}

static void prepare_srm_rsp(GObex *obex, GObexPacket *pkt)
{
	GObexHeader *hdr;

	if (!obex->use_srm || obex->srm == NULL)
		return;

	if (obex->srm->enabled)
		return;

	hdr = g_obex_packet_get_header(pkt, G_OBEX_HDR_SRM);
	if (hdr != NULL)
		return;

	hdr = g_obex_header_new_uint8(G_OBEX_HDR_SRM, G_OBEX_SRM_ENABLE);
	g_obex_packet_prepend_header(pkt, hdr);
}

gboolean g_obex_send(GObex *obex, GObexPacket *pkt, GError **err)
{
	struct pending_pkt *p;
	gboolean ret;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	if (obex == NULL || pkt == NULL) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_INVALID_ARGS,
				"Invalid arguments");
		g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", (*err)->message);
		return FALSE;
	}

	switch (obex->rx_last_op) {
	case G_OBEX_OP_CONNECT:
		prepare_connect_rsp(obex, pkt);
		break;
	case G_OBEX_OP_GET:
	case G_OBEX_OP_PUT:
		prepare_srm_rsp(obex, pkt);
		break;
	}

	p = g_new0(struct pending_pkt, 1);
	p->pkt = pkt;

	ret = g_obex_send_internal(obex, p, err);
	if (ret == FALSE)
		pending_pkt_free(p);

	return ret;
}

static void prepare_srm_req(GObex *obex, GObexPacket *pkt)
{
	GObexHeader *hdr;

	if (!obex->use_srm)
		return;

	if (obex->srm != NULL && obex->srm->enabled)
		return;

	hdr = g_obex_packet_get_header(pkt, G_OBEX_HDR_SRM);
	if (hdr != NULL)
		return;

	hdr = g_obex_header_new_uint8(G_OBEX_HDR_SRM, G_OBEX_SRM_ENABLE);
	g_obex_packet_prepend_header(pkt, hdr);
}

guint g_obex_send_req(GObex *obex, GObexPacket *req, int timeout,
			GObexResponseFunc func, gpointer user_data,
			GError **err)
{
	GObexHeader *hdr;
	struct pending_pkt *p;
	static guint id = 1;
	guint8 op;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	op = g_obex_packet_get_operation(req, NULL);
	if (op == G_OBEX_OP_PUT || op == G_OBEX_OP_GET) {
		/* Only enable SRM automatically for GET and PUT */
		prepare_srm_req(obex, req);
	}

	if (obex->conn_id == CONNID_INVALID)
		goto create_pending;

	if (obex->rx_last_op == G_OBEX_RSP_CONTINUE)
		goto create_pending;

	if (g_obex_srm_active(obex) && obex->pending_req != NULL)
		goto create_pending;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_CONNECTION);
	if (hdr != NULL)
		goto create_pending;

	hdr = g_obex_header_new_uint32(G_OBEX_HDR_CONNECTION, obex->conn_id);
	g_obex_packet_prepend_header(req, hdr);

create_pending:
	p = g_new0(struct pending_pkt, 1);

	p->pkt = req;
	p->id = id++;
	p->rsp_func = func;
	p->rsp_data = user_data;

	if (timeout < 0)
		p->timeout = G_OBEX_DEFAULT_TIMEOUT;
	else
		p->timeout = timeout;

	if (!g_obex_send_internal(obex, p, err)) {
		pending_pkt_free(p);
		return 0;
	}

	return p->id;
}

static int pending_pkt_cmp(gconstpointer a, gconstpointer b)
{
	const struct pending_pkt *p = a;
	guint id = GPOINTER_TO_UINT(b);

	return (p->id - id);
}

gboolean g_obex_pending_req_abort(GObex *obex, GError **err)
{
	struct pending_pkt *p = obex->pending_req;
	GObexPacket *req;

	if (p->cancelled)
		return TRUE;

	p->cancelled = TRUE;

	g_source_remove(p->timeout_id);
	p->timeout = G_OBEX_ABORT_TIMEOUT;
	p->timeout_id = g_timeout_add_seconds(p->timeout, req_timeout, obex);

	req = g_obex_packet_new(G_OBEX_OP_ABORT, TRUE, G_OBEX_HDR_INVALID);

	return g_obex_send(obex, req, err);
}

static gboolean cancel_complete(gpointer user_data)
{
	struct pending_pkt *p = user_data;
	GObex *obex = p->obex;
	GError *err;

	g_assert(p->rsp_func != NULL);

	err = g_error_new(G_OBEX_ERROR, G_OBEX_ERROR_CANCELLED,
					"The request was cancelled");
	p->rsp_func(obex, err, NULL, p->rsp_data);

	g_error_free(err);

	pending_pkt_free(p);

	return FALSE;
}

gboolean g_obex_cancel_req(GObex *obex, guint req_id, gboolean remove_callback)
{
	GList *match;
	struct pending_pkt *p;

	if (obex->pending_req && obex->pending_req->id == req_id) {
		if (!g_obex_pending_req_abort(obex, NULL)) {
			p = obex->pending_req;
			obex->pending_req = NULL;
			goto immediate_completion;
		}

		if (remove_callback)
			obex->pending_req->rsp_func = NULL;

		return TRUE;
	}

	match = g_queue_find_custom(obex->tx_queue, GUINT_TO_POINTER(req_id),
							pending_pkt_cmp);
	if (match == NULL)
		return FALSE;

	p = match->data;

	g_queue_delete_link(obex->tx_queue, match);

immediate_completion:
	p->cancelled = TRUE;
	p->obex = g_obex_ref(obex);

	if (remove_callback || p->rsp_func == NULL)
		pending_pkt_free(p);
	else
		g_idle_add(cancel_complete, p);

	return TRUE;
}

gboolean g_obex_send_rsp(GObex *obex, guint8 rspcode, GError **err,
						guint8 first_hdr_type, ...)
{
	GObexPacket *rsp;
	va_list args;

	va_start(args, first_hdr_type);
	rsp = g_obex_packet_new_valist(rspcode, TRUE, first_hdr_type, args);
	va_end(args);

	return g_obex_send(obex, rsp, err);
}

void g_obex_set_disconnect_function(GObex *obex, GObexFunc func,
							gpointer user_data)
{
	obex->disconn_func = func;
	obex->disconn_func_data = user_data;
}

static int req_handler_cmpop(gconstpointer a, gconstpointer b)
{
	const struct req_handler *handler = a;
	guint opcode = GPOINTER_TO_UINT(b);

	return (int) handler->opcode - (int) opcode;
}

static int req_handler_cmpid(gconstpointer a, gconstpointer b)
{
	const struct req_handler *handler = a;
	guint id = GPOINTER_TO_UINT(b);

	return (int) handler->id - (int) id;
}

guint g_obex_add_request_function(GObex *obex, guint8 opcode,
						GObexRequestFunc func,
						gpointer user_data)
{
	struct req_handler *handler;
	static guint next_id = 1;

	handler = g_new0(struct req_handler, 1);
	handler->id = next_id++;
	handler->opcode = opcode;
	handler->func = func;
	handler->user_data = user_data;

	obex->req_handlers = g_slist_prepend(obex->req_handlers, handler);

	return handler->id;
}

gboolean g_obex_remove_request_function(GObex *obex, guint id)
{
	struct req_handler *handler;
	GSList *match;

	match = g_slist_find_custom(obex->req_handlers, GUINT_TO_POINTER(id),
							req_handler_cmpid);
	if (match == NULL)
		return FALSE;

	handler = match->data;

	obex->req_handlers = g_slist_delete_link(obex->req_handlers, match);
	g_free(handler);

	return TRUE;
}

void g_obex_suspend(GObex *obex)
{
	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	if (obex->write_source > 0) {
		g_source_remove(obex->write_source);
		obex->write_source = 0;
	}

	obex->suspended = TRUE;
}

void g_obex_resume(GObex *obex)
{
	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	obex->suspended = FALSE;

	if (g_queue_get_length(obex->tx_queue) > 0 || obex->tx_data > 0)
		enable_tx(obex);
}

gboolean g_obex_srm_active(GObex *obex)
{
	gboolean ret = FALSE;

	if (!obex->use_srm)
		return FALSE;

	if (obex->srm == NULL || !obex->srm->enabled)
		goto done;

	if (obex->srm->srmp <= G_OBEX_SRMP_NEXT_WAIT)
		goto done;

	ret = TRUE;
done:
	g_obex_debug(G_OBEX_DEBUG_COMMAND, "%s", ret ? "yes" : "no");
	return ret;
}

static void parse_connect_data(GObex *obex, GObexPacket *pkt)
{
	const struct connect_data *data;
	GObexHeader *connid;
	guint16 u16;
	size_t data_len;

	data = g_obex_packet_get_data(pkt, &data_len);
	if (data == NULL || data_len != sizeof(*data))
		return;

	memcpy(&u16, &data->mtu, sizeof(u16));

	obex->tx_mtu = g_ntohs(u16);
	if (obex->io_tx_mtu > 0 && obex->tx_mtu > obex->io_tx_mtu)
		obex->tx_mtu = obex->io_tx_mtu;
	obex->tx_buf = g_realloc(obex->tx_buf, obex->tx_mtu);

	connid = g_obex_packet_get_header(pkt, G_OBEX_HDR_CONNECTION);
	if (connid != NULL)
		g_obex_header_get_uint32(connid, &obex->conn_id);
}

static gboolean parse_response(GObex *obex, GObexPacket *rsp)
{
	struct pending_pkt *p = obex->pending_req;
	guint8 opcode, rspcode;
	gboolean final;

	rspcode = g_obex_packet_get_operation(rsp, &final);

	opcode = g_obex_packet_get_operation(p->pkt, NULL);
	if (opcode == G_OBEX_OP_CONNECT)
		parse_connect_data(obex, rsp);

	setup_srm(obex, rsp, FALSE);

	if (!g_obex_srm_active(obex))
		return final;

	/*
	 * Resposes have final bit set but in case of GET with SRM
	 * we should not remove the request since the remote side will
	 * continue sending responses until the transfer is finished
	 */
	if (opcode == G_OBEX_OP_GET && rspcode == G_OBEX_RSP_CONTINUE) {
		g_source_remove(p->timeout_id);
		p->timeout_id = g_timeout_add_seconds(p->timeout, req_timeout,
									obex);
		return FALSE;
	}

	return final;
}

static void handle_response(GObex *obex, GError *err, GObexPacket *rsp)
{
	struct pending_pkt *p = obex->pending_req;
	gboolean disconn = err ? TRUE : FALSE, final_rsp = TRUE;

	if (rsp != NULL)
		final_rsp = parse_response(obex, rsp);

	if (p->cancelled)
		err = g_error_new(G_OBEX_ERROR, G_OBEX_ERROR_CANCELLED,
					"The operation was cancelled");

	if (err)
		g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", err->message);

	if (p->rsp_func) {
		p->rsp_func(obex, err, rsp, p->rsp_data);

		/* Check if user callback removed the request */
		if (p != obex->pending_req)
			return;
	}

	if (p->cancelled)
		g_error_free(err);

	if (final_rsp) {
		pending_pkt_free(p);
		obex->pending_req = NULL;
	}

	if (!disconn && g_queue_get_length(obex->tx_queue) > 0)
		enable_tx(obex);
}

static gboolean check_connid(GObex *obex, GObexPacket *pkt)
{
	GObexHeader *hdr;
	guint32 id;

	if (obex->conn_id == CONNID_INVALID)
		return TRUE;

	hdr = g_obex_packet_get_header(pkt, G_OBEX_HDR_CONNECTION);
	if (hdr == NULL)
		return TRUE;

	g_obex_header_get_uint32(hdr, &id);

	return obex->conn_id == id;
}

static int parse_request(GObex *obex, GObexPacket *req)
{
	guint8 op;
	gboolean final;

	op = g_obex_packet_get_operation(req, &final);
	switch (op) {
	case G_OBEX_OP_CONNECT:
		parse_connect_data(obex, req);
		break;
	case G_OBEX_OP_ABORT:
		break;
	default:
		if (check_connid(obex, req))
			break;

		return -G_OBEX_RSP_SERVICE_UNAVAILABLE;
	}

	setup_srm(obex, req, FALSE);

	return op;
}

static void handle_request(GObex *obex, GObexPacket *req)
{
	GSList *match;
	int op;

	op = parse_request(obex, req);
	if (op < 0)
		goto fail;

	match = g_slist_find_custom(obex->req_handlers, GUINT_TO_POINTER(op),
							req_handler_cmpop);
	if (match) {
		struct req_handler *handler = match->data;
		handler->func(obex, req, handler->user_data);
		return;
	}

	op = -G_OBEX_RSP_NOT_IMPLEMENTED;

fail:
	g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", g_obex_strerror(-op));
	g_obex_send_rsp(obex, -op, NULL, G_OBEX_HDR_INVALID);
}

static gboolean read_stream(GObex *obex, GError **err)
{
	GIOChannel *io = obex->io;
	GIOStatus status;
	gsize rbytes, toread;
	guint16 u16;
	char *buf;

	if (obex->rx_data >= 3)
		goto read_body;

	rbytes = 0;
	toread = 3 - obex->rx_data;
	buf = (char *) &obex->rx_buf[obex->rx_data];

	status = g_io_channel_read_chars(io, buf, toread, &rbytes, NULL);
	if (status != G_IO_STATUS_NORMAL)
		return TRUE;

	obex->rx_data += rbytes;
	if (obex->rx_data < 3)
		goto done;

	memcpy(&u16, &buf[1], sizeof(u16));
	obex->rx_pkt_len = g_ntohs(u16);

	if (obex->rx_pkt_len > obex->rx_mtu) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
				"Too big incoming packet");
		g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", (*err)->message);
		return FALSE;
	}

read_body:
	if (obex->rx_data >= obex->rx_pkt_len)
		goto done;

	do {
		toread = obex->rx_pkt_len - obex->rx_data;
		buf = (char *) &obex->rx_buf[obex->rx_data];

		status = g_io_channel_read_chars(io, buf, toread, &rbytes, NULL);
		if (status != G_IO_STATUS_NORMAL)
			goto done;

		obex->rx_data += rbytes;
	} while (rbytes > 0 && obex->rx_data < obex->rx_pkt_len);

done:
	g_obex_dump(G_OBEX_DEBUG_DATA, ">", obex->rx_buf, obex->rx_data);

	return TRUE;
}

static gboolean read_packet(GObex *obex, GError **err)
{
	GIOChannel *io = obex->io;
	GError *read_err = NULL;
	GIOStatus status;
	gsize rbytes;
	guint16 u16;

	if (obex->rx_data > 0) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
				"RX buffer not empty before reading packet");
		goto fail;
	}

	status = g_io_channel_read_chars(io, (char *) obex->rx_buf,
					obex->rx_mtu, &rbytes, &read_err);
	if (status != G_IO_STATUS_NORMAL) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
				"Unable to read data: %s", read_err->message);
		g_error_free(read_err);
		goto fail;
	}

	obex->rx_data += rbytes;

	if (rbytes < 3) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
				"Incomplete packet received");
		goto fail;
	}

	memcpy(&u16, &obex->rx_buf[1], sizeof(u16));
	obex->rx_pkt_len = g_ntohs(u16);

	if (obex->rx_pkt_len != rbytes) {
		g_set_error(err, G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
			"Data size doesn't match packet size (%zu != %u)",
			rbytes, obex->rx_pkt_len);
		return FALSE;
	}

	g_obex_dump(G_OBEX_DEBUG_DATA, ">", obex->rx_buf, obex->rx_data);

	return TRUE;
fail:
	g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", (*err)->message);
	return FALSE;
}

static gboolean incoming_data(GIOChannel *io, GIOCondition cond,
							gpointer user_data)
{
	GObex *obex = user_data;
	GObexPacket *pkt;
	ssize_t header_offset;
	GError *err = NULL;
	guint8 opcode;

	if (cond & G_IO_NVAL)
		return FALSE;

	if (cond & (G_IO_HUP | G_IO_ERR)) {
		err = g_error_new(G_OBEX_ERROR, G_OBEX_ERROR_DISCONNECTED,
					"Transport got disconnected");
		goto failed;
	}

	if (!obex->read(obex, &err))
		goto failed;

	if (obex->rx_data < 3 || obex->rx_data < obex->rx_pkt_len)
		return TRUE;

	obex->rx_last_op = obex->rx_buf[0] & ~FINAL_BIT;

	if (obex->pending_req) {
		struct pending_pkt *p = obex->pending_req;
		opcode = g_obex_packet_get_operation(p->pkt, NULL);
		header_offset = rsp_header_offset(opcode);
	} else {
		opcode = obex->rx_last_op;
		/* Unexpected response -- fail silently */
		if (opcode > 0x1f && opcode != G_OBEX_OP_ABORT) {
			obex->rx_data = 0;
			return TRUE;
		}
		header_offset = req_header_offset(opcode);
	}

	if (header_offset < 0) {
		err = g_error_new(G_OBEX_ERROR, G_OBEX_ERROR_PARSE_ERROR,
				"Unknown header offset for opcode 0x%02x",
				opcode);
		goto failed;
	}

	pkt = g_obex_packet_decode(obex->rx_buf, obex->rx_data, header_offset,
							G_OBEX_DATA_REF, &err);
	if (pkt == NULL)
		goto failed;

	/* Protect against user callback freeing the object */
	g_obex_ref(obex);

	if (obex->pending_req)
		handle_response(obex, NULL, pkt);
	else
		handle_request(obex, pkt);

	obex->rx_data = 0;

	g_obex_unref(obex);

	if (err != NULL)
		g_error_free(err);

	if (pkt != NULL)
		g_obex_packet_free(pkt);

	return TRUE;

failed:
	if (err)
		g_obex_debug(G_OBEX_DEBUG_ERROR, "%s", err->message);

	g_io_channel_unref(obex->io);
	obex->io = NULL;
	obex->io_source = 0;
	obex->rx_data = 0;

	/* Protect against user callback freeing the object */
	g_obex_ref(obex);

	if (obex->pending_req)
		handle_response(obex, err, NULL);

	if (obex->disconn_func)
		obex->disconn_func(obex, err, obex->disconn_func_data);

	g_obex_unref(obex);

	g_error_free(err);

	return FALSE;
}

static GDebugKey keys[] = {
	{ "error",	G_OBEX_DEBUG_ERROR },
	{ "command",	G_OBEX_DEBUG_COMMAND },
	{ "transfer",	G_OBEX_DEBUG_TRANSFER },
	{ "header",	G_OBEX_DEBUG_HEADER },
	{ "packet",	G_OBEX_DEBUG_PACKET },
	{ "data",	G_OBEX_DEBUG_DATA },
	{ "apparam",	G_OBEX_DEBUG_APPARAM },
};

GObex *g_obex_new(GIOChannel *io, GObexTransportType transport_type,
					gssize io_rx_mtu, gssize io_tx_mtu)
{
	GObex *obex;
	GIOCondition cond;

	if (gobex_debug == 0) {
		const char *env = g_getenv("GOBEX_DEBUG");

		if (env) {
			gobex_debug = g_parse_debug_string(env, keys, 7);
			g_setenv("G_MESSAGES_DEBUG", "gobex", FALSE);
		} else
			gobex_debug = G_OBEX_DEBUG_NONE;
	}

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "");

	if (io == NULL)
		return NULL;

	if (io_rx_mtu >= 0 && io_rx_mtu < G_OBEX_MINIMUM_MTU)
		return NULL;

	if (io_tx_mtu >= 0 && io_tx_mtu < G_OBEX_MINIMUM_MTU)
		return NULL;

	obex = g_new0(GObex, 1);

	obex->io = g_io_channel_ref(io);
	obex->ref_count = 1;
	obex->conn_id = CONNID_INVALID;
	obex->rx_last_op = G_OBEX_OP_NONE;

	obex->io_rx_mtu = io_rx_mtu;
	obex->io_tx_mtu = io_tx_mtu;

	if (io_rx_mtu > G_OBEX_MAXIMUM_MTU)
		obex->rx_mtu = G_OBEX_MAXIMUM_MTU;
	else if (io_rx_mtu < G_OBEX_MINIMUM_MTU)
		obex->rx_mtu = G_OBEX_DEFAULT_MTU;
	else
		obex->rx_mtu = io_rx_mtu;

	obex->tx_mtu = G_OBEX_MINIMUM_MTU;

	obex->tx_queue = g_queue_new();
	obex->rx_buf = g_malloc(obex->rx_mtu);
	obex->tx_buf = g_malloc(obex->tx_mtu);

	switch (transport_type) {
	case G_OBEX_TRANSPORT_STREAM:
		obex->read = read_stream;
		obex->write = write_stream;
		break;
	case G_OBEX_TRANSPORT_PACKET:
		obex->use_srm = TRUE;
		obex->read = read_packet;
		obex->write = write_packet;
		break;
	default:
		g_obex_unref(obex);
		return NULL;
	}

	g_io_channel_set_encoding(io, NULL, NULL);
	g_io_channel_set_buffered(io, FALSE);
	cond = G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL;
	obex->io_source = g_io_add_watch(io, cond, incoming_data, obex);

	return obex;
}

GObex *g_obex_ref(GObex *obex)
{
	int refs;

	if (obex == NULL)
		return NULL;

	refs = __sync_add_and_fetch(&obex->ref_count, 1);

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "ref %u", refs);

	return obex;
}

void g_obex_unref(GObex *obex)
{
	int refs;

	refs = __sync_sub_and_fetch(&obex->ref_count, 1);

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "ref %u", refs);

	if (refs > 0)
		return;

	g_slist_free_full(obex->req_handlers, g_free);

	g_queue_foreach(obex->tx_queue, (GFunc) pending_pkt_free, NULL);
	g_queue_free(obex->tx_queue);

	if (obex->io != NULL)
		g_io_channel_unref(obex->io);

	if (obex->io_source > 0)
		g_source_remove(obex->io_source);

	if (obex->write_source > 0)
		g_source_remove(obex->write_source);

	g_free(obex->rx_buf);
	g_free(obex->tx_buf);
	g_free(obex->srm);

	if (obex->pending_req)
		pending_pkt_free(obex->pending_req);

	g_free(obex);
}

/* Higher level functions */

guint g_obex_connect(GObex *obex, GObexResponseFunc func, gpointer user_data,
					GError **err, guint8 first_hdr_id, ...)
{
	GObexPacket *req;
	struct connect_data data;
	va_list args;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "");

	va_start(args, first_hdr_id);
	req = g_obex_packet_new_valist(G_OBEX_OP_CONNECT, TRUE,
							first_hdr_id, args);
	va_end(args);

	init_connect_data(obex, &data);
	g_obex_packet_set_data(req, &data, sizeof(data), G_OBEX_DATA_COPY);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint g_obex_setpath(GObex *obex, const char *path, GObexResponseFunc func,
					gpointer user_data, GError **err)
{
	GObexPacket *req;
	struct setpath_data data;
	const char *folder;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	req = g_obex_packet_new(G_OBEX_OP_SETPATH, TRUE, G_OBEX_HDR_INVALID);

	memset(&data, 0, sizeof(data));

	if (path != NULL && strncmp("..", path, 2) == 0) {
		data.flags = 0x03;
		folder = (path[2] == '/') ? &path[3] : NULL;
	} else {
		data.flags = 0x02;
		folder = path;
	}

	if (folder != NULL) {
		GObexHeader *hdr;
		hdr = g_obex_header_new_unicode(G_OBEX_HDR_NAME, folder);
		g_obex_packet_add_header(req, hdr);
	}

	g_obex_packet_set_data(req, &data, sizeof(data), G_OBEX_DATA_COPY);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint g_obex_mkdir(GObex *obex, const char *path, GObexResponseFunc func,
					gpointer user_data, GError **err)
{
	GObexPacket *req;
	struct setpath_data data;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	req = g_obex_packet_new(G_OBEX_OP_SETPATH, TRUE, G_OBEX_HDR_NAME, path,
							G_OBEX_HDR_INVALID);

	memset(&data, 0, sizeof(data));
	g_obex_packet_set_data(req, &data, sizeof(data), G_OBEX_DATA_COPY);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint g_obex_delete(GObex *obex, const char *name, GObexResponseFunc func,
					gpointer user_data, GError **err)
{
	GObexPacket *req;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	req = g_obex_packet_new(G_OBEX_OP_PUT, TRUE, G_OBEX_HDR_NAME, name,
							G_OBEX_HDR_INVALID);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint g_obex_copy(GObex *obex, const char *name, const char *dest,
			GObexResponseFunc func, gpointer user_data,
			GError **err)
{
	GObexPacket *req;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	req = g_obex_packet_new(G_OBEX_OP_ACTION, TRUE,
					G_OBEX_HDR_ACTION, G_OBEX_ACTION_COPY,
					G_OBEX_HDR_NAME, name,
					G_OBEX_HDR_DESTNAME, dest,
					G_OBEX_HDR_INVALID);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint g_obex_move(GObex *obex, const char *name, const char *dest,
			GObexResponseFunc func, gpointer user_data,
			GError **err)
{
	GObexPacket *req;

	g_obex_debug(G_OBEX_DEBUG_COMMAND, "conn %u", obex->conn_id);

	req = g_obex_packet_new(G_OBEX_OP_ACTION, TRUE,
					G_OBEX_HDR_ACTION, G_OBEX_ACTION_MOVE,
					G_OBEX_HDR_NAME, name,
					G_OBEX_HDR_DESTNAME, dest,
					G_OBEX_HDR_INVALID);

	return g_obex_send_req(obex, req, -1, func, user_data, err);
}

guint8 g_obex_errno_to_rsp(int err)
{
	switch (err) {
	case 0:
		return G_OBEX_RSP_SUCCESS;
	case -EPERM:
	case -EACCES:
		return G_OBEX_RSP_FORBIDDEN;
	case -ENOENT:
		return G_OBEX_RSP_NOT_FOUND;
	case -EINVAL:
	case -EBADR:
		return G_OBEX_RSP_BAD_REQUEST;
	case -EFAULT:
		return G_OBEX_RSP_SERVICE_UNAVAILABLE;
	case -ENOSYS:
		return G_OBEX_RSP_NOT_IMPLEMENTED;
	case -ENOTEMPTY:
	case -EEXIST:
		return G_OBEX_RSP_PRECONDITION_FAILED;
	default:
		return G_OBEX_RSP_INTERNAL_SERVER_ERROR;
	}
}
