///|
pub(all) struct CandidatePolicy {
  low : Int
  high : Int
  max_leap : Int
  require_consonance : Bool
  prefer_contrary : Bool
  avoid_repetition : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PitchCandidate {
  note : Note
  vertical_interval : Int
  melodic_interval : Int
  consonant : Bool
  motion : MotionKind
  score : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct CandidateBatch {
  source : Note
  candidates : Array[PitchCandidate]
  accepted : Int
  rejected : Int
} derive(Debug, Eq, ToJson)

///|
pub fn CandidatePolicy::default() -> CandidatePolicy {
  {
    low: 48,
    high: 84,
    max_leap: 12,
    require_consonance: true,
    prefer_contrary: true,
    avoid_repetition: false,
  }
}

///|
pub fn CandidatePolicy::strict() -> CandidatePolicy {
  {
    low: 55,
    high: 79,
    max_leap: 9,
    require_consonance: true,
    prefer_contrary: true,
    avoid_repetition: true,
  }
}

///|
pub fn CandidatePolicy::is_valid(self : CandidatePolicy) -> Bool {
  self.low >= 0 &&
  self.high <= 127 &&
  self.low <= self.high &&
  self.max_leap >= 0
}

///|
pub fn candidate_note(source : Note, midi : Int) -> Note {
  Note::new(
    midi~,
    onset=source.onset,
    duration=source.duration,
    tied_from_previous=source.tied_from_previous,
    label=pitch_label(midi),
  )
}

///|
pub fn candidate_motion(
  cantus_before : Note,
  cantus_after : Note,
  counter_before : Note,
  counter_after : Note,
) -> MotionKind {
  motion(cantus_before, cantus_after, counter_before, counter_after)
}

///|
pub fn candidate_score(
  vertical : Int,
  melodic : Int,
  motion_value : MotionKind,
  policy : CandidatePolicy,
) -> Int {
  let consonance_score = if is_consonance(vertical) { 40 } else { -60 }
  let leap = if melodic < 0 { -melodic } else { melodic }
  let leap_score = if leap <= 2 {
    25
  } else if leap <= policy.max_leap {
    10
  } else {
    -40
  }
  let motion_score = if policy.prefer_contrary && motion_value == Contrary {
    20
  } else {
    0
  }
  consonance_score + leap_score + motion_score
}

///|
pub fn candidate_is_allowed(
  cantus : Note,
  previous : Note?,
  candidate : Note,
  policy : CandidatePolicy,
) -> Bool {
  if !policy.is_valid() ||
    candidate.midi < policy.low ||
    candidate.midi > policy.high {
    false
  } else {
    let vertical = harmonic_interval(cantus, candidate)
    let melodic = match previous {
      None => 0
      Some(note) => melodic_interval(note, candidate)
    }
    let leap = if melodic < 0 { -melodic } else { melodic }
    let consonant_ok = !policy.require_consonance || is_consonance(vertical)
    let leap_ok = previous is None || leap <= policy.max_leap
    let repeat_ok = match previous {
      None => true
      Some(note) => !policy.avoid_repetition || note.midi != candidate.midi
    }
    consonant_ok && leap_ok && repeat_ok
  }
}

///|
pub fn generate_candidates(
  cantus : Note,
  previous : Note?,
  policy : CandidatePolicy,
) -> Array[PitchCandidate] {
  let result : Array[PitchCandidate] = []
  if !policy.is_valid() {
    return result
  }
  for midi in policy.low..<=policy.high {
    let candidate = candidate_note(cantus, midi)
    if candidate_is_allowed(cantus, previous, candidate, policy) {
      let vertical = harmonic_interval(cantus, candidate)
      let melodic = match previous {
        None => 0
        Some(note) => melodic_interval(note, candidate)
      }
      let score = candidate_score(vertical, melodic, Static, policy)
      result.push({
        note: candidate,
        vertical_interval: vertical,
        melodic_interval: melodic,
        consonant: is_consonance(vertical),
        motion: Static,
        score,
      })
    }
  }
  result
}

///|
pub fn generate_voice_candidates(
  cantus_before : Note,
  cantus_after : Note,
  counter_before : Note?,
  policy : CandidatePolicy,
) -> CandidateBatch {
  let result : Array[PitchCandidate] = []
  let mut rejected = 0
  for midi in policy.low..<=policy.high {
    let candidate = candidate_note(cantus_after, midi)
    let allowed = candidate_is_allowed(
      cantus_after, counter_before, candidate, policy,
    )
    if allowed {
      let vertical = harmonic_interval(cantus_after, candidate)
      let melodic = match counter_before {
        None => 0
        Some(note) => melodic_interval(note, candidate)
      }
      let motion_value = match counter_before {
        None => Static
        Some(note) =>
          candidate_motion(cantus_before, cantus_after, note, candidate)
      }
      result.push({
        note: candidate,
        vertical_interval: vertical,
        melodic_interval: melodic,
        consonant: is_consonance(vertical),
        motion: motion_value,
        score: candidate_score(vertical, melodic, motion_value, policy),
      })
    } else {
      rejected += 1
    }
  }
  {
    source: cantus_after,
    candidates: result,
    accepted: result.length(),
    rejected,
  }
}

///|
pub fn candidate_has_rule(candidate : PitchCandidate, rule : RuleKind) -> Bool {
  match rule {
    DissonancePlacement => !candidate.consonant
    LargeLeap => {
      let leap = if candidate.melodic_interval < 0 {
        -candidate.melodic_interval
      } else {
        candidate.melodic_interval
      }
      leap > 12
    }
    ParallelPerfect =>
      candidate.motion == Parallel &&
      is_perfect_consonance(candidate.vertical_interval)
    HiddenPerfect =>
      candidate.motion == Similar &&
      is_perfect_consonance(candidate.vertical_interval)
    _ => false
  }
}

///|
pub fn candidate_count_in_range(
  candidates : Array[PitchCandidate],
  low : Int,
  high : Int,
) -> Int {
  let mut result = 0
  for candidate in candidates {
    if low <= candidate.note.midi && candidate.note.midi <= high {
      result += 1
    }
  }
  result
}

///|
pub fn candidate_scores(candidates : Array[PitchCandidate]) -> Array[Int] {
  let result : Array[Int] = []
  for candidate in candidates {
    result.push(candidate.score)
  }
  result
}

///|
pub fn best_candidate(candidates : Array[PitchCandidate]) -> PitchCandidate? {
  if candidates.length() == 0 {
    return None
  }
  let mut best = candidates[0]
  for candidate in candidates {
    if candidate.score > best.score {
      best = candidate
    }
  }
  Some(best)
}

///|
pub fn best_candidates(
  candidates : Array[PitchCandidate],
  limit : Int,
) -> Array[PitchCandidate] {
  let result : Array[PitchCandidate] = []
  let remaining = candidates
  let count = if limit < 0 {
    0
  } else if limit > remaining.length() {
    remaining.length()
  } else {
    limit
  }
  for _ in 0.. ()
      Some(best) => {
        result.push(best)
        let mut found = false
        let copy : Array[PitchCandidate] = []
        for candidate in remaining {
          if !found && candidate == best {
            found = true
          } else {
            copy.push(candidate)
          }
        }
      }
    }
  }
  result
}

///|
pub fn candidate_range(candidates : Array[PitchCandidate]) -> Int {
  if candidates.length() == 0 {
    return 0
  }
  let mut low = candidates[0].note.midi
  let mut high = low
  for candidate in candidates {
    if candidate.note.midi < low {
      low = candidate.note.midi
    }
    if candidate.note.midi > high {
      high = candidate.note.midi
    }
  }
  high - low
}