#include "remote_transmitter.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"

// clang-tidy cannot parse the Realtek SDK headers pulled in via ArduinoPrivate.h
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)

// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout etc.) with the core's fixes for
// type-name collisions between the two (e.g. PinMode)
#include <ArduinoPrivate.h>
#include <FreeRTOS.h>
#include <task.h>

namespace esphome::remote_transmitter {

static const char *const TAG = "remote_transmitter";

// The carrier is generated by the PWM peripheral instead of bit-banging the pin: software carrier
// generation requires disabling interrupts for the whole frame, but this core's micros() is derived
// from the FreeRTOS tick and freezes while interrupts are off, so the timing loop never advances and
// the watchdog resets the chip. With hardware PWM, software only times the mark/space envelope and
// interrupts can stay enabled.
//
// The PWM is driven through the SDK's pwmout HAL directly rather than the Arduino wiring layer:
// changing the carrier frequency via the wiring requires a GPIO/PWM pin mode round-trip, which
// use-after-frees the core's per-pin state (pinRemoveMode() frees without nulling) and corrupts the
// heap. pwmout_period_us() changes the frequency with no mode transitions.

void RemoteTransmitterComponent::setup() {
  // Deliberately no pin_->setup(): registering the pin as GPIO claims it in the SDK's pin
  // management, and the pad is then never handed over to the PWM peripheral -- pwmout_init()
  // must own the pin from the start.
  PinInfo *info = pinInfo(this->pin_->get_pin());
  if (info == nullptr || !pinSupported(info, PIN_PWM)) {
    // checked here because the AmebaZ (RTL8710B) SDK does not report PWM init failure
    ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin());
    this->mark_failed();
    return;
  }
  auto *pwm = new pwmout_t();
  this->pwm_ = pwm;
  pwmout_init(pwm, static_cast<PinName>(info->gpio));
#if LT_RTL8720C
  // only the AmebaZ2 SDK's pwmout_s reports init success
  if (!pwm->is_init) {
    ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
    delete pwm;
    this->pwm_ = nullptr;
    this->mark_failed();
    return;
  }
#endif
  pwmout_period_us(pwm, 26);  // placeholder; the real carrier period is set per transmission
  pwmout_write(pwm, this->pin_->is_inverted() ? 1.0f : 0.0f);
}

void RemoteTransmitterComponent::dump_config() {
  ESP_LOGCONFIG(TAG,
                "Remote Transmitter:\n"
                "  Carrier Duty: %u%%",
                this->carrier_duty_percent_);
  LOG_PIN("  Pin: ", this->pin_);
}

void RemoteTransmitterComponent::await_target_time_() {
  const uint32_t current_time = micros();
  if (this->target_time_ == 0) {
    this->target_time_ = current_time;
  } else {
    while ((int32_t) (this->target_time_ - micros()) > 0) {
    }
  }
}

void RemoteTransmitterComponent::digital_write(bool value) {
  if (this->pwm_ == nullptr)
    return;
  pwmout_write(static_cast<pwmout_t *>(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f);
}

void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
  auto *pwm = static_cast<pwmout_t *>(this->pwm_);
  if (pwm == nullptr) {
    ESP_LOGW(TAG, "Cannot send: PWM not initialized");
    return;
  }
  ESP_LOGD(TAG, "Sending remote code");
  const uint32_t carrier_frequency = this->temp_.get_carrier_frequency();
  // unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
  float mark_duty =
      (carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f;
  float space_duty = 0.0f;
  if (this->pin_->is_inverted()) {
    mark_duty = 1.0f - mark_duty;
    space_duty = 1.0f;
  }
  if (carrier_frequency > 0 && carrier_frequency != this->current_carrier_frequency_) {
    // round(1000000/freq), clamped like the bit-bang path so a bad lambda can't hand the SDK a zero period
    const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
    pwmout_period_us(pwm, period);
    this->current_carrier_frequency_ = carrier_frequency;
  }
  this->transmit_trigger_.trigger();
  const UBaseType_t saved_priority = uxTaskPriorityGet(nullptr);
  for (uint32_t i = 0; i < send_times; i++) {
    // Boost task priority for the frame only, so WiFi/lwIP tasks can't preempt mid-frame and
    // merge adjacent marks. Interrupts stay enabled: micros() needs the FreeRTOS tick, and
    // ISR latency is within receiver tolerance.
    vTaskPrioritySet(nullptr, configMAX_PRIORITIES - 1);
    // Re-anchor every iteration: a late exit from the normal-priority gap wait must not
    // leave the schedule behind micros(), which would compress the next frame's leading items
    this->target_time_ = 0;
    for (int32_t item : this->temp_.get_data()) {
      const bool is_mark = item > 0;
      this->await_target_time_();
      pwmout_write(pwm, is_mark ? mark_duty : space_duty);
      this->target_time_ += is_mark ? uint32_t(item) : uint32_t(-item);
      App.feed_wdt();
    }
    this->await_target_time_();  // wait for duration of last pulse
    pwmout_write(pwm, space_duty);
    vTaskPrioritySet(nullptr, saved_priority);
    if (i + 1 < send_times) {
      // The repeat gap is user-configurable and unbounded, so wait it out at normal
      // priority, feeding the watchdog
      const uint32_t gap_end = micros() + send_wait;
      while ((int32_t) (gap_end - micros()) > 0) {
        App.feed_wdt();
      }
    }
  }
  this->complete_trigger_.trigger();
}

}  // namespace esphome::remote_transmitter

#endif  // USE_RTL87XX && !CLANG_TIDY
