/* Faulty Block Device (fault injection proxy), by D.C. van Moolenbroek */
#include <stdlib.h>
#include <minix/drivers.h>
#include <minix/blockdriver.h>
#include <minix/drvlib.h>
#include <minix/ioctl.h>
#include <sys/ioc_fbd.h>
#include <minix/ds.h>
#include <minix/optset.h>
#include <assert.h>

#include "rule.h"

/* Constants. */
#define BUF_SIZE (NR_IOREQS * CLICK_SIZE)	/* 256k */

/* Function declarations. */
static int fbd_open(dev_t minor, int access);
static int fbd_close(dev_t minor);
static int fbd_transfer(dev_t minor, int do_write, u64_t position,
	endpoint_t endpt, iovec_t *iov, unsigned int nr_req, int flags);
static int fbd_ioctl(dev_t minor, unsigned int request, endpoint_t endpt,
	cp_grant_id_t grant);

/* Variables. */
static char *fbd_buf;			/* scratch buffer */

static char driver_label[32] = "";	/* driver DS label */
static dev_t driver_minor = -1;	/* driver's partition minor to use */
static endpoint_t driver_endpt;	/* driver endpoint */

/* Entry points to this driver. */
static struct blockdriver fbd_dtab = {
	BLOCKDRIVER_TYPE_OTHER,	/* do not handle partition requests */
	fbd_open,		/* open or mount request, initialize device */
	fbd_close,		/* release device */
	fbd_transfer,		/* do the I/O */
	fbd_ioctl,		/* perform I/O control request */
	NULL,			/* nothing to clean up */
	NULL,			/* we will not be asked about partitions */
	NULL,			/* we will not be asked for geometry */
	NULL,			/* ignore leftover hardware interrupts */
	NULL,			/* ignore alarms */
	NULL,			/* ignore other messages */
	NULL			/* no multithreading support */
};

/* Options supported by this driver. */
static struct optset optset_table[] = {
	{ "label",	OPT_STRING,	driver_label,	sizeof(driver_label) },
	{ "minor",	OPT_INT,	&driver_minor,	10		     },
	{ NULL,		0,		NULL,		0		     }
};

/*===========================================================================*
 *				sef_cb_init_fresh			     *
 *===========================================================================*/
static int sef_cb_init_fresh(int type, sef_init_info_t *UNUSED(info))
{
	clock_t uptime;
	int r;

	/* Parse the given parameters. */
	if (env_argc > 1)
		optset_parse(optset_table, env_argv[1]);

	if (driver_label[0] == '\0')
		panic("no driver label given");

	if (ds_retrieve_label_endpt(driver_label, &driver_endpt))
		panic("unable to resolve driver label");

	if (driver_minor > 255)
		panic("no or invalid driver minor given");

#if DEBUG
	printf("FBD: driver label '%s' (endpt %d), minor %d\n",
		driver_label, driver_endpt, driver_minor);
#endif

	/* Initialize resources. */
	fbd_buf = alloc_contig(BUF_SIZE, 0, NULL);

	assert(fbd_buf != NULL);

	if ((r = getuptime(&uptime)) != OK)
		panic("getuptime failed (%d)\n", r);

	srand48(uptime);

	/* Announce we are up! */
	blockdriver_announce(type);

	return OK;
}

/*===========================================================================*
 *				sef_cb_signal_handler			     *
 *===========================================================================*/
static void sef_cb_signal_handler(int signo)
{
	/* Terminate immediately upon receiving a SIGTERM. */
	if (signo != SIGTERM) return;

#if DEBUG
	printf("FBD: shutting down\n");
#endif

	/* Clean up resources. */
	free_contig(fbd_buf, BUF_SIZE);

	exit(0);
}

/*===========================================================================*
 *				sef_local_startup			     *
 *===========================================================================*/
static void sef_local_startup(void)
{
	/* Register init callbacks. */
	sef_setcb_init_fresh(sef_cb_init_fresh);
	sef_setcb_init_restart(sef_cb_init_fresh);
	sef_setcb_init_lu(sef_cb_init_fresh);

	/* Register signal callback. */
	sef_setcb_signal_handler(sef_cb_signal_handler);

	/* Let SEF perform startup. */
	sef_startup();
}

/*===========================================================================*
 *				main					     *
 *===========================================================================*/
int main(int argc, char **argv)
{
	/* SEF local startup. */
	env_setargs(argc, argv);
	sef_local_startup();

	/* Call the generic receive loop. */
	blockdriver_task(&fbd_dtab);

	return OK;
}

/*===========================================================================*
 *				fbd_open				     *
 *===========================================================================*/
static int fbd_open(dev_t UNUSED(minor), int access)
{
	/* Open a device. */
	message m;
	int r;

	/* We simply forward this request to the real driver. */
	memset(&m, 0, sizeof(m));
	m.m_type = BDEV_OPEN;
	m.BDEV_MINOR = driver_minor;
	m.BDEV_ACCESS = access;
	m.BDEV_ID = 0;

	if ((r = sendrec(driver_endpt, &m)) != OK)
		panic("sendrec to driver failed (%d)\n", r);

	if (m.m_type != BDEV_REPLY)
		panic("invalid reply from driver (%d)\n", m.m_type);

	return m.BDEV_STATUS;
}

/*===========================================================================*
 *				fbd_close				     *
 *===========================================================================*/
static int fbd_close(dev_t UNUSED(minor))
{
	/* Close a device. */
	message m;
	int r;

	/* We simply forward this request to the real driver. */
	memset(&m, 0, sizeof(m));
	m.m_type = BDEV_CLOSE;
	m.BDEV_MINOR = driver_minor;
	m.BDEV_ID = 0;

	if ((r = sendrec(driver_endpt, &m)) != OK)
		panic("sendrec to driver failed (%d)\n", r);

	if (m.m_type != BDEV_REPLY)
		panic("invalid reply from driver (%d)\n", m.m_type);

	return m.BDEV_STATUS;
}

/*===========================================================================*
 *				fbd_ioctl				     *
 *===========================================================================*/
static int fbd_ioctl(dev_t UNUSED(minor), unsigned int request,
	endpoint_t endpt, cp_grant_id_t grant)
{
	/* Handle an I/O control request. */
	cp_grant_id_t gid;
	message m;
	int r;

	/* We only handle the FBD requests, and pass on everything else. */
	switch (request) {
	case FBDCADDRULE:
	case FBDCDELRULE:
	case FBDCGETRULE:
		return rule_ctl(request, endpt, grant);
	}

	assert(grant != GRANT_INVALID);

	gid = cpf_grant_indirect(driver_endpt, endpt, grant);
	assert(gid != GRANT_INVALID);

	memset(&m, 0, sizeof(m));
	m.m_type = BDEV_IOCTL;
	m.BDEV_MINOR = driver_minor;
	m.BDEV_REQUEST = request;
	m.BDEV_GRANT = gid;
	m.BDEV_ID = 0;

	if ((r = sendrec(driver_endpt, &m)) != OK)
		panic("sendrec to driver failed (%d)\n", r);

	if (m.m_type != BDEV_REPLY)
		panic("invalid reply from driver (%d)\n", m.m_type);

	cpf_revoke(gid);

	return m.BDEV_STATUS;
}

/*===========================================================================*
 *				fbd_transfer_direct			     *
 *===========================================================================*/
static ssize_t fbd_transfer_direct(int do_write, u64_t position,
	endpoint_t endpt, iovec_t *iov, unsigned int count, int flags)
{
	/* Forward the entire transfer request, without any intervention. */
	iovec_s_t iovec[NR_IOREQS];
	cp_grant_id_t grant;
	message m;
	int i, r;

	for (i = 0; i < count; i++) {
		iovec[i].iov_size = iov[i].iov_size;
		iovec[i].iov_grant = cpf_grant_indirect(driver_endpt, endpt,
			iov[i].iov_addr);
		assert(iovec[i].iov_grant != GRANT_INVALID);
	}

	grant = cpf_grant_direct(driver_endpt, (vir_bytes) iovec,
		count * sizeof(iovec[0]), CPF_READ);
	assert(grant != GRANT_INVALID);

	m.m_type = do_write ? BDEV_SCATTER : BDEV_GATHER;
	m.BDEV_MINOR = driver_minor;
	m.BDEV_COUNT = count;
	m.BDEV_GRANT = grant;
	m.BDEV_FLAGS = flags;
	m.BDEV_ID = 0;
	m.BDEV_POS_LO = ex64lo(position);
	m.BDEV_POS_HI = ex64hi(position);

	if ((r = sendrec(driver_endpt, &m)) != OK)
		panic("sendrec to driver failed (%d)\n", r);

	if (m.m_type != BDEV_REPLY)
		panic("invalid reply from driver (%d)\n", m.m_type);

	cpf_revoke(grant);

	for (i = 0; i < count; i++)
		cpf_revoke(iovec[i].iov_grant);

	return m.BDEV_STATUS;
}

/*===========================================================================*
 *				fbd_transfer_copy			     *
 *===========================================================================*/
static ssize_t fbd_transfer_copy(int do_write, u64_t position,
	endpoint_t endpt, iovec_t *iov, unsigned int count, size_t size,
	int flags)
{
	/* Interpose on the request. */
	iovec_s_t iovec[NR_IOREQS];
	struct vscp_vec vscp_vec[SCPVEC_NR];
	cp_grant_id_t grant;
	size_t off, len;
	message m;
	char *ptr;
	int i, j, r;
	ssize_t rsize;

	assert(count > 0 && count <= SCPVEC_NR);

	if (size > BUF_SIZE) {
		printf("FBD: allocating memory for %d bytes\n", size);

		ptr = alloc_contig(size, 0, NULL);

		assert(ptr != NULL);
	}
	else ptr = fbd_buf;

	/* For write operations, first copy in the data to write. */
	if (do_write) {
		for (i = off = 0; i < count; i++) {
			len = iov[i].iov_size;

			vscp_vec[i].v_from = endpt;
			vscp_vec[i].v_to = SELF;
			vscp_vec[i].v_gid = iov[i].iov_addr;
			vscp_vec[i].v_offset = 0;
			vscp_vec[i].v_addr = (vir_bytes) (ptr + off);
			vscp_vec[i].v_bytes = len;

			off += len;
		}

		if ((r = sys_vsafecopy(vscp_vec, i)) != OK)
			panic("vsafecopy failed (%d)\n", r);

		/* Trigger write hook. */
		rule_io_hook(ptr, size, position, FBD_FLAG_WRITE);
	}

	/* Allocate grants for the data, in the same chunking as the original
	 * vector. This avoids performance fluctuations with bad hardware as
	 * observed with the filter driver.
	 */
	for (i = off = 0; i < count; i++) {
		len = iov[i].iov_size;

		iovec[i].iov_size = len;
		iovec[i].iov_grant = cpf_grant_direct(driver_endpt,
			(vir_bytes) (ptr + off), len,
			do_write ? CPF_READ : CPF_WRITE);
		assert(iovec[i].iov_grant != GRANT_INVALID);

		off += len;
	}

	grant = cpf_grant_direct(driver_endpt, (vir_bytes) iovec,
		count * sizeof(iovec[0]), CPF_READ);
	assert(grant != GRANT_INVALID);

	m.m_type = do_write ? BDEV_SCATTER : BDEV_GATHER;
	m.BDEV_MINOR = driver_minor;
	m.BDEV_COUNT = count;
	m.BDEV_GRANT = grant;
	m.BDEV_FLAGS = flags;
	m.BDEV_ID = 0;
	m.BDEV_POS_LO = ex64lo(position);
	m.BDEV_POS_HI = ex64hi(position);

	if ((r = sendrec(driver_endpt, &m)) != OK)
		panic("sendrec to driver failed (%d)\n", r);

	if (m.m_type != BDEV_REPLY)
		panic("invalid reply from driver (%d)\n", m.m_type);

	cpf_revoke(grant);

	for (i = 0; i < count; i++)
		cpf_revoke(iovec[i].iov_grant);

	/* For read operations, finish by copying out the data read. */
	if (!do_write) {
		/* Trigger read hook. */
		rule_io_hook(ptr, size, position, FBD_FLAG_READ);

		/* Upon success, copy back whatever has been processed. */
		rsize = m.BDEV_STATUS;
		for (i = j = off = 0; rsize > 0 && i < count; i++) {
			len = MIN(rsize, iov[i].iov_size);

			vscp_vec[j].v_from = SELF;
			vscp_vec[j].v_to = endpt;
			vscp_vec[j].v_gid = iov[i].iov_addr;
			vscp_vec[j].v_offset = 0;
			vscp_vec[j].v_addr = (vir_bytes) (ptr + off);
			vscp_vec[j].v_bytes = len;

			off += len;
			rsize -= len;
			j++;
		}

		if (j > 0 && (r = sys_vsafecopy(vscp_vec, j)) != OK)
			panic("vsafecopy failed (%d)\n", r);
	}

	if (ptr != fbd_buf)
		free_contig(ptr, size);

	return m.BDEV_STATUS;
}

/*===========================================================================*
 *				fbd_transfer				     *
 *===========================================================================*/
static int fbd_transfer(dev_t UNUSED(minor), int do_write, u64_t position,
	endpoint_t endpt, iovec_t *iov, unsigned int nr_req, int flags)
{
	/* Transfer data from or to the device. */
	unsigned count;
	size_t size, osize;
	int i, hooks;
	ssize_t r;

	/* Compute the total size of the request. */
	for (size = i = 0; i < nr_req; i++)
		size += iov[i].iov_size;

	osize = size;
	count = nr_req;

	hooks = rule_find(position, size,
		do_write ? FBD_FLAG_WRITE : FBD_FLAG_READ);

#if DEBUG
	printf("FBD: %s operation for pos %"PRIx64" size %u -> hooks %x\n",
		do_write ? "write" : "read", position, size, hooks);
#endif

	if (hooks & PRE_HOOK)
		rule_pre_hook(iov, &count, &size, &position);

	if (count > 0) {
		if (hooks & IO_HOOK) {
			r = fbd_transfer_copy(do_write, position, endpt, iov,
				count, size, flags);
		} else {
			r = fbd_transfer_direct(do_write, position, endpt, iov,
				count, flags);
		}
	}
	else r = 0;

	if (hooks & POST_HOOK)
		rule_post_hook(osize, &r);

#if DEBUG
	printf("FBD: returning %d\n", r);
#endif

	return r;
}
