///|
pub(all) enum Change {
  Added(ReadOnlyArray[Out])
  Removed(Int32)
  Retain(Int32)
}

///|
pub fn Change::to_json(self : Change) -> Json {
  match self {
    Added(values) =>
      { "insert": Json(values.iter().map(Out::to_json).to_array()) }
    Removed(count) => { "delete": Json(count) }
    Retain(count) => { "retain": Json(count) }
  }
}

///|
pub type Delta = DeltaOf[Out]

///|
pub fn Delta::to_json(self : Delta) -> Json {
  let result : Map[String, Json] = Map([])
  let attributes = match self {
    Inserted(value, attributes) => {
      result["insert"] = value.to_json()
      attributes
    }
    Deleted(length) => {
      result["delete"] = Json(length)
      []
    }
    Retain(length, attributes) => {
      result["retain"] = Json(length)
      attributes
    }
  }
  if !attributes.is_empty() {
    let attrs : Map[String, Json] = Map([])
    for (key, value) in attributes {
      attrs[key] = value.to_json()
    }
    result["attributes"] = Json(attrs)
  }
  Json(result)
}

///|
pub struct TextEvent {
  priv route : ReadOnlyArray[PathSegment]
  priv text : Text
  priv changes : ReadOnlyArray[Delta]
}

///|
pub fn TextEvent::target(self : TextEvent) -> Text {
  self.text
}

///|
pub fn TextEvent::delta(self : TextEvent) -> ReadOnlyArray[Delta] {
  self.changes
}

///|
pub struct ArrayEvent {
  priv inserted_ids : ReadOnlyArray[Id]
  priv removed_ids : ReadOnlyArray[Id]
  priv route : ReadOnlyArray[PathSegment]
  priv array : ArrayRef
  priv changes : ReadOnlyArray[Change]
}

///|
pub fn ArrayEvent::target(self : ArrayEvent) -> ArrayRef {
  self.array
}

///|
pub fn ArrayEvent::delta(self : ArrayEvent) -> ReadOnlyArray[Change] {
  self.changes
}

///|
fn Transaction::has_added(self : Transaction, id : Id) -> Bool {
  self.added.any(fn(r) { r.0 == id.client && r.1 <= id.clock && id.clock < r.2 })
}

///|
fn Transaction::has_deleted(self : Transaction, id : Id) -> Bool {
  self.removed.any(fn(r) {
    r.0 == id.client && r.1 <= id.clock && id.clock < r.2
  })
}

///|
fn Transaction::observable(self : Transaction, parent : Parent) -> Bool {
  match parent {
    Root(_) => true
    Nested(id) =>
      !self.has_added(id) &&
      self.document.find(id).map(fn(item) { !item.deleted }).unwrap_or(false)
  }
}

///|
fn Item::values(self : Item, doc : Doc) -> Array[Out] {
  match self.content {
    Values(values) => values.iter().map(fn(v) { Out::Value(v) }).to_array()
    LegacyJson(values) =>
      values.iter().map(fn(value) { Out::Value(value) }).to_array()
    Binary(bytes) => [Value(Buffer(bytes))]
    Embed(value) => [Value(value)]
    Shared(kind) => [doc.shared_output(self.id, kind)]
    Subdocument(child) => [Subdoc(child)]
    _ => []
  }
}

///|
fn Transaction::array_delta(
  self : Transaction,
  parent : Parent,
) -> ReadOnlyArray[Change] {
  let result : Array[Change] = []
  let mut cursor = self.document.branch(parent).head
  fn push(change : Change) -> Unit {
    match (result.last(), change) {
      (Some(Retain(a)), Retain(b)) =>
        result[result.length() - 1] = Retain(a + b)
      (Some(Removed(a)), Removed(b)) =>
        result[result.length() - 1] = Removed(a + b)
      (Some(Added(a)), Added(b)) => {
        let values = a.iter().to_array()
        for value in b {
          values.push(value)
        }
        result[result.length() - 1] = Added(
          ReadOnlyArray::from_array(values[:]),
        )
      }
      _ => result.push(change)
    }
  }
  while cursor is Some(id) {
    let item = self.document.items[id]
    if item.content.offset_length(self.document) > 0 {
      if item.deleted {
        if !self.has_added(id) && self.has_deleted(id) {
          push(Removed(item.content.offset_length(self.document)))
        }
      } else if self.has_added(id) {
        push(Added(ReadOnlyArray::from_array(item.values(self.document)[:])))
      } else {
        push(Retain(item.content.offset_length(self.document)))
      }
    }
    cursor = item.right
  }
  if result.last() is Some(Retain(_)) {
    result.pop() |> ignore
  }
  ReadOnlyArray::from_array(result[:])
}

///|
fn Transaction::text_delta(
  self : Transaction,
  parent : Parent,
) -> ReadOnlyArray[Delta] {
  // Preserve the upstream delta assembler's formatting transitions. Comparing
  // only before/after attributes loses retained formatting after deleted marks.
  let result : Array[Delta] = []
  let old_attrs : Map[String, Any] = Map([])
  let current_attrs : Map[String, Any] = Map([])
  let attrs : Map[String, Any] = Map([])
  let mut action = 0 // 0 none, 1 insert, 2 retain, 3 delete
  let mut inserted : Out? = None
  let mut text = ""
  let mut count = 0
  fn flush() -> Unit {
    match action {
      1 => {
        result.push(
          Inserted(
            inserted.unwrap_or(Value(String(text))),
            ReadOnlyArray::from_array(sorted_attributes(current_attrs)[:]),
          ),
        )
        inserted = None
        text = ""
      }
      2 =>
        result.push(
          Retain(count, ReadOnlyArray::from_array(sorted_attributes(attrs)[:])),
        )
      3 => result.push(Deleted(count))
      _ => ()
    }
    action = 0
    count = 0
  }
  let mut cursor = self.document.branch(parent).head
  while cursor is Some(id) {
    let item = self.document.items[id]
    let added = self.has_added(id)
    let deleted = self.has_deleted(id)
    match item.content {
      Characters(value) =>
        if added {
          if !deleted {
            if action != 1 {
              flush()
              action = 1
            }
            text += value
          }
        } else if deleted {
          if action != 3 {
            flush()
            action = 3
          }
          count += item.content.offset_length(self.document)
        } else if !item.deleted {
          if action != 2 {
            flush()
            action = 2
          }
          count += item.content.offset_length(self.document)
        }
      Embed(_) | Shared(_) =>
        if added {
          if !deleted {
            flush()
            action = 1
            inserted = item.values(self.document).get(0)
            flush()
          }
        } else if deleted {
          if action != 3 {
            flush()
            action = 3
          }
          count += 1
        } else if !item.deleted {
          if action != 2 {
            flush()
            action = 2
          }
          count += 1
        }
      Format(key, value) => {
        if added {
          if !deleted && current_attrs.get(key) != Some(value) {
            if action == 2 {
              flush()
            }
            if old_attrs.get(key).unwrap_or(Null) == value {
              attrs.remove(key)
            } else {
              attrs[key] = value
            }
          }
        } else if deleted {
          old_attrs[key] = value
          let current = current_attrs.get(key).unwrap_or(Null)
          if current != value {
            if action == 2 {
              flush()
            }
            attrs[key] = current
          }
        } else if !item.deleted {
          old_attrs[key] = value
          if attrs.get(key) is Some(previous) && previous != value {
            if action == 2 {
              flush()
            }
            update_attribute(attrs, key, value)
          }
        }
        if !item.deleted {
          if action == 1 {
            flush()
          }
          update_attribute(current_attrs, key, value)
        }
      }
      _ => ()
    }
    cursor = item.right
  }
  flush()
  while result.last() is Some(Retain(_, attrs)) && attrs.is_empty() {
    result.pop() |> ignore
  }
  ReadOnlyArray::from_array(result[:])
}

///|
pub fn Text::observe(
  self : Text,
  key : String,
  callback : (TextEvent, Transaction) -> Unit,
) -> Unit {
  let observers = match self.doc.text_observers.get(self.name) {
    Some(observers) => observers
    None => {
      let observers : Observer[String, (TextEvent, Transaction) -> Unit] = Observer()
      self.doc.text_observers[self.name] = observers
      observers
    }
  }
  observers.subscribe(key, callback)
}

///|
pub fn Text::unobserve(self : Text, key : String) -> Bool {
  guard self.doc.text_observers.get(self.name) is Some(observers) else {
    return false
  }
  let found = observers.unsubscribe(key)
  if observers.is_empty() {
    self.doc.text_observers.remove(self.name)
  }
  found
}

///|
pub fn ArrayRef::observe(
  self : ArrayRef,
  key : String,
  callback : (ArrayEvent, Transaction) -> Unit,
) -> Unit {
  let observers = match self.doc.array_observers.get(self.name) {
    Some(observers) => observers
    None => {
      let observers : Observer[String, (ArrayEvent, Transaction) -> Unit] = Observer()
      self.doc.array_observers[self.name] = observers
      observers
    }
  }
  observers.subscribe(key, callback)
}

///|
pub fn ArrayRef::unobserve(self : ArrayRef, key : String) -> Bool {
  guard self.doc.array_observers.get(self.name) is Some(observers) else {
    return false
  }
  let found = observers.unsubscribe(key)
  if observers.is_empty() {
    self.doc.array_observers.remove(self.name)
  }
  found
}

///|
/// IDs of inserted blocks, before post-transaction block compaction.
pub fn ArrayEvent::inserts(self : ArrayEvent) -> ReadOnlyArray[Id] {
  self.inserted_ids
}

///|
pub fn ArrayEvent::removes(self : ArrayEvent) -> ReadOnlyArray[Id] {
  self.removed_ids
}

///|
fn Transaction::sequence_ids(
  self : Transaction,
  parent : Parent,
) -> (ReadOnlyArray[Id], ReadOnlyArray[Id]) {
  let added = []
  let removed = []
  let mut cursor = self.document.branch(parent).head
  while cursor is Some(id) {
    let item = self.document.items[id]
    if item.deleted {
      if self.has_deleted(id) && !self.has_added(id) {
        removed.push(id)
      }
    } else if self.has_added(id) {
      added.push(id)
    }
    cursor = item.right
  }
  (ReadOnlyArray::from_array(added[:]), ReadOnlyArray::from_array(removed[:]))
}