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

#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/dma-mapping.h>
#include <asm/cacheflush.h>
#include <linux/arm-smccc.h>
#include "dolby_fw.h"
#include <linux/amlogic/secmon.h>

#define DOLBY_FW_DEVICE_NAME   "dolby_fw"
#define DOLBY_FW_DRIVER_NAME   "dolby_fw"
#define DOLBY_FW_CLASS_NAME    "dolby_fw"
#define DOLBY_FW_MODULE_NAME   "dolby_fw"

static int major_id;
static struct class *class_dolby_fw;
static struct device *dev_dolby_fw;
static struct device *device;
static int mem_size;
static void __iomem *sharemem_in_base;
static void __iomem *sharemem_out_base;

#define IOCTL_SIGNATURE	0x10
#define IOCTL_DECRYPT		0x20
#define IOCTL_VERIFY		0x30
#define IOCTL_GET_SESSION_ID	0x40
#define IOCTL_VERIFY_LIB_RESP	0x41
#define IOCTL_AUDIO_LICENSE_QUERY 0x50
#define IOCTL_CRITICAL_QUERY 0x60

#define AUDIO_LICENSE_QUERY_MAX_SIZE	0x40
#define DOLBY_FW_CRITICAL_MAX_SIZE 0x400

struct dolby_fw_args {
	u64 src_addr;
	unsigned int src_len;
	u64 dest_addr;
	unsigned int dest_len;
	u64 arg1;
	u64 arg2;
	u64 arg3;
};

static int dolby_fw_smc_call(u64 function_id, u64 arg1, u64 arg2, u64 arg3)
{
	struct arm_smccc_res res;

	arm_smccc_smc(function_id, arg1, arg2, arg3, 0, 0, 0, 0, &res);

	return res.a0;
}

static int dolby_fw_open(struct inode *inode, struct file *file)
{
	return 0;
}

static int dolby_fw_release(struct inode *inode, struct file *file)
{
	return 0;
}

static int dolby_fw_signature(struct dolby_fw_args *info)
{
	int ret = -1;

	meson_sm_mutex_lock();

	if (info->src_len > mem_size)
		goto sig_err;
	ret = copy_from_user(sharemem_in_base,
			(void *)(uintptr_t)info->src_addr,
			info->src_len);
	if (ret != 0) {
		pr_err("%s:%d: copy signature data fail!\n",
					__func__, __LINE__);
		goto sig_err;
	}
	ret = dolby_fw_smc_call(DOLBY_FW_SIGNATURE, info->src_len, 0, 0);
sig_err:
	meson_sm_mutex_unlock();

	return ret;
}

static int dolby_fw_decrypt(struct dolby_fw_args *info)
{
	int ret = -1;
	int i, data_size, size, process_size;

	if (info->src_len != info->dest_len) {
		pr_err("%s:%d: args are error!\n",
					__func__, __LINE__);
		goto err;
	}
	data_size = info->src_len;

	meson_sm_mutex_lock();

	for (i = 0, size = 0; i <= data_size / mem_size; i++) {
		if (size + mem_size <= data_size)
			process_size = mem_size;
		else
			process_size = data_size - size;

		if (process_size > mem_size)
			goto err;
		ret = copy_from_user(sharemem_in_base,
				(void *)(uintptr_t)(info->src_addr + size),
				process_size);
		if (ret != 0) {
			pr_err("%s:%d: decrypt copy data fail!\n",
					__func__, __LINE__);
			goto err;
		}

		ret = dolby_fw_smc_call(DOLBY_FW_DECRYPT,
				0, 0, process_size);
		if (ret) {
			pr_err("%s:%d: audio firmware decrypt fail!\n",
					__func__, __LINE__);
			goto err;
		}
		if (process_size > info->dest_len)
			goto err;

		ret = copy_to_user((void *)(uintptr_t)(info->dest_addr + size),
				sharemem_out_base, process_size);
		if (ret != 0) {
			pr_err("%s:%d: decrypt write data fail!\n",
					__func__, __LINE__);
			goto err;
		}
		size += process_size;
	}
	ret = 0;
err:
	meson_sm_mutex_unlock();
	return ret;
}

static int dolby_fw_verify(struct dolby_fw_args *info)
{
	int ret = -1;
	int i, data_size, size, process_size, cnt;
	unsigned char pre_shared[32];

	if (info->dest_len < 0x20) {
		pr_err("%s:%d: verify dest len erro!\n",
					__func__, __LINE__);
		goto err;
	}
	if (!info->arg2) {
		pr_err("%s:%d: critical data size error!\n",
				__func__, __LINE__);
		goto err;
	}

	data_size = info->src_len;
	cnt = (data_size / mem_size);

	meson_sm_mutex_lock();
	for (i = 0, size = 0; i < cnt; i++) {
		ret = copy_from_user(sharemem_in_base,
				(void *)(uintptr_t)(info->src_addr + size),
				mem_size);
		if (ret != 0) {
			pr_err("%s:%d: verify copy data fail!\n",
					__func__, __LINE__);
			goto err1;
		}
		ret = dolby_fw_smc_call(DOLBY_FW_VERIFY_UPDATE,
				0, mem_size, 0);
		if (ret) {
			pr_err("%s:%d: audio firmware verify update fail!\n",
				__func__, __LINE__);
			goto err1;
		}
		size += mem_size;
	}

	process_size = data_size - size;
	if (process_size) {
		if (process_size > mem_size)
			goto err2;
		ret = copy_from_user(sharemem_in_base,
				(void *)(uintptr_t)(info->src_addr + size),
				process_size);
		if (ret != 0) {
			pr_err("%s:%d: verify copy data fail!\n",
					__func__, __LINE__);
			goto err2;
		}
	}

	ret = dolby_fw_smc_call(DOLBY_FW_VERIFY_FINAL, 0, process_size, 0);
	if (ret) {
		pr_err("%s:%d: audio firmware verify fail!\n",
				__func__, __LINE__);
		goto err2;
	}
	memcpy(pre_shared, (void *)sharemem_out_base, 0x20);

	/*critical data*/
	if (info->arg2 > mem_size)
		goto err2;
	ret = copy_from_user(sharemem_in_base,
						(void *)(uintptr_t)(info->arg1),
						info->arg2);
	if (ret != 0) {
		pr_err("%s:%d: critical copy data fail!\n",
				__func__, __LINE__);
		goto err2;
	}
	ret = dolby_fw_smc_call(DOLBY_FW_VERIFY_CRITICAL, info->arg2, 0, info->arg3);
	if (ret) {
		pr_err("%s:%d: verify critical fail!\n",
				__func__, __LINE__);
		goto err2;
	}

	/* copy share memory output for pre_shared_secret */
	ret = copy_to_user((void *)(uintptr_t)(info->dest_addr),
				(void *)pre_shared, 0x20);
	if (ret != 0) {
		pr_err("%s:%d: verify copy data fail!\n",
				__func__, __LINE__);
		ret = -1;
		goto err2;
	}

err2:
err1:
	meson_sm_mutex_unlock();
err:
	return ret;
}

static int dolby_fw_get_session_id(struct dolby_fw_args *info)
{
	int ret = -1;

	meson_sm_mutex_lock();
	dolby_fw_smc_call(DOLBY_FW_GET_SESSION_ID, 0, 0, 0);
	ret = copy_to_user((void *)(uintptr_t)(info->dest_addr),
				sharemem_out_base, 16 + 4);
	if (ret != 0) {
		pr_err("%s:%d: get session id fail!\n",
				__func__, __LINE__);
		ret = -1;
	}
	meson_sm_mutex_unlock();
	return ret;
}

static int dolby_fw_verify_lib_resp(struct dolby_fw_args *info)
{
	int ret = -1;

	if (info->src_len < (16 + 32) || info->src_len > 0x1000) {
		pr_err("verify lib resp input len error!\n");
		goto err;
	}
	if (info->dest_len < 32) {
		pr_err("verify lib resp output len error!\n");
		goto err;
	}

	meson_sm_mutex_lock();
	ret = copy_from_user(sharemem_in_base,
				(void *)(uintptr_t)info->src_addr,
				info->src_len);
	if (ret != 0) {
		pr_err("%s:%d: copy verify lib resp fail!\n",
					__func__, __LINE__);
		goto err1;
	}
	ret = dolby_fw_smc_call(DOLBY_FW_VERIFY_LIB_RESP, 0, 0, info->arg3);
	if (ret) {
		pr_err("%s:%d: verify lib resp fail %d!\n",
					__func__, __LINE__, ret);
		ret = -1;
		goto err1;
	}
	ret = copy_to_user((void *)(uintptr_t)(info->dest_addr),
			sharemem_out_base, 32);
	if (ret != 0) {
		pr_err("%s:%d: verify lib resp return fail!\n",
			__func__, __LINE__);
		ret = -1;
	}
err1:
	meson_sm_mutex_unlock();
err:
	return ret;
}

static unsigned int dolby_fw_audio_license_query(struct dolby_fw_args *info)
{
	unsigned int ret = 0;
	unsigned int outlen;

	if (info->src_len > AUDIO_LICENSE_QUERY_MAX_SIZE) {
		pr_err("%s:%d: audio license query src length error!\n",
					__func__, __LINE__);
		ret = 0;
		goto err;
	}

	meson_sm_mutex_lock();
	ret = copy_from_user(sharemem_in_base,
			(void *)(uintptr_t)info->src_addr,
			info->src_len);
	if (ret != 0) {
		pr_err("%s:%d: copy audio license query fail!\n",
					__func__, __LINE__);
		ret = 0;
		goto err1;
	}
	outlen = dolby_fw_smc_call(DOLBY_FW_AUDIO_LICENSE_QUERY,
			info->src_len, info->dest_len, 0);
	if (!outlen || info->dest_len < outlen) {
		pr_err("%s:%d: audio license query fail!\n",
			__func__, __LINE__);
		goto err1;
	}
	ret = copy_to_user((void *)(uintptr_t)(info->dest_addr),
			sharemem_out_base, outlen);
	if (ret != 0) {
		pr_err("%s:%d: audio license query fail!\n",
			__func__, __LINE__);
		ret = 0;
		goto err1;
	}
	ret = outlen;
err1:
	meson_sm_mutex_unlock();
err:
	return ret;
}

static int dolby_fw_critical_query(struct dolby_fw_args *info)
{
	int ret = 0;
	unsigned int size;

	if (info->src_len != 0) {
		if (info->src_len > DOLBY_FW_CRITICAL_MAX_SIZE) {
			pr_err("%s:%d: critical query src length error!\n",
					__func__, __LINE__);
			ret = 0;
			goto err;
		}
	}

	meson_sm_mutex_lock();

	if (info->src_len) {
		ret = copy_from_user(sharemem_in_base,
				(void *)(uintptr_t)info->src_addr,
				info->src_len);
		if (ret != 0) {
			pr_err("%s:%d: copy critical src buffer fail!\n",
					__func__, __LINE__);
			ret = 0;
			goto err1;
		}
	}

	size = dolby_fw_smc_call(DOLBY_FW_CRITICAL_DATA_QUERY,
			info->src_len, 0, info->arg3);
	if (!size) {
		pr_err("%s:%d: query critical fail!\n",
				__func__, __LINE__);
		goto err1;
	}

	if (info->dest_len < size || size > DOLBY_FW_CRITICAL_MAX_SIZE) {
		pr_err("%s:%d: critical size error!\n",
				__func__, __LINE__);
		goto err1;
	}
	ret = copy_to_user((void *)(uintptr_t)(info->dest_addr),
				sharemem_out_base, size);
	if (ret != 0) {
		pr_err("%s:%d: get critical fail!\n",
				__func__, __LINE__);
		ret = 0;
		goto err1;
	}
	ret = size;

err1:
	meson_sm_mutex_unlock();
err:
	return ret;
}

static long dolby_fw_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	void __user	*argp = (void __user *)arg;
	struct dolby_fw_args info;
	int ret;

	if (!argp) {
		pr_err("%s:%d,wrong param\n",
				__func__, __LINE__);
		return -1;
	}

	ret = copy_from_user(&info, (struct dolby_fw_args *)argp,
				sizeof(struct dolby_fw_args));
	if (ret != 0) {
		pr_err("%s:%d,copy_from_user fail\n",
			__func__, __LINE__);
		return ret;
	}

	switch (cmd) {
	case IOCTL_SIGNATURE:
		ret = dolby_fw_signature(&info);
		break;
	case IOCTL_DECRYPT:
		ret = dolby_fw_decrypt(&info);
		break;
	case IOCTL_VERIFY:
		ret = dolby_fw_verify(&info);
		break;
	case IOCTL_GET_SESSION_ID:
		ret = dolby_fw_get_session_id(&info);
		break;
	case IOCTL_VERIFY_LIB_RESP:
		ret = dolby_fw_verify_lib_resp(&info);
		break;
	case IOCTL_AUDIO_LICENSE_QUERY:
		ret = dolby_fw_audio_license_query(&info);
		break;
	case IOCTL_CRITICAL_QUERY:
		ret = dolby_fw_critical_query(&info);
		break;
	default:
		return -ENOTTY;
	}

	return ret;
}

struct file_operations const dolby_fw_fops = {
	.owner = THIS_MODULE,
	.open = dolby_fw_open,
	.release = dolby_fw_release,
	.unlocked_ioctl = dolby_fw_ioctl,
	.compat_ioctl = dolby_fw_ioctl,
};

static int dolby_fw_probe(struct platform_device *pdev)
{
	int ret = 0;
	unsigned int val;
	int sharemem_in, sharemem_out;

	device = &pdev->dev;
	platform_set_drvdata(pdev, NULL);

	ret = register_chrdev(0, DOLBY_FW_DEVICE_NAME,
						&dolby_fw_fops);
	if (ret < 0) {
		pr_err("Can't register %s device", DOLBY_FW_DEVICE_NAME);
		ret = -ENODEV;
		goto err;
	}
	major_id = ret;

	class_dolby_fw = class_create(THIS_MODULE,
						DOLBY_FW_DEVICE_NAME);
	if (IS_ERR(class_dolby_fw)) {
		ret = PTR_ERR(class_dolby_fw);
		goto err1;
	}

	dev_dolby_fw = device_create(class_dolby_fw, NULL,
					MKDEV(major_id, 0),
					NULL, DOLBY_FW_DEVICE_NAME);
	if (IS_ERR(dev_dolby_fw)) {
		ret = PTR_ERR(dev_dolby_fw);
		goto err2;
	}

	ret = of_property_read_u32(pdev->dev.of_node, "mem_size", &val);
	if (ret) {
		dev_err(&pdev->dev, "config mem_size in dts\n");
		ret = -1;
		goto err3;
	}
	mem_size = val;
	sharemem_in = get_secmon_sharemem_in_size();
	sharemem_out = get_secmon_sharemem_out_size();
	if (mem_size > sharemem_in)
		mem_size = sharemem_in;
	if (mem_size > sharemem_out)
		mem_size = sharemem_out;
	sharemem_in_base = get_meson_sm_input_base();
	sharemem_out_base = get_meson_sm_output_base();
	if (!sharemem_in_base || !sharemem_out_base)
		goto err3;

	return 0;

err3:
	device_destroy(class_dolby_fw, MKDEV(major_id, 0));
err2:
	class_destroy(class_dolby_fw);
err1:
	unregister_chrdev(major_id, DOLBY_FW_DEVICE_NAME);
err:
	return ret;
}

static int dolby_fw_remove(struct platform_device *pdev)
{
	device_destroy(class_dolby_fw, MKDEV(major_id, 0));
	class_destroy(class_dolby_fw);
	unregister_chrdev(major_id, DOLBY_FW_DEVICE_NAME);
	platform_set_drvdata(pdev, NULL);
	return 0;
}

static const struct of_device_id amlogic_dolby_fw_dt_match[] = {
	{	.compatible = "amlogic, dolby_fw",
	},
	{},
};

static struct platform_driver dolby_fw_driver = {
	.probe = dolby_fw_probe,
	.remove = dolby_fw_remove,
	.driver = {
		.name = DOLBY_FW_DEVICE_NAME,
		.of_match_table = amlogic_dolby_fw_dt_match,
		.owner = THIS_MODULE,
	},
};

int __init dolby_fw_init(void)
{
	int ret = -1;

	ret = platform_driver_register(&dolby_fw_driver);
	if (ret != 0) {
		pr_err("failed to register dolby fw driver, error %d\n", ret);
		return -ENODEV;
	}
	return ret;
}

/* module unload */
void __exit dolby_fw_exit(void)
{
	platform_driver_unregister(&dolby_fw_driver);
}
