///|
/// Pure MoonBit Music Macro Language (MML) Parser and Melody Compiler.
/// Supports classic retro game MML syntax (e.g. "T140 O4 C8 E8 G8 > C4").

///|
/// Helper to extract Char at index from String.
fn char_at(s : String, idx : Int32) -> Char {
  s[idx].to_int().unsafe_to_char()
}

///|
/// Parses an unsigned integer from a string starting at index `start`.
/// Returns a tuple of (parsed_value, next_index).
fn parse_int_digits(s : String, start : Int32) -> (Int32, Int32) {
  let mut val = 0
  let mut idx = start
  let mut found = false
  while idx < s.length() {
    let c = char_at(s, idx)
    if c >= '0' && c <= '9' {
      let digit = c.to_int() - '0'.to_int()
      val = val * 10 + digit
      idx = idx + 1
      found = true
    } else {
      break
    }
  }
  if found {
    (val, idx)
  } else {
    (0, start)
  }
}

///|
/// Maps a numeric duration value (1, 2, 4, 8, 16) to the Duration enum.
fn number_to_duration(num : Int32, default_dur : Duration) -> Duration {
  match num {
    1 => Whole
    2 => Half
    4 => Quarter
    8 => Eighth
    16 => Sixteenth
    _ => default_dur
  }
}

///|
/// Parses note pitch class from note letter and accidental ('#' / '+' / '-').
fn parse_pitch(letter : Char, accidental : Char) -> Note {
  match letter {
    'C' | 'c' => if accidental == '#' || accidental == '+' { Cs } else { C }
    'D' | 'd' =>
      if accidental == '#' || accidental == '+' {
        Ds
      } else if accidental == '-' {
        Cs
      } else {
        D
      }
    'E' | 'e' => if accidental == '-' { Ds } else { E }
    'F' | 'f' => if accidental == '#' || accidental == '+' { Fs } else { F }
    'G' | 'g' =>
      if accidental == '#' || accidental == '+' {
        Gs
      } else if accidental == '-' {
        Fs
      } else {
        G
      }
    'A' | 'a' =>
      if accidental == '#' || accidental == '+' {
        As
      } else if accidental == '-' {
        Gs
      } else {
        A
      }
    'B' | 'b' => if accidental == '-' { As } else { B }
    _ => Rest
  }
}

///|
/// Parses an MML string into a Score and tempo BPM.
///
/// Supported MML Syntax:
/// - `T`: Sets playback tempo (e.g. `T140`). Default: 120.
/// - `O`: Sets octave 1..7 (e.g. `O4`). Default: 4.
/// - `>`: Octave up by 1.
/// - `<`: Octave down by 1.
/// - `L`: Sets default note duration: 1 (Whole), 2 (Half), 4 (Quarter), 8 (Eighth), 16 (Sixteenth). Default: 4.
/// - `V`: Sets volume 0..100 (e.g. `V80` -> 0.8). Default: 80.
/// - Notes `C, D, E, F, G, A, B`:
///   - Optional accidentals: `#` or `+` (sharp), `-` (flat)
///   - Optional explicit duration: e.g. `C8`, `F#16`, `D-2`, `A1`
/// - Rests: `R` or `P` with optional duration (e.g. `R4`, `R8`).
/// - Whitespace and bar dividers `|`, `,`, `;` are ignored.
pub fn parse_mml(mml : String) -> (Score, Int32) {
  let score = Score::new()
  let len = mml.length()

  let mut bpm = 120
  let mut octave = 4
  let mut default_dur = Duration::Quarter
  let mut volume = 0.8

  let mut i = 0
  while i < len {
    let ch = char_at(mml, i)

    // Skip whitespace, separators, and bar lines
    if ch == ' ' ||
      ch == '\t' ||
      ch == '\n' ||
      ch == '\r' ||
      ch == '|' ||
      ch == ',' ||
      ch == ';' {
      i = i + 1
      continue
    }

    match ch {
      // Tempo
      'T' | 't' => {
        let (val, next_i) = parse_int_digits(mml, i + 1)
        if val > 0 {
          bpm = val
        }
        i = next_i
      }

      // Octave
      'O' | 'o' => {
        let (val, next_i) = parse_int_digits(mml, i + 1)
        if val >= 1 && val <= 7 {
          octave = val
        }
        i = next_i
      }

      // Octave Up
      '>' => {
        octave = if octave < 7 { octave + 1 } else { 7 }
        i = i + 1
      }

      // Octave Down
      '<' => {
        octave = if octave > 1 { octave - 1 } else { 1 }
        i = i + 1
      }

      // Default Length
      'L' | 'l' => {
        let (val, next_i) = parse_int_digits(mml, i + 1)
        if val > 0 {
          default_dur = number_to_duration(val, default_dur)
        }
        i = next_i
      }

      // Volume (0..100)
      'V' | 'v' => {
        let (val, next_i) = parse_int_digits(mml, i + 1)
        if val >= 0 {
          let v = val.to_double() / 100.0
          volume = if v > 1.0 { 1.0 } else { v }
        }
        i = next_i
      }

      // Rest (Silence)
      'R' | 'r' | 'P' | 'p' => {
        let (val, next_i) = parse_int_digits(mml, i + 1)
        let dur = if val > 0 {
          number_to_duration(val, default_dur)
        } else {
          default_dur
        }
        score.add(Rest, octave, dur, 0.0)
        i = next_i
      }

      // Pitch Notes: C, D, E, F, G, A, B
      'C'
      | 'c'
      | 'D'
      | 'd'
      | 'E'
      | 'e'
      | 'F'
      | 'f'
      | 'G'
      | 'g'
      | 'A'
      | 'a'
      | 'B'
      | 'b' => {
        let mut accidental = ' '
        let mut cur_i = i + 1
        if cur_i < len {
          let next_ch = char_at(mml, cur_i)
          if next_ch == '#' || next_ch == '+' || next_ch == '-' {
            accidental = next_ch
            cur_i = cur_i + 1
          }
        }

        let (val, next_i) = parse_int_digits(mml, cur_i)
        let dur = if val > 0 {
          number_to_duration(val, default_dur)
        } else {
          default_dur
        }
        let pitch = parse_pitch(ch, accidental)
        score.add(pitch, octave, dur, volume)
        i = next_i
      }

      // Unknown character, skip
      _ => i = i + 1
    }
  }

  (score, bpm)
}

///|
/// Compiles an MML string directly into a rendered AudioBuffer.
pub fn render_mml(
  mml : String,
  waveform : Waveform,
  adsr : ADSR,
  sample_rate : Int32,
) -> AudioBuffer {
  let (score, bpm) = parse_mml(mml)
  render_score(score, bpm, waveform, adsr, sample_rate)
}