//! PM track-format codec — Stage 1 of the Rust port (see `docs/rust-port.md`).
//!
//! Pure parse/serialize for the track DSL, validated against
//! `tests/fixtures/track-format.json` — the same golden vectors `engine.js` and
//! `app.py` pass. This is the third implementation; it must agree with them.
//!
//! `no_std` + `alloc`: builds for the RP2350 firmware target
//! (`thumbv8m.main-none-eabihf`) as well as on the host for tests.
#![no_std]

#[macro_use]
extern crate alloc;

use alloc::collections::BTreeSet;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

pub mod schedule;

#[derive(Debug, Clone)]
pub struct Lane {
    pub sound: String,
    pub groups: Vec<u32>,
    pub sub: u32,
    pub swing: bool,
    pub poly: bool,
    pub mute: bool,
    pub gain_db: f64,
    pub levels: Vec<u8>,
    /// per-step ornament parallel to `levels`: 0 none / 1 flam / 2 drag / 3 roll.
    pub orns: Vec<u8>,
}

#[derive(Debug, Clone)]
pub struct Ramp {
    pub start: i64,
    pub amt: i64,
    pub every: i64,
}

#[derive(Debug, Clone)]
pub struct Trainer {
    pub play: i64,
    pub mute: i64,
}

#[derive(Debug, Clone, PartialEq)]
pub enum End {
    Stop,
    Goto(i64), // relative track offset; +1 == "next"
}

#[derive(Debug, Clone)]
pub struct Track {
    pub bpm: i64,
    pub bars: i64,
    pub volume: Option<f64>,
    pub count_ms: i64,
    pub ramp: Option<Ramp>,
    pub trainer: Option<Trainer>,
    pub rep: Option<i64>,
    pub end: Option<End>,
    pub lanes: Vec<Lane>,
}

/// General-MIDI percussion note number -> voice name (matches GM_NUM in engine.js / app.py).
fn gm_num(n: u32) -> Option<&'static str> {
    Some(match n {
        35 | 36 => "kick",
        37 => "rim",
        38 | 40 => "snare",
        39 => "clap",
        41 | 43 => "tomLow",
        42 | 44 => "hatClosed",
        45 | 47 => "tomMid",
        46 => "hatOpen",
        48 | 50 => "tomHigh",
        49 => "crash",
        51 | 53 => "ride",
        54 => "tambourine",
        56 => "cowbell",
        75 => "claves",
        76 | 77 => "woodblock",
        _ => return None,
    })
}

/// Known voices (matches SOUND_GM keys in app.py); anything else falls back to `beep`.
fn known_sound(s: &str) -> bool {
    matches!(
        s,
        "kick" | "kick808" | "kick909" | "snare" | "snare808" | "snare909"
            | "clap" | "clap808" | "clap909" | "rim" | "hatClosed" | "hat808" | "hat909"
            | "hatOpen" | "openHat808" | "ride" | "ride909" | "crash" | "crash909"
            | "tomLow" | "tom808" | "tomMid" | "tomHigh" | "tambourine" | "cowbell"
            | "cowbell808" | "woodblock" | "jamblock" | "claves" | "beep"
    )
}

/// Euclidean distribution: k hits over n steps, rotated by rot (matches euclid() in engine.js).
fn euclid(k: i64, n: i64, rot: i64) -> Vec<u8> {
    let n = n.max(1);
    let k = k.clamp(0, n);
    let rot = ((rot % n) + n) % n;
    (0..n)
        .map(|i| {
            let j = (i + rot) % n;
            if (j * k) % n < k { 1 } else { 0 }
        })
        .collect()
}

/// Pattern char -> (level, ornament). Ornament letters: UPPER = accented, lower = normal
/// (the case carries the dynamic, so dynamics stay orthogonal): f/F flam, d/D drag, z/Z roll.
fn cell(c: char) -> (u8, u8) {
    match c {
        'X' => (2, 0),
        'x' | '1' => (1, 0),
        'g' => (3, 0),
        'f' => (1, 1),
        'F' => (2, 1),
        'd' => (1, 2),
        'D' => (2, 2),
        'z' => (1, 3),
        'Z' => (2, 3),
        _ => (0, 0),
    }
}

/// (level, ornament) -> pattern char (inverse of `cell`).
fn cell_ch(v: u8, o: u8) -> char {
    match o {
        1 => if v >= 2 { 'F' } else { 'f' },
        2 => if v >= 2 { 'D' } else { 'd' },
        3 => if v >= 2 { 'Z' } else { 'z' },
        _ => match v {
            2 => 'X',
            1 => 'x',
            3 => 'g',
            _ => '.',
        },
    }
}

fn parse_lane(tok: &str) -> Lane {
    let poly = tok.contains('~');
    let mute = tok.contains('!');
    let cleaned: String = tok.chars().filter(|&c| c != '~' && c != '!').collect();

    let mut gain_db = 0.0;
    let body = if let Some(at) = cleaned.find('@') {
        gain_db = cleaned[at + 1..].parse::<f64>().unwrap_or(0.0);
        cleaned[..at].to_string()
    } else {
        cleaned
    };

    let (sound0, rest0) = match body.find(':') {
        Some(i) => (body[..i].to_string(), body[i + 1..].to_string()),
        None => (body.clone(), String::new()),
    };
    let mut sound = sound0;
    if let Ok(n) = sound.parse::<u32>() {
        if let Some(name) = gm_num(n) {
            sound = name.to_string();
        }
    }

    let mut rest = rest0;

    // euclid (k[,n[,rot]]) shorthand — pulled before the =/ splits
    let mut euc: Option<Vec<i64>> = None;
    if let Some(lp) = rest.find('(') {
        if let Some(rel) = rest[lp..].find(')') {
            let rp = lp + rel;
            let nums: Vec<i64> = rest[lp + 1..rp]
                .split(',')
                .filter_map(|x| x.trim().parse::<i64>().ok())
                .collect();
            rest = format!("{}{}", &rest[..lp], &rest[rp + 1..]);
            if !nums.is_empty() {
                euc = Some(nums);
            }
        }
    }

    let mut pattern: Option<String> = None;
    if let Some(eq) = rest.find('=') {
        pattern = Some(rest[eq + 1..].to_string());
        rest = rest[..eq].to_string();
    }

    let mut sub: u32 = 1;
    let mut swing = false;
    if let Some(sl) = rest.find('/') {
        let sd = &rest[sl + 1..];
        swing = sd.ends_with('s');
        sub = sd.trim_end_matches('s').parse::<u32>().unwrap_or(1);
        rest = rest[..sl].to_string();
    }

    let mut groups: Vec<u32> = rest.split('+').filter_map(|g| g.parse::<u32>().ok()).collect();
    if groups.is_empty() {
        groups = vec![4];
    }
    let beats: u32 = groups.iter().sum();
    let mut starts = BTreeSet::new();
    let mut acc = 0u32;
    for &g in &groups {
        starts.insert(acc);
        acc += g;
    }

    let (levels, orns): (Vec<u8>, Vec<u8>) = if let Some(e) = euc {
        // euclidean: k hits over n steps, first hit accented (no ornaments)
        let k = e[0];
        let n = if e.len() > 1 { e[1] } else { (beats * sub) as i64 };
        let rot = if e.len() > 2 { e[2] } else { 0 };
        if e.len() > 1 {
            if n % (beats as i64) == 0 {
                sub = (n / beats as i64) as u32;
            } else {
                groups = vec![n as u32];
                sub = 1;
            }
        }
        let mut first = true;
        let lv: Vec<u8> = euclid(k, n, rot)
            .into_iter()
            .map(|h| {
                if h != 0 {
                    let v = if first { 2 } else { 1 };
                    first = false;
                    v
                } else {
                    0
                }
            })
            .collect();
        let orn = vec![0u8; lv.len()];
        (lv, orn)
    } else if let Some(p) = pattern {
        let steps = (beats * sub) as usize;
        let cells: Vec<(u8, u8)> = p.chars().map(cell).collect();
        let mut lv: Vec<u8> = cells.iter().map(|c| c.0).collect();
        let mut orn: Vec<u8> = cells.iter().map(|c| c.1).collect();
        if lv.len() < steps {
            lv.resize(steps, 0);
            orn.resize(steps, 0);
        }
        (lv, orn)
    } else {
        // default: every subdivision sounds at normal, accent only on group starts
        let steps = beats * sub;
        let lv: Vec<u8> = (0..steps)
            .map(|i| {
                if i % sub == 0 {
                    if starts.contains(&(i / sub)) { 2 } else { 1 }
                } else {
                    1
                }
            })
            .collect();
        let orn = vec![0u8; lv.len()];
        (lv, orn)
    };

    if !known_sound(&sound) {
        sound = "beep".to_string();
    }
    Lane { sound, groups, sub, swing, poly, mute, gain_db, levels, orns }
}

pub fn parse(s: &str) -> Track {
    let mut bpm = 120i64;
    let mut bars = 0i64;
    let mut ramp = None;
    let mut trainer = None;
    let mut rep: Option<i64> = None;
    let mut end: Option<End> = None;
    let mut volume: Option<f64> = None;
    let mut count_ms = 0i64;
    let mut lanes = Vec::new();

    for raw in s.trim().split(';') {
        let tok = raw.trim();
        if tok.is_empty() {
            continue;
        }
        if tok.len() > 1 && tok.starts_with('t') && tok[1..].bytes().all(|b| b.is_ascii_digit()) {
            bpm = tok[1..].parse().unwrap_or(120);
            continue;
        }
        if tok.len() > 1 && tok.starts_with('b') && tok[1..].bytes().all(|b| b.is_ascii_digit()) {
            bars = tok[1..].parse().unwrap_or(0);
            continue;
        }
        if tok.starts_with("rmp") {
            let p: Vec<&str> = tok[3..].split('/').collect();
            if p.len() == 3 {
                if let (Ok(a), Ok(b), Ok(c)) =
                    (p[0].parse::<i64>(), p[1].parse::<i64>(), p[2].parse::<i64>())
                {
                    ramp = Some(Ramp { start: a, amt: b, every: c.max(1) });
                }
            }
            continue;
        }
        if tok.starts_with("tr") && tok.contains('/') && !tok.contains(':') {
            let p: Vec<&str> = tok[2..].split('/').collect();
            if p.len() == 2 {
                if let (Ok(a), Ok(b)) = (p[0].parse::<i64>(), p[1].parse::<i64>()) {
                    trainer = Some(Trainer { play: a.max(0), mute: b.max(0) });
                }
            }
            continue;
        }
        if tok.starts_with("vol") {
            volume = Some(tok[3..].parse::<i64>().unwrap_or(0) as f64 / 100.0);
            continue;
        }
        if tok.starts_with("cd") {
            count_ms = tok[2..].parse::<i64>().unwrap_or(0) * 1000;
            continue;
        }
        if tok.starts_with("rep=") {
            rep = Some(tok[4..].parse::<i64>().unwrap_or(1).max(1));
            continue;
        }
        if tok.starts_with("end=") {
            let v = &tok[4..];
            end = Some(if v == "stop" {
                End::Stop
            } else if v == "next" {
                End::Goto(1)
            } else {
                End::Goto(v.parse::<i64>().unwrap_or(0))
            });
            continue;
        }
        if !tok.contains(':') {
            continue;
        }
        lanes.push(parse_lane(tok));
    }
    if lanes.is_empty() {
        lanes.push(parse_lane("beep:4"));
    }
    Track {
        bpm: bpm.clamp(5, 300),
        bars,
        volume,
        count_ms,
        ramp,
        trainer,
        rep,
        end,
        lanes,
    }
}

fn lane_to_str(l: &Lane) -> String {
    let mut s = format!(
        "{}:{}",
        l.sound,
        l.groups.iter().map(|g| g.to_string()).collect::<Vec<_>>().join("+")
    );
    if l.sub != 1 || l.swing {
        s.push_str(&format!("/{}{}", l.sub, if l.swing { "s" } else { "" }));
    }
    s.push('=');
    s.extend(l.levels.iter().enumerate().map(|(i, &v)| cell_ch(v, l.orns.get(i).copied().unwrap_or(0))));
    if l.gain_db != 0.0 {
        s.push_str(&format!("@{}", l.gain_db));
    }
    if l.poly {
        s.push('~');
    }
    if l.mute {
        s.push('!');
    }
    s
}

pub fn serialize(t: &Track) -> String {
    let mut parts = vec![format!("t{}", t.bpm)];
    if let Some(v) = t.volume {
        parts.push(format!("vol{}", (v * 100.0 + 0.5) as i64)); // no_std-safe round (volume is 0..1)
    }
    if t.count_ms > 0 {
        parts.push(format!("cd{}", t.count_ms / 1000));
    }
    if t.bars != 0 {
        parts.push(format!("b{}", t.bars));
    }
    if let Some(r) = &t.ramp {
        parts.push(format!("rmp{}/{}/{}", r.start, r.amt, r.every));
    }
    if let Some(tr) = &t.trainer {
        parts.push(format!("tr{}/{}", tr.play, tr.mute));
    }
    for l in &t.lanes {
        parts.push(lane_to_str(l));
    }
    if let Some(e) = &t.end {
        if let Some(rep) = t.rep {
            if rep > 1 {
                parts.push(format!("rep={}", rep));
            }
        }
        parts.push(match e {
            End::Stop => "end=stop".to_string(),
            End::Goto(1) => "end=next".to_string(),
            End::Goto(n) if *n > 0 => format!("end=+{}", n),
            End::Goto(n) => format!("end={}", n),
        });
    }
    parts.join(";")
}
