//! PM_K-1 firmware — Stage 3 bring-up.
//! Milestone 1 (done): blink GP25 → proved boot/flash.
//! Milestone 2 (this): init the ST7796 320×480 display over SPI0 and draw to it.
//! Pins (from the CircuitPython firmware): SCK=GP2, MOSI=GP3, CS=GP5, DC=GP6, RST=GP7;
//! BGR panel, colours inverted. LED on GP25 keeps blinking as a heartbeat.

#![no_std]
#![no_main]

extern crate alloc;
use alloc::vec::Vec;
use embedded_alloc::LlffHeap as Heap;

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

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal::pwm::SetDutyCycle;
use embedded_hal_0_2::adc::OneShot;
use embedded_hal::spi::SpiBus;
use defmt::info;
use defmt_rtt as _; // global defmt logger over RTT (read by `probe-rs run`)
use panic_probe as _; // panic handler that prints the message + location over defmt, then halts
use rp235x_hal as hal;
use hal::fugit::RateExtU32;
use hal::Clock;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::Rectangle;

/// Image definition block — the RP2350 bootrom looks for this to boot the image.
#[link_section = ".start_block"]
#[used]
pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe();

const XTAL_FREQ_HZ: u32 = 12_000_000;
const WIDTH: u16 = 320;
const HEIGHT: u16 = 480;

/// Dirty-tile grid for incremental updates. TILE wide is sub-screen-width so each tile blit writes
/// faster than the panel scan crosses it → minimal tearing. 320/40=8 cols, 480/40=12 rows = 96 tiles.
const TILE: u16 = 40;
const NX: usize = WIDTH as usize / TILE as usize;
const NY: usize = HEIGHT as usize / TILE as usize;

/// Direct ST7796 driver — a faithful port of the proven MicroPython driver in `pico/main.py`,
/// which runs this exact panel on this exact board. Per-command CS framing (CS low → command →
/// data → CS high), MADCTL 0x48, INVON, NO row/column offset, big-endian RGB565. This replaces
/// mipidsi, whose split-transaction CS and orientation/offset math were mangling the geometry.
/// We render into the RAM `FrameBuf` (embedded-graphics), then `blit` the whole frame in one
/// windowed RAMWR stream — exactly like main.py's `_window` + `spi.write`.
struct St7796<SPI, DC, CS, RST> {
    spi: SPI,
    dc: DC,
    cs: CS,
    rst: RST,
}
impl<SPI, DC, CS, RST> St7796<SPI, DC, CS, RST>
where
    SPI: SpiBus,
    DC: OutputPin,
    CS: OutputPin,
    RST: OutputPin,
{
    fn cmd(&mut self, c: u8, data: &[u8]) {
        let _ = self.cs.set_low();
        let _ = self.dc.set_low();
        let _ = self.spi.write(&[c]);
        if !data.is_empty() {
            let _ = self.dc.set_high();
            let _ = self.spi.write(data);
        }
        let _ = self.cs.set_high();
    }

    /// main.py reset() + init() verbatim.
    fn init(&mut self, delay: &mut impl DelayNs) {
        let _ = self.cs.set_high();
        let _ = self.dc.set_low();
        let _ = self.rst.set_high();
        delay.delay_ms(20);
        let _ = self.rst.set_low();
        delay.delay_ms(40);
        let _ = self.rst.set_high();
        delay.delay_ms(150);

        self.cmd(0x01, &[]); // SWRESET
        delay.delay_ms(120);
        self.cmd(0x11, &[]); // SLPOUT
        delay.delay_ms(120);
        self.cmd(0xF0, &[0xC3]); // unlock extension command set
        self.cmd(0xF0, &[0x96]);
        self.cmd(0x36, &[0x48]); // MADCTL = MX | BGR
        self.cmd(0x3A, &[0x55]); // COLMOD 16bpp
        self.cmd(0xB4, &[0x01]);
        self.cmd(0xB6, &[0x80, 0x02, 0x3B]);
        self.cmd(0xE8, &[0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]);
        self.cmd(0xC1, &[0x06]);
        self.cmd(0xC2, &[0xA7]);
        self.cmd(0xC5, &[0x18]);
        delay.delay_ms(120);
        self.cmd(0xE0, &[0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]);
        self.cmd(0xE1, &[0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]);
        self.cmd(0xF0, &[0x3C]); // lock
        self.cmd(0xF0, &[0x69]);
        delay.delay_ms(120);
        self.cmd(0x21, &[]); // INVON (INVERT_COLORS = true)
        self.cmd(0x29, &[]); // DISPON
        delay.delay_ms(50);
    }

    /// Blit a full-width band of rows `ylo..ylo+h`. CASET is the full width (0..=319, exactly like
    /// the full-screen blit), only RASET is partial — and because a full-width band is *contiguous*
    /// in the row-major framebuffer, `px` is just that contiguous slice, streamed with the identical
    /// loop the full blit uses. The full-screen blit is `blit_band(&px[..], 0, HEIGHT)`.
    fn blit_band(&mut self, px: &[Rgb565], ylo: u16, h: u16) {
        let y1 = ylo + h - 1;
        self.cmd(0x2A, &[0x00, 0x00, 0x01, 0x3F]); // CASET 0..=319 (full width)
        self.cmd(0x2B, &[(ylo >> 8) as u8, (ylo & 0xFF) as u8, (y1 >> 8) as u8, (y1 & 0xFF) as u8]);
        let _ = self.cs.set_low();
        let _ = self.dc.set_low();
        let _ = self.spi.write(&[0x2C]); // RAMWR
        let _ = self.dc.set_high();
        let mut buf = [0u8; 1024];
        let mut n = 0;
        for &c in px {
            let raw = c.into_storage(); // u16 RGB565, big-endian like main.py
            buf[n] = (raw >> 8) as u8;
            buf[n + 1] = (raw & 0xFF) as u8;
            n += 2;
            if n == buf.len() {
                let _ = self.spi.write(&buf);
                n = 0;
            }
        }
        if n > 0 {
            let _ = self.spi.write(&buf[..n]);
        }
        let _ = self.cs.set_high();
    }

    /// Full-screen blit — the known-good path. Just a full-height band.
    fn blit(&mut self, px: &[Rgb565]) {
        self.blit_band(px, 0, HEIGHT);
    }

    /// Blit a sub-rectangle `(x,y,w,h)` gathered from the full framebuffer. Used for small tile
    /// updates: sub-width writes finish a region faster than the panel scan crosses it (datasheet
    /// §10.8 Example 1), so the visible tear is tiny vs a full-width band. Same streaming loop and
    /// 1024-byte buffer as `blit_band` (the 512-byte buffer was what broke the earlier version).
    fn blit_rect(&mut self, fb: &[Rgb565], x: u16, y: u16, w: u16, h: u16) {
        let x1 = x + w - 1;
        let y1 = y + h - 1;
        self.cmd(0x2A, &[(x >> 8) as u8, (x & 0xFF) as u8, (x1 >> 8) as u8, (x1 & 0xFF) as u8]);
        self.cmd(0x2B, &[(y >> 8) as u8, (y & 0xFF) as u8, (y1 >> 8) as u8, (y1 & 0xFF) as u8]);
        let _ = self.cs.set_low();
        let _ = self.dc.set_low();
        let _ = self.spi.write(&[0x2C]); // RAMWR
        let _ = self.dc.set_high();
        let mut buf = [0u8; 1024];
        let mut n = 0;
        for r in 0..h as usize {
            let row = (y as usize + r) * WIDTH as usize + x as usize;
            for c in 0..w as usize {
                let raw = fb[row + c].into_storage();
                buf[n] = (raw >> 8) as u8;
                buf[n + 1] = (raw & 0xFF) as u8;
                n += 2;
                if n == buf.len() {
                    let _ = self.spi.write(&buf);
                    n = 0;
                }
            }
        }
        if n > 0 {
            let _ = self.spi.write(&buf[..n]);
        }
        let _ = self.cs.set_high();
    }
}


/// Off-screen framebuffer: a full 320×480 Rgb565 image in RAM (300 KB). The pm-ui draw functions
/// render into THIS (instant, no SPI), then we push the whole thing to the panel in one
/// `fill_contiguous` sweep — so the panel never shows a clear-then-redraw transition → no flicker.
const FB_LEN: usize = WIDTH as usize * HEIGHT as usize;
static mut FRAMEBUF: [Rgb565; FB_LEN] = [Rgb565::new(0, 0, 0); FB_LEN];

struct FrameBuf {
    px: &'static mut [Rgb565],
}
impl DrawTarget for FrameBuf {
    type Color = Rgb565;
    type Error = core::convert::Infallible;
    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Pixel<Self::Color>>,
    {
        for Pixel(p, c) in pixels {
            if p.x >= 0 && p.y >= 0 && (p.x as u16) < WIDTH && (p.y as u16) < HEIGHT {
                self.px[p.y as usize * WIDTH as usize + p.x as usize] = c;
            }
        }
        Ok(())
    }
    fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Self::Color>,
    {
        // fast path used by clear()/fill — write straight into the backing slice row by row
        let mut it = colors.into_iter();
        let x0 = area.top_left.x.max(0) as u16;
        let y0 = area.top_left.y.max(0) as u16;
        let x1 = (area.top_left.x + area.size.width as i32).min(WIDTH as i32) as u16;
        let y1 = (area.top_left.y + area.size.height as i32).min(HEIGHT as i32) as u16;
        for y in y0..y1 {
            for x in x0..x1 {
                if let Some(c) = it.next() {
                    self.px[y as usize * WIDTH as usize + x as usize] = c;
                }
            }
        }
        Ok(())
    }
}
impl OriginDimensions for FrameBuf {
    fn size(&self) -> Size {
        Size::new(WIDTH as u32, HEIGHT as u32)
    }
}

#[hal::entry]
fn main() -> ! {
    // heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
    const HEAP_SIZE: usize = 16 * 1024;
    {
        use core::mem::MaybeUninit;
        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) }
    }
    info!("== pm-kit boot == heap {}KB, {} free", HEAP_SIZE / 1024, HEAP.free());

    let mut pac = hal::pac::Peripherals::take().unwrap();
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut timer = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks);
    let sio = hal::Sio::new(pac.SIO);
    let pins = hal::gpio::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);

    let mut led = pins.gpio25.into_push_pull_output();
    led.set_high().unwrap(); // solid ON during init: if it stays solid → hung in init; slow blink → reached the loop

    // --- ST7796 over SPI0 (direct driver, ported from pico/main.py) ---
    let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
    let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
    let dc = pins.gpio6.into_push_pull_output();
    let rst = pins.gpio7.into_push_pull_output();
    let cs = pins.gpio5.into_push_pull_output();

    let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
    let spi = spi.init(
        &mut pac.RESETS,
        clocks.peripheral_clock.freq(),
        62500.kHz(), // pico-cp SPI_BAUD: 62.5 MHz — "faster SPI = smaller tearing window" (drop to 40 MHz if unstable)
        embedded_hal::spi::MODE_0,
    );

    let mut st = St7796 { spi, dc, cs, rst };
    st.init(&mut timer);
    info!("display init ok (ported st7796 driver), {} free", HEAP.free());

    // ---- inputs + speaker ----
    let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop
    let mut btn_b = pins.gpio14.into_pull_up_input(); // B = grid/notation view
    let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS);
    let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap();
    let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap();
    let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
    let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B
    spk.set_div_int(125);
    spk.set_top(600); // ~2 kHz click
    spk.enable();
    spk.channel_b.output_to(pins.gpio13);
    info!("scaffolding (buttons/adc/pwm) up, {} free", HEAP.free());

    // ---- built-in grooves ----
    const GROOVES: [&str; 4] = [
        "t120;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2=xxxxxxxx",
        "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x",
        "t140;kick:4=X..x;snare:4=.X.X;hatClosed:4/4=xxxxxxxxxxxxxxxx",
        "t100;kick:4=Xxxx;claves:5=Xxxxx~",
    ];
    const NAMES: [&str; 4] = ["Four on the floor", "Half-time shuffle", "Driving 16ths", "5 over 4"];

    let mut idx = 0usize;
    let mut track = track_format::parse(GROOVES[idx]);
    info!("parsed groove 0: bpm={} lanes={}, {} free", track.bpm, track.lanes.len(), HEAP.free());
    let mut tempo: i64 = track.bpm;
    let mut playing = true;
    // View cycle on button-B: Grid → Staff → TUBS → Konnakol → Grid …
    #[derive(Clone, Copy, PartialEq)]
    enum View {
        Grid,
        Staff,
        Tubs,
        Konnakol,
    }
    let mut view = View::Grid;

    // double-buffer: draw into the RAM framebuffer, then push only the full-width ROW BANDS that
    // changed vs the last frame. Full-width windows (CASET 0..319) behave exactly like the proven
    // full-screen blit — unlike sub-width tiles, which the panel's MX bit mirror-maps wrongly.
    let mut fb = FrameBuf { px: unsafe { &mut *core::ptr::addr_of_mut!(FRAMEBUF) } };
    let mut tile_hash = [0u64; NX * NY]; // FNV-1a of each tile at its last blit (0 → force first paint)

    let mut bar_start = timer.get_counter().ticks();
    let mut last_step = usize::MAX; // audio: click on a new step
    let mut drawn_step = usize::MAX; // draw: advance the playhead
    let mut click_off_us: u64 = 0;
    let (mut pa, mut pb) = (false, false);
    let mut joy_zone = 0i8;
    let mut dirty = true; // force the first frame
    let mut force_full = true; // first paint + big changes use the known-good full blit
    let mut hb = false;
    let mut hb_us = 0u64;
    led.set_low().unwrap();
    info!("entering metronome loop (double-buffered)");

    loop {
        let now = timer.get_counter().ticks();

        // ---- inputs ----
        let a = btn_a.is_low().unwrap_or(false);
        let b = btn_b.is_low().unwrap_or(false);
        if a && !pa {
            playing = !playing;
            if playing {
                bar_start = now;
                last_step = usize::MAX;
            }
            dirty = true;
        }
        if b && !pb {
            view = match view {
                View::Grid => View::Staff,
                View::Staff => View::Tubs,
                View::Tubs => View::Konnakol,
                View::Konnakol => View::Grid,
            };
            dirty = true;
            force_full = true; // whole screen changes — use the clean full blit
        }
        pa = a;
        pb = b;

        let jx = adc.read(&mut joy_x).unwrap_or(2048);
        let jy = adc.read(&mut joy_y).unwrap_or(2048);
        // rotate the joystick reading 90° CCW for control
        let rx = jy as i32;
        let ry = 4095 - jx as i32;
        let zone: i8 = if ry > 3200 { 1 } else if ry < 900 { 2 } else if rx > 3200 { 3 } else if rx < 900 { 4 } else { 0 };
        if zone != 0 && joy_zone == 0 {
            match zone {
                1 => tempo = (tempo + 4).min(300),                 // up → tempo+
                2 => tempo = (tempo - 4).max(30),                  // down → tempo-
                3 => {
                    idx = (idx + 1) % GROOVES.len();               // right → next groove
                    track = track_format::parse(GROOVES[idx]);
                    tempo = track.bpm;
                    bar_start = now;
                    last_step = usize::MAX;
                }
                4 => {
                    idx = (idx + GROOVES.len() - 1) % GROOVES.len(); // left → prev
                    track = track_format::parse(GROOVES[idx]);
                    tempo = track.bpm;
                    bar_start = now;
                    last_step = usize::MAX;
                }
                _ => {}
            }
            dirty = true;
            if zone == 3 || zone == 4 {
                force_full = true; // groove change → new lanes, use the clean full blit
            }
        }
        joy_zone = zone;

        // ---- clock ----
        let master = &track.lanes[0];
        let msteps = master.levels.len().max(1) as u64;
        let beats = master.groups.iter().map(|&g| g as u64).sum::<u64>().max(1);
        let bar_us = 60_000_000u64 * beats / tempo.max(1) as u64;
        let step_us = (bar_us / msteps).max(1);
        let elapsed = now.wrapping_sub(bar_start) % bar_us;
        let cur_step = (elapsed / step_us) as usize;
        let phase = elapsed as f32 / bar_us as f32;

        // ---- audio: click on a new step that has a hit ----
        if playing && cur_step != last_step {
            let lvl = master.levels[cur_step.min(msteps as usize - 1)];
            if lvl > 0 {
                let _ = spk.channel_b.set_duty_cycle(if lvl == 2 { 380 } else { 240 });
                click_off_us = now + if lvl == 2 { 28_000 } else { 14_000 };
            }
            last_step = cur_step;
        }
        if now >= click_off_us {
            let _ = spk.channel_b.set_duty_cycle(0);
        }

        // redraw when the playhead steps to a new cell (animation), or on any state change
        if playing && cur_step != drawn_step {
            dirty = true;
        }

        // ---- draw into the framebuffer, then blit ONCE (flicker-free) ----
        if dirty {
            let lanes: Vec<pm_ui::LaneView> = track
                .lanes
                .iter()
                .map(|l| pm_ui::LaneView {
                    name: &l.sound,
                    levels: &l.levels,
                    orns: &l.orns,
                    groups: &l.groups,
                    beats: l.groups.iter().map(|&g| g as u32).sum::<u32>().min(255) as u8,
                    poly: l.poly,
                    muted: l.mute,
                })
                .collect();
            let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes };
            match view {
                View::Grid => pm_ui::draw_metronome(&mut fb, &screen).ok(),
                View::Staff => pm_ui::notation::draw(&mut fb, &screen, pm_ui::ViewMode::Staff).ok(),
                View::Tubs => pm_ui::notation::draw(&mut fb, &screen, pm_ui::ViewMode::Tubs).ok(),
                View::Konnakol => pm_ui::notation::draw(&mut fb, &screen, pm_ui::ViewMode::Konnakol).ok(),
            };
            // FNV-1a of one TILE×TILE block at grid cell (tx, ty)
            fn tile_fnv(px: &[Rgb565], tx: usize, ty: usize) -> u64 {
                let mut h = 0xcbf29ce484222325u64; // FNV-1a offset basis
                let x0 = tx * TILE as usize;
                let y0 = ty * TILE as usize;
                for r in 0..TILE as usize {
                    let base = (y0 + r) * WIDTH as usize + x0;
                    for c in 0..TILE as usize {
                        h ^= px[base + c].into_storage() as u64;
                        h = h.wrapping_mul(0x100000001b3);
                    }
                }
                h
            }
            if force_full {
                // known-good full-screen blit; seed the tile hashes so increments diff against it
                st.blit(&fb.px[..]);
                for ty in 0..NY {
                    for tx in 0..NX {
                        tile_hash[ty * NX + tx] = tile_fnv(&fb.px[..], tx, ty);
                    }
                }
                force_full = false;
                info!("FULL blit (seeded {} tiles)", (NX * NY) as u32);
            } else {
                // ---- dirty-tile blit: push only the changed TILE×TILE blocks (sub-width = less tearing) ----
                let mut pushed = 0u32;
                for ty in 0..NY {
                    for tx in 0..NX {
                        let h = tile_fnv(&fb.px[..], tx, ty);
                        let idx = ty * NX + tx;
                        if h != tile_hash[idx] {
                            tile_hash[idx] = h;
                            st.blit_rect(&fb.px[..], (tx * TILE as usize) as u16, (ty * TILE as usize) as u16, TILE, TILE);
                            pushed += 1;
                        }
                    }
                }
                if pushed > 0 {
                    info!("blit {} / {} tiles", pushed, (NX * NY) as u32);
                }
            }
            drawn_step = cur_step;
            dirty = false;
        }

        // heartbeat LED + log (~2 Hz)
        if now.wrapping_sub(hb_us) > 500_000 {
            hb = !hb;
            let _ = if hb { led.set_high() } else { led.set_low() };
            info!("alive: idx={} step={} playing={} free={}", idx, cur_step, playing, HEAP.free());
            hb_us = now;
        }
        timer.delay_ms(4);
    }
}

/// picotool metadata (visible via `picotool info`).
#[link_section = ".bi_entries"]
#[used]
pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 1] =
    [hal::binary_info::rp_program_name!(c"pm-kit display")];
