/*
 * exynos-wow.c - Exynos Workload Watcher Driver
 *
 *  Copyright (C) 2021 Samsung Electronics
 *  Hanjun Shin <hanjun.shin@samsung.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <soc/samsung/exynos-wow.h>
#include <linux/of_address.h>

static struct exynos_wow_data *exynos_wow;
static char *exynos_wow_event_name[] = { "CCNT", "RW_ACITVE", "RW_DATA", "RW_REQUEST", "R_MO_COUNT" };

static int exynos_wow_set_polling(struct exynos_wow_data *data)
{
	schedule_delayed_work(&data->dwork, msecs_to_jiffies(data->polling_delay));

	return 0;
}

static int exynos_wow_start_polling(struct exynos_wow_data *data)
{
	schedule_delayed_work(&data->dwork, 0);

	return 0;
}

static int exynos_wow_stop_polling(struct exynos_wow_data *data)
{
	cancel_delayed_work(&data->dwork);

	return 0;
}

static int exynos_wow_set_counter(struct exynos_wow_data *data, int mode)
{
	int i, ret = 0;
	unsigned int value, offset;

	switch (mode) {
		case WOW_PPC_START:
			value = WOW_PPC_PMNC_GLB_CNT_EN | WOW_PPC_PMNC_Q_CH_MODE;
			offset = WOW_PPC_PMNC;
			break;
		case WOW_PPC_STOP:
			value = 0;
			offset = WOW_PPC_PMNC;
			break;
		case WOW_PPC_RESET:
			value = WOW_PPC_PMNC_RESET_CCNT_PMCNT;
			offset = WOW_PPC_PMNC;
			break;
		default:
			return -EINVAL;
	}

	for (i = 0; i < data->nr_ip; i++) {
		struct exynos_wow_ip_data *ip_data = &data->ip_data[i];
		int j;

		for (j = 0; j < ip_data->nr_base; j++) {
			void __iomem *base = ip_data->wow_base[j];

			// start counter
			writel(value, base + offset);
		}
	}

	return ret;
}

static inline int exynos_wow_start_counter(struct exynos_wow_data *data)
{
	return exynos_wow_set_counter(data, WOW_PPC_START);
}

static inline int exynos_wow_stop_counter(struct exynos_wow_data *data)
{
	return exynos_wow_set_counter(data, WOW_PPC_STOP);
}

static inline int exynos_wow_reset_counter(struct exynos_wow_data *data)
{
	return exynos_wow_set_counter(data, WOW_PPC_RESET);
}

static int exynos_wow_accumulator(struct exynos_wow_data *data)
{
	int i;
	u64 total_ccnt = 0, total_active = 0;

	exynos_wow_stop_counter(data);

	for (i = 0; i < data->nr_ip; i++) {
		struct exynos_wow_ip_data *ip_data = &data->ip_data[i];
		void __iomem *base = ip_data->wow_base[0];
		u64 rw_data, rw_request, active, ccnt, mo_count;

		// Get counter value
		ccnt = readl(base + WOW_PPC_CCNT);
		active = readl(base + WOW_PPC_PMCNT0);
		rw_data = readl(base + WOW_PPC_PMCNT1) * ip_data->nr_ppc;
		rw_request = readl(base + WOW_PPC_PMCNT2);
		mo_count = readl(base + WOW_PPC_PMCNT3_HIGH) & 0xff;
		mo_count = mo_count << 32 | readl(base + WOW_PPC_PMCNT3);

		// update per IP counter
		ip_data->wow_counter[CCNT] += ccnt; 
		ip_data->wow_counter[RW_ACTIVE] += active;
		ip_data->wow_counter[RW_DATA] += rw_data;
		ip_data->wow_counter[RW_REQUEST] += rw_request;
		ip_data->wow_counter[R_MO_COUNT] += mo_count;

		// update total counter
		if (!ip_data->monitor_only) {
			data->profile.transfer_data += (rw_data * ip_data->bus_width);

			if (total_active < active) {
				total_ccnt = ccnt;
				total_active = active;
			}

			if (ip_data->use_latency) {
				data->profile.nr_requests += rw_request;
				data->profile.mo_count += mo_count;
			}
		}
	}

	data->profile.ccnt += total_ccnt;
	data->profile.active += total_active;

	exynos_wow_reset_counter(data);
	exynos_wow_start_counter(data);

	return 0;
}

int exynos_wow_get_data(struct exynos_wow_profile *result)
{
	struct exynos_wow_data *data = exynos_wow;

	if (data == NULL)
		return -ENODEV;

	mutex_lock(&data->lock);

	exynos_wow_accumulator(data);

	memcpy(result, &data->profile, sizeof(struct exynos_wow_profile));

	mutex_unlock(&data->lock);

	return 0;
}
EXPORT_SYMBOL(exynos_wow_get_data);

static void exynos_wow_work(struct work_struct *work)
{
	struct exynos_wow_data *data =
		container_of(work, struct exynos_wow_data, dwork.work);

	mutex_lock(&data->lock);

	exynos_wow_accumulator(data);

	if (data->mode == WOW_ENABLED)
		exynos_wow_set_polling(data);

	mutex_unlock(&data->lock);
}

/* sysfs nodes for amb control */
#define sysfs_printf(...) count += snprintf(buf + count, PAGE_SIZE, __VA_ARGS__)

static ssize_t
exynos_wow_mode_show(struct device *dev, struct device_attribute *devattr,
		       char *buf)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	unsigned int count = 0;

	sysfs_printf("%s\n", data->mode ? "enabled" : "disabled");

	return count;
}

static ssize_t
exynos_wow_mode_store(struct device *dev, struct device_attribute *devattr,
			const char *buf, size_t count)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct exynos_wow_data *data = platform_get_drvdata(pdev);

	data->mode = true;

	return 0;
}

static ssize_t
exynos_wow_raw_counter_show(struct device *dev, struct device_attribute *devattr,
		       char *buf)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	unsigned int i, j, count = 0;

	sysfs_printf("IP_NAME ");

	for (i = 0; i < NUM_WOW_EVENT; i++)
		sysfs_printf("%s ", exynos_wow_event_name[i]);

	sysfs_printf("\n");

	for (i = 0; i < data->nr_ip; i++) {
		struct exynos_wow_ip_data *ip_data = &data->ip_data[i];

		sysfs_printf("%s ", ip_data->ppc_name);

		for (j = 0; j < NUM_WOW_EVENT; j++)
			sysfs_printf("%llu ", ip_data->wow_counter[j]);

		sysfs_printf("\n");
	}

	return count;
}

static ssize_t
exynos_wow_total_counter_show(struct device *dev, struct device_attribute *devattr,
		       char *buf)
{
	unsigned int i, count = 0;
	struct exynos_wow_profile profile;

	exynos_wow_get_data(&profile);

	sysfs_printf("CCNT | ACTIVE | TRANSFER_DATA | MO_COUNT | NR_REQUEST\n");

	for (i = 0; i < (sizeof(struct exynos_wow_profile) / sizeof(u64)); i++)
		sysfs_printf("%llu ", ((u64*)&profile)[i]);
	sysfs_printf("\n");


	return count;
}

static ssize_t
exynos_wow_start_show(struct device *dev, struct device_attribute *devattr,
		       char *buf)
{
	return 0;
}

static ssize_t
exynos_wow_start_store(struct device *dev, struct device_attribute *devattr,
			const char *buf, size_t count)
{
	struct exynos_wow_data *data = exynos_wow;

	exynos_wow_start_counter(data);
	return 0;
}

static ssize_t
exynos_wow_stop_show(struct device *dev, struct device_attribute *devattr,
		       char *buf)
{
	return 0;
}

static ssize_t
exynos_wow_stop_store(struct device *dev, struct device_attribute *devattr,
			const char *buf, size_t count)
{
	struct exynos_wow_data *data = exynos_wow;

	exynos_wow_stop_counter(data);
	return 0;
}

static DEVICE_ATTR(wow_mode, S_IWUSR | S_IRUGO,
		exynos_wow_mode_show, exynos_wow_mode_store);

static DEVICE_ATTR(raw_counter, S_IRUGO,
		exynos_wow_raw_counter_show, NULL);

static DEVICE_ATTR(total_counter, S_IRUGO,
		exynos_wow_total_counter_show, NULL);

static DEVICE_ATTR(start, S_IWUSR | S_IRUGO,
		exynos_wow_start_show, exynos_wow_start_store);

static DEVICE_ATTR(stop, S_IWUSR | S_IRUGO,
		exynos_wow_stop_show, exynos_wow_stop_store);

static struct attribute *exynos_wow_attrs[] = {
	&dev_attr_wow_mode.attr,
	&dev_attr_raw_counter.attr,
	&dev_attr_total_counter.attr,
	&dev_attr_start.attr,
	&dev_attr_stop.attr,
	NULL,
};

static const struct attribute_group exynos_wow_attr_group = {
	.attrs = exynos_wow_attrs,
};

static int exynos_wow_work_init(struct platform_device *pdev)
{
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	int ret = 0;

	/*
	if (!thermal_irq_wq) {
		attr.nice = 0;
		attr.no_numa = true;
		cpumask_copy(attr.cpumask, cpu_coregroup_mask(0));

		thermal_irq_wq = alloc_workqueue("%s", WQ_HIGHPRI | WQ_UNBOUND |\
				WQ_MEM_RECLAIM | WQ_FREEZABLE,
				0, "thermal_irq");
		apply_workqueue_attrs(thermal_irq_wq, &attr);
	}
	*/

	INIT_DELAYED_WORK(&data->dwork, exynos_wow_work);

	return ret;
}

static int exynos_wow_ip_init(struct platform_device *pdev)
{
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	int i, ret = 0;
	unsigned int value;

	for (i = 0; i < data->nr_ip; i++) {
		struct exynos_wow_ip_data *ip_data = &data->ip_data[i];
		int j;

		for (j = 0; j < ip_data->nr_base; j++) {
			void __iomem *base = ip_data->wow_base[j];
#ifdef USE_PPMU
			// Set PPMU event type
			writel(PPMU_PPC_EVENT_TYPE_R_ACTIVE, base + PPMU_PPC_EVENT_EV0_TYPE);
			writel(PPMU_PPC_EVENT_TYPE_RW_DATA, base + PPMU_PPC_EVENT_EV1_TYPE);
			writel(PPMU_PPC_EVENT_TYPE_RW_REQUEST, base + PPMU_PPC_EVENT_EV2_TYPE);
			writel(PPMU_PPC_EVENT_TYPE_R_MO_COUNT, base + PPMU_PPC_EVENT_EV3_TYPE);
#endif
			// Enable counters
			value = 1 << WOW_PPC_CNTENS_CCNT_OFFSET |
				1 << WOW_PPC_CNTENS_PMCNT0_OFFSET |
				1 << WOW_PPC_CNTENS_PMCNT1_OFFSET |
				1 << WOW_PPC_CNTENS_PMCNT2_OFFSET |
				1 << WOW_PPC_CNTENS_PMCNT3_OFFSET;
			writel(value, base + WOW_PPC_CNTENS);

			// Run counters
			writel(WOW_PPC_PMNC_GLB_CNT_EN | WOW_PPC_PMNC_Q_CH_MODE, base + WOW_PPC_PMNC);
		}
	}

	return ret;
}

static int exynos_wow_parse_dt(struct platform_device *pdev)
{
	struct device_node *np, *wow_ip, *child;
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	struct resource res;
	int i = 0;

	np = pdev->dev.of_node;
	if (of_property_read_u32(np, "polling_delay", &data->polling_delay))
		data->polling_delay = 60000;
	dev_info(&pdev->dev, "polling_delay (%u)\n", data->polling_delay);

	/* Get wow-ip info */
	wow_ip = of_find_node_by_name(np, "wow-ip");
	data->nr_ip = of_get_child_count(wow_ip);
	data->ip_data = kzalloc(sizeof(struct exynos_wow_ip_data) *
			data->nr_ip, GFP_KERNEL);

	for_each_available_child_of_node(wow_ip, child) {
		struct exynos_wow_ip_data *ip_data = &data->ip_data[i++];
		int index = 0, reg_index[10];

		if (of_property_read_u32(child, "bus_width", &ip_data->bus_width))
			ip_data->bus_width = 32;
		dev_info(&pdev->dev, "bus_width (%u)\n", ip_data->bus_width);

		if (of_property_read_u32(child, "nr_ppc", &ip_data->nr_ppc))
			ip_data->nr_ppc = 1;
		dev_info(&pdev->dev, "nr_ppc (%u)\n", ip_data->nr_ppc);

		if (of_property_read_u32(child, "nr_base", &ip_data->nr_base))
			ip_data->nr_base = 1;
		dev_info(&pdev->dev, "nr_base (%u)\n", ip_data->nr_base);

		if (of_property_read_bool(child, "monitor_only"))
			ip_data->monitor_only = true;
		dev_info(&pdev->dev, "monitor_only (%d)\n", ip_data->monitor_only);

		if (of_property_read_bool(child, "use_latency"))
			ip_data->use_latency = true;
		dev_info(&pdev->dev, "use_latency (%d)\n", ip_data->use_latency);

		strncpy(ip_data->ppc_name, child->name, PPC_NAME_LENGTH);

		of_property_read_u32_array(child, "reg_index", reg_index, ip_data->nr_base);

		ip_data->wow_base = kzalloc(sizeof(void __iomem *) * ip_data->nr_base, GFP_KERNEL);

		for (index = 0; index < ip_data->nr_base; index++) {
			of_address_to_resource(np, reg_index[index], &res);

			ip_data->wow_base[index] = devm_ioremap(&pdev->dev, res.start, resource_size(&res));
			dev_info(&pdev->dev, "paddr (0x%llx) vaddr (0x%llx)\n", res.start, ip_data->wow_base[index]);
		}
	}

	return 0;
}

static int exynos_wow_probe(struct platform_device *pdev)
{
	int ret = 0;

	exynos_wow = kzalloc(sizeof(struct exynos_wow_data), GFP_KERNEL);
	platform_set_drvdata(pdev, exynos_wow);

	ret = exynos_wow_parse_dt(pdev);

	if (ret) {
		dev_err(&pdev->dev, "failed to parse dt (%ld)\n", ret);
		kfree(exynos_wow);
		exynos_wow = NULL;
		return ret;
	}

	mutex_init(&exynos_wow->lock);

	exynos_wow_ip_init(pdev);

	exynos_wow_work_init(pdev);

//	wow->pm_notify.notifier_call = exynos_wow_pm_notify;
//	register_pm_notifier(&wow->pm_notify);

	ret = sysfs_create_group(&pdev->dev.kobj, &exynos_wow_attr_group);
	if (ret)
		dev_err(&pdev->dev, "cannot create exynos wow attr group");

	exynos_wow->mode = WOW_ENABLED;

	exynos_wow_start_polling(exynos_wow);

	dev_info(&pdev->dev, "Probe exynos wow successfully\n");

	return 0;
}

static int exynos_wow_remove(struct platform_device *pdev)
{
	struct exynos_wow_data *data = platform_get_drvdata(pdev);

	kfree(data);

	return 0;
}

static int exynos_wow_suspend(struct device *dev)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	int ret = 0;

	// lock
	mutex_lock(&data->lock);

	// disable wow
	data->mode = WOW_DISABLED;

	// unlock
	mutex_unlock(&data->lock);

	exynos_wow_stop_polling(data);
	exynos_wow_stop_counter(data);

	return ret;
}


static int exynos_wow_resume(struct device *dev)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	struct exynos_wow_data *data = platform_get_drvdata(pdev);
	int ret = 0;

	// lock
	mutex_lock(&data->lock);

	// enable wow
	exynos_wow_ip_init(pdev);
	data->mode = WOW_ENABLED;

	// unlock
	mutex_unlock(&data->lock);

	exynos_wow_start_polling(data);

	return ret;
}

static const struct of_device_id exynos_wow_match[] = {
	{ .compatible = "samsung,exynos-wow", },
	{ /* sentinel */ },
};

static const struct dev_pm_ops exynos_wow_pm_ops = {
	.suspend_late = exynos_wow_suspend,
	.resume_early = exynos_wow_resume,
};

static struct platform_driver exynos_wow_driver = {
	.driver = {
		.name   = "exynos-wow",
		.of_match_table = exynos_wow_match,
		.suppress_bind_attrs = true,
		.pm = &exynos_wow_pm_ops,
	},
	.probe = exynos_wow_probe,
	.remove	= exynos_wow_remove,
};
module_platform_driver(exynos_wow_driver);

MODULE_DEVICE_TABLE(of, exynos_wow_match);

MODULE_AUTHOR("Hanjun Shin <hanjun.shin@samsung.com>");
MODULE_DESCRIPTION("EXYNOS Workload Watcher Driver");
MODULE_LICENSE("GPL");