/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "console/dbgserial.h"
#include "console/dbgserial_input.h"
#include "drivers/flash.h"
#include "drivers/periph_config.h"
#include "drivers/rtc.h"
#include "drivers/lptim_systick.h"
#include "drivers/task_watchdog.h"
#include "os/tick.h"
#include "kernel/util/stop.h"
#include "kernel/util/wfi.h"
#include "mcu/interrupts.h"
#include "services/common/analytics/analytics.h"
#include "system/passert.h"
#include "console/dbgserial_input.h"

#define STM32F2_COMPATIBLE
#define STM32F4_COMPATIBLE
#define STM32F7_COMPATIBLE
#define NRF5_COMPATIBLE
#define SF32LB52_COMPATIBLE
#include <mcu.h>

#include <stdbool.h>
#include <inttypes.h>

static int s_num_items_disallowing_stop_mode = 0;

#ifdef PBL_NOSLEEP
static bool s_sleep_mode_allowed = false;
#else
static bool s_sleep_mode_allowed = true;
#endif

typedef struct {
  uint32_t active_count;
  RtcTicks ticks_when_stop_mode_disabled;
  RtcTicks total_ticks_while_disabled;
} InhibitorTickProfile;

// Note: These variables should be protected within a critical section since
// they are read and modified by multiple threads
static InhibitorTickProfile s_inhibitor_profile[InhibitorNumItems];

#if MICRO_FAMILY_NRF5
void enter_stop_mode(void) {
  dbgserial_enable_rx_exti();
  dbgserial_disable_rx_dma_before_stop();

  flash_power_down_for_stop_mode();
  rtc_systick_pause();

  /* XXX(nrf5): LATER: have MPSL turn off HFCLK */

  __DSB(); // Drain any pending memory writes before entering sleep.
  do_wfi(); // Wait for Interrupt (enter sleep mode). Work around F2/F4 errata.
  __ISB(); // Let the pipeline catch up (force the WFI to activate before moving on).

  rtc_systick_resume();
  flash_power_up_after_stop_mode();

  dbgserial_enable_rx_dma_after_stop();
}
#elif MICRO_FAMILY_SF32LB52
static uint32_t s_iser_bak[16];
static void clear_interrupt_setting(void)
{
    uint32_t i;
    for (i = 0; i < 16; i++)
    {
        s_iser_bak[i] = NVIC->ISER[i];
        NVIC->ICER[i] = 0xFFFFFFFF;
        __DSB();
        __ISB();
    }
}

static void restore_interrupt_setting(void)
{
    uint32_t i;
    for (i = 0; i < 16; i++)
    {
        __COMPILER_BARRIER();
        NVIC->ISER[i] = s_iser_bak[i];
        __COMPILER_BARRIER();
    }
}

// Refer from sdk sifli_deep_handler function
void enter_stop_mode(void) {
  uint32_t dll1_freq;
  uint32_t dll2_freq;
  int clk_src;

  clear_interrupt_setting();

  /* Wait flash cache idle */
  HAL_Delay_us(100);

  NVIC_EnableIRQ(AON_IRQn);
  NVIC_EnableIRQ(LPTIM1_IRQn);

  // Switch system clock to HRC48, backup current clock source
  clk_src = HAL_RCC_HCPU_GetClockSrc(RCC_CLK_MOD_SYS);
  HAL_RCC_HCPU_ClockSelect(RCC_CLK_MOD_SYS, RCC_SYSCLK_HRC48);
  dll1_freq = HAL_RCC_HCPU_GetDLL1Freq();
  dll2_freq = HAL_RCC_HCPU_GetDLL2Freq();
  
  HAL_RCC_HCPU_DisableDLL1();
  HAL_RCC_HCPU_DisableDLL2();

  HAL_HPAON_DISABLE_PAD();
  HAL_HPAON_DISABLE_VHP();
  
  HAL_HPAON_CLEAR_HP_ACTIVE();
  HAL_HPAON_SET_POWER_MODE(AON_PMR_DEEP_SLEEP);

  __WFI();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  __NOP();
  
  HAL_HPAON_ENABLE_PAD();
  HAL_HPAON_ENABLE_VHP();

  HAL_HPAON_SET_HP_ACTIVE();
  HAL_HPAON_CLEAR_POWER_MODE();

  // Wait for HXT48 to be ready
  if (dll1_freq != 0) {
    while (0 == (hwp_hpsys_aon->ACR & HPSYS_AON_ACR_HXT48_RDY)) {
      __NOP();
    }
  }

  // Switch back to original clock source
  HAL_RCC_HCPU_EnableDLL1(dll1_freq);
  HAL_RCC_HCPU_ClockSelect(RCC_CLK_MOD_SYS, clk_src);
  HAL_RCC_HCPU_EnableDLL2(dll2_freq);
  HAL_Delay_us(0);

  restore_interrupt_setting();
}
#else /* STM32 */
void enter_stop_mode(void) {
  // enable the interrupt on the debug RX line so that we can use the serial
  // console even when we are in stop mode.
  dbgserial_enable_rx_exti();

  flash_power_down_for_stop_mode();

  // Turn on the power control peripheral so that we can put the regulator into low-power mode
  periph_config_enable(PWR, RCC_APB1Periph_PWR);

  if (mcu_state_are_interrupts_enabled()) {
    // If INTs aren't disabled here, we would wind up servicing INTs
    // immediately after the WFI (while running at the wrong clock speed) which
    // can confuse peripherals in subtle ways
    WTF;
  }

  // Enter stop mode.
  //PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
  // We don't use ^^ the above function because of a silicon bug which
  // causes the processor to skip some instructions upon wake from STOP
  // in certain sitations. See the STM32F20x and STM32F21x Errata sheet
  // section 2.1.3 "Debugging Stop mode with WFE entry", or the erratum
  // of the same name in section 2.1.2 of the STM32F42x and STM32F43x
  // Errata sheet, for (misleading) details.
  // http://www.st.com/web/en/resource/technical/document/errata_sheet/DM00027213.pdf
  // http://www.st.com/web/en/resource/technical/document/errata_sheet/DM00068628.pdf

  // Configure the PWR peripheral to put us in low-power STOP mode when
  // the processor enters deepsleep.
#if defined(MICRO_FAMILY_STM32F7)
  uint32_t temp = PWR->CR1;
  temp &= ~PWR_CR1_PDDS;
  temp |= PWR_CR1_LPDS;
  PWR->CR1 = temp;
#else
  uint32_t temp = PWR->CR;
  temp &= ~PWR_CR_PDDS;
  temp |= PWR_CR_LPDS;
#if STM32F412xG
  // STM32F412xG suports a new "low-power regulator low voltage in deep sleep" mode.
  temp |= PWR_CR_LPLVDS;
#endif
  PWR->CR = temp;
#endif

  // Configure the processor core to enter deepsleep mode when we
  // execute a WFI or WFE instruction.
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;

  // Go stop now.
  __DSB(); // Drain any pending memory writes before entering sleep.
  do_wfi(); // Wait for Interrupt (enter sleep mode). Work around F2/F4 errata.
  __ISB(); // Let the pipeline catch up (force the WFI to activate before moving on).

  // Tell the processor not to emter deepsleep mode for future WFIs.
  SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;

  // Stop mode will change our system clock to the HSI. Move it back to the PLL.

  // Enable the PLL and wait until it's ready
  RCC_PLLCmd(ENABLE);
  while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {}

  // Select PLL as system clock source and wait until it's being used
  RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
  while (RCC_GetSYSCLKSource() != 0x08) {}

  // No longer need the power control peripheral
  periph_config_disable(PWR, RCC_APB1Periph_PWR);

  flash_power_up_after_stop_mode();
}
#endif

void stop_mode_disable( StopModeInhibitor inhibitor ) {
  portENTER_CRITICAL();
  ++s_num_items_disallowing_stop_mode;

  ++s_inhibitor_profile[inhibitor].active_count;
  // TODO: We should probably check if s_inhibitor_profile.active_count == 1
  // before doing this assignment. We don't seem to ever run into this case
  // yet (i.e. active_count is never > 1), but when we do, this code would
  // report the wrong number of nostop ticks.
  s_inhibitor_profile[inhibitor].ticks_when_stop_mode_disabled = rtc_get_ticks();
  portEXIT_CRITICAL();
}

void stop_mode_enable( StopModeInhibitor inhibitor ) {
  portENTER_CRITICAL();
  PBL_ASSERTN(s_num_items_disallowing_stop_mode != 0);
  PBL_ASSERTN(s_inhibitor_profile[inhibitor].active_count != 0);

  --s_num_items_disallowing_stop_mode;
  --s_inhibitor_profile[inhibitor].active_count;
  if (s_inhibitor_profile[inhibitor].active_count == 0) {
    s_inhibitor_profile[inhibitor].total_ticks_while_disabled += rtc_get_ticks() -
        s_inhibitor_profile[inhibitor].ticks_when_stop_mode_disabled;
  }
  portEXIT_CRITICAL();
}

bool stop_mode_is_allowed(void) {
#if PBL_NOSTOP
  return false;
#else
  return s_num_items_disallowing_stop_mode == 0;
#endif
}

void sleep_mode_enable(bool enable) {
  s_sleep_mode_allowed = enable;
}

bool sleep_mode_is_allowed(void) {
#ifdef PBL_NOSLEEP
  return false;
#endif
  return s_sleep_mode_allowed;
}

static RtcTicks prv_get_nostop_ticks(StopModeInhibitor inhibitor, RtcTicks now_ticks) {
    RtcTicks total_ticks = s_inhibitor_profile[inhibitor].total_ticks_while_disabled;
    if (s_inhibitor_profile[inhibitor].active_count != 0) {
        total_ticks += (now_ticks - s_inhibitor_profile[inhibitor].ticks_when_stop_mode_disabled);
    }
    return total_ticks;
}

static void prv_collect(AnalyticsMetric metric, StopModeInhibitor inhibitor, RtcTicks now_ticks) {
  // operating on 64 bit values so the load/stores will _not_ be atomic
  portENTER_CRITICAL();
  RtcTicks ticks = prv_get_nostop_ticks(inhibitor, now_ticks);
  s_inhibitor_profile[inhibitor].total_ticks_while_disabled = 0;
  portEXIT_CRITICAL();
  analytics_set(metric, ticks_to_milliseconds(ticks), AnalyticsClient_System);
}

void analytics_external_collect_stop_inhibitor_stats(RtcTicks now_ticks) {
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_MAIN_TIME, InhibitorMain, now_ticks);
  // We don't care about the serial console nostop time, it should always
  // be zero on watches in the field anyway. (InhibitorDbgSerial skipped)
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BUTTON_TIME, InhibitorButton, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BLUETOOTH_TIME, InhibitorBluetooth, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_DISPLAY_TIME, InhibitorDisplay, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BACKLIGHT_TIME, InhibitorBacklight, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_COMM_TIME, InhibitorCommMode, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_FLASH_TIME, InhibitorFlash, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_I2C1_TIME, InhibitorI2C1, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_ACCESSORY, InhibitorAccessory, now_ticks);
  prv_collect(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_MIC, InhibitorMic, now_ticks);
  // TODO PBL-37941: Add analytics for InhibitorDMA
}

void command_scheduler_force_active(void) {
  sleep_mode_enable(false);
}

void command_scheduler_resume_normal(void) {
  sleep_mode_enable(true);
}
