// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2020 MediaTek Inc.
 * Author: Yu-Chang Wang <Yu-Chang.Wang@mediatek.com>
 */
#include <linux/device.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/string.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#include "clk-fhctl.h"
#include "clk-fhctl-pll.h"
#include "clk-fhctl-util.h"

enum FH_DEBUG_CMD_ID {
	FH_DBG_CMD_DVFS_API = 0x1002,
	FH_DBG_CMD_SSC_ENABLE = 0x1004,
	FH_DBG_CMD_SSC_DISABLE = 0x1005,
};

static bool has_perms;
static int __fh_ctrl_cmd_hdlr(struct pll_dts *array,
			unsigned int cmd,
			char *pll_name,
			unsigned int arg)
{
	int i;
	struct fh_hdlr *hdlr = NULL;
	int num_pll = array->num_pll;

	/* pll_name to pll_id */
	for (i = 0; i < num_pll; i++, array++) {
		FHDBG("<%s,%s>\n", pll_name, array->pll_name);
		if (strcmp(pll_name,
					array->pll_name) == 0) {
			hdlr = array->hdlr;
			FHDBG("hdlr<%lx>\n", (unsigned long)hdlr);
			break;
		}
	}

	if (!hdlr) {
		FHDBG("\n");
		return -1;
	}

	FHDBG("perms<%x,%lx,%lx>\n",
			array->perms, (unsigned long)PERM_DBG_HOP, (unsigned long)PERM_DBG_SSC);
	switch (cmd) {
	case FH_DBG_CMD_DVFS_API:
		if (array->perms & PERM_DBG_HOP)
			hdlr->ops->hopping(hdlr->data,
					array->domain,
					array->fh_id,
					arg, -1);
		break;
	case FH_DBG_CMD_SSC_ENABLE:
		if (array->perms & PERM_DBG_SSC)
			hdlr->ops->ssc_enable(hdlr->data,
					array->domain,
					array->fh_id,
					arg);
		break;
	case FH_DBG_CMD_SSC_DISABLE:
		if (array->perms & PERM_DBG_SSC)
			hdlr->ops->ssc_disable(hdlr->data,
					array->domain,
					array->fh_id);
		break;
	default:
		FHDBG(" Not Support CMD:%x\n", cmd);
		break;
	}
	return 0;
}

static bool prop_request(char *kbuf,
		struct pll_dts *array)
{
	unsigned int n, i, arg;
	int num_pll = array->num_pll;
	struct pll_dts *entry = NULL;
	char pll_name[32];
	char prop[32];

	/* retrieve prop/pll_name/arg from kbuf */
	n = sscanf(kbuf, "%s %31s %x", prop, pll_name, &arg);
		FHDBG("prop<%s>, pll_name<%s>, arg<%x>\n",
				prop, pll_name, arg);

	/* get entry by pll_name */
	for (i = 0; i < num_pll; i++, array++) {
		if (strcmp(pll_name,
					array->pll_name) == 0) {
			entry = array;
			break;
		}
	}

	if (!entry)
		return false;

	/* update entry by prop/arg */
	if (strstr(prop, "perms"))
		entry->perms = arg;
	else if (strstr(prop, "ssc-rate"))
		entry->ssc_rate = arg;
	else
		return false;

	return true;
}

static ssize_t fh_ctrl_proc_write(struct file *file,
				const char *buffer, size_t count, loff_t *data)
{
	int ret, n;
	char kbuf[256];
	char pll_name[32];
	size_t len = 0;
	unsigned int cmd, arg;
	struct pll_dts *array = file->f_inode->i_private;

	FHDBG("array<%lx>\n", (unsigned long)array);
	len = min(count, (sizeof(kbuf) - 1));

	FHDBG("count: %ld", count);
	if (count == 0)
		return -1;

	if (count > 255)
		count = 255;

	ret = copy_from_user(kbuf, buffer, count);
	if (ret < 0)
		return -1;

	kbuf[count] = '\0';

	/* permission control */
	if (strstr(kbuf, array->comp)) {
		has_perms = true;
		FHDBG("has_perms to true\n");
		return count;
	} else if (!has_perms) {
		FHDBG("!has_perms\n");
		return count;
	} else if (prop_request(kbuf, array)) {
		FHDBG("prop_request = true\n");
		return count;
	}

	n = sscanf(kbuf, "%x %31s %x", &cmd, pll_name, &arg);
	if ((n != 3) && (n != 2)) {
		FHDBG("error input format\n");
		return -EINVAL;
	}

	FHDBG("pll:%s cmd:0x%x arg:0x%x", pll_name, cmd, arg);
	__fh_ctrl_cmd_hdlr(array, cmd, pll_name, arg);

	return count;
}

static int fh_ctrl_proc_read(struct seq_file *m, void *v)
{
	int i;
	struct pll_dts *array = m->private;
	int num_pll = array->num_pll;

	seq_printf(m, "====== FHCTL CTRL, has_perms<%d>, comp<%s>======\n",
			has_perms, array->comp);

	seq_puts(m, "[Name fh-id perms ssc-rate]");
	seq_puts(m, "[domain method]");
	seq_puts(m, "[Hdlr]");
	seq_puts(m, "\n");

	for (i = 0; i < num_pll; i++, array++) {
		seq_printf(m, "<%s,%d,%x,%d>,<%s,%s>,<%lx>\n",
				array->pll_name, array->fh_id,
				array->perms, array->ssc_rate,
				array->domain, array->method,
				(unsigned long)array->hdlr);
	}
	return 0;
}

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

static const struct file_operations ctrl_fops = {
	.owner = THIS_MODULE,
	.open = fh_ctrl_proc_open,
	.read = seq_read,
	.write = fh_ctrl_proc_write,
	.release = single_release,
};

static int __sample_dds(struct fh_pll_regs *regs,
		struct fh_pll_data *data,
		unsigned int *dds_max,
		unsigned int *dds_min)
{
	int i, ssc_rate = 0;
	unsigned int mon_dds;
	unsigned int dds;

	mon_dds = readl(regs->reg_mon) & data->dds_mask;
	dds = readl(regs->reg_dds) & data->dds_mask;

	*dds_max = dds;
	*dds_min = mon_dds;

	/* Sample 200*10us */
	for (i = 0 ; i < 200 ; i++) {
		mon_dds = readl(regs->reg_mon) & data->dds_mask;

		if (mon_dds > *dds_max)
			*dds_max = mon_dds;

		if (mon_dds < *dds_min)
			*dds_min = mon_dds;

		udelay(10);
	}

	if ((*dds_max == 0) ||
		(*dds_min == 0))
		ssc_rate = 0;
	else {
		int diff = (*dds_max - *dds_min);

		ssc_rate = (diff * 1000) / *dds_max;
	}

	return ssc_rate;
}

static int fh_dumpregs_read(struct seq_file *m, void *v)
{
	int i;
	struct pll_dts *array = m->private;
	int num_pll = array->num_pll;

	FHDBG("array<%lx>\n", (unsigned long)array);
	seq_puts(m, "FHCTL dumpregs Read\n");

	for (i = 0; i < num_pll ; i++, array++) {
		struct fh_pll_domain *domain;
		struct fh_pll_regs *regs;
		struct fh_pll_data *data;
		int fh_id;
		char *pll_name;
		int ssc_rate;
		unsigned int dds_max, dds_min;

		if (!(array->perms & PERM_DBG_DUMP))
			continue;

		fh_id = array->fh_id;
		pll_name = array->pll_name;
		domain = get_fh_domain(array->domain);
		regs = &domain->regs[fh_id];
		data = &domain->data[fh_id];

		ssc_rate = __sample_dds(regs, data,
				&dds_max, &dds_min);

		seq_printf(m, "FH_ID:%d NAME:%s",
			fh_id, pll_name);
		seq_printf(m, " PCW:%x SSC_RATE:%d\n",
			readl(regs->reg_con_pcw) & data->dds_mask,
			ssc_rate);

		seq_puts(m, "HP_EN, CLK_CON, SLOPE0, SLOPE1\n");
		seq_printf(m, "0x%08x 0x%08x 0x%08x 0x%08x\n",
			readl(regs->reg_hp_en), readl(regs->reg_clk_con),
			readl(regs->reg_slope0), readl(regs->reg_slope1));

		seq_puts(m, "CFG, UPDNLMT, DDS, DVFS, MON\n");
		seq_printf(m, "0x%08x 0x%08x 0x%08x 0x%08x 0x%08x\n",
			readl(regs->reg_cfg), readl(regs->reg_updnlmt),
			readl(regs->reg_dds), readl(regs->reg_dvfs),
			readl(regs->reg_mon));
		seq_puts(m, "CON_PCW\n");
		seq_printf(m, "0x%08x\n",
			readl(regs->reg_con_pcw));

		seq_printf(m,
			"dds_max:0x%08x dds_mix:0x%08x ssc(1/1000):%d\n\n",
			dds_max, dds_min, ssc_rate);

		if (regs->reg_hp_en_set)
			seq_puts(m, "Support hp_en/rst_con/clk_con SET/CLR register!\n");
	}
	return 0;
}

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

static const struct file_operations dumpregs_fops = {
	.owner = THIS_MODULE,
	.open = fh_dumpregs_open,
	.read = seq_read,
	.release = single_release,
};

int fhctl_debugfs_init(struct pll_dts *array)
{
	struct dentry *root;
	struct dentry *fh_dumpregs_dir;
	struct dentry *fh_ctrl_dir;
	int i;
	int num_pll = array->num_pll;

	FHDBG("array<%lx>\n", (unsigned long)array);

	root = debugfs_create_dir("fhctl", NULL);
	if (IS_ERR(root)) {
		FHDBG("\n");
		return -1;
	}

	/* /sys/kernel/debug/fhctl/dumpregs */
	fh_dumpregs_dir = debugfs_create_file("dumpregs", 0664,
						root, array, &dumpregs_fops);
	if (IS_ERR(fh_dumpregs_dir)) {
		FHDBG("\n");
		return -1;
	}

	/* /sys/kernel/debug/fhctl/ctrl */
	fh_ctrl_dir = debugfs_create_file("ctrl", 0664,
						root, array, &ctrl_fops);
	if (IS_ERR(fh_ctrl_dir)) {
		FHDBG("\n");
		return -1;
	}

	/* init resources */
	for (i = 0; i < num_pll; i++, array++) {
		init_fh_domain(array->domain,
				array->comp,
				array->fhctl_base,
				array->apmixed_base);
	}

	return 0;
}
