///|
/// Per-program action-command sync state (plan
/// hot-path-invalidation-boilerplate-convergence D1). `runtime_commands(model)`
/// is re-derived after every message, but the map and its dispatch handles only
/// need rebuilding when the derived command set actually changes: scroll and
/// pointer storms re-derive an unchanged list many times per frame, and a
/// rebuild would reallocate every closure each time.
///
/// Correctness with model-dependent messages: the fingerprint covers only the
/// `ActionCommand` list (intent, shortcut, label, enabled, ...). The handles do
/// not capture the message values from build time — they index into
/// `latest_commands`, which every sync refreshes, so a handle always dispatches
/// the message derived from the current model even when the map itself is
/// reused. A changed fingerprint (menu opened, command disabled, set edited)
/// rebuilds the map against the new list.
priv struct ProgramCommandSync[Msg] {
  latest_commands : Ref[Array[@core.ProgramCommand[Msg]]]
  mut cached_commands : Array[@core.ActionCommand]
  mut cached_map : ActionCommandMap?
}

///|
fn[Msg] ProgramCommandSync::new() -> ProgramCommandSync[Msg] {
  { latest_commands: Ref([]), cached_commands: [], cached_map: None, }
}

///|
/// Folds a freshly derived command list into the cache. Returns the map to
/// install, or `None` when the derived `ActionCommand` list equals the cached
/// one — the installed map is already current, so the caller skips
/// `set_action_commands` entirely.
fn[Msg] ProgramCommandSync::sync(
  self : ProgramCommandSync[Msg],
  commands : Array[@core.ProgramCommand[Msg]],
  dispatch : (Msg) -> Unit,
) -> ActionCommandMap? {
  self.latest_commands.val = commands
  let fingerprints = commands.map(command => command.command())
  match self.cached_map {
    Some(_) if fingerprints == self.cached_commands => {
      // Unchanged set: keep the closures and the map; refresh the fingerprint
      // copy so the cache does not pin the previous derivation's array.
      self.cached_commands = fingerprints
      None
    }
    _ => {
      let latest = self.latest_commands
      let bindings : Array[@core.CommandBinding] = []
      for index, program_command in commands {
        bindings.push(
          @core.CommandBinding::new(command=program_command.command(), handle=() => {
            let current = latest.val
            if index < current.length() {
              dispatch(current[index].message())
            }
          }),
        )
      }
      let map = ActionCommandMap::new(bindings~)
      self.cached_commands = fingerprints
      self.cached_map = Some(map)
      Some(map)
    }
  }
}