///|
pub(all) struct Change {
  path : Array[Selector]
  operation : Edit
} derive(Debug)

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

///|
/// Sequential paths address the result of the preceding change. A failed batch
/// never modifies the original document. Final output must satisfy text limits.
pub fn Document::apply(
  self : Document,
  changes : Array[Change],
  options? : ParseOptions = ParseOptions::default(),
) -> Document raise {
  if changes.length() > 10000 {
    raise InvalidEdit("A transaction supports at most 10000 changes")
  }
  if changes.length() > 1 && changes.all(c => c.operation is Replace(_)) {
    try {
      let result = self.apply_snapshot(changes)
      parse(stringify(result), options~) |> ignore
      return result
    } catch {
      InvalidEdit(_) => ()
      error => raise error
    }
  }
  let mut result = self
  for change in changes {
    result = result.edit(change.path, change.operation)
  }
  let text = stringify(result)
  parse(text, options~) |> ignore
  result
}

///|
/// Apply a batch to independent source snapshots; nothing is published on failure.
pub fn SourceDocument::apply(
  self : SourceDocument,
  changes : Array[Change],
) -> SourceDocument raise {
  if changes.length() > 10000 {
    raise InvalidEdit("A transaction supports at most 10000 changes")
  }
  if changes.length() > 1 && changes.all(c => c.operation is Replace(_)) {
    try {
      return self.apply_snapshot(changes)
    } catch {
      InvalidEdit(_) => ()
      error => raise error
    }
  }
  let mut result = self
  for change in changes {
    result = result.edit(change.path, change.operation)
  }
  result
}

///|
/// Portable JSON plan: [{path:[{key,occurrence}],op,value/key/index/entry}].
/// Values and entries use the same ordered representation as Document::to_json.
pub fn changes_from_json(json : Json) -> Array[Change] raise {
  guard json is Array(rows) && rows.length() <= 10000 else {
    raise InvalidEdit("Expected an array of at most 10000 changes")
  }
  let result : Array[Change] = []
  for row in rows {
    guard row is Object(fields) &&
      fields.get("path") is Some(Array(raw_path)) &&
      fields.get("op") is Some(String(op)) else {
      raise InvalidEdit("Change requires path and op")
    }
    let path : Array[Selector] = []
    for selector in raw_path {
      guard selector is Object(s) &&
        s.length() == 2 &&
        s.get("key") is Some(String(key)) &&
        s.get("occurrence") is Some(Number(n, ..)) &&
        n >= 0 &&
        n <= 2147483647.0 &&
        n == n.to_int().to_double() else {
        raise InvalidEdit("Invalid path selector")
      }
      path.push({ key, occurrence: n.to_int(), })
    }
    if path.length() > 128 {
      raise InvalidEdit("Path exceeds 128 levels")
    }
    let operation = match op {
      "remove" => {
        if fields.length() != 2 {
          raise InvalidEdit("Unexpected remove fields")
        }
        Edit::Remove
      }
      "rename" => {
        guard fields.length() == 3 && fields.get("key") is Some(String(key)) else {
          raise InvalidEdit("rename requires key")
        }
        Rename(key)
      }
      "replace" => {
        guard fields.length() == 3 && fields.get("value") is Some(value) else {
          raise InvalidEdit("replace requires value")
        }
        Replace(plan_value(value))
      }
      "insert" => {
        guard fields.length() == 4 &&
          fields.get("index") is Some(Number(n, ..)) &&
          n >= 0 &&
          n <= 2147483647.0 &&
          n == n.to_int().to_double() &&
          fields.get("entry") is Some(entry) else {
          raise InvalidEdit("insert requires integer index and entry")
        }
        let doc = document_from_json(Json::array([entry]))
        Insert(n.to_int(), doc.entries[0])
      }
      _ => raise InvalidEdit("Unknown change operation: " + op)
    }
    result.push({ path, operation, })
  }
  result
}

///|
fn plan_value(json : Json) -> Value raise {
  match json {
    String(s) => Text(s)
    Array(_) => Object(document_from_json(json).entries)
    _ => raise InvalidEdit("Value must be text or an ordered entry array")
  }
}