///|
pub(all) enum SequenceIssue {
  EmptySequence
  NegativePitch(index~ : Int)
  InvalidDuration(index~ : Int)
  NonMonotonicOnset(index~ : Int)
  Overlap(index~ : Int)
  DuplicateOnset(index~ : Int)
  ExcessiveGap(index~ : Int)
} derive(Debug, Eq, ToJson)

///|
pub(all) struct SequenceReport {
  note_count : Int
  duration : Int
  issues : Array[SequenceIssue]
} derive(Debug, Eq, ToJson)

///|
pub fn sequence_report(notes : Array[Note]) -> SequenceReport {
  let issues : Array[SequenceIssue] = []
  if notes.length() == 0 {
    issues.push(EmptySequence)
    return { note_count: 0, duration: 0, issues }
  }
  let mut end = 0
  for i, note in notes {
    if note.midi < 0 {
      issues.push(NegativePitch(index=i))
    }
    if note.duration <= 0 {
      issues.push(InvalidDuration(index=i))
    }
    if i > 0 {
      let previous = notes[i - 1]
      if note.onset < previous.onset {
        issues.push(NonMonotonicOnset(index=i))
      }
      if note.onset == previous.onset {
        issues.push(DuplicateOnset(index=i))
      }
      if note.onset < previous.onset + previous.duration {
        issues.push(Overlap(index=i))
      }
      if note.onset > previous.onset + previous.duration + 8 {
        issues.push(ExcessiveGap(index=i))
      }
    }
    if note.onset + note.duration > end {
      end = note.onset + note.duration
    }
  }
  { note_count: notes.length(), duration: end, issues }
}

///|
pub fn SequenceReport::is_valid(self : SequenceReport) -> Bool {
  self.issues.length() == 0
}

///|
pub fn SequenceReport::error_count(self : SequenceReport) -> Int {
  let mut total = 0
  for issue in self.issues {
    match issue {
      EmptySequence => total += 1
      InvalidDuration(_) => total += 1
      NegativePitch(_) => total += 1
      NonMonotonicOnset(_) => total += 1
      Overlap(_) => total += 1
      DuplicateOnset(_) => total += 1
      ExcessiveGap(_) => total += 1
    }
  }
  total
}

///|
pub fn note_end(note : Note) -> Int {
  note.onset + note.duration
}

///|
pub fn sequence_end(notes : Array[Note]) -> Int {
  let mut result = 0
  for note in notes {
    if note_end(note) > result {
      result = note_end(note)
    }
  }
  result
}

///|
pub fn notes_at(notes : Array[Note], onset : Int) -> Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    if note.onset == onset {
      result.push(note)
    }
  }
  result
}

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

///|
pub fn count_duration(notes : Array[Note], duration : Int) -> Int {
  let mut result = 0
  for note in notes {
    if note.duration == duration {
      result += 1
    }
  }
  result
}

///|
pub fn onset_span(notes : Array[Note]) -> Int {
  if notes.length() < 2 {
    return 0
  }
  let mut lowest = notes[0].onset
  let mut highest = notes[0].onset
  for note in notes {
    if note.onset < lowest {
      lowest = note.onset
    }
    if note.onset > highest {
      highest = note.onset
    }
  }
  highest - lowest
}

///|
pub fn total_sounding_duration(notes : Array[Note]) -> Int {
  let mut result = 0
  for note in notes {
    result += note.duration
  }
  result
}

///|
pub fn tied_count(notes : Array[Note]) -> Int {
  let mut result = 0
  for note in notes {
    if note.tied_from_previous {
      result += 1
    }
  }
  result
}

///|
pub fn first_pitch(notes : Array[Note]) -> Int? {
  if notes.length() == 0 {
    None
  } else {
    Some(notes[0].midi)
  }
}

///|
pub fn last_pitch(notes : Array[Note]) -> Int? {
  if notes.length() == 0 {
    None
  } else {
    Some(notes[notes.length() - 1].midi)
  }
}

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

///|
pub fn transpose(notes : Array[Note], semitones : Int) -> Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    result.push({ ..note, midi: note.midi + semitones })
  }
  result
}

///|
pub fn scale_time(notes : Array[Note], factor : Int) -> Array[Note] {
  let result : Array[Note] = []
  if factor <= 0 {
    return result
  }
  for note in notes {
    result.push({
      ..note,
      onset: note.onset * factor,
      duration: note.duration * factor,
    })
  }
  result
}