/*
 *
 *  oFono - Open Source Telephony
 *
 *  Copyright (C) 2009-2010  Nokia Corporation and/or its subsidiary(-ies).
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/uio.h>
#include <net/if.h>
#include <errno.h>
#include <glib.h>

#pragma GCC diagnostic ignored "-Waddress-of-packed-member"

#include "message.h"
#include "common.h"
#include "modem.h"
#include "socket.h"

#define ISIDBG(m, fmt, ...)				\
	if ((m) != NULL && (m)->debug != NULL)		\
		m->debug("gisi: "fmt, ##__VA_ARGS__);

struct _GIsiServiceMux {
	GIsiModem *modem;
	GSList *pending;
	GIsiVersion version;
	uint8_t resource;
	uint8_t last_utid;
	uint16_t object;
	unsigned subscriptions;
	unsigned registrations;
	gboolean reachable;
	gboolean version_pending;
};
typedef struct _GIsiServiceMux GIsiServiceMux;

struct _GIsiModem {
	unsigned index;
	uint8_t device;
	GHashTable *services;
	gboolean subs_source;
	int req_fd;
	int ind_fd;
	guint req_watch;
	guint ind_watch;
	GIsiDebugFunc debug;
	GIsiNotifyFunc trace;
	void *opaque;
	unsigned long flags;
};

struct _GIsiPending {
	enum GIsiMessageType type;
	GIsiServiceMux *service;
	gpointer owner;
	guint timeout;
	GIsiNotifyFunc notify;
	GDestroyNotify destroy;
	void *data;
	uint8_t utid;
	uint8_t msgid;
};

static GIsiServiceMux *service_get(GIsiModem *modem, uint8_t resource)
{
	GIsiServiceMux *mux;
	int key = resource;

	mux = g_hash_table_lookup(modem->services, GINT_TO_POINTER(key));
	if (mux != NULL)
		return mux;

	mux = g_try_new0(GIsiServiceMux, 1);
	if (mux == NULL)
		return NULL;

	g_hash_table_insert(modem->services, GINT_TO_POINTER(key), mux);

	mux->modem = modem;
	mux->resource = resource;
	mux->version.major = -1;
	mux->version.minor = -1;
	mux->reachable = FALSE;
	mux->version_pending = FALSE;

	return mux;
}

static gint utid_equal(gconstpointer a, gconstpointer b)
{
	const GIsiPending *pa = a;
	const GIsiPending *pb = b;

	return pa->utid - pb->utid;
}

static const char *pend_type_to_str(enum GIsiMessageType type)
{
	switch (type) {
	case GISI_MESSAGE_TYPE_REQ:
		return "REQ";
	case GISI_MESSAGE_TYPE_IND:
		return "IND";
	case GISI_MESSAGE_TYPE_NTF:
		return "NTF";
	case GISI_MESSAGE_TYPE_RESP:
		return "RESP";
	case GISI_MESSAGE_TYPE_COMMON:
		return "COMMON";
	}
	return "UNKNOWN";
}

static void pending_dispatch(GIsiPending *pend, GIsiMessage *msg)
{
	GIsiModem *modem;

	if (pend->notify == NULL)
		return;

	modem = pend->service->modem;

	ISIDBG(modem, "%s %s to %p [res=0x%02X, id=0x%02X, utid=0x%02X]",
		g_isi_msg_strerror(msg), pend_type_to_str(pend->type), pend,
		g_isi_msg_resource(msg), g_isi_msg_id(msg),
		g_isi_msg_utid(msg));

	pend->notify(msg, pend->data);
}

static void pending_remove_and_dispatch(GIsiPending *op, GIsiMessage *msg)
{
	GIsiModem *modem;

	op->service->pending = g_slist_remove(op->service->pending, op);

	if (op->notify == NULL || msg == NULL)
		goto destroy;

	modem = op->service->modem;

	ISIDBG(modem, "%s %s to %p [res=0x%02X, id=0x%02X, utid=0x%02X]",
		g_isi_msg_error(msg) ? g_isi_msg_strerror(msg) : "normal",
		pend_type_to_str(op->type), op,
		g_isi_msg_resource(msg), g_isi_msg_id(msg),
		g_isi_msg_utid(msg));

	op->notify(msg, op->data);

destroy:
	if (op->timeout > 0)
		g_source_remove(op->timeout);

	if (op->destroy != NULL)
		op->destroy(op->data);

	g_free(op);
}

static void service_dispatch(GIsiServiceMux *mux, GIsiMessage *msg,
				gboolean is_indication)
{
	uint8_t msgid = g_isi_msg_id(msg);
	uint8_t utid = g_isi_msg_utid(msg);

	GSList *l = mux->pending;

	while (l != NULL) {
		GSList *next = l->next;
		GIsiPending *pend = l->data;

		/*
		 * REQs, NTFs and INDs are dispatched on message ID.  While
		 * INDs have the unique transaction ID set to zero, NTFs
		 * typically mirror the UTID of the request that set up the
		 * session, and REQs can naturally have any transaction ID.
		 *
		 * RESPs are dispatched on unique transaction ID, explicitly
		 * ignoring the msgid.  A RESP also completes a transaction,
		 * so it needs to be removed after being notified of.
		 *
		 * Version query responses are dispatched in a similar fashion
		 * as RESPs, but based on the pending type and the message ID.
		 * Some of these may be synthesized, but nevertheless need to
		 * be removed.
		 */
		if (pend->type < GISI_MESSAGE_TYPE_RESP
				&& pend->msgid == msgid) {

			pending_dispatch(pend, msg);

		} else if (pend->type == GISI_MESSAGE_TYPE_RESP &&
				!is_indication && pend->utid == utid) {

			pending_remove_and_dispatch(pend, msg);
			break;

		} else if (pend->type == GISI_MESSAGE_TYPE_COMMON &&
				msgid == COMMON_MESSAGE &&
				pend->msgid == COMM_ISI_VERSION_GET_REQ) {

			pending_remove_and_dispatch(pend, msg);
		}

		l = next;
	}
}

static void common_message_decode(GIsiServiceMux *mux, GIsiMessage *msg)
{
	uint8_t code;
	uint8_t major;
	uint8_t minor;

	if (!g_isi_msg_data_get_byte(msg, 0, &code))
		return;

	switch (code) {
	case COMM_ISA_ENTITY_NOT_REACHABLE_RESP:
		mux->reachable = FALSE;
		msg->error = ENOENT;
		break;

	case COMM_ISI_VERSION_GET_RESP:

		if (g_isi_msg_data_get_byte(msg, 1, &major) &&
				g_isi_msg_data_get_byte(msg, 2, &minor)) {
			mux->version.major = major;
			mux->version.minor = minor;
		}
		/* fall through */

	default:
		/*
		 * PN_SIM doesn't support ISI version, but sends a
		 * garbage message as a response. Work around this
		 * modem wart.
		 */
		mux->object = g_isi_msg_object(msg);
		mux->version_pending = FALSE;
		mux->reachable = TRUE;
		break;
	}
	msg->version = &mux->version;
}

static void firewall_notify_handle(GIsiModem *modem, GIsiMessage *msg)
{
	uint8_t id;

	if (!g_isi_msg_data_get_byte(msg, 0, &id))
		return;

	ISIDBG(modem, "firewall blocked message 0x%02X", id);
}

static gboolean isi_callback(GIOChannel *channel, GIOCondition cond,
				gpointer data)
{
	GIsiModem *modem = data;
	int len;
	int fd;

	if (cond & (G_IO_NVAL|G_IO_HUP)) {
		ISIDBG(modem, "Unexpected event on PhoNet channel %p", channel);
		return FALSE;
	}

	fd = g_io_channel_unix_get_fd(channel);
	len = g_isi_phonet_peek_length(channel);

	if (len > 0) {
		struct sockaddr_pn addr;
		uint32_t buf[(len + 3) / 4];

		GIsiServiceMux *mux;
		GIsiMessage msg;
		unsigned key;

		len = g_isi_phonet_read(channel, buf, len, &addr);
		if (len < 2)
			return TRUE;

		msg.addr = &addr;
		msg.error = 0;
		msg.data = buf;
		msg.len = len;

		if (modem->trace != NULL)
			modem->trace(&msg, NULL);

		key = addr.spn_resource;
		mux = g_hash_table_lookup(modem->services,
						GINT_TO_POINTER(key));
		if (mux == NULL) {
			/*
			 * Unfortunately, the FW report has the wrong
			 * resource ID in the N900 modem.
			 */
			if (key == PN_FIREWALL)
				firewall_notify_handle(modem, &msg);

			return TRUE;
		}

		msg.version = &mux->version;

		if (g_isi_msg_id(&msg) == COMMON_MESSAGE)
			common_message_decode(mux, &msg);

		service_dispatch(mux, &msg, fd == modem->ind_fd);
	}
	return TRUE;
}

static gboolean modem_subs_update(gpointer data)
{
	GHashTableIter iter;
	gpointer keyptr, value;

	GIsiModem *modem = data;
	gboolean legacy = modem->flags & GISI_MODEM_FLAG_USE_LEGACY_SUBSCRIBE;
	struct sockaddr_pn commgr = {
		.spn_family = AF_PHONET,
		.spn_resource = PN_COMMGR,
		.spn_dev = modem->device,
	};
	uint8_t msg[4 + 1024] = {
		0,	/* UTID */
		legacy ? PNS_SUBSCRIBED_RESOURCES_IND :
			PNS_SUBSCRIBED_RESOURCES_EXTEND_IND,
		0,	/* Count */
		0,	/* Filler */
	};
	uint8_t count = 0;
	size_t len;

	modem->subs_source = 0;

	g_hash_table_iter_init(&iter, modem->services);

	while (g_hash_table_iter_next(&iter, &keyptr, &value)) {
		GIsiServiceMux *mux = value;

		if (mux->subscriptions == 0)
			continue;

		if (legacy)
			msg[3 + count] = mux->resource;
		else
			/* Resource field is 32bit and Little-endian */
			msg[4 + count * 4 + 3] = mux->resource;

		count++;
	}

	len = legacy ? 3 + count : 4 + count * 4;
	msg[2] = count;

	sendto(modem->ind_fd, msg, len, MSG_NOSIGNAL, (void *) &commgr,
		sizeof(commgr));

	return FALSE;
}

static void modem_subs_update_when_idle(GIsiModem *modem)
{
	if (modem->subs_source > 0)
		return;

	modem->subs_source = g_idle_add(modem_subs_update, modem);
}

static void service_name_register(GIsiServiceMux *mux)
{
	struct sockaddr_pn namesrv = {
		.spn_family = AF_PHONET,
		.spn_resource = PN_NAMESERVICE,
		.spn_dev = mux->modem->device,
	};
	uint8_t msg[] = {
		0, PNS_NAME_ADD_REQ, 0, 0,
		0, 0, 0, mux->resource,	/* 32-bit Big-Endian name */
		0, 0,			/* device/object */
		0, 0,			/* filler */
	};
	uint16_t object = 0;

	if (ioctl(mux->modem->req_fd, SIOCPNGETOBJECT, &object) < 0) {
		ISIDBG(mux->modem, "ioctl(SIOCPNGETOBJECT): %s",
			strerror(errno));
		return;
	}

	/* Fill in the object ID */
	msg[8] = object >> 8;
	msg[9] = object & 0xFF;

	sendto(mux->modem->req_fd, msg, sizeof(msg), MSG_NOSIGNAL,
		(void *) &namesrv, sizeof(namesrv));
}

static void service_name_deregister(GIsiServiceMux *mux)
{
	struct sockaddr_pn namesrv = {
		.spn_family = AF_PHONET,
		.spn_resource = PN_NAMESERVICE,
		.spn_dev = mux->modem->device,
	};
	const uint8_t msg[] = {
		0, PNS_NAME_REMOVE_REQ, 0, 0,
		0, 0, 0, mux->resource,
	};

	sendto(mux->modem->req_fd, msg, sizeof(msg), MSG_NOSIGNAL,
		(void *) &namesrv, sizeof(namesrv));
}

static void pending_destroy(gpointer value, gpointer user)
{
	GIsiPending *op = value;

	if (op == NULL)
		return;

	if (op->timeout > 0)
		g_source_remove(op->timeout);

	if (op->destroy != NULL)
		op->destroy(op->data);

	g_free(op);
}

static void service_finalize(gpointer value)
{
	GIsiServiceMux *mux = value;
	GIsiModem *modem = mux->modem;

	if (mux->subscriptions > 0)
		modem_subs_update_when_idle(modem);

	if (mux->registrations > 0)
		service_name_deregister(mux);

	g_slist_foreach(mux->pending, pending_destroy, NULL);
	g_slist_free(mux->pending);
	g_free(mux);
}

GIsiModem *g_isi_modem_create(unsigned index)
{
	GIsiModem *modem;
	GIOChannel *inds;
	GIOChannel *reqs;

	if (index == 0) {
		errno = ENODEV;
		return NULL;
	}

	modem = g_try_new0(GIsiModem, 1);
	if (modem == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	inds = g_isi_phonet_new(index);
	reqs = g_isi_phonet_new(index);

	if (inds == NULL || reqs == NULL) {
		g_free(modem);
		return NULL;
	}

	modem->req_fd = g_io_channel_unix_get_fd(reqs);
	modem->req_watch = g_io_add_watch(reqs,
					G_IO_IN|G_IO_ERR|G_IO_HUP|G_IO_NVAL,
					isi_callback, modem);
	modem->ind_fd = g_io_channel_unix_get_fd(inds);
	modem->ind_watch = g_io_add_watch(inds,
					G_IO_IN|G_IO_ERR|G_IO_HUP|G_IO_NVAL,
					isi_callback, modem);

	g_io_channel_unref(reqs);
	g_io_channel_unref(inds);

	modem->index = index;
	modem->services = g_hash_table_new_full(g_direct_hash, NULL,
						NULL, service_finalize);

	return modem;
}

GIsiModem *g_isi_modem_create_by_name(const char *name)
{
	return g_isi_modem_create(if_nametoindex(name));
}

void *g_isi_modem_set_userdata(GIsiModem *modem, void *data)
{
	void *old;

	if (modem == NULL)
		return NULL;

	old = modem->opaque;
	modem->opaque = data;

	return old;
}

void *g_isi_modem_get_userdata(GIsiModem *modem)
{
	if (modem == NULL)
		return NULL;

	return modem->opaque;
}

unsigned long g_isi_modem_flags(GIsiModem *modem)
{
	if (modem == NULL)
		return 0;

	return modem->flags;
}

void g_isi_modem_set_flags(GIsiModem *modem, unsigned long flags)
{
	if (modem == NULL)
		return;

	modem->flags = flags;
}

uint8_t g_isi_modem_device(GIsiModem *modem)
{
	if (modem == NULL)
		return 0;

	return modem->device;
}

int g_isi_modem_set_device(GIsiModem *modem, uint8_t remote)
{
	if (modem == NULL)
		return -EINVAL;

	if (remote != PN_DEV_HOST && remote != PN_DEV_MODEM)
		return -EINVAL;

	modem->device = remote;

	return 0;
}

static uint8_t service_next_utid(GIsiServiceMux *mux)
{
	if (mux->last_utid == 0x00 || mux->last_utid == 0xFF)
		return 1;

	return mux->last_utid + 1;
}

static void service_subs_incr(GIsiServiceMux *mux)
{
	GIsiModem *modem = mux->modem;

	mux->subscriptions++;

	if (mux->subscriptions == 1)
		modem_subs_update_when_idle(modem);
}

static void service_subs_decr(GIsiServiceMux *mux)
{
	GIsiModem *modem = mux->modem;

	if (mux->subscriptions == 0)
		return;

	mux->subscriptions--;

	if (mux->subscriptions == 0)
		modem_subs_update_when_idle(modem);
}

static void service_regs_incr(GIsiServiceMux *mux)
{
	mux->registrations++;

	if (mux->registrations == 1)
		service_name_register(mux);
}

static void service_regs_decr(GIsiServiceMux *mux)
{
	if (mux->registrations == 0)
		return;

	mux->registrations--;

	if (mux->registrations == 0)
		service_name_deregister(mux);
}

void g_isi_modem_destroy(GIsiModem *modem)
{
	if (modem == NULL)
		return;

	g_hash_table_remove_all(modem->services);

	if (modem->subs_source > 0) {
		g_source_remove(modem->subs_source);
		modem_subs_update(modem);
	}

	g_hash_table_unref(modem->services);

	if (modem->ind_watch > 0)
		g_source_remove(modem->ind_watch);

	if (modem->req_watch > 0)
		g_source_remove(modem->req_watch);

	g_free(modem);
}

unsigned g_isi_modem_index(GIsiModem *modem)
{
	return modem != NULL ? modem->index : 0;
}

GIsiPending *g_isi_request_send(GIsiModem *modem, uint8_t resource,
					const void *__restrict buf, size_t len,
					unsigned timeout, GIsiNotifyFunc notify,
					void *data, GDestroyNotify destroy)
{
	struct sockaddr_pn dst = {
		.spn_family = AF_PHONET,
		.spn_resource = resource,
		.spn_dev = modem->device,
	};

	return g_isi_request_sendto(modem, &dst, buf, len, timeout, notify,
					data, destroy);
};

GIsiPending *g_isi_request_vsend(GIsiModem *modem, uint8_t resource,
					const struct iovec *__restrict iov,
					size_t iovlen, unsigned timeout,
					GIsiNotifyFunc notify, void *data,
					GDestroyNotify destroy)
{
	struct sockaddr_pn dst = {
		.spn_family = AF_PHONET,
		.spn_resource = resource,
		.spn_dev = modem->device,
	};

	return g_isi_request_vsendto(modem, &dst, iov, iovlen, timeout, notify,
					data, destroy);
}

GIsiPending *g_isi_request_sendto(GIsiModem *modem, struct sockaddr_pn *dst,
					const void *__restrict buf, size_t len,
					unsigned timeout, GIsiNotifyFunc notify,
					void *data, GDestroyNotify destroy)
{
	const struct iovec iov = {
		.iov_base = (void *)buf,
		.iov_len = len,
	};

	return g_isi_request_vsendto(modem, dst, &iov, 1, timeout, notify, data,
					destroy);
}

static void vtrace(struct sockaddr_pn *dst,
			const struct iovec *__restrict iov, size_t iovlen,
			size_t total_len, GIsiNotifyFunc trace)
{
	uint8_t buffer[total_len];
	uint8_t *ptr = buffer;
	GIsiMessage msg = {
		.addr = dst,
		.data = (const void *)buffer,
		.len = total_len,
	};
	size_t i;

	for (i = 0; i < iovlen; i++) {
		memcpy(ptr, iov[i].iov_base, iov[i].iov_len);
		ptr += iov[i].iov_len;
	}

	trace(&msg, NULL);
}

static gboolean resp_timeout(gpointer data)
{
	GIsiPending *op = data;
	GIsiMessage msg = {
		.error = ETIMEDOUT,
	};

	op->timeout = 0;

	pending_remove_and_dispatch(op, &msg);

	return FALSE;
}

GIsiPending *g_isi_request_vsendto(GIsiModem *modem, struct sockaddr_pn *dst,
					const struct iovec *__restrict iov,
					size_t iovlen, unsigned timeout,
					GIsiNotifyFunc notify, void *data,
					GDestroyNotify destroy)
{
	struct iovec _iov[1 + iovlen];
	struct msghdr msg = {
		.msg_name = (void *)dst,
		.msg_namelen = sizeof(struct sockaddr_pn),
		.msg_iov = _iov,
		.msg_iovlen = 1 + iovlen,
		.msg_control = NULL,
		.msg_controllen = 0,
		.msg_flags = 0,
	};
	ssize_t ret;
	size_t i, len;

	GIsiServiceMux *mux;
	GIsiPending *resp;

	if (modem == NULL) {
		errno = EINVAL;
		return NULL;
	}

	mux = service_get(modem, dst->spn_resource);
	if (mux == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	resp = g_try_new0(GIsiPending, 1);
	if (resp == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	resp->type = GISI_MESSAGE_TYPE_RESP;
	resp->utid = service_next_utid(mux);
	resp->service = mux;
	resp->notify = notify;
	resp->destroy = destroy;
	resp->data = data;

	if (g_slist_find_custom(mux->pending, resp, utid_equal)) {
		/*
		 * FIXME: perhaps retry with randomized access after
		 * initial miss. Although if the rate at which
		 * requests are sent is so high that the unique
		 * transaction ID wraps, it's likely there is
		 * something wrong and we might as well fail here.
		 */
		ISIDBG(modem, "ERROR: UTID wrapped, modem busy");
		errno = EBUSY;
		goto error;
	}

	_iov[0].iov_base = &resp->utid;
	_iov[0].iov_len = 1;

	for (i = 0, len = 1; i < iovlen; i++) {
		_iov[1 + i] = iov[i];
		len += iov[i].iov_len;
	}

	if (modem->trace != NULL)
		vtrace(dst, _iov, 1 + iovlen, len, modem->trace);

	ret = sendmsg(modem->req_fd, &msg, MSG_NOSIGNAL);
	if (ret == -1)
		goto error;

	if (ret != (ssize_t)len) {
		errno = EMSGSIZE;
		goto error;
	}

	mux->pending = g_slist_prepend(mux->pending, resp);

	if (timeout > 0)
		resp->timeout = g_timeout_add_seconds(timeout, resp_timeout,
							resp);

	mux->last_utid = resp->utid;
	return resp;

error:
	g_free(resp);
	return NULL;
}

uint8_t g_isi_request_utid(GIsiPending *resp)
{
	return resp != NULL ? resp->utid : 0;
}

void g_isi_pending_remove(GIsiPending *op)
{
	if (op == NULL)
		return;

	if (op->type == GISI_MESSAGE_TYPE_IND)
		service_subs_decr(op->service);

	if (op->type == GISI_MESSAGE_TYPE_REQ)
		service_regs_decr(op->service);

	if (op->type == GISI_MESSAGE_TYPE_RESP && op->notify != NULL) {
		GIsiMessage msg = {
			.error = ESHUTDOWN,
		};

		pending_remove_and_dispatch(op, &msg);
		return;
	}

	op->service->pending = g_slist_remove(op->service->pending, op);

	pending_destroy(op, NULL);
}

static void foreach_destroy(GIsiPending *op)
{
	if (op->type == GISI_MESSAGE_TYPE_IND)
		service_subs_decr(op->service);

	if (op->type == GISI_MESSAGE_TYPE_REQ)
		service_regs_decr(op->service);

	if (op->type == GISI_MESSAGE_TYPE_RESP && op->notify != NULL) {
		GIsiMessage msg = {
			.error = ESHUTDOWN,
		};

		pending_dispatch(op, &msg);
	}

	pending_destroy(op, NULL);
}

void g_isi_pending_set_owner(GIsiPending *op, gpointer owner)
{
	if (op == NULL)
		return;

	op->owner = owner;
}

void g_isi_remove_pending_by_owner(GIsiModem *modem, uint8_t resource,
					gpointer owner)
{
	GIsiServiceMux *mux;
	GSList *l;
	GSList *next;
	GIsiPending *op;
	GSList *owned = NULL;

	mux = service_get(modem, resource);
	if (mux == NULL)
		return;

	for (l = mux->pending; l != NULL; l = next) {
		next = l->next;
		op = l->data;

		if (op->owner != owner)
			continue;

		mux->pending = g_slist_remove_link(mux->pending, l);

		l->next = owned;
		owned = l;
	}

	for (l = owned; l != NULL; l = l->next) {
		op = l->data;

		foreach_destroy(op);
	}

	g_slist_free(owned);
}

GIsiPending *g_isi_ntf_subscribe(GIsiModem *modem, uint8_t resource,
					uint8_t msgid, GIsiNotifyFunc notify,
					void *data, GDestroyNotify destroy)
{
	GIsiServiceMux *mux;
	GIsiPending *ntf;

	mux = service_get(modem, resource);
	if (mux == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ntf = g_try_new0(GIsiPending, 1);
	if (ntf == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ntf->type = GISI_MESSAGE_TYPE_NTF;
	ntf->service = mux;
	ntf->notify = notify;
	ntf->data = data;
	ntf->destroy = destroy;
	ntf->msgid = msgid;

	mux->pending = g_slist_append(mux->pending, ntf);

	ISIDBG(modem, "Subscribed to %s (%p) [res=0x%02X, id=0x%02X]",
		pend_type_to_str(ntf->type), ntf, resource, msgid);

	return ntf;
}

GIsiPending *g_isi_service_bind(GIsiModem *modem, uint8_t resource,
				uint8_t msgid, GIsiNotifyFunc notify,
				void *data, GDestroyNotify destroy)
{
	GIsiServiceMux *mux;
	GIsiPending *srv;

	mux = service_get(modem, resource);
	if (mux == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	srv = g_try_new0(GIsiPending, 1);
	if (srv == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	srv->type = GISI_MESSAGE_TYPE_REQ;
	srv->service = mux;
	srv->notify = notify;
	srv->data = data;
	srv->destroy = destroy;
	srv->msgid = msgid;

	mux->pending = g_slist_append(mux->pending, srv);

	ISIDBG(modem, "Bound service for %s (%p) [res=0x%02X, id=0x%02X]",
		pend_type_to_str(srv->type), srv, resource, msgid);

	service_regs_incr(mux);

	return srv;
}

GIsiPending *g_isi_ind_subscribe(GIsiModem *modem, uint8_t resource,
					uint8_t msgid, GIsiNotifyFunc notify,
					void *data, GDestroyNotify destroy)
{
	GIsiServiceMux *mux;
	GIsiPending *ind;

	mux = service_get(modem, resource);
	if (mux == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ind = g_try_new0(GIsiPending, 1);
	if (ind == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ind->type = GISI_MESSAGE_TYPE_IND;
	ind->service = mux;
	ind->notify = notify;
	ind->data = data;
	ind->destroy = destroy;
	ind->msgid = msgid;

	mux->pending = g_slist_append(mux->pending, ind);

	ISIDBG(modem, "Subscribed for %s (%p) [res=0x%02X, id=0x%02X]",
		pend_type_to_str(ind->type), ind, resource, msgid);

	service_subs_incr(mux);

	return ind;
}

int g_isi_response_send(GIsiModem *modem, const GIsiMessage *req,
				const void *__restrict buf, size_t len)
{
	const struct iovec iov = {
		.iov_base = (void *)buf,
		.iov_len = len,
	};

	return g_isi_response_vsend(modem, req, &iov, 1);
}

int g_isi_response_vsend(GIsiModem *modem, const GIsiMessage *req,
				const struct iovec *__restrict iov,
				size_t iovlen)
{
	struct iovec _iov[1 + iovlen];
	uint8_t utid;
	size_t i;

	utid = g_isi_msg_utid(req);

	_iov[0].iov_base = &utid;
	_iov[0].iov_len = 1;

	for (i = 0; i < iovlen; i++)
		_iov[1 + i] = iov[i];

	return g_isi_modem_vsendto(modem, req->addr, _iov, 1 + iovlen);
}

int g_isi_modem_send(GIsiModem *modem, uint8_t resource,
			const void *__restrict buf, size_t len)
{
	struct sockaddr_pn dst = {
		.spn_family = AF_PHONET,
		.spn_resource = resource,
		.spn_dev = modem->device,
	};

	return g_isi_modem_sendto(modem, &dst, buf, len);
}

int g_isi_modem_vsend(GIsiModem *modem, uint8_t resource,
				const struct iovec *__restrict iov,
				size_t iovlen)
{
	struct sockaddr_pn dst = {
		.spn_family = AF_PHONET,
		.spn_resource = resource,
		.spn_dev = modem->device,
	};

	return g_isi_modem_vsendto(modem, &dst, iov, iovlen);
}

int g_isi_modem_sendto(GIsiModem *modem, struct sockaddr_pn *dst,
			const void *__restrict buf, size_t len)
{
	const struct iovec iov = {
		.iov_base = (void *)buf,
		.iov_len = len,
	};

	return g_isi_modem_vsendto(modem, dst, &iov, 1);
}

int g_isi_modem_vsendto(GIsiModem *modem, struct sockaddr_pn *dst,
				const struct iovec *__restrict iov,
				size_t iovlen)
{
	struct msghdr msg = {
		.msg_name = (void *)dst,
		.msg_namelen = sizeof(struct sockaddr_pn),
		.msg_iov = (struct iovec *)iov,
		.msg_iovlen = iovlen,
		.msg_control = NULL,
		.msg_controllen = 0,
		.msg_flags = 0,
	};
	ssize_t ret;
	size_t i, len;
	GIsiServiceMux *mux;

	if (modem == NULL)
		return -EINVAL;

	mux = service_get(modem, dst->spn_resource);
	if (mux == NULL)
		return -ENOMEM;

	for (i = 0, len = 0; i < iovlen; i++)
		len += iov[i].iov_len;

	if (modem->trace != NULL)
		vtrace(dst, iov, iovlen, len, modem->trace);

	ret = sendmsg(modem->req_fd, &msg, MSG_NOSIGNAL);
	if (ret == -1)
		return -errno;

	if (ret != (ssize_t)len)
		return -EMSGSIZE;

	return 0;
}

void g_isi_modem_set_trace(GIsiModem *modem, GIsiNotifyFunc trace)
{
	if (modem == NULL)
		return;

	modem->trace = trace;
}

void g_isi_modem_set_debug(GIsiModem *modem, GIsiDebugFunc debug)
{
	if (modem == NULL)
		return;

	modem->debug = debug;
}

static int version_get_send(GIsiModem *modem, GIsiPending *ping)
{
	GIsiServiceMux *mux = ping->service;
	struct sockaddr_pn dst = {
		.spn_family = AF_PHONET,
		.spn_resource = mux->resource,
		.spn_dev = modem->device,
	};
	uint8_t msg[] = {
		ping->utid,	/* UTID */
		COMMON_MESSAGE,
		COMM_ISI_VERSION_GET_REQ,
		0,		/* Filler */
	};
	ssize_t ret;

	if (g_slist_find_custom(mux->pending, ping, utid_equal))
		return -EBUSY;

	ret = sendto(modem->req_fd, msg, sizeof(msg), MSG_NOSIGNAL,
			(void *)&dst, sizeof(dst));

	if (ret == -1)
		return -errno;

	if (ret != (ssize_t)sizeof(msg))
		return -EMSGSIZE;

	mux->last_utid = ping->utid;
	mux->version_pending = TRUE;
	return 0;
}

static gboolean reachable_notify(gpointer data)
{
	GIsiPending *pong = data;
	GIsiServiceMux *mux = pong->service;

	struct sockaddr_pn addr = {
		.spn_resource = mux->resource,
		.spn_dev = mux->object >> 8,
		.spn_obj = mux->object & 0xff,
	};
	GIsiMessage msg = {
		.version = &mux->version,
		.addr = &addr,
	};

	pending_remove_and_dispatch(pong, &msg);

	return FALSE;
}

GIsiPending *g_isi_resource_ping(GIsiModem *modem, uint8_t resource,
					GIsiNotifyFunc notify, void *data,
					GDestroyNotify destroy)
{
	GIsiServiceMux *mux;
	GIsiPending *ping;
	int ret;

	mux = service_get(modem, resource);
	if (mux == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ping = g_try_new0(GIsiPending, 1);
	if (ping == NULL) {
		errno = ENOMEM;
		return NULL;
	}

	ping->type = GISI_MESSAGE_TYPE_COMMON;
	ping->utid = service_next_utid(mux);
	ping->service = mux;
	ping->notify = notify;
	ping->data = data;
	ping->destroy = destroy;
	ping->msgid = COMM_ISI_VERSION_GET_REQ;

	if (mux->reachable) {
		g_idle_add(reachable_notify, ping);
		return ping;
	}

	if (!mux->version_pending) {
		ret = version_get_send(modem, ping);
		if (ret < 0) {
			g_free(ping);
			errno = ret;
			return NULL;
		}
		mux->last_utid = ping->utid;
	}

	ping->timeout = g_timeout_add_seconds(COMMON_TIMEOUT, resp_timeout,
						ping);
	mux->pending = g_slist_prepend(mux->pending, ping);
	mux->version_pending = TRUE;

	ISIDBG(modem, "Ping sent %s (%p) [res=0x%02X]",
		pend_type_to_str(ping->type), ping, resource);

	return ping;
}
