//! Shared PM_K-1 UI rendering — `no_std`, generic over any `embedded-graphics` `DrawTarget`.
//!
//! The firmware draws this onto the real ST7796; the host simulator (`rust/uisim`) draws the
//! SAME code onto a framebuffer and exports a PNG. So UI/layout is developed and reviewed
//! without the device. `pm-ui` is a pure *view* over borrowed data (no alloc) — the data source
//! (a parsed track + app state) lives elsewhere.

#![no_std]

pub mod notation;
pub use notation::ViewMode;

use embedded_graphics::{
    mono_font::{ascii::{FONT_10X20, FONT_6X10, FONT_9X18_BOLD}, MonoTextStyle},
    pixelcolor::Rgb565,
    prelude::*,
    primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, RoundedRectangle, Triangle},
    text::{Alignment, Text},
};

// ---- palette (0xRRGGBB → Rgb565) ----
const fn rgb(hex: u32) -> Rgb565 {
    Rgb565::new(((hex >> 19) & 0x1F) as u8, ((hex >> 10) & 0x3F) as u8, ((hex >> 3) & 0x1F) as u8)
}
const BG: Rgb565 = rgb(0x06090E);
const PANEL: Rgb565 = rgb(0x141A24);
const TXT: Rgb565 = rgb(0xC7D0DB);
const MUTE: Rgb565 = rgb(0x55606E);
const CYAN: Rgb565 = rgb(0x0AB3F7);
const AMBER: Rgb565 = rgb(0xFF9B2E);
const GREEN: Rgb565 = rgb(0x2FE07A);
const GHOST: Rgb565 = rgb(0x7A5AD0);
const CELL_REST: Rgb565 = rgb(0x1A2330);
const GRID: Rgb565 = rgb(0x26303E);

/// One meter lane to draw.
pub struct LaneView<'a> {
    pub name: &'a str,
    /// per-step dynamics: 0 rest, 1 normal, 2 accent, 3 ghost
    pub levels: &'a [u8],
    /// per-step ornaments: 0 none, 1 flam, 2 drag, 3 roll (parallel to `levels`; may be shorter/empty)
    pub orns: &'a [u8],
    /// group structure (e.g. `[2,2,3]` for 7/8) — drives time-signature + group-aware beaming.
    /// `beats` = sum(groups). Empty → notation defaults to [4].
    pub groups: &'a [u32],
    /// beats per bar (for the group/beat gridlines); 0 = none. Usually `groups.iter().sum()`.
    pub beats: u8,
    pub poly: bool,
    pub muted: bool,
}

/// The full metronome screen state.
pub struct Screen<'a> {
    pub name: &'a str,
    pub bpm: i64,
    pub playing: bool,
    /// progress through the master bar, 0.0..1.0 (drives the playhead column)
    pub phase: f32,
    pub lanes: &'a [LaneView<'a>],
}

fn level_color(l: u8) -> Rgb565 {
    match l {
        2 => AMBER,
        3 => GHOST,
        1 => CYAN,
        _ => CELL_REST,
    }
}

/// Format a small unsigned int into a stack buffer (no alloc).
fn fmt_u32(mut n: u32, buf: &mut [u8; 12]) -> &str {
    if n == 0 {
        buf[11] = b'0';
        return core::str::from_utf8(&buf[11..]).unwrap();
    }
    let mut i = buf.len();
    while n > 0 {
        i -= 1;
        buf[i] = b'0' + (n % 10) as u8;
        n /= 10;
    }
    core::str::from_utf8(&buf[i..]).unwrap()
}

/// Draw the metronome screen. Designed for 320×480 portrait but reads the size from the target.
pub fn draw_metronome<D>(d: &mut D, s: &Screen) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    let h = bb.size.height as i32;
    const M: i32 = 12;

    d.clear(BG)?;

    // ---- header: track name (left) + big BPM (right) ----
    let name_style = MonoTextStyle::new(&FONT_10X20, TXT);
    Text::new(s.name, Point::new(M, 22), name_style).draw(d)?;

    let bpm_style = MonoTextStyle::new(&FONT_9X18_BOLD, CYAN);
    let mut nb = [0u8; 12];
    let bpm_str = fmt_u32(s.bpm.max(0) as u32, &mut nb);
    Text::with_alignment(bpm_str, Point::new(w - M, 18), bpm_style, Alignment::Right).draw(d)?;
    let lbl = MonoTextStyle::new(&FONT_6X10, MUTE);
    Text::with_alignment("BPM", Point::new(w - M, 34), lbl, Alignment::Right).draw(d)?;

    // ---- transport line ----
    let ty = 46;
    if s.playing {
        Triangle::new(Point::new(M, ty - 8), Point::new(M, ty + 4), Point::new(M + 11, ty - 2))
            .into_styled(PrimitiveStyle::with_fill(GREEN))
            .draw(d)?;
    } else {
        Rectangle::new(Point::new(M, ty - 8), Size::new(11, 12)).into_styled(PrimitiveStyle::with_fill(MUTE)).draw(d)?;
    }
    let st = MonoTextStyle::new(&FONT_6X10, if s.playing { GREEN } else { MUTE });
    Text::new(if s.playing { "PLAYING" } else { "STOPPED" }, Point::new(M + 20, ty), st).draw(d)?;

    // divider
    Line::new(Point::new(M, 58), Point::new(w - M, 58)).into_styled(PrimitiveStyle::with_stroke(PANEL, 2)).draw(d)?;

    // ---- lane grid ----
    let grid_top = 70;
    let footer = 16;
    let n = s.lanes.len().max(1) as i32;
    let row_h = (((h - grid_top - footer) / n).min(54)).max(20);
    let label_w = 58;
    let gx0 = M + label_w;
    let gx1 = w - M;
    let gw = (gx1 - gx0).max(1);

    let name_sty_on = MonoTextStyle::new(&FONT_6X10, TXT);
    let name_sty_off = MonoTextStyle::new(&FONT_6X10, MUTE);

    for (i, lane) in s.lanes.iter().enumerate() {
        let ry = grid_top + i as i32 * row_h;
        let cy = ry + 4;
        let ch = row_h - 8;

        // lane name (left)
        let nstyle = if lane.muted { name_sty_off } else { name_sty_on };
        Text::new(lane.name, Point::new(M, ry + row_h / 2 + 4), nstyle).draw(d)?;
        if lane.poly {
            Text::new("~", Point::new(M + label_w - 10, ry + row_h / 2 + 4), MonoTextStyle::new(&FONT_6X10, GHOST)).draw(d)?;
        }

        // cells
        let steps = lane.levels.len().max(1) as i32;
        let cell_w = gw / steps;
        let ph_step = (s.phase * steps as f32) as i32;
        for (sidx, &lvl) in lane.levels.iter().enumerate() {
            let cx = gx0 + sidx as i32 * cell_w;
            let cw = (cell_w - 2).max(1) as u32;
            let col = if lane.muted { CELL_REST } else { level_color(lvl) };
            RoundedRectangle::with_equal_corners(
                Rectangle::new(Point::new(cx, cy), Size::new(cw, ch as u32)),
                Size::new(3, 3),
            )
            .into_styled(PrimitiveStyle::with_fill(col))
            .draw(d)?;
            // playhead highlight on the active step while playing
            if s.playing && sidx as i32 == ph_step {
                RoundedRectangle::with_equal_corners(
                    Rectangle::new(Point::new(cx, cy), Size::new(cw, ch as u32)),
                    Size::new(3, 3),
                )
                .into_styled(PrimitiveStyleBuilder::new().stroke_color(TXT).stroke_width(2).build())
                .draw(d)?;
            }
        }
        // faint beat gridlines across this lane
        if lane.beats > 1 {
            let sub = steps / lane.beats as i32;
            if sub >= 1 {
                for b in 1..lane.beats as i32 {
                    let lx = gx0 + b * sub * cell_w;
                    Line::new(Point::new(lx, cy), Point::new(lx, cy + ch))
                        .into_styled(PrimitiveStyle::with_stroke(GRID, 1))
                        .draw(d)?;
                }
            }
        }
    }

    // ---- footer ----
    let fstyle = MonoTextStyle::new(&FONT_6X10, MUTE);
    Text::new("PM_K-1", Point::new(M, h - 4), fstyle).draw(d)?;
    Ok(())
}

/// Thin bar-position strip the firmware can update cheaply every frame (no full redraw → no
/// flicker). `phase` 0..1 through the bar. Draw it at `top` (draw_metronome leaves y≈60 free).
pub fn draw_progress<D>(d: &mut D, phase: f32, playing: bool, top: i32) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    const M: i32 = 12;
    let bw = w - 2 * M;
    Rectangle::new(Point::new(M, top), Size::new(bw as u32, 5)).into_styled(PrimitiveStyle::with_fill(PANEL)).draw(d)?;
    if playing {
        let fw = ((bw as f32) * phase.clamp(0.0, 1.0)) as u32;
        Rectangle::new(Point::new(M, top), Size::new(fw, 5)).into_styled(PrimitiveStyle::with_fill(GREEN)).draw(d)?;
    }
    Ok(())
}

/// Live state for the peripheral self-test.
pub struct PeriphState {
    pub a: bool,
    pub b: bool,
    pub x: u16, // joystick X, 0..4095
    pub y: u16, // joystick Y, 0..4095
    pub beep: bool,
}

/// Peripheral self-test screen: button states, joystick position + values, speaker activity.
pub fn draw_peripheral_test<D>(d: &mut D, s: &PeriphState) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    d.clear(BG)?;

    Text::new("PERIPHERAL TEST", Point::new(12, 22), MonoTextStyle::new(&FONT_10X20, TXT)).draw(d)?;

    // buttons
    let lab = MonoTextStyle::new(&FONT_10X20, Rgb565::BLACK);
    for (i, (name, on)) in [("A", s.a), ("B", s.b)].iter().enumerate() {
        let bx = 16 + i as i32 * 96;
        Rectangle::new(Point::new(bx, 48), Size::new(84, 84))
            .into_styled(PrimitiveStyle::with_fill(if *on { GREEN } else { PANEL }))
            .draw(d)?;
        Text::with_alignment(name, Point::new(bx + 42, 98), lab, Alignment::Center).draw(d)?;
    }
    // speaker / beep
    let beep_sty = MonoTextStyle::new(&FONT_9X18_BOLD, if s.beep { AMBER } else { MUTE });
    Text::new("SPK", Point::new(216, 78), beep_sty).draw(d)?;
    if s.beep {
        Text::new("BEEP", Point::new(216, 100), MonoTextStyle::new(&FONT_6X10, AMBER)).draw(d)?;
    }

    // joystick box + dot
    let jb_x = 40;
    let jb_y = 170;
    let jb = 200;
    Rectangle::new(Point::new(jb_x, jb_y), Size::new(jb as u32, jb as u32))
        .into_styled(PrimitiveStyleBuilder::new().stroke_color(GRID).stroke_width(2).fill_color(PANEL).build())
        .draw(d)?;
    Line::new(Point::new(jb_x + jb / 2, jb_y), Point::new(jb_x + jb / 2, jb_y + jb)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?;
    Line::new(Point::new(jb_x, jb_y + jb / 2), Point::new(jb_x + jb, jb_y + jb / 2)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?;
    let dx = jb_x + (s.x as i32 * (jb - 12) / 4095) + 6;
    let dy = jb_y + (s.y as i32 * (jb - 12) / 4095) + 6;
    Rectangle::new(Point::new(dx - 6, dy - 6), Size::new(12, 12)).into_styled(PrimitiveStyle::with_fill(CYAN)).draw(d)?;

    // numeric values
    let vs = MonoTextStyle::new(&FONT_6X10, TXT);
    let mut nb = [0u8; 12];
    Text::new("X", Point::new(jb_x, jb_y + jb + 18), vs).draw(d)?;
    Text::new(fmt_u32(s.x as u32, &mut nb), Point::new(jb_x + 16, jb_y + jb + 18), vs).draw(d)?;
    let mut nb2 = [0u8; 12];
    Text::new("Y", Point::new(jb_x + 90, jb_y + jb + 18), vs).draw(d)?;
    Text::new(fmt_u32(s.y as u32, &mut nb2), Point::new(jb_x + 106, jb_y + jb + 18), vs).draw(d)?;

    Text::with_alignment("press A/B = click, move joystick", Point::new(w / 2, jb_y + jb + 44), vs, Alignment::Center).draw(d)?;
    Ok(())
}

// ---- drum notation ----

/// Render one bar of the groove as drum notation (Staff view). Thin wrapper over the ported
/// engine in `notation` — kept for back-compat with existing call sites that defaulted to Staff.
pub fn draw_notation<D>(d: &mut D, s: &Screen) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    notation::draw(d, s, ViewMode::Staff)
}

/// Bring-up diagnostic pattern (kept for hardware bring-up / fallback).
pub fn draw_ui<D>(d: &mut D) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let (w, h) = (bb.size.width as i32, bb.size.height as i32);
    let m: i32 = 36;
    Rectangle::new(Point::zero(), bb.size).into_styled(PrimitiveStyle::with_fill(Rgb565::new(0, 0, 31))).draw(d)?;
    Rectangle::new(Point::zero(), bb.size)
        .into_styled(PrimitiveStyleBuilder::new().stroke_color(Rgb565::RED).stroke_width(8).build())
        .draw(d)?;
    let ms = Size::new(m as u32, m as u32);
    for (x, y, c) in [(0, 0, Rgb565::GREEN), (w - m, 0, Rgb565::YELLOW), (0, h - m, Rgb565::CYAN), (w - m, h - m, Rgb565::MAGENTA)] {
        Rectangle::new(Point::new(x, y), ms).into_styled(PrimitiveStyle::with_fill(c)).draw(d)?;
    }
    let label = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
    Text::new("TL", Point::new(44, 28), label).draw(d)?;
    Text::new("PMK", Point::new(w / 2 - 30, h / 2), label).draw(d)?;
    Ok(())
}
