///|
/// A batch either returns its complete result or raises without mutating input.
pub(all) enum Patch {
  Add(String, Value)
  Replace(String, Value)
  Remove(String)
  Test(String, Value)
  Copy(String, String)
  Move(String, String)
}

///|
/// Dictionary order is ignored; numeric types remain distinct. NaNs compare equal.
pub fn equivalent(left : Value, right : Value) -> Bool {
  match (left, right) {
    (Real(a), Real(b)) => a == b || (a.is_nan() && b.is_nan())
    (List(a), List(b)) => {
      if a.length() != b.length() {
        return false
      }
      for i = 0; i < a.length(); i = i + 1 {
        if !equivalent(a[i], b[i]) {
          return false
        }
      }
      true
    }
    (Dict(a), Dict(b)) => {
      if a.length() != b.length() {
        return false
      }
      for (key, value) in a {
        match right.get(key) {
          Some(v) => if !equivalent(value, v) { return false }
          None => return false
        }
      }
      true
    }
    _ => left == right
  }
}

///|
pub fn apply_patch(
  value : Value,
  operations : Array[Patch],
  limits~ : Limits = Limits::default(),
) -> Value raise PlistError {
  validate(value, limits~)
  if operations.length() > limits.max_items {
    raise Limit("patch operation budget")
  }
  let mut result = value
  for operation in operations {
    result = match operation {
      Add(path, v) => insert(result, path, v, limits~)
      Replace(path, v) => replace(result, path, v, limits~)
      Remove(path) => remove(result, path, limits~)
      Test(path, expected) => {
        validate(expected, limits~)
        if !equivalent(lookup(result, path, limits~), expected) {
          raise PathError("patch test failed")
        }
        result
      }
      Copy(source, target) =>
        insert(result, target, lookup(result, source, limits~), limits~)
      Move(source, target) => {
        let item = lookup(result, source, limits~)
        let from = parse_pointer(source)
        let dest = parse_pointer(target)
        let mut prefix = from.length() < dest.length()
        if prefix {
          for i = 0; i < from.length(); i = i + 1 {
            if from[i] != dest[i] {
              prefix = false
            }
          }
        }
        if prefix {
          raise PathError("cannot move into descendant")
        }
        if source == target {
          result
        } else {
          insert(remove(result, source, limits~), target, item, limits~)
        }
      }
    }
  }
  result
}