///|
pub(all) enum RepairKind {
  StepUp
  StepDown
  ReduceLeap
  MoveIntoRange
  PreserveCadence
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RepairSuggestion {
  index : Int
  kind : RepairKind
  replacement_midi : Int
  score : Int
  rationale : String
} derive(Debug, Eq, ToJson)

///|
/// Produce bounded single-event edits that can be reviewed before applying them.
pub fn repair_suggestions(
  finding : Finding,
  notes : Array[Note],
  max_leap : Int,
  low : Int,
  high : Int,
) -> Array[RepairSuggestion] {
  let result : Array[RepairSuggestion] = []
  if finding.index < 0 || finding.index >= notes.length() || low > high {
    return result
  }
  let current = notes[finding.index]
  let previous = if finding.index > 0 {
    Some(notes[finding.index - 1])
  } else {
    None
  }
  let next = if finding.index + 1 < notes.length() {
    Some(notes[finding.index + 1])
  } else {
    None
  }
  let proposals = repair_proposals(
    finding.rule,
    current,
    previous,
    next,
    max_leap,
  )
  for proposal in proposals {
    if proposal.0 >= low &&
      proposal.0 <= high &&
      proposal.0 != current.midi &&
      !contains_replacement(result, proposal.0) {
      result.push({
        index: finding.index,
        kind: proposal.1,
        replacement_midi: proposal.0,
        score: repair_score(current.midi, proposal.0, previous, next),
        rationale: repair_rationale(finding.rule, proposal.1),
      })
    }
  }
  sort_repairs(result)
  result
}

///|
fn repair_proposals(
  rule : RuleKind,
  current : Note,
  previous : Note?,
  next : Note?,
  max_leap : Int,
) -> Array[(Int, RepairKind)] {
  let result : Array[(Int, RepairKind)] = []
  match rule {
    LargeLeap | LeapRecovery =>
      match previous {
        Some(note) => {
          let direction = if current.midi >= note.midi { 1 } else { -1 }
          let step = if max_leap < 2 { max_leap } else { 2 }
          result.push((note.midi + direction * step, ReduceLeap))
          result.push((note.midi + direction, StepUp))
          result.push((note.midi - direction, StepDown))
        }
        None => {
          result.push((current.midi - 1, StepDown))
          result.push((current.midi + 1, StepUp))
        }
      }
    MelodyRange => {
      result.push((current.midi - 1, MoveIntoRange))
      result.push((current.midi + 1, MoveIntoRange))
    }
    CadenceShape =>
      match next {
        Some(note) => {
          result.push((note.midi - 1, PreserveCadence))
          result.push((note.midi + 1, PreserveCadence))
        }
        None => {
          result.push((current.midi - 1, PreserveCadence))
          result.push((current.midi + 1, PreserveCadence))
        }
      }
    _ => {
      result.push((current.midi - 1, StepDown))
      result.push((current.midi + 1, StepUp))
      result.push((current.midi - 2, StepDown))
      result.push((current.midi + 2, StepUp))
    }
  }
  result
}

///|
fn contains_replacement(
  suggestions : Array[RepairSuggestion],
  midi : Int,
) -> Bool {
  for suggestion in suggestions {
    if suggestion.replacement_midi == midi {
      return true
    }
  }
  false
}

///|
fn abs_value(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
fn repair_score(
  current : Int,
  replacement : Int,
  previous : Note?,
  next : Note?,
) -> Int {
  let edit_cost = abs_value(current - replacement)
  let previous_cost = match previous {
    Some(note) => abs_value(replacement - note.midi)
    None => 0
  }
  let next_cost = match next {
    Some(note) => abs_value(replacement - note.midi)
    None => 0
  }
  let raw = 100 - edit_cost * 3 - previous_cost - next_cost
  if raw < 0 {
    0
  } else {
    raw
  }
}

///|
fn repair_rationale(rule : RuleKind, kind : RepairKind) -> String {
  let action = repair_kind_name(kind)
  match rule {
    LargeLeap =>
      "\{action} keeps the melodic transition inside the configured leap policy."
    LeapRecovery => "\{action} creates a local response to the preceding leap."
    MelodyRange => "\{action} moves the event toward the permitted register."
    CadenceShape =>
      "\{action} preserves the final approach while changing one event."
    _ => "\{action} is a minimal local edit for the reported rule."
  }
}

///|
pub fn repair_kind_name(kind : RepairKind) -> String {
  match kind {
    StepUp => "step-up"
    StepDown => "step-down"
    ReduceLeap => "reduce-leap"
    MoveIntoRange => "move-into-range"
    PreserveCadence => "preserve-cadence"
  }
}

///|
fn sort_repairs(suggestions : Array[RepairSuggestion]) -> Unit {
  for i in 0.. suggestions[i].score {
        let value = suggestions[i]
        suggestions[i] = suggestions[j]
        suggestions[j] = value
      }
    }
  }
}

///|
pub fn repair_suggestion_count(suggestions : Array[RepairSuggestion]) -> Int {
  suggestions.length()
}

///|
pub fn repair_suggestion_all_in_range(
  suggestions : Array[RepairSuggestion],
  low : Int,
  high : Int,
) -> Bool {
  for suggestion in suggestions {
    if suggestion.replacement_midi < low || suggestion.replacement_midi > high {
      return false
    }
  }
  true
}

///|
pub fn apply_repair_suggestion(
  notes : Array[Note],
  suggestion : RepairSuggestion,
) -> Array[Note] {
  let result = notes.copy()
  if suggestion.index >= 0 && suggestion.index < result.length() {
    let old = result[suggestion.index]
    result[suggestion.index] = {
      ..old,
      midi: suggestion.replacement_midi,
      label: pitch_label(suggestion.replacement_midi),
    }
  }
  result
}

///|
pub fn repair_suggestion_summary(suggestion : RepairSuggestion) -> String {
  "index=\{suggestion.index} kind=\{repair_kind_name(suggestion.kind)} midi=\{suggestion.replacement_midi} score=\{suggestion.score}"
}

///|
pub fn repair_suggestion_markdown(
  suggestions : Array[RepairSuggestion],
) -> String {
  let lines : Array[String] = [
    "## Repair suggestions", "", "| Index | Kind | Pitch | Score | Rationale |",
    "| ---: | --- | ---: | ---: | --- |",
  ]
  for suggestion in suggestions {
    lines.push(
      "| \{suggestion.index} | \{repair_kind_name(suggestion.kind)} | \{suggestion.replacement_midi} | \{suggestion.score} | \{suggestion.rationale} |",
    )
  }
  lines.join("\n")
}

///|
pub fn best_repair_suggestion(
  suggestions : Array[RepairSuggestion],
) -> RepairSuggestion? {
  if suggestions.length() == 0 {
    None
  } else {
    Some(suggestions[0])
  }
}

///|
pub fn repair_suggestion_average_score(
  suggestions : Array[RepairSuggestion],
) -> Int {
  if suggestions.length() == 0 {
    0
  } else {
    let mut total = 0
    for suggestion in suggestions {
      total += suggestion.score
    }
    total / suggestions.length()
  }
}

///|
pub fn repair_suggestion_kinds(
  suggestions : Array[RepairSuggestion],
) -> Array[String] {
  let result : Array[String] = []
  for suggestion in suggestions {
    let name = repair_kind_name(suggestion.kind)
    let mut seen = false
    for existing in result {
      if existing == name {
        seen = true
      }
    }
    if !seen {
      result.push(name)
    }
  }
  result
}

///|
pub fn repair_suggestion_for_index(
  suggestions : Array[RepairSuggestion],
  index : Int,
) -> Array[RepairSuggestion] {
  let result : Array[RepairSuggestion] = []
  for suggestion in suggestions {
    if suggestion.index == index {
      result.push(suggestion)
    }
  }
  result
}