///|
/// Query, projection, and deterministic serialization helpers.
pub(all) struct BindingQuery {
  binding : Binding
  context_distance : Int
  platform_match : Bool
  command_match : Bool
} derive(Eq, @debug.Debug)

///|
pub(all) struct KeymapSelection {
  name : String
  bindings : Array[Binding]
  contexts : Array[Context]
  query : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct MergeResult {
  keymap : Keymap
  conflicts : Array[String]
  adopted : Int
  skipped : Int
} derive(Eq, @debug.Debug)

///|
fn binding_index(keymap : Keymap, id : String) -> Int? {
  for i, binding in keymap.bindings {
    if binding.id == id {
      return Some(i)
    }
  }
  None
}

///|
/// Look up one binding by its stable id.
pub fn find_binding(keymap : Keymap, id : String) -> Binding? {
  match binding_index(keymap, id) {
    Some(index) => Some(keymap.bindings[index])
    None => None
  }
}

///|
/// Return all bindings belonging to a command, retaining declaration order.
pub fn find_command(keymap : Keymap, command : String) -> Array[Binding] {
  keymap.bindings.filter(binding => binding.command == command)
}

///|
fn context_distance(keymap : Keymap, ancestor : String, child : String) -> Int? {
  if ancestor == child {
    return Some(0)
  }
  let mut current = child
  let mut distance = 1
  while distance <= keymap.contexts.length() {
    match context_by_name(keymap, current) {
      Some(context) => {
        if context.parent.length() == 0 {
          return None
        }
        if context.parent == ancestor {
          return Some(distance)
        }
        current = context.parent
      }
      None => return None
    }
    distance += 1
  }
  None
}

///|
/// Resolve bindings active in a context. Parent declarations come first so a
/// caller can apply child-priority rules while streaming the result.
pub fn active_bindings(
  keymap : Keymap,
  context : String,
  platform : String,
) -> Array[BindingQuery] {
  let result : Array[BindingQuery] = []
  for binding in keymap.bindings {
    if binding.enabled &&
      binding_active_in_context(keymap, binding.context, context) &&
      platforms_overlap(binding.platform, platform) {
      let distance = match context_distance(keymap, binding.context, context) {
        Some(value) => value
        None => 0
      }
      result.push({
        binding,
        context_distance: distance,
        platform_match: platforms_overlap(binding.platform, platform),
        command_match: false,
      })
    }
  }
  result
}

///|
/// Query by optional command, context, platform, and enabled state.
pub fn select_bindings(
  keymap : Keymap,
  command? : String = "",
  context? : String = "",
  platform? : String = "all",
  enabled_only? : Bool = false,
) -> KeymapSelection {
  let result : Array[Binding] = []
  for binding in keymap.bindings {
    let command_ok = command.length() == 0 || binding.command == command
    let context_ok = context.length() == 0 ||
      binding_active_in_context(keymap, binding.context, context)
    let platform_ok = platforms_overlap(binding.platform, platform)
    let enabled_ok = !enabled_only || binding.enabled
    if command_ok && context_ok && platform_ok && enabled_ok {
      result.push(binding)
    }
  }
  {
    name: keymap.name,
    bindings: result,
    contexts: keymap.contexts,
    query: "command=" +
    command +
    ";context=" +
    context +
    ";platform=" +
    platform,
  }
}

///|
fn compare_bindings(left : Binding, right : Binding) -> Int {
  if left.id < right.id {
    -1
  } else if left.id > right.id {
    1
  } else {
    0
  }
}

///|
fn sorted_bindings(bindings : Array[Binding]) -> Array[Binding] {
  let output = bindings.copy()
  output.sort_by(compare_bindings)
  output
}

///|
/// Return a canonical copy with bindings and contexts sorted by name.
pub fn normalize_keymap(keymap : Keymap) -> Keymap {
  let bindings = sorted_bindings(keymap.bindings)
  let contexts = keymap.contexts.copy()
  contexts.sort_by((left, right) => {
    if left.name < right.name {
      -1
    } else if left.name > right.name {
      1
    } else {
      0
    }
  })
  let reserved = keymap.reserved.copy()
  reserved.sort()
  { ..keymap, bindings, contexts, reserved }
}

///|
fn binding_to_dsl(binding : Binding) -> String {
  let enabled = if binding.enabled { "true" } else { "false" }
  "bind " +
  binding.id +
  " command=" +
  binding.command +
  " keys=" +
  binding.keys.canonical +
  " context=" +
  binding.context +
  " platform=" +
  binding.platform +
  " priority=" +
  binding.priority.to_string() +
  " enabled=" +
  enabled +
  " description=" +
  binding.description
}

///|
/// Serialize a keymap back to a review-friendly DSL document.
pub fn keymap_to_dsl(keymap : Keymap) -> String {
  let normalized = normalize_keymap(keymap)
  let lines : Array[String] = [
    "keymap name=" + normalized.name + " version=" + normalized.version,
  ]
  for context in normalized.contexts {
    lines.push(
      "context " +
      context.name +
      " parent=" +
      context.parent +
      " rank=" +
      context.rank.to_string() +
      " description=" +
      context.description,
    )
  }
  for marker in normalized.reserved {
    let (platform, keys) = split_once(marker, ":")
    lines.push("reserve " + keys + " platform=" + platform)
  }
  for binding in normalized.bindings {
    lines.push(binding_to_dsl(binding))
  }
  lines.join("\n")
}

///|
/// Merge an overlay keymap. Existing ids are replaced only when `priority` is
/// at least as high; equal ids with a different command are reported.
pub fn merge_keymaps(base : Keymap, overlay : Keymap) -> MergeResult {
  let bindings = base.bindings.copy()
  let conflicts : Array[String] = []
  let mut adopted = 0
  let mut skipped = 0
  for incoming in overlay.bindings {
    match binding_index({ ..Keymap::empty(), bindings, }, incoming.id) {
      Some(index) => {
        let existing = bindings[index]
        if existing.command != incoming.command {
          conflicts.push(
            "id " +
            incoming.id +
            " changes command from " +
            existing.command +
            " to " +
            incoming.command,
          )
        }
        if incoming.priority >= existing.priority {
          bindings[index] = incoming
          adopted += 1
        } else {
          skipped += 1
        }
      }
      None => {
        bindings.push(incoming)
        adopted += 1
      }
    }
  }
  let merged_contexts = base.contexts.copy()
  for incoming in overlay.contexts {
    if !context_exists({ ..base, contexts: merged_contexts }, incoming.name) {
      merged_contexts.push(incoming)
    }
  }
  let merged_reserved = base.reserved.copy()
  for marker in overlay.reserved {
    if !array_contains(merged_reserved, marker) {
      merged_reserved.push(marker)
    }
  }
  {
    keymap: {
      ..base,
      version: overlay.version,
      bindings,
      contexts: merged_contexts,
      reserved: merged_reserved,
    },
    conflicts,
    adopted,
    skipped,
  }
}

///|
/// Return ids that are declared but cannot be reached in a context.
pub fn unreachable_bindings(
  keymap : Keymap,
  context : String,
  platform : String,
) -> Array[Binding] {
  keymap.bindings.filter(binding => {
    binding.enabled &&
    (
      !binding_active_in_context(keymap, binding.context, context) ||
      !platforms_overlap(binding.platform, platform)
    )
  })
}

///|
/// Return one binding per unique command, choosing the most specific one.
pub fn primary_commands(
  keymap : Keymap,
  context : String,
  platform : String,
) -> Array[Binding] {
  let result : Array[Binding] = []
  let commands : Array[String] = []
  for binding in keymap.bindings {
    if binding.enabled &&
      binding_active_in_context(keymap, binding.context, context) &&
      platforms_overlap(binding.platform, platform) {
      let mut command_index = -1
      for i, command in commands {
        if command == binding.command {
          command_index = i
        }
      }
      if command_index >= 0 {
        let index = command_index
        let winner = more_specific(keymap, result[index], binding)
        result[index] = winner
      } else {
        commands.push(binding.command)
        result.push(binding)
      }
    }
  }
  result
}

///|
/// Produce a compact selection report for API clients.
pub fn selection_to_text(selection : KeymapSelection) -> String {
  let lines : Array[String] = [
    "selection " + selection.query,
    "keymap=" +
    selection.name +
    " count=" +
    selection.bindings.length().to_string(),
  ]
  for binding in selection.bindings {
    lines.push(
      binding.id +
      " " +
      binding.keys.canonical +
      " " +
      binding.command +
      " [" +
      binding.context +
      "]",
    )
  }
  lines.join("\n")
}