// SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause)
/*
 * Copyright (c) 2023 MediaTek Inc.
 *
 */

#include <linux/clk.h>
#include <linux/hrtimer.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/perf_event.h>
#include <linux/slab.h>
#include <linux/of_platform.h>
#include <linux/io.h>
#include <linux/of_address.h>
#include <linux/device.h>
#include "emi_pmu.h"
#include "emi_plt.h"

static DEFINE_IDA(emi_pmu_ida);
static struct mtk_emi_pmu *emi_pmu;
void __iomem *emi_base_remap_addr;

/*
 * Default polling period is set to 100ms.
 */
unsigned int emi_pmu_poll_period_us = EMI_PMU_DEFAULT_POLLING_PERIOD_US;

module_param_named(pmu_pmu_poll_period_us, emi_pmu_poll_period_us, uint, 0644);

ktime_t emi_pmu_timer_period(void)
{
	return ns_to_ktime((u64)emi_pmu_poll_period_us * TIME_TRANS_US_TO_NS);
}

PMU_EVENT_ATTR_STRING(total_bw, emi_pmu_total_bw, "event=0x00")
//PMU_EVENT_ATTR_STRING(total_bw.unit, emi_pmu_total_bw_unit, "bytes");
PMU_EVENT_ATTR_STRING(cpu_bw, emi_pmu_cpu_bw, "event=0x01")
//PMU_EVENT_ATTR_STRING(cpu_bw.unit, emi_pmu_cpu_bw_unit, "bytes");
PMU_EVENT_ATTR_STRING(gpu_bw, emi_pmu_gpu_bw, "event=0x02")
//PMU_EVENT_ATTR_STRING(gpu_bw.unit, emi_pmu_gpu_bw_unit, "bytes");
PMU_EVENT_ATTR_STRING(mm_bw, emi_pmu_mm_bw, "event=0x03")
//PMU_EVENT_ATTR_STRING(mm_bw.unit, emi_pmu_mm_bw_unit, "bytes");
PMU_EVENT_ATTR_STRING(other_bw, emi_pmu_other_bw, "event=0x04")
//PMU_EVENT_ATTR_STRING(other_bw.unit, emi_pmu_other_bw_unit, "bytes");
PMU_EVENT_ATTR_STRING(cpu_trans, emi_pmu_cpu_transactions, "event=0x05")
PMU_EVENT_ATTR_STRING(cpu_total_latency, emi_pmu_cpu_total_latency, "event=0x06")

static ssize_t emi_pmu_cpumask_show(struct device *dev,
		struct device_attribute *attr, char *buf)
{
	struct mtk_emi_pmu *pmu = dev_get_drvdata(dev);

	return cpumap_print_to_pagebuf(true, buf, &pmu->cpu);
}

static struct device_attribute emi_pmu_cpumask_attr =
	__ATTR(cpumask, 0444, emi_pmu_cpumask_show, NULL);

static struct attribute *emi_pmu_cpumask_attrs[] = {
	&emi_pmu_cpumask_attr.attr,
	NULL,
};

static struct attribute_group emi_pmu_cpumask_attr_group = {
	.attrs = emi_pmu_cpumask_attrs,
};

static struct attribute *emi_pmu_events_attrs[] = {
	&emi_pmu_total_bw.attr.attr,
	&emi_pmu_cpu_bw.attr.attr,
	&emi_pmu_gpu_bw.attr.attr,
	&emi_pmu_mm_bw.attr.attr,
	&emi_pmu_other_bw.attr.attr,
	&emi_pmu_cpu_transactions.attr.attr,
	&emi_pmu_cpu_total_latency.attr.attr,
	NULL,
};

static struct attribute_group emi_pmu_events_attr_group = {
	.name = "events",
	.attrs = emi_pmu_events_attrs,
};

PMU_FORMAT_ATTR(event, "config:0-7");
//PMU_FORMAT_ATTR(rw_mon_type, "config1:0-7");
//PMU_FORMAT_ATTR(bus_id, "config1:8-15");
//PMU_FORMAT_ATTR(sample_period_ms, "config1:16-23");

static struct attribute *emi_pmu_format_attrs[] = {
	&format_attr_event.attr,
//	&format_attr_rw_mon_type.attr,
//	&format_attr_bus_id.attr,
//	&format_attr_sample_period_ms.attr,
	NULL,
};

static struct attribute_group emi_pmu_format_attr_group = {
	.name = "format",
	.attrs = emi_pmu_format_attrs,
};

static const struct attribute_group *attr_groups[] = {
	&emi_pmu_events_attr_group,
	&emi_pmu_format_attr_group,
	&emi_pmu_cpumask_attr_group,
	NULL,
};

void emi_pmu_event_update(struct perf_event *event)
{
	u32 counter = emi_pmu_read_counter(event->attr.config);

	//printk("read %d counter: %u\n", (u32)event->attr.config, counter);

	local64_add(counter, &event->count);
}

void emi_pmu_event_start(struct perf_event *event, int flags)
{
	struct hw_perf_event *hwc = &event->hw;
	struct mtk_emi_pmu *mtk_pmu = to_mtk_pmu(event->pmu);
	//u32 sample_period = (event->attr.config1 >> 8) & 0xFFFF;

	emi_mon_start();
	/* Specify hrtimer_period_ms by perf filter */
	//if (sample_period)
	//	mtk_pmu->hrtimer_period_ms = sample_period;
	//printk("%s: hrtimer_period_ms: %d\n", __func__, mtk_pmu->hrtimer_period_ms);
	/* start hrtimer to profile emi counter */
	hrtimer_start(&mtk_pmu->hrtimer, emi_pmu_timer_period(), HRTIMER_MODE_REL_PINNED);
	hwc->state = 0;
}

int emi_pmu_event_add(struct perf_event *event, int flags)
{
	struct mtk_emi_pmu *mtk_pmu = to_mtk_pmu(event->pmu);
	struct hw_perf_event *hwc = &event->hw;
	int cfg = event->attr.config;
//	int rw_mon_type = event->attr.config1 & 0xFF;
//	int msel_group_val = (event->attr.config1 >> 8) & 0xFF;

	if (cfg < 0 || cfg >= MTK_EMI_NUM_COUNTERS) {
		pr_warn("Invalid pmu event counter(%d)!\n", cfg);
		return -EINVAL;
	}

	if (mtk_pmu->events[cfg] != NULL) {
		pr_err("%s: cfg(%d) add repeatedly!\n", __func__, cfg);
		return -EAGAIN;
	}

//	if (rw_mon_type < 0 || rw_mon_type >= 3) {
//		pr_warn("Invalid filter rw_mon_type(%d)!\n", rw_mon_type);
//		return -EINVAL;
//	}

//	if (msel_group_val < 0 || msel_group_val >= 0xFF) {
//		pr_warn("Invalid filter msel_group_val(%d)!\n", rw_mon_type);
//		return -EINVAL;
//	}

	mtk_pmu->events[cfg] = event;
	mtk_pmu->active_events++;
	hwc->idx = cfg;
	hwc->state |= PERF_HES_STOPPED | PERF_HES_UPTODATE;
	emi_port_mon_select(cfg);

//	printk("%s chan_%d select rw_mon_type(%d), msel_group_val(%d)\n",
//				__func__, cfg, rw_mon_type, msel_group_val);

	if (flags & PERF_EF_START)
		emi_pmu_event_start(event, flags);

	return 0;
}

void emi_pmu_event_stop(struct perf_event *event, int flags)
{
	struct hw_perf_event *hwc = &event->hw;

	emi_pmu_event_update(event);
	hwc->state |= PERF_HES_STOPPED | PERF_HES_UPTODATE;

	//emi_mon_clear();
}

void emi_pmu_event_del(struct perf_event *event, int flags)
{
	struct mtk_emi_pmu *mtk_pmu = to_mtk_pmu(event->pmu);
	u32 cfg = event->attr.config;

	mtk_pmu->events[cfg] = NULL;
	mtk_pmu->active_events--;

	if (mtk_pmu->active_events == 0)
		hrtimer_cancel(&mtk_pmu->hrtimer);

	emi_pmu_event_stop(event, PERF_EF_UPDATE);
}

static bool emi_pmu_group_event_is_valid(struct perf_event *event,
					  struct pmu *pmu,
					  unsigned long *used_counters)
{
	int cfg = event->attr.config;

	if (cfg < 0 || cfg >= MTK_EMI_NUM_COUNTERS) {
		pr_warn("Invalid pmu event counter(%d)!\n", cfg);
		return -EINVAL;
	}

	if (is_software_event(event))
		return true;

	if (event->pmu != pmu)
		return false;

	return !test_and_set_bit(cfg, used_counters);
}

/*
 * Each event has a single fixed-purpose counter, so we can only have a
 * single active event for each at any point in time. Here we just check
 * for duplicates, and rely on emi_pmu_event_init to verify that the HW
 * event numbers are valid.
 */
static bool emi_pmu_group_is_valid(struct perf_event *event)
{
	struct pmu *pmu = event->pmu;
	struct perf_event *leader = event->group_leader;
	struct perf_event *sibling;
	unsigned long counter_mask = 0;

	set_bit(leader->attr.config, &counter_mask);

	if (event != leader) {
		if (!emi_pmu_group_event_is_valid(event, pmu, &counter_mask))
			return false;
	}

	for_each_sibling_event(sibling, leader) {
		if (!emi_pmu_group_event_is_valid(sibling, pmu, &counter_mask))
			return false;
	}

	return true;
}

int emi_pmu_event_init(struct perf_event *event)
{
	struct mtk_emi_pmu *mtk_pmu = to_mtk_pmu(event->pmu);
	int cfg = event->attr.config;
	struct hw_perf_event *hwc = &event->hw;

	if (event->attr.type != event->pmu->type)
		return -ENOENT;

	if (is_sampling_event(event) || event->attach_state & PERF_ATTACH_TASK)
		return -EOPNOTSUPP;

	if (event->cpu < 0) {
		pr_warn("Can't provide per-task data!\n");
		return -EOPNOTSUPP;
	}

	if (event->attr.sample_period)
		return -EINVAL;

	if (cfg < 0 || cfg >= MTK_EMI_NUM_COUNTERS) {
		pr_warn("Invalid pmu event counter(%d)!\n", cfg);
		return -EINVAL;
	}

	/*
	 * Validate if the events in group does not exceed the
	 * available counters in hardware.
	 */
	if (!emi_pmu_group_is_valid(event)) {
		pr_warn("Invalid pmu event in group!\n");
		return -EINVAL;
	}

	emi_mon_init();

	/*
	 * We don't assign an index until we actually place the event onto
	 * hardware. Use -1 to signify that we haven't decided where to put it
	 * yet.
	 */
	hwc->idx		= -1;
	hwc->config_base	= cfg;
	/* Enforce to use the same CPU for all events in this PMU */
	event->cpu = cpumask_first(&mtk_pmu->cpu);
	return 0;
}


void emi_perf_pmu_enable(struct pmu *pmu)
{
	//disable DCM
	dramc_dcm_enable(false);
}

void emi_perf_pmu_disable(struct pmu *pmu)
{
	//enable DCM
	dramc_dcm_enable(true);
	//disable BM
	emi_counter_enable(false);
}

static void emi_pmu_overflow_handler(struct mtk_emi_pmu *mtk_pmu)
{
	int i;


	for (i = 0; i < MTK_EMI_NUM_COUNTERS; i++) {
		struct perf_event *event = mtk_pmu->events[i];

		if (event)
			emi_pmu_event_update(event);
	}
	emi_mon_restart();
}

static enum hrtimer_restart emi_pmu_timer_handler(struct hrtimer *hrtimer)
{
	struct mtk_emi_pmu *pmu = container_of(hrtimer, struct mtk_emi_pmu, hrtimer);

	emi_pmu_overflow_handler(pmu);
	hrtimer_forward_now(hrtimer, emi_pmu_timer_period());

	return HRTIMER_RESTART;
}

int emi_pmu_offline_cpu(unsigned int cpu, struct hlist_node *node)
{
	struct mtk_emi_pmu *mtk_pmu = hlist_entry_safe(node, struct mtk_emi_pmu, node);
	int target;

	if (!mtk_pmu)
		return 0;

	if (!cpumask_test_and_clear_cpu(cpu, &mtk_pmu->cpu))
		return 0;

	target = cpumask_any_but(cpu_online_mask, cpu);
	if (target >= nr_cpu_ids)
		return 0;

	perf_pmu_migrate_context(&mtk_pmu->pmu, cpu, target);
	cpumask_set_cpu(target, &mtk_pmu->cpu);

	return 0;
}

static int emi_perf_init(struct mtk_emi_pmu *pmu)
{
	*pmu = (struct mtk_emi_pmu) {
		.pmu = (struct pmu) {
			.module	      = THIS_MODULE,
			.capabilities = PERF_PMU_CAP_NO_EXCLUDE,
			.task_ctx_nr = perf_invalid_context,
			.attr_groups = attr_groups,
			.event_init  = emi_pmu_event_init,
			.add	     = emi_pmu_event_add,
			.del	     = emi_pmu_event_del,
			.start	     = emi_pmu_event_start,
			.stop	     = emi_pmu_event_stop,
			.read	     = emi_pmu_event_update,
			//.pmu_enable  = emi_perf_pmu_enable,
			//.pmu_disable = emi_perf_pmu_disable,
		},
		.active_events = 0,
	};

	pmu->id = ida_simple_get(&emi_pmu_ida, 0, 0, GFP_KERNEL);
	return pmu->id;
}

static void *find_device_addr(char type, u32 id)
{
	struct device_node *np;
	char dev_name[MAX_DEV_NAME_LEN];
	void __iomem *base;
	int num;

	if (type == EMI_CEN_DRV)
		num = snprintf(dev_name, MAX_DEV_NAME_LEN, "emi_cen%d", id);
	else
		num = MAX_DEV_NAME_LEN;

	if (num >= MAX_DEV_NAME_LEN)
		return NULL;

	np = of_find_node_by_name(NULL, dev_name);
	if (!np)
		return NULL;

	base = of_iomap(np, 0);
	if (!base) {
		pr_err("%pOF: of_iomap failed\n", np);
		return NULL;
	}

	return base;
}

static int __init emi_perf_drv_init(void)
{
	int num, ret;

	/* remap all EMI regs */
	emi_base_remap_addr = find_device_addr(EMI_CEN_DRV, EMI_0);
	if (!emi_base_remap_addr) {
		pr_err("Can't find EMI device base!!\n");
		return -ENXIO;
	}

	emi_pmu = kzalloc(sizeof(*emi_pmu), GFP_KERNEL);
	if (!emi_pmu) {
		ret = -ENOMEM;
		goto kzalloc_err;
	}
	num = emi_perf_init(emi_pmu);

	emi_pmu->name = kasprintf(GFP_KERNEL, MTK_EMI_PERF_DRV_NAME);
	if (!emi_pmu->name) {
		ret = -ENOMEM;
		goto kasprintf_err;
	}

	cpumask_set_cpu(raw_smp_processor_id(), &emi_pmu->cpu);

	/* The first instance registers the hotplug state */
	if (!emi_pmu->cpuhp_emi_state) {
		ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN,
				MTK_EMI_CPUHP_CB_NAME, NULL, emi_pmu_offline_cpu);
		if (ret < 0) {
			pr_err("cpuhp_setup_state_multi failed(%d)\n", ret);
			goto cpuhp_setup_state_multi_err;
		}
		emi_pmu->cpuhp_emi_state = ret;
	}

	/* Register the pmu instance for cpu hotplug */
	ret = cpuhp_state_add_instance_nocalls(emi_pmu->cpuhp_emi_state, &emi_pmu->node);
	if (ret) {
		pr_err("Error %d registering hotplug\n", ret);
		goto cpuhp_state_add_instance_nocalls_err;
	}

	/*
	 * hrtimer is required because emi does not provide an interrupt so
	 * polling is necessary.
	 */
	hrtimer_init(&emi_pmu->hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	emi_pmu->hrtimer.function = emi_pmu_timer_handler;
	//emi_pmu->hrtimer_period_ms = EMI_PMU_DEFAULT_SAMPLE_PERIOD_MS;

	ret = perf_pmu_register(&(emi_pmu->pmu), emi_pmu->name, -1);
	if (ret)
		goto pmu_register_err;

	pr_info("%s startup\n", __func__);

	return 0;

pmu_register_err:
	hrtimer_cancel(&emi_pmu->hrtimer);
	cpuhp_state_remove_instance_nocalls(emi_pmu->cpuhp_emi_state, &emi_pmu->node);
cpuhp_state_add_instance_nocalls_err:
cpuhp_setup_state_multi_err:
	kfree(emi_pmu->name);
kasprintf_err:
	ida_simple_remove(&emi_pmu_ida, emi_pmu->id);
	kfree(emi_pmu);
kzalloc_err:
	iounmap(emi_base_remap_addr);

	pr_err("%s failed: %d\n", __func__, ret);

	return ret;
}

static void __exit emi_perf_drv_exit(void)
{
	perf_pmu_unregister(&emi_pmu->pmu);
	hrtimer_cancel(&emi_pmu->hrtimer);
	cpuhp_state_remove_instance_nocalls(emi_pmu->cpuhp_emi_state, &emi_pmu->node);
	kfree(emi_pmu->name);
	ida_simple_remove(&emi_pmu_ida, emi_pmu->id);
	kfree(emi_pmu);
	iounmap(emi_base_remap_addr);
}

module_init(emi_perf_drv_init);
module_exit(emi_perf_drv_exit);
MODULE_LICENSE("GPL v2");
