///|
/// A small deterministic dispatcher simulation used to validate analyzer
/// assumptions without depending on a GUI or operating-system backend.
pub(all) enum DispatchStatus {
  Matched
  Prefix
  NoMatch
  Ambiguous
  Disabled
  Invalid
} derive(Eq, @debug.Debug)

///|
pub(all) struct DispatchResult {
  status : DispatchStatus
  input : String
  canonical : String
  command : String
  binding_ids : Array[String]
  context : String
  platform : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct DispatchStep {
  input : String
  result : DispatchResult
  elapsed_ms : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct DispatchTrace {
  context : String
  platform : String
  steps : Array[DispatchStep]
  matched_count : Int
  prefix_count : Int
  no_match_count : Int
} derive(Eq, @debug.Debug)

///|
fn dispatch_result(
  status : DispatchStatus,
  input : String,
  canonical : String,
  command : String,
  ids : Array[String],
  context : String,
  platform : String,
  message : String,
) -> DispatchResult {
  {
    status,
    input,
    canonical,
    command,
    binding_ids: ids,
    context,
    platform,
    message,
  }
}

///|
fn dispatch_status_name(status : DispatchStatus) -> String {
  match status {
    Matched => "matched"
    Prefix => "prefix"
    NoMatch => "no-match"
    Ambiguous => "ambiguous"
    Disabled => "disabled"
    Invalid => "invalid"
  }
}

///|
fn matching_bindings(
  keymap : Keymap,
  sequence : KeySequence,
  context : String,
  platform : String,
) -> Array[Binding] {
  keymap.bindings.filter(binding => {
    binding.enabled &&
    binding.keys.canonical == sequence.canonical &&
    binding_active_in_context(keymap, binding.context, context) &&
    platforms_overlap(binding.platform, platform)
  })
}

///|
fn prefix_bindings(
  keymap : Keymap,
  sequence : KeySequence,
  context : String,
  platform : String,
) -> Array[Binding] {
  keymap.bindings.filter(binding => {
    binding.enabled &&
    key_prefix(sequence, binding.keys) &&
    binding_active_in_context(keymap, binding.context, context) &&
    platforms_overlap(binding.platform, platform)
  })
}

///|
fn sort_dispatch_candidates(
  keymap : Keymap,
  bindings : Array[Binding],
) -> Array[Binding] {
  let result = bindings.copy()
  result.sort_by((left, right) => {
    if left.priority > right.priority {
      -1
    } else if left.priority < right.priority {
      1
    } else {
      let left_rank = binding_context_rank(keymap, left)
      let right_rank = binding_context_rank(keymap, right)
      if left_rank > right_rank {
        -1
      } else if left_rank < right_rank {
        1
      } else if left.id < right.id {
        -1
      } else if left.id > right.id {
        1
      } else {
        0
      }
    }
  })
  result
}

///|
/// Resolve one complete or partial sequence.
pub fn dispatch(
  keymap : Keymap,
  raw : String,
  context? : String = "global",
  platform? : String = "all",
) -> DispatchResult {
  match parse_keys(raw) {
    Err(message) =>
      dispatch_result(Invalid, raw, "", "", [], context, platform, message)
    Ok(sequence) => {
      let exact = sort_dispatch_candidates(
        keymap,
        matching_bindings(keymap, sequence, context, platform),
      )
      if exact.length() > 0 {
        let ids : Array[String] = []
        for binding in exact {
          ids.push(binding.id)
        }
        let command = exact[0].command
        let status = if exact.length() > 1 &&
          exact[0].priority == exact[1].priority &&
          binding_context_rank(keymap, exact[0]) ==
          binding_context_rank(keymap, exact[1]) {
          DispatchStatus::Ambiguous
        } else {
          Matched
        }
        let message = if status == DispatchStatus::Ambiguous {
          "multiple equally specific bindings"
        } else {
          "selected highest-priority binding"
        }
        dispatch_result(
          status,
          raw,
          sequence.canonical,
          command,
          ids,
          context,
          platform,
          message,
        )
      } else if prefix_bindings(keymap, sequence, context, platform).length() >
        0 {
        dispatch_result(
          Prefix,
          raw,
          sequence.canonical,
          "",
          [],
          context,
          platform,
          "waiting for the next chord",
        )
      } else {
        let disabled = keymap.bindings
          .filter(binding => {
            !binding.enabled && binding.keys.canonical == sequence.canonical
          })
          .length() >
          0
        if disabled {
          dispatch_result(
            Disabled,
            raw,
            sequence.canonical,
            "",
            [],
            context,
            platform,
            "matching binding is disabled",
          )
        } else {
          dispatch_result(
            NoMatch,
            raw,
            sequence.canonical,
            "",
            [],
            context,
            platform,
            "no binding matches this context and platform",
          )
        }
      }
    }
  }
}

///|
/// Replay a sequence of user inputs and count outcomes.
pub fn replay(
  keymap : Keymap,
  inputs : Array[String],
  context? : String = "global",
  platform? : String = "all",
) -> DispatchTrace {
  let steps : Array[DispatchStep] = []
  let mut matched = 0
  let mut prefixes = 0
  let mut misses = 0
  let mut elapsed = 0
  for input in inputs {
    let result = dispatch(keymap, input, context~, platform~)
    match result.status {
      Matched => matched += 1
      Prefix => prefixes += 1
      _ => misses += 1
    }
    elapsed += 1
    steps.push({ input, result, elapsed_ms: elapsed })
  }
  {
    context,
    platform,
    steps,
    matched_count: matched,
    prefix_count: prefixes,
    no_match_count: misses,
  }
}

///|
pub fn dispatch_result_to_json(result : DispatchResult) -> String {
  let ids : Array[String] = []
  for id in result.binding_ids {
    ids.push(json_string(id))
  }
  "{\"status\":" +
  json_string(dispatch_status_name(result.status)) +
  ",\"input\":" +
  json_string(result.input) +
  ",\"canonical\":" +
  json_string(result.canonical) +
  ",\"command\":" +
  json_string(result.command) +
  ",\"bindings\":[" +
  ids.join(",") +
  "],\"message\":" +
  json_string(result.message) +
  "}"
}

///|
pub fn dispatch_trace_to_markdown(trace : DispatchTrace) -> String {
  let lines : Array[String] = [
    "## Dispatch replay",
    "",
    "Context: `" + trace.context + "`  Platform: `" + trace.platform + "`",
    "",
    "| Input | Status | Command | Message |",
    "| --- | --- | --- | --- |",
  ]
  for step in trace.steps {
    lines.push(
      "| `" +
      step.input +
      "` | " +
      dispatch_status_name(step.result.status) +
      " | `" +
      step.result.command +
      "` | " +
      step.result.message +
      " |",
    )
  }
  lines.push("")
  lines.push(
    "matched=" +
    trace.matched_count.to_string() +
    ", prefix=" +
    trace.prefix_count.to_string() +
    ", other=" +
    trace.no_match_count.to_string(),
  )
  lines.join("\n")
}

///|
/// Check that every command has at least one dispatchable binding.
pub fn unreachable_commands(
  keymap : Keymap,
  context : String,
  platform : String,
) -> Array[String] {
  let commands : Array[String] = []
  for binding in keymap.bindings {
    if !array_contains(commands, binding.command) {
      commands.push(binding.command)
    }
  }
  let missing_commands : Array[String] = []
  for command in commands {
    let bindings = select_bindings(
        keymap,
        command~,
        context~,
        platform~,
        enabled_only=true,
      ).bindings
    if bindings.length() == 0 {
      missing_commands.push(command)
    }
  }
  missing_commands
}