/*
 * Copyright (c) 2015 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#ifndef ZEPHYR_INCLUDE_PM_DEVICE_H_
#define ZEPHYR_INCLUDE_PM_DEVICE_H_

#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/iterable_sections.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Device Power Management API
 * @defgroup subsys_pm_device Device
 * @ingroup subsys_pm
 * @{
 */

/** @cond INTERNAL_HIDDEN */

struct device;

/** @brief Device PM flags. */
enum pm_device_flag {
	/** Indicate if the device is busy or not. */
	PM_DEVICE_FLAG_BUSY,
	/** Indicate if the device failed to power up. */
	PM_DEVICE_FLAG_TURN_ON_FAILED,
	/** Indicate if the device has claimed a power domain */
	PM_DEVICE_FLAG_PD_CLAIMED,
	/**
	 * Indicates whether or not the device is capable of waking the system
	 * up.
	 */
	PM_DEVICE_FLAG_WS_CAPABLE,
	/** Indicates if the device is being used as wakeup source. */
	PM_DEVICE_FLAG_WS_ENABLED,
	/** Indicates if device runtime is enabled  */
	PM_DEVICE_FLAG_RUNTIME_ENABLED,
	/** Indicates if the device is used as a power domain */
	PM_DEVICE_FLAG_PD,
	/** Indicates if device runtime PM should be automatically enabled */
	PM_DEVICE_FLAG_RUNTIME_AUTO,
	/** Indicates that device runtime PM supports suspending and resuming from any context. */
	PM_DEVICE_FLAG_ISR_SAFE,
};

/** @endcond */

/** @brief Flag indicating that runtime PM API for the device can be called from any context.
 *
 * If @ref PM_DEVICE_ISR_SAFE flag is used for device definition, it indicates that PM actions
 * are synchronous and can be executed from any context. This approach can be used for cases where
 * suspending and resuming is short as it is executed in the critical section. This mode requires
 * less resources (~80 byte less RAM) and allows to use device runtime PM from any context
 * (including interrupts).
 */
#define PM_DEVICE_ISR_SAFE 1

/** @brief Device power states. */
enum pm_device_state {
	/** Device is in active or regular state. */
	PM_DEVICE_STATE_ACTIVE,
	/**
	 * Device is suspended.
	 *
	 * @note
	 *     Device context may be lost.
	 */
	PM_DEVICE_STATE_SUSPENDED,
	/** Device is being suspended. */
	PM_DEVICE_STATE_SUSPENDING,
	/**
	 * Device is turned off (power removed).
	 *
	 * @note
	 *     Device context is lost.
	 */
	PM_DEVICE_STATE_OFF
};

/** @brief Device PM actions. */
enum pm_device_action {
	/** Suspend. */
	PM_DEVICE_ACTION_SUSPEND,
	/** Resume. */
	PM_DEVICE_ACTION_RESUME,
	/**
	 * Turn off.
	 * @note
	 *     Action triggered only by a power domain.
	 */
	PM_DEVICE_ACTION_TURN_OFF,
	/**
	 * Turn on.
	 * @note
	 *     Action triggered only by a power domain.
	 */
	PM_DEVICE_ACTION_TURN_ON,
};

/**
 * @brief Device PM action callback.
 *
 * @param dev Device instance.
 * @param action Requested action.
 *
 * @retval 0 If successful.
 * @retval -ENOTSUP If the requested action is not supported.
 * @retval Errno Other negative errno on failure.
 */
typedef int (*pm_device_action_cb_t)(const struct device *dev,
				     enum pm_device_action action);

/**
 * @brief Device PM action failed callback
 *
 * @param dev Device that failed the action.
 * @param err Return code of action failure.
 *
 * @return True to continue iteration, false to halt iteration.
 */
typedef bool (*pm_device_action_failed_cb_t)(const struct device *dev,
					 int err);

/**
 * @brief Device PM info
 *
 * Structure holds fields which are common for two PM devices: generic and
 * synchronous.
 */
struct pm_device_base {
	/** Device PM status flags. */
	atomic_t flags;
	/** Device power state */
	enum pm_device_state state;
	/** Device PM action callback */
	pm_device_action_cb_t action_cb;
#if defined(CONFIG_PM_DEVICE_RUNTIME) || defined(__DOXYGEN__)
	/** Device usage count */
	uint32_t usage;
#endif /* CONFIG_PM_DEVICE_RUNTIME */
#ifdef CONFIG_PM_DEVICE_POWER_DOMAIN
	/** Power Domain it belongs */
	const struct device *domain;
#endif /* CONFIG_PM_DEVICE_POWER_DOMAIN */
};

/**
 * @brief Runtime PM info for device with generic PM.
 *
 * Generic PM involves suspending and resuming operations which can be blocking,
 * long lasting or asynchronous. Runtime PM API is limited when used from
 * interrupt context.
 */
struct pm_device {
	/** Base info. */
	struct pm_device_base base;
#if defined(CONFIG_PM_DEVICE_RUNTIME) || defined(__DOXYGEN__)
	/** Pointer to the device */
	const struct device *dev;
	/** Lock to synchronize the get/put operations */
	struct k_sem lock;
	/** Event var to listen to the sync request events */
	struct k_event event;
	/** Work object for asynchronous calls */
	struct k_work_delayable work;
#endif /* CONFIG_PM_DEVICE_RUNTIME */
};

/**
 * @brief Runtime PM info for device with synchronous PM.
 *
 * Synchronous PM can be used with devices which suspend and resume operations can
 * be performed in the critical section as they are short and non-blocking.
 * Runtime PM API can be used from any context in that case.
 */
struct pm_device_isr {
	/** Base info. */
	struct pm_device_base base;
#if defined(CONFIG_PM_DEVICE_RUNTIME) || defined(__DOXYGEN__)
	/** Lock to synchronize the synchronous get/put operations */
	struct k_spinlock lock;
#endif
};

/* Base part must be the first element. */
BUILD_ASSERT(offsetof(struct pm_device, base) == 0);
BUILD_ASSERT(offsetof(struct pm_device_isr, base) == 0);

/** @cond INTERNAL_HIDDEN */

#ifdef CONFIG_PM_DEVICE_RUNTIME
#define Z_PM_DEVICE_RUNTIME_INIT(obj)			\
	.lock = Z_SEM_INITIALIZER(obj.lock, 1, 1),	\
	.event = Z_EVENT_INITIALIZER(obj.event),
#else
#define Z_PM_DEVICE_RUNTIME_INIT(obj)
#endif /* CONFIG_PM_DEVICE_RUNTIME */

#ifdef CONFIG_PM_DEVICE_POWER_DOMAIN
#define	Z_PM_DEVICE_POWER_DOMAIN_INIT(_node_id)			\
	.domain = DEVICE_DT_GET_OR_NULL(DT_PHANDLE(_node_id,	\
				   power_domains)),
#else
#define Z_PM_DEVICE_POWER_DOMAIN_INIT(obj)
#endif /* CONFIG_PM_DEVICE_POWER_DOMAIN */

/**
 * @brief Utility macro to initialize #pm_device_base flags
 *
 * @param node_id Devicetree node for the initialized device (can be invalid).
 */
#define Z_PM_DEVICE_FLAGS(node_id)					 \
	(COND_CODE_1(							 \
		 DT_NODE_EXISTS(node_id),				 \
		 ((DT_PROP_OR(node_id, wakeup_source, 0)		 \
			 << PM_DEVICE_FLAG_WS_CAPABLE) |		 \
		  (DT_PROP_OR(node_id, zephyr_pm_device_runtime_auto, 0) \
			 << PM_DEVICE_FLAG_RUNTIME_AUTO) |		 \
		  (DT_NODE_HAS_COMPAT(node_id, power_domain) <<		 \
			 PM_DEVICE_FLAG_PD)),				 \
		 (0)))

/**
 * @brief Utility macro to initialize #pm_device.
 *
 * @note #DT_PROP_OR is used to retrieve the wakeup_source property because
 * it may not be defined on all devices.
 *
 * @param obj Name of the #pm_device_base structure being initialized.
 * @param node_id Devicetree node for the initialized device (can be invalid).
 * @param pm_action_cb Device PM control callback function.
 * @param _flags Additional flags passed to the structure.
 */
#define Z_PM_DEVICE_BASE_INIT(obj, node_id, pm_action_cb, _flags)	     \
	{								     \
		.flags = ATOMIC_INIT(Z_PM_DEVICE_FLAGS(node_id) | (_flags)), \
		.state = PM_DEVICE_STATE_ACTIVE,			     \
		.action_cb = pm_action_cb,				     \
		Z_PM_DEVICE_POWER_DOMAIN_INIT(node_id)			     \
	}

/**
 * @brief Utility macro to initialize #pm_device_rt.
 *
 * @note #DT_PROP_OR is used to retrieve the wakeup_source property because
 * it may not be defined on all devices.
 *
 * @param obj Name of the #pm_device_base structure being initialized.
 * @param node_id Devicetree node for the initialized device (can be invalid).
 * @param pm_action_cb Device PM control callback function.
 */
#define Z_PM_DEVICE_INIT(obj, node_id, pm_action_cb, isr_safe)			\
	{									\
		.base = Z_PM_DEVICE_BASE_INIT(obj, node_id, pm_action_cb,	\
				isr_safe ? BIT(PM_DEVICE_FLAG_ISR_SAFE) : 0),	\
		COND_CODE_1(isr_safe, (), (Z_PM_DEVICE_RUNTIME_INIT(obj)))	\
	}

/**
 * Get the name of device PM resources.
 *
 * @param dev_id Device id.
 */
#define Z_PM_DEVICE_NAME(dev_id) _CONCAT(__pm_device_, dev_id)

#ifdef CONFIG_PM
/**
 * @brief Define device PM slot.
 *
 * This macro defines a pointer to a device in the pm_device_slots region.
 * When invoked for each device with PM, it will effectively result in a device
 * pointer array with the same size of the actual devices with PM enabled. This
 * is used internally by the PM subsystem to keep track of suspended devices
 * during system power transitions.
 *
 * @param dev_id Device id.
 */
#define Z_PM_DEVICE_DEFINE_SLOT(dev_id)					\
	static STRUCT_SECTION_ITERABLE_ALTERNATE(pm_device_slots, device, \
			_CONCAT(__pm_slot_, dev_id))
#else
#define Z_PM_DEVICE_DEFINE_SLOT(dev_id)
#endif /* CONFIG_PM */

#ifdef CONFIG_PM_DEVICE
/**
 * Define device PM resources for the given node identifier.
 *
 * @param node_id Node identifier (DT_INVALID_NODE if not a DT device).
 * @param dev_id Device id.
 * @param pm_action_cb PM control callback.
 */
#define Z_PM_DEVICE_DEFINE(node_id, dev_id, pm_action_cb, isr_safe)		\
	Z_PM_DEVICE_DEFINE_SLOT(dev_id);					\
	static struct COND_CODE_1(isr_safe, (pm_device_isr), (pm_device))	\
		Z_PM_DEVICE_NAME(dev_id) =					\
		Z_PM_DEVICE_INIT(Z_PM_DEVICE_NAME(dev_id), node_id,		\
				 pm_action_cb, isr_safe)

/**
 * Get a reference to the device PM resources.
 *
 * @param dev_id Device id.
 */
#define Z_PM_DEVICE_GET(dev_id) ((struct pm_device_base *)&Z_PM_DEVICE_NAME(dev_id))

#else
#define Z_PM_DEVICE_DEFINE(node_id, dev_id, pm_action_cb, isr_safe)
#define Z_PM_DEVICE_GET(dev_id) NULL
#endif /* CONFIG_PM_DEVICE */

/** @endcond */

/**
 * Define device PM resources for the given device name.
 *
 * @note This macro is a no-op if @kconfig{CONFIG_PM_DEVICE} is not enabled.
 *
 * @param dev_id Device id.
 * @param pm_action_cb PM control callback.
 * @param ... Optional flag to indicate that ISR safe. Use @ref PM_DEVICE_ISR_SAFE or 0.
 *
 * @see #PM_DEVICE_DT_DEFINE, #PM_DEVICE_DT_INST_DEFINE
 */
#define PM_DEVICE_DEFINE(dev_id, pm_action_cb, ...)			\
	Z_PM_DEVICE_DEFINE(DT_INVALID_NODE, dev_id, pm_action_cb,	\
			COND_CODE_1(IS_EMPTY(__VA_ARGS__), (0), (__VA_ARGS__)))

/**
 * Define device PM resources for the given node identifier.
 *
 * @note This macro is a no-op if @kconfig{CONFIG_PM_DEVICE} is not enabled.
 *
 * @param node_id Node identifier.
 * @param pm_action_cb PM control callback.
 * @param ... Optional flag to indicate that device is isr_ok. Use @ref PM_DEVICE_ISR_SAFE or 0.
 *
 * @see #PM_DEVICE_DT_INST_DEFINE, #PM_DEVICE_DEFINE
 */
#define PM_DEVICE_DT_DEFINE(node_id, pm_action_cb, ...) \
	Z_PM_DEVICE_DEFINE(node_id, Z_DEVICE_DT_DEV_ID(node_id), pm_action_cb, \
			COND_CODE_1(IS_EMPTY(__VA_ARGS__), (0), (__VA_ARGS__)))

/**
 * Define device PM resources for the given instance.
 *
 * @note This macro is a no-op if @kconfig{CONFIG_PM_DEVICE} is not enabled.
 *
 * @param idx Instance index.
 * @param pm_action_cb PM control callback.
 * @param ... Optional flag to indicate that device is isr_ok. Use @ref PM_DEVICE_ISR_SAFE or 0.
 *
 * @see #PM_DEVICE_DT_DEFINE, #PM_DEVICE_DEFINE
 */
#define PM_DEVICE_DT_INST_DEFINE(idx, pm_action_cb, ...)		\
	Z_PM_DEVICE_DEFINE(DT_DRV_INST(idx),				\
			   Z_DEVICE_DT_DEV_ID(DT_DRV_INST(idx)),	\
			   pm_action_cb,				\
			   COND_CODE_1(IS_EMPTY(__VA_ARGS__), (0), (__VA_ARGS__)))

/**
 * @brief Obtain a reference to the device PM resources for the given device.
 *
 * @param dev_id Device id.
 *
 * @return Reference to the device PM resources (NULL if device
 * @kconfig{CONFIG_PM_DEVICE} is disabled).
 */
#define PM_DEVICE_GET(dev_id) \
	Z_PM_DEVICE_GET(dev_id)

/**
 * @brief Obtain a reference to the device PM resources for the given node.
 *
 * @param node_id Node identifier.
 *
 * @return Reference to the device PM resources (NULL if device
 * @kconfig{CONFIG_PM_DEVICE} is disabled).
 */
#define PM_DEVICE_DT_GET(node_id) \
	PM_DEVICE_GET(Z_DEVICE_DT_DEV_ID(node_id))

/**
 * @brief Obtain a reference to the device PM resources for the given instance.
 *
 * @param idx Instance index.
 *
 * @return Reference to the device PM resources (NULL if device
 * @kconfig{CONFIG_PM_DEVICE} is disabled).
 */
#define PM_DEVICE_DT_INST_GET(idx) \
	PM_DEVICE_DT_GET(DT_DRV_INST(idx))

/**
 * @brief Get name of device PM state
 *
 * @param state State id which name should be returned
 */
const char *pm_device_state_str(enum pm_device_state state);

/**
 * @brief Run a pm action on a device.
 *
 * This function calls the device PM control callback so that the device does
 * the necessary operations to execute the given action.
 *
 * @param dev Device instance.
 * @param action Device pm action.
 *
 * @retval 0 If successful.
 * @retval -ENOTSUP If requested state is not supported.
 * @retval -EALREADY If device is already at the requested state.
 * @retval -EBUSY If device is changing its state.
 * @retval -ENOSYS If device does not support PM.
 * @retval -EPERM If device has power state locked.
 * @retval Errno Other negative errno on failure.
 */
int pm_device_action_run(const struct device *dev,
		enum pm_device_action action);

/**
 * @brief Run a pm action on all children of a device.
 *
 * This function calls all child devices PM control callback so that the device
 * does the necessary operations to execute the given action.
 *
 * @param dev Device instance.
 * @param action Device pm action.
 * @param failure_cb Function to call if a child fails the action, can be NULL.
 */
void pm_device_children_action_run(const struct device *dev,
		enum pm_device_action action,
		pm_device_action_failed_cb_t failure_cb);

#if defined(CONFIG_PM_DEVICE) || defined(__DOXYGEN__)
/**
 * @brief Obtain the power state of a device.
 *
 * @param dev Device instance.
 * @param state Pointer where device power state will be stored.
 *
 * @retval 0 If successful.
 * @retval -ENOSYS If device does not implement power management.
 */
int pm_device_state_get(const struct device *dev,
			enum pm_device_state *state);

/**
 * @brief Initialize a device state to #PM_DEVICE_STATE_SUSPENDED.
 *
 * By default device state is initialized to #PM_DEVICE_STATE_ACTIVE. However
 * in order to save power some drivers may choose to only initialize the device
 * to the suspended state, or actively put the device into the suspended state.
 * This function can therefore be used to notify the PM subsystem that the
 * device is in #PM_DEVICE_STATE_SUSPENDED instead of the default.
 *
 * @param dev Device instance.
 */
static inline void pm_device_init_suspended(const struct device *dev)
{
	struct pm_device_base *pm = dev->pm_base;

	pm->state = PM_DEVICE_STATE_SUSPENDED;
}

/**
 * @brief Initialize a device state to #PM_DEVICE_STATE_OFF.
 *
 * By default device state is initialized to #PM_DEVICE_STATE_ACTIVE. In
 * general, this makes sense because the device initialization function will
 * resume and configure a device, leaving it operational. However, when power
 * domains are enabled, the device may be connected to a switchable power
 * source, in which case it won't be powered at boot. This function can
 * therefore be used to notify the PM subsystem that the device is in
 * #PM_DEVICE_STATE_OFF instead of the default.
 *
 * @param dev Device instance.
 */
static inline void pm_device_init_off(const struct device *dev)
{
	struct pm_device_base *pm = dev->pm_base;

	pm->state = PM_DEVICE_STATE_OFF;
}

/**
 * @brief Mark a device as busy.
 *
 * Devices marked as busy will not be suspended when the system goes into
 * low-power states. This can be useful if, for example, the device is in the
 * middle of a transaction.
 *
 * @param dev Device instance.
 *
 * @see pm_device_busy_clear()
 */
void pm_device_busy_set(const struct device *dev);

/**
 * @brief Clear a device busy status.
 *
 * @param dev Device instance.
 *
 * @see pm_device_busy_set()
 */
void pm_device_busy_clear(const struct device *dev);

/**
 * @brief Check if any device is busy.
 *
 * @retval false If no device is busy
 * @retval true If one or more devices are busy
 */
bool pm_device_is_any_busy(void);

/**
 * @brief Check if a device is busy.
 *
 * @param dev Device instance.
 *
 * @retval false If the device is not busy
 * @retval true If the device is busy
 */
bool pm_device_is_busy(const struct device *dev);

/**
 * @brief Enable or disable a device as a wake up source.
 *
 * A device marked as a wake up source will not be suspended when the system
 * goes into low-power modes, thus allowing to use it as a wake up source for
 * the system.
 *
 * @param dev Device instance.
 * @param enable @c true to enable or @c false to disable
 *
 * @retval true If the wakeup source was successfully enabled.
 * @retval false If the wakeup source was not successfully enabled.
 */
bool pm_device_wakeup_enable(const struct device *dev, bool enable);

/**
 * @brief Check if a device is enabled as a wake up source.
 *
 * @param dev Device instance.
 *
 * @retval true if the wakeup source is enabled.
 * @retval false if the wakeup source is not enabled.
 */
bool pm_device_wakeup_is_enabled(const struct device *dev);

/**
 * @brief Check if a device is wake up capable
 *
 * @param dev Device instance.
 *
 * @retval true If the device is wake up capable.
 * @retval false If the device is not wake up capable.
 */
bool pm_device_wakeup_is_capable(const struct device *dev);

/**
 * @brief Check if the device is on a switchable power domain.
 *
 * @param dev Device instance.
 *
 * @retval true If device is on a switchable power domain.
 * @retval false If device is not on a switchable power domain.
 */
bool pm_device_on_power_domain(const struct device *dev);

/**
 * @brief Add a device to a power domain.
 *
 * This function adds a device to a given power domain.
 *
 * @param dev Device to be added to the power domain.
 * @param domain Power domain.
 *
 * @retval 0 If successful.
 * @retval -EALREADY If device is already part of the power domain.
 * @retval -ENOSYS If the application was built without power domain support.
 * @retval -ENOSPC If there is no space available in the power domain to add the device.
 */
int pm_device_power_domain_add(const struct device *dev,
			       const struct device *domain);

/**
 * @brief Remove a device from a power domain.
 *
 * This function removes a device from a given power domain.
 *
 * @param dev Device to be removed from the power domain.
 * @param domain Power domain.
 *
 * @retval 0 If successful.
 * @retval -ENOSYS If the application was built without power domain support.
 * @retval -ENOENT If device is not in the given domain.
 */
int pm_device_power_domain_remove(const struct device *dev,
				  const struct device *domain);

/**
 * @brief Check if the device is currently powered.
 *
 * @param dev Device instance.
 *
 * @retval true If device is currently powered, or is assumed to be powered
 * (i.e. it does not support PM or is not under a PM domain)
 * @retval false If device is not currently powered
 */
bool pm_device_is_powered(const struct device *dev);

/**
 * @brief Setup a device driver into the lowest valid power mode
 *
 * This helper function is intended to be called at the end of a driver
 * init function to automatically setup the device into the lowest power
 * mode. It assumes that the device has been configured as if it is in
 * @ref PM_DEVICE_STATE_OFF, or @ref PM_DEVICE_STATE_SUSPENDED if device can
 * never be powered off.
 *
 * @param dev Device instance.
 * @param action_cb Device PM control callback function.
 * @retval 0 On success.
 * @retval -errno Error code from @a action_cb on failure.
 */
int pm_device_driver_init(const struct device *dev, pm_device_action_cb_t action_cb);

#else
static inline int pm_device_state_get(const struct device *dev,
				      enum pm_device_state *state)
{
	ARG_UNUSED(dev);

	*state = PM_DEVICE_STATE_ACTIVE;

	return 0;
}

static inline void pm_device_init_suspended(const struct device *dev)
{
	ARG_UNUSED(dev);
}
static inline void pm_device_init_off(const struct device *dev)
{
	ARG_UNUSED(dev);
}
static inline void pm_device_busy_set(const struct device *dev)
{
	ARG_UNUSED(dev);
}
static inline void pm_device_busy_clear(const struct device *dev)
{
	ARG_UNUSED(dev);
}
static inline bool pm_device_is_any_busy(void) { return false; }
static inline bool pm_device_is_busy(const struct device *dev)
{
	ARG_UNUSED(dev);
	return false;
}
static inline bool pm_device_wakeup_enable(const struct device *dev,
					   bool enable)
{
	ARG_UNUSED(dev);
	ARG_UNUSED(enable);
	return false;
}
static inline bool pm_device_wakeup_is_enabled(const struct device *dev)
{
	ARG_UNUSED(dev);
	return false;
}
static inline bool pm_device_wakeup_is_capable(const struct device *dev)
{
	ARG_UNUSED(dev);
	return false;
}
static inline bool pm_device_on_power_domain(const struct device *dev)
{
	ARG_UNUSED(dev);
	return false;
}

static inline int pm_device_power_domain_add(const struct device *dev,
					     const struct device *domain)
{
	ARG_UNUSED(dev);
	ARG_UNUSED(domain);
	return -ENOSYS;
}

static inline int pm_device_power_domain_remove(const struct device *dev,
						const struct device *domain)
{
	ARG_UNUSED(dev);
	ARG_UNUSED(domain);
	return -ENOSYS;
}

static inline bool pm_device_is_powered(const struct device *dev)
{
	ARG_UNUSED(dev);
	return true;
}

static inline int pm_device_driver_init(const struct device *dev, pm_device_action_cb_t action_cb)
{
	int rc;

	/* When power management is not enabled, all drivers should initialise to active state */
	rc = action_cb(dev, PM_DEVICE_ACTION_TURN_ON);
	if ((rc < 0) && (rc != -ENOTSUP)) {
		return rc;
	}

	rc = action_cb(dev, PM_DEVICE_ACTION_RESUME);
	if (rc < 0) {
		return rc;
	}

	return 0;
}

#endif /* CONFIG_PM_DEVICE */

/** @} */

#ifdef __cplusplus
}
#endif

#endif
