///|
/// XML Schema nonNegativeInteger has no machine-word upper bound.
pub struct CountHint {
  priv decimal : String
}

///|
pub fn CountHint::parse(input : String) -> CountHint raise GraphError {
  let token = input.trim(chars=" \t\r\n").to_owned()
  let negative = token.has_prefix("-")
  let start = if token.has_prefix("+") || negative { 1 } else { 0 }
  require(token.length() > start, "parse.count", "empty nonnegative integer")
  for i = start; i < token.length(); i = i + 1 {
    let c = token.code_unit_at(i).to_int()
    require(c >= 48 && c <= 57, "parse.count", "invalid nonnegative integer")
  }
  let mut first = start
  while first + 1 < token.length() && token.code_unit_at(first).to_int() == 48 {
    first = first + 1
  }
  let decimal = token[first:].to_owned()
  require(!negative || decimal == "0", "parse.count", "negative integer")
  { decimal, }
}

///|
pub fn CountHint::decimal(self : CountHint) -> String {
  self.decimal
}

///|
/// Return None on machine-word overflow without invalidating a legal XML Schema value.
pub fn CountHint::to_int64(self : CountHint) -> Int64? {
  Some(@string.parse_int64(self.decimal)) catch {
    _ => None
  }
}

///|
pub(all) enum IdHint {
  CanonicalIds
  FreeIds
} derive(Eq, @debug.Debug)

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

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

///|
pub(all) enum OrderHint {
  FreeOrder
  NodesFirst
  AdjacencyList
} derive(Eq, @debug.Debug)

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

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

///|
pub(all) struct ParseInfo {
  nodes : CountHint?
  edges : CountHint?
  max_in_degree : CountHint?
  max_out_degree : CountHint?
  node_ids : IdHint?
  edge_ids : IdHint?
  order : OrderHint?
}

///|
pub(all) struct DegreeHints {
  in_degree : CountHint?
  out_degree : CountHint?
}

///|
fn hint_attribute(
  attributes : Array[XmlAttributeValue],
  name : String,
) -> String? {
  for attribute in attributes {
    if attribute.namespace_uri is None && attribute.name == name {
      return Some(attribute.value)
    }
  }
  None
}

///|
fn count_hint(
  attributes : Array[XmlAttributeValue],
  name : String,
) -> CountHint? raise GraphError {
  match hint_attribute(attributes, name) {
    Some(value) => Some(CountHint::parse(value))
    None => None
  }
}

///|
fn id_hint(
  attributes : Array[XmlAttributeValue],
  name : String,
) -> IdHint? raise GraphError {
  match
    hint_attribute(attributes, name).map(fn(value) {
      value.trim(chars=" \t\r\n").to_owned()
    }) {
    Some("canonical") => Some(CanonicalIds)
    Some("free") => Some(FreeIds)
    None => None
    _ => raise InvalidGraph(path=name, reason="expected canonical or free")
  }
}

///|
/// Parse hints are advisory metadata; lexical validity does not prove content consistency.
pub fn Graph::parse_info(self : Graph) -> ParseInfo raise GraphError {
  let order = match
    hint_attribute(self.extra, "parse.order").map(fn(value) {
      value.trim(chars=" \t\r\n").to_owned()
    }) {
    Some("free") => Some(FreeOrder)
    Some("nodesfirst") => Some(NodesFirst)
    Some("adjacencylist") => Some(AdjacencyList)
    None => None
    _ => raise InvalidGraph(path="parse.order", reason="invalid order hint")
  }
  {
    nodes: count_hint(self.extra, "parse.nodes"),
    edges: count_hint(self.extra, "parse.edges"),
    max_in_degree: count_hint(self.extra, "parse.maxindegree"),
    max_out_degree: count_hint(self.extra, "parse.maxoutdegree"),
    node_ids: id_hint(self.extra, "parse.nodeids"),
    edge_ids: id_hint(self.extra, "parse.edgeids"),
    order,
  }
}

///|
pub fn Node::degree_hints(self : Node) -> DegreeHints raise GraphError {
  {
    in_degree: count_hint(self.extra, "parse.indegree"),
    out_degree: count_hint(self.extra, "parse.outdegree"),
  }
}

///|
/// Explicit flat-profile check. Nested graphs are intentionally not assigned invented counting rules.
pub fn ParseInfo::check_flat_counts(
  self : ParseInfo,
  graph : Graph,
) -> Unit raise GraphError {
  require_flat(graph)
  if self.nodes is Some(count) {
    require(
      count.decimal == graph.nodes.length().to_string(),
      "parse.nodes",
      "flat node count disagrees with stored structure",
    )
  }
  if self.edges is Some(count) {
    require(
      count.decimal == graph.edges.length().to_string(),
      "parse.edges",
      "flat edge count disagrees with stored structure",
    )
  }
  if self.node_ids == Some(CanonicalIds) {
    for i = 0; i < graph.nodes.length(); i = i + 1 {
      require(
        graph.nodes[i].id == "n\{i}",
        "parse.nodeids",
        "flat canonical node IDs must follow n0,n1,...",
      )
    }
  }
  if self.edge_ids == Some(CanonicalIds) {
    for i = 0; i < graph.edges.length(); i = i + 1 {
      require(
        graph.edges[i].id == Some("e\{i}"),
        "parse.edgeids",
        "flat canonical edge IDs must follow e0,e1,...",
      )
    }
  }
}