///|
/// Keyboard token parsing and canonicalization.
fn modifier_name(name : String) -> String {
  match lower_ascii(trim_ascii(name)) {
    "ctrl" => "Ctrl"
    "control" => "Ctrl"
    "cmd" => "Cmd"
    "command" => "Cmd"
    "meta" => "Cmd"
    "super" => "Cmd"
    "win" => "Cmd"
    "alt" => "Alt"
    "option" => "Alt"
    "shift" => "Shift"
    "ctrlorcmd" => "CtrlOrCmd"
    "primary" => "CtrlOrCmd"
    "hyper" => "Hyper"
    "fn" => "Fn"
    other => other
  }
}

///|
fn is_modifier(name : String) -> Bool {
  let canonical = modifier_name(name)
  canonical == "Ctrl" ||
  canonical == "Cmd" ||
  canonical == "Alt" ||
  canonical == "Shift" ||
  canonical == "CtrlOrCmd" ||
  canonical == "Hyper" ||
  canonical == "Fn"
}

///|
fn key_name(name : String) -> String {
  let clean = trim_ascii(name)
  let lower = lower_ascii(clean)
  match lower {
    "esc" => "Escape"
    "return" => "Enter"
    "del" => "Delete"
    "ins" => "Insert"
    "spacebar" => "Space"
    "spc" => "Space"
    "left" => "ArrowLeft"
    "right" => "ArrowRight"
    "up" => "ArrowUp"
    "down" => "ArrowDown"
    "pgup" => "PageUp"
    "pgdn" => "PageDown"
    "backspace" => "Backspace"
    "tab" => "Tab"
    "plus" => "Plus"
    "minus" => "Minus"
    "" => ""
    _ => if clean.length() == 1 { lower_ascii(clean) } else { clean }
  }
}

///|
fn order_modifier(name : String) -> Int {
  match name {
    "Ctrl" => 0
    "CtrlOrCmd" => 1
    "Cmd" => 2
    "Alt" => 3
    "Shift" => 4
    "Hyper" => 5
    "Fn" => 6
    _ => 20
  }
}

///|
fn contains_string(items : Array[String], wanted : String) -> Bool {
  for item in items {
    if item == wanted {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn sort_modifiers(items : Array[String]) -> Array[String] {
  let out = items.copy()
  let mut i = 0
  while i < out.length() {
    let mut j = i + 1
    while j < out.length() {
      if order_modifier(out[j]) < order_modifier(out[i]) {
        let hold = out[i]
        out[i] = out[j]
        out[j] = hold
      }
      j += 1
    }
    i += 1
  }
  out
}

///|
fn parse_chord(raw : String) -> (String, Bool, Int, Bool) {
  let pieces = raw.split("+").map(x => x.to_owned()).to_array()
  let modifiers : Array[String] = []
  let mut key = ""
  let mut valid = true
  for piece in pieces {
    let clean = trim_ascii(piece)
    if clean.length() == 0 {
      valid = false
    } else if is_modifier(clean) {
      let normalized = modifier_name(clean)
      if !contains_string(modifiers, normalized) {
        modifiers.push(normalized)
      }
    } else if key.length() == 0 {
      key = key_name(clean)
    } else {
      valid = false
    }
  }
  if key.length() == 0 {
    valid = false
  }
  let ordered = sort_modifiers(modifiers)
  let text = if ordered.length() == 0 {
    key
  } else {
    ordered.join("+") + "+" + key
  }
  (text, ordered.length() > 0, ordered.length(), valid)
}

///|
/// Parse a sequence such as `Ctrl+K Ctrl+C` or `cmd+k,cmd+c`.
pub fn parse_keys(raw : String) -> Result[KeySequence, String] {
  let prepared = trim_ascii(raw).replace(old=",", new=" ")
  if prepared.length() == 0 {
    return Err("empty shortcut")
  }
  let source_steps = words(prepared)
  if source_steps.length() == 0 {
    return Err("empty shortcut")
  }
  let steps : Array[String] = []
  let mut has_modifier = false
  let mut modifier_count = 0
  for source in source_steps {
    let (normalized, modifier, count, valid) = parse_chord(source)
    if !valid {
      return Err("invalid chord '" + source + "'")
    }
    steps.push(normalized)
    has_modifier = has_modifier || modifier
    modifier_count += count
  }
  Ok(
    KeySequence::new(raw, steps, steps.join(" "), has_modifier, modifier_count),
  )
}

///|
/// Normalize a key sequence for comparisons. Invalid input returns an empty
/// string, which makes it safe to use in diagnostics without throwing.
pub fn canonical_keys(raw : String) -> String {
  let result = parse_keys(raw)
  match result {
    Ok(sequence) => sequence.canonical
    Err(_) => ""
  }
}

///|
/// True if the first sequence is a strict prefix of the second.
pub fn key_prefix(prefix : KeySequence, full : KeySequence) -> Bool {
  if prefix.steps.length() >= full.steps.length() {
    false
  } else {
    let mut same = true
    for i in 0.. Bool {
  sequence.has_modifier
}

///|
/// Return the final key without modifiers, useful for reserved-key policies.
pub fn shortcut_final_key(sequence : KeySequence) -> String {
  if sequence.steps.length() == 0 {
    ""
  } else {
    let final_chord = sequence.steps[sequence.steps.length() - 1]
    let parts = final_chord.split("+").map(x => x.to_owned()).to_array()
    parts[parts.length() - 1]
  }
}