///|
fn add_span_finding(
  notes : Array[Note],
  options : AnalysisOptions,
  out : Array[Finding],
) -> Unit {
  if notes.length() == 0 {
    return
  }
  let mut lowest = notes[0].midi
  let mut highest = notes[0].midi
  for n in notes {
    if n.midi < lowest {
      lowest = n.midi
    }
    if n.midi > highest {
      highest = n.midi
    }
  }
  let span = highest - lowest
  if span > options.max_melody_span {
    out.push(
      Finding::warning(
        rule=MelodyRange,
        index=0,
        message="melodic span \{span} exceeds configured limit \{options.max_melody_span}",
      ),
    )
  }
}

///|
fn add_leap_findings(
  notes : Array[Note],
  options : AnalysisOptions,
  out : Array[Finding],
) -> Unit {
  if notes.length() < 2 {
    return
  }
  for i in 1.. options.max_leap {
      out.push(
        Finding::error(
          rule=LargeLeap,
          index=i,
          message="melodic leap \{leap} semitones is larger than \{options.max_leap}",
        ),
      )
    }
    if i + 1 < notes.length() && leap > 7 {
      let before = direction(notes[i - 1], notes[i])
      let after = direction(notes[i], notes[i + 1])
      let recovery = absolute_melodic_interval(notes[i], notes[i + 1])
      if before == after || after == Same || recovery > 4 {
        out.push(
          Finding::warning(
            rule=LeapRecovery,
            index=i + 1,
            message="large leap at \{i} should recover by small contrary motion",
          ),
        )
      }
    }
  }
}

///|
pub fn analyze_melody(
  notes : Array[Note],
  options : AnalysisOptions,
) -> Array[Finding] {
  let out : Array[Finding] = []
  add_span_finding(notes, options, out)
  add_leap_findings(notes, options, out)
  out
}