/* SPDX-License-Identifier: GPL-2.0 */

/*
 * (C) COPYRIGHT 2021 Samsung Electronics Inc. All rights reserved.
 *
 * This program is free software and is provided to you under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation, and any use by you of this program is subject to the terms
 * of such GNU licence.
 *
 * 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, you can access it online at
 * http://www.gnu.org/licenses/gpl-2.0.html.
 */

#include <linux/device.h>
#include <linux/of.h>
#include <linux/sysfs.h>
#include <linux/slab.h>

#include <gpex_utils.h>
#include <gpexbe_devicetree.h>

#define CPU_MAX INT_MAX
static gpu_dt dt_info;
static struct _dt_clock_item *clock_table;
static struct _dt_clqos_item *clqos_table;

static int gpexbe_devicetree_read_u32(const char *of_string, u32 *of_data)
{
	int ret = 0;

	if (!of_string || !of_data) {
		GPU_LOG(MALI_EXYNOS_ERROR, "NULL: failed to get item from dt\n");
		return -EINVAL;
	}

	ret = of_property_read_u32(dt_info.dev->of_node, of_string, of_data);

	if (ret) {
		GPU_LOG(MALI_EXYNOS_ERROR,
			"%s: failed to get item from dt. Data will be set to 0.\n", of_string);
		*of_data = 0;
	}

	return ret;
}

/* TODO: document in the interface header. Return value same as of_property_read_string */
static int gpexbe_devicetree_read_string(const char *of_string, const char **of_data)
{
	int ret = 0;

	if (!of_string || !of_data) {
		GPU_LOG(MALI_EXYNOS_ERROR, "NULL: failed to get item from dt\n");
		return -EINVAL;
	}

	ret = of_property_read_string(dt_info.dev->of_node, of_string, of_data);
	if (ret) {
		GPU_LOG(MALI_EXYNOS_ERROR,
			"%s: failed to get item from dt. Data will be set to NULL.\n", of_string);
		*of_data = NULL;
	}

	return ret;
}

static int gpexbe_devicetree_read_u32_array(const char *of_string, int *of_data, int sz)
{
	int ret = 0;

	if (!of_string || !of_data || sz < 0) {
		GPU_LOG(MALI_EXYNOS_ERROR, "NULL: failed to get item from dt. Invalid params\n");
		return -EINVAL;
	}

	ret = of_property_read_u32_array(dt_info.dev->of_node, of_string, of_data, sz);

	if (ret)
		GPU_LOG(MALI_EXYNOS_ERROR, "%s: failed to get item from dt (u32 array)\n",
			of_string);

	return ret;
}

static int read_interactive_info_array(void)
{
	int interactive_info[3] = { 0, 0, 0 };

	gpexbe_devicetree_read_u32_array("interactive_info", interactive_info, 3);

	/* TODO: change default value to something more sensible */
	dt_info.interactive_info.highspeed_clock =
		interactive_info[0] == 0 ? 500 : (u32)interactive_info[0];
	dt_info.interactive_info.highspeed_load =
		interactive_info[1] == 0 ? 100 : (u32)interactive_info[1];
	dt_info.interactive_info.highspeed_delay =
		interactive_info[2] == 0 ? 0 : (u32)interactive_info[2];

	return 0;
}

unsigned int custom_clock[] = {1209000, 1105000, 1001000, 897000, 806000, 702000, 611000, 507000, 403000, 312000, 208000, 104000};
unsigned int custom_min_threshold[] = {90, 87, 85, 82, 80, 79, 78, 70, 60, 50, 30, 0};
unsigned int custom_max_threshold[] = {100, 96 ,95, 95, 95, 95, 95, 90, 80, 70, 60, 40};
unsigned int custom_staycount[] = {5, 5, 5, 5, 5, 5, 5, 3, 3, 2, 2, 1};
unsigned int custom_mem_freq[] = {2093000, 1794000, 1794000, 1794000, 1539000, 1352000, 1352000, 1014000, 1014000, 845000, 676000, 676000};
unsigned int custom_lit[] = {1536000, 1440000, 1248000, 1056000, 1056000, 1056000, 1056000, 1056000, 0, 0, 0, 0};
unsigned int custom_mid = 0;
unsigned int custom_big = CPU_MAX;
int custom_array_size = sizeof(custom_clock) / sizeof(custom_clock[0]);

static int build_clk_table(void)
{
	int array_size = custom_array_size;
	int i = 0;

	if (array_size <= 0) {
		return -EINVAL;
	}
	clock_table = kcalloc(array_size, sizeof(*clock_table), GFP_KERNEL);

	for (i = 0; i < array_size; i++) {
		clock_table[i].clock = custom_clock[i];
		clock_table[i].min_threshold = custom_min_threshold[i];
		clock_table[i].max_threshold = custom_max_threshold[i];
		clock_table[i].down_staycount = custom_staycount[i];
		clock_table[i].mem_freq = custom_mem_freq[i];
		clock_table[i].cpu_little_min_freq = custom_lit[i];

		if (dt_info.gpu_pmqos_cpu_cluster_num == 3) {
			clock_table[i].cpu_middle_min_freq = custom_mid;
			clock_table[i].cpu_big_max_freq = custom_big;

			GPU_LOG(MALI_EXYNOS_INFO,
				"up [%d] down [%d] staycnt [%d] mif [%d] lit [%d] mid [%d] big [%d]\n",
				clock_table[i].max_threshold, clock_table[i].min_threshold,
				clock_table[i].down_staycount, clock_table[i].mem_freq,
				clock_table[i].cpu_little_min_freq,
				clock_table[i].cpu_middle_min_freq,
				clock_table[i].cpu_big_max_freq);
		} else {
			// Assuming cpu cluster number is 2
			clock_table[i].cpu_big_max_freq = custom_big;

			GPU_LOG(MALI_EXYNOS_INFO,
				"up [%d] down [%d] staycnt [%d] mif [%d] lit [%d] big [%d]\n",
				clock_table[i].max_threshold, clock_table[i].min_threshold,
				clock_table[i].down_staycount, clock_table[i].mem_freq,
				clock_table[i].cpu_little_min_freq,
				clock_table[i].cpu_big_max_freq);
		}
	}
	return 0;
}

static int build_cl_pmqos_table(void)
{
	int array_size = dt_info.gpu_cl_pmqos_table_size.row * dt_info.gpu_cl_pmqos_table_size.col;
	u32 *raw_table;
	int row = 0;

	if (array_size <= 0) {
		int num_rows = dt_info.gpu_dvfs_table_size.row;

		clqos_table = kcalloc(num_rows, sizeof(*clqos_table), GFP_KERNEL);

		for (row = 0; row < num_rows; row++)
			clqos_table[row].clock = clock_table[row].clock;

		dt_info.gpu_cl_pmqos_table_size.row = num_rows;
		dt_info.gpu_cl_pmqos_table_size.col = 1;

		return 0;
	}

	raw_table = kcalloc(array_size, sizeof(*raw_table), GFP_KERNEL);

	if (!raw_table)
		return -ENOMEM;

	gpexbe_devicetree_read_u32_array("gpu_cl_pmqos_table", raw_table, array_size);

	clqos_table =
		kcalloc(dt_info.gpu_cl_pmqos_table_size.row, sizeof(*clqos_table), GFP_KERNEL);

	for (row = 0; row < dt_info.gpu_cl_pmqos_table_size.row; row++) {
		int table_idx = row * dt_info.gpu_cl_pmqos_table_size.col;

		clqos_table[row].clock = raw_table[table_idx];
		clqos_table[row].mif_min = raw_table[table_idx + 1];
		clqos_table[row].little_min = raw_table[table_idx + 2];
		clqos_table[row].middle_min = raw_table[table_idx + 3];
		clqos_table[row].big_max = raw_table[table_idx + 4];
	}

	kfree(raw_table);

	return 0;
}

static void read_from_dt(void)
{
	/* clock backend */
	gpexbe_devicetree_read_u32("g3d_cmu_cal_id", &dt_info.g3d_cmu_cal_id);

	/* PM backend */
	gpexbe_devicetree_read_string("g3d_genpd_name", &dt_info.g3d_genpd_name);

	/* CLOCK */
	dt_info.gpu_max_clock = custom_clock[0];
	dt_info.gpu_min_clock = custom_clock[custom_array_size - 1];
	gpexbe_devicetree_read_u32("gpu_pmqos_cpu_cluster_num", &dt_info.gpu_pmqos_cpu_cluster_num);

	dt_info.gpu_dvfs_table_size.col = 8; // 8 values for each freq
	dt_info.gpu_dvfs_table_size.row = custom_array_size;

	gpexbe_devicetree_read_u32_array("gpu_cl_pmqos_table_size",
					 (int *)&dt_info.gpu_cl_pmqos_table_size, 2);

	/* DEBUG_BACKEND */
	gpexbe_devicetree_read_u32("gpu_ess_id_type", &dt_info.gpu_ess_id_type);

	/* LEGACY DVFS */
	gpexbe_devicetree_read_string("governor", &dt_info.governor);
	gpexbe_devicetree_read_u32("gpu_dvfs_bl_config_clock", &dt_info.gpu_dvfs_bl_config_clock);
	gpexbe_devicetree_read_u32("gpu_dvfs_polling_time", &dt_info.gpu_dvfs_polling_time);

	/* IFPO */
	gpexbe_devicetree_read_u32("gpu_inter_frame_pm", &dt_info.gpu_inter_frame_pm);

	/* PM BACKEND */
	gpexbe_devicetree_read_u32("gpu_pmu_status_reg_offset", &dt_info.gpu_pmu_status_reg_offset);
	gpexbe_devicetree_read_u32("gpu_pmu_status_local_pwr_mask",
				   &dt_info.gpu_pmu_status_local_pwr_mask);

	/* PM */
	gpexbe_devicetree_read_u32("gpu_runtime_pm_delay_time", &dt_info.gpu_runtime_pm_delay_time);

	/* QOS */
	gpexbe_devicetree_read_u32("gpu_bts_support", &dt_info.gpu_bts_support);
	gpexbe_devicetree_read_u32("gpu_mo_min_clock", &dt_info.gpu_mo_min_clock);

	/* UTILS */
	gpexbe_devicetree_read_u32("gpu_debug_level", &dt_info.gpu_debug_level);

	/* HCM */
	gpexbe_devicetree_read_u32("gpu_heavy_compute_cpu0_min_clock;",
				   &dt_info.gpu_heavy_compute_cpu0_min_clock);
	gpexbe_devicetree_read_u32("gpu_heavy_compute_vk_cpu0_min_clock;",
				   &dt_info.gpu_heavy_compute_vk_cpu0_min_clock);

	/* TSG */
	gpexbe_devicetree_read_u32("gpu_weight_table_idx_0", &dt_info.gpu_weight_table_idx_0);
	gpexbe_devicetree_read_u32("gpu_weight_table_idx_1", &dt_info.gpu_weight_table_idx_1);
}

/******************
 * GETTERS
 * ***************/
dt_clock_item *gpexbe_devicetree_get_clock_table(void)
{
	return clock_table;
}

/********************************************
 * Helper Functions. (Must remain non static)
 * ******************************************/
int gpexbe_devicetree_get_int_internal(size_t offset, const char *str)
{
	//pr_warn("mali: %s %s %d\n", __func__, str, *(int*)((u8*)&dt_info + offset));
	//GPU_LOG(MALI_EXYNOS_INFO, "%s: %s\n", of_string, *of_data);
	return *(int *)((u8 *)&dt_info + offset);
}

char *gpexbe_devicetree_get_str_internal(size_t offset, const char *str)
{
	//pr_warn("mali: %s %s %s\n", __func__, str, *(char**)((u8*)&dt_info + offset));
	//GPU_LOG(MALI_EXYNOS_INFO, "%s: %s\n", of_string, *of_data);
	return *(char **)((u8 *)&dt_info + offset);
}

gpu_dt *gpexbe_devicetree_get_gpu_dt(void)
{
	return &dt_info;
}

/************************************************************************
 * INIT and TERM functions
 ************************************************************************/

int gpexbe_devicetree_init(struct device *dev)
{
	dt_info.dev = dev;
	read_from_dt();
	build_clk_table();
	build_cl_pmqos_table();
	read_interactive_info_array();

	dt_info.gpu_dvfs_table = clock_table;
	dt_info.gpu_cl_pmqos_table = clqos_table;

	dt_info.initialized = 1;

	return 0;
}

void gpexbe_devicetree_term(void)
{
	kfree(clock_table);
	kfree(clqos_table);

	clock_table = NULL;
	clqos_table = NULL;

	memset(&dt_info, 0, sizeof(gpu_dt));

	return;
}