// Wheel revolution counter
// ========================
//
// Battery-powered node that counts exercise-wheel revolutions and reports them
// over an RFM69 radio.
//
//   Hardware (ATmega328PB @ 8 MHz):
//     PD3/INT1  reed switch, closes to ground once per wheel revolution
//     PD2/INT0  MAX31329 RTC alarm output (open drain), fires once per minute
//     SPI1      RFM69 packet radio + M95128 EEPROM (used as a retry spool)
//     I2C       MAX31329 RTC + ST25DV NFC tag (holds "<name>,<diameter>")
//
//   Operation:
//     1. Sleep in power-down. A reed pulse increments the count for the
//        current minute slot; an RTC alarm advances to the next slot.
//     2. Every SEND_INTERVAL minutes, pack the per-minute counts into one
//        radio packet (name, diameter, battery, timestamp, counts, hash).
//     3. If the base station does not acknowledge, spool the packet to
//        EEPROM; whenever a live packet is acknowledged, retry one spooled
//        packet.

#include "adc.h"
#include "defines.h"
#include "interrupts.h"
#include "m95128.h"
#include "max31329.h"
#include "ndef.h"
#include "power_mgmt.h"
#include "rfm69.h"
#include "st25dv.h"
#include "states.h"
#include "uart.h"

#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/sleep.h>
#include <stdbool.h>
#include <util/atomic.h>
#include <util/delay.h>

// How many minute slots are collected before a packet is sent. ITERATING is a
// bench-test mode that sends every minute (and lets the reed switch stand in
// for the minute alarm).
#if ITERATING
#define SEND_INTERVAL 1
#else
#define SEND_INTERVAL 15
#endif

#define WHEEL_COUNT_SLOTS 15

// Erased EEPROM reads back as 0xFF; anything else is a real spool depth.
#define EEPROM_LAST_PAGE_UNINIT 0xFF

// ---------------------------------------------------------------------------
// State shared with the interrupt handlers
// ---------------------------------------------------------------------------

static volatile bool minute_alarm_fired = false; // Set by INT0, consumed by main loop
static volatile bool reed_is_debouncing = false; // Set by INT1, cleared by WDT expiry
static volatile uint8_t minute_slot = 0; // Which wheel_counts[] slot is being filled
static volatile uint16_t wheel_counts[WHEEL_COUNT_SLOTS]; // Revolutions per minute slot

// Whether we ever got a valid timestamp from the base station
static RTC_RFM69_STATUS time_sync_status;

// ---------------------------------------------------------------------------
// Interrupt handlers
//
// Both external interrupts are low-level triggered (the only mode that can
// wake the MCU from power-down), so each handler must mask itself while its
// source still holds the line low. See interrupts.c.
// ---------------------------------------------------------------------------

// RTC minute alarm. The RTC holds INTB low until main reads its flag
// registers, so mask INT0 here; main re-arms it after clearing the flags.
ISR(INT0_vect)
{
    EIMSK &= ~(1 << INT0);
    minute_alarm_fired = true;
    LOG("\t\t\t\tMINUTE INTERRUPT\n");
}

// Reed switch: one revolution. The magnet holds the reed closed far longer
// than one bounce, so mask INT1 for a WDT-timed debounce window; the WDT
// handler below re-arms it.
ISR(INT1_vect)
{
#if ITERATING
    minute_alarm_fired = true;
#endif
    LOG("\t\t\t\tREED INTERRUPT\n");

    if (!reed_is_debouncing) {
        if (minute_slot < WHEEL_COUNT_SLOTS) {
            wheel_counts[minute_slot]++;
        }
        reed_is_debouncing = true;
        EIMSK &= ~(1 << INT1);
        wdt_isr_enable();
    }
}

// Debounce window over: allow the next reed pulse to count.
ISR(WDT_vect)
{
    reed_is_debouncing = false;
    wdt_isr_disable();
    reed_interrupt_enable();
}

// ---------------------------------------------------------------------------
// Power management
// ---------------------------------------------------------------------------

// Cut power to every peripheral and enter power-down until the reed switch,
// the RTC alarm, or the debounce watchdog wakes us.
static void sleep_until_interrupt(void)
{
    spi_eeprom_select(false);
    spi_rfm69_select(false);
    rfid_set_low_power_down(true);
    rfid_set_i2c_power(false);
    ldo_set_state(false);
    _delay_ms(10);

    set_sleep_mode(SLEEP_MODE_PWR_DOWN);

    // avr-libc sleep idiom: test for pending work with interrupts off, and
    // sei() only immediately before sleep_cpu() (the next instruction always
    // executes before any pending interrupt), so an alarm that fired while the
    // rails were dropping cannot be slept through. sleep_bod_disable() is a
    // timed 3-cycle sequence and must sit directly before sleep_cpu().
    cli();
    if (!minute_alarm_fired) {
        sleep_enable();
        sleep_bod_disable();
        sei();
        sleep_cpu();
        sleep_disable();
    }
    sei();
}

// Restore the supplies that sleep_until_interrupt() dropped. Everything on the
// I2C bus is dead until this runs.
static void wake_peripheral_rails(void)
{
    ldo_set_state(true);
    rfid_set_i2c_power(true);
    _delay_ms(1);
}

// ---------------------------------------------------------------------------
// Measurement helpers
// ---------------------------------------------------------------------------

// Battery level via the internal 1.1 V bandgap: the first conversions after
// enabling the ADC read low, so take three and keep the last.
static uint16_t read_battery_level(void)
{
    adc_Enable();
    adc_GetConversion(ADC_CHANNEL_BANDGAP);
    adc_GetConversion(ADC_CHANNEL_BANDGAP);
    uint16_t level = adc_GetConversion(ADC_CHANNEL_BANDGAP);
    adc_Disable();
    return level;
}

// Atomically hand out the collected counts and start the next collection
// period, so a reed pulse landing mid-copy is neither lost nor double-counted.
static void take_counts_snapshot(uint16_t snapshot[WHEEL_COUNT_SLOTS])
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        for (uint8_t c = 0; c < WHEEL_COUNT_SLOTS; c++) {
            snapshot[c] = wheel_counts[c];
            wheel_counts[c] = 0;
        }
        minute_slot = 0;
    }
}

// ---------------------------------------------------------------------------
// Radio reporting
// ---------------------------------------------------------------------------

// Build and send the periodic counts packet. Unacknowledged packets go to the
// EEPROM spool; each acknowledged one buys a retry of one spooled packet.
static void send_wheel_counts_report(void)
{
    rfm69_init();
    rfid_set_low_power_down(false);
    _delay_ms(1);

    // Re-read the tag each period so a renamed nugget takes effect without a
    // reset.
    IDENTIFIER = get_nugget_data();

    if (time_sync_status == RTC_RFM69_SET_TIME_FAILED) {
        time_sync_status = set_time_from_rfm69(IDENTIFIER);
    }

    uint16_t counts_snapshot[WHEEL_COUNT_SLOTS];
    take_counts_snapshot(counts_snapshot);

    reset_txrx_struct(&TX_DATA);
    TX_DATA = generate_wheel_counts_message(
        IDENTIFIER, rtc_read_time(), read_battery_level(), counts_snapshot);
    LOG("TX DATA Sent\n");
#if DO_UART
    uart_print_tx_rx_data(TX_DATA);
#endif

    DATA_SEND_STATUS result = send_message(TX_DATA);
    if (result == DATA_NOT_SENT) {
        LOG("       TX DATA not sent, writing to SPI\n");
        write_struct_to_last_page(TX_DATA);
        return;
    }

    // The base station is listening -- use the chance to drain one packet from
    // the spool.
    if (get_last_page() > 0) {
        reset_txrx_struct(&TX_DATA);
        TX_DATA = read_struct_last_page();
        TX_DATA.flags = MSG_RESENT_COUNTS;
        _delay_ms(250);

        result = send_message(TX_DATA);
        LOG("TX DATA From SPI Memory\n");
#if DO_UART
        uart_print_tx_rx_data(TX_DATA);
#endif

        // Only drop the spooled page once it is actually acknowledged;
        // deleting on failure would lose the data.
        if (result == DATA_SEND_SUCCESS) {
            delete_last_page();
        } else {
            LOG("       SPI not sent\n");
        }
    }
}

// One RTC alarm has fired: clear it, advance the minute slot, and send a
// report if a full period has been collected.
static void handle_minute_alarm(void)
{
    LOG("In minute index\n");

    uint8_t slots_filled;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        if (minute_slot < WHEEL_COUNT_SLOTS) {
            minute_slot += 1;
        }
        slots_filled = minute_slot;
    }

    wake_peripheral_rails();

    // Reading the RTC flag registers releases the (level-triggered) INTB line,
    // after which INT0 can safely be re-armed.
    rtc_read_interrupt_register();
    rtc_read_status_register();
    minute_interrupt_enable();

    if (slots_filled >= SEND_INTERVAL) {
        send_wheel_counts_report();
    }
}

// ---------------------------------------------------------------------------
// Start-up
// ---------------------------------------------------------------------------

static void init_all_hardware(void)
{
    ldo_set_state(true);
    _delay_ms(10);
    init_pins();

#if DO_UART
    uart_init();
    LOG("---- STARTING ----\n");
    uart_wait_until_sent();
#endif

    i2c_init();
    init_spi();
    adc_Initialize();

    set_up_reed_interrupt();
    set_up_minute_interrupt();
    LOG("Set up AVR interrupts\n");

    rtc_set_per_minute_alarm();
    rtc_set_alarm_config();
    rtc_enable_interrupts();
    rtc_read_interrupt_register(); // Clear any alarm already pending
    rtc_read_status_register();
    LOG("Set up RTC interrupts\n");

    rfm69_init();
    LOG("Initialized RFM69\n");

    // Only initialise the spool pointer when it has never been written --
    // clearing it unconditionally would discard every unsent message across a
    // reset.
    if (get_last_page() == EEPROM_LAST_PAGE_UNINIT) {
        write_last_page_value(0);
    }
    LOG("Set up last page value for SPI flash\n");
}

// Five long blinks for a successful time sync, five short ones for a failure.
static void blink_time_sync_result(bool success)
{
    for (uint8_t i = 0; i < 5; i++) {
        led_1_set_state(true);
        if (success) {
            _delay_ms(90);
        } else {
            _delay_ms(10);
        }
        led_1_set_state(false);
        if (success) {
            _delay_ms(10);
        } else {
            _delay_ms(90);
        }
    }
}

int main(void)
{
    init_all_hardware();

    for (uint8_t c = 0; c < WHEEL_COUNT_SLOTS; c++) {
        wheel_counts[c] = 0;
    }

    // The nugget's name and wheel diameter live on the NFC tag
    IDENTIFIER = get_nugget_data();
    LOG("Got nugget data from RFID\n");

    // Ask the base station for the current time and load it into the RTC
    time_sync_status = set_time_from_rfm69(IDENTIFIER);
    blink_time_sync_result(time_sync_status == RTC_RFM69_SET_TIME_SUCCESS);
    LOG(time_sync_status == RTC_RFM69_SET_TIME_SUCCESS ? "Success in get time \n"
                                                       : "Failed to get time \n");

    read_battery_level(); // Throwaway read to settle the ADC path

    while (1) {
        sleep_until_interrupt();

        bool minute_elapsed;
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
        {
            minute_elapsed = minute_alarm_fired;
            minute_alarm_fired = false;
        }

        if (minute_elapsed) {
            handle_minute_alarm();
        }
    }
}
