///|
// Fresh IDs across independently parsed or copied trees prevent cache collisions.
fn reindex_document(
  value : Value,
  owner : Array[String],
  work : Work,
  depth : Int,
) -> Value raise ParseError {
  tree_step(value, depth, work)
  match value {
    Object(fields) | SealedObject(fields) => {
      let result = Map([])
      for key, child in fields {
        work.spend(key.length())
        result[key] = reindex_document(
          child,
          append_path(owner, [key]),
          work,
          depth + 1,
        )
      }
      rebuild_object(value, result)
    }
    List(items) =>
      List(items.map(v => reindex_document(v, owner, work, depth + 1)))
    Bound(_, _, value) =>
      Bound(
        owner.copy(),
        work.id(),
        reindex_document(value, owner, work, depth + 1),
      )
    Substitution(paths, optional, list_env, _) =>
      Substitution(paths.map(p => p.copy()), optional, list_env, work.id())
    Reference(path) => Substitution([split_path(path)], false, false, work.id())
    PathReference(parts, optional) =>
      Substitution([parts.copy()], optional, false, work.id())
    Concat(parts) =>
      Concat(
        parts.map(p => (p.0, reindex_document(p.1, owner, work, depth + 1))),
      )
    DelayedMerge(a, b) =>
      DelayedMerge(
        reindex_document(a, owner, work, depth + 1),
        reindex_document(b, owner, work, depth + 1),
      )
    _ => value
  }
}

///|
/// Resolve a detached document; missing mandatory references may remain deferred.
pub fn resolve(
  config : Value,
  environment? : Map[String, String] = Map([]),
  allow_unresolved? : Bool = false,
) -> Value raise ParseError {
  let root = reindex_document(config, [], tree_work(), 0)
  resolve_against(root, root, environment, allow_unresolved, true)
}

///|
/// Only the supplied source is searched for substitutions; source fields are not merged.
pub fn resolve_with(
  config : Value,
  source : Value,
  environment? : Map[String, String] = Map([]),
  allow_unresolved? : Bool = false,
) -> Value raise ParseError {
  ignore(object_fields(source))
  let same = physical_equal(config, source) ||
    (match (config, source) {
      (Object(a) | SealedObject(a), Object(b) | SealedObject(b)) =>
        physical_equal(a, b)
      _ => false
    })
  if same {
    return resolve(config, environment~, allow_unresolved~)
  }
  let work = tree_work()
  let root = reindex_document(config, [], work, 0)
  let source = reindex_document(source, [], work, 0)
  resolve_against(root, source, environment, allow_unresolved, false)
}

///|
fn known_object(value : Value) -> Bool {
  match value {
    Object(_) | SealedObject(_) => true
    Bound(_, _, body) => known_object(body)
    DelayedMerge(_, high) => known_object(high)
    _ => false
  }
}

///|
fn peek_document_child(
  value : Value,
  key : String,
  depth : Int,
  work : Work,
) -> Value? raise ParseError {
  tree_step(value, depth, work)
  match value {
    Object(fields) | SealedObject(fields) => fields.get(key)
    Bound(_, _, body) => peek_document_child(body, key, depth + 1, work)
    DelayedMerge(low, high) => {
      if !known_object(high) {
        raise Invalid("unresolved object lookup")
      }
      match peek_document_child(high, key, depth + 1, work) {
        Some(value) => {
          if !known_object(value) &&
            !uncertain(value) &&
            resolved_tree(value, depth + 1, work) {
            return Some(value)
          }
          let previous = peek_document_child(low, key, depth + 1, work)
          match previous {
            Some(previous) => Some(merge(previous, value))
            None => Some(value)
          }
        }
        None => peek_document_child(low, key, depth + 1, work)
      }
    }
    _ =>
      if uncertain(value) {
        raise Invalid("unresolved object lookup")
      } else {
        None
      }
  }
}

///|
/// Required lookup with null/unresolved rejection and known leaves of delayed objects.
fn peek_document_path(config : Value, path : String) -> Value? raise ParseError {
  let parts = split_path(path)
  let mut current = config
  for part in parts {
    let child = match current {
      Object(fields) | SealedObject(fields) => fields.get(part)
      _ => peek_document_child(current, part, 0, tree_work())
    }
    match child {
      None => return None
      Some(value) => current = value
    }
  }
  Some(current)
}

///|
fn required_document_value(
  config : Value,
  path : String,
) -> Value raise ParseError {
  let current = peek_document_path(config, path).unwrap_or_else(() => {
    raise Invalid("missing path: " + path)
  })
  required_leaf(current, path)
}

///|
fn required_leaf(current : Value, path : String) -> Value raise ParseError {
  if current is Null {
    raise Invalid("null path: " + path)
  }
  if uncertain(current) {
    raise Invalid("unresolved path: " + path)
  }
  current
}

///|
pub fn get_value(config : Value, path : String) -> Value raise ParseError {
  let value = peek_document_path(config, path).unwrap_or_else(() => {
    raise Invalid("missing path: " + path)
  })
  if value is Null {
    raise Invalid("null path: " + path)
  }
  ignore(value_type(value))
  copy_value(value)
}

///|
fn check_edit_descent(
  config : Value,
  parts : Array[String],
) -> Unit raise ParseError {
  let mut current = config
  for i = 0; i + 1 < parts.length(); i = i + 1 {
    match current {
      Object(fields) | SealedObject(fields) =>
        current = fields.get(parts[i]).unwrap_or(Null)
      _ => return
    }
    if !(current is (Object(_) | SealedObject(_))) && known_object(current) {
      raise Invalid("cannot edit inside an unresolved merged object")
    }
  }
}