///|
/// Largest array index `setpath` will grow an array to (jq's limit).
const MAX_SETPATH_INDEX : Int32 = 536870911

///|
/// Helper to set value at a path in JSON structure.
///
/// Like jq, missing structure is created: a `null` becomes an object for a
/// string segment or an array for a number segment, and arrays are padded
/// with `null` up to the index being set. Negative indices count from the
/// end. Other type mismatches leave the value unchanged.
fn set_at_path(
  root : Json,
  path : ArrayView[Json],
  value : Json,
) -> Json raise PrimitiveError {
  match path {
    [] => value
    [key, .. rest] => {
      guard path_segment(key) is Some(segment) else { return root }
      guard segment.read(root) is Some(current) else { return root }
      segment.write(root, set_at_path(current, rest, value))
    }
  }
}

///|
/// Sort and group raw path prefixes as jq does. Child edits are spliced back
/// before deleting this level's keys; overlapping slices can therefore change
/// the positions addressed by a later group.
fn delete_paths(
  root : Json,
  paths : ArrayView[ArrayView[Json]],
) -> Json raise PrimitiveError {
  if paths.is_empty() {
    return root
  }
  let sorted = paths.to_owned()
  sorted.sort_by(compare_array_view)
  if sorted[0].is_empty() {
    return null
  }
  delete_paths_sorted(root, sorted, 0)
}

///|
fn delete_paths_sorted(
  root : Json,
  paths : ArrayView[ArrayView[Json]],
  depth : Int32,
) -> Json raise PrimitiveError {
  let mut updated = root
  let removed : Array[PathSegment] = []
  for i = 0; i < paths.length(); {
    let key = paths[i][depth]
    let mut end = i + 1
    while end < paths.length() && paths[end][depth] == key {
      end += 1
    }
    if path_segment(key) is Some(segment) {
      if paths[i].length() == depth + 1 {
        removed.push(segment)
      } else if segment.read(updated) is Some(current) && !(current is Null) {
        let next = delete_paths_sorted(current, paths[i:end], depth + 1)
        updated = segment.write(updated, next)
      }
    }
    continue end
  }
  delete_segments(updated, removed)
}

///|
/// Delete sibling targets simultaneously, normalizing indices and slice bounds
/// against the container after all child edits have been applied.
fn delete_segments(root : Json, segments : Array[PathSegment]) -> Json {
  match root {
    Object(obj) => {
      let out = obj.copy()
      for segment in segments {
        if segment is Field(key) {
          out.remove(key)
        }
      }
      Json::object(out)
    }
    Array(arr) => {
      let removed : Map[Int32, Bool] = Map([])
      for segment in segments {
        match segment {
          Element(i) => removed[normalize_index(i, arr.length())] = true
          Region(start, end) => {
            let (first, last) = slice_bounds(start, end, arr.length())
            for i in first.. ()
        }
      }
      let out : Array[Json] = []
      for i, v in arr {
        if !removed.contains(i) {
          out.push(v)
        }
      }
      Json::array(out)
    }
    _ => root
  }
}