// SPDX-License-Identifier: GPL-2.0
//
// Copyright (c) 2021 MediaTek Inc.

#define DEBUG

#include <linux/cdev.h>
#include <linux/dma-buf.h>
#include <linux/dma-direction.h>
#include <linux/dma-direct.h>
#include <linux/module.h>
#include <linux/rpmsg.h>
#include <linux/remoteproc.h>
#include <linux/scatterlist.h>
#include <linux/slab.h>
#include <linux/version.h>
#include <mtk_shmctrl.h>
#include <linux/sysfs.h>
#include <linux/debugfs.h>

#include "mtk_shmctrl_internal.h"

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 16, 0)
MODULE_IMPORT_NS(DMA_BUF);
#endif

#define RPMSG_DEV_MAX	(MINORMASK + 1)
#define SHMCTRL_TIMEOUT	1000
#define SHM_NAME_LEN 64

struct shmctrl_ctx {
	struct completion done;
	u64 id;
	u32 err;
};

struct shmctrl_dev {
	struct cdev cdev;
	struct device dev;
	struct rproc *rproc;
	struct rpmsg_device *rpdev;
	struct list_head buf_list; // = LIST_HEAD_INIT(buf_list);
	struct mutex buf_list_lock; //= __MUTEX_INITIALIZER(mutexname)
	/* used for ipc msg */
	struct mutex ctx_lock;
	struct shmctrl_ctx ctx;
};

struct shmbuf_info {
	struct dma_buf *dmabuf;
	struct dma_buf_attachment *attach;
	struct sg_table *sgt;
	struct list_head list;
	dma_addr_t dma_addr;
	size_t size;
	char name[SHM_NAME_LEN];
};

#define dev_to_ctrldev(dev) container_of(dev, struct shmctrl_dev, dev)
#define cdev_to_ctrldev(i_cdev) container_of(i_cdev, struct shmctrl_dev, cdev)

static dev_t rpmsg_major;
static struct class *rpmsg_class;
static struct dentry *mtk_shmctrl_dbg_dir;

static DEFINE_IDA(shm_ctrl_ida);
static DEFINE_IDA(rpmsg_minor_ida);

static __u32 u32DbgLevel = SHMCTRL_LOG_LEVEL_ERROR;
static char s_shmctrl_debugfs_buf[PAGE_SIZE];

#define SHMCTRL_LOG_E(format, args...) \
	do { \
		if (u32DbgLevel & SHMCTRL_LOG_LEVEL_ERROR) { \
			pr_err("[shmctrl]"format, ##args); \
		} \
	} while (0)

#define SHMCTRL_LOG_I(format, args...) \
	do { \
		if (u32DbgLevel & SHMCTRL_LOG_LEVEL_INFO) { \
			pr_info("[shmctrl] "format, ##args); \
		} \
	} while (0)

#define SHMCTRL_LOG_D(format, args...) \
	do { \
		if (u32DbgLevel & SHMCTRL_LOG_LEVEL_DEBUG) { \
			pr_info("[shmctrl]"format, ##args); \
		} \
	} while (0)

static ssize_t log_level_show(struct device *dev,
		struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE, "u32DbgLevel = 0x%x\n", u32DbgLevel);
}

static ssize_t log_level_store(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t count)
{
	unsigned long val;
	int err;

	err = kstrtoul(buf, 0, &val);
	if (err)
		return err;

	u32DbgLevel = val;
	return count;
}

static ssize_t mtk_shmctrl_log_level_read(struct file *filp, char __user *userbuf,
	size_t count, loff_t *ppos)
{
	int len = 0;

	memset(s_shmctrl_debugfs_buf, 0, sizeof(s_shmctrl_debugfs_buf));

	len = log_level_show(NULL, NULL, s_shmctrl_debugfs_buf);

	return simple_read_from_buffer(userbuf, count, ppos, s_shmctrl_debugfs_buf, len);
}

static ssize_t mtk_shmctrl_log_level_write(struct file *filp, const char __user *userbuf,
	size_t count, loff_t *ppos)
{
	int ret = 0;

	if (count > sizeof(s_shmctrl_debugfs_buf))
		return count;

	memset(s_shmctrl_debugfs_buf, 0, sizeof(s_shmctrl_debugfs_buf));

	ret = copy_from_user(s_shmctrl_debugfs_buf, userbuf, count);
	if (ret)
		return count;

	return log_level_store(NULL, NULL, s_shmctrl_debugfs_buf, count);
}

static const struct file_operations mtk_shmctrl_log_level_ops = {
	.owner = THIS_MODULE,
	.open = simple_open,
	.read = mtk_shmctrl_log_level_read,
	.write = mtk_shmctrl_log_level_write,
};


static int shmctrl_rpmsg_send(struct shmctrl_msg *msg, struct shmctrl_dev *ctrldev)
{
	struct shmctrl_ctx *ctx = &ctrldev->ctx;
	struct rpmsg_device *rpdev = ctrldev->rpdev;
	int ret = 0;

	msg->host_hdl = (u64)msg;
	ret = rpmsg_send(rpdev->ept, msg, sizeof(*msg));

wait_again:
	/* wait for ack */
	if (!wait_for_completion_timeout
	    (&ctx->done, msecs_to_jiffies(SHMCTRL_TIMEOUT))) {
		SHMCTRL_LOG_E("ipc wait for ack timeout\n");
		ret = -ETIMEDOUT;
		goto exit;
	}

	mutex_lock(&ctrldev->ctx_lock);
	if (ctx->id != (u64)msg) {
		SHMCTRL_LOG_E("not my id, something wrong, ignore it (%llx vs %llx)\n",
				(unsigned long long)ctx->id,
				(unsigned long long)msg);
		mutex_unlock(&ctrldev->ctx_lock);
		goto wait_again;
	}

	if (ctx->err != SHMCT_SUCCESS) {
		SHMCTRL_LOG_E("ipc share buffer fail.(%d)\n", ctx->err);
		ret = -EIO;
	}

	mutex_unlock(&ctrldev->ctx_lock);
exit:
	return ret;
}

static int shmctrl_share_buf(struct shmctrl_dev *ctrldev,
			     struct shmctrl_export_info *info)
			     /*struct dma_buf *dmabuf,
			     const char *buf_name)*/
{
	int ret;
	unsigned long pfn, last_pfn = 0;
	struct device *dev = &ctrldev->dev;
	struct device *rproc_dev = ctrldev->rproc->dev.parent;
	struct dma_buf_attachment *attach;
	struct shmctrl_msg msg;
	struct sg_table *sgt;
	struct shmbuf_info *shm;
	dma_addr_t dma_addr;
	struct sg_page_iter sgiter;

	shm = kzalloc(sizeof(*shm), GFP_KERNEL);
	if (!shm)
		return -ENOMEM;

	shm->dmabuf = dma_buf_get(info->fd);
	if (IS_ERR_OR_NULL(shm->dmabuf)) {
		SHMCTRL_LOG_E("can not get dmabuf\n");
		ret = PTR_ERR(shm->dmabuf);
		goto err_shmfree;
	}

	attach = dma_buf_attach(shm->dmabuf, rproc_dev);
	if (IS_ERR(attach)) {
		SHMCTRL_LOG_E("buf attach fail\n");
		ret = -EINVAL;
		goto err_dma_buf_put;
	}

	sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
	if (IS_ERR(sgt)) {
		SHMCTRL_LOG_E("fail to get sgt, err = %ld\n", PTR_ERR(sgt));
		ret = PTR_ERR(sgt);
		goto err_detach;
	}

	for_each_sg_page(sgt->sgl, &sgiter, sgt->nents, 0) {
		struct page *page = sg_page_iter_page(&sgiter);

		pfn = page_to_pfn(page);
		SHMCTRL_LOG_I("pfn = %lx last_pfn = %lx\n", pfn, last_pfn);
		if (last_pfn && pfn != last_pfn + 1) {
			SHMCTRL_LOG_E("invalid dma buf, only support physical continuous pages\n");
			ret = -EINVAL;
			goto err_unmap;
		}

		last_pfn = pfn;
	}

	dma_addr = sg_dma_address(sgt->sgl);
	SHMCTRL_LOG_D("dma addr = %pad\n", &dma_addr);

	/* debug msg */
	for_each_sg_page(sgt->sgl, &sgiter, sgt->nents, 0) {
		struct page *page = sg_page_iter_page(&sgiter);
		phys_addr_t phys = page_to_phys(page);
		dma_addr_t dmaaddr = phys_to_dma(rproc_dev, phys);

		SHMCTRL_LOG_D("dmaaddr = %pad, phys = %pap\n", &dmaaddr, &phys);
	}

	msg.cmd = SHMCT_CMD_SHARE;
	msg.info.dma_addr = (u32)dma_addr;
	msg.info.len = (u32)shm->dmabuf->size;
	strncpy(msg.info.name, info->name, sizeof(msg.info.name));
	msg.info.name[SHMCT_MAX_STR_SIZE - 1] = '\0';
	ret = shmctrl_rpmsg_send(&msg, ctrldev);
	if (ret)
		goto err_unmap;

	shm->sgt = sgt;
	shm->attach = attach;
	shm->dma_addr = dma_addr;
	shm->size = shm->dmabuf->size;
	strncpy(shm->name, info->name, SHM_NAME_LEN);
	SHMCTRL_LOG_D("shminfo: dma_addr(%pad) shm_name(%s)\n",
		&shm->dma_addr, shm->name);

	mutex_lock(&ctrldev->buf_list_lock);
	list_add_tail(&shm->list, &ctrldev->buf_list);
	mutex_unlock(&ctrldev->buf_list_lock);

	return ret;
err_unmap:
	dma_buf_unmap_attachment(attach, sgt, DMA_BIDIRECTIONAL);
err_detach:
	dma_buf_detach(shm->dmabuf, attach);
err_dma_buf_put:
	dma_buf_put(shm->dmabuf);
err_shmfree:
	kfree(shm);
	return ret;
}

static int shmctrl_reclaim_buf(struct shmctrl_dev *ctrldev,
			       struct shmctrl_export_info *info)
			     /*struct dma_buf *dmabuf,
			     const char *buf_name)*/
{
	int ret;
	struct device *dev = &ctrldev->dev;
	struct shmbuf_info *shm = NULL;
	struct shmctrl_msg msg;
	struct list_head *l, *n;

	SHMCTRL_LOG_D("ioctl reclaim\n");
	if (list_empty(&ctrldev->buf_list)) {
		SHMCTRL_LOG_E("buffer list is empty\n");
		return -EINVAL;
	}

	mutex_lock(&ctrldev->buf_list_lock);
	list_for_each_safe(l, n, &ctrldev->buf_list) {
		struct shmbuf_info *tmp;

		tmp = list_entry(l, struct shmbuf_info, list);
		if (!strncmp(info->name, tmp->name, SHM_NAME_LEN)) {
			list_del(l);
			shm = tmp;
			break;
		}
	}

	mutex_unlock(&ctrldev->buf_list_lock);
	if (!shm) {
		dev_err(dev, "buffer not found in list\n");
		return -EINVAL;
	}

	msg.cmd = SHMCT_CMD_RECLAIM;
	msg.info.dma_addr = (u32)shm->dma_addr;
	msg.info.len = (u32)shm->size;
	strncpy(msg.info.name, info->name, sizeof(msg.info.name));
	msg.info.name[SHMCT_MAX_STR_SIZE - 1] = '\0';
	ret = shmctrl_rpmsg_send(&msg, ctrldev);
	if (ret)
		goto err;

	dma_buf_unmap_attachment(shm->attach, shm->sgt, DMA_BIDIRECTIONAL);
	dma_buf_detach(shm->dmabuf, shm->attach);
	dma_buf_put(shm->dmabuf);
	kfree(shm);

	return ret;
err:
	/* reclaim fail, add shmbuf back to buf_list */
	mutex_lock(&ctrldev->buf_list_lock);
	list_add_tail(&shm->list, &ctrldev->buf_list);
	mutex_unlock(&ctrldev->buf_list_lock);
	return ret;
}

static int shmctrl_open(struct inode *inode, struct file *filp)
{
	struct shmctrl_dev *ctrldev = cdev_to_ctrldev(inode->i_cdev);

	get_device(&ctrldev->dev);
	filp->private_data = ctrldev;

	return 0;
}

static int shmctrl_release(struct inode *inode, struct file *filp)
{
	struct shmctrl_dev *ctrldev = cdev_to_ctrldev(inode->i_cdev);

	put_device(&ctrldev->dev);

	return 0;
}

static long shmctrl_ioctl(struct file *fp, unsigned int cmd,
				unsigned long arg)
{
	int ret = 0;
	struct shmctrl_dev *ctrldev = fp->private_data;
	struct device *rproc_dev = ctrldev->rproc->dev.parent;
	struct shmctrl_export_info info;
	void __user *argp = (void __user *)arg;

	if (!rproc_dev)
		return -ENXIO;

	if (copy_from_user(&info, argp, sizeof(info))) {
		SHMCTRL_LOG_E("copy from user fail\n");
		return -EFAULT;
	}

	info.name[BUF_NAME_LEN - 1] = '\0';

	switch (cmd) {
	case SHMCTRL_IOC_SHARE:
		ret = shmctrl_share_buf(ctrldev, &info); //dmabuf, info.name);
		break;
	case SHMCTRL_IOC_RECLAIM:
		ret = shmctrl_reclaim_buf(ctrldev, &info); //dmabuf, info.name);
		break;
	default:
		SHMCTRL_LOG_E("invalid cmd\n");
		ret = -EINVAL;
	}

	return ret;
};

static const struct file_operations shmctrl_fops = {
	.owner = THIS_MODULE,
	.open = shmctrl_open,
	.release = shmctrl_release,
	.unlocked_ioctl = shmctrl_ioctl,
	.compat_ioctl = shmctrl_ioctl,
};

static void rpmsg_ctrldev_release_device(struct device *dev)
{
	struct shmctrl_dev *ctrldev = dev_to_ctrldev(dev);

	ida_simple_remove(&shm_ctrl_ida, dev->id);
	ida_simple_remove(&rpmsg_minor_ida, MINOR(dev->devt));
	kfree(ctrldev);
}

#define SHMCTRL_DUMP_ROW_SIZE 16
static int shmctrl_cb(struct rpmsg_device *rpdev, void *data, int len,
						void *priv, u32 src)
{
	struct shmctrl_dev *ctrldev = dev_get_drvdata(&rpdev->dev);
	struct shmctrl_msg *msg = (struct shmctrl_msg *)data;
	struct shmctrl_ctx *ctx = &ctrldev->ctx;

	switch (msg->cmd) {

	case SHMCT_CMD_ACK:
		mutex_lock(&ctrldev->ctx_lock);
		ctx->id = (u64)msg->host_hdl;
		ctx->err = msg->err;
		mutex_unlock(&ctrldev->ctx_lock);
		complete(&ctx->done);
		SHMCTRL_LOG_I("complete\n");
		break;
	default:
		SHMCTRL_LOG_E("unknown cmd %d\n", msg->cmd);
	}

	print_hex_dump_debug(__func__, DUMP_PREFIX_NONE, SHMCTRL_DUMP_ROW_SIZE,
			     1, data, len, true);
	return 0;
}

static int shmctrl_probe(struct rpmsg_device *rpdev)
{
	struct device *dev;
	int ret;
	struct rproc *rproc;
	struct shmctrl_dev *shmctrl_dev;

	rproc = rproc_get_by_child(&rpdev->dev);
	if (!rproc) {
		SHMCTRL_LOG_E("shmctrl device not child of rproc\n");
		return -EINVAL;
	}

	shmctrl_dev = kzalloc(sizeof(*shmctrl_dev), GFP_KERNEL);
	if (!shmctrl_dev)
		return -ENOMEM;

	shmctrl_dev->rproc = rproc;
	dev = &shmctrl_dev->dev;
	device_initialize(dev);
	dev->parent = &rpdev->dev;
	dev->class = rpmsg_class;

	cdev_init(&shmctrl_dev->cdev, &shmctrl_fops);
	shmctrl_dev->cdev.owner = THIS_MODULE;

	ret = ida_simple_get(&rpmsg_minor_ida, 0, RPMSG_DEV_MAX, GFP_KERNEL);
	if (ret < 0)
		goto free_ctrldev;

	dev->devt = MKDEV(MAJOR(rpmsg_major), ret);

	ret = ida_simple_get(&shm_ctrl_ida, 0, 0, GFP_KERNEL);
	if (ret < 0)
		goto free_minor_ida;

	dev->id = ret;
	if (rproc->name)
		dev_set_name(&shmctrl_dev->dev, "%s_%s", "shmctrl", rproc->name);
	else
		dev_set_name(&shmctrl_dev->dev, "%s%d", "shmctrl", ret);

	/* We can now rely on the release function for cleanup */
	dev->release = rpmsg_ctrldev_release_device;

	ret = cdev_device_add(&shmctrl_dev->cdev, dev);
	if (ret) {
		SHMCTRL_LOG_E("device_add failed: %d\n", ret);
		put_device(dev);
		goto free_ctrl_ida;
	}

	dev_set_drvdata(&rpdev->dev, shmctrl_dev);
	shmctrl_dev->rpdev = rpdev;
	INIT_LIST_HEAD(&shmctrl_dev->buf_list);
	mutex_init(&shmctrl_dev->buf_list_lock);

	/* ctx init */
	mutex_init(&shmctrl_dev->ctx_lock);
	init_completion(&shmctrl_dev->ctx.done);

	if (debugfs_initialized()) {
		mtk_shmctrl_dbg_dir = debugfs_create_dir(KBUILD_MODNAME, NULL);
		if (!mtk_shmctrl_dbg_dir) {
			SHMCTRL_LOG_E("can't create debugfs dir\n");
		} else {
			debugfs_create_file("log_level", 0600, mtk_shmctrl_dbg_dir, NULL,
				&mtk_shmctrl_log_level_ops);
		}
	}

	SHMCTRL_LOG_D("&ept = %lx\n", (unsigned long)rpdev->ept);

	return ret;

free_ctrl_ida:
	ida_simple_remove(&shm_ctrl_ida, dev->id);
free_minor_ida:
	ida_simple_remove(&rpmsg_minor_ida, MINOR(dev->devt));
free_ctrldev:
	kfree(shmctrl_dev);

	return ret;
}

static void shmctrl_remove(struct rpmsg_device *rpdev)
{
	struct shmctrl_dev *ctrldev = dev_get_drvdata(&rpdev->dev);

	cdev_device_del(&ctrldev->cdev, &ctrldev->dev);
	put_device(&ctrldev->dev);

	debugfs_remove_recursive(mtk_shmctrl_dbg_dir);
}

static const struct rpmsg_device_id shmctrl_match[] = {
	{ "shmctrl-ept" },
	{}
};

static struct rpmsg_driver mtk_shmctrl_driver = {
	.probe		= shmctrl_probe,
	.callback	= shmctrl_cb,
	.remove		= shmctrl_remove,
	.id_table	= shmctrl_match,
	.drv = {
		.name = "shmctrl-ept",
	},
};

static int shmctrl_init(void)
{
	int ret;

	ret = alloc_chrdev_region(&rpmsg_major, 0, RPMSG_DEV_MAX, "shmctrl");
	if (ret < 0) {
		SHMCTRL_LOG_E("rpmsg: failed to allocate char dev region\n");
		return ret;
	}

	rpmsg_class = class_create(THIS_MODULE, "shmctrl");
	if (IS_ERR(rpmsg_class)) {
		SHMCTRL_LOG_E("failed to create rpmsg class\n");
		unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX);
		return PTR_ERR(rpmsg_class);
	}

	ret = register_rpmsg_driver(&mtk_shmctrl_driver);
	if (ret < 0) {
		SHMCTRL_LOG_E("rpmsgchr: failed to register rpmsg driver\n");
		class_destroy(rpmsg_class);
		unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX);
	}

	return ret;
}
postcore_initcall(shmctrl_init);

static void shmctrl_exit(void)
{
	unregister_rpmsg_driver(&mtk_shmctrl_driver);
	class_destroy(rpmsg_class);
	unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX);
}
module_exit(shmctrl_exit);

module_param_named(log_level, u32DbgLevel, int, 0444);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Mark-PK Tsai <mark-pk.tsai@mediatek.com>");
MODULE_DESCRIPTION("MediaTek Share Memory Control driver");
