///|
/// A source location. Columns count Unicode scalar values, starting at one.
#derive(Eq, Hash, @debug.Debug)
pub(all) struct Location {
  source : String
  line : Int32
  column : Int32
}

///|
pub extend Location with Hash::{hash, hash_combine}

///|
/// Diagnostics are errors in the supported subset, not pkgconf exit codes.
#derive(Eq, Hash, @debug.Debug)
pub(all) struct Diagnostic {
  code : String
  message : String
  location : Location
}

///|
pub extend Diagnostic with Hash::{hash, hash_combine}

///|
#derive(Eq, @debug.Debug)
pub(all) enum EntryKind {
  Variable
  Field
}

///|
/// Original value is retained for explaining substitutions.
#derive(Eq, @debug.Debug)
pub(all) struct Entry {
  kind : EntryKind
  name : String
  raw : String
  value : String
  location : Location
  value_location : Location
}

///|
/// A parsed document. Invalid entries are not available to subsequent queries.
#derive(Eq, @debug.Debug)
pub(all) struct Document {
  entries : Array[Entry]
  diagnostics : Array[Diagnostic]
}

///|
#derive(Eq, @debug.Debug)
pub(all) enum VersionOp {
  Any
  Equal
  NotEqual
  Less
  LessEqual
  Greater
  GreaterEqual
}

///|
/// A named dependency with an optional version constraint.
#derive(Eq, @debug.Debug)
pub(all) struct Requirement {
  name : String
  op : VersionOp
  version : String
}

///|
pub fn Document::field(self : Document, name : String) -> Entry? {
  let key = field_key(name)
  for e in self.entries {
    if e.kind == Field && field_key(e.name) == key {
      return Some(e)
    }
  }
  None
}

///|
// Only standard fields ignore ASCII case. Extension fields and variables keep
// their spelling, and entries retain the source's original field name.
fn field_key(name : String) -> String {
  for
    known in [
      "Name", "Description", "Version", "URL", "Requires", "Requires.private", "Conflicts",
      "Provides", "Cflags", "Cflags.private", "Libs", "Libs.private",
    ] {
    if name.equal_ignore_ascii_case(known) {
      return known
    }
  }
  name
}

///|
pub fn Document::variable(self : Document, name : String) -> Entry? {
  for e in self.entries {
    if e.kind == Variable && e.name == name {
      return Some(e)
    }
  }
  None
}

///|
fn diag(code : String, message : String, location : Location) -> Diagnostic {
  { code, message, location, }
}

///|
fn chars_text(chars : Array[Char], start : Int32, end : Int32) -> String {
  let out = StringBuilder()
  for i = start; i < end; i = i + 1 {
    out.write_char(chars[i])
  }
  out.to_string()
}

///|
fn whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\r' || c == '\n'
}