///|
pub(all) struct NoteRow {
  midi : Int
  onset : Int
  duration : Int
  tied : Bool
  label : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct SequenceEnvelope {
  name : String
  meter : Meter
  notes : Array[NoteRow]
  version : String
} derive(Debug, Eq, ToJson)

///|
pub fn NoteRow::from_note(note : Note) -> NoteRow {
  {
    midi: note.midi,
    onset: note.onset,
    duration: note.duration,
    tied: note.tied_from_previous,
    label: note.label,
  }
}

///|
pub fn NoteRow::to_note(self : NoteRow) -> Note {
  Note::new(
    midi=self.midi,
    onset=self.onset,
    duration=self.duration,
    tied_from_previous=self.tied,
    label=self.label,
  )
}

///|
pub fn rows_from_notes(notes : Array[Note]) -> Array[NoteRow] {
  let result : Array[NoteRow] = []
  for note in notes {
    result.push(NoteRow::from_note(note))
  }
  result
}

///|
pub fn notes_from_rows(rows : Array[NoteRow]) -> Array[Note] {
  let result : Array[Note] = []
  for row in rows {
    result.push(row.to_note())
  }
  result
}

///|
fn escape_field(value : String) -> String {
  value
  .replace(old="\\", new="\\\\")
  .replace(old="\t", new="\\t")
  .replace(old="\n", new="\\n")
}

///|
pub fn unescape_field(value : String) -> String {
  value
  .replace(old="\\t", new="\t")
  .replace(old="\\n", new="\n")
  .replace(old="\\\\", new="\\")
}

///|
pub fn note_row_header() -> String {
  "midi\tonset\tduration\ttied\tlabel"
}

///|
pub fn note_row_to_line(row : NoteRow) -> String {
  "\{row.midi}\t\{row.onset}\t\{row.duration}\t\{row.tied}\t\{escape_field(row.label)}"
}

///|
pub fn notes_to_lines(notes : Array[Note]) -> Array[String] {
  let result : Array[String] = [note_row_header()]
  for row in rows_from_notes(notes) {
    result.push(note_row_to_line(row))
  }
  result
}

///|
pub fn notes_to_tsv(notes : Array[Note]) -> String {
  notes_to_lines(notes).join("\n")
}

///|
pub fn envelope(
  name : String,
  meter : Meter,
  notes : Array[Note],
) -> SequenceEnvelope {
  { name, meter, notes: rows_from_notes(notes), version: "1" }
}

///|
pub fn envelope_notes(value : SequenceEnvelope) -> Array[Note] {
  notes_from_rows(value.notes)
}

///|
pub fn envelope_summary(value : SequenceEnvelope) -> String {
  let notes = envelope_notes(value)
  "\{value.name}: \{notes.length()} notes, \{value.meter.numerator}/\{value.meter.denominator}"
}

///|
pub fn manifest(profile : RuleProfileConfig, meter : Meter) -> Array[String] {
  [
    "module=lyyjavastudy/moonbit-counterpoint",
    "profile=\{profile.label()}",
    "meter=\{meter.numerator}/\{meter.denominator}",
    "subdivision=\{meter.subdivision}",
    "format=counterpoint-tsv-v1",
  ]
}

///|
pub fn manifest_text(profile : RuleProfileConfig, meter : Meter) -> String {
  manifest(profile, meter).join("\n")
}

///|
pub fn analysis_manifest(analysis : Analysis) -> Array[String] {
  let summary = summarize(analysis)
  [
    "grade=\{summary.grade()}",
    "score=\{summary.score}",
    "total=\{summary.total}",
    "errors=\{summary.errors}",
    "warnings=\{summary.warnings}",
  ]
}

///|
pub fn analysis_manifest_text(analysis : Analysis) -> String {
  analysis_manifest(analysis).join("\n")
}

///|
pub fn pitch_rows(
  low : Int,
  high : Int,
  onset : Int,
  duration : Int,
) -> Array[NoteRow] {
  let result : Array[NoteRow] = []
  if low > high {
    return result
  }
  for midi in low..<=high {
    result.push({ midi, onset, duration, tied: false, label: pitch_label(midi) })
  }
  result
}

///|
pub fn notes_from_pitches(
  pitches_value : Array[Int],
  duration : Int,
) -> Array[Note] {
  let result : Array[Note] = []
  for i, midi in pitches_value {
    result.push(Note::new(midi~, onset=i * duration, duration~))
  }
  result
}

///|
pub fn notes_to_pitches(notes : Array[Note]) -> Array[Int] {
  pitches(notes)
}

///|
pub fn sequence_fingerprint(notes : Array[Note]) -> String {
  let parts : Array[String] = []
  for note in notes {
    parts.push("\{note.midi}:\{note.onset}:\{note.duration}")
  }
  parts.join("|")
}

///|
pub fn same_sequence(left : Array[Note], right : Array[Note]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for i in 0.. Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    result.push({ ..note, onset: note.onset + delta })
  }
  result
}

///|
pub fn trim_before(notes : Array[Note], time : Int) -> Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    if note_end(note) > time {
      let onset = if note.onset < time { time } else { note.onset }
      let end = note_end(note)
      let duration = end - onset
      if duration > 0 {
        result.push({ ..note, onset, duration })
      }
    }
  }
  result
}

///|
pub fn trim_after(notes : Array[Note], time : Int) -> Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    if note.onset < time {
      let end = if note_end(note) > time { time } else { note_end(note) }
      let duration = end - note.onset
      if duration > 0 {
        result.push({ ..note, duration, })
      }
    }
  }
  result
}

///|
pub fn normalize_onsets(notes : Array[Note]) -> Array[Note] {
  if notes.length() == 0 {
    return []
  }
  let offset = notes[0].onset
  shift_onsets(notes, -offset)
}

///|
pub fn format_note_row(row : NoteRow) -> String {
  "\{pitch_label(row.midi)} @ \{row.onset}+\{row.duration}"
}

///|
pub fn format_sequence(notes : Array[Note]) -> String {
  let result : Array[String] = []
  for row in rows_from_notes(notes) {
    result.push(format_note_row(row))
  }
  result.join(", ")
}