// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2019 MediaTek Inc.
 */
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <linux/of_device.h>
#include <linux/pm_wakeup.h>
#include <linux/sched/clock.h>
#include <linux/slab.h>
#include <linux/random.h>
#include <linux/seq_file.h>
#include <linux/debugfs.h>
#include <linux/uaccess.h>
#include <linux/version.h>

#include <apusys_power.h>

#include <common/mdla_device.h>
#include <common/mdla_power_ctrl.h>

#include <utilities/mdla_util.h>
#include <utilities/mdla_debug.h>
#include <utilities/mdla_profile.h>

#define get_pwr_id(core_id) (MDLA0 + core_id)

struct mdla_pwr_ctrl {
	int mdla_id;
	struct mutex lock;
	struct wakeup_source *wakeup;
	struct timer_list power_off_timer;
	struct work_struct power_off_work;
	void (*power_off_cb_func)(struct work_struct *work);
};

static int mdla_pwr_dummy_on(u32 core_id, bool force)
{
	mdla_pwr_ops_get()->hw_reset(core_id,
				mdla_dbg_get_reason_str(REASON_SIMULATOR));
	return 0;
}
static int mdla_pwr_dummy_off(u32 core_id, int suspend, bool force)
{
	return 0;
}

static void mdla_pwr_dummy_hw_reset(u32 core_id, const char *str) {}
static void mdla_pwr_dummy_opp(u32 a0, int a1) {}
static void mdla_pwr_dummy_lock(u32 a0) {}
static void mdla_pwr_dummy_ops(u32 a0) {}

static struct mdla_pwr_ops mdla_power = {
	.on                 = mdla_pwr_dummy_on,
	.off                = mdla_pwr_dummy_off,
	.off_timer_start    = mdla_pwr_dummy_ops,
	.off_timer_cancel   = mdla_pwr_dummy_ops,
	.set_opp            = mdla_pwr_dummy_opp,
	.set_opp_by_boost   = mdla_pwr_dummy_opp,
	.switch_off_on      = mdla_pwr_dummy_ops,
	.hw_reset           = mdla_pwr_dummy_hw_reset,
	.lock               = mdla_pwr_dummy_lock,
	.unlock             = mdla_pwr_dummy_lock,
	.wake_lock          = mdla_pwr_dummy_lock,
	.wake_unlock        = mdla_pwr_dummy_lock,
};


static void mdla_pwr_timeup(struct timer_list *timer)
{
	struct mdla_pwr_ctrl *pwr_ctrl;

	pwr_ctrl = container_of(timer, struct mdla_pwr_ctrl, power_off_timer);
	schedule_work(&pwr_ctrl->power_off_work);
}

static void mdla_pwr_off(struct work_struct *work)
{
	struct mdla_pwr_ctrl *pwr_ctrl;

	pwr_ctrl = container_of(work, struct mdla_pwr_ctrl, power_off_work);
	mdla_power.off(pwr_ctrl->mdla_id, 0, false);
}

static void mdla_pwr_off_timer_start(u32 core_id)
{
	u32 poweroff_time = mdla_dbg_read_u32(FS_POWEROFF_TIME);
	struct mdla_dev *mdla_device = mdla_get_device(core_id);
	struct mdla_pwr_ctrl *pwr_ctrl = mdla_device->power;

	mutex_lock_nested(&pwr_ctrl->lock, core_id);

	if (mdla_device->cmd_list_cnt > 0)
		mdla_device->cmd_list_cnt--;

	if (mdla_device->cmd_list_cnt == 0)
		mdla_prof_stop(core_id, 1);

	//if (poweroff_time) {
	mdla_pwr_debug("%s: MDLA %d start power_timer\n",
			__func__, core_id);
	mod_timer(&pwr_ctrl->power_off_timer,
		jiffies + msecs_to_jiffies(poweroff_time));

	mutex_unlock(&pwr_ctrl->lock);
}

static void mdla_pwr_off_timer_cancel(u32 core_id)
{
	struct mdla_pwr_ctrl *pwr_ctrl = mdla_get_device(core_id)->power;

	if (timer_pending(&pwr_ctrl->power_off_timer))
		del_timer(&pwr_ctrl->power_off_timer);
}

static void mdla_pwr_set_opp(u32 core_id, int opp)
{
	apu_device_set_opp(get_pwr_id(core_id), opp);
}

static void mdla_pwr_set_opp_by_boost(u32 core_id, int boost_val)
{
	unsigned char mdla_opp = 0;

	mdla_opp = apusys_boost_value_to_opp(get_pwr_id(core_id), (unsigned char)boost_val);
	apu_device_set_opp(get_pwr_id(core_id), mdla_opp);
	mdla_pwr_debug("core: %d, opp: %d\n", core_id, mdla_opp);
}

static void mdla_pwr_switch_off_on(u32 core_id)
{
	struct mdla_dev *mdla_device = mdla_get_device(core_id);
	struct mdla_pwr_ctrl *pwr_ctrl = mdla_get_device(core_id)->power;

	mutex_lock_nested(&pwr_ctrl->lock, core_id);
	// xiu hang if power off during cmd_list_cnt > 1
	if (mdla_device->cmd_list_cnt > 1) {
		mutex_unlock(&pwr_ctrl->lock);
		return;
	}

	if (apu_device_power_off(get_pwr_id(core_id)) ||
		apu_device_power_on(get_pwr_id(core_id)))
		mdla_err("%s: fail\n", __func__);
	mutex_unlock(&pwr_ctrl->lock);
}

static void mdla_pwr_lock(u32 core_id)
{
	mutex_lock_nested(&mdla_get_device(core_id)->power->lock, core_id);
}
static void mdla_pwr_unlock(u32 core_id)
{
	mutex_unlock(&mdla_get_device(core_id)->power->lock);
}

static void mdla_pwr_wake_lock(u32 core_id)
{
	//__pm_stay_awake(mdla_get_device(core_id)->power->wakeup);
}

static void mdla_pwr_wake_unlock(u32 core_id)
{
	//__pm_relax(mdla_get_device(core_id)->power->wakeup);
}

static int mdla_dbg_pwr_show(struct seq_file *s, void *data)
{
	int i;
	struct mdla_dev *mdla_device;

	for_each_mdla_core(i) {
		mdla_device = mdla_get_device(i);

		seq_printf(s, "---- core %d ----\n", i);
		seq_printf(s, "apu power status = %d\n", apu_get_power_on_status(get_pwr_id(i)));
		seq_printf(s, "power_is_on      = %d\n", mdla_device->power_is_on);
		seq_printf(s, "sw_power_is_on   = %d\n", mdla_device->sw_power_is_on);
		seq_printf(s, "cmd_list_cnt     = %d\n", mdla_device->cmd_list_cnt);
	}
	seq_printf(s, "\nRandom opp test: %u\n", mdla_dbg_read_u32(FS_DVFS_RAND));

	seq_puts(s, "\n==== usage ====\n");
	seq_printf(s, "echo [param] > /d/mdla/%s\n", DBGFS_PWR_NAME);
	seq_puts(s, "\tparam:\n");
	seq_puts(s, "\t  0: force all core power off\n");
	seq_puts(s, "\t  1: force all core power on\n");
	seq_printf(s, "echo [0|1] > /d/mdla/%s\n", mdla_dbg_get_u32_node_str(FS_DVFS_RAND));
	seq_puts(s, "\tEnable/Disable random opp test\n");

	return 0;
}

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

static ssize_t mdla_dbg_pwr_write(struct file *flip,
		const char __user *buffer,
		size_t count, loff_t *f_pos)
{
	char *buf;
	u32 param;
	int i, ret;

	buf = kzalloc(count + 1, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	ret = copy_from_user(buf, buffer, count);
	if (ret)
		goto out;

	buf[count] = '\0';

	if (kstrtouint(buf, 10, &param) != 0) {
		ret = -EINVAL;
		goto out;
	}

	for_each_mdla_core(i) {
		if (param == 0)
			mdla_pwr_ops_get()->off(i, 0, true);
		else if (param == 1)
			mdla_pwr_ops_get()->on(i, true);
	}

out:
	kfree(buf);
	return count;
}

static const struct file_operations mdla_dbg_pwr_fops = {
	.open = mdla_dbg_pwr_open,
	.read = seq_read,
	.llseek = seq_lseek,
	.release = single_release,
	.write = mdla_dbg_pwr_write,
};


static void mdla_pwr_fs_init(void)
{
	struct dentry *d = mdla_dbg_get_fs_root();

	if (d)
		debugfs_create_file(DBGFS_PWR_NAME, 0644, d, NULL,
				&mdla_dbg_pwr_fops);
}

const struct mdla_pwr_ops *mdla_pwr_ops_get(void)
{
	return &mdla_power;
}

int mdla_pwr_get_random_boost_val(void)
{
	int val;

	/**
	 * Get opp 0 only when boost_val is 100
	 * Using division of 128 to increase probabiliy of getting opp 0
	 */
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 0, 0)
	val = (get_random_int() & 0x7F) + 1;
#else
	unsigned int random;

	get_random_bytes(&random, sizeof(unsigned int));
	val = (random & 0X7f) + 1;
#endif

	return val > 100 ? 100 : val;
}

bool mdla_pwr_apusys_disabled(void)
{
	return !apusys_power_check();
}

void mdla_pwr_reset_setup(void (*hw_reset)(u32 core_id, const char *str))
{
	if (hw_reset)
		mdla_power.hw_reset = hw_reset;
}

#ifdef LEGACY_PROJ
struct apu_dev_power_data {
	int dev_type;
	int dev_core;
	void *data;
};

#endif

int mdla_pwr_device_register(struct platform_device *pdev,
			int (*on)(u32 core_id, bool force),
			int (*off)(u32 core_id, int suspend, bool force))
{
	int i, ret = 0;
	enum DVFS_USER user_mdla;
	struct mdla_dev *mdla_device;
	struct mdla_pwr_ctrl *pwr_ctrl;
	struct apu_dev_power_data *pwr_data;

	mdla_drv_debug("probe 0, pdev id = %d name = %s, name = %s\n",
						pdev->id, pdev->name,
						pdev->dev.of_node->name);

	pwr_data = kzalloc(sizeof(struct apu_dev_power_data), GFP_KERNEL);
	if (!pwr_data)
		return -1;

	/* Only one platform_device */
	pwr_data->dev_type = MDLA0;
	pwr_data->dev_core = 0;
	platform_set_drvdata(pdev, pwr_data);

	for_each_mdla_core(i) {
		user_mdla = get_pwr_id(i);
		ret = apu_power_device_register(user_mdla, pdev);
		if (!ret) {
			mdla_drv_debug("%s register power device %d success\n",
							__func__,
							user_mdla);
		} else {
			mdla_err("%s register power device %d fail\n",
							__func__,
							user_mdla);
			goto reg_pwr_err;
		}
	}

	if ((on == NULL) && (off == NULL))
		return 0;

	for_each_mdla_core(i) {

		pwr_ctrl = kzalloc(sizeof(struct mdla_pwr_ctrl), GFP_KERNEL);
		if (!pwr_ctrl)
			goto alloc_err;

		pwr_ctrl->mdla_id = i;
		mutex_init(&pwr_ctrl->lock);
#ifdef IS_KERNEL_4_14
		pwr_ctrl->power_off_timer.data = i;
		pwr_ctrl->power_off_timer.function = mdla_pwr_timeup;
		init_timer(&pwr_ctrl->power_off_timer);
#else
		timer_setup(&pwr_ctrl->power_off_timer, mdla_pwr_timeup, 0);
#endif

		pwr_ctrl->power_off_cb_func = mdla_pwr_off;
		INIT_WORK(&pwr_ctrl->power_off_work,
				pwr_ctrl->power_off_cb_func);

#ifdef RV_COMP /*  No backporting to kernel-4.14 */
		pwr_ctrl->wakeup = wakeup_source_register("mdla");
#else
		pwr_ctrl->wakeup = wakeup_source_register(NULL, "mdla");
#endif
		if (!pwr_ctrl->wakeup)
			mdla_err("mdla%d wakelock register fail!\n", i);

		mdla_device = mdla_get_device(i);
		mdla_device->power          = pwr_ctrl;
		mdla_device->power_is_on    = false;
		mdla_device->sw_power_is_on = false;
	}

	if (on)
		mdla_power.on = on;
	if (off)
		mdla_power.off = off;

	mdla_power.set_opp              = mdla_pwr_set_opp;
	mdla_power.set_opp_by_boost     = mdla_pwr_set_opp_by_boost;
	mdla_power.switch_off_on        = mdla_pwr_switch_off_on;
	mdla_power.off_timer_start      = mdla_pwr_off_timer_start;
	mdla_power.off_timer_cancel     = mdla_pwr_off_timer_cancel;
	mdla_power.lock                 = mdla_pwr_lock;
	mdla_power.unlock               = mdla_pwr_unlock;
	mdla_power.wake_lock            = mdla_pwr_wake_lock;
	mdla_power.wake_unlock          = mdla_pwr_wake_unlock;

	mdla_pwr_fs_init();

	return 0;

alloc_err:
	for (i = i - 1;  i >= 0; i--) {
		pwr_ctrl = mdla_get_device(i)->power;
		wakeup_source_unregister(pwr_ctrl->wakeup);
		mutex_destroy(&pwr_ctrl->lock);
		kfree(pwr_ctrl);
		mdla_get_device(i)->power = NULL;
	}
	i = mdla_util_get_core_num();

reg_pwr_err:
	for (i = i - 1;  i >= 0; i--)
		apu_power_device_unregister(get_pwr_id(i));

	kfree(pwr_data);

	return -1;
}

int mdla_pwr_device_unregister(struct platform_device *pdev)
{
	struct mdla_pwr_ctrl *pwr_ctrl;
	int i;

	for_each_mdla_core(i)
		mdla_power.off(i, 0, true);

	for_each_mdla_core(i) {
		apu_power_device_unregister(get_pwr_id(i));

		pwr_ctrl = mdla_get_device(i)->power;
		if (!pwr_ctrl)
			continue;
		wakeup_source_unregister(pwr_ctrl->wakeup);
		mutex_destroy(&pwr_ctrl->lock);
		kfree(pwr_ctrl);
	}

	kfree(platform_get_drvdata(pdev));

	return 0;
}
