///|
/// Select the zero-based occurrence of a key, preserving duplicate siblings.
pub(all) struct Selector {
  key : String
  occurrence : Int
} derive(Eq, Debug)

///|
pub extend Selector with Eq::{equal, not_equal}

///|
pub extend Selector with @debug.Debug::{to_repr}

///|
pub(all) enum Edit {
  Replace(Value)
  Remove
  Rename(String)
  Insert(Int, Entry)
} derive(Debug)

///|
pub extend Edit with @debug.Debug::{to_repr}

///|
pub suberror EditError {
  InvalidEdit(String)
} derive(Debug)

///|
pub extend EditError with @debug.Debug::{to_repr}

///|
/// Return a changed document without modifying the input arrays. An empty
/// path selects the root object and supports Insert or Replace(Object(...)).
pub fn Document::edit(
  self : Document,
  path : Array[Selector],
  operation : Edit,
) -> Document raise EditError {
  if path.length() > 128 {
    raise InvalidEdit("Edit path exceeds 128 levels")
  }
  if path.is_empty() {
    match operation {
      Remove | Rename(_) =>
        raise InvalidEdit("The root has no key to remove or rename")
      _ => ()
    }
  }
  guard edit_value(Object(self.entries), path, 0, operation) is Object(entries) else {
    raise InvalidEdit("The document root must remain an object")
  }
  { entries, }
}

///|
fn edit_value(
  value : Value,
  path : Array[Selector],
  level : Int,
  operation : Edit,
) -> Value raise EditError {
  if level == path.length() {
    return match operation {
      Replace(replacement) => replacement
      Insert(index, entry) => {
        guard value is Object(entries) else {
          raise InvalidEdit("Insertion target is not an object")
        }
        if index < 0 || index > entries.length() {
          raise InvalidEdit("Insertion index is out of range")
        }
        let result : Array[Entry] = []
        for i in 0.. raise InvalidEdit("Removal and rename must select a keyed entry")
    }
  }
  guard value is Object(entries) else {
    raise InvalidEdit("Path crosses a text value")
  }
  let selector = path[level]
  if selector.occurrence < 0 {
    raise InvalidEdit("Occurrence must be nonnegative")
  }
  let mut seen = 0
  let mut found = false
  let result : Array[Entry] = []
  for entry in entries {
    let selected = entry.key == selector.key && seen == selector.occurrence
    if entry.key == selector.key {
      seen = seen + 1
    }
    if selected {
      found = true
      if level + 1 == path.length() {
        match operation {
          Remove => continue
          Rename(key) => {
            result.push({ key, value: entry.value, })
            continue
          }
          _ => ()
        }
      }
      result.push({
        key: entry.key,
        value: edit_value(entry.value, path, level + 1, operation),
      })
    } else {
      result.push(entry)
    }
  }
  if !found {
    raise InvalidEdit("Key occurrence was not found")
  }
  Object(result)
}