#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

#include <linux/iomap.h>

#include "drexchar.h"
#include "drexdma.h"

#define MAX_DEV 16

static int drexpcie_open(struct inode *inode, struct file *file);
static int drexpcie_release(struct inode *inode, struct file *file);
static int drexpcie_mmap(struct file *file, struct vm_area_struct *vma);
static long drexpcie_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static ssize_t drexpcie_read(struct file *file, char __user *buf, size_t count, loff_t *offset);
static ssize_t drexpcie_write(struct file *file, const char __user *buf, size_t count, loff_t *offset);

static const struct file_operations drexpcie_fops = {
	.owner      = THIS_MODULE,
	.open       = drexpcie_open,
	.release    = drexpcie_release,
   .mmap       = drexpcie_mmap,   
	.unlocked_ioctl = drexpcie_ioctl,
	.read       = drexpcie_read,
	.write      = drexpcie_write
};


struct drexpcie_device_data {
	struct device* dev;
	struct cdev cdev;
   uint32_t type;
   uint32_t offset;
};


static int dev_major = 0;
static struct class *drexpcie_class = NULL;
static struct drexpcie_device_data drexpcie_dev_data[MAX_DEV];
static struct drexpcie_driver_priv* driv_access = NULL;

static int drexpcie_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
	add_uevent_var(env, "DEVMODE=%#o", 0666);

	return 0;
}

int drexpcie_chardev_create(struct drexpcie_driver_priv *driv_priv)
{
	int err, i;
	dev_t dev;

   char device_string[64];

   int pl_count = 0;
   int dma_count = 0;

   driv_access = driv_priv;

   err = alloc_chrdev_region(&dev, 0, MAX_DEV, "drexpcie");

	dev_major = MAJOR(dev);

	// drexpcie_class = class_create(THIS_MODULE, "drexpcie-dev");
   drexpcie_class = class_create("drexpcie-dev");

	drexpcie_class->dev_uevent = drexpcie_uevent;

	for (i = 0; i < MAX_DEV; i++) {
      drexpcie_dev_data[i].type = ioread32(driv_access->bar0_mem+(i*4)*4);
      if (drexpcie_dev_data[i].type == 0x10000) {
         snprintf(device_string,sizeof(device_string),"drexpl%d",pl_count);  
         pl_count++;       
      }
      else if (drexpcie_dev_data[i].type == 0x20000) {
         snprintf(device_string,sizeof(device_string),"drexdma%d",dma_count);
         dma_count++;
      }
      else {
         drexpcie_dev_data[i].type = 0;
         break;
      }

      // save the offset of the device area;
      drexpcie_dev_data[i].offset = ioread32(driv_access->bar0_mem+(i*4+1)*4);

		cdev_init(&drexpcie_dev_data[i].cdev, &drexpcie_fops);
		drexpcie_dev_data[i].cdev.owner = THIS_MODULE;
		cdev_add(&drexpcie_dev_data[i].cdev, MKDEV(dev_major, i), 1);
      
		drexpcie_dev_data[i].dev = device_create(drexpcie_class, NULL, MKDEV(dev_major, i), NULL, "%s",device_string);

	}

	return 0;
}

int drexpcie_chardev_destroy(void)
{
	int i;

	for (i = 0; i < MAX_DEV; i++) {
		device_destroy(drexpcie_class, MKDEV(dev_major, i));
	}

	class_unregister(drexpcie_class);
	class_destroy(drexpcie_class);

	unregister_chrdev_region(MKDEV(dev_major, 0), MINORMASK);

	return 0;
}


static int drexpcie_open(struct inode *inode, struct file *file)
{
	struct drexpcie_device_priv* drexpcie_priv;
	unsigned int minor = iminor(inode);

	drexpcie_priv = kzalloc(sizeof(struct drexpcie_device_priv), GFP_KERNEL);
	drexpcie_priv->chnum = minor;
	drexpcie_priv->driv_priv = driv_access;
//   drexpcie_priv->dev = &(drexpcie_dev_data[drexpcie_priv->chnum].pdev->dev);
   drexpcie_priv->type = drexpcie_dev_data[drexpcie_priv->chnum].type;
   drexpcie_priv->base_mem = driv_access->bar0_mem +  drexpcie_dev_data[drexpcie_priv->chnum].offset;

   drexpcie_priv->num_bufs = 512;
   drexpcie_priv->num_bytes = 4194304;
   drexpcie_priv->valid_bufs = 0;

   drexpcie_priv->bufPtr = NULL;
   drexpcie_priv->dmaPtr = (dma_addr_t *) NULL;    

	file->private_data = drexpcie_priv;

	return 0;
}

static int drexpcie_release(struct inode *inode, struct file *file)
{
	struct drexpcie_device_priv* drexpcie_priv = file->private_data;

	kfree(drexpcie_priv);

	drexpcie_priv = NULL;

	return 0;
}

static int drexpcie_mmap(struct file *file, struct vm_area_struct *vma)
{
   struct drexpcie_device_priv* drexpcie_priv = file->private_data;
   int    ret;

   // DMA doesn't support mmap
   if (drexpcie_priv->type == 0x20000) return -EINVAL; 

   vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    
   ret = io_remap_pfn_range(vma, vma->vm_start, ((unsigned long) drexpcie_priv->driv_priv->mem_start) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);
   if (ret != 0) {
      printk(KERN_ERR "Error in calling remap_pfn_range: returned %d\n", ret);
      return -EAGAIN;
   }
    
	 return 0;
}

static long drexpcie_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct drexpcie_device_priv* drexpcie_priv = file->private_data;
   drexpcie_ioctl_t drexpcie_ioctl;

   // uint32_t temp;

   if (copy_from_user(&drexpcie_ioctl, (drexpcie_ioctl_t *)arg,
                       sizeof(drexpcie_ioctl_t))) {
      return -EACCES;
   } 

   printk("drexpcie[%d]: Received IOCTL cmd=%d\n", drexpcie_priv->chnum,  drexpcie_ioctl.cmd);

   switch (drexpcie_ioctl.cmd) {
      case DREXDMA_DMA_INIT:
         return dma_init(drexpcie_priv);
         break;

      case DREXDMA_DMA_CLEAR:
         return dma_clear(drexpcie_priv);
         break;         

      case DREXDMA_DMA_START:
         return dma_start(drexpcie_priv);
         break;
               
      case DREXDMA_DMA_STOP:
         return dma_stop(drexpcie_priv);
         break;

      case DREXDMA_SET_NUM_BUFS:
         drexpcie_priv->num_bufs = drexpcie_ioctl.value;
         break;

      case DREXDMA_SET_NUM_BYTES:
         drexpcie_priv->num_bytes = drexpcie_ioctl.value;
         break;

      case DREXDMA_GET_NUM_BUFS:
         drexpcie_ioctl.value = drexpcie_priv->num_bufs;
         if (copy_to_user((drexpcie_ioctl_t *)arg, &drexpcie_ioctl, sizeof(drexpcie_ioctl_t))) {
            return -EACCES;
         }
         break;

      case DREXDMA_GET_NUM_BYTES:
         drexpcie_ioctl.value = drexpcie_priv->num_bytes;
         if (copy_to_user((drexpcie_ioctl_t *)arg, &drexpcie_ioctl, sizeof(drexpcie_ioctl_t))) {
            return -EACCES;
         }
         break;    

      case DREXDMA_GET_FREE_BUFS:
         drexpcie_ioctl.value = ioread32(drexpcie_priv->base_mem + DREXDMA_OFFSET_STAT + 0x8);
         if (copy_to_user((drexpcie_ioctl_t *)arg, &drexpcie_ioctl, sizeof(drexpcie_ioctl_t))) {
            return -EACCES;
         }
         break;                 

      default:
         printk("drexpcie[%d]: error, invalid IOCTL cmd\n", drexpcie_priv->chnum);
         return -EINVAL;
    }

	return 0;
}

static ssize_t drexpcie_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
   struct drexpcie_device_priv* drexpcie_priv = file->private_data;

   uint32_t temp[3];
   uint32_t dma_index;

   // read status register to determine is something is in the complete queue
   temp[0] = ioread32(drexpcie_priv->base_mem + DREXDMA_OFFSET_STAT+0xC);

   // if bit 0 is high, FIFO is empty return 0 count value;
   if (temp[0] & 0x01) {
      count = 0;
   }
   // else read 72 bit FIFO value;  value is popped after MSW read;
   else {
      temp[0] = ioread32(drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO);
      // temp[1] = ioread32(drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO+4);
      temp[2] = ioread32(drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO+8);
      
      // determine which buffer index;
      dma_index = temp[0] & 0x000001FF;

      // determine amount of data in buffer;
      count = (temp[0] & 0xFFFFFE00) >> 9;

      //using dma_index, copy correct buffer to user space 
      if (copy_to_user(buf, drexpcie_priv->bufPtr[dma_index], count)) {
         count = 0;
      }      
   
      // create new LSW using index and size
      temp[0] = (((drexpcie_priv->num_bytes) & 0x7FFFFF) << 9);
      temp[0] |= (((dma_index) & 0x000001FF));

      // write now unused buffer back to the available queue
      // If using loop mode dont need to write buffers back in
      // iowrite32(temp[0], drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO);
      // iowrite32(temp[1], drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO+4);
      // iowrite32(temp[2], drexpcie_priv->base_mem + DREXDMA_OFFSET_FIFO+8);     

   }

	return count;
}

static ssize_t drexpcie_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
	return count;
}

