#include "DriverTest.h"

#include "Config.h"
#include "Log.h"

#include <driver/gpio.h>
#include <esp_cpu.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <esp32-hal-cpu.h>
#include <soc/gpio_struct.h>
#include <string.h>

static inline uint32_t IRAM_ATTR maskAllInterrupts() {
  uint32_t state;
  asm volatile("rsil %0, 15" : "=a"(state) :: "memory");
  return state;
}

static inline void IRAM_ATTR restoreInterrupts(uint32_t state) {
  asm volatile("wsr %0, ps\nrsync" :: "a"(state) : "memory");
}

void DriverEdgeStats::reset() {
  memset(this, 0, sizeof(*this));
  minDelayTicks = minResponseTicks = UINT32_MAX;
}

void DriverStats::reset() {
  memset(this, 0, sizeof(*this));
  minDelayTicks = minResponseTicks = UINT32_MAX;
  turnOn.reset();
  turnOff.reset();
  reason = FailReason::NONE;
}

uint64_t DriverTest::nsToTicks(uint32_t ns) const {
  return (static_cast<uint64_t>(ns) * captureHz_ + 999999999ULL) /
    1000000000ULL;
}

uint64_t DriverTest::ticksToNs(uint64_t ticks) const {
  return (ticks * 1000000000ULL + captureHz_ / 2U) / captureHz_;
}

bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
                       float tolerancePct, uint32_t testTimeMs,
                       uint8_t settleCycles, bool activeTxLightOn,
                       bool activeRxLightOn) {
  (void)tolerancePct;
  (void)settleCycles;
  if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
      !testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false;

  requestCaptureStop();
  if (!waitCaptureStopped(25U)) return false;
  if (!pollTask_ && xTaskCreatePinnedToCore(pollTaskEntry, "driver-poll",
        3072, this, configMAX_PRIORITIES - 1U, &pollTask_, 0) != pdPASS)
    return false;
  if (!analyzerTask_ && xTaskCreatePinnedToCore(analyzerTaskEntry,
        "driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
    return false;

  // The ACK edges can be less than 1 us apart. MCPWM capture delivers all
  // channels through one group ISR and can overwrite an earlier channel
  // timestamp before that ISR reaches it. During DRIVER test dedicate core 0
  // to direct GPIO sampling; PWM itself remains fully hardware-generated.
  captureHz_ = getCpuFrequencyMhz() * 1000000UL;
  if (!captureHz_ || captureHz_ % frequencyHz) return false;
  captureFrequencyHz_ = frequencyHz;
  capturePulseNs_ = pulseNs;
  captureTxLightOn_ = activeTxLightOn;
  txPulseLightOn_ = activeTxLightOn;
  pollPeriodCycles_ = captureHz_ / frequencyHz;
  pollWindowBeforeCycles_ = captureHz_ / 200000U;  // 5 us
  const uint64_t periodNs = 1000000000ULL / frequencyHz;
  uint64_t windowNs = pulseNs + 50000ULL;
  const uint64_t maximumWindowNs = periodNs * 3ULL / 4ULL;
  if (windowNs > maximumWindowNs) windowNs = maximumWindowNs;
  pollWindowAfterCycles_ = static_cast<uint32_t>(
    windowNs * captureHz_ / 1000000000ULL);
  const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL :
    TX_LIGHT_OFF_GPIO_LEVEL;
  pollTxStartRawHigh_ = activeTxRaw == HIGH;
  rxActiveRawHigh_ =
    ((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn);

  ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
  faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
  stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
  testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
  const uint32_t requestedSubsamples = testTimeMs < 1000U ?
    DRIVER_SHORT_SAMPLE_PROGRESS_STEPS :
    (testTimeMs + DRIVER_PROGRESS_INTERVAL_MS - 1U) /
      DRIVER_PROGRESS_INTERVAL_MS;
  subsampleCount_ = static_cast<uint8_t>(
    requestedSubsamples > UINT8_MAX ? UINT8_MAX : requestedSubsamples);
  subsampleTicks_ = testTicks_ / subsampleCount_;
  if (!pollPeriodCycles_ || !pollWindowAfterCycles_ ||
      !ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ ||
      !testTicks_ || !subsampleTicks_)
    return false;

  clearCapture();
  stats_.reset();
  publishStats();
  pendingCount_ = 0;
  response_ = {};
  measurementStartTick_ = deadlineTick_ = 0;
  pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0;
  // Skip two complete periods after the polling task synchronizes with TX.
  settleCycles_ = DRIVER_CAPTURE_SYNC_CYCLES;
  Log::printf("DRIVER", "capture=GPIO-%luMHz sync-periods=%u ACK-timeout=%luns",
    static_cast<unsigned long>(captureHz_ / 1000000UL),
    static_cast<unsigned>(settleCycles_),
    static_cast<unsigned long>(DRIVER_ACK_START_MAX_NS));
  const uint64_t periodUs =
    (1000000ULL + frequencyHz - 1ULL) / frequencyHz;
  settlingTimeoutUs_ = periodUs *
    (static_cast<uint64_t>(DRIVER_CAPTURE_SYNC_CYCLES) + 2ULL) + 1000ULL;
  settlingDeadlineUs_ = 0;
  settledCycles_ = 0;
  completedSubsamples_ = 0;
  measurementClosed_ = false;
  havePointOrigin_ = false;
  haveLastActiveTx_ = false;
  rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
    rxActiveRawHigh_;
  currentStep_ = 0;
  traceWrite_ = traceCount_ = 0;
  __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
  state_ = DriverState::SETTLING;
  return armCapture();
}

bool DriverTest::armCapture() {
  // Never wait for USB/Serial here: a disconnected or slow host must not
  // delay a subsample or consume the test's global timeout.
  if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) {
    TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0);
    const bool watched = idle0 && esp_task_wdt_status(idle0) == ESP_OK;
    const bool disabled = watched && disableCore0WDT();
    __atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE);
    // If IDLE0 is not watched there is nothing to remove or restore.
  }
  __atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
  __atomic_store_n(&captureActive_, true, __ATOMIC_RELEASE);
  xTaskNotifyGive(pollTask_);
  const uint32_t readyDeadline = millis() + 25U;
  while (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
         static_cast<int32_t>(millis() - readyDeadline) < 0) delay(0);
  if (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) {
    requestCaptureStop();
    waitCaptureStopped(25U);
    state_ = DriverState::IDLE;
    return false;
  }
  settlingDeadlineUs_ = static_cast<uint64_t>(esp_timer_get_time()) +
    settlingTimeoutUs_;
  xTaskNotifyGive(analyzerTask_);
  return true;
}

bool DriverTest::resumeSubsample() {
  if (state_ != DriverState::SUBSAMPLE_DONE) return false;
  if (!waitCaptureStopped(25U)) {
    fail(FailReason::DATA_LOSS, lastEventTick_);
    return false;
  }

  clearCapture();
  pendingCount_ = 0;
  response_ = {};
  measurementStartTick_ = deadlineTick_ = 0;
  lastActiveTxTick_ = 0;
  settlingDeadlineUs_ = 0;
  settledCycles_ = 0;
  measurementClosed_ = false;
  haveLastActiveTx_ = false;
  rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
    rxActiveRawHigh_;
  state_ = DriverState::SETTLING;
  if (armCapture()) return true;
  fail(FailReason::DATA_LOSS, lastEventTick_);
  return false;
}

void DriverTest::pollTaskEntry(void *context) {
  static_cast<DriverTest *>(context)->pollTaskLoop();
}

void IRAM_ATTR DriverTest::pollTaskLoop() {
  constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    uint32_t levels = GPIO.in & PIN_MASK;
    uint32_t nextStart = 0;
    uint32_t windowEnd = 0;
    uint32_t lastTxStart = 0;
    RawEvent hotEvents[32] = {};
    uint8_t hotCount = 0;
    bool sawTxStart = false;
    bool critical = false;
    bool allInterruptsMasked = false;
    uint32_t interruptState = 0;

    auto sampleOnce = [&]() {
      const uint32_t current = GPIO.in & PIN_MASK;
      if (current == levels) return;
      const uint32_t now = esp_cpu_get_cycle_count();
      const uint32_t changed = current ^ levels;
      if ((changed & (1UL << GPIO_PWM)) && hotCount < 32U)
        hotEvents[hotCount++] = {now,
          (current & (1UL << GPIO_PWM)) != 0U, Source::TX};
      if ((changed & (1UL << GPIO_RX)) && hotCount < 32U)
        hotEvents[hotCount++] = {now,
          (current & (1UL << GPIO_RX)) != 0U, Source::RX};
      if ((changed & (1UL << GPIO_PWM)) &&
          ((current & (1UL << GPIO_PWM)) != 0U) == pollTxStartRawHigh_) {
        lastTxStart = now;
        sawTxStart = true;
      }
      levels = current;
    };

    auto flushHot = [&]() {
      for (uint8_t i = 0; i < hotCount; ++i)
        recordRaw(hotEvents[i].tick, hotEvents[i].rising,
                  hotEvents[i].source);
      hotCount = 0;
    };

    portENTER_CRITICAL(&pollMux_);
    critical = true;
    __atomic_store_n(&captureReady_, true, __ATOMIC_RELEASE);
    while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && !sawTxStart)
      for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
    if (sawTxStart) windowEnd = lastTxStart + pollWindowAfterCycles_;
    const bool synchronized = sawTxStart;
    if (synchronized) {
      interruptState = maskAllInterrupts();
      allInterruptsMasked = true;
    }

    while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && synchronized) {
      while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
             static_cast<int32_t>(esp_cpu_get_cycle_count() - windowEnd) < 0)
        for (uint8_t i = 0; i < 16U; ++i) sampleOnce();

      restoreInterrupts(interruptState);
      allInterruptsMasked = false;
      portEXIT_CRITICAL(&pollMux_);
      critical = false;
      flushHot();
      // This marker is written only after every TX/RX edge from the completed
      // sampling window. The analyzer may now safely decide that an ACK was
      // absent without racing the producer that writes those edges.
      recordRaw(esp_cpu_get_cycle_count(), false, Source::WINDOW_END);
      if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
      nextStart = lastTxStart + pollPeriodCycles_;
      sawTxStart = false;

      uint32_t outsideSpins = 0;
      while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
             static_cast<int32_t>(esp_cpu_get_cycle_count() -
               (nextStart - pollWindowBeforeCycles_)) < 0) {
        for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
        if (++outsideSpins >= 256U) {
          outsideSpins = 0;
          taskYIELD();
        }
      }
      if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;

      portENTER_CRITICAL(&pollMux_);
      critical = true;
      interruptState = maskAllInterrupts();
      allInterruptsMasked = true;
      windowEnd = nextStart + pollWindowAfterCycles_;
    }
    if (allInterruptsMasked) restoreInterrupts(interruptState);
    if (critical) portEXIT_CRITICAL(&pollMux_);
    flushHot();
    if (__atomic_exchange_n(&core0WdtDisabled_, false,
                            __ATOMIC_ACQ_REL)) enableCore0WDT();
    __atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
  }
}

void DriverTest::analyzerTaskEntry(void *context) {
  static_cast<DriverTest *>(context)->analyzerTaskLoop();
}

void DriverTest::analyzerTaskLoop() {
  TimedEvent events[64] = {};
  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    while (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING) {
      const size_t count = readRaw(events, 64, pdMS_TO_TICKS(1));
      for (size_t i = 0; i < count &&
           (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING);
           ++i) processEvent(events[i]);
      const uint32_t dropped = takeDropped();
      if (dropped) {
        stats_.droppedItems += dropped;
        fail(FailReason::DATA_LOSS, lastEventTick_);
      }
      if (!count && state_ == DriverState::SETTLING &&
                 static_cast<uint64_t>(esp_timer_get_time()) >=
                   settlingDeadlineUs_) {
        fail(FailReason::ACK_MISSING, lastEventTick_);
      }
    }
  }
}

void DriverTest::processEvent(const TimedEvent &event) {
  lastEventTick_ = event.tick;
  if (event.source == Source::WINDOW_END) {
    if (state_ == DriverState::RUNNING) {
      expirePending(event.tick);
      if (state_ == DriverState::RUNNING) completeIfPossible(event.tick);
    }
    return;
  }
  if (!havePointOrigin_) {
    pointOriginTick_ = event.tick;
    havePointOrigin_ = true;
  }
  rememberTrace(event);
  if (state_ == DriverState::SETTLING) processSettling(event);
  else if (state_ == DriverState::RUNNING) processRunning(event);
}

void DriverTest::processSettling(const TimedEvent &event) {
  if (event.source == Source::RX) {
    rxActive_ = event.rising == rxActiveRawHigh_;
    return;
  }
  const uint8_t rawLevel = event.rising ? HIGH : LOW;
  const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
  if (lightOn != txPulseLightOn_) return;
  if (settledCycles_ < settleCycles_) {
    ++settledCycles_;
    return;
  }
  if (rxActive_) {
    fail(FailReason::DRIVER_FAULT, event.tick);
    return;
  }
  state_ = DriverState::RUNNING;
  measurementStartTick_ = event.tick;
  const uint64_t measuredBefore =
    static_cast<uint64_t>(completedSubsamples_) * subsampleTicks_;
  const uint64_t thisSubsampleTicks =
    completedSubsamples_ + 1U == subsampleCount_ ?
      testTicks_ - measuredBefore : subsampleTicks_;
  deadlineTick_ = event.tick + thisSubsampleTicks;
  processTx(event, lightOn);
}

void DriverTest::processRunning(const TimedEvent &event) {
  if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
    const uint64_t delay = response_.associated ?
      response_.startTick - response_.tx.tick : 0;
    const uint64_t trigger = haveLastActiveTx_ &&
      response_.startTick >= lastActiveTxTick_ ?
      response_.startTick - lastActiveTxTick_ : delay;
    fail(FailReason::DRIVER_FAULT, event.tick, delay,
      event.tick - response_.startTick, trigger);
    return;
  }
  if (event.source == Source::TX) {
    expirePending(event.tick);
    if (state_ != DriverState::RUNNING) return;
    const uint8_t rawLevel = event.rising ? HIGH : LOW;
    const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
    if (event.tick < deadlineTick_) processTx(event, lightOn);
    else measurementClosed_ = true;
  } else {
    // A delayed fault indication can start after the normal ACK deadline.
    // Measure the RX pulse before expiring its possible causal TX edge.
    processRx(event, event.rising == rxActiveRawHigh_);
    if (state_ != DriverState::RUNNING) return;
    if (!response_.active) expirePending(event.tick);
  }
  if (state_ != DriverState::RUNNING) return;
  completeIfPossible(event.tick);
}

bool DriverTest::addPending(uint64_t tick, bool lightOn) {
  if (pendingCount_ >= MAX_PENDING) {
    fail(FailReason::DATA_LOSS, tick);
    return false;
  }
  pending_[pendingCount_++] = {tick, lightOn};
  return true;
}

void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
  if (!addPending(event.tick, lightOn)) return;
  if (lightOn == txPulseLightOn_) {
    lastActiveTxTick_ = event.tick;
    haveLastActiveTx_ = true;
  }
  ++stats_.inputEdges;
}

int8_t DriverTest::matchingPending(uint64_t rxTick) const {
  for (uint8_t i = 0; i < pendingCount_; ++i)
    if (rxTick >= pending_[i].tick &&
        rxTick - pending_[i].tick <= ackStartMaxTicks_)
      return static_cast<int8_t>(i);
  return -1;
}

void DriverTest::removePending(uint8_t index) {
  if (index >= pendingCount_) return;
  for (uint8_t i = index + 1U; i < pendingCount_; ++i)
    pending_[i - 1U] = pending_[i];
  --pendingCount_;
}

void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
  rxActive_ = activeNow;
  if (activeNow) {
    if (response_.active) {
      fail(FailReason::DATA_LOSS, event.tick);
      return;
    }
    response_ = {};
    response_.active = true;
    response_.startTick = event.tick;
    const int8_t index = matchingPending(event.tick);
    if (index >= 0) {
      response_.associated = true;
      response_.tx = pending_[index];
      removePending(static_cast<uint8_t>(index));
    } else ++stats_.unexpectedResponses;
    return;
  }

  if (!response_.active) return;
  const uint64_t width = event.tick - response_.startTick;
  if (!response_.associated) {
    const uint64_t trigger = haveLastActiveTx_ &&
      response_.startTick >= lastActiveTxTick_ ?
      response_.startTick - lastActiveTxTick_ : 0;
    response_ = {};
    fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger);
    return;
  }

  const uint64_t guard = mergeGuardTicks();
  for (uint8_t i = 0; i < pendingCount_; ++i) {
    if (pending_[i].tick > response_.startTick &&
        event.tick - pending_[i].tick >= guard) {
      const uint64_t delay = response_.startTick - response_.tx.tick;
      const uint64_t trigger = event.tick - pending_[i].tick;
      response_ = {};
      fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger);
      return;
    }
  }
  if (width >= faultLongTicks_) {
    const uint64_t delay = response_.startTick - response_.tx.tick;
    const uint64_t trigger = haveLastActiveTx_ &&
      response_.startTick >= lastActiveTxTick_ ?
      response_.startTick - lastActiveTxTick_ : delay;
    response_ = {};
    fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger);
    return;
  }

  const uint64_t delay = response_.startTick - response_.tx.tick;
  const bool lightOn = response_.tx.lightOn;
  response_ = {};
  acceptAcknowledgement(delay, width, lightOn);
}

uint64_t DriverTest::mergeGuardTicks() const {
  uint32_t observedMax = stats_.maxDelayTicks;
  const uint64_t baseline = observedMax ? observedMax :
    nsToTicks(DRIVER_ACK_DELAY_NS + 500U);
  return baseline + nsToTicks(DRIVER_ACK_MERGE_MARGIN_NS);
}

void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
                                       bool lightOn) {
  const uint32_t delay32 = delay > UINT32_MAX ? UINT32_MAX :
    static_cast<uint32_t>(delay);
  const uint32_t width32 = width > UINT32_MAX ? UINT32_MAX :
    static_cast<uint32_t>(width);
  stats_.lastDelayTicks = delay32;
  stats_.lastResponseTicks = width32;
  ++stats_.responses;
  if (delay32 < stats_.minDelayTicks) stats_.minDelayTicks = delay32;
  if (delay32 > stats_.maxDelayTicks) stats_.maxDelayTicks = delay32;
  if (width32 < stats_.minResponseTicks) stats_.minResponseTicks = width32;
  if (width32 > stats_.maxResponseTicks) stats_.maxResponseTicks = width32;

  DriverEdgeStats &edge = lightOn ? stats_.turnOn : stats_.turnOff;
  ++edge.responses;
  edge.delaySumTicks += delay32;
  edge.responseSumTicks += width32;
  if (delay32 < edge.minDelayTicks) edge.minDelayTicks = delay32;
  if (delay32 > edge.maxDelayTicks) edge.maxDelayTicks = delay32;
  if (width32 < edge.minResponseTicks) edge.minResponseTicks = width32;
  if (width32 > edge.maxResponseTicks) edge.maxResponseTicks = width32;
  publishStats();
}

void DriverTest::expirePending(uint64_t now) {
  for (uint8_t i = 0; i < pendingCount_; ++i) {
    if (now < pending_[i].tick + ackStartMaxTicks_) continue;
    // The failure belongs to the ACK deadline itself. A later TX edge or the
    // end-of-window marker is only the safe moment when absence is confirmed.
    fail(FailReason::ACK_MISSING,
      pending_[i].tick + ackStartMaxTicks_, ackStartMaxTicks_);
    return;
  }
}

void DriverTest::completeIfPossible(uint64_t now) {
  if (state_ != DriverState::RUNNING) return;
  if (!measurementClosed_ && now >= deadlineTick_) measurementClosed_ = true;
  if (!measurementClosed_ || response_.active || pendingCount_) return;
  if (!stats_.turnOn.responses || !stats_.turnOff.responses) {
    fail(FailReason::ACK_MISSING, now);
    return;
  }
  ++completedSubsamples_;
  currentStep_ = completedSubsamples_;
  publishStats();
  requestCaptureStop();
  if (!waitCaptureStopped(25U)) {
    fail(FailReason::DATA_LOSS, lastEventTick_);
    return;
  }
  if (completedSubsamples_ >= subsampleCount_) {
    __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
    state_ = DriverState::PASS;
  } else {
    __atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
    state_ = DriverState::SUBSAMPLE_DONE;
  }
}

void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay,
                      uint64_t pulseWidth, uint64_t triggerAfterTx) {
  if (state_ == DriverState::FAIL || state_ == DriverState::PASS) return;
  if (stats_.reason == FailReason::NONE) {
    stats_.reason = reason;
    if (tick && havePointOrigin_ && tick >= pointOriginTick_)
      stats_.errorElapsedTicks = tick - pointOriginTick_;
    const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay;
    if (trigger) {
      stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX :
        static_cast<uint32_t>(trigger);
      stats_.errorTriggerValid = true;
    }
    if (delay) {
      stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
        static_cast<uint32_t>(delay);
      stats_.errorDelayValid = true;
    }
    if (pulseWidth) {
      stats_.errorPulseTicks = pulseWidth > UINT32_MAX ? UINT32_MAX :
        static_cast<uint32_t>(pulseWidth);
      stats_.errorPulseValid = true;
    }
  }
  publishStats();
  __atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
  requestCaptureStop();
  state_ = DriverState::FAIL;
}

void DriverTest::publishStats() {
  portENTER_CRITICAL(&statsMux_);
  publishedStats_ = stats_;
  portEXIT_CRITICAL(&statsMux_);
}

void DriverTest::forceFail(FailReason reason) {
  if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
      state_ == DriverState::SUBSAMPLE_DONE)
    fail(reason, lastEventTick_);
}

void DriverTest::abort() {
  if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
      state_ == DriverState::SUBSAMPLE_DONE)
    fail(FailReason::ABORTED, lastEventTick_);
  else {
    requestCaptureStop();
    state_ = DriverState::IDLE;
  }
}

bool DriverTest::takeProgressUpdate() {
  return __atomic_exchange_n(&progressUpdatePending_, false,
                             __ATOMIC_ACQ_REL);
}

void DriverTest::requestCaptureStop() {
  __atomic_store_n(&captureActive_, false, __ATOMIC_RELEASE);
}

bool DriverTest::waitCaptureStopped(uint32_t timeoutMs) {
  const uint32_t deadline = millis() + timeoutMs;
  while (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
         static_cast<int32_t>(millis() - deadline) < 0) delay(0);
  if (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) return false;
  if (__atomic_exchange_n(&core0WdtDisabled_, false,
                          __ATOMIC_ACQ_REL)) enableCore0WDT();
  return true;
}

void DriverTest::clearCapture() {
  const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
  __atomic_store_n(&ringRead_, write, __ATOMIC_RELEASE);
  __atomic_store_n(&droppedItems_, 0U, __ATOMIC_RELEASE);
  haveRawTick_ = false;
  lastRawTick_ = 0;
  tickEpoch_ = 0;
}

void IRAM_ATTR DriverTest::recordRaw(uint32_t tick, bool rising,
                                    Source source) {
  const uint16_t write = ringWrite_;
  const uint16_t next = static_cast<uint16_t>(
    (write + 1U) & (RING_CAPACITY - 1U));
  if (next == ringRead_) {
    ++droppedItems_;
    return;
  }
  ring_[write] = {tick, rising, source};
  asm volatile("memw" ::: "memory");
  ringWrite_ = next;
}

size_t DriverTest::readRaw(TimedEvent *events, size_t capacity,
                           TickType_t waitTicks) {
  if (!events || !capacity) return 0;
  uint16_t read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
  if (read == __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
    vTaskDelay(waitTicks);
    read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
  }
  const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
  size_t count = 0;
  while (read != write && count < capacity) {
    const RawEvent raw = ring_[read];
    read = static_cast<uint16_t>((read + 1U) & (RING_CAPACITY - 1U));
    if (haveRawTick_ && raw.tick < lastRawTick_ &&
        lastRawTick_ - raw.tick > 0x80000000UL) tickEpoch_ += 1ULL << 32U;
    lastRawTick_ = raw.tick;
    haveRawTick_ = true;
    events[count++] = {tickEpoch_ + raw.tick, raw.rising, raw.source};
  }
  __atomic_store_n(&ringRead_, read, __ATOMIC_RELEASE);
  return count;
}

uint32_t DriverTest::takeDropped() {
  return __atomic_exchange_n(&droppedItems_, 0U, __ATOMIC_ACQ_REL);
}

void DriverTest::rememberTrace(const TimedEvent &event) {
  trace_[traceWrite_] = {event.tick, static_cast<uint8_t>(event.source),
    static_cast<uint8_t>(event.rising), static_cast<uint8_t>(state_),
    pendingCount_};
  traceWrite_ = static_cast<uint8_t>((traceWrite_ + 1U) % TRACE_CAPACITY);
  if (traceCount_ < TRACE_CAPACITY) ++traceCount_;
}

void DriverTest::printSummary() const {
  auto printEdge = [&](const char *name, const DriverEdgeStats &edge) {
    if (!edge.responses) {
      Log::printf("DRIVER", "%s ACK=0", name);
      return;
    }
    Log::printf("DRIVER",
      "%s ACK=%lu D=%lluns/%lluns/%lluns P=%lluns/%lluns/%lluns",
      name, static_cast<unsigned long>(edge.responses),
      static_cast<unsigned long long>(ticksToNs(edge.minDelayTicks)),
      static_cast<unsigned long long>(ticksToNs(
        edge.delaySumTicks / edge.responses)),
      static_cast<unsigned long long>(ticksToNs(edge.maxDelayTicks)),
      static_cast<unsigned long long>(ticksToNs(edge.minResponseTicks)),
      static_cast<unsigned long long>(ticksToNs(
        edge.responseSumTicks / edge.responses)),
      static_cast<unsigned long long>(ticksToNs(edge.maxResponseTicks)));
  };
  Log::printf("DRIVER", "TX edges=%lu responses=%lu dropped=%lu unexpected=%lu result=%s",
    static_cast<unsigned long>(publishedStats_.inputEdges),
    static_cast<unsigned long>(publishedStats_.responses),
    static_cast<unsigned long>(publishedStats_.droppedItems),
    static_cast<unsigned long>(publishedStats_.unexpectedResponses),
    failName(publishedStats_.reason));
  if (publishedStats_.reason != FailReason::NONE) {
    auto formatOptional = [&](bool valid, uint32_t ticks,
                              char *out, size_t size) {
      if (!valid) snprintf(out, size, "---");
      else snprintf(out, size, "%lluns",
        static_cast<unsigned long long>(ticksToNs(ticks)));
    };
    char trigger[24], pulse[24];
    formatOptional(publishedStats_.errorTriggerValid,
      publishedStats_.errorTriggerTicks, trigger, sizeof(trigger));
    formatOptional(publishedStats_.errorPulseValid,
      publishedStats_.errorPulseTicks, pulse, sizeof(pulse));
    if (publishedStats_.errorPulseValid)
      Log::printf("DRIVER", "error timing: T=%s P=%s", trigger, pulse);
    else Log::printf("DRIVER", "error timing: T=%s", trigger);
  }
  printEdge("ON", publishedStats_.turnOn);
  printEdge("OFF", publishedStats_.turnOff);
}

void DriverTest::printTrace() const {
  if (!traceCount_) return;
  const uint8_t first = static_cast<uint8_t>(
    (traceWrite_ + TRACE_CAPACITY - traceCount_) % TRACE_CAPACITY);
  const uint64_t origin = trace_[first].tick;
  Log::printf("DRIVER", "RAM trace: %u events, tick=%luHz", traceCount_,
    static_cast<unsigned long>(captureHz_));
  for (uint8_t i = 0; i < traceCount_; ++i) {
    const TraceEvent &event = trace_[(first + i) % TRACE_CAPACITY];
    Log::printf("DRIVER", "E%02u +%lluns %s/%s state=%u pending=%u", i,
      static_cast<unsigned long long>(ticksToNs(event.tick - origin)),
      event.source == static_cast<uint8_t>(Source::TX) ? "TX" : "RX",
      event.rising ? "rise" : "fall", event.state, event.pending);
  }
}
