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

#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/clk.h>
#include <linux/slab.h>
#include <linux/clk-provider.h>
#include <linux/uaccess.h>
#include <linux/amlogic/clk_measure.h>
#include <linux/kallsyms.h>
#include <linux/arm-smccc.h>

static struct clk *debug_clk;

static ssize_t parent_write(struct file *file, const char __user *buffer,
			    size_t count, loff_t *ppos)
{
	int ret;
	char clk_name[20];
	char input[20];
	struct clk *clk;

	if (count >= sizeof(input)) {
		pr_err("wrong notice: clk_name too long  > 20, pls shorten clk_name\n");
		return -EINVAL;
	}

	if (copy_from_user(input, buffer, count))
		return -EFAULT;

	input[count] = '\0';

	ret = sscanf(input, "%s", clk_name);
	if (ret != 1) {
		pr_err("wrong usage, try: echo clk_name  > clk\n");
		return -EINVAL;
	}

	clk = clk_get_sys(clk_name, NULL);
	if (IS_ERR_OR_NULL(clk)) {  /* error code or NULL may be returned */
		pr_err("Can't find the clock, have a look in /sys/kernel/debug/clk\n");
		return -EPERM;
	}

	if (debug_clk) {
		ret = clk_set_parent(debug_clk, clk);
		if (ret < 0)
			pr_err("failed to set parent for %s\n",
			       __clk_get_name(debug_clk));
		else
			pr_info("now %s parent is %s\n",
				__clk_get_name(debug_clk),
				__clk_get_name(clk_get_parent(debug_clk)));
	} else {
		pr_err("try: echo clk_name > clk,echo clk_name > parent\n");
	}

	return count;
}

static ssize_t measure_write(struct file *file, const char __user *buffer,
			     size_t count, loff_t *ppos)
{
	int ret;
	char input[5];
	unsigned int id = 0, rate;

	if (count == 1)
		return -EFAULT;

	count = min_t(size_t, count, (sizeof(input) - 1));
	if (copy_from_user(input, buffer, count))
		return -EFAULT;

	input[count] = '\0';

	ret = kstrtouint(input, 0, &id);
	if (ret)
		return -EINVAL;

	rate = meson_clk_measure(id);
	pr_info("Measure the index %u clock,its rate = %u\n", id, rate);

	return count;
}

static ssize_t enable_read(struct file *file, char __user *buffer,
			   size_t count, loff_t *ppos)
{
	char buf[4];
	unsigned int len;

	len = sprintf(buf, "%d\n", __clk_is_enabled(debug_clk) ? 1 : 0);

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

static ssize_t enable_write(struct file *file, const char __user *buffer,
			    size_t count, loff_t *ppos)
{
	int ret;
	char *input;
	unsigned int enable = 0;

	input = kzalloc(count, GFP_KERNEL);
	if (!input) {
		kfree(input);
		return -ENOMEM;
	}
	if (copy_from_user(input, buffer, count)) {
		kfree(input);
		return -EFAULT;
	}

	ret = kstrtouint(input, 0, &enable);
	if (ret) {
		kfree(input);
		return -EINVAL;
	}

	if (enable != 0)
		clk_prepare_enable(debug_clk);
	else
		clk_disable_unprepare(debug_clk);

	kfree(input);
	return count;
}

static ssize_t rate_read(struct file *file, char __user *buffer,
			 size_t count, loff_t *ppos)
{
	/*
	 * the rate unit is HZ, need 16 char
	 */
	char buf[16];
	unsigned int len;

	len = sprintf(buf, "%lu\n", clk_get_rate(debug_clk));

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

static ssize_t rate_write(struct file *file, const char __user *buffer,
			  size_t count, loff_t *ppos)
{
	int ret;
	char *input;
	unsigned long rate = 0;

	input = kzalloc(count, GFP_KERNEL);
	if (!input) {
		kfree(input);
		return -ENOMEM;
	}

	if (copy_from_user(input, buffer, count)) {
		kfree(input);
		return -EFAULT;
	}
	ret = kstrtoul(input, 0, &rate);
	if (ret) {
		kfree(input);
		return -EINVAL;
	}
	pr_info("input rate = %lu, old rate = %lu\n", rate,
		clk_get_rate(debug_clk));

	ret = clk_set_rate(debug_clk, rate);
	if (ret) {
		kfree(input);
		pr_err("failed to set rate,ret =%d\n", ret);
		return ret;
	}

	pr_info("current rate = %lu\n", clk_get_rate(debug_clk));

	kfree(input);

	return count;
}

static ssize_t clk_read(struct file *file, char __user *buffer,
			size_t count, loff_t *ppos)
{
	char buf[48];
	unsigned int len;

	len = sprintf(buf, "get %s clock, its rate = %lu\n",
		      __clk_get_name(debug_clk),
		      clk_get_rate(debug_clk));

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

static ssize_t clk_write(struct file *file, const char __user *buffer,
			 size_t count, loff_t *ppos)
{
	int ret;
	char clk_name[20];
	char input[20];
	struct clk *clk;

	if (count >= sizeof(input)) {
		pr_err("wrong notice: clk_name too long  > 20, pls shorten clk_name\n");
		return -EINVAL;
	}

	if (copy_from_user(input, buffer, count))
		return -EFAULT;

	input[count] = '\0';

	ret = sscanf(input, "%s", clk_name);
	if (ret != 1) {
		pr_err("wrong usage, try: echo clk_name  > clk\n");
		return -EINVAL;
	}

	clk = clk_get_sys(clk_name, NULL);
	/* error code or NULL may be returned */
	if (!IS_ERR_OR_NULL(clk)) {
		pr_info("success get %s clock, its rate = %lu, its parent is %s\n",
			clk_name, clk_get_rate(clk), __clk_get_name(clk_get_parent(clk)));
		/* store the clk pointer */
		if (debug_clk)
			clk_put(debug_clk);

		debug_clk = clk;
	} else {
		pr_err("Can't find the clock, have a look in /sys/kernel/debug/clk.\n");
	}

	return count;
}

static ssize_t secure_reg_write(struct file *file, const char __user *buffer,
				size_t count, loff_t *ppos)
{
	int ret;
	char *input;
	unsigned long reg = 0;
	struct arm_smccc_res res;

	input = kzalloc(count, GFP_KERNEL);
	if (!input) {
		kfree(input);
		return -ENOMEM;
	}

	if (copy_from_user(input, buffer, count)) {
		kfree(input);
		return -EFAULT;
	}
	ret = kstrtoul(input, 0, &reg);
	if (ret) {
		kfree(input);
		return -EINVAL;
	}

	arm_smccc_smc(0x82000099, 0x30,
		      reg, 0, 0, 0, 0, 0, &res);

	kfree(input);

	return count;
}

static const struct file_operations parent_file_ops = {
	.open		= simple_open,
	.write		= parent_write,
};

static const struct file_operations measure_file_ops = {
	.open		= simple_open,
	.write		= measure_write,
};

static const struct file_operations enable_file_ops = {
	.open		= simple_open,
	.read		= enable_read,
	.write		= enable_write,
};

static const struct file_operations rate_file_ops = {
	.open		= simple_open,
	.read		= rate_read,
	.write		= rate_write,
};

static const struct file_operations clk_file_ops = {
	.open		= simple_open,
	.read		= clk_read,
	.write		= clk_write,
};

static const struct file_operations secure_file_ops = {
	.open		= simple_open,
	.write		= secure_reg_write,
};

int __init clk_debug_init(void)
{
	struct dentry *root;

	root = debugfs_create_dir("clk_debug", NULL);
	debugfs_create_file("clk", 0600, root, NULL, &clk_file_ops);
	debugfs_create_file("rate", 0600, root, NULL, &rate_file_ops);
	debugfs_create_file("enable", 0600, root, NULL, &enable_file_ops);
	debugfs_create_file("parent", 0200, root, NULL, &parent_file_ops);
	debugfs_create_file("measure", 0200, root, NULL, &measure_file_ops);
	debugfs_create_file("secure_reg", 0200, root, NULL, &secure_file_ops);

	return 0;
}

#ifndef MODULE
late_initcall_sync(clk_debug_init);
#endif

MODULE_DESCRIPTION("Amlogic meson clock debug driver");
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("jian hu <jian.hu@amlogic.com>");
