///|
/// An owned resolved target: the owning resource's retrieval URI (None for
/// an unnamed input document), the pointer within it, and the immutable value
/// found there.
pub struct Location {
  resource : UriReference?
  pointer : JsonPointer
  target : @json.Value
} derive(Debug)

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

///|
pub fn Location::resource(self : Location) -> UriReference? {
  self.resource
}

///|
pub fn Location::pointer(self : Location) -> JsonPointer {
  self.pointer
}

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

///|
/// Normalized resource identity plus pointer; equal for every access path.
pub fn Location::identity(self : Location) -> String {
  let resource = match self.resource {
    Some(uri) => uri.identity()
    None => ""
  }
  resource + "#" + self.pointer.to_string()
}

///|
/// One reference in a chain: where it appeared (None for the initial input),
/// its source text, and its absolute form when a base was available.
pub struct ReferenceStep {
  site : Location?
  reference : Reference
  resolved : UriReference?
} derive(Debug)

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

///|
pub fn ReferenceStep::site(self : ReferenceStep) -> Location? {
  self.site
}

///|
pub fn ReferenceStep::reference(self : ReferenceStep) -> Reference {
  self.reference
}

///|
pub fn ReferenceStep::resolved(self : ReferenceStep) -> UriReference? {
  self.resolved
}

///|
/// A resolution result. `complete` is false when the answer depends on input
/// that was not supplied or on work beyond the limits; a broken pointer or a
/// cycle is a complete failure.
pub struct Resolution {
  target : Location?
  priv chain : Array[ReferenceStep]
  priv diagnostics : Array[@json.Diagnostic]
  complete : Bool
} derive(Debug)

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

///|
pub fn Resolution::target(self : Resolution) -> Location? {
  self.target
}

///|
pub fn Resolution::chain(self : Resolution) -> Array[ReferenceStep] {
  self.chain.copy()
}

///|
pub fn Resolution::diagnostics(self : Resolution) -> Array[@json.Diagnostic] {
  self.diagnostics.copy()
}

///|
pub fn Resolution::complete(self : Resolution) -> Bool {
  self.complete
}

///|
priv enum Hop {
  Found(Location)
  Failed(@json.Diagnostic, Bool)
}

///|
fn ResourceSet::hop(
  self : ResourceSet,
  reference : Reference,
  base : UriReference?,
  chain : Array[ReferenceStep],
  site : Location?,
  limits : @json.Limits,
) -> Hop {
  let absolute = if reference.uri.is_absolute() {
    Ok(reference.uri)
  } else {
    match base {
      Some(base) if base.is_absolute() => reference.uri.resolve(base, limits~)
      _ => {
        chain.push({ site, reference, resolved: None, })
        return Failed(
          @json.Diagnostic::new(
            @json.MissingBaseUri,
            "Relative reference " +
            reference.text() +
            " has no absolute base URI",
          ),
          false,
        )
      }
    }
  }
  let absolute = match absolute {
    Ok(uri) => uri
    Err(d) => {
      chain.push({ site, reference, resolved: None, })
      return Failed(d, d.code() != @json.LimitExceeded)
    }
  }
  chain.push({ site, reference, resolved: Some(absolute), })
  let resource = match self.get(absolute) {
    Some(resource) => resource
    None =>
      return Failed(
        @json.Diagnostic::new(
          @json.UnavailableResource,
          "Resource " + absolute.without_fragment().text() + " was not supplied",
        ),
        false,
      )
  }
  let pointer = match reference.fragment {
    WholeResource => JsonPointer::{ parts: [], }
    PointerFragment(pointer) => pointer
    IdentifierFragment(name) =>
      return Failed(
        @json.Diagnostic::new(
          @json.UnsupportedCheck,
          "Identifier fragment #" + name + " requires a schema scope index",
        ),
        false,
      )
  }
  match pointer.resolve(resource.value, limits~) {
    Ok(target) => Found({ resource: Some(resource.uri), pointer, target, })
    Err(d) => Failed(d, d.code() != @json.LimitExceeded)
  }
}

///|
/// Resolve one reference against the explicit resources. A relative reference
/// uses `base`, else the set's root base. Nothing is fetched or executed.
pub fn ResourceSet::resolve(
  self : ResourceSet,
  reference : Reference,
  base~ : UriReference? = None,
  limits~ : @json.Limits = @json.Limits::new(),
) -> Resolution {
  let chain = []
  let base = if base is Some(_) { base } else { self.root_base }
  match self.hop(reference, base, chain, None, limits) {
    Found(location) =>
      { target: Some(location), chain, diagnostics: [], complete: true, }
    Failed(d, complete) => { target: None, chain, diagnostics: [d], complete, }
  }
}

///|
/// Follow a reference chain while `next` reports that a target is itself a
/// reference. Each later reference resolves against its owning resource URI.
/// Revisiting a location is a `ReferenceCycle`; chains longer than
/// `max_depth` are incomplete.
pub fn ResourceSet::follow(
  self : ResourceSet,
  reference : Reference,
  next : (Location) -> Reference?,
  base~ : UriReference? = None,
  limits~ : @json.Limits = @json.Limits::new(),
) -> Resolution {
  let chain = []
  let seen : Set[String] = Set([])
  let mut base = if base is Some(_) { base } else { self.root_base }
  let mut current = reference
  let mut site : Location? = None
  while true {
    if chain.length() >= limits.max_depth() {
      return {
        target: None,
        chain,
        diagnostics: [limit_error("Reference chain length limit exceeded")],
        complete: false,
      }
    }
    match self.hop(current, base, chain, site, limits) {
      Failed(d, complete) =>
        return { target: None, chain, diagnostics: [d], complete, }
      Found(location) => {
        if !seen.add_and_check(location.identity()) {
          return {
            target: None,
            chain,
            diagnostics: [
              @json.Diagnostic::new(
                @json.ReferenceCycle,
                "Reference chain revisits " + location.identity(),
                path=location.pointer.tokens(),
              ),
            ],
            complete: true,
          }
        }
        match next(location) {
          Some(following) => {
            current = following
            base = location.resource
            site = Some(location)
          }
          None =>
            return {
              target: Some(location),
              chain,
              diagnostics: [],
              complete: true,
            }
        }
      }
    }
  }
  panic()
}