#include "Pwm.h"
#include "Config.h"
#include "Core.h"
#if CONFIG_IDF_TARGET_ESP32C3
#include <hal/ledc_ll.h>
#elif CONFIG_IDF_TARGET_ESP32S3
#include <driver/mcpwm_prelude.h>
#endif

namespace {
#if CONFIG_IDF_TARGET_ESP32C3
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0;

void setDivider(uint32_t dividerRaw) {
  ledc_dev_t *hardware = LEDC_LL_GET_HW();
  ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER);
  ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER,
    dividerRaw);
  ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER);
  ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER);
  ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER);
}

bool dividerIsSet(uint32_t expectedRaw) {
  uint32_t rawDivider = 0;
  ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider);
  return rawDivider == expectedRaw;
}
#elif CONFIG_IDF_TARGET_ESP32S3
mcpwm_timer_handle_t mcpwmTimer = nullptr;
mcpwm_oper_handle_t mcpwmOperator = nullptr;
mcpwm_cmpr_handle_t mcpwmComparator = nullptr;
mcpwm_gen_handle_t mcpwmGenerator = nullptr;
uint32_t mcpwmFrequencyHz = 0;

void releaseMcpwm() {
  if (mcpwmGenerator) {
    mcpwm_del_generator(mcpwmGenerator);
    mcpwmGenerator = nullptr;
  }
  if (mcpwmComparator) {
    mcpwm_del_comparator(mcpwmComparator);
    mcpwmComparator = nullptr;
  }
  if (mcpwmOperator) {
    mcpwm_del_operator(mcpwmOperator);
    mcpwmOperator = nullptr;
  }
  if (mcpwmTimer) {
    mcpwm_timer_disable(mcpwmTimer);
    mcpwm_del_timer(mcpwmTimer);
    mcpwmTimer = nullptr;
  }
}

uint8_t periodResolutionBits(uint32_t periodTicks) {
  uint8_t bits = 0;
  while (periodTicks > 1U) {
    periodTicks >>= 1U;
    ++bits;
  }
  return bits ? bits : 1U;
}
#endif
}

void PwmGenerator::begin() {
#if CONFIG_IDF_TARGET_ESP32C3
  // Match LEDC_SOURCE_CLOCK_HZ and make the timer calculation deterministic.
  ledcSetClockSource(LEDC_USE_XTAL_CLK);
  pinMode(GPIO_PWM, OUTPUT);
  stop();
#elif CONFIG_IDF_TARGET_ESP32S3
  mcpwm_timer_config_t timerConfig = {};
  timerConfig.group_id = 0;
  timerConfig.clk_src = MCPWM_TIMER_CLK_SRC_PLL160M;
  timerConfig.resolution_hz = MCPWM_RESOLUTION_HZ;
  timerConfig.count_mode = MCPWM_TIMER_COUNT_MODE_UP;
  timerConfig.period_ticks = MCPWM_RESOLUTION_HZ / 1000U;

  mcpwm_operator_config_t operatorConfig = {};
  operatorConfig.group_id = 0;
  mcpwm_comparator_config_t comparatorConfig = {};
  mcpwm_generator_config_t generatorConfig = {};
  generatorConfig.gen_gpio_num = GPIO_PWM;

  bool ok = mcpwm_new_timer(&timerConfig, &mcpwmTimer) == ESP_OK;
  ok = ok && mcpwm_new_operator(&operatorConfig, &mcpwmOperator) == ESP_OK;
  ok = ok && mcpwm_operator_connect_timer(mcpwmOperator, mcpwmTimer) == ESP_OK;
  ok = ok && mcpwm_new_comparator(mcpwmOperator, &comparatorConfig, &mcpwmComparator) == ESP_OK;
  ok = ok && mcpwm_new_generator(mcpwmOperator, &generatorConfig, &mcpwmGenerator) == ESP_OK;
  ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator,
    timerConfig.period_ticks / 2U) == ESP_OK;
  ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
    MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
      MCPWM_TIMER_EVENT_EMPTY, TX_LIGHT_ON_GPIO_LEVEL == HIGH ?
        MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
  ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
    MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
      mcpwmComparator, TX_LIGHT_OFF_GPIO_LEVEL == HIGH ?
        MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
  ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
  if (!ok) {
    releaseMcpwm();
    pinMode(GPIO_PWM, OUTPUT);
    digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
    return;
  }
  mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
#endif
}

bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
  const uint8_t activeLevel = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
    TX_LIGHT_OFF_GPIO_LEVEL;
  const uint8_t inactiveLevel = activeLevel == HIGH ? LOW : HIGH;
#if CONFIG_IDF_TARGET_ESP32C3
  IntegerPwmConfig config = {};
  if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
  const uint8_t bits = config.bits;
  const uint32_t levels = 1UL << bits;
  const uint32_t duty = config.dutyCount;
  for (uint8_t attempt = 0; attempt < 2; ++attempt) {
    stop();
    const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL);
    if (attached) {
      // Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when
      // the configured active pulse level is LOW.
      if (!ledcOutputInvert(GPIO_PWM, activeLevel == LOW)) {
        ledcDetach(GPIO_PWM);
        delay(2);
        continue;
      }
      // Apply the selected Q10.8 divider explicitly. This lets pulse width,
      // rather than duty percentage, drive the hardware quantization.
      setDivider(config.dividerRaw);
    }
    if (attached && dividerIsSet(config.dividerRaw) && ledcWriteChannel(LEDC_CHANNEL, duty)) {
      // On the first configuration after power-up the duty update is latched
      // on a timer edge. Reading immediately can therefore return zero.
      uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz);
      if (settleUs > 2000U) settleUs = 2000U;
      delayMicroseconds(settleUs);
      const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
      if (actualHz == config.actualHz) {
        a = {hz, actualHz, pulseNs, config.actualPulseNs,
          100.0f * duty / levels, bits};
        running_ = true;
        return true;
      }
    }
    if (attached) ledcDetach(GPIO_PWM);
    delay(2);
  }
  pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
  return false;
#elif CONFIG_IDF_TARGET_ESP32S3
  if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs ||
      MCPWM_RESOLUTION_HZ % hz) return false;
  const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
  if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
  uint32_t activeTicks = static_cast<uint32_t>(
    (static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) /
    1000000000ULL);
  if (activeTicks == 0U) activeTicks = 1U;
  if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U;

  stop();
  bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK;
  ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK;
  ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
    MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
      MCPWM_TIMER_EVENT_EMPTY, activeLevel == HIGH ?
        MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
  ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
    MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
      mcpwmComparator, inactiveLevel == HIGH ?
        MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
  // stop() applies a continuous force level (hold_on=true). Remove that same
  // continuous-force action; hold_on=false addresses a different, one-shot
  // force mechanism and would leave the safe level permanently active.
  ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, true) == ESP_OK;
  ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK;
  if (!ok) {
    mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
    return false;
  }

  const uint32_t actualPulseNs = static_cast<uint32_t>(
    (static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
    MCPWM_RESOLUTION_HZ);
  a = {hz, hz, pulseNs, actualPulseNs, 100.0f * activeTicks / periodTicks,
    periodResolutionBits(periodTicks)};
  mcpwmFrequencyHz = hz;
  running_ = true;
  return true;
#endif
}

void PwmGenerator::stop() {
#if CONFIG_IDF_TARGET_ESP32C3
  if (running_) ledcDetach(GPIO_PWM);
  pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3
  if (mcpwmGenerator) mcpwm_generator_set_force_level(
    mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
  if (running_ && mcpwmTimer) {
    mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY);
    const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U;
    delayMicroseconds(waitUs);
  }
  mcpwmFrequencyHz = 0;
#endif
  running_ = false;
}

void PwmGenerator::active() {
  // First detach/stop the PWM peripheral, then apply the same active level
  // that denotes the pulse during a running test.
  stop();
#if CONFIG_IDF_TARGET_ESP32C3
  digitalWrite(GPIO_PWM, activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
    TX_LIGHT_OFF_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3
  const uint8_t level = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
    TX_LIGHT_OFF_GPIO_LEVEL;
  if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, level, true);
  else {
    pinMode(GPIO_PWM, OUTPUT);
    digitalWrite(GPIO_PWM, level);
  }
#endif
}

void PwmGenerator::lightOn() {
  stop();
#if CONFIG_IDF_TARGET_ESP32C3
  digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3
  if (mcpwmGenerator)
    mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_ON_GPIO_LEVEL, true);
  else {
    pinMode(GPIO_PWM, OUTPUT);
    digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
  }
#endif
}
