/*
 * Block driver for Multi Media Cards (MMC).
 */
/* kernel headers */
#include <minix/syslib.h>
#include <minix/driver.h>
#include <minix/blockdriver.h>
#include <minix/drvlib.h>

/* system headers */
#include <sys/ioc_disk.h>	/* disk IOCTL's */

/* usr headers */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <signal.h>

/* local headers */
#include "mmchost.h"
#include "mmclog.h"

/* used for logging */
static struct mmclog log = {
	.name = "mmc_block",
	.log_level = LEVEL_DEBUG,
	.log_func = default_log
};

/* holding the current host controller */
static struct mmc_host host;

/*@TODO REMOVE THIS */
void
read_tsc_64(u64_t * t)
{
}

u32_t
tsc_64_to_micros(u64_t tsc)
{
	return 0;
}

#define SUB_PER_DRIVE           (NR_PARTITIONS * NR_PARTITIONS)
#define NR_SUBDEVS              (MAX_DRIVES * SUB_PER_DRIVE)

/* When passing data over a grant one needs to pass
 * a buffer to sys_safecopy copybuff is used for that*/
#define COPYBUFF_SIZE 0x1000	/* 4k buff */
static unsigned char copybuff[COPYBUFF_SIZE];

static struct sd_slot *get_slot(dev_t minor);

/* Prototypes for the block device */
static int block_open(dev_t minor, int access);
static int block_close(dev_t minor);
static int block_transfer(dev_t minor,
    int do_write,
    u64_t position,
    endpoint_t endpt, iovec_t * iov, unsigned int nr_req, int flags);

static int block_ioctl(dev_t minor,
    unsigned int request, endpoint_t endpt, cp_grant_id_t grant);
static struct device *block_part(dev_t minor);

/* System even handling */
static void sef_local_startup();
static int block_system_event_cb(int type, sef_init_info_t * info);
static void block_signal_handler_cb(int signo);

static int apply_env();

#if 0
/* set the global logging level */
static void set_log_level(int level);
#endif

/* Entry points for the BLOCK driver. */
static struct blockdriver mmc_driver = {
	BLOCKDRIVER_TYPE_DISK,	/* handle partition requests */
	block_open,		/* open or mount */
	block_close,		/* on a close */
	block_transfer,		/* does the I/O */
	block_ioctl,		/* ioclt's */
	NULL,			/* no need to clean up (yet) */
	block_part,		/* return partition information */
	NULL,			/* no geometry */
	NULL,			/* no interrupt processing */
	NULL,			/* no alarm processing */
	NULL,			/* no processing of other messages */
	NULL			/* no threading support */
};

static int
apply_env()
{
	/* apply the env setting passed to this driver parameters accepted
	 * log_level=[0-4] (NONE,WARNING,INFO,DEBUG,TRACE) instance=[0-3]
	 * instance/bus number to use for this driver Passing these arguments
	 * is done when starting the driver using the service command in the
	 * following way service up /sbin/mmc -args "log_level=2 instance=1 
	 * driver=dummy" -dev /dev/c2d0 */
	char driver[16];
	memset(driver, '\0', 16);
	(void) env_get_param("driver", driver, 16);
	if (strlen(driver) == 0
	    || strncmp(driver, "mmchs", strlen("mmchs") + 1) == 0) {
		/* early init of host mmc host controller. This code should
		 * depend on knowing the hardware that is running bellow. */
		host_initialize_host_structure_mmchs(&host);
	} else if (strncmp(driver, "dummy", strlen("dummy") + 1) == 0) {
		host_initialize_host_structure_dummy(&host);
	} else {
		mmc_log_warn(&log, "Unknown driver %s\n", driver);
	}
#if 0
	long v;
	/* The following code(env_parse) uses strtol.c and needs __aeabi_idiv */
	/* @TODO: re-enable this function when __aeabi_idiv will be present */
	/* Initialize the verbosity level. */
	v = 0;
	if (env_parse("log_level", "d", 0, &v, LEVEL_NONE,
		LEVEL_TRACE) == EP_SET) {
		set_log_level(v);
	}

	/* Find out which driver instance we are. */
	v = 0;
	env_parse("instance", "d", 0, &v, 0, 3);
	if (host.host_set_instance(&host, v)) {
		mmc_log_warn(&log, "Failed to set mmc instance to  %d\n", v);
		return -1;	/* NOT OK */
	}
#endif
	return OK;
}

;

/*===========================================================================*
 *                    block_open                                             *
 *===========================================================================*/
static int
block_open(dev_t minor, int access)
{
	struct sd_slot *slot;
	slot = get_slot(minor);
	int i, j;
	int part_count, sub_part_count;

	i = j = part_count = sub_part_count = 0;

	if (!slot) {
		mmc_log_debug(&log,
		    "Not handling open on non existing slot\n");
		return EIO;
	}

	assert(slot->host != NULL);

	if (!slot->host->card_detect(slot)) {
		mmc_log_debug(&log, "No card inserted in the SD slot\n");
		return EIO;
	}

	/* If we are already open just increase the open count and return */
	if (slot->card.state == SD_MODE_DATA_TRANSFER_MODE) {
		assert(slot->card.open_ct >= 0);
		slot->card.open_ct++;
		mmc_log_trace(&log, "increased open count to %d\n",
		    slot->card.open_ct);
		return OK;
	}

	/* We did not have an sd-card inserted so we are going to probe for it 
	 */
	mmc_log_debug(&log, "First open on (%d)\n", minor);
	if (!host.card_initialize(slot)) {
		// * TODO: set card state to INVALID until removed? */
		return EIO;
	}

	partition(&mmc_driver, 0 /* first card on bus */ , P_PRIMARY,
	    0 /* atapi device?? */ );

	mmc_log_debug(&log, "descr \toffset(bytes)      size(bytes)\n", minor);

	mmc_log_debug(&log, "disk %d\t0x%016llx 0x%016llx\n", i,
	    slot->card.part[0].dv_base, slot->card.part[0].dv_size);
	for (i = 1; i < 5; i++) {
		if (slot->card.part[i].dv_size == 0)
			continue;
		part_count++;
		mmc_log_debug(&log, "part %d\t0x%016llx 0x%016llx\n", i,
		    slot->card.part[i].dv_base, slot->card.part[i].dv_size);
		for (j = 0; j < 4; j++) {
			if (slot->card.subpart[(i - 1) * 4 + j].dv_size == 0)
				continue;
			sub_part_count++;
			mmc_log_debug(&log,
			    " sub %d/%d\t0x%016llx 0x%016llx\n", i, j,
			    slot->card.subpart[(i - 1) * 4 + j].dv_base,
			    slot->card.subpart[(i - 1) * 4 + j].dv_size);
		}
	}
	mmc_log_info(&log, "Found %d partitions and %d sub partitions\n",
	    part_count, sub_part_count);
	slot->card.open_ct++;
	assert(slot->card.open_ct == 1);
	return OK;
}

/*===========================================================================*
 *                    block_close                                            *
 *===========================================================================*/
static int
block_close(dev_t minor)
{
	struct sd_slot *slot;

	slot = get_slot(minor);
	if (!slot) {
		mmc_log_debug(&log,
		    "Not handling open on non existing slot\n");
		return EIO;
	}

	/* if we arrived here we expect a card to be present, we will need do
	 * deal with removal later */
	assert(slot->host != NULL);
	assert(slot->card.open_ct >= 1);

	/* If this is not the last open count simply decrease the counter and
	 * return */
	if (slot->card.open_ct > 1) {
		slot->card.open_ct--;
		mmc_log_trace(&log, "decreased open count to %d\n",
		    slot->card.open_ct);
		return OK;
	}

	assert(slot->card.open_ct == 1);
	mmc_log_debug(&log,
	    "freeing the block device as it is no longer used\n");

	/* release the card as check the open_ct should be 0 */
	slot->host->card_release(&slot->card);
	assert(slot->card.open_ct == 0);
	return OK;
}

static int
copyto(endpoint_t dst_e,
    cp_grant_id_t gr_id, vir_bytes offset, vir_bytes address, size_t bytes)
{
	/* Helper function that used memcpy to copy data when the endpoint ==
	 * SELF */
	if (dst_e == SELF) {
		memcpy((char *) gr_id + offset, (char *) address, bytes);
		return OK;
	} else {
		/* Read io_size bytes from our data at the correct * offset
		 * and write it to the output buffer at 0 */
		return sys_safecopyto(dst_e, gr_id, offset, address, bytes);
	}
}

static int
copyfrom(endpoint_t src_e,
    cp_grant_id_t gr_id, vir_bytes offset, vir_bytes address, size_t bytes)
{
	/* Helper function that used memcpy to copy data when the endpoint ==
	 * SELF */
	if (src_e == SELF) {
		memcpy((char *) address, (char *) gr_id + offset, bytes);
		return OK;
	} else {
		return sys_safecopyfrom(src_e, gr_id, offset, address, bytes);
	}
}

/*===========================================================================*
 *                    block_transfer                                         *
 *===========================================================================*/
static int
block_transfer(dev_t minor,	/* minor device number */
    int do_write,		/* read or write? */
    u64_t position,		/* offset on device to read or write */
    endpoint_t endpt,		/* process doing the request */
    iovec_t * iov,		/* pointer to read or write request vector */
    unsigned int nr_req,	/* length of request vector */
    int flags			/* transfer flags */
    )
{
	unsigned long counter;
	iovec_t *ciov;		/* Current IO Vector */
	struct device *dev;	/* The device used */
	struct sd_slot *slot;	/* The sd slot the requests is pointed to */
	vir_bytes io_size;	/* Size to read/write to/from the iov */
	vir_bytes io_offset;	/* Size to read/write to/from the iov */
	vir_bytes bytes_written;

	int r, blk_size, i;

	/* Get the current "device" geometry */
	dev = block_part(minor);
	if (dev == NULL) {
		mmc_log_warn(&log,
		    "Transfer requested on unknown device minor(%d)\n", minor);
		/* Unknown device */
		return ENXIO;
	}
	mmc_log_trace(&log, "I/O %d %s 0x%llx\n", minor,
	    (do_write) ? "Write" : "Read", position);

	slot = get_slot(minor);
	assert(slot);

	if (slot->card.blk_size == 0) {
		mmc_log_warn(&log, "Request on a card with block size of 0\n");
		return EINVAL;
	}
	if (slot->card.blk_size > COPYBUFF_SIZE) {
		mmc_log_warn(&log,
		    "Card block size (%d) exceeds internal buffer size %d\n",
		    slot->card.blk_size, COPYBUFF_SIZE);
		return EINVAL;
	}

	/* It is fully up to the driver to decide on restrictions for the
	 * parameters of transfers, in those cases we return EINVAL */
	if (position % slot->card.blk_size != 0) {
		/* Starting at a block boundary */
		mmc_log_warn(&log,
		    "Requests must start at a block boundary"
		    "(start,block size)=(%016llx,%08x)\n", position,
		    slot->card.blk_size);
		return EINVAL;
	}

	blk_size = slot->card.blk_size;

	bytes_written = 0;

	/* Are we trying to start reading past the end */
	if (position >= dev->dv_size) {
		mmc_log_warn(&log, "start reading past drive size\n");
		return 0;
	};

	ciov = iov;
	/* do some more validation */
	for (counter = 0; counter < nr_req; counter++) {
		assert(ciov != NULL);
		if (ciov->iov_size % blk_size != 0) {
			/* transfer a multiple of blk_size */
			mmc_log_warn(&log,
			    "Requests must start at a block boundary "
			    "(start,block size)=(%016llx,%08x)\n", position,
			    slot->card.blk_size);
			return EINVAL;
		}

		if (ciov->iov_size <= 0) {
			mmc_log_warn(&log,
			    "Invalid iov size for iov %d of %d size\n",
			    counter, nr_req, ciov->iov_size);
			return EINVAL;
		}
		ciov++;
	}

	ciov = iov;
	for (counter = 0; counter < nr_req; counter++) {
		/* Assume we are to transfer the amount of data given in the
		 * input/output vector but ensure we are not doing i/o past
		 * our own boundaries */
		io_size = ciov->iov_size;
		io_offset = position + bytes_written;

		/* Check we are not reading/writing past the end */
		if (position + bytes_written + io_size > dev->dv_size) {
			io_size = dev->dv_size - (position + bytes_written);
		};

		mmc_log_trace(&log,
		    "I/O %s request(%d/%d) iov(grant,size,iosize,"
		    "offset)=(%d,%d,%d,%d)\n",
		    (do_write) ? "write" : "read", counter + 1, nr_req,
		    ciov->iov_addr, ciov->iov_size, io_size, io_offset);
		/* transfer max one block at the time */
		for (i = 0; i < io_size / blk_size; i++) {
			if (do_write) {
				/* Read io_size bytes from i/o vector starting
				 * at 0 and write it to out buffer at the
				 * correct offset */
				r = copyfrom(endpt, ciov->iov_addr,
				    i * blk_size, (vir_bytes) copybuff,
				    blk_size);
				if (r != OK) {
					mmc_log_warn(&log,
					    "I/O write error: %s iov(base,size)=(%d,%d)"
					    " at offset=%d\n",
					    strerror(_SIGN r), ciov->iov_addr,
					    ciov->iov_size, io_offset);
					return EINVAL;
				}

				/* write a single block */
				slot->host->write(&slot->card,
				    (dev->dv_base / blk_size) +
				    (io_offset / blk_size) + i, 1, copybuff);
				bytes_written += blk_size;
			} else {
				/* read a single block info copybuff */
				slot->host->read(&slot->card,
				    (dev->dv_base / blk_size) +
				    (io_offset / blk_size) + i, 1, copybuff);
				/* Read io_size bytes from our data at the
				 * correct offset and write it to the output
				 * buffer at 0 */
				r = copyto(endpt, ciov->iov_addr, i * blk_size,
				    (vir_bytes) copybuff, blk_size);
				if (r != OK) {
					mmc_log_warn(&log,
					    "I/O read error: %s iov(base,size)=(%d,%d)"
					    " at offset=%d\n",
					    strerror(_SIGN r), ciov->iov_addr,
					    ciov->iov_size, io_offset);
					return EINVAL;
				}
				bytes_written += blk_size;
			}
		}
		ciov++;
	}
	return bytes_written;
}

/*===========================================================================*
 *				block_ioctl		                     *
 *===========================================================================*/
static int
block_ioctl(dev_t minor,
    unsigned int request, endpoint_t endpt, cp_grant_id_t grant)
{
	/* IOCTL handling */
	struct sd_slot *slot;
	mmc_log_trace(&log,
	    "enter (minor,request,endpoint,grant)=(%d,%lu,%d)\n", minor,
	    request, endpt, grant);

	slot = get_slot(minor);
	if (!slot) {
		mmc_log_warn(&log,
		    "Doing ioctl on non existing block device(%d)\n", minor);
		return EINVAL;
	}

	switch (request) {
	case DIOCOPENCT:
		// TODO: add a check for card validity */
		mmc_log_trace(&log, "returning open count %d\n",
		    slot->card.open_ct);
		/* return the current open count */
		return sys_safecopyto(endpt, grant, 0,
		    (vir_bytes) & slot->card.open_ct,
		    sizeof(slot->card.open_ct));
	case DIOCFLUSH:
		/* No need to flush but some devices like movinands require
		 * 500 ms inactivity */
		return OK;
	}

	return EINVAL;
}

/*===========================================================================*
 *                    block_part                                             *
 *===========================================================================*/
static struct device *
block_part(dev_t minor)
{
	/* 
	 * Reuse the existing MINIX major/minor partitioning scheme.
	 * - 8 drives
	 * - 5 devices per drive allowing direct access to the disk and up to 4
	 *   partitions (IBM style partitioning without extended partitions)
	 * - 4 Minix style sub partitions per partitions
	 */
	struct device *dev;
	struct sd_slot *slot;

	dev = NULL;
	slot = get_slot(minor);
	if (!slot) {
		mmc_log_warn(&log,
		    "Device information requested for non existing partition "
		    "minor(%d)\n", minor);
		return NULL;
	}

	if (!slot->host->card_detect(slot)) {
		mmc_log_warn(&log,
		    "Device information requested from empty slot(%d)\n",
		    minor);
		return NULL;
	}

	if (minor < 5) {
		/* we are talking about the first disk */
		dev = &slot->card.part[minor];
		mmc_log_trace(&log,
		    "returning partition(%d) (base,size)=(0x%016llx,0x%016llx)\n",
		    minor, dev->dv_base, dev->dv_size);
	} else if (minor >= 128 && minor < 128 + 16) {
		/* sub partitions of the first disk we don't care about the
		 * rest */
		dev = &slot->card.subpart[minor - 128];
		mmc_log_trace(&log,
		    "returning sub partition(%d) (base,size)=(0x%016llx,0x%016llx)\n",
		    minor - 128, dev->dv_base, dev->dv_size);

	} else {
		mmc_log_warn(&log,
		    "Device information requested for non existing "
		    "partition minor(%d)\n", minor);
	}
	return dev;
}

/*===========================================================================*
 *                         sef_local_startup                                 *
 *===========================================================================*/
static void
sef_local_startup()
{
	mmc_log_info(&log, "Initializing the MMC block device\n");
	if (apply_env()) {
		mmc_log_warn(&log,
		    "Failed while applying environment settings\n");
		exit(EXIT_FAILURE);
	}

	if (host.host_init(&host)) {
		mmc_log_warn(&log,
		    "Failed to initialize the host controller\n");
		exit(EXIT_FAILURE);
	}
	/* 
	 * Register callbacks for fresh start, live update and restart.
	 *  Use the same function for all event types
	 */
	sef_setcb_init_fresh(block_system_event_cb);
	sef_setcb_init_lu(block_system_event_cb);

	/* Register a signal handler */
	sef_setcb_signal_handler(block_signal_handler_cb);

	/* SEF startup */
	sef_startup();
}

/*===========================================================================*
 *                         block_system_event_cb                             *
 *===========================================================================*/
static int
block_system_event_cb(int type, sef_init_info_t * info)
{
	/* 
	 * Callbacks for the System event framework as registered in 
	 * sef_local_startup */
	switch (type) {
	case SEF_INIT_FRESH:
		mmc_log_info(&log, "System event framework fresh start\n");
		break;

	case SEF_INIT_LU:
		/* Restore the state. post update */
		mmc_log_info(&log, "System event framework live update\n");
		break;

	case SEF_INIT_RESTART:
		mmc_log_info(&log, "System event framework post restart\n");
		break;
	}
	blockdriver_announce(type);
	return OK;
}

/*===========================================================================*
 *                         block_signal_handler_cb                           *
 *===========================================================================*/
static void
block_signal_handler_cb(int signo)
{
	mmc_log_debug(&log, "System event framework signal handler sig(%d)\n",
	    signo);
	/* Only check for termination signal, ignore anything else. */
	if (signo != SIGTERM)
		return;
	// FIXME shutdown
	exit(0);
}

#define IS_MINIX_SUB_PARTITION_MINOR(minor) (minor >= MINOR_d0p0s0 )

static struct sd_slot *
get_slot(dev_t minor)
{
	/* 
	 * Get an sd_slot based on the minor number.
	 *
	 * This driver only supports a single card at at time. Also as
	 * we are following the major/minor scheme of other driver we
	 * must return a slot for all minors on disk 0 these are  0-5
	 * for the disk and 4 main partitions and
	 * number 128 till 144 for sub partitions.
	 */
	/* If this is a minor for the first disk (e.g. minor 0 till 5) */
	if (minor / DEV_PER_DRIVE == 0) {
		/* we are talking about the first disk and that is all we
		 * support */
		return &host.slot[0];
	} else if (IS_MINIX_SUB_PARTITION_MINOR(minor)
	    && (((minor - MINOR_d0p0s0) / SUB_PER_DRIVE) == 0)) {
		/* a minor from the first disk */
		return &host.slot[0];
	} else {
		mmc_log_trace(&log,
		    "Device information requested for non existing partition "
		    "minor(%d)\n", minor);
		return NULL;
	}
}

#if 0
static void
set_log_level(int level)
{
	if (level < 0 || level >= 4) {
		return;
	}
	mmc_log_debug(&log, "Setting verbosity level to %d\n", level);
	log.log_level = level;
	if (host.set_log_level) {
		host.set_log_level(level);
	}
}
#endif

int
main(int argc, char **argv)
{

	/* Set and apply the environment */
	env_setargs(argc, argv);

	sef_local_startup();
	blockdriver_task(&mmc_driver);
	return EXIT_SUCCESS;
}
