// Mailbox mail-presence decision logic. Plain C-compatible, no floats, no Arduino calls,
// so it compiles on the ATtiny402 AND in the host tests (test/test_detect.cpp).
//
// Inputs per beam (raw 10-bit ADC on the phototransistor collector, 0..1023):
//   dark = LEDs OFF  (ambient only)     lit = LEDs ON
// Light pulls the collector DOWN, so a clear beam gives lit < dark.
//   delta = dark - lit  = how much LED light reached the sensor.
// Calibrated baseline = delta with the box EMPTY.
//   delta <  25 % of baseline -> beam BLOCKED (mail)
//   delta >  40 % of baseline -> beam CLEAR
//   in between                -> keep that beam's last state (hysteresis)
// If dark < FLOOD_MIN the sensor is swamped by sunlight: that beam is "unknown" and holds.
// Mail = ANY beam blocked. The output only changes after DEBOUNCE agreeing readings in a row.
#ifndef MAILBOX_DETECT_H
#define MAILBOX_DETECT_H
#include <stdint.h>

#define DET_BEAMS      2
#define FLOOD_MIN      150   // dark reading below this = sensor flooded by sunlight
#define MIN_BASE       60    // a calibration delta smaller than this = LEDs not reaching the sensor
#define CAL_SAMPLES    8
#define BLOCK_PCT      25
#define CLEAR_PCT      40
#define DEBOUNCE       2
#define TRACK_SHIFT    5     // baseline follows clear readings with a 1/32 step (LED ageing, dust)

enum { MAIL_NO = 0, MAIL_YES = 1 };
enum { BEAM_CLEAR = 0, BEAM_BLOCKED = 1, BEAM_UNKNOWN = 2 };

typedef struct {
  uint16_t base[DET_BEAMS];      // calibrated empty-box delta per beam
  uint32_t base16[DET_BEAMS];    // base * 16 (fixed point, for slow tracking)
  uint32_t cal_sum[DET_BEAMS];
  uint8_t  cal_n;
  uint8_t  beam[DET_BEAMS];      // last state per beam
  uint8_t  out;                  // current debounced answer
  uint8_t  pending, pending_n;   // candidate change + how many readings agreed
  uint8_t  last_flooded;         // 1 if the last reading had a flooded sensor (for diagnostics)
} Detector;

// Always start from a zeroed detector (uninitialised memory = garbage calibration count).
static inline void det_init(Detector *d) { uint8_t *p = (uint8_t *)d; for (uint16_t i = 0; i < sizeof(Detector); i++) p[i] = 0; }

static inline int16_t det_delta(uint16_t dark, uint16_t lit) { return (int16_t)dark - (int16_t)lit; }

// Feed one EMPTY-box reading. Flooded or too-weak samples are ignored.
static inline void det_calibrate_sample(Detector *d, uint16_t dark1, uint16_t lit1, uint16_t dark2, uint16_t lit2) {
  uint16_t dk[2] = {dark1, dark2}; int16_t dl[2] = {det_delta(dark1, lit1), det_delta(dark2, lit2)};
  if (d->cal_n >= CAL_SAMPLES) return;
  for (uint8_t i = 0; i < DET_BEAMS; i++) if (dk[i] < FLOOD_MIN || dl[i] < MIN_BASE) return;
  for (uint8_t i = 0; i < DET_BEAMS; i++) d->cal_sum[i] += (uint32_t)dl[i];
  if (++d->cal_n == CAL_SAMPLES) {
    for (uint8_t i = 0; i < DET_BEAMS; i++) {
      d->base[i] = (uint16_t)(d->cal_sum[i] / CAL_SAMPLES);
      d->base16[i] = (uint32_t)d->base[i] << 4;
      d->beam[i] = BEAM_CLEAR;
    }
    d->out = MAIL_NO; d->pending_n = 0;
  }
}
static inline uint8_t det_calibrate_done(const Detector *d) { return d->cal_n >= CAL_SAMPLES; }

static inline uint8_t det_beam(Detector *d, uint8_t i, uint16_t dark, uint16_t lit) {
  int16_t dl = det_delta(dark, lit);
  if (dark < FLOOD_MIN) return BEAM_UNKNOWN;
  if (dl < 0) dl = 0;
  uint32_t pct100 = (uint32_t)dl * 100;
  if (pct100 < (uint32_t)BLOCK_PCT * d->base[i]) d->beam[i] = BEAM_BLOCKED;
  else if (pct100 > (uint32_t)CLEAR_PCT * d->base[i]) d->beam[i] = BEAM_CLEAR;
  return d->beam[i];
}

// One reading of both beams -> debounced MAIL_YES / MAIL_NO.
static inline uint8_t det_update(Detector *d, uint16_t dark1, uint16_t lit1, uint16_t dark2, uint16_t lit2) {
  uint16_t dk[2] = {dark1, dark2}, lt[2] = {lit1, lit2};
  uint8_t st[2], any_blocked = 0, known = 0;
  d->last_flooded = 0;
  for (uint8_t i = 0; i < DET_BEAMS; i++) {
    st[i] = det_beam(d, i, dk[i], lt[i]);
    if (st[i] == BEAM_UNKNOWN) d->last_flooded = 1; else known++;
    if (st[i] == BEAM_BLOCKED) any_blocked = 1;
  }
  uint8_t cand;
  if (any_blocked) cand = MAIL_YES;
  else if (known == DET_BEAMS) cand = MAIL_NO;      // only call it empty when BOTH beams were readable
  else cand = d->out;                               // a flooded beam can't prove the box is empty: hold
  if (cand == d->out) d->pending_n = 0;
  else {
    if (d->pending_n && d->pending == cand) d->pending_n++; else { d->pending = cand; d->pending_n = 1; }
    if (d->pending_n >= DEBOUNCE) { d->out = cand; d->pending_n = 0; }
  }
  // slow baseline tracking: only on clearly-clear readings while the box is empty
  if (d->out == MAIL_NO)
    for (uint8_t i = 0; i < DET_BEAMS; i++) if (st[i] == BEAM_CLEAR) {
      int32_t target = (int32_t)det_delta(dk[i], lt[i]) << 4;
      d->base16[i] = (uint32_t)((int32_t)d->base16[i] + ((target - (int32_t)d->base16[i]) >> TRACK_SHIFT));
      d->base[i] = (uint16_t)(d->base16[i] >> 4);
    }
  return d->out;
}
#endif
