///|
pub enum Cmd[Sub] {
  Insert(Sub)
  Modify(old~ : Sub, new~ : Sub)
  Remove(Sub)
}

///|
pub fn[T, U] Cmd::map(self : Cmd[T], map : (T) -> U) -> Cmd[U] {
  match self {
    Insert(sub) => Insert(map(sub))
    Modify(old~, new~) => Modify(old=map(old), new=map(new))
    Remove(sub) => Remove(map(sub))
  }
}

///|
fn[Sub] diff(
  old~ : Map[String, Sub],
  new~ : Map[String, Sub],
) -> Array[Cmd[Sub]] {
  let cmds = []
  for old_ent in old.iter().collect() {
    let (old_key, old_sub) = old_ent
    if new.get(old_key) is Some(new_sub) {
      if !physical_equal(old_sub, new_sub) {
        old[old_key] = new_sub
        cmds.push(Modify(old=old_sub, new=new_sub))
      }
    } else {
      old.remove(old_key)
      cmds.push(Remove(old_sub))
    }
  }
  for new_key, new_sub in new {
    if !old.contains(new_key) {
      old[new_key] = new_sub
      cmds.push(Insert(new_sub))
    }
  }
  cmds
}

///|
pub(all) struct Subscribe[Model, Message]((Model, (Message) -> Unit) -> Unit)

///|
/// A single subscription source: a function that extracts a keyed map of
/// subscriptions from the model, paired with a `manage` handler that
/// processes `Insert`/`Modify`/`Remove` commands. Compose multiple sources
/// into one app-level `Subscribe` with `subscribe`.
pub fn[Model, Msg, Sub] Subscribe::new(
  subscribe : (Model) -> Map[String, Sub],
  manage : (Cmd[Sub], (Msg) -> Unit) -> Unit,
) -> Subscribe[Model, Msg] {
  let old = Map([])
  (model, dispatch) => {
    let new = subscribe(model)
    let cmds = diff(old~, new~)
    for cmd in cmds {
      manage(cmd, dispatch)
    }
  }
}

///|
/// Combine multiple sources into one `Subscribe` for the app. Each inner
/// source keeps its own diff state and runs independently, so communication
/// between sources flows through `Model` rather than between subscriptions.
pub fn[Model, Msg] subscribe(
  subs : Array[Subscribe[Model, Msg]],
) -> Subscribe[Model, Msg] {
  (model, dispatch) => {
    for s in subs {
      s(model, dispatch)
    }
  }
}