//! PM Daisy-Pod spike — play the PolyMeter click engine on real hardware.
//!
//! Drives the shared [`pm_synth::Player`] (the SAME sequencer the host `synthrender` renders to
//! `pm-daisy-preview.wav`) from the Daisy Seed's SAI audio DMA interrupt, out the Daisy Pod's audio
//! jack. The engine is mono; both stereo channels get the same sample. The Seed's USER LED flashes
//! on every click as a beat indicator.
//!
//! This is a digital/DSP-home experiment (see `docs/daisy-spike.md`), not the heirloom analog path.
//! The audio loop structure follows the `daisy` crate's `examples/audio.rs`.
//!
//! Build/flash: see `build.sh` and `README.md`. The board REVISION feature (Cargo.toml) must match
//! your Seed's codec or you get silence.

#![no_main]
#![no_std]

extern crate alloc;

use core::cell::RefCell;
use core::mem::MaybeUninit;
use core::sync::atomic::{AtomicU32, Ordering};

use cortex_m::asm;
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use defmt_rtt as _;
use panic_probe as _;

use embedded_alloc::LlffHeap as Heap;
use hal::pac::{self, interrupt};
use stm32h7xx_hal as hal;

use daisy::audio;
use pm_synth::{Player, SPIKE_BARS, SPIKE_PROGRAM};

#[global_allocator]
static HEAP: Heap = Heap::empty();

/// The running groove and the audio peripheral live in globals so the DMA audio interrupt can reach
/// them. Both are installed once in `main` (inside a critical section) before audio starts.
static PLAYER: Mutex<RefCell<Option<Player>>> = Mutex::new(RefCell::new(None));
static AUDIO_INTERFACE: Mutex<RefCell<Option<audio::Interface>>> = Mutex::new(RefCell::new(None));

/// Total clicks fired, published from the audio IRQ. The main loop watches it to flash the beat LED.
static FIRED: AtomicU32 = AtomicU32::new(0);

#[entry]
fn main() -> ! {
    // Heap first: `Player::new` parses the program into Vec/String, and each `trigger()` allocates a
    // voice. 64 KB lives comfortably in the 128 KB DTCM. (Allocating inside the audio IRQ is a known
    // real-time smell — fine for the spike; see docs/daisy-spike.md "Decision criteria".)
    {
        const HEAP_SIZE: usize = 64 * 1024;
        static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
        unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
    }

    let mut cp = cortex_m::Peripherals::take().unwrap();
    let dp = pac::Peripherals::take().unwrap();

    // Caches give a major DSP performance boost; the `daisy` crate handles cache management around
    // the audio DMA buffers for us.
    cp.SCB.enable_icache();
    cp.SCB.enable_dcache(&mut cp.CPUID);

    // Board bring-up (clocks, GPIO, LED, audio) via the daisy crate's macros.
    let board = daisy::Board::take().unwrap();
    let ccdr = daisy::board_freeze_clocks!(board, dp);
    let pins = daisy::board_split_gpios!(board, ccdr, dp);
    let mut led_user = daisy::board_split_leds!(pins).USER;
    let audio_interface = daisy::board_split_audio!(ccdr, pins);

    // Build the groove from the shared spike pattern.
    let track = track_format::parse(SPIKE_PROGRAM);
    let player = Player::new(&track, SPIKE_BARS);
    defmt::info!("pm-daisy: playing `{}` ({=i64} bars), heap {=usize} B free", SPIKE_PROGRAM, SPIKE_BARS, HEAP.free());

    // Start audio and hand the peripheral + player to the interrupt.
    let audio_interface = audio_interface.spawn().unwrap();
    cortex_m::interrupt::free(|cs| {
        PLAYER.borrow(cs).replace(Some(player));
        AUDIO_INTERFACE.borrow(cs).replace(Some(audio_interface));
    });

    // Beat LED: a ~25 ms flash whenever the click count advances. Audio is fully interrupt-driven,
    // so blocking the main loop with `asm::delay` is harmless.
    let ms = ccdr.clocks.sys_ck().to_Hz() / 1000;
    let mut last = 0u32;
    loop {
        let fired = FIRED.load(Ordering::Relaxed);
        if fired != last {
            last = fired;
            led_user.set_high();
            asm::delay(ms * 25);
            led_user.set_low();
        }
        asm::delay(ms); // ~1 ms poll cadence
    }
}

/// Audio is transferred to/from the codec periodically over DMA. When a transfer completes, the
/// DMA1 Stream 1 interrupt fires asking for the next block — we fill it from the engine.
#[interrupt]
fn DMA1_STR1() {
    cortex_m::interrupt::free(|cs| {
        let mut ai = AUDIO_INTERFACE.borrow(cs).borrow_mut();
        let mut pl = PLAYER.borrow(cs).borrow_mut();
        if let (Some(audio_interface), Some(player)) = (ai.as_mut(), pl.as_mut()) {
            audio_interface
                .handle_interrupt_dma1_str1(|audio_buffer| {
                    for frame in audio_buffer {
                        let s = player.next_sample(); // mono engine sample
                        *frame = (s, s); // → left + right
                    }
                })
                .unwrap();
            // Publish the click count for the main-loop beat LED.
            FIRED.store(player.fired(), Ordering::Relaxed);
        }
    });
}
