/*
 * Copyright (c) 2015, ARM Limited and Contributors. All rights reserved.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <arch_helpers.h>
#include <assert.h>
#include <errno.h>
#include <platform.h>
#include <psci.h>

/*
 * The platform hooks exported by the platform using the earlier version of
 * platform interface
 */
const plat_pm_ops_t *pm_ops;

/*
 * The hooks exported by the compatibility layer
 */
static plat_psci_ops_t compat_psci_ops;

/*
 * The secure entry point to be used on warm reset.
 */
static unsigned long secure_entrypoint;

/*
 * This array stores the 'power_state' requests of each CPU during
 * CPU_SUSPEND and SYSTEM_SUSPEND to support querying of state-ID
 * by the platform.
 */
unsigned int psci_power_state_compat[PLATFORM_CORE_COUNT];

/*******************************************************************************
 * The PSCI compatibility helper to parse the power state and populate the
 * 'pwr_domain_state' for each power level. It is assumed that, when in
 * compatibility mode, the PSCI generic layer need to know only whether the
 * affinity level will be OFF or in RETENTION and if the platform supports
 * multiple power down and retention states, it will be taken care within
 * the platform layer.
 ******************************************************************************/
static int parse_power_state(unsigned int power_state,
		    psci_power_state_t *req_state)
{
	int i;
	int pstate = psci_get_pstate_type(power_state);
	int aff_lvl = psci_get_pstate_pwrlvl(power_state);

	if (aff_lvl > PLATFORM_MAX_AFFLVL)
		return PSCI_E_INVALID_PARAMS;

	/* Sanity check the requested state */
	if (pstate == PSTATE_TYPE_STANDBY) {
		/*
		 * Set the CPU local state as retention and ignore the higher
		 * levels. This allows the generic PSCI layer to invoke
		 * plat_psci_ops 'cpu_standby' hook and the compatibility
		 * layer invokes the 'affinst_standby' handler with the
		 * correct power_state parameter thus preserving the correct
		 * behavior.
		 */
		req_state->pwr_domain_state[0] =
					PLAT_MAX_RET_STATE;
	} else {
		for (i = 0; i <= aff_lvl; i++)
			req_state->pwr_domain_state[i] =
					PLAT_MAX_OFF_STATE;
	}

	return PSCI_E_SUCCESS;
}

/*******************************************************************************
 * The PSCI compatibility helper to set the 'power_state' in
 * psci_power_state_compat[] at index corresponding to the current core.
 ******************************************************************************/
static void set_psci_power_state_compat(unsigned int power_state)
{
	unsigned int my_core_pos = plat_my_core_pos();

	psci_power_state_compat[my_core_pos] = power_state;
	flush_dcache_range((uintptr_t) &psci_power_state_compat[my_core_pos],
			sizeof(psci_power_state_compat[my_core_pos]));
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'validate_power_state'
 * hook.
 ******************************************************************************/
static int validate_power_state_compat(unsigned int power_state,
			    psci_power_state_t *req_state)
{
	int rc;
	assert(req_state);

	if (pm_ops->validate_power_state) {
		rc = pm_ops->validate_power_state(power_state);
		if (rc != PSCI_E_SUCCESS)
			return rc;
	}

	/* Store the 'power_state' parameter for the current CPU. */
	set_psci_power_state_compat(power_state);

	return parse_power_state(power_state, req_state);
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t
 * 'get_sys_suspend_power_state' hook.
 ******************************************************************************/
void get_sys_suspend_power_state_compat(psci_power_state_t *req_state)
{
	unsigned int power_state;
	assert(req_state);

	power_state = pm_ops->get_sys_suspend_power_state();

	/* Store the 'power_state' parameter for the current CPU. */
	set_psci_power_state_compat(power_state);

	if (parse_power_state(power_state, req_state) != PSCI_E_SUCCESS)
		assert(0);
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'validate_ns_entrypoint'
 * hook.
 ******************************************************************************/
static int validate_ns_entrypoint_compat(uintptr_t ns_entrypoint)
{
	return pm_ops->validate_ns_entrypoint(ns_entrypoint);
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'affinst_standby' hook.
 ******************************************************************************/
static void cpu_standby_compat(plat_local_state_t cpu_state)
{
	unsigned int powerstate = psci_get_suspend_powerstate();

	assert(powerstate != PSCI_INVALID_DATA);

	pm_ops->affinst_standby(powerstate);
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'affinst_on' hook.
 ******************************************************************************/
static int pwr_domain_on_compat(u_register_t mpidr)
{
	int level, rc;

	/*
	 * The new PSCI framework does not hold the locks for higher level
	 * power domain nodes when this hook is invoked. Hence figuring out the
	 * target state of the parent power domains does not make much sense.
	 * Hence we hard-code the state as PSCI_STATE_OFF for all the levels.
	 * We expect the platform to perform the necessary CPU_ON operations
	 * when the 'affinst_on' is invoked only for level 0.
	 */
	for (level = PLATFORM_MAX_AFFLVL; level >= 0; level--) {
		rc = pm_ops->affinst_on((unsigned long)mpidr, secure_entrypoint,
					level, PSCI_STATE_OFF);
		if (rc != PSCI_E_SUCCESS)
			break;
	}

	return rc;
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'affinst_off' hook.
 ******************************************************************************/
static void pwr_domain_off_compat(const psci_power_state_t *target_state)
{
	int level;
	unsigned int plat_state;

	for (level = 0; level <= PLATFORM_MAX_AFFLVL; level++) {
		plat_state = (is_local_state_run(
				target_state->pwr_domain_state[level]) ?
				PSCI_STATE_ON : PSCI_STATE_OFF);
		pm_ops->affinst_off(level, plat_state);
	}
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'affinst_suspend' hook.
 ******************************************************************************/
static void pwr_domain_suspend_compat(const psci_power_state_t *target_state)
{
	int level;
	unsigned int plat_state;

	for (level = 0; level <= psci_get_suspend_afflvl(); level++) {
		plat_state = (is_local_state_run(
				target_state->pwr_domain_state[level]) ?
				PSCI_STATE_ON : PSCI_STATE_OFF);
		pm_ops->affinst_suspend(secure_entrypoint, level, plat_state);
	}
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'affinst_on_finish'
 * hook.
 ******************************************************************************/
static void pwr_domain_on_finish_compat(const psci_power_state_t *target_state)
{
	int level;
	unsigned int plat_state;

	for (level = PLATFORM_MAX_AFFLVL; level >= 0; level--) {
		plat_state = (is_local_state_run(
				target_state->pwr_domain_state[level]) ?
				PSCI_STATE_ON : PSCI_STATE_OFF);
		pm_ops->affinst_on_finish(level, plat_state);
	}
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t
 * 'affinst_suspend_finish' hook.
 ******************************************************************************/
static void pwr_domain_suspend_finish_compat(
				const psci_power_state_t *target_state)
{
	int level;
	unsigned int plat_state;

	for (level = psci_get_suspend_afflvl(); level >= 0; level--) {
		plat_state = (is_local_state_run(
				target_state->pwr_domain_state[level]) ?
				PSCI_STATE_ON : PSCI_STATE_OFF);
		pm_ops->affinst_suspend_finish(level, plat_state);
	}
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'system_off' hook.
 ******************************************************************************/
static void __dead2 system_off_compat(void)
{
	pm_ops->system_off();
}

/*******************************************************************************
 * The PSCI compatibility helper for plat_pm_ops_t 'system_reset' hook.
 ******************************************************************************/
static void __dead2 system_reset_compat(void)
{
	pm_ops->system_reset();
}

/*******************************************************************************
 * Export the compatibility compat_psci_ops. The assumption made is that the
 * power domains correspond to affinity instances on the platform.
 ******************************************************************************/
int plat_setup_psci_ops(uintptr_t sec_entrypoint,
				const plat_psci_ops_t **psci_ops)
{
	platform_setup_pm(&pm_ops);

	secure_entrypoint = (unsigned long) sec_entrypoint;

	/*
	 * It is compulsory for the platform ports using the new porting
	 * interface to export a hook to validate the power state parameter
	 */
	compat_psci_ops.validate_power_state = validate_power_state_compat;

	/*
	 * Populate the compatibility plat_psci_ops_t hooks if available
	 */
	if (pm_ops->validate_ns_entrypoint)
		compat_psci_ops.validate_ns_entrypoint =
				validate_ns_entrypoint_compat;

	if (pm_ops->affinst_standby)
		compat_psci_ops.cpu_standby = cpu_standby_compat;

	if (pm_ops->affinst_on)
		compat_psci_ops.pwr_domain_on = pwr_domain_on_compat;

	if (pm_ops->affinst_off)
		compat_psci_ops.pwr_domain_off = pwr_domain_off_compat;

	if (pm_ops->affinst_suspend)
		compat_psci_ops.pwr_domain_suspend = pwr_domain_suspend_compat;

	if (pm_ops->affinst_on_finish)
		compat_psci_ops.pwr_domain_on_finish =
				pwr_domain_on_finish_compat;

	if (pm_ops->affinst_suspend_finish)
		compat_psci_ops.pwr_domain_suspend_finish =
				pwr_domain_suspend_finish_compat;

	if (pm_ops->system_off)
		compat_psci_ops.system_off = system_off_compat;

	if (pm_ops->system_reset)
		compat_psci_ops.system_reset = system_reset_compat;

	if (pm_ops->get_sys_suspend_power_state)
		compat_psci_ops.get_sys_suspend_power_state =
				get_sys_suspend_power_state_compat;

	*psci_ops = &compat_psci_ops;
	return 0;
}
