/*
 * Copyright (C) 2021 NIKHEF Amsterdam
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

/* 
 * File:   misc_dev.c
 * Author: Ton Damen <tond@nikhef.nl>
 *
 * Created on April 27, 2020, 7:46 PM
 */

#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include "spec7.h"
#include "misc_dev.h"

#define VMEM_FLAGS (VM_IO | VM_DONTEXPAND | VM_DONTDUMP)

static int miscdev_open(struct inode *inode, struct file *filep)
{
    struct miscdevice *mdev_ptr = filep->private_data;
    filep->private_data = container_of(mdev_ptr, struct spec7_bar, mdev);        
    return 0;
}

/*
 * character device file operations for control bus (through control bridge)
 */
static ssize_t miscdev_read(struct file *filep, char __user *buf, size_t count,
                              loff_t *pos)
{
    void *addr;
    u32 w;
    int rv;
    
    struct spec7_bar *bar = filep->private_data;

    /* only 32-bit aligned and 32-bit multiples */
    if (*pos & 3)
        return -EPROTO;
    /* first address is BAR base plus file position offset */
    addr = bar->base + *pos;
    
    if (addr >= (bar->base + bar->size))
        return 0;
    
    //w = read_register(reg);
    w = ioread32(addr);
    
    rv = copy_to_user(buf, &w, 4);
    if (rv)
        pr_info("Copy to userspace failed but continuing\n");

    *pos += 4;
    return 4;
}

static ssize_t miscdev_write(struct file *filep, const char __user *buf,
                               size_t count, loff_t *pos)
{
    void *addr;
    u32 w;
    int rv;
    
    struct spec7_bar *bar = filep->private_data;

    /* only 32-bit aligned and 32-bit multiples */
    if (*pos & 3)
        return -EPROTO;

    /* first address is BAR base plus file position offset */
    addr = bar->base + *pos;
       
    if (addr >= (bar->base + bar->size))
        return -EFAULT;
    
    rv = copy_from_user(&w, buf, 4);
    if (rv) {
        pr_info("copy from user failed %d/4, but continuing.\n", rv);
    }

    iowrite32(w, addr);
    *pos += 4;
    return 4;
}


/* maps the PCIe BAR into user space for memory-like access using mmap() */
static int miscdev_mmap(struct file *filep, struct vm_area_struct *vma)
{
    unsigned long offs, vsize, psize, phys;
    
    struct spec7_bar *bar = filep->private_data;
    
    offs = vma->vm_pgoff << PAGE_SHIFT;
    // physical address
    phys = bar->phys + offs;
    
    vsize = vma->vm_end - vma->vm_start;
    /* complete resource */
    psize = bar->size - offs;
    
    if (vsize > psize)
        return -EINVAL;
    /*
     * pages must not be cached as this would result in cache line sized
     * accesses to the end point
     */
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    /*
     * prevent touching the pages (byte access) for swap-in,
     * and prevent the pages from being swapped out
     */
    vma->vm_flags |= VMEM_FLAGS;
    /* make MMIO accessible to user space */
    if (io_remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT,
                            vsize, vma->vm_page_prot) != 0)
        return -EAGAIN;
    
    return 0;
}

static long miscdev_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
    struct spec7_bar *bar = filep->private_data;
    
    switch(cmd){
        case SPEC7_IOCQSIZE:
        return bar->size;
        
        default:
            return -ENOTTY;
    }
    return 0;
}

static const struct file_operations spec7_fops = {
	.owner = THIS_MODULE,
        .open = miscdev_open,
        .read = miscdev_read,
        .write = miscdev_write,
        .unlocked_ioctl = miscdev_ioctl,
        .mmap = miscdev_mmap
};

int miscdev_init(const char *name, struct miscdevice *mdev)
{
    int rc;
    mdev->minor = MISC_DYNAMIC_MINOR;
    mdev->fops = &spec7_fops;
    mdev->name = name;
    rc = misc_register(mdev);
    if (rc == 0) 
        return mdev->minor;    
    return rc;
}