///|
/// An immutable caller-supplied JSON resource. Its retrieval URI and aliases
/// are absolute and fragment-free; the value can be any extracted JSON fragment.
pub struct Resource {
  uri : UriReference
  value : @json.Value
  priv aliases : Array[UriReference]
  priv schema_roots : Array[JsonPointer]
  priv nodes : Int32
} derive(Debug)

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

///|
fn retrieval_name(
  uri : UriReference,
  role : String,
) -> Result[Unit, @json.Diagnostic] {
  if !uri.is_absolute() {
    return Err(
      @json.Diagnostic::new(
        @json.InvalidReference,
        role + " must be an absolute URI",
      ),
    )
  }
  if uri.fragment is Some(_) {
    return Err(
      @json.Diagnostic::new(
        @json.InvalidReference,
        role + " must not have a fragment",
      ),
    )
  }
  Ok(())
}

///|
/// Count value nodes iteratively, stopping once `budget` is exceeded.
fn node_count(value : @json.Value, budget : Int32) -> Int32? {
  let stack = [value]
  let mut count = 0
  while stack.pop() is Some(current) {
    count += 1
    if count > budget {
      return None
    }
    match current.kind() {
      Array(items) => stack.append(items)
      Object(fields) =>
        for field in fields {
          stack.push(field.1)
        }
      _ => ()
    }
  }
  Some(count)
}

///|
fn limit_error(message : String) -> @json.Diagnostic {
  @json.Diagnostic::new(@json.LimitExceeded, message)
}

///|
pub fn Resource::new(
  uri : UriReference,
  value : @json.Value,
  aliases~ : Array[UriReference] = [],
  schema_roots~ : Array[JsonPointer] = [],
  limits~ : @json.Limits = @json.Limits::new(),
) -> Result[Resource, @json.Diagnostic] {
  if aliases.length() > limits.max_nodes() - 1 ||
    schema_roots.length() > limits.max_nodes() - 1 {
    return Err(limit_error("Resource registration limit exceeded"))
  }
  if retrieval_name(uri, "Resource URI") is Err(d) {
    return Err(d)
  }
  for name in aliases {
    if retrieval_name(name, "Resource alias") is Err(d) {
      return Err(d)
    }
  }
  let nodes = match node_count(value, limits.max_nodes()) {
    Some(n) => n
    None => return Err(limit_error("Resource value node limit exceeded"))
  }
  for root in schema_roots {
    if root.resolve(value, limits~) is Err(d) {
      return Err(d)
    }
  }
  Ok({
    uri,
    value,
    aliases: aliases.copy(),
    schema_roots: schema_roots.copy(),
    nodes,
  })
}

///|
pub fn Resource::uri(self : Resource) -> UriReference {
  self.uri
}

///|
pub fn Resource::value(self : Resource) -> @json.Value {
  self.value
}

///|
pub fn Resource::aliases(self : Resource) -> Array[UriReference] {
  self.aliases.copy()
}

///|
/// Locations whose values are declared draft-07 schema roots.
pub fn Resource::schema_roots(self : Resource) -> Array[JsonPointer] {
  self.schema_roots.copy()
}

///|
/// Explicit immutable resource snapshots with an optional root base URI.
/// Lookup uses syntax-based URI identity and never retrieves anything.
pub struct ResourceSet {
  priv resources : Array[Resource]
  root_base : UriReference?
  priv index : Map[String, Int32]
} derive(Debug)

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

///|
pub fn ResourceSet::empty() -> ResourceSet {
  { resources: [], root_base: None, index: Map([]), }
}

///|
pub fn ResourceSet::new(
  resources : Array[Resource],
  root_base~ : UriReference? = None,
  limits~ : @json.Limits = @json.Limits::new(),
) -> Result[ResourceSet, @json.Diagnostic] {
  let mut registrations = 0
  let mut nodes = 0
  for resource in resources {
    if resource.aliases.length() >= limits.max_nodes() - registrations {
      return Err(limit_error("Resource registration limit exceeded"))
    }
    registrations += resource.aliases.length() + 1
    if resource.nodes > limits.max_nodes() - nodes {
      return Err(limit_error("Resource set node limit exceeded"))
    }
    nodes += resource.nodes
  }
  let index : Map[String, Int32] = Map([])
  for i, resource in resources {
    for name in [resource.uri, ..resource.aliases] {
      let identity = name.identity()
      match index.get(identity) {
        Some(owner) if owner != i =>
          return Err(
            @json.Diagnostic::new(
              @json.ConflictingIdentifier,
              "Resource identity " +
              name.text() +
              " is already registered by " +
              resources[owner].uri.text(),
            ),
          )
        _ => index.set(identity, i)
      }
    }
  }
  Ok({ resources: resources.copy(), root_base, index, })
}

///|
pub fn ResourceSet::resources(self : ResourceSet) -> Array[Resource] {
  self.resources.copy()
}

///|
pub fn ResourceSet::root_base(self : ResourceSet) -> UriReference? {
  self.root_base
}

///|
/// The resource registered under the fragment-free identity of `uri`.
pub fn ResourceSet::get(self : ResourceSet, uri : UriReference) -> Resource? {
  match self.index.get(uri.without_fragment().identity()) {
    Some(i) => Some(self.resources[i])
    None => None
  }
}