///|
/// A typed libconfig value. Text stores bytes, including non-UTF-8 escapes.
pub(all) enum Value {
  Integer(Int32)
  Integer64(Int64)
  Float(Double)
  Boolean(Bool)
  Text(Bytes)
  Group(Array[(String, Value)])
  ScalarArray(Array[Value])
  List(Array[Value])
} derive(Debug, Eq)

///|
/// Resource limits count input/string bytes and value nodes (excluding root).
pub(all) struct Limits {
  max_input : Int32
  max_depth : Int32
  max_nodes : Int32
  max_string : Int32
} derive(Debug, Eq)

///|
/// Conservative defaults for in-memory configuration files.
pub fn Limits::default() -> Limits {
  { max_input: 1048576, max_depth: 64, max_nodes: 100000, max_string: 262144, }
}

///|
/// Locations are one-based lines/byte-columns; offset is zero-based bytes.
pub(all) struct Diagnostic {
  code : String
  message : String
  offset : Int32
  line : Int32
  column : Int32
} derive(Debug, Eq)

///|
/// Parsing, access and writing errors share a structured diagnostic.
pub suberror ConfigError {
  ConfigError(Diagnostic)
} derive(Debug)

///|
/// Stable type names; array element checking uses the exact scalar type.
pub fn Value::kind(self : Value) -> String {
  match self {
    Integer(_) => "integer"
    Integer64(_) => "integer64"
    Float(_) => "float"
    Boolean(_) => "boolean"
    Text(_) => "string"
    Group(_) => "group"
    ScalarArray(_) => "array"
    List(_) => "list"
  }
}

///|
fn failure(code : String, message : String) -> ConfigError {
  ConfigError({ code, message, offset: 0, line: 1, column: 1, })
}

///|
/// Strict UTF-8 extraction; escaped arbitrary bytes may fail decoding.
pub fn Value::as_string(self : Value) -> String raise ConfigError {
  guard self is Text(bytes) else {
    raise failure("type_mismatch", "expected string")
  }
  @utf8.decode(bytes) catch {
    _ => raise failure("invalid_utf8", "string contains invalid UTF-8 bytes")
  }
}