#include <array>
#include <string>
#include <numeric>
#include <tc/adt/SmallVector.h>

#define FMT_HEADER_ONLY 1
#define FMT_STATIC_THOUSANDS_SEPARATOR 1
#define FMT_USE_FCNTL 0
#define FMT_USE_CONSTEXPR 1
#define FMT_USE_NOEXCEPT 1
#define FMT_USE_DOUBLE 0
#define FMT_USE_LONG_DOUBLE 0
#include <fmt/format.h>

#include <stdlib.h>
#include <stdint.h>
#include "saml10.h"
#include "hal_gpio.h"

///// DIGITAL I/O MACROS
HAL_GPIO_PIN(UART_TX,  A,   0)
HAL_GPIO_PIN(UART_RX,  A,   1)
HAL_GPIO_PIN(SVSAMP,   A,   2)
HAL_GPIO_PIN(CV4,      A,   3)
HAL_GPIO_PIN(CV3,      A,   4)
HAL_GPIO_PIN(CV2,      A,   5)
HAL_GPIO_PIN(CV1,      A,   6)
HAL_GPIO_PIN(POTSAMP,  A,   7)
HAL_GPIO_PIN(c0cspwm,  A,   8)
HAL_GPIO_PIN(c1cspwm,  A,   9)
HAL_GPIO_PIN(c2cspwm,  A,  10)
HAL_GPIO_PIN(c3cspwm,  A,  11)
HAL_GPIO_PIN(led0,     A,  14)
HAL_GPIO_PIN(led1,     A,  15)
HAL_GPIO_PIN(c0sspwm,  A,  16)
HAL_GPIO_PIN(c1sspwm,  A,  17)
HAL_GPIO_PIN(sense_en, A,  18)
HAL_GPIO_PIN(sample,   A,  19)
HAL_GPIO_PIN(c2sspwm,  A,  22)
HAL_GPIO_PIN(c3sspwm,  A,  23)
HAL_GPIO_PIN(led2,     A,  24)
HAL_GPIO_PIN(led3,     A,  25)
HAL_GPIO_PIN(input_en, A,  27)
HAL_GPIO_PIN(BUTTON,   A,  31)

////// DIGITAL I/O SETUP
void SetupIOPins() {
  // first, set up serial port pin pair.
  // set PA00/PA01 to 'alternative' peripheral function D (0x3), to get at SERCOM1
/*
  PORT->Group[0].WRCONFIG.reg = (uint32_t)(PORT_WRCONFIG_WRPINCFG|
					   PORT_WRCONFIG_WRPMUX|
					   //   PORT_WRCONFIG_INEN|
					   PORT_WRCONFIG_PINMASK(1<<0)| // PA00
					   PORT_WRCONFIG_PINMASK(1<<1)| // PA01
					   PORT_WRCONFIG_PMUXEN|        // yes, enable peripheral multiplexer
					   PORT_WRCONFIG_PMUX(3));      // peripheral function D
*/
  // (analog pins get no config here)

  // now set up remaining digital I/Os:
  HAL_GPIO_c0cspwm_out();
  HAL_GPIO_c0cspwm_set();
  HAL_GPIO_c0sspwm_out();
  HAL_GPIO_c0sspwm_set();
  HAL_GPIO_c1cspwm_out();
  HAL_GPIO_c1cspwm_set();
  HAL_GPIO_c1sspwm_out();
  HAL_GPIO_c1sspwm_set();
  HAL_GPIO_c2cspwm_out();
  HAL_GPIO_c2cspwm_set();
  HAL_GPIO_c2sspwm_out();
  HAL_GPIO_c2sspwm_set();
  HAL_GPIO_c3cspwm_out();
  HAL_GPIO_c3cspwm_set();
  HAL_GPIO_c3sspwm_out();
  HAL_GPIO_c3sspwm_set();

  HAL_GPIO_input_en_out();
  HAL_GPIO_input_en_clr();
  HAL_GPIO_sense_en_out();
  HAL_GPIO_sense_en_clr();
  HAL_GPIO_sample_out();
  HAL_GPIO_sample_clr();

  HAL_GPIO_led0_out();
  HAL_GPIO_led1_out();
  HAL_GPIO_led2_out();
  HAL_GPIO_led3_out();
  HAL_GPIO_led0_clr();
  HAL_GPIO_led1_clr();
  HAL_GPIO_led2_clr();
  HAL_GPIO_led3_clr();
  
  HAL_GPIO_BUTTON_in();
  HAL_GPIO_BUTTON_pullup();
}

// LED state and helpers
std::array<uint8_t, 4> LEDOnTimes, LEDOffTimes, LEDResetTimes{};
std::array<uint8_t, 4> LEDBlinkCtrs{};
void SetLED(const uint8_t led, const uint8_t state) {
  if (led == 0)
    HAL_GPIO_led0_write(state);
  if (led == 1)
    HAL_GPIO_led1_write(state);
  if (led == 2)
    HAL_GPIO_led2_write(state);
  if (led == 3)
    HAL_GPIO_led3_write(state);
}
void RenderLEDs() {
  for (int led = 0; led < 4; ++led) {
    if (LEDOnTimes[led] == 0) {
      SetLED(led, 0);
      LEDBlinkCtrs[led] = 0;
    } else {
      LEDBlinkCtrs[led]++;
      if (LEDBlinkCtrs[led] == LEDOnTimes[led]) {
        SetLED(led, 1);
      }
      if (LEDBlinkCtrs[led] == LEDOffTimes[led]) {
        SetLED(led, 0);
      }
      if (LEDBlinkCtrs[led] == LEDResetTimes[led]) {
        LEDBlinkCtrs[led] = 0;
      }
    }
  }
}

// UART RX buffer
char RxBuf[80];
char RxCmd[80];
uint8_t RxBufPtr{};

static bool ScrapeUARTRX() {
  if (SERCOM1->USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
    char c = SERCOM1->USART.DATA.reg;
    RxBuf[RxBufPtr++] = c;
    RxBufPtr &= 0x4F;
    if (c == '\n') {
      for (int i = 0; i < 80; ++i) {
        RxCmd[i] = RxBuf[i];
        RxBuf[i] = 0;
      }
      RxCmd[79] = 0;
      RxBufPtr = 0;
      return true;
    }
  }
  return false;
}

static void uart_putc(char c) {
  while (!(SERCOM1->USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE));
  SERCOM1->USART.DATA.reg = c;
}

void uart_puts(const char *s) {
  while (*s)
    uart_putc(*s++);
}

static std::array<uint16_t, 6> GrabADCSamples(const bool cal = false) {
  std::array<uint16_t, 6> samples{};

  // SENSE_EN
  HAL_GPIO_sense_en_set();
  for (int i = 0; i < 1000; i++)
    __NOP();
  if (!cal) {
    HAL_GPIO_sample_set();
  } else {
    // if we are calibrating, isolate the ADC input pins...
    HAL_GPIO_sample_clr();

    // ...and set them to outputs we drive low.
    HAL_GPIO_SVSAMP_out();
    HAL_GPIO_SVSAMP_clr();
    HAL_GPIO_CV4_out();
    HAL_GPIO_CV4_clr();
    HAL_GPIO_CV3_out();
    HAL_GPIO_CV3_clr();
    HAL_GPIO_CV2_out();
    HAL_GPIO_CV2_clr();
    HAL_GPIO_CV1_out();
    HAL_GPIO_CV1_clr();
    HAL_GPIO_POTSAMP_out();
    HAL_GPIO_POTSAMP_clr();
  }

  // wait a moment
  for (int i = 0; i < 1000; i++)
    __NOP();

  // cycle through inputs, grabbing samples
  for (int i = 0; i < 6; i++) {
    // set input
    ADC->INPUTCTRL.bit.MUXPOS = i; // {stack, cv4, cv3, cv2, cv1, trimpot}
    while (ADC->SYNCBUSY.reg & ADC_SYNCBUSY_INPUTCTRL);

    ADC->SWTRIG.reg = ADC_SWTRIG_START | ADC_SWTRIG_FLUSH;
    while (ADC->SYNCBUSY.reg & ADC_SYNCBUSY_SWTRIG);

    while ((ADC->INTFLAG.reg & ADC_INTFLAG_RESRDY) == 0);
    samples[i] = ADC->RESULT.reg;
  }

  if (cal) {
    // if we ran calibration, set pins back to inputs
    HAL_GPIO_SVSAMP_in();
    HAL_GPIO_CV4_in();
    HAL_GPIO_CV3_in();
    HAL_GPIO_CV2_in();
    HAL_GPIO_CV1_in();
    HAL_GPIO_POTSAMP_in();
  }

  return samples;
}

static void timer_set_period(uint16_t i) {
  TC1->COUNT16.CC[0].reg = (F_CPU / 1000ul / 256) * i;

  TC1->COUNT16.COUNT.reg = 0;
}

extern "C" void irq_handler_tc1(void) {
  if (TC1->COUNT16.INTFLAG.reg & TC_INTFLAG_MC(1)) {
    RenderLEDs();
    TC1->COUNT16.INTFLAG.reg = TC_INTFLAG_MC(1);
  }
}

// record button press times in 1024ths of a second
uint32_t buttonStartTime{};
uint32_t buttonStopTime{};
extern "C" void irq_handler_eic_4(void) {
  if (EIC->INTFLAG.reg & EIC_INTFLAG_EXTINT(1 << 7)) {
    if (HAL_GPIO_BUTTON_read()) {
      buttonStartTime = RTC->MODE0.COUNT.reg;
    } else {
      buttonStopTime = RTC->MODE0.COUNT.reg;
    }
    EIC->INTFLAG.reg = EIC_INTFLAG_EXTINT(1 << 7);
  }
}

// configure RTC as 1kHz 32b counter (one wrap every ~49 days)
void SetupRTC() {
  // enable RTC APB clock
  MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC;

  // select internal 1.024kHz from internal 32.768kHz RC oscillator
  OSC32KCTRL->RTCCTRL.bit.RTCSEL = OSC32KCTRL_RTCCTRL_RTCSEL(OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K_Val);

  // disable RTC
  RTC->MODE0.CTRLA.reg &= ~(RTC_MODE0_CTRLA_ENABLE);
  while(RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_ENABLE);

  // reset RTC
  RTC->MODE0.CTRLA.reg = (RTC_MODE0_CTRLA_SWRST);
  while(RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_SWRST);

  // div-by-1 prescaler required for events/interrupts, and place into "mode 0" (32b counter)
  // TODO: turn this off if it ends up unused
  RTC->MODE0.CTRLA.reg = RTC_MODE0_CTRLA_PRESCALER(RTC_MODE0_CTRLA_PRESCALER_DIV1_Val)|
                         RTC_MODE0_CTRLA_MODE(RTC_MODE0_CTRLA_MODE_COUNT32_Val);
/*
  // RTC Periodic event control
  RTC->MODE0.EVCTRL.reg = RTC_MODE0_EVCTRL_PEREO7;

  // Clear All interrupt flags
  RTC->MODE0.INTFLAG.reg = 0xFFFF;

  // set interrupt (@1.024kHz)
  RTC->MODE0.INTENSET.reg |= RTC_MODE0_INTENSET_PER7;

  // Enable RTC interrupt at core Level (NVIC) set the highest priority
  NVIC_EnableIRQ(RTC_IRQn);
  NVIC_SetPriority(RTC_IRQn,0);
*/
  // enable synchronous reads of the COUNT (counter value) register
  RTC->MODE0.CTRLA.bit.COUNTSYNC = 1;
  while(RTC->MODE0.SYNCBUSY.bit.COUNTSYNC);

  // re-enable RTC
  RTC->MODE0.CTRLA.bit.ENABLE = 1;
  while(RTC->MODE0.SYNCBUSY.bit.ENABLE);
}

uint64_t curTime{}, prevTime{};
uint32_t curTicks{}, prevTicks{};
void UpdateTime() {
  curTicks = RTC->MODE0.COUNT.reg;
  uint64_t correction{};
  uint32_t delta = curTicks - prevTicks;
  if (curTicks < prevTicks) {
    // lapped
    correction = (1ULL<<32);
  }
  prevTicks = curTicks;
  curTime = prevTime + correction + delta;
  prevTime = curTime;
}

uint64_t TimeNow() {
  UpdateTime();
  return curTime;
}
  
// RTC interrupt handler: clear the interrupt!
void RTC_Handler () {
  RTC->MODE0.INTFLAG.reg;
  /*** Check if an ALARM interrupt appeared ***/
  /*** Clear ALARM interrupt flag ***/
  RTC->MODE0.INTFLAG.reg |= RTC_MODE0_INTENSET_PER7;
}

// TODO: blip codegen
// TODO: adjust blips for number of cells
#define NOP10 { __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); }
void blipCell0ToStack(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c0cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c0cspwm_set();
    HAL_GPIO_c0sspwm_clr();
    NOP10
    HAL_GPIO_c0sspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipCell1ToStack(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c1cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c1cspwm_set();
    HAL_GPIO_c1sspwm_clr();
    NOP10
    HAL_GPIO_c1sspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipCell2ToStack(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c2cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c2cspwm_set();
    HAL_GPIO_c2sspwm_clr();
    NOP10
    HAL_GPIO_c2sspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipCell3ToStack(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c3cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c3cspwm_set();
    HAL_GPIO_c3sspwm_clr();
    NOP10
    HAL_GPIO_c3sspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipStackToCell0(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c0sspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c0sspwm_set();
    HAL_GPIO_c0cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c0cspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipStackToCell1(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c1sspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c1sspwm_set();
    HAL_GPIO_c1cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c1cspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipStackToCell2(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c2sspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c2sspwm_set();
    HAL_GPIO_c2cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c2cspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        
void blipStackToCell3(const uint8_t n = 0, const uint16_t gap = 255) {
  __disable_irq();
  for (uint8_t i = 0; i < n; ++i) {
    HAL_GPIO_c3sspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c3sspwm_set();
    HAL_GPIO_c3cspwm_clr();
    NOP10
    NOP10
    HAL_GPIO_c3cspwm_set();
    for (uint8_t i = 0; i < gap; ++i)
      __NOP();
  }
  __enable_irq();
}        

static void timer_init(void) {
  MCLK->APBCMASK.reg |= MCLK_APBCMASK_TC1;

  GCLK->PCHCTRL[TC1_GCLK_ID].reg = GCLK_PCHCTRL_GEN(0) | GCLK_PCHCTRL_CHEN;
  while (0 == (GCLK->PCHCTRL[TC1_GCLK_ID].reg & GCLK_PCHCTRL_CHEN));

  TC1->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV256 |
      TC_CTRLA_PRESCSYNC_RESYNC;

  TC1->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;

  TC1->COUNT16.COUNT.reg = 0;

  timer_set_period(50);

  TC1->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;

  TC1->COUNT16.INTENSET.reg = TC_INTENSET_MC(1);
  NVIC_EnableIRQ(TC1_IRQn);
}

static void uart_init(uint32_t baud) {
  uint64_t br = (uint64_t)65536 * (F_CPU - 16 * baud) / F_CPU;

  HAL_GPIO_UART_TX_out();
  HAL_GPIO_UART_TX_pmuxen(HAL_GPIO_PMUX_D);
  HAL_GPIO_UART_RX_in();
  HAL_GPIO_UART_RX_pmuxen(HAL_GPIO_PMUX_D);

  MCLK->APBCMASK.reg |= MCLK_APBCMASK_SERCOM1;

  GCLK->PCHCTRL[SERCOM1_GCLK_ID_CORE].reg = GCLK_PCHCTRL_GEN(0) | GCLK_PCHCTRL_CHEN;
  while (0 == (GCLK->PCHCTRL[SERCOM1_GCLK_ID_CORE].reg & GCLK_PCHCTRL_CHEN));

  // TXPO 0 means TX is SERCOM PAD[0]
  // TXPO 1 means TX is SERCOM PAD[2]
  // RXPO 0 through 3 means RX is SERCOM PAD[0] through PAD[3] respectively
  SERCOM1->USART.CTRLA.reg =
      SERCOM_USART_CTRLA_DORD | SERCOM_USART_CTRLA_MODE(1/*USART_INT_CLK*/) |
      SERCOM_USART_CTRLA_RXPO(1/*PAD1*/) | SERCOM_USART_CTRLA_TXPO(0/*PAD0*/);

  SERCOM1->USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN |
      SERCOM_USART_CTRLB_CHSIZE(0/*8 bits*/);

  SERCOM1->USART.BAUD.reg = (uint16_t)br;

  SERCOM1->USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;
}

static void sys_init() {
  // Switch to the highest performance level
  PM->INTFLAG.reg = PM_INTFLAG_PLRDY;
  PM->PLCFG.reg = PM_PLCFG_PLSEL_PL2;
  while (0 == PM->INTFLAG.bit.PLRDY);

  // Switch to 16MHz clock (disable prescaler)
  OSCCTRL->OSC16MCTRL.reg = OSCCTRL_OSC16MCTRL_ENABLE | OSCCTRL_OSC16MCTRL_FSEL_16;
}

// configure EIC to trigger interrupts on debounced edges on BUTTON GPIO (EXTINT[7])
// TODO: reduce power consumption - check clocking
static void SetupEIC() {
  // first, mux BUTTON to EIC (peripheral A)
  HAL_GPIO_BUTTON_pmuxen(HAL_GPIO_PMUX_A);

  // route clocks to EIC:
  MCLK->APBAMASK.reg |= MCLK_APBAMASK_EIC;
  GCLK->PCHCTRL[EIC_GCLK_ID].reg = GCLK_PCHCTRL_GEN(0) | GCLK_PCHCTRL_CHEN;
  while (0 == (GCLK->PCHCTRL[EIC_GCLK_ID].reg & GCLK_PCHCTRL_CHEN));

  // disable and reset EIC
  EIC->CTRLA.bit.ENABLE = 0;
  while (EIC->SYNCBUSY.reg & EIC_SYNCBUSY_ENABLE);
  EIC->CTRLA.bit.SWRST = 1;
  while (EIC->SYNCBUSY.reg & EIC_SYNCBUSY_ENABLE);

  // run EIC domain from CLK_ULP32K
  EIC->CTRLA.bit.CKSEL = 1;
  
  // set input sense to 'both edges' and enable filtering
  EIC->CONFIG[0].reg = EIC_CONFIG_SENSE7_BOTH | EIC_CONFIG_FILTEN7;
    
  // enable interrupt
  EIC->INTENSET.bit.EXTINT = EIC_INTENSET_EXTINT(1 << 7);
  NVIC_EnableIRQ(EIC_4_IRQn);
  
  // finally, enable EIC:
  EIC->CTRLA.bit.ENABLE = 1;
  while (EIC->SYNCBUSY.reg & EIC_SYNCBUSY_ENABLE);
}

void SetupADC() {
  // first: set up ADC clocks:
  MCLK->APBCMASK.reg |= MCLK_APBCMASK_ADC;
  GCLK->PCHCTRL[ADC_GCLK_ID].reg = GCLK_PCHCTRL_GEN(0) | GCLK_PCHCTRL_CHEN;
  while (0 == (GCLK->PCHCTRL[ADC_GCLK_ID].reg & GCLK_PCHCTRL_CHEN));

  // next: disable and reset ADC
  ADC->CTRLA.bit.ENABLE = 0;
  while (ADC->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE);
  ADC->CTRLA.bit.SWRST = 1;
  while (ADC->SYNCBUSY.reg & ADC_SYNCBUSY_SWRST);

  // enable ADC events
  //ADC->EVCTRL.bit.STARTEI = 1;
  //ADC->EVCTRL.bit.FLUSHEI = 1;

  /*** Enable ADC result ready interrupt ***/
  //ADC->INTENSET.bit.RESRDY = 1;

  // 2. configure clock prescaler
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV16; // other bits in CTRLB are reserved

  // 3. set reference to Vdd/2
  ADC->REFCTRL.reg = ADC_REFCTRL_REFSEL_INTVCC1; // | ADC_REFCTRL_REFCOMP;

  // 4. set to single-ended
  ADC->CTRLC.bit.DIFFMODE = 0;

  // 5. enable offset and gain correction
  ADC->CTRLC.bit.CORREN = 0; // XXX

  ADC->CTRLC.bit.FREERUN = 0; // XXX

//  ADC->SAMPCTRL.reg = 0x80; // Comparator Offset Compensation Enable
  // 6. set averaging
  ADC->CTRLC.bit.RESSEL = 0x1;      // set 16b output resolution
  ADC->AVGCTRL.bit.SAMPLENUM = 0x8; // accumulate 256 samples
  ADC->AVGCTRL.bit.ADJRES = 0x0;    // and yield 16b results

  // Load in the fixed device ADC calibration constants
  #define BIASCOMP_VAL ((*(uint32_t *)ADC_FUSES_BIASCOMP_ADDR & ADC_FUSES_BIASCOMP_Msk) >> ADC_FUSES_BIASCOMP_Pos)
  #define BIASREFBUF_VAL ((*(uint32_t *)ADC_FUSES_BIASREFBUF_ADDR & ADC_FUSES_BIASREFBUF_Msk) >> ADC_FUSES_BIASREFBUF_Pos)
  ADC->CALIB.reg = ADC_CALIB_BIASCOMP(BIASCOMP_VAL) | ADC_CALIB_BIASREFBUF(BIASREFBUF_VAL);

  // last: enable ADC
  ADC->CTRLA.bit.ENABLE = 1;
  while (ADC->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE);

  // statically select -ve input to be GND
  ADC->INPUTCTRL.bit.MUXNEG = 0x18; // internal GND
}

void SetInputDecap(const bool enable = false) {
  HAL_GPIO_input_en_write(enable);
  for (int i = 0; i < 50000; i++)
    __NOP();
}

// attempt to render device safe/low power consumption in case of emergency
void RenderSafe() {
  // disable all stack-side power
  HAL_GPIO_c0sspwm_set();
  HAL_GPIO_c1sspwm_set();
  HAL_GPIO_c2sspwm_set();
  HAL_GPIO_c3sspwm_set();

  // disable all cell-side power
  HAL_GPIO_c0cspwm_set();
  HAL_GPIO_c1cspwm_set();
  HAL_GPIO_c2cspwm_set();
  HAL_GPIO_c3cspwm_set();

  // disconnect sense circuitry
  HAL_GPIO_sample_clr();
  for (int i = 0; i < 100; i++)
    __NOP();
  HAL_GPIO_sense_en_clr();
  
  // disconnect decap
  SetInputDecap(false);
  
  // TODO: turn LEDs off
  
  // TODO: set CPU freq as low as possible, turn off clocks+peripherals
}

void SafetyHalt() {
  RenderSafe();
  for(;;)
    __NOP();
}

void PowerOnDebounce() {
  const int minTime = 2000000;
  int i;
  for (i = 0; i < minTime; ++i)
    __NOP();
  if (i != minTime)
    SafetyHalt();
}

float CellDividerHigh[4];
float CellDividerLow[4];
float StackDividerHigh;
float StackDividerLow;
float ADC3v3{3.3f}; // nominal ADC supply rail
void SetDefaultResistorValues() {
  // TODO: store/retrieve these values from flash
  StackDividerHigh = 100e3f;
  StackDividerLow = 2.2e3f;
  for (int i = 0; i < 4; ++i) {
    CellDividerHigh[i] = 100e3f;
    CellDividerLow[i] = 2.2e3f;
  }
}

// TODO: make this robust
template <typename T, typename Total, const uint32_t N>
class MovingAverage {
public:
  MovingAverage& operator()(T sample) {
    T& oldest = samples_[num_samples_];
    num_samples_++;
    num_samples_ %= N;
    total_ += static_cast<int32_t>(sample) - static_cast<int32_t>(oldest);
    oldest = sample;
    return *this;
  }

  void SetCal(const Total offset) {
    offset_ = offset;
  }
  
  Total Sum() const {
    return total_;
  }
  
  void UpdateScaleFactor(const float highR, const float lowR) {
    scaleFactor = ADC3v3 * (highR + lowR) / (2.0f * 65535.0f * lowR);
  }
  
  operator float() const { return scaleFactor * ((total_ - offset_) / N); }

private:
  std::array<T, N> samples_;
  uint32_t num_samples_;
  float scaleFactor;
  Total total_;
  Total offset_;
};

class Voltages {
// TODO: detect / sanity check resistor dividers?
public:
  void Update(bool cal = false) {
    const auto samples = GrabADCSamples(cal);
    
    Vstack(samples[0]);
    CV3(samples[1]);
    CV2(samples[2]);
    CV1(samples[3]);
    CV0(samples[4]);
    trimpot = samples[5];
  }

  void SetCal(const Voltages &cal) {
    Vstack.SetCal(cal.Vstack.Sum());
    CV0.SetCal(cal.CV0.Sum());
    CV1.SetCal(cal.CV1.Sum());
    CV2.SetCal(cal.CV2.Sum());
    CV3.SetCal(cal.CV3.Sum());
  }

  MovingAverage<uint16_t, int32_t, 64> Vstack;
  MovingAverage<uint16_t, int32_t, 64> CV0;
  MovingAverage<uint16_t, int32_t, 64> CV1;
  MovingAverage<uint16_t, int32_t, 64> CV2;
  MovingAverage<uint16_t, int32_t, 64> CV3;
  uint16_t trimpot;
};

uint32_t cnt{};
Voltages v, cal;
uint64_t RemoteCellAvgRXTime{};
static constexpr uint32_t RemoteCellAvgTimeout{60}; // 60s
float remoteAvgCellV{};

void UpdateScaleFactors() {
  v.Vstack.UpdateScaleFactor(StackDividerHigh, StackDividerLow);
  cal.Vstack.UpdateScaleFactor(StackDividerHigh, StackDividerLow);
  v.CV0.UpdateScaleFactor(CellDividerHigh[0], CellDividerLow[0]);
  v.CV1.UpdateScaleFactor(CellDividerHigh[1], CellDividerLow[1]);
  v.CV2.UpdateScaleFactor(CellDividerHigh[2], CellDividerLow[2]);
  v.CV3.UpdateScaleFactor(CellDividerHigh[3], CellDividerLow[3]);
  cal.CV0.UpdateScaleFactor(CellDividerHigh[0], CellDividerLow[0]);
  cal.CV1.UpdateScaleFactor(CellDividerHigh[1], CellDividerLow[1]);
  cal.CV2.UpdateScaleFactor(CellDividerHigh[2], CellDividerLow[2]);
  cal.CV3.UpdateScaleFactor(CellDividerHigh[3], CellDividerLow[3]);
}

// helper to abstract away fact that bottom cell voltage is a
// single-ended measurement, but other cells are differential
class CellVoltage {
public:
  CellVoltage(const int8_t id) : ID(id) {}
  operator float() const {
    if (ID == 0) {
      return v.CV0;
    }
    if (ID == 1) {
      return v.CV1 - v.CV0;
    }
    if (ID == 2) {
      return v.CV2 - v.CV1;
    }
    if (ID == 3) {
      return v.CV3 - v.CV2;
    }
    return 0.0f;
  }
  int8_t GetID() const { return ID; }
private:
  int8_t ID;
};
llvm::SmallVector<CellVoltage, 4> CVs;

float LowCellThreshold{2.7f};
float HighCellThreshold{16.0f};
int8_t CellCount{0};
int8_t GetCellCount() {
  if (CVs[0] <= LowCellThreshold && CVs[1] <= LowCellThreshold && CVs[2] <= LowCellThreshold && CVs[3] <= LowCellThreshold)
    return 0;
  if (CVs[0]  > LowCellThreshold && CVs[1] <= LowCellThreshold && CVs[2] <= LowCellThreshold && CVs[3] <= LowCellThreshold)
    return 1;
  if (CVs[0]  > LowCellThreshold && CVs[1]  > LowCellThreshold && CVs[2] <= LowCellThreshold && CVs[3] <= LowCellThreshold)
    return 2;
  if (CVs[0]  > LowCellThreshold && CVs[1]  > LowCellThreshold && CVs[2]  > LowCellThreshold && CVs[3] <= LowCellThreshold)
    return 3;
  if (CVs[0]  > LowCellThreshold && CVs[1]  > LowCellThreshold && CVs[2]  > LowCellThreshold && CVs[3]  > LowCellThreshold)
    return 4;

  return -1;
}

void CheckSafety() {
  // TODO: establish Vdd is actually reasonable (compare e.g. Vdd/2 to internal voltage reference)
  // TODO: check that battery voltages aren't too low / possible shorts
  // TODO: check for lose/lost connections (possible opens)
  // TODO: check that battery voltages aren't wildly mismatched
  // TODO: check that cells are corrected correctly (in order)
  // TODO: check battery voltages aren't changing too rapidly
  // TODO: check temperature (can't, atmel bug?)
}

char consoleBuffer[80];
void PrintUptimeHeader() {
  fmt::format_to_n(consoleBuffer, std::size(consoleBuffer) - 1, "[{:012.3f}] ", TimeNow() * 0.9765625e-3f);
  uart_puts(consoleBuffer);
}

void CalibrateADC() {
  PrintUptimeHeader();
  uart_puts("calibrating ADC...\n");

  // take calibration samples
  for (int i = 0; i < 66; i++)
    cal.Update(true);

  PrintUptimeHeader();
  uart_puts("finished ADC cal.\n");
}

// UpdatePlan(): high level "what to do" logic
//   - this works out where charge is to be shuffled to/from
//     (if anywhere) but not the rate at which this happens
// TODO: support simultaneous multi-cell balancing
// TODO: support non-"stack is sum of cells" operation

// control states
enum State : uint8_t {
  Idle = 0,
  IdleDueTooFewCells = 10,
  IdleDueStackMismatch = 20,
  IdleDueCellsInBalance = 30,
  IdleDueUserPause = 40,
  IdleDueTimeout = 50,
  PeakIdleState = 99,
  Balancing = 100,
  Immediate = 150,
  Shutdown = 255
};
State state{State::Idle};
uint32_t BalanceTimeout{600}; // stop balancing if balance not achieved by timeout
uint64_t BalanceStartTime{};
float startBalThresh{0.06f}; // start balancing if 60mV imbalance detected
float stopBalThresh{0.04f};  // stop balancing if cells are within 40mV
uint8_t ManualChargeRate; // rate to transfer charge in manual mode, as a percentage of peak
llvm::SmallVector<int8_t, 4> cellsCharging; // which cell IDs are charging from stack?
llvm::SmallVector<int8_t, 4> cellsDischarging; // which cell IDs are discharging to stack?
void UpdatePlan() {
  // if we are shut down or paused due to user request or timeout, remain in that state
  if (state == State::Shutdown ||
      state == State::IdleDueUserPause ||
      state == State::IdleDueTimeout) {
    cellsCharging.clear();
    cellsDischarging.clear();
    return;
  }
  
  const auto timeNow = TimeNow();

  // remove remote average cell voltage if stale
  if (static_cast<uint32_t>((timeNow - RemoteCellAvgRXTime) >> 10) > RemoteCellAvgTimeout) {
    remoteAvgCellV = 0.0f;
  }
  
  // if we are in immediate (manual) mode, remain in this state unless we've timed out
  if (state == State::Immediate) {
    if (static_cast<uint32_t>((timeNow - BalanceStartTime) >> 10) > BalanceTimeout) {
      cellsCharging.clear();
      cellsDischarging.clear();
      state = State::IdleDueTimeout;
    }
    return;
  }

  // if there is only one cell, or none at all, we idle. Perhaps
  // another cell will come along later.
  if (CellCount <= 1) {
    cellsCharging.clear();
    cellsDischarging.clear();
    state = State::IdleDueTooFewCells;
    return;
  } else {
    // we have enough cells, so don't have 'too few cells' as a reason for idling
    if (state == State::IdleDueTooFewCells)
      state = State::Idle;
  }
  
  llvm::SmallVector<CellVoltage, 4> sortedCVs;
  for (const auto &cv : CVs) {
    if (cv > LowCellThreshold && cv < HighCellThreshold) // if between 2.7V and 16.0V
      sortedCVs.push_back(cv);
  }
  if (sortedCVs.size() <= 1)
    return;
  std::sort(sortedCVs.begin(), sortedCVs.end());

  // we idle if all active cells are already balanced, unless
  // we're peering with a board we're not balanced with
  const auto mismatch = sortedCVs.back() - sortedCVs.front();
  const float activeCellSum = std::accumulate(sortedCVs.begin(), sortedCVs.end(), 0.0f);
  const bool peering = (remoteAvgCellV != 0.0f);
  const float localAvgCellV = activeCellSum / static_cast<float>(CellCount);
  const float remoteMismatch = localAvgCellV - remoteAvgCellV;
  if (mismatch < stopBalThresh) {
    if (!peering || std::abs(remoteMismatch) < stopBalThresh) {
      cellsCharging.clear();
      cellsDischarging.clear();
      state = State::IdleDueCellsInBalance;
      return;
    }
  }

  // we also idle if the stack isn't within 10% of the active cell
  // voltage sum
  const auto stackV = v.Vstack;
  if (activeCellSum > 1.1f * stackV ||
      activeCellSum < 0.9f * stackV) {
    cellsCharging.clear();
    cellsDischarging.clear();
    state = State::IdleDueStackMismatch;
    return;
  } else {
    // stack matches cell voltages, so don't have this as a reason for idling
    if (state == State::IdleDueStackMismatch)
      state = State::Idle;
  }

  // if we're balancing (or need to start):
  if (mismatch > startBalThresh ||
      (peering && remoteMismatch < -startBalThresh) ||
      state == State::Balancing) {

    // check for timeout
    if (state == State::Balancing) {
      // SHR10 due to time counted in 1024ths of a second
      if (static_cast<uint32_t>((timeNow - BalanceStartTime) >> 10) > BalanceTimeout) {
        cellsDischarging.clear();
        cellsCharging.clear();
        state = State::IdleDueTimeout;
      }
    }
    
    // TODO: when efficient plans are implemented, compute cases where
    //       shuffling from cell to stack may be optimal, this can also
    //       help guard against cell droop
    //
    // for now: we _always_ balance from the stack to the lowest cell
    if (sortedCVs.size() >= 2) {
      cellsDischarging.clear();
      cellsCharging.clear();
      cellsCharging.push_back(sortedCVs[0].GetID());
      if (state != State::Balancing) {
        // if we're starting to balance, note this
        state = State::Balancing;
        BalanceStartTime = timeNow;
      }
    }
  }

}

// this function displays the current state by writing to the
// serial port and updating the LED duty cycles
char src[3], dst[3];
char cellCnt[3];
char cellDesc[16];

void WriteState() {
  PrintUptimeHeader();

  std::fill(LEDOnTimes.begin(), LEDOnTimes.end(), 0);
  std::fill(src, src + 3, 0);
  std::fill(dst, dst + 3, 0);
  std::fill(dst, dst + 3, 0);
  std::fill(cellCnt, cellCnt + 3, 0);
  
  if (state <= State::PeakIdleState) {
    uart_puts("Idle ");
    if (state == State::IdleDueTooFewCells) {
      uart_puts("(too few cells)");
    }
    if (state == State::IdleDueStackMismatch) {
      uart_puts("(stack mismatch)");
    }
    if (state == State::IdleDueCellsInBalance) {
      uart_puts("(in balance)");
    }
    if (state == State::IdleDueUserPause) {
      uart_puts("(user paused)");
    }
    if (state == State::IdleDueTimeout) {
      uart_puts("(timeout reached)");
    }
    // set led 0 to blink briefly, rarely
    LEDOnTimes[0] = 2;
    LEDOffTimes[0] = 3;
    LEDResetTimes[0] = 100;
  }
  if (state == State::Balancing || state == State::Immediate) {
    if (cellsCharging.size() == 1) {
      fmt::format_to_n(src, std::size(src) - 1, "ST");
      fmt::format_to_n(dst, std::size(dst) - 1, "C{:1}", cellsCharging[0] + 1);
      const auto led = cellsCharging[0];
      LEDOnTimes[led] = 2;
      LEDOffTimes[led] = 6;
      LEDResetTimes[led] = 20;
    } else if (cellsDischarging.size() == 1) {
      fmt::format_to_n(src, std::size(src) - 1, "C{:1}", cellsDischarging[0] + 1);
      const auto led = cellsDischarging[0];
      LEDOnTimes[led] = 2;
      LEDOffTimes[led] = 6;
      LEDResetTimes[led] = 10;
      fmt::format_to_n(dst, std::size(dst) - 1, "ST");
    } else {
      fmt::format_to_n(src, std::size(src) - 1, "??");
      fmt::format_to_n(dst, std::size(dst) - 1, "??");
    }
    if (state == State::Balancing)
      uart_puts("Balancing: ");
    if (state == State::Immediate)
      uart_puts("Transferring: ");
    uart_puts(src);
    uart_puts(" => ");
    uart_puts(dst);
  }
  if (state == State::Shutdown) {
    uart_puts("Shutdown!");
    // TODO: LEDs: if overvoltage error, blink LEDs @ 1Hz 50% duty
  }
  uart_puts("\n");

  PrintUptimeHeader();
  
  fmt::format_to_n(cellCnt, std::size(cellCnt) - 1, "{}", CellCount);
  uart_puts(cellCnt);
  uart_puts("S Stack");

  // decorate stack with charge/discharge direction  
  if (cellsCharging.size())
    uart_puts("=>");
  else if (cellsDischarging.size())
    uart_puts("<=");
  else
    uart_puts("  ");
  
  std::fill(cellDesc, cellDesc + 16, 0);
  fmt::format_to_n(cellDesc, std::size(cellDesc) - 1, " {:2.2f}V | ", v.Vstack);
  uart_puts(cellDesc);
  
  for (int8_t cell = 3; cell >= 0; --cell) {
    std::fill(src, src + 3, 0);
    fmt::format_to_n(src, std::size(src) - 1, "C{}", cell + 1);
    uart_puts(src);
    const auto v = CVs[cell];
    if (v < LowCellThreshold)
      uart_puts("L!");
    else if (v > HighCellThreshold)
      uart_puts("H!");
    else if (std::count(cellsCharging.begin(), cellsCharging.end(), cell))
      uart_puts("<=");
    else if (std::count(cellsDischarging.begin(), cellsDischarging.end(), cell))
      uart_puts("=>");
    else
      uart_puts("  ");

    std::fill(cellDesc, cellDesc + 16, 0);
    fmt::format_to_n(cellDesc, std::size(cellDesc) - 1, " {:2.2f}V | ", CVs[cell]);
    uart_puts(cellDesc);
  }
  uart_puts("\n");
}

// shuffle power: if cellsCharging is non-empty, we're shuffling
// power from the stack to that cell
// if cellsDischarging is non-empty, we're shuffling power _from_
// that cell to 
void RunPlan() {
  if (state != State::Balancing &&
      state != State::Immediate)
    return;

  // TODO: stop assuming we are charging/discharging at most one cell

  if (cellsCharging.size()) {
    if (cellsCharging[0] == 0) {
      blipStackToCell0(10, 10);
    }
    if (cellsCharging[0] == 1) {
      blipStackToCell1(10, 10);
    }
    if (cellsCharging[0] == 2) {
      blipStackToCell2(10, 10);
    }
    if (cellsCharging[0] == 3) {
      blipStackToCell3(10, 10);
    }
  }

  if (cellsDischarging.size()) {
    if (cellsDischarging[0] == 0) {
      blipCell0ToStack(10, 10);
    }
    if (cellsDischarging[0] == 1) {
      blipCell1ToStack(10, 10);
    }
    if (cellsDischarging[0] == 2) {
      blipCell2ToStack(10, 10);
    }
    if (cellsDischarging[0] == 3) {
      blipCell3ToStack(10, 10);
    }
  }
  
}

char ParserResult[72];
void ClearParserResult() {
  std::fill(ParserResult, ParserResult + 72, 0);
}

void SetBalanceThreshold(const float t) {
  startBalThresh = t;
  PrintUptimeHeader();
  fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Cells can vary up to {}V before balancing starts\n", startBalThresh);
  uart_puts(ParserResult);
}

void SetLowCellThreshold(const float val) {
  LowCellThreshold = val;
  PrintUptimeHeader();
  fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Setting low cell threshold to {}V\n", LowCellThreshold);
  uart_puts(ParserResult);
}

void SetHighCellThreshold(const float val) {
  HighCellThreshold = val;
  PrintUptimeHeader();
  fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Setting high cell threshold to {}V\n", HighCellThreshold);
  uart_puts(ParserResult);
}

uint32_t NumShortPresses{};
uint64_t PrevShortPressTime{};
void HandleButton() {
  // if the button is currently depressed, do nothing.
  if (buttonStopTime <= buttonStartTime)
    return;

  // do we need to handle a sequence of short button presses?
  if (NumShortPresses > 0) {
    const auto now = TimeNow();
    // ensure the last short button press was more than 2s ago
    if ((now - PrevShortPressTime) > 2048) {
      if (NumShortPresses == 1) {
        // print revision info to serial and dance LEDs
        PrintUptimeHeader();
        uart_puts("CRBB firmware v1.00\n");
        for (int i = 0; i < 10; ++i) {
          for (int j = 0; j < 4; ++j) {
            SetLED(j, 1);
            for (int p = 0; p < 200000; ++p)
              __NOP();
            SetLED(j, 0);
          }
        }
      } else if (NumShortPresses == 2) {
        SetBalanceThreshold(static_cast<float>(v.trimpot) * 1.52587890625e-5f);
      } else if (NumShortPresses == 4) {
        SetLowCellThreshold(static_cast<float>(v.trimpot) * 0.0002288818359375f);
      } else if (NumShortPresses == 5) {
        SetHighCellThreshold(static_cast<float>(v.trimpot) * 0.0002288818359375f);
      }
      NumShortPresses = 0;
    }
  }

  const auto pressTime = buttonStopTime - buttonStartTime;
  
  // ignore any button presses below 50ms or above 10s
  if (pressTime < 51 || pressTime > 10240)
    return;

  // count short button presses:
  if (pressTime < 512) {
    buttonStartTime = buttonStopTime;
    const auto now = TimeNow();
    if ((now - PrevShortPressTime) < 2048) {
      NumShortPresses++;
      PrevShortPressTime = now;
    }
  }
  
  // if this is a longer press, kill any chain of short presses
  if (pressTime >= 768) {
    NumShortPresses = 0;
  }
  
  // pause/resume balancing (one medium press: ~1-2s)
  if (pressTime >= 768 && pressTime < 2560) {
    buttonStartTime = buttonStopTime;
    if (state == State::IdleDueUserPause)
      state = State::Idle;
    else if (state < State::Shutdown) {
      PrintUptimeHeader();
      uart_puts("Pausing...\n");
      state = State::IdleDueUserPause;
    }
  }
}

// if we're here, we've probably just received a serial command,
// this is where we act on them.
void HandleInput() {
  ClearParserResult();
  
  // pause?
  if (RxCmd[0] == 'p') {
    if (state < State::Shutdown) {
      PrintUptimeHeader();
      uart_puts("Pausing...\n");
      state = State::IdleDueUserPause;
    }
    return;
  }
  
  // resume?
  if (RxCmd[0] == 'r') {
    if (state == State::IdleDueUserPause) {
      PrintUptimeHeader();
      uart_puts("Resuming from pause...\n");
      state = State::Idle;
    }
    return;
  }

  int int1, int2, int3;
  // setting timeout?
  if (sscanf(RxCmd, "t %d", &int1) == 1) {
    if (int1 >= 10 && int1 <= 86400) {
      BalanceTimeout = int1;
      PrintUptimeHeader();
      fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Set balance timeout to {} seconds\n", BalanceTimeout);
      uart_puts(ParserResult);
      return;
    }
  }

  // setting low cell threshold?
  if (sscanf(RxCmd, "l %d", &int1) == 1) {
    if (int1 >= 2500 && int1 <= 16000) {
      const float val = static_cast<float>(int1) * 0.001f;
      if (val <= HighCellThreshold) {
        SetLowCellThreshold(val);
        return;
      }
    }
  }

  // setting high cell threshold?
  if (sscanf(RxCmd, "h %d", &int1) == 1) {
    if (int1 >= 2500 && int1 <= 16000) {
      const float val = static_cast<float>(int1) * 0.001f;
      if (val >= LowCellThreshold) {
        SetHighCellThreshold(val);
        return;
      }
    }
  }
  
  // setting "start balancing" point?
  if (sscanf(RxCmd, "l %d", &int1) == 1) {
    if (int1 >= 50 && int1 <= 1000) {
      SetBalanceThreshold(static_cast<float>(int1) * 0.001f);
      return;
    }
  }

  // immediate cell-in command
  if (sscanf(RxCmd, "i%d %d", &int1, &int2) == 2) {
    if (int1 >= 1 && int1 <= 4 && int2 >= 1 && int2 <= 100) {
      state = State::Immediate;
      cellsCharging.clear();
      cellsDischarging.clear();
      cellsCharging.push_back(int1 - 1);
      ManualChargeRate = int2;
      BalanceStartTime = TimeNow();
      PrintUptimeHeader();
      fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Transferring charge into cell {} at {}% rate\n", int1, int2);
      uart_puts(ParserResult);
      return;
    }
  }

  // immediate cell-out command
  if (sscanf(RxCmd, "o%d %d", &int1, &int2) == 2) {
    if (int1 >= 1 && int1 <= 4 && int2 >= 1 && int2 <= 100) {
      state = State::Immediate;
      cellsCharging.clear();
      cellsDischarging.clear();
      cellsDischarging.push_back(int1 - 1);
      ManualChargeRate = int2;
      BalanceStartTime = TimeNow();
      PrintUptimeHeader();
      fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Transferring charge out of cell {} at {}% rate\n", int1, int2);
      uart_puts(ParserResult);
      return;
    }
  }
  
  // set cell voltage divider
  if (sscanf(RxCmd, "c%d %d %d", &int1, &int2, &int3) == 3) {
    if (int1 >= 1 && int1 <= 4) {
      if (int2 >= 100 && int3 >= 100) {
        int cell = int1 - 1;
        CellDividerHigh[cell] = static_cast<float>(int2);
        CellDividerLow[cell] = static_cast<float>(int3);
        UpdateScaleFactors();
        PrintUptimeHeader();
        fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Set cell #{} to have voltage divider ratio of {}:{}", int1, int2, int3);
        uart_puts(ParserResult);
        return;
      }
    }
  }

  // set stack voltage divider
  if (sscanf(RxCmd, "st %d %d", &int1, &int2) == 2) {
    if (int1 >= 100 && int2 >= 100) {
      StackDividerHigh = int1;
      StackDividerLow = int2;
      v.Vstack.UpdateScaleFactor(int1, int2);
      cal.Vstack.UpdateScaleFactor(int1, int2);
      PrintUptimeHeader();
      fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Set stack to have voltage divider ratio of {}:{}", int1, int2);
      uart_puts(ParserResult);
      return;
    }
  }

  // set full scale (ADC 3.3V rail)
  if (sscanf(RxCmd, "v %d", &int1) == 1) {
    if (int1 >= 2500 && int1 <= 3630) {
      ADC3v3 = static_cast<float>(int1) * 0.001f;
      UpdateScaleFactors();
      PrintUptimeHeader();
      fmt::format_to_n(ParserResult, std::size(ParserResult) - 1, "Set MCU 3.3V rail to {}V", ADC3v3);
      uart_puts(ParserResult);
      return;
    }
  }
  
  // cell voltage status line from a peer CRBB
  char js[16];
  float svf, cv3f, cv2f, cv1f, cv0f;
  if (sscanf(RxCmd, "[%d.%d] %dS %15s %fV | C%6s %fV | C%6s %fV | C%6s %fV | C%6s %fV",
                    &int1, &int2, &int3, js, &svf, js, &cv3f, js, &cv2f, js, &cv1f, js, &cv0f) == 13) {
    // sanity check the timestamp and remote stack size
    if (int1 > 0 && int2 >= 0 && int2 < 1000 && int3 >= 1 && int3 <= 4) {
      // update remote cell average
      remoteAvgCellV = 0.0f;
      if (int3 >= 1)
        remoteAvgCellV += cv0f;
      if (int3 >= 2)
        remoteAvgCellV += cv1f;
      if (int3 >= 3)
        remoteAvgCellV += cv2f;
      if (int3 >= 4)
        remoteAvgCellV += cv3f;
      remoteAvgCellV /= static_cast<float>(int3);
      RemoteCellAvgRXTime = TimeNow();
    }
  }
  
  // TODO: print some info/advice if we received serial input but didn't
  //       act on it, being mindful that this could lead to an infinite
  //       loop if we are peering with another CRBB
}

int main(void) {
  sys_init();
  timer_init();
  uart_init(38400);

  SetupIOPins();
  SetupADC();
  SetupEIC();
  SetupRTC();
  SetDefaultResistorValues();
  UpdateScaleFactors();

  PowerOnDebounce();

  for (int8_t i = 0; i < 4; ++i)
    CVs.push_back(CellVoltage(i));

  CheckSafety();
    
  for (int i = 0; i < 66; ++i)
    v.Update();
    
  while (1) {
    if (ScrapeUARTRX()) {
      HandleInput();
    }

    if ((cnt & 0x7FFFFFF) == 0x0000000) {
      CalibrateADC();
      v.SetCal(cal);
    }

    if ((cnt & 0xFFFF) == 0x8000) {
      v.Update();
      CheckSafety();
      CellCount = GetCellCount();

      if ((cnt & 0xFFFFF) == 0x88000) {
        UpdatePlan();
        SetInputDecap(state == State::Balancing || state == State::Immediate);
        WriteState();
      }
        
      HandleButton();
      // TODO: if idle, switch to low power / sleep until (RTC?) wakeup interrupt
    }

    // TODO: compute correct limits, then add adjustment
    if ((cnt & 0x1F) == 0x00) {
      RunPlan();
    }
    
    cnt++;
  }

  return 0;
}
