///|
/// Conflict analysis engine.
fn push_if_some(findings : Array[Finding], item : Finding?) -> Unit {
  match item {
    Some(finding) => findings.push(finding)
    None => ()
  }
}

///|
fn enabled_bindings(keymap : Keymap) -> Array[Binding] {
  keymap.bindings.filter(binding => binding.enabled)
}

///|
fn exact_or_prefix(left : Binding, right : Binding) -> IssueKind? {
  if left.keys.canonical == right.keys.canonical {
    Some(ExactConflict)
  } else if key_prefix(left.keys, right.keys) ||
    key_prefix(right.keys, left.keys) {
    Some(PrefixConflict)
  } else {
    None
  }
}

///|
fn analyze_pair(
  keymap : Keymap,
  left : Binding,
  right : Binding,
  findings : Array[Finding],
) -> Unit {
  if !contexts_overlap(keymap, left.context, right.context) ||
    !platforms_overlap(left.platform, right.platform) {
    return
  }
  match exact_or_prefix(left, right) {
    None => ()
    Some(ExactConflict) => {
      if left.command == right.command {
        findings.push(
          pair_finding(
            "MK103",
            DuplicateCommand,
            Info,
            "the same command is bound more than once in overlapping scopes",
            left,
            right,
            "keep both only when the duplicate is intentional and documented",
          ),
        )
      } else {
        findings.push(
          pair_finding(
            "MK001",
            ExactConflict,
            Error,
            "two different commands claim the same shortcut in overlapping scopes",
            left,
            right,
            "change one chord, context, platform, or priority",
          ),
        )
      }
      let winner = more_specific(keymap, left, right)
      let loser = if winner.id == left.id { right } else { left }
      if winner.command != loser.command && winner.priority != loser.priority {
        findings.push(
          pair_finding(
            "MK002",
            ShadowedBinding,
            Warning,
            "a lower-priority binding is shadowed by a more specific binding",
            loser,
            winner,
            "make the priority explicit or choose a non-overlapping chord",
          ),
        )
      }
      if left.platform != right.platform &&
        (
          lower_ascii(left.platform) == "all" ||
          lower_ascii(right.platform) == "all"
        ) {
        findings.push(
          pair_finding(
            "MK003",
            PlatformOverlap,
            Warning,
            "an all-platform binding overlaps a platform-specific binding",
            left,
            right,
            "split the all-platform declaration or document the intended fallback",
          ),
        )
      }
    }
    Some(PrefixConflict) =>
      findings.push(
        pair_finding(
          "MK004",
          PrefixConflict,
          Warning,
          "a shorter chord is a prefix of another chord and can delay or swallow input",
          left,
          right,
          "use a timeout-aware dispatcher or choose chords with distinct prefixes",
        ),
      )
    Some(_) => ()
  }
}

///|
fn count_severity(findings : Array[Finding], severity : Severity) -> Int {
  let mut count = 0
  for item in findings {
    if item.severity == severity {
      count += 1
    }
  }
  count
}

///|
fn score_findings(findings : Array[Finding]) -> Int {
  let mut score = 100
  for finding in findings {
    match finding.severity {
      Error => score -= 18
      Warning => score -= 6
      Info => score -= 1
    }
  }
  if score < 0 {
    0
  } else {
    score
  }
}

///|
/// Analyze declarations using all built-in rules.
pub fn analyze(keymap : Keymap) -> Analysis {
  let findings : Array[Finding] = []
  let active = enabled_bindings(keymap)
  for binding in keymap.bindings {
    push_if_some(findings, invalid_context_finding(keymap, binding))
    push_if_some(findings, accessibility_finding(binding))
    push_if_some(findings, reserved_finding(keymap, binding))
    push_if_some(findings, disabled_finding(binding))
  }
  for i in 0.. (ParseResult, Analysis) {
  let parsed = parse_keymap(source, name~)
  (parsed, analyze(parsed.keymap))
}