///|
pub suberror AccessError {
  InvalidAccess(Array[Selector], String)
} derive(Debug)

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

///|
pub(all) struct LocatedValue {
  path : Array[Selector]
  value : Value
} derive(Eq, Debug)

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

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

///|
/// Exact, duplicate-aware lookup. Errors retain the complete requested path.
pub fn Document::lookup_at(
  self : Document,
  path : Array[Selector],
) -> Value raise AccessError {
  if path.length() > 128 {
    raise InvalidAccess(path, "Path exceeds 128 levels")
  }
  let mut current = Value::Object(self.entries)
  for selector in path {
    if selector.occurrence < 0 {
      raise InvalidAccess(path, "Negative occurrence")
    }
    guard current is Object(entries) else {
      raise InvalidAccess(path, "Path crosses a scalar")
    }
    let mut count = 0
    let mut found = None
    for entry in entries {
      if entry.key == selector.key {
        if count == selector.occurrence {
          found = Some(entry.value)
          break
        }
        count += 1
      }
    }
    guard found is Some(value) else {
      raise InvalidAccess(path, "Path does not exist")
    }
    current = value
  }
  current
}

///|
/// Follow every matching duplicate at every level, retaining document order.
pub fn Document::lookup_all(
  self : Document,
  path : Array[String],
) -> Array[Value] raise AccessError {
  if path.length() > 128 {
    raise InvalidAccess([], "Path exceeds 128 levels")
  }
  let mut current = [Value::Object(self.entries)]
  for key in path {
    let next : Array[Value] = []
    for value in current {
      if value is Object(entries) {
        for entry in entries {
          if entry.key == key {
            next.push(entry.value)
          }
        }
      }
    }
    current = next
  }
  current
}

///|
/// Preorder visitor; returning false skips this object's descendants. Paths are
/// independent snapshots. The root itself is not emitted. Exceptions propagate.
pub fn Document::walk(
  self : Document,
  visitor : (Array[Selector], Value) -> Bool raise,
) -> Unit raise {
  walk_values(self.entries, [], visitor)
}

///|
fn walk_values(
  entries : Array[Entry],
  path : Array[Selector],
  visitor : (Array[Selector], Value) -> Bool raise,
) -> Unit raise {
  if path.length() > 128 {
    raise InvalidAccess(path, "Traversal exceeds 128 levels")
  }
  let occurrences : Map[String, Int] = Map([])
  for entry in entries {
    let occurrence = occurrences.get(entry.key).unwrap_or(0)
    occurrences[entry.key] = occurrence + 1
    let next = [..path, Selector::{ key: entry.key, occurrence, }]
    if visitor(next.map(s => s), entry.value) {
      if entry.value is Object(children) {
        walk_values(children, next, visitor)
      }
    }
  }
}

///|
pub fn Document::find(
  self : Document,
  key : String,
) -> Array[LocatedValue] raise {
  let result : Array[LocatedValue] = []
  self.walk((path, value) => {
    if path[path.length() - 1].key == key {
      result.push({ path, value, })
    }
    true
  })
  result
}

///|
pub fn Document::read_text(
  self : Document,
  path : Array[Selector],
) -> String raise AccessError {
  guard self.lookup_at(path) is Text(text) else {
    raise InvalidAccess(path, "Expected a text value")
  }
  text
}

///|
pub fn Document::read_int64(
  self : Document,
  path : Array[Selector],
) -> Int64 raise AccessError {
  let text = self.read_text(path)
  let digits = if text.has_prefix("-") { text[1:].to_owned() } else { text }
  if !decimal_id(digits) {
    raise InvalidAccess(path, "Expected a signed decimal Int64")
  }
  @strconv.parse_int64(text[:]) catch {
    _ => raise InvalidAccess(path, "Int64 is out of range")
  }
}

///|
pub fn Document::read_bool(
  self : Document,
  path : Array[Selector],
) -> Bool raise AccessError {
  match self.read_text(path) {
    "true" | "1" => true
    "false" | "0" => false
    _ => raise InvalidAccess(path, "Expected true, false, 1 or 0")
  }
}

///|
pub fn Document::read_double(
  self : Document,
  path : Array[Selector],
) -> Double raise AccessError {
  let text = self.read_text(path)
  if text.is_empty() || text.trim().to_owned() != text {
    raise InvalidAccess(path, "Expected a finite decimal number")
  }
  let n = @strconv.parse_double(text[:]) catch {
    _ => raise InvalidAccess(path, "Expected a finite decimal number")
  }
  if n.is_inf() || n.is_nan() {
    raise InvalidAccess(path, "Expected a finite decimal number")
  }
  n
}