/*
 * Copyright 2021-2022 NXP
 * All rights reserved.
 *
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

/************************************************************************************
 * Include
 ************************************************************************************/

#include "fsl_common.h"
#include "PWR_Interface.h"
#include "fwk_platform_lowpower.h"
#include "fsl_pm_core.h"
#include "fwk_debug.h"

/************************************************************************************
 * Private type definitions and macros
 ************************************************************************************/

//#define PWR_DBG_DISABLE_LOWPOWER

/************************************************************************************
 * Private functions prototypes
 ************************************************************************************/
static status_t PWR_LowpowerCb(pm_event_type_t eventType, uint8_t powerState, void *data);
__WEAK uint32_t PWR_GetRadioNextEventUs(void);

/************************************************************************************
 * Private memory declarations
 ************************************************************************************/

static bool        initialized = false;
static pm_handle_t pm_hdl;

static pm_notify_element_t g_PwrNotify = {
    .notifyCallback = PWR_LowpowerCb,
    .data           = NULL,
};

/************************************************************************************
 * Private functions definitions
 ************************************************************************************/

/*!
 * \brief Callback registered to SDK Power Manager to get notified of entry/exit of low power modes
 *
 * \param[in] eventType event specifying if we entered or exited from low power mode
 * \param[in] powerState low power mode used during low power period
 * \param[in] data Optional data passed when the callback got registered (not used currently)
 * \return status_t
 */
static status_t PWR_LowpowerCb(pm_event_type_t eventType, uint8_t powerState, void *data)
{
    NOT_USED(data);

#if defined(gDbg_Enabled_d) && (gDbg_Enabled_d > 0)
    static int lp_nb = 0;
    if (eventType == kPM_EventEnteringSleep)
    {
        PWR_DBG_LOG("[%d] evt=%d pwrstate=%d", lp_nb, eventType, powerState);
    }
#endif
    /* Nothing to do on Sleep state (WFI) but only Deeper low power mode */
    if (powerState < PLATFORM_DEEP_SLEEP_STATE)
    {
        /* Nothing to do when entering WFI or Sleep low power state
            NVIC fully functionnal to trigger upcoming interrupts */
    }
    else
    {
        if (eventType == kPM_EventEnteringSleep)
        {
#if defined(gDbg_Enabled_d) && (gDbg_Enabled_d > 0)
            /* NXP logging dump */
#define DUMP_EVERY_X_TIMES 4
            static int count = DUMP_EVERY_X_TIMES;
            if (--count == 0)
            {
                DBG_LOG_DUMP();
                count = DUMP_EVERY_X_TIMES;
            }
#endif
            /* Platform module can implement platform specific methods to execute
             * when entering and exiting any low power mode.
             * Those methods should implement only mandatory procedures for the
             * platform, compatible with any connectivity protocol */
#if 0
            uint8_t mode;
            mode = PM_GetAllowedLowestPowerMode();
            if (mode  == (PLATFORM_DEEP_POWER_DOWN_STATE))
            {
                BOARD_DbgStopLoggingTimer();
            }
#endif

            PLATFORM_EnterLowPower();

            if (powerState >= PLATFORM_POWER_DOWN_STATE)
            {
                /* Power gated low power modes often require extra specific
                 * entry/exit low power procedures, those should be implemented
                 * in the following PLATFORM API */
                PLATFORM_EnterPowerDown();
            }

            if (powerState == PLATFORM_DEEP_POWER_DOWN_STATE)
            {
                /* Perform specific procedures when entering RAMOFF such as
                 * powering off the radio domain */
                PLATFORM_EnterDeepPowerDown();
            }
        }
        else
        {
            /* Check if Main power domain domain really went to Power down,
             *   powerState variable is just an indication, Lowpower mode could have been skipped by an immediate wakeup
             */
            PLATFORM_PowerDomainState_t main_pd_state = PLATFORM_NO_LOWPOWER;
            PLATFORM_status_t           status;
            status = PLATFORM_GetLowpowerMode(PLATFORM_MainDomain, &main_pd_state);
            assert(status == PLATFORM_Successful);
            (void)status;
#if 0
            PLATFORM_PowerDomainState_t   wakeup_pd_state;
            PLATFORM_GetLowpowerMode(PLATFORM_WakeupDomain, &wakeup_pd_state);
            PWR_DBG_LOG("main_pd=%d wakeup_pd=%d", main_pd_state, wakeup_pd_state);
#endif

            if (main_pd_state == PLATFORM_POWER_DOWN_MODE)
            {
                /* Power gated low power modes often require specific
                 * entry/exit low power procedures, those should be implemented
                 * in the following PLATFORM API */
                PLATFORM_ExitPowerDown();
            }

            // DBG_LOG_WAKEUP_CHECK();

            /* Platform specific procedures to execute when exiting low power mode
             * any low power mode */
            PLATFORM_ExitLowPower();
        }
    }

    /* Debug Only */
#if defined(gDbg_Enabled_d) && (gDbg_Enabled_d > 0)
    /* On wakeup, display pending interrupts */
    if (eventType != kPM_EventEnteringSleep)
    {
        lp_nb++;
        BOARD_DbgCheckIrqPending(false);
    }
#endif

    return kStatus_Success;
}

static uint64_t PWR_GetMinValue(uint64_t osTime, uint64_t radioTime)
{
    uint64_t minTime;

    /* 0 is considered as no timeout, so this is not the minimal value */
    if (osTime == 0U)
    {
        minTime = radioTime;
    }
    if (radioTime == 0U)
    {
        minTime = osTime;
    }
    else
    {
        minTime = MIN(osTime, radioTime);
    }
    return minTime;
}

/************************************************************************************
 * Public functions
 ************************************************************************************/
/* Weak version of the systick functions to have a definition even in baremetal, as they are declared in
 * PWR_Interface.h, those functions are not supposed to be called in baremetal*/
__attribute__((weak)) bool PWR_SysticksPreProcess(uint32_t xExpectedIdleTime, uint64_t *expectedIdleTimeUs)
{
    (void)xExpectedIdleTime;
    (void)expectedIdleTimeUs;
    assert(0);
    return false;
}
__attribute__((weak)) void PWR_SysticksPostProcess(uint64_t expectedIdleTimeUs, uint64_t actualIdleTimeUs)
{
    (void)expectedIdleTimeUs;
    (void)actualIdleTimeUs;
    assert(0);
}
void PWR_Init(void)
{
    status_t status;

    // BOARD_DbgResetCheck();
    if (initialized == false)
    {
        PM_CreateHandle(&pm_hdl);
        /* Register the PWR low power Notify callbacks as high priority (kPM_NotifyGroup2) */
        status = PM_RegisterNotify(kPM_NotifyGroup2, &g_PwrNotify);
        assert(status == kStatus_Success);
        (void)status;

        PM_EnablePowerManager(true);

        /* Initialize platform specific ressources for low power support */
        PLATFORM_LowPowerInit();

        // BOARD_DbgCheckIrqPending(FALSE);

        initialized = true;
    }

    // PWR_DBG_LOG("<--", 0);
}

uint64_t PWR_EnterLowPower(uint64_t timeoutUs)
{
    uint64_t lowPowerDurationUs = 0U;

    // PWR_DBG_LOG("osTimeoutUs=%d %d", (uint32_t)(osTimeoutUs >> 32), (uint32_t)osTimeoutUs);

    /* identification for PWR_EnterLowPower() entry */
    BOARD_DBGLPIOSET(3, 1);
    BOARD_DBGLPIOSET(3, 0);
    BOARD_DBGLPIOSET(3, 1);

    if (initialized == true)
    {
        uint64_t lowPowerEntryTimestamp = 0U;
        uint64_t lowPowerExitTimestamp  = 0U;

        uint64_t minTimeoutUs;
        uint64_t nextRadioEvtUs;

        /* getting Next radio event is only usefull for the SDK power manager to estimate the best low power mode
            it can get into depending of the low power mode exit latency. if the next Radio event time is too short,
            a lighter low power mode will be chosen */
        nextRadioEvtUs = (uint64_t)PWR_GetRadioNextEventUs();

        /* Get Min value considering 0 is no timeout */
        minTimeoutUs = PWR_GetMinValue(timeoutUs, nextRadioEvtUs);

        if (minTimeoutUs == 0U)
        {
            /* no next event schedule */
            PWR_DBG_LOG("no next event schedule");
        }
        else if (minTimeoutUs == timeoutUs)
        {
            /* BLE Link layer has its own wakeup capability so start timer only if the next event if for os */
            PLATFORM_StartWakeUpTimer(timeoutUs);
            PWR_DBG_LOG("Timer started: %d ", (uint32_t)minTimeoutUs);
        }
        else
        {
            /* next event is Link layer, This last will wake up the device from BLE LL timer, not SOC timer */
            PWR_DBG_LOG("nxt evt is LL: %d ", (uint32_t)nextRadioEvtUs);
        }

        lowPowerEntryTimestamp = PLATFORM_GetLowPowerTimestampUs();

        BOARD_DBGLPIOSET(3, 0);
        BOARD_DBGLPIOSET(3, 1);

#ifdef PWR_DBG_DISABLE_LOWPOWER
        SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
        __WFI();
#else
        /* MinTimeoutUs is only given to estimate the best low power mode in case it is too short
            0 means no timeout*/
        PM_EnterLowPower(minTimeoutUs);
#endif
        BOARD_DBGLPIOSET(3, 0);
        BOARD_DBGLPIOSET(3, 1);

        /* disable in case it was started for os event */
        PLATFORM_StopWakeUpTimer();

        /* get time the device was in low power */
        lowPowerExitTimestamp = PLATFORM_GetLowPowerTimestampUs();
        lowPowerDurationUs    = PLATFORM_GetLowPowerDurationUs(lowPowerEntryTimestamp, lowPowerExitTimestamp);

        // PWR_DBG_LOG("%d %d %d", (uint32_t)lowPowerEntryTimestamp, (uint32_t)lowPowerExitTimestamp,
        // (uint32_t)lowPowerDurationUs);
    }

    BOARD_DBGLPIOSET(3, 0);

    return lowPowerDurationUs;
}

PWR_ReturnStatus_t PWR_SetLowPowerModeConstraint(PWR_LowpowerMode_t mode)
{
    PWR_ReturnStatus_t ret = PWR_Success;
    status_t           st  = kStatus_Success;

    // PWR_DBG_LOG("-->mode=%d, deepest lp mode=%d", mode, PM_GetAllowedLowestPowerMode());

    switch (mode)
    {
        case PWR_WFI:
            st = PM_SetConstraints(PLATFORM_WFI_STATE, PLATFORM_WFI_CONSTRAINTS);
            break;

        case PWR_Sleep:
            st = PM_SetConstraints(PLATFORM_SLEEP_STATE, PLATFORM_SLEEP_CONSTRAINTS);
            break;

        case PWR_DeepSleep:
            st = PM_SetConstraints(PLATFORM_DEEP_SLEEP_STATE, PLATFORM_DEEP_SLEEP_CONSTRAINTS);
            break;

        case PWR_PowerDown:
        {
            st = PM_SetConstraints(PLATFORM_POWER_DOWN_STATE, PLATFORM_POWER_DOWN_CONSTRAINTS);
            break;
        }

        case PWR_DeepPowerDown:
        {
            st = PM_SetConstraints(PLATFORM_DEEP_POWER_DOWN_STATE, PLATFORM_DEEP_POWER_DOWN_CONSTRAINTS);
            break;
        }

        default:
            ret = PWR_ErrorNotSupported;
            break;
    }

    if (st != kStatus_Success)
    {
        ret = PWR_Error;
    }

    // PWR_DBG_LOG("<--deepest lp mode=%d", PM_GetAllowedLowestPowerMode());

    return ret;
}

PWR_ReturnStatus_t PWR_ReleaseLowPowerModeConstraint(PWR_LowpowerMode_t mode)
{
    PWR_ReturnStatus_t ret = PWR_Success;
    status_t           st  = kStatus_Success;

    // PWR_DBG_LOG("-->mode=%d, deepest lp mode=%d", mode, PM_GetAllowedLowestPowerMode());

    switch (mode)
    {
        case PWR_WFI:
            st = PM_ReleaseConstraints(PLATFORM_WFI_STATE, PLATFORM_WFI_CONSTRAINTS);
            break;

        case PWR_Sleep:
            st = PM_ReleaseConstraints(PLATFORM_SLEEP_STATE, PLATFORM_SLEEP_CONSTRAINTS);
            break;

        case PWR_DeepSleep:
            st = PM_ReleaseConstraints(PLATFORM_DEEP_SLEEP_STATE, PLATFORM_DEEP_SLEEP_CONSTRAINTS);
            break;

        case PWR_PowerDown:
            st = PM_ReleaseConstraints(PLATFORM_POWER_DOWN_STATE, PLATFORM_POWER_DOWN_CONSTRAINTS);
            break;

        case PWR_DeepPowerDown:
            st = PM_ReleaseConstraints(PLATFORM_DEEP_POWER_DOWN_STATE, PLATFORM_DEEP_POWER_DOWN_CONSTRAINTS);
            break;

        default:
            ret = PWR_ErrorNotSupported;
            break;
    }

    if (st != kStatus_Success)
    {
        ret = PWR_Error;
    }

    // PWR_DBG_LOG("-->mode=%d, deepest lp mode=%d", mode, PM_GetAllowedLowestPowerMode());

    return ret;
}

int32_t PWR_LowPowerEnterCritical(int32_t power_mode)
{
    // PWR_DBG_LOG("");
    return ((int32_t)PWR_SetLowPowerModeConstraint((PWR_LowpowerMode_t)power_mode));
}

int32_t PWR_LowPowerExitCritical(int32_t power_mode)
{
    // PWR_DBG_LOG("");
    return ((int32_t)PWR_ReleaseLowPowerModeConstraint((PWR_LowpowerMode_t)power_mode));
}

__WEAK uint32_t PWR_GetRadioNextEventUs(void)
{
    return 0u; // no next event
}
