/*
 * Copyright (C) 2012 CERN (www.cern.ch)
 * Author: Alessandro Rubini <rubini@gnudd.com>
 *
 * Released according to the GNU GPL, version 2 or any later version.
 *
 * This work is part of the White Rabbit project, a research effort led
 * by CERN, the European Institute for Nuclear Research.
 */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/spinlock.h>
#include <linux/fmc.h>
#include <linux/uaccess.h>

static LIST_HEAD(fc_devices);
static DEFINE_SPINLOCK(fc_lock);

struct fc_instance {
	struct list_head list;
	struct fmc_device *fmc;
	struct miscdevice misc;
};

/* at open time, we must identify our device */
static int fc_open(struct inode *ino, struct file *f)
{
	struct fmc_device *fmc;
	struct fc_instance *fc;
	int minor = iminor(ino);

	list_for_each_entry(fc, &fc_devices, list)
		if (fc->misc.minor == minor)
			break;
	if (fc->misc.minor != minor)
		return -ENODEV;
	fmc = fc->fmc;
	if (try_module_get(fmc->owner) == 0)
		return -ENODEV;

	f->private_data = fmc;
	return 0;
}

static int fc_release(struct inode *ino, struct file *f)
{
	struct fmc_device *fmc = f->private_data;
	module_put(fmc->owner);
	return 0;
}

/* read and write are simple after the default llseek has been used */
static ssize_t fc_read(struct file *f, char __user *buf, size_t count,
		       loff_t *offp)
{
	struct fmc_device *fmc = f->private_data;
	unsigned long addr;
	uint32_t val;

	if (count < sizeof(val))
		return -EINVAL;
	count = sizeof(val);

	addr = *offp;
	if (addr > fmc->memlen)
		return -ESPIPE; /* Illegal seek */
	val = fmc_readl(fmc, addr);
	if (copy_to_user(buf, &val, count))
		return -EFAULT;
	*offp += count;
	return count;
}

static ssize_t fc_write(struct file *f, const char __user *buf, size_t count,
			loff_t *offp)
{
	struct fmc_device *fmc = f->private_data;
	unsigned long addr;
	uint32_t val;

	if (count < sizeof(val))
		return -EINVAL;
	count = sizeof(val);

	addr = *offp;
	if (addr > fmc->memlen)
		return -ESPIPE; /* Illegal seek */
	if (copy_from_user(&val, buf, count))
		return -EFAULT;
	fmc_writel(fmc, val, addr);
	*offp += count;
	return count;
}

static const struct file_operations fc_fops = {
	.owner = THIS_MODULE,
	.open = fc_open,
	.release = fc_release,
	.llseek = generic_file_llseek,
	.read = fc_read,
	.write = fc_write,
};


/* Device part .. */
static int fc_probe(struct fmc_device *fmc);
static int fc_remove(struct fmc_device *fmc);

static struct fmc_driver fc_drv = {
	.version = FMC_VERSION,
	.driver.name = KBUILD_MODNAME,
	.probe = fc_probe,
	.remove = fc_remove,
	/* no table: we want to match everything */
};

/* We accept the generic busid parameter */
FMC_PARAM_BUSID(fc_drv);

/* probe and remove must allocate and release a misc device */
static int fc_probe(struct fmc_device *fmc)
{
	int ret;
	int index = 0;

	struct fc_instance *fc;

	index = fmc_validate(fmc, &fc_drv);
	if (index < 0)
		return -EINVAL; /* not our device: invalid */

	/* Create a char device: we want to create it anew */
	fc = kzalloc(sizeof(*fc), GFP_KERNEL);
	if (!fc)
		return -ENOMEM;
	fc->fmc = fmc;
	fc->misc.minor = MISC_DYNAMIC_MINOR;
	fc->misc.fops = &fc_fops;
	fc->misc.name = kstrdup(dev_name(&fmc->dev), GFP_KERNEL);

	ret = misc_register(&fc->misc);
	if (ret < 0)
		goto out;
	spin_lock(&fc_lock);
	list_add(&fc->list, &fc_devices);
	spin_unlock(&fc_lock);
	dev_info(&fc->fmc->dev, "Created misc device \"%s\"\n",
		 fc->misc.name);
	return 0;

out:
	kfree(fc->misc.name);
	kfree(fc);
	return ret;
}

static int fc_remove(struct fmc_device *fmc)
{
	struct fc_instance *fc;

	list_for_each_entry(fc, &fc_devices, list)
		if (fc->fmc == fmc)
			break;
	if (fc->fmc != fmc) {
		dev_err(&fmc->dev, "remove called but not found\n");
		return -ENODEV;
	}

	spin_lock(&fc_lock);
	list_del(&fc->list);
	spin_unlock(&fc_lock);
	misc_deregister(&fc->misc);
	kfree(fc->misc.name);
	kfree(fc);

	return 0;
}


static int fc_init(void)
{
	int ret;

	ret = fmc_driver_register(&fc_drv);
	return ret;
}

static void fc_exit(void)
{
	fmc_driver_unregister(&fc_drv);
}

module_init(fc_init);
module_exit(fc_exit);

MODULE_LICENSE("GPL");
