///|
/// Create a change that reverses this change against the supplied full base
/// document. The base is required because deleted content is absent from a
/// Delta change.
pub fn Delta::invert(self : Delta, base : Delta) -> Delta raise DeltaError {
  self.invert_with_handlers(base, default_embed_handlers)
}

///|
/// Invert a change with registered algorithms for embed payload updates.
pub fn Delta::invert_with_handlers(
  self : Delta,
  base : Delta,
  handlers : EmbedHandlers,
) -> Delta raise DeltaError {
  if !base.is_document() {
    raise Invalid("invert requires an insert-only base document")
  }
  let result = Delta::new()
  let mut offset = 0
  for op in self.ops {
    match op {
      InsertText(_, _) | InsertEmbed(_, _) => ignore(result.delete(op.length()))
      Delete(length) => {
        if offset + length > base.length() {
          raise Invalid("delete exceeds the base document")
        }
        for original in base.slice(offset, offset + length).ops {
          ignore(result.push(original))
        }
        offset += length
      }
      Retain(length, attrs) => {
        if offset + length > base.length() {
          raise Invalid("retain exceeds the base document")
        }
        let segment = base.slice(offset, offset + length)
        for original in segment.ops {
          let inverted = attributes_invert(attrs, original.attributes())
          ignore(result.retain(original.length(), inverted))
        }
        offset += length
      }
      RetainEmbed(update, attrs) => {
        if offset + 1 > base.length() {
          raise Invalid("embed retain exceeds the base document")
        }
        let original = base.slice(offset, offset + 1)
        let original_op = original.ops[0]
        match original_op {
          InsertEmbed(original_embed, original_attrs) => {
            let (kind, update_data, original_data) = embed_parts(update, original_embed)
            let handler = handlers.require(kind)
            let inverse_data = (handler.invert_value)(update_data, original_data)
            let inverse_attrs = attributes_invert(attrs, original_attrs)
            ignore(result.retain_embed(wrap_embed(kind, inverse_data), inverse_attrs))
          }
          _ => raise Invalid("embed retain requires an embed in the base document")
        }
        offset += 1
      }
    }
  }
  result.chop()
}