///|
/// Reconstruct a stack item against a loaded document. Retain deleted content
/// when persisting documents whose undo stacks will be restored later.
pub fn[M] StackItem::StackItem(
  doc : Doc,
  insertions~ : IdSet = IdSet(),
  deletions~ : IdSet = IdSet(),
  meta~ : M,
) -> StackItem[M] {
  {
    document: doc,
    inserted: insertions.ranges(),
    deleted: deletions.ranges(),
    meta,
  }
}

///|
fn[M] StackItem::copy(self : StackItem[M]) -> StackItem[M] {
  {
    document: self.document,
    inserted: self.inserted.copy(),
    deleted: self.deleted.copy(),
    meta: self.meta,
  }
}

///|
pub fn[M] StackItem::merge(
  self : StackItem[M],
  other : StackItem[M],
  merge_meta : (M, M) -> M,
) -> Unit {
  guard self.document.identity == other.document.identity else {
    abort("cannot merge undo items from different documents")
  }
  self.inserted = IdSet(ranges=self.inserted + other.inserted).ranges()
  self.deleted = IdSet(ranges=self.deleted + other.deleted).ranges()
  self.meta = merge_meta(self.meta, other.meta)
}

///|
/// Matches the Rust serde stack-item schema, with caller-defined metadata encoding.
pub fn[M] StackItem::to_json(
  self : StackItem[M],
  encode_meta : (M) -> Json,
) -> Json {
  {
    "doc": Json(self.document.guid()),
    "deletions": IdSet(ranges=self.deleted).to_json(),
    "insertions": IdSet(ranges=self.inserted).to_json(),
    "meta": encode_meta(self.meta),
  }
}

///|
pub fn[M] StackItem::from_json(
  json : Json,
  doc : Doc,
  decode_meta : (Json) -> M raise DecodeError,
) -> StackItem[M] raise DecodeError {
  guard json
    is {
      "doc": String(guid),
      "deletions": deletions,
      "insertions": insertions,
      "meta": meta,
      ..
    } else {
    raise InvalidData("invalid undo stack item")
  }
  guard guid == doc.guid() else {
    raise InvalidData("undo stack document GUID mismatch")
  }
  StackItem(
    doc,
    insertions=IdSet::from_json(insertions),
    deletions=IdSet::from_json(deletions),
    meta=decode_meta(meta),
  )
}

///|
pub fn[M] UndoEvent::changed_parent_types(
  self : UndoEvent[M],
) -> ReadOnlyArray[Out] {
  self.changed
}

///|
pub fn[M] UndoEvent::has_changed(self : UndoEvent[M], target : Out) -> Bool {
  guard target.location() is Some((doc, parent)) else { return false }
  self.changed
  .iter()
  .any(fn(value) {
    match value.location() {
      Some((other, location)) =>
        doc.identity == other.identity && parent == location
      None => false
    }
  })
}

///|
fn[M] UndoManager::remove_document(self : UndoManager[M], doc : Doc) -> Unit {
  self.scopes.remove(doc.identity)
  for stack in [self.undo_items, self.redo_items] {
    let keep = stack.filter(fn(entry) {
      entry.document.identity != doc.identity
    })
    stack.clear()
    for entry in keep {
      stack.push(entry)
    }
  }
}

///|
pub fn[M] StackItem::set_meta(self : StackItem[M], meta : M) -> Unit {
  self.meta = meta
}