// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2019 Amlogic, Inc. All rights reserved.
 */

#include <linux/types.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/string.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/debugfs.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/kasan.h>
#include <linux/highmem.h>
#include <linux/uaccess.h>

struct aml_ddev {
	unsigned long cached_reg_addr;
	unsigned long size;
	int (*debugfs_reg_access)(struct aml_ddev *indio_dev,
				  unsigned long reg, unsigned int writeval,
				  unsigned int *readval);
};

static struct aml_ddev paddr_dev;
static struct aml_ddev pdump_dev;
static struct aml_ddev vaddr_dev;
static struct aml_ddev vdump_dev;

int aml_reg_access(struct aml_ddev *indio_dev,
		   unsigned long reg, unsigned int writeval,
		   unsigned int *readval)
{
	void __iomem *vaddr;

	reg = round_down(reg, 0x3);
	vaddr = ioremap(reg, 0x4);
	if (!vaddr)
		return -ENOMEM;

	if (readval)
		*readval = readl_relaxed(vaddr);
	else
		writel_relaxed(writeval, vaddr);
	iounmap(vaddr);
	return 0;
}

int aml_mem_access(struct aml_ddev *indio_dev,
		   unsigned long addr, unsigned int writeval,
		   unsigned int *readval)
{
	void *vaddr;
	struct page *page = pfn_to_page(addr >> PAGE_SHIFT);

	vaddr = kmap(page) + (addr & ~PAGE_MASK);
	kasan_disable_current();
	if (readval)
		*readval = *(unsigned int *)vaddr;
	else
		*(unsigned int *)vaddr = writeval;
	kasan_enable_current();
	kunmap(page);
	return 0;
}

int aml_vaddr_access(struct aml_ddev *indio_dev,
		   unsigned long vaddr, unsigned int writeval,
		   unsigned int *readval)
{
	int ret = 0;

	if (readval)
		ret = copy_from_kernel_nofault(readval, (void *)vaddr, sizeof(int));
	else
		memcpy((void *)vaddr, (void *)&writeval, sizeof(int));
	return ret;
}

static ssize_t paddr_read_file(struct file *file, char __user *userbuf,
			       size_t count, loff_t *ppos)
{
	struct aml_ddev *indio_dev = file->private_data;
	char buf[80];
	unsigned int val = 0;
	ssize_t len;
	int ret = -1;

	if (indio_dev->debugfs_reg_access)
		ret = indio_dev->debugfs_reg_access(indio_dev,
					   indio_dev->cached_reg_addr,
					   0, &val);
	if (ret)
		pr_err("%s: read failed\n", __func__);

	len = snprintf(buf, sizeof(buf), "[0x%lx] = 0x%X\n",
		       indio_dev->cached_reg_addr, val);

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

static ssize_t paddr_write_file(struct file *file, const char __user *userbuf,
				size_t count, loff_t *ppos)
{
	struct aml_ddev *indio_dev = file->private_data;
	unsigned long reg;
	unsigned int val;
	char buf[80];
	int ret;

	count = min_t(size_t, count, (sizeof(buf) - 1));
	if (copy_from_user(buf, userbuf, count))
		return -EFAULT;

	buf[count] = 0;

	ret = sscanf(buf, "%lx %x", &reg, &val);
#ifdef CONFIG_ARM64
	if (pfn_is_map_memory(reg >> PAGE_SHIFT))
#else
	if (pfn_valid(reg >> PAGE_SHIFT))
#endif
		indio_dev->debugfs_reg_access = aml_mem_access;
	else
		indio_dev->debugfs_reg_access = aml_reg_access;

	switch (ret) {
	case 1:
		indio_dev->cached_reg_addr = reg;
		break;
	case 2:
		indio_dev->cached_reg_addr = reg;
		ret = indio_dev->debugfs_reg_access(indio_dev, reg,
							  val, NULL);
		if (ret) {
			pr_err("%s: write failed\n", __func__);
			return ret;
		}
		break;
	default:
		return -EINVAL;
	}

	return count;
}

static const struct file_operations paddr_file_ops = {
	.open		= simple_open,
	.read		= paddr_read_file,
	.write		= paddr_write_file,
};

static ssize_t pdump_write_file(struct file *file, const char __user *userbuf,
			       size_t count, loff_t *ppos)
{
	struct seq_file *s = file->private_data;
	struct aml_ddev *indio_dev = s->private;
	unsigned long reg;
	unsigned int val;
	char buf[80];
	int ret;

	count = min_t(size_t, count, (sizeof(buf) - 1));
	if (copy_from_user(buf, userbuf, count))
		return -EFAULT;

	buf[count] = 0;

	ret = sscanf(buf, "%lx %i", &reg, &val);
#ifdef CONFIG_ARM64
	if (pfn_is_map_memory(reg >> PAGE_SHIFT))
#else
	if (pfn_valid(reg >> PAGE_SHIFT))
#endif
		indio_dev->debugfs_reg_access = aml_mem_access;
	else
		indio_dev->debugfs_reg_access = aml_reg_access;
	switch (ret) {
	case 2:
		indio_dev->cached_reg_addr = reg;
		indio_dev->size = val;
		break;
	default:
		return -EINVAL;
	}

	return count;
}

static int dump_show(struct seq_file *s, void *what)
{
	unsigned int  i = 0, val;
	int ret;
	struct aml_ddev *indio_dev = s->private;

	for (i = 0; i < indio_dev->size ; i++) {
		ret = indio_dev->debugfs_reg_access(indio_dev,
				  indio_dev->cached_reg_addr,
				  0, &val);
		if (ret)
			pr_err("%s: read failed\n", __func__);

		seq_printf(s, "[0x%lx] = 0x%X\n",
			   indio_dev->cached_reg_addr, val);
		indio_dev->cached_reg_addr += sizeof(int);
	}
	indio_dev->cached_reg_addr -= sizeof(int) * indio_dev->size;

	return 0;
}

static int dump_open(struct inode *inode, struct file *file)
{
	return single_open(file, dump_show, inode->i_private);
}

static const struct file_operations pdump_file_ops = {
	.open		= dump_open,
	.read		= seq_read,
	.write		= pdump_write_file,
	.llseek		= seq_lseek,
	.release	= single_release,
};

static ssize_t vaddr_read_file(struct file *file, char __user *userbuf,
			       size_t count, loff_t *ppos)
{
	struct aml_ddev *indio_dev = file->private_data;
	char buf[80];
	unsigned int val = 0;
	ssize_t len;
	int ret = -1;

	if (indio_dev->debugfs_reg_access)
		ret = indio_dev->debugfs_reg_access(indio_dev,
					   indio_dev->cached_reg_addr,
					   0, &val);
	if (ret)
		pr_err("%s: read failed\n", __func__);

	len = snprintf(buf, sizeof(buf), "[0x%lx] = 0x%X\n",
		       indio_dev->cached_reg_addr, val);

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

static ssize_t vaddr_write_file(struct file *file, const char __user *userbuf,
				size_t count, loff_t *ppos)
{
	struct aml_ddev *indio_dev = file->private_data;
	unsigned long reg;
	unsigned int val;
	char buf[80];
	int ret;

	count = min_t(size_t, count, (sizeof(buf) - 1));
	if (copy_from_user(buf, userbuf, count))
		return -EFAULT;

	buf[count] = 0;

	ret = sscanf(buf, "%lx %x", &reg, &val);

	indio_dev->debugfs_reg_access = aml_vaddr_access;

	switch (ret) {
	case 1:
		indio_dev->cached_reg_addr = reg;
		break;
	case 2:
		indio_dev->cached_reg_addr = reg;
		ret = indio_dev->debugfs_reg_access(indio_dev, reg,
					val, NULL);
		if (ret) {
			pr_err("%s: write failed,ret = %d\n", __func__, ret);
			return ret;
		}
		break;
	default:
		return -EINVAL;
	}

	return count;
}

static const struct file_operations vaddr_file_ops = {
	.open		= simple_open,
	.read		= vaddr_read_file,
	.write		= vaddr_write_file,
};

static ssize_t vdump_write_file(struct file *file, const char __user *userbuf,
			       size_t count, loff_t *ppos)
{
	struct seq_file *s = file->private_data;
	struct aml_ddev *indio_dev = s->private;
	unsigned long reg;
	unsigned int val;
	char buf[80];
	int ret;

	count = min_t(size_t, count, (sizeof(buf) - 1));
	if (copy_from_user(buf, userbuf, count))
		return -EFAULT;

	buf[count] = 0;

	ret = sscanf(buf, "%lx %i", &reg, &val);

	indio_dev->debugfs_reg_access = aml_vaddr_access;
	switch (ret) {
	case 2:
		indio_dev->cached_reg_addr = reg;
		indio_dev->size = val;
		break;
	default:
		return -EINVAL;
	}

	return count;
}

static const struct file_operations vdump_file_ops = {
	.open		= dump_open,
	.read		= seq_read,
	.write		= vdump_write_file,
	.llseek		= seq_lseek,
	.release	= single_release,
};

int __init aml_reg_init(void)
{
	static struct dentry *dir_aml_reg;

	if (IS_ENABLED(CONFIG_DEBUG_FS)) {
		dir_aml_reg = debugfs_create_dir("aml_reg", NULL);
		if (IS_ERR_OR_NULL(dir_aml_reg)) {
			pr_warn("failed to create debugfs directory\n");
			dir_aml_reg = NULL;
			return -ENOMEM;
		}
		debugfs_create_file("paddr", S_IFREG | 0440,
				    dir_aml_reg, &paddr_dev, &paddr_file_ops);
		debugfs_create_file("pdump", S_IFREG | 0440,
				    dir_aml_reg, &pdump_dev, &pdump_file_ops);
		debugfs_create_file("vaddr", S_IFREG | 0440,
				    dir_aml_reg, &vaddr_dev, &vaddr_file_ops);
		debugfs_create_file("vdump", S_IFREG | 0440,
				    dir_aml_reg, &vdump_dev, &vdump_file_ops);
	}

	return 0;
}

void __exit aml_reg_exit(void)
{
}
