// created by (aw)
// taking clues from https://stackoverflow.com/questions/37919223/map-reserver-memory-at-boot-to-user-space-using-remap-pfn-range
// 23 Dec 2023

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/kdev_t.h>
#include <linux/mm.h>

#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>

#ifndef VM_RESERVED
	#define VM_RESERVED (VM_DONTEXPAND | VM_DONTDUMP)
#endif

MODULE_LICENSE("GPL");
MODULE_AUTHOR("MPSI Technologies GmbH");
MODULE_DESCRIPTION("dbeaxishmem - WhizniumDBE AXI shared memory loadable kernel module");

#define DRIVER_NAME "dbeaxishmem"

#define NMAX_MINOR 8

static struct class* deviceFileClass;
static int major_num;

struct dbeaxishmem_data_t {
	dev_t dev;
	resource_size_t phys;
	void __iomem* mm;
	bool busyNotIdle;
};
static struct dbeaxishmem_data_t devdatas[NMAX_MINOR];

/***** file operations *****/

long device_ioctl(
			struct file* file
			, unsigned int ioctl_num
			, unsigned long ioctl_param
		) {
	return 0;
};

static int device_open(
			struct inode* inode
			, struct file* file
		) {
	int ix;
	struct dbeaxishmem_data_t* devdata = NULL;

	for (ix = 0; ix < NMAX_MINOR; ix++)
		if (devdatas[ix].dev == inode->i_rdev) {
			devdata = &(devdatas[ix]);
			break;
		};

	if (!devdata) return -ENXIO;

	file->private_data = devdata;

	if (devdata->busyNotIdle) return -EBUSY;

	try_module_get(THIS_MODULE);

	devdata->busyNotIdle = true;

	return 0;
};

static int device_mmap(
			struct file* file
			, struct vm_area_struct* vma
		) {
	int res;

	struct dbeaxishmem_data_t* devdata = file->private_data;

	// interestingly, vma->vm_end - vma->vm_start (without + 1)
  res = remap_pfn_range(vma, vma->vm_start, (devdata->phys + (vma->vm_pgoff << PAGE_SHIFT)) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);

	return res;
};

static int device_close(
			struct inode* inode
			, struct file* file
		) {
	struct dbeaxishmem_data_t* devdata = file->private_data;

	if (!devdata->busyNotIdle) return -EBUSY;

	module_put(THIS_MODULE);

	devdata->busyNotIdle = false;

	return 0;
};

static struct file_operations fileops = {
			.owner = THIS_MODULE
			, .unlocked_ioctl = device_ioctl
			, .open = device_open
			, .mmap = device_mmap
			, .release = device_close
			, };

/***** module probe and remove *****/

static int dbeaxishmem_probe(
			struct platform_device* pdev
		) {
	int ix;
	struct dbeaxishmem_data_t* devdata = NULL;

	struct device_node* node;
	struct resource r_mem;

	struct device* dev = &pdev->dev;

	for (ix = 0; ix < NMAX_MINOR; ix++)
		if (devdatas[ix].mm == NULL) {
			devdata = &(devdatas[ix]);
			break;
		};

	if (!devdata) {
		dev_err(dev, "no minor number available\n");

		return -ENODEV;
	};

	dev->devt = MKDEV(major_num, ix);
	devdata->dev = dev->devt;

	device_create(deviceFileClass, NULL, dev->devt, NULL, DRIVER_NAME "%d", ix);

	node = of_parse_phandle(dev->of_node, "memory-region", 0);
	if (!node) {
		dev_err(dev, "no memory region specified\n");

		return -ENODEV;
	};

	if (of_address_to_resource(node, 0, &r_mem)) {
		dev_err(dev, "invalid address\n");

		return -ENODEV;
	};

	devdata->phys = r_mem.start;
	devdata->mm = memremap(r_mem.start, r_mem.end - r_mem.start + 1, MEMREMAP_WB);
	if (!devdata->mm) {
		dev_err(dev, "error mapping memory region at 0x%10x\n", (unsigned int __force) r_mem.start);
		release_mem_region(r_mem.start, r_mem.end - r_mem.start + 1);

		return -EIO;
	};

	//memset(devdata->mm, '*', r_mem.end - r_mem.start + 1);

	devdata->busyNotIdle = false;

	dev_info(dev, "dbeaxishmem device successfully mapped to memory\n");

	return 0;
};

#if LINUX_VERSION_CODE <= KERNEL_VERSION(6,10,14)
static int dbeaxishmem_remove(
#else
static void dbeaxishmem_remove(
#endif
			struct platform_device* pdev
		) {
	int ix;
	struct dbeaxishmem_data_t* devdata = NULL;

	struct resource* r_mem;
	struct device* dev = &pdev->dev;

	for (ix = 0; ix < NMAX_MINOR; ix++) {
		if (devdatas[ix].dev == pdev->dev.devt) {
			devdata = &(devdatas[ix]);
			break;
		};
	};

	if (!devdata) {
		dev_err(dev, "device not found\n");

#if LINUX_VERSION_CODE <= KERNEL_VERSION(6,10,14)
		return 0;
#else
		return;
#endif
	};

	iounmap(devdata->mm);

	r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!r_mem) {
		dev_err(dev, "invalid address\n");

#if LINUX_VERSION_CODE <= KERNEL_VERSION(6,10,14)
		return 0;
#else
		return;
#endif
	};

	release_mem_region(r_mem->start, r_mem->end - r_mem->start + 1);

	devdata->dev = 0;
	devdata->phys = 0;
	devdata->mm = NULL;

#if LINUX_VERSION_CODE <= KERNEL_VERSION(6,10,14)
	return 0;
#endif
};

#ifdef CONFIG_OF
	static struct of_device_id dbeaxishmem_of_match[] = {{
				.compatible = "mpsi,dbeaxishmem"
				, }, {}, };
	MODULE_DEVICE_TABLE(of, dbeaxishmem_of_match);
#endif

static struct platform_driver plfdrv = {
			.driver = {
						.name = DRIVER_NAME
						, .owner = THIS_MODULE
#ifdef CONFIG_OF
						, .of_match_table	= dbeaxishmem_of_match
#endif
						//,
			}
			, .probe = dbeaxishmem_probe
			, .remove = dbeaxishmem_remove//,
};

/***** main *****/

static int __init dbeaxishmem_init(void) {
	int ix;

	major_num = register_chrdev(0, DRIVER_NAME, &fileops);
	if (major_num < 0) printk(KERN_ALERT "error registering character device!\n");

#if LINUX_VERSION_CODE>=KERNEL_VERSION(6, 4, 0)
	deviceFileClass = class_create(DRIVER_NAME);
#else
	deviceFileClass = class_create(THIS_MODULE, DRIVER_NAME);
#endif

	for (ix = 0; ix < NMAX_MINOR; ix++) {
		devdatas[ix].dev = 0;
		devdatas[ix].phys = 0;
		devdatas[ix].mm = NULL;
		devdatas[ix].busyNotIdle = false;
	};

	return platform_driver_register(&plfdrv);
};

static void __exit dbeaxishmem_exit(void) {
	platform_driver_unregister(&plfdrv);
	unregister_chrdev(major_num, DRIVER_NAME);
};

module_init(dbeaxishmem_init);
module_exit(dbeaxishmem_exit);
