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

//#define DEBUG
#include <linux/module.h>
#include <linux/seq_file.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/vmalloc.h>
#include <linux/highmem.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/file.h>

#define DEVICE_NAME		"audio_utils"
#define TEST_IOC_MAGIC		'T'
#define TEST_IOC_SET_LIB_SIZE	_IOW(TEST_IOC_MAGIC, 0x00, uint32_t)
#define TEST_IOC_WRITE_LIB	_IOW(TEST_IOC_MAGIC, 0x01, uint32_t)
#define TEST_IOC_FREE_LIB	_IOW(TEST_IOC_MAGIC, 0x02, uint32_t)

static LIST_HEAD(code_list);
static int written;
static int write_size;
static int lib_size;

static int audio_utils_open(struct inode *inode, struct file *file)
{
	pr_info("%s\n", __func__);
	return 0;
}

static ssize_t audio_utils_write(struct file *file, const char __user *buffer,
			      size_t _count, loff_t *ppos)
{
	struct page *page;
	int ret, count, write = 0;
	void *tmp;

	write_size = 0;
	count = _count;
	while (count > 0) {
		page = alloc_page(GFP_HIGHUSER);
		if (!page) {
			pr_err("%s-%d\n", __func__, __LINE__);
			return -ENOMEM;
		}
		pr_debug("%s page: %lx, count: %d\n",
			 __func__, page_to_pfn(page), count);
		tmp = kmap(page);
		WARN_ON(!tmp);
		if (count >= PAGE_SIZE) {
			ret = copy_from_user(tmp, buffer + write, PAGE_SIZE);
			if (ret) {
				kunmap(tmp);
				__free_page(page);
				pr_err("%s-%d write offset=%d size=%d total size=%zu ret=%d\n",
				       __func__, __LINE__, write, (int)PAGE_SIZE, _count, ret);
				return -EINVAL;
			}
			count -= PAGE_SIZE;
			write += PAGE_SIZE;
		} else if (count) { /* remain */
			ret = copy_from_user(tmp, buffer + write, count);
			if (ret) {
				kunmap(tmp);
				__free_page(page);
				pr_err("%s-%d write offset=%d size=%d total size=%zu ret=%d\n",
				       __func__, __LINE__, write, count, _count, ret);
				return -EINVAL;
			}
			count -= count;
			write += count;
		}
		list_add_tail(&page->lru, &code_list);
		kunmap(tmp);
	}
	write_size = _count;
	file->f_inode->i_size = write_size;
	pr_debug("%s write_size: %d\n", __func__, write_size);

	return _count;
}

static long audio_utils_ioctl(struct file *f,
			   unsigned int cmd, unsigned long arg)
{
	struct page *page, *next;
	void __user *argp = (void __user *)arg;

	/*
	 * TODO: here just free maintained pages, need more ioctrl commands
	 */
	switch (cmd) {
	case TEST_IOC_SET_LIB_SIZE:
		pr_info("%s SET_LIB_SIZE %ld\n", __func__, arg);
		lib_size = arg;
		break;

	case TEST_IOC_WRITE_LIB:
		pr_info("%s WRITE_LIB\n", __func__);
		if (written) {
			pr_err("%s have written\n", __func__);
			return -EINVAL;
		}

		if (audio_utils_write(f, argp, lib_size, NULL) != lib_size) {
			pr_err("%s, %d\n", __func__, __LINE__);
			list_for_each_entry_safe(page, next, &code_list, lru) {
				__free_page(page);
			}
			INIT_LIST_HEAD(&code_list);
			f->f_inode->i_size = 0;
			lib_size = 0;
			return -ENOMEM;
		}
		written = 1;
		break;

	case TEST_IOC_FREE_LIB:
		pr_info("%s FREE_LIB\n", __func__);
		written = 0;
		list_for_each_entry_safe(page, next, &code_list, lru) {
			__free_page(page);
		}
		INIT_LIST_HEAD(&code_list);
		f->f_inode->i_size = 0;
		lib_size = 0;
		break;
	default:
		break;
	}

	return 0;
}

static int audio_utils_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct page *page;
	unsigned long addr, size;
	int ret, total_pages;

	addr = vma->vm_start;
	size = vma->vm_end - vma->vm_start;
	total_pages = ALIGN(write_size, PAGE_SIZE) / PAGE_SIZE;
	if (vma->vm_pgoff > total_pages) {
		pr_err("%s wrong page off:%ld, total:%d, fsize:%d\n",
		       __func__, vma->vm_pgoff, total_pages, write_size);
		return -EINVAL;
	}
	page = list_first_entry(&code_list, struct page, lru);
	for (ret = 0; ret < vma->vm_pgoff; ret++) {	/* move to right page */
		page = list_next_entry(page, lru);
	}

	while (addr < vma->vm_end) {
		ret = vm_insert_page(vma, addr, page);
		pr_debug("%s insert page %5lx for %lx, ret:%d\n",
			 __func__, page_to_pfn(page), addr, ret);
		if (ret < 0) {
			pr_err("%s-%d\n", __func__, __LINE__);
			return ret;
		}
		addr += PAGE_SIZE;
		page = list_next_entry(page, lru);
		if (&page->lru == &code_list)
			break;
	}

	return 0;
}

#ifdef CONFIG_COMPAT
static long audio_utils_compact_ioctl(struct file *f,
				   unsigned int cmd, unsigned long arg)
{
	arg = (unsigned long)compat_ptr(arg);
	return audio_utils_ioctl(f, cmd, arg);
}
#endif

ssize_t audio_utils_read(struct file *file, char __user *buf,
		      size_t size, loff_t *ppos)
{
	int off = 0, off1, rd_size = 0, can_read, to = 0;
	struct page *page;
	void *tmp;

	pr_debug("%s off:%lx, size:%ld\n",
		 __func__, (unsigned long)*ppos, (unsigned long)size);
	page = list_first_entry(&code_list, struct page, lru);
	while (off < *ppos) {
		off += PAGE_SIZE;
		page = list_next_entry(page, lru);
	}
	off1 = *ppos & ~(PAGE_MASK);
	if (off1)
		can_read = PAGE_SIZE - off1;
	else
		can_read = PAGE_SIZE;
	rd_size = size;
	if (can_read >= rd_size)	/* 1st read align to page size */
		can_read = rd_size;

	while (rd_size > 0) {
		tmp = kmap(page);
		if (!tmp) {
			pr_err("%s-%d\n", __func__, __LINE__);
			return -EINVAL;
		}
		if (copy_to_user(buf + to, tmp, can_read)) {
			kunmap(tmp);
			pr_err("%s-%d\n", __func__, __LINE__);
			return -EINVAL;
		}
		pr_debug("%s buf:%p, to:%d, canread:%d, rdsize:%d, page:%lx\n",
			 __func__, buf, to, can_read,
			 rd_size, page_to_pfn(page));
		rd_size -= can_read;
		to += can_read;
		kunmap(tmp);
		if (rd_size >= PAGE_SIZE)
			can_read = PAGE_SIZE;
		else
			can_read = rd_size;
		page = list_next_entry(page, lru);
		if (&page->lru == &code_list)
			break;
	}
	*ppos += size;
	return size;
}

loff_t audio_utils_seek(struct file *file, loff_t offset, int whence)
{
	pr_debug("%s where:%d, off:%lx\n",
		 __func__, whence, (unsigned long)offset);
	return 0;
}

static int audio_utils_release(struct inode *inode, struct file *file)
{
	pr_info("%s\n", __func__);
	return 0;
}

static const struct file_operations audio_utils_fops = {
	.open		= audio_utils_open,
	.read		= audio_utils_read,
	.llseek		= audio_utils_seek,
	.unlocked_ioctl = audio_utils_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl   = audio_utils_compact_ioctl,
#endif
	.mmap		= audio_utils_mmap,
	.release	= audio_utils_release,
};

static struct class audio_utils_class = {
	.name			= DEVICE_NAME,
	.owner			= THIS_MODULE,
};

static int major;		/* major number we get from the kernel */
static int __init audio_utils_init(void)
{
	int r;
	struct device *cdev;

	r = class_register(&audio_utils_class);
	if (r) {
		pr_err("%s regist class failed\n", __func__);
		return -EINVAL;
	}
	major = register_chrdev(0, DEVICE_NAME, &audio_utils_fops);
	if (major < 0) {
		pr_err("%s register cdev failed\n", __func__);
		return -EINVAL;
	}
	cdev = device_create(&audio_utils_class, NULL,
			     MKDEV(major, 0), NULL, DEVICE_NAME);
	if (IS_ERR_OR_NULL(cdev)) {
		pr_err("%s alloc cdev failed, cdev:%p\n", __func__, cdev);
		return -EINVAL;
	}

	return 0;
}

static void __exit audio_utils_exit(void)
{
	device_destroy(&audio_utils_class, MKDEV(major, 0));
	unregister_chrdev(major, DEVICE_NAME);
	class_unregister(&audio_utils_class);
}

module_init(audio_utils_init);
module_exit(audio_utils_exit);
MODULE_LICENSE("GPL");
