///|
/// A zero-based byte offset and one-based line/byte-column in the original input.
pub(all) struct SourcePosition {
  offset : Int32
  line : Int32
  column : Int32
} derive(Debug, Eq)

///|
/// Starting positions of a setting name and its value. List elements use the same position.
pub(all) struct SourceLocation {
  setting : SourcePosition
  value : SourcePosition
} derive(Debug, Eq)

///|
/// Parsed values and original source locations. Locations do not follow subsequent edits.
pub struct Document {
  tree : Value
  locations : Map[String, SourceLocation]
}

///|
/// Parse UTF-8 text while preserving setting/value starting locations.
pub fn parse_document(
  input : String,
  limits~ : Limits = Limits::default(),
) -> Document raise ConfigError {
  if input.length() > limits.max_input && limits.max_input >= 0 {
    raise failure("input_limit", "input exceeds byte limit")
  }
  parse_document_bytes(@utf8.encode(input), limits~)
}

///|
/// Parse bytes with locations; no filesystem access is performed.
pub fn parse_document_bytes(
  input : Bytes,
  limits~ : Limits = Limits::default(),
) -> Document raise ConfigError {
  let reader = Reader::new(input, limits)
  reader.trace = true
  let origin = reader.position()
  reader.locations[""] = { setting: origin, value: origin, }
  let tree = Group(reader.settings(0, -1))
  { tree, locations: reader.locations, }
}

///|
/// Return the value tree. Unchanged values/containers are shared with this document.
pub fn Document::root(self : Document) -> Value {
  self.tree
}

///|
fn canonical_path(parts : ArrayView[Segment]) -> String {
  let mut path = ""
  for part in parts {
    path = match part {
      Key(name) => member_path(path, name)
      Index(i) => path + "[" + i.to_string() + "]"
    }
  }
  path
}

///|
/// Return original positions; both peers[0] and peers.[0] address the same location.
pub fn Document::location(
  self : Document,
  path : String,
) -> SourceLocation? raise ConfigError {
  self.locations.get(canonical_path(parse_path(path)))
}

///|
fn Document::annotate(
  self : Document,
  parts : ArrayView[Segment],
  d : Diagnostic,
) -> ConfigError {
  let path = canonical_path(parts)
  let mut nearest = self.locations.get("")
  for i = 1; i <= parts.length(); i = i + 1 {
    match self.locations.get(canonical_path(parts[0:i])) {
      Some(location) => nearest = Some(location)
      None => ()
    }
  }
  let position = match nearest {
    Some(location) => location.value
    None => { offset: 0, line: 1, column: 1, }
  }
  ConfigError({
    code: d.code,
    message: "at " +
    (if path == "" { "" } else { path }) +
    ": " +
    d.message,
    offset: position.offset,
    line: position.line,
    column: position.column,
  })
}

///|
/// Query with source-aware errors. Missing members/indices use the nearest known parent.
pub fn Document::lookup(
  self : Document,
  path : String,
) -> Value raise ConfigError {
  let parts = parse_path(path)
  self.tree.get_path(parts) catch {
    ConfigError(d) => raise self.annotate(parts, d)
  }
}

///|
fn Document::expect(
  self : Document,
  path : String,
  kind : String,
) -> Value raise ConfigError {
  let value = self.lookup(path)
  if value.kind() != kind {
    let ConfigError(d) = failure(
      "type_mismatch",
      "expected " + kind + "; actual " + value.kind(),
    )
    raise self.annotate(parse_path(path), d)
  }
  value
}

///|
/// Strict typed queries, anchored to the original value's position.
pub fn Document::get_int(
  self : Document,
  path : String,
) -> Int32 raise ConfigError {
  self.expect(path, "integer").as_int()
}

///|
pub fn Document::get_int64(
  self : Document,
  path : String,
) -> Int64 raise ConfigError {
  self.expect(path, "integer64").as_int64()
}

///|
pub fn Document::get_double(
  self : Document,
  path : String,
) -> Double raise ConfigError {
  self.expect(path, "float").as_double()
}

///|
pub fn Document::get_bool(
  self : Document,
  path : String,
) -> Bool raise ConfigError {
  self.expect(path, "boolean").as_bool()
}

///|
pub fn Document::get_bytes(
  self : Document,
  path : String,
) -> Bytes raise ConfigError {
  self.expect(path, "string").as_bytes()
}

///|
pub fn Document::get_string(
  self : Document,
  path : String,
) -> String raise ConfigError {
  let value = self.expect(path, "string")
  value.as_string() catch {
    ConfigError(d) => raise self.annotate(parse_path(path), d)
  }
}

///|
fn Reader::position(self : Reader) -> SourcePosition {
  { offset: self.pos, line: self.line, column: self.column, }
}