#include <linux/module.h>
#include <linux/pci.h>
#include <linux/cdev.h>
#include <linux/vmalloc.h>
#include <linux/uaccess.h>

#include "sio_fpga.h"

MODULE_AUTHOR("ULM Projct <ulm-soft@vsp.cpg.sony.co.jp>");
MODULE_DESCRIPTION("SIO FPGA Driver");
MODULE_VERSION(SIO_FPGA_DRIVER_VERSION);
MODULE_LICENSE("GPL v2");

static int  init_chrdev (struct sio_fpga_driver_data *data) __init;
static int  sio_fpga_open(struct inode *inode, struct file *file);
static long sio_fpga_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);
static int sio_fpga_release(struct inode* inode, struct file* file);

#ifdef DEBUG
#define DBG_PRINT(fmt , ...) printk(fmt, ## __VA_ARGS__)
#else
#define DBG_PRINT(fmt , ...)
#endif

static struct file_operations sio_fpga_fops =
{
    .owner          = THIS_MODULE,
    .open           = sio_fpga_open,
    .release        = sio_fpga_release,
    .unlocked_ioctl = sio_fpga_ioctl,
};

static int __init init_chrdev(struct sio_fpga_driver_data* data)
{
    int dev_major = 0;
    int dev_minor = 0;
    int devno     = -1;
    int result;
    struct device* dev;

    result    = alloc_chrdev_region(&(data->cdevno), dev_minor, 1, SIO_FPGA_DEVFILE);

    dev_major     = MAJOR(data->cdevno);

    if (result < 0) {
        printk(KERN_DEBUG "cannot get major ID %d", dev_major);
    }

    devno         = MKDEV(dev_major, dev_minor);

    cdev_init(&(data->cdev), &sio_fpga_fops);
    data->cdev.owner = THIS_MODULE;
    data->cdev.ops   = &sio_fpga_fops;
    result = cdev_add(&(data->cdev), devno, 1);

    if (result) {
        return -1;
    }

    data->class = class_create(THIS_MODULE, SIO_FPGA_DRIVER_NAME);
    if (IS_ERR(data->class)) {
        printk(KERN_ERR "class_create\n");
        cdev_del(&(data->cdev));
        return -1;
    }

    dev = device_create(data->class, NULL, devno, NULL, SIO_FPGA_DEVFILE);
    if (IS_ERR(dev)) {
        printk(KERN_ERR "device_create %ld\n", PTR_ERR(dev));
        cdev_del(&(data->cdev));
        return -1;
    }

    return 0;
}

static int sio_fpga_open(struct inode* inode, struct file* file)
{
    struct sio_fpga_driver_data* drv_data = NULL;
    drv_data = container_of(inode->i_cdev, struct sio_fpga_driver_data, cdev);
    if (drv_data == NULL || file == NULL) {
        printk(KERN_ERR "%s data or file is NULL\n", __FUNCTION__);
        return -EFAULT;
    }

    file->private_data = drv_data;
    DBG_PRINT("sio_fpga_open success");
    return 0;
}

static int sio_fpga_release(struct inode* inode, struct file* file)
{
    DBG_PRINT("sio_fpga_release success");
    return 0;
}

static long sio_fpga_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
    struct sio_fpga_driver_data* drv_data = filp->private_data;
    void __iomem* addr;
    struct sio_fpga_driver_opt opt;
    int i = 0;
    uint offset;
    int rc = 0;
    int remain;

    if (_IOC_TYPE(cmd) != SIO_FPGA_MAGIC) {
        printk(KERN_ERR "%s: invalid command\n", __func__);
        return -ENOTTY;
    }
    if (drv_data == NULL) {
        printk("KERN_ERR %s: private_data is NULL\n", __func__);
        return -EFAULT;
    }

    switch (cmd) {
    case SIO_FPGA_READ:
        DBG_PRINT("SIO_FPGA_READ\n");
        if ((_IOC_SIZE(cmd) == sizeof(opt)) && copy_from_user(&opt, (struct sio_fpga_driver_opt __user*)arg, sizeof(opt))) {
            printk(KERN_ERR "copy_from_user error\n");
            return -EFAULT;
        }

        if (opt.offset > drv_data->bar_size) {
            printk(KERN_ERR "Illegal offset\n");
            return -EINVAL;
        }
        if (opt.length > drv_data->bar_size || opt.length + opt.offset > drv_data->bar_size) {
            printk(KERN_ERR "Illegal offset or length\n");
            return -EINVAL;
        }
        rc = mutex_lock_interruptible(&drv_data->mutex);
        if (rc != 0) {
            return rc;
        }
        offset = opt.offset;
        remain = opt.length;
        for (i = 0; i < opt.length;) {
            addr = drv_data->bar_addr + offset;
            if (!(offset & 0b11) && remain >= 4) {
                DBG_PRINT("32bit read");
                *(u32*)&(drv_data->buffer[i]) = ioread32(addr);
                i += 4;
                offset += 4;
                remain -= 4;
            } else if (!(offset & 1) && remain >= 2) {
                DBG_PRINT("16bit read");
                *(u16*)&(drv_data->buffer[i]) = ioread16(addr);
                i += 2;
                offset += 2;
                remain -= 2;
            } else {
                DBG_PRINT("8bit read");
                drv_data->buffer[i] = ioread8(addr);
                i += 1;
                offset += 1;
                remain -= 1;
            }
        }
        if (copy_to_user((u8 __user*)opt.buff, drv_data->buffer, opt.length)) {
            printk(KERN_ERR "SIO_FPGA_READ: copy_to_user error\n");
            rc = -EFAULT;
        }
        mutex_unlock(&drv_data->mutex);
        break;
    case SIO_FPGA_WRITE:
        DBG_PRINT("SIO_FPGA_WRITE\n");
        if ((_IOC_SIZE(cmd) == sizeof(opt)) && copy_from_user(&opt, (struct sio_fpga_driver_opt __user*)arg, sizeof(opt))) {
            printk(KERN_ERR "copy_from_user error\n");
            return -EFAULT;
        }
        if (opt.offset > drv_data->bar_size) {
            printk(KERN_ERR "Illegal offset\n");
            return -EINVAL;
        }
        if (opt.length > drv_data->bar_size || opt.length + opt.offset > drv_data->bar_size) {
            printk(KERN_ERR "Illegal offset or length\n");
            printk("%d\n", -EINVAL);
            return -EINVAL;
        }
        rc = mutex_lock_interruptible(&drv_data->mutex);
        if (rc != 0) {
            return rc;
        }
        if (copy_from_user(drv_data->buffer, (u8 __user*)opt.buff, opt.length)) {
            printk(KERN_ERR "copy_from_user error\n");
            return -EFAULT;
        }
        offset = opt.offset;

        remain = opt.length;
        for (i = 0; i < opt.length;) {
            addr = drv_data->bar_addr + offset;
            if (!(offset & 0b11) && remain >= 4) {
                DBG_PRINT("32bit write\n");
                iowrite32(*(u32*)&(drv_data->buffer[i]), addr);
                i += 4;
                offset += 4;
                remain -= 4;
            } else if (!(offset & 1) && remain >= 2) {
                DBG_PRINT("16bit write\n");
                iowrite16(*(u16*)&(drv_data->buffer[i]), addr);
                i += 2;
                offset += 2;
                remain -= 2;
            } else {
                DBG_PRINT("8bit write\n");
                iowrite8(drv_data->buffer[i], addr);
                i += 1;
                offset += 1;
                remain -= 1;
            }
        }
        mutex_unlock(&drv_data->mutex);
        break;
    default:
        return -EINVAL;
    }
    return rc;
}

static void __init sio_fpga_probe_err(struct sio_fpga_driver_data* data)
{
    if (data == NULL) {
        return;
    }

    pci_release_region(data->pci_dev, 0);
    if (atomic_read(&data->pci_dev->enable_cnt) > 0) {
        pci_disable_device(data->pci_dev);
    }
    if (data->cdevno != 0) {
        unregister_chrdev_region(data->cdevno, 1);
    }
    vfree(data);
}

static int __init sio_fpga_driver_probe(struct pci_dev* dev, const struct pci_device_id* id)
{
    struct sio_fpga_driver_data* data = NULL;
    int rc = 0;

    data = vmalloc(sizeof(struct sio_fpga_driver_data));
    if (data == NULL) {
        sio_fpga_probe_err(NULL);
        return -ENOMEM;
    }

    data->pci_dev = dev;
    pci_set_drvdata(dev, data);

    rc = init_chrdev(data);
    if (rc) {
        dev_err(&dev->dev, "init_chrdev() failed\n");
        sio_fpga_probe_err(data);
        return rc;
    }

    rc = pci_enable_device(dev);
    if (rc) {
        dev_err(&dev->dev, "pci_enable_device() failed\n");
        sio_fpga_probe_err(data);
        return rc;
    } else {
        dev_info(&dev->dev, "pci_enable_device() successful");
    }

    rc = pci_request_region(dev, 0, SIO_FPGA_DRIVER_NAME);
    if (rc) {
        dev_err(&dev->dev, "pci_request_region() 0 failed\n");
        sio_fpga_probe_err(data);
        return rc;
    }

    data->bar_addr = pci_ioremap_bar(dev, 0);
    data->bar_size = pci_resource_len(dev, 0);
    if (data->bar_addr) {
        dev_info(&dev->dev, "BAR0 mappted to 0x%p, length %lu", data->bar_addr, (long unsigned int)(data->bar_size));
    }

    data->buffer = vmalloc(sizeof(SIO_FPGA_BAR0_SIZE));
    if (!data->buffer) {
        printk(KERN_ERR "%s r/w buffer vmalloc_error\n", __func__);
        return -ENOMEM;
    }
    mutex_init(&(data->mutex));
    return rc;
}

static void __exit sio_fpga_driver_remove(struct pci_dev* dev)
{
    struct sio_fpga_driver_data* data = NULL;
    data = pci_get_drvdata(dev);

    pci_iounmap(dev, data->bar_addr);
    data->bar_addr = NULL;

    pci_release_regions(dev);
    pci_disable_device(dev);

    device_destroy(data->class, MKDEV(MAJOR(data->cdevno), 0));
    class_destroy(data->class);
    data->class = NULL;
    cdev_del(&data->cdev);
    unregister_chrdev_region(data->cdevno, 1);

    pci_set_drvdata(dev, NULL);
    vfree(data->buffer);
    vfree(data);
    printk(SIO_FPGA_DRIVER_NAME " remove\n");
}

static struct pci_device_id pci_ids[] = {
    { PCI_DEVICE(SIO_FPGA_VID, SIO_FPGA_DID) },
    { 0 }
};

MODULE_DEVICE_TABLE(pci, pci_ids);

static struct pci_driver sio_fpga_driver_ops = {
    .name     = SIO_FPGA_DRIVER_NAME,
    .id_table = pci_ids,
    .probe    = sio_fpga_driver_probe,
    .remove   = sio_fpga_driver_remove,
};

module_pci_driver(sio_fpga_driver_ops);
