#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>

#include <soc/samsung/cal-if.h>
#include <soc/samsung/exynos-profiler.h>
#include <soc/samsung/exynos-migov.h>
#include <soc/samsung/exynos-sci.h>

#include <soc/samsung/exynos-devfreq.h>
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
#include <soc/samsung/exynos-bcm_dbg.h>
#else
#include <soc/samsung/exynos-wow.h>
#endif

/* Result during profile time */
struct profile_result {
	struct freq_cstate_result	fc_result;

	s32			cur_temp;
	s32			avg_temp;

	/* private data */
	u64			*freq_stats[4];
	u64			freq_stats0_sum;
	u64			freq_stats_ratio;
	u64			freq_stats0_avg;
	u64			freq_stats1_sum;
	int			llc_status;
};

static struct profiler {
	struct device_node	*root;

	int			enabled;

	s32			migov_id;
	u32			cal_id;
	u32			devfreq_type;

	struct freq_table	*table;
	u32			table_cnt;
	u32			dyn_pwr_coeff;
	u32			st_pwr_coeff;

	const char			*tz_name;		/* currently do not use in MIF */
	struct thermal_zone_device	*tz;			/* currently do not use in MIF */

	struct freq_cstate		fc;			/* latest time_in_state info */
	struct freq_cstate_snapshot	fc_snap[NUM_OF_USER];	/* previous time_in_state info */

#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
	u64			*freq_stats[4];
	u64			*freq_stats_snap[4];
#else
	struct exynos_wow_profile	*prev_profile;
#endif

	u32			cur_freq_idx;	/* current freq_idx */
	u32			max_freq_idx;	/* current max_freq_idx */
	u32			min_freq_idx;	/* current min_freq_idx */

	struct profile_result	result[NUM_OF_USER];

	struct kobject		kobj;

	struct exynos_devfreq_freq_infos freq_infos;
} profiler;

/************************************************************************
 *				HELPER					*
 ************************************************************************/

static inline void calc_delta(u64 *result_table, u64 *prev_table, u64 *cur_table, int size)
{
	int i;
	u64 delta, cur;

	for (i = 0; i < size; i++) {
		cur = cur_table[i];
		delta = cur - prev_table[i];
		result_table[i] = delta;
		prev_table[i] = cur;
	}
}

/************************************************************************
 *				SUPPORT-MIGOV				*
 ************************************************************************/
u32 mifpro_get_table_cnt(s32 id)
{
	return profiler.table_cnt;
}

u32 mifpro_get_freq_table(s32 id, u32 *table)
{
	int idx;
	for (idx = 0; idx < profiler.table_cnt; idx++)
		table[idx] = profiler.table[idx].freq;

	return idx;
}

u32 mifpro_get_max_freq(s32 id)
{
	return exynos_pm_qos_request(profiler.freq_infos.pm_qos_class_max);
}

u32 mifpro_get_min_freq(s32 id)
{
	return exynos_pm_qos_request(profiler.freq_infos.pm_qos_class);
}

u32 mifpro_get_freq(s32 id)
{
	return profiler.result[MIGOV].fc_result.freq[ACTIVE];
}

void mifpro_get_power(s32 id, u64 *dyn_power, u64 *st_power)
{
	*dyn_power = profiler.result[MIGOV].fc_result.dyn_power;
	*st_power = profiler.result[MIGOV].fc_result.st_power;
}

void mifpro_get_power_change(s32 id, s32 freq_delta_ratio,
			u32 *freq, u64 *dyn_power, u64 *st_power)
{
	struct profile_result *result = &profiler.result[MIGOV];
	struct freq_cstate_result *fc_result = &result->fc_result;
	int flag = (STATE_SCALE_WO_SPARE | STATE_SCALE_CNT);
	u64 dyn_power_backup;

	get_power_change(profiler.table, profiler.table_cnt,
		profiler.cur_freq_idx, profiler.min_freq_idx, profiler.max_freq_idx,
		result->freq_stats[0], fc_result->time[CLK_OFF], freq_delta_ratio,
		fc_result->profile_time, result->avg_temp, flag, dyn_power, st_power, freq);

	dyn_power_backup = *dyn_power;

	get_power_change(profiler.table, profiler.table_cnt,
		profiler.cur_freq_idx, profiler.min_freq_idx, profiler.max_freq_idx,
		fc_result->time[ACTIVE], fc_result->time[CLK_OFF], freq_delta_ratio,
		fc_result->profile_time, result->avg_temp, flag, dyn_power, st_power, freq);

	*dyn_power = dyn_power_backup;
}

u32 mifpro_get_active_pct(s32 id)
{
	return profiler.result[MIGOV].fc_result.ratio[ACTIVE];
}

s32 mifpro_get_temp(s32 id)
{
	return profiler.result[MIGOV].avg_temp;
}
void mifpro_set_margin(s32 id, s32 margin)
{
	return;
}

u32 mifpro_update_profile(int user);
u32 mifpro_update_mode(s32 id, int mode)
{
	int i;

	if (profiler.enabled == 0 && mode == 1) {
		struct freq_cstate *fc = &profiler.fc;
		struct freq_cstate_snapshot *fc_snap = &profiler.fc_snap[MIGOV];

		sync_fcsnap_with_cur(fc, fc_snap, profiler.table_cnt);
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
		exynos_bcm_calc_enable(1);
#endif

		profiler.enabled = mode;

		exynos_devfreq_set_profile(profiler.devfreq_type, 1);
	}
	else if (profiler.enabled == 1 && mode == 0) {
		exynos_devfreq_set_profile(profiler.devfreq_type, 0);
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
		exynos_bcm_calc_enable(0);
#endif
		profiler.enabled = mode;

		// clear
		for (i = 0; i < NUM_OF_CSTATE; i++) {
			memset(profiler.result[MIGOV].fc_result.time[i], 0, sizeof(ktime_t) * profiler.table_cnt);
			profiler.result[MIGOV].fc_result.ratio[i] = 0;
			profiler.result[MIGOV].fc_result.freq[i] = 0;
			memset(profiler.fc.time[i], 0, sizeof(ktime_t) * profiler.table_cnt);
			memset(profiler.fc_snap[MIGOV].time[i], 0, sizeof(ktime_t) * profiler.table_cnt);
		}
		profiler.result[MIGOV].fc_result.dyn_power = 0;
		profiler.result[MIGOV].fc_result.st_power = 0;
		profiler.result[MIGOV].fc_result.profile_time = 0;
		profiler.result[MIGOV].freq_stats0_sum = 0;
		profiler.result[MIGOV].freq_stats0_avg = 0;
		profiler.result[MIGOV].freq_stats1_sum = 0;
		profiler.result[MIGOV].freq_stats_ratio = 0;
		profiler.fc_snap[MIGOV].last_snap_time = 0;
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
		for (i = 0; i < 4; i++) {
			memset(profiler.freq_stats[i], 0, sizeof(u64) * profiler.table_cnt);
			memset(profiler.freq_stats_snap[i], 0, sizeof(u64) * profiler.table_cnt);
			memset(profiler.result[MIGOV].freq_stats[i], 0, sizeof(u64) * profiler.table_cnt);

		}

#else
		memset(profiler.prev_profile, 0, sizeof(struct exynos_wow_profile) * profiler.table_cnt);
#endif

		return 0;
	}

	mifpro_update_profile(MIGOV);

	return 0;
}

u64 mifpro_get_freq_stats0_sum(void) { return profiler.result[MIGOV].freq_stats0_sum; };
u64 mifpro_get_freq_stats1_sum(void) { return profiler.result[MIGOV].freq_stats1_sum; };
u64 mifpro_get_freq_stats0_avg(void) { return profiler.result[MIGOV].freq_stats0_avg; };
u64 mifpro_get_freq_stats_ratio(void) { return profiler.result[MIGOV].freq_stats_ratio; };
u64 mifpro_get_llc_status(void) { return profiler.result[MIGOV].llc_status; };

struct private_fn_mif mif_pd_fn = {
	.get_stats0_sum	= &mifpro_get_freq_stats0_sum,
	.get_stats0_avg	= &mifpro_get_freq_stats0_avg,
	.get_stats1_sum	= &mifpro_get_freq_stats1_sum,
	.get_stats_ratio	= &mifpro_get_freq_stats_ratio,
	.get_llc_status = &mifpro_get_llc_status,
};

struct domain_fn mif_fn = {
	.get_table_cnt		= &mifpro_get_table_cnt,
	.get_freq_table		= &mifpro_get_freq_table,
	.get_max_freq		= &mifpro_get_max_freq,
	.get_min_freq		= &mifpro_get_min_freq,
	.get_freq		= &mifpro_get_freq,
	.get_power		= &mifpro_get_power,
	.get_power_change	= &mifpro_get_power_change,
	.get_active_pct		= &mifpro_get_active_pct,
	.get_temp		= &mifpro_get_temp,
	.set_margin		= &mifpro_set_margin,
	.update_mode		= &mifpro_update_mode,
};

/************************************************************************
 *			Gathering MIFFreq Information			*
 ************************************************************************/
//ktime_t * exynos_stats_get_mif_time_in_state(void);
extern u64 exynos_bcm_get_ccnt(unsigned int idx);
u32 mifpro_update_profile(int user)
{
	struct freq_cstate *fc = &profiler.fc;
	struct freq_cstate_snapshot *fc_snap = &profiler.fc_snap[user];
	struct freq_cstate_result *fc_result = &profiler.result[user].fc_result;
	struct profile_result *result = &profiler.result[user];
	int i;
	u64 total_active_time = 0, freq_stats2_sum = 0, freq_stats3_sum = 0, diff_ccnt = 0;
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
	u64 ccnt = 0;
	static u64 prev_ccnt = 0;
#else
	struct exynos_wow_profile *profile_in_state;
#endif

	profiler.cur_freq_idx = get_idx_from_freq(profiler.table, profiler.table_cnt,
			*(profiler.freq_infos.cur_freq), RELATION_LOW);
	profiler.max_freq_idx = get_idx_from_freq(profiler.table, profiler.table_cnt,
			exynos_pm_qos_request(profiler.freq_infos.pm_qos_class_max), RELATION_LOW);
	profiler.min_freq_idx = get_idx_from_freq(profiler.table, profiler.table_cnt,
			exynos_pm_qos_request(profiler.freq_infos.pm_qos_class), RELATION_HIGH);

#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
	// Update time in state and get tables from DVFS driver
	exynos_devfreq_get_profile(profiler.devfreq_type, fc->time, profiler.freq_stats);

	// calculate delta from previous status
	make_snapshot_and_time_delta(fc, fc_snap, fc_result, profiler.table_cnt);

	for (i = 0; i < 4; i++)
		calc_delta(result->freq_stats[i], profiler.freq_stats_snap[i], profiler.freq_stats[i], profiler.table_cnt);
	// Call to calc power
	compute_freq_cstate_result(profiler.table, fc_result, profiler.table_cnt,
					profiler.cur_freq_idx, result->avg_temp);

	ccnt = exynos_bcm_get_ccnt(0);

	diff_ccnt = ccnt - prev_ccnt;
	prev_ccnt = ccnt;
#else
	profile_in_state = kzalloc(sizeof(struct exynos_wow_profile) * profiler.table_cnt, GFP_KERNEL);

	exynos_devfreq_get_profile(profiler.devfreq_type, fc->time, profile_in_state);

	// calculate delta from previous status
	make_snapshot_and_time_delta(fc, fc_snap, fc_result, profiler.table_cnt);

	// Call to calc power
	compute_freq_cstate_result(profiler.table, fc_result, profiler.table_cnt,
					profiler.cur_freq_idx, result->avg_temp);

	// Calculate freq_stats array
	for (i = 0; i < profiler.table_cnt; i++) {
		result->freq_stats[0][i] = (profile_in_state[i].transfer_data - profiler.prev_profile[i].transfer_data) >> 20;
		result->freq_stats[2][i] = (profile_in_state[i].nr_requests - profiler.prev_profile[i].nr_requests);
		result->freq_stats[3][i] = (profile_in_state[i].mo_count - profiler.prev_profile[i].mo_count);
		diff_ccnt += (profile_in_state[i].ccnt - profiler.prev_profile[i].ccnt);
	}
	memcpy(profiler.prev_profile, profile_in_state, sizeof(struct exynos_wow_profile) * profiler.table_cnt);
	kfree(profile_in_state);
	diff_ccnt /= 1000;
#endif


	for (i = 0 ; i < profiler.table_cnt; i++)
		fc->time[CLK_OFF][i] -= fc->time[ACTIVE][i];
	result->freq_stats0_sum = 0;
	result->freq_stats1_sum = 0;
	fc_result->dyn_power = 0;

	for (i = 0; i < profiler.table_cnt; i++) {
		result->freq_stats0_sum += result->freq_stats[0][i];
		result->freq_stats1_sum += result->freq_stats[1][i];
		result->freq_stats0_avg += (result->freq_stats[0][i] << 40) / (profiler.table[i].freq / 1000);
		total_active_time += fc_result->time[ACTIVE][i];
		freq_stats2_sum += result->freq_stats[2][i];
		freq_stats3_sum += result->freq_stats[3][i];
		fc_result->dyn_power += ((result->freq_stats[0][i] * profiler.table[i].dyn_cost) / fc_result->profile_time);
	}

	result->freq_stats0_sum = result->freq_stats0_sum * 1000000000 / fc_result->profile_time;
	result->freq_stats1_sum = result->freq_stats1_sum * 1000000000 / fc_result->profile_time;
	result->freq_stats0_avg = result->freq_stats0_avg / total_active_time;
	result->freq_stats_ratio = fc_result->profile_time * freq_stats3_sum / freq_stats2_sum / diff_ccnt;
	result->llc_status = llc_get_en();

	if (profiler.tz) {
		int temp = get_temp(profiler.tz);
		profiler.result[user].avg_temp = (temp + profiler.result[user].cur_temp) >> 1;
		profiler.result[user].cur_temp = temp;
	}

	return 0;
}

/************************************************************************
 *				INITIALIZATON				*
 ************************************************************************/
/* Initialize profiler data */
static int register_export_fn(u32 *max_freq, u32 *min_freq, u32 *cur_freq)
{
	struct exynos_devfreq_freq_infos *freq_infos = &profiler.freq_infos;

	exynos_devfreq_get_freq_infos(profiler.devfreq_type, freq_infos);

//	*max_freq = freq_infos->max_freq;		/* get_org_max_freq(void) */
	*max_freq = 3172000;
	*min_freq = freq_infos->min_freq;		/* get_org_min_freq(void) */
	*cur_freq = *freq_infos->cur_freq;		/* get_cur_freq(void)	  */
	profiler.table_cnt = freq_infos->max_state;	/* get_freq_table_cnt(void)  */
	/*
	// 2020
	profiler.fc.time[ACTIVE] = exynos_stats_get_mif_time_in_state();

	// Olympus
	profiler.fc.time[ACTIVE] = get_time_inf_freq(ACTIVE); 
	profiler.fc.time[CLKOFF] = get_time_in_freq(CLKOFF);
	*/

	return 0;
}

static int parse_dt(struct device_node *dn)
{
	int ret;

	/* necessary data */
	ret = of_property_read_u32(dn, "cal-id", &profiler.cal_id);
	if (ret)
		return -2;

	/* un-necessary data */
	ret = of_property_read_s32(dn, "migov-id", &profiler.migov_id);
	if (ret)
		profiler.migov_id = -1;	/* Don't support migov */

	ret = of_property_read_s32(dn, "devfreq-type", &profiler.devfreq_type);
	if (ret)
		profiler.devfreq_type = -1;	/* Don't support migov */

	of_property_read_u32(dn, "power-coefficient", &profiler.dyn_pwr_coeff);
	of_property_read_u32(dn, "static-power-coefficient", &profiler.st_pwr_coeff);
//	of_property_read_string(dn, "tz-name", &profiler.tz_name);

	return 0;
}

static int init_profile_result(struct profile_result *result, int size)
{
	int state;

	if (init_freq_cstate_result(&result->fc_result, NUM_OF_CSTATE, size))
		return -ENOMEM;

	/* init private data */
	for (state = 0; state < 4; state++) {
		ktime_t *state_in_freq;
		state_in_freq = alloc_state_in_freq(size);
		if (!state_in_freq)
			return -ENOMEM;
		result->freq_stats[state] = state_in_freq;
	}

	return 0;
}

#ifdef CONFIG_EXYNOS_DEBUG_INFO
static void show_profiler_info(void)
{
	int idx;

	pr_info("================ mif domain ================\n");
	pr_info("min= %dKHz, max= %dKHz\n",
			profiler.table[profiler.table_cnt - 1].freq, profiler.table[0].freq);
	for (idx = 0; idx < profiler.table_cnt; idx++)
		pr_info("lv=%3d freq=%8d volt=%8d dyn_cost=%5d st_cost=%5d\n",
			idx, profiler.table[idx].freq, profiler.table[idx].volt,
			profiler.table[idx].dyn_cost,
			profiler.table[idx].st_cost);
	if (profiler.tz_name)
		pr_info("support temperature (tz_name=%s)\n", profiler.tz_name);
	if (profiler.migov_id != -1)
		pr_info("support migov domain(id=%d)\n", profiler.migov_id);
}
#endif

static int exynos_mif_profiler_probe(struct platform_device *pdev)
{
	unsigned int org_max_freq, org_min_freq, cur_freq;
	int ret, idx;
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
	int i;
#endif

	/* get node of device tree */
	if (!pdev->dev.of_node) {
		pr_err("mifpro: failed to get device treee\n");
		return -EINVAL;
	}
	profiler.root = pdev->dev.of_node;

	/* Parse data from Device Tree to init domain */
	ret = parse_dt(profiler.root);
	if (ret) {
		pr_err("mifpro: failed to parse dt(ret: %d)\n", ret);
		return -EINVAL;
	}

	register_export_fn(&org_max_freq, &org_min_freq, &cur_freq);

	/* init freq table */
	profiler.table = init_freq_table(NULL, profiler.table_cnt,
			profiler.cal_id, org_max_freq, org_min_freq,
			profiler.dyn_pwr_coeff, profiler.st_pwr_coeff,
			PWR_COST_CVV, PWR_COST_CVV);
	if (!profiler.table) {
		pr_err("mifpro: failed to init freq_table\n");
		return -EINVAL;
	}
	profiler.max_freq_idx = 0;
	profiler.min_freq_idx = profiler.table_cnt - 1;
	profiler.cur_freq_idx = get_idx_from_freq(profiler.table,
				profiler.table_cnt, cur_freq, RELATION_HIGH);

	if (init_freq_cstate(&profiler.fc, NUM_OF_CSTATE, profiler.table_cnt))
			return -ENOMEM;

	/* init snapshot & result table */
	for (idx = 0; idx < NUM_OF_USER; idx++) {
		if (init_freq_cstate_snapshot(&profiler.fc_snap[idx],
						NUM_OF_CSTATE, profiler.table_cnt))
			return -ENOMEM;

		if (init_profile_result(&profiler.result[idx], profiler.table_cnt))
			return -EINVAL;
#if defined(CONFIG_SOC_S5E9925_EVT0) || defined(CONFIG_SOC_S5E8825)
		for (i = 0 ; i < 4; i++) {
			profiler.freq_stats[i] = kzalloc(sizeof(u64) * profiler.table_cnt, GFP_KERNEL);
			profiler.freq_stats_snap[i] = kzalloc(sizeof(u64) * profiler.table_cnt, GFP_KERNEL);
		}
#else
		profiler.prev_profile = kzalloc(sizeof(struct exynos_wow_profile) * profiler.table_cnt, GFP_KERNEL);
#endif
	}

	/* get thermal-zone to get temperature */
	if (profiler.tz_name)
		profiler.tz = init_temp(profiler.tz_name);

	if (profiler.tz)
		init_static_cost(profiler.table, profiler.table_cnt,
				1, profiler.root, profiler.tz);

	ret = exynos_migov_register_domain(MIGOV_MIF, &mif_fn, &mif_pd_fn);

#ifdef CONFIG_EXYNOS_DEBUG_INFO
	show_profiler_info();
#endif

	return ret;
}

static const struct of_device_id exynos_mif_profiler_match[] = {
	{
		.compatible	= "samsung,exynos-mif-profiler",
	},
	{},
};
MODULE_DEVICE_TABLE(of, exynos_mif_profiler_match);

static struct platform_driver exynos_mif_profiler_driver = {
	.probe		= exynos_mif_profiler_probe,
	.driver	= {
		.name	= "exynos-mif-profiler",
		.owner	= THIS_MODULE,
		.of_match_table = exynos_mif_profiler_match,
	},
};

static int exynos_mif_profiler_init(void)
{
	return platform_driver_register(&exynos_mif_profiler_driver);
}
late_initcall(exynos_mif_profiler_init);

MODULE_DESCRIPTION("Exynos MIF Profiler");
MODULE_LICENSE("GPL");