///|
pub(all) struct PhraseWindow {
  index : Int
  start_index : Int
  end_index : Int
  start_time : Int
  end_time : Int
  note_count : Int
  pitch_range : Int
  density : Int
  anchor_beat : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PhrasePartition {
  windows : Array[PhraseWindow]
  total_notes : Int
  total_duration : Int
  window_duration : Int
} derive(Debug, Eq, ToJson)

///|
/// Divide a sequence into deterministic measure-sized windows for local analysis.
pub fn partition_phrases(
  notes : Array[Note],
  meter : Meter,
  measures_per_window : Int,
) -> PhrasePartition {
  let measure_length = meter.measure_length()
  let safe_measure = if measure_length <= 0 { 1 } else { measure_length }
  let safe_measures = if measures_per_window <= 0 {
    1
  } else {
    measures_per_window
  }
  let window_duration = safe_measure * safe_measures
  let total_duration = sequence_end(notes)
  let window_count = if total_duration <= 0 {
    0
  } else {
    (total_duration + window_duration - 1) / window_duration
  }
  let windows : Array[PhraseWindow] = []
  for window_index in 0.. total_duration {
      total_duration
    } else {
      start_time + window_duration
    }
    let start_index = first_note_index_at_or_after(notes, start_time)
    let end_index = first_note_index_at_or_after(notes, end_time)
    if start_index < end_index {
      windows.push(
        make_phrase_window(
          window_index, notes, start_index, end_index, start_time, end_time, meter,
        ),
      )
    }
  }
  { windows, total_notes: notes.length(), total_duration, window_duration }
}

///|
fn first_note_index_at_or_after(notes : Array[Note], time : Int) -> Int {
  for i, note in notes {
    if note.onset >= time {
      return i
    }
  }
  notes.length()
}

///|
fn make_phrase_window(
  index : Int,
  notes : Array[Note],
  start_index : Int,
  end_index : Int,
  start_time : Int,
  end_time : Int,
  meter : Meter,
) -> PhraseWindow {
  let first_pitch = notes[start_index].midi
  let mut lowest = first_pitch
  let mut highest = first_pitch
  for i in start_index.. highest {
      highest = pitch
    }
  }
  let count = end_index - start_index
  let duration = if end_time <= start_time { 1 } else { end_time - start_time }
  {
    index,
    start_index,
    end_index,
    start_time,
    end_time,
    note_count: count,
    pitch_range: highest - lowest,
    density: count * 100 / duration,
    anchor_beat: beat_label(beat_point(meter, start_time, start_index)),
  }
}

///|
pub fn phrase_window_count(partition : PhrasePartition) -> Int {
  partition.windows.length()
}

///|
pub fn phrase_window(partition : PhrasePartition, index : Int) -> PhraseWindow? {
  if index < 0 || index >= partition.windows.length() {
    None
  } else {
    Some(partition.windows[index])
  }
}

///|
pub fn phrase_window_contains(window : PhraseWindow, note_index : Int) -> Bool {
  note_index >= window.start_index && note_index < window.end_index
}

///|
pub fn phrase_window_duration(window : PhraseWindow) -> Int {
  window.end_time - window.start_time
}

///|
pub fn phrase_window_is_dense(window : PhraseWindow, threshold : Int) -> Bool {
  window.density >= threshold
}

///|
pub fn phrase_window_summary(window : PhraseWindow) -> String {
  "window=\{window.index} notes=\{window.note_count} range=\{window.pitch_range} density=\{window.density}% beat=\{window.anchor_beat}"
}

///|
pub fn phrase_partition_summary(partition : PhrasePartition) -> String {
  "windows=\{partition.windows.length()} notes=\{partition.total_notes} duration=\{partition.total_duration} window=\{partition.window_duration}"
}

///|
pub fn phrase_partition_markdown(partition : PhrasePartition) -> String {
  let lines : Array[String] = [
    "## Phrase windows",
    "",
    "- \{phrase_partition_summary(partition)}",
    "",
    "| Window | Notes | Range | Density | Anchor |",
    "| ---: | ---: | ---: | ---: | --- |",
  ]
  for window in partition.windows {
    lines.push(
      "| \{window.index} | \{window.note_count} | \{window.pitch_range} | \{window.density}% | \{window.anchor_beat} |",
    )
  }
  lines.join("\n")
}

///|
pub fn phrase_partition_boundaries(partition : PhrasePartition) -> Array[Int] {
  let result : Array[Int] = []
  for window in partition.windows {
    result.push(window.start_time)
  }
  result
}

///|
pub fn phrase_partition_for_meter(
  notes : Array[Note],
  meter : Meter,
) -> PhrasePartition {
  partition_phrases(notes, meter, 1)
}