///|
/// A segment of a JSON pointer as seen by `Specification::maybe_in_subresource`
/// (Python's `int | str`): array indices are `Index`, everything else `Key`.
pub(all) enum Segment {
  Key(String)
  Index(Int)
} derive(Debug, Eq)

///|
/// Whether the segment is the string `key` (Python's `segment == key`).
pub fn Segment::is_key(self : Segment, key : String) -> Bool {
  self is Key(k) && k == key
}

///|
/// A specification which defines referencing behavior.
///
/// The various callbacks of a `Specification` allow for varying referencing
/// behavior across JSON Schema specification versions, etc. Each callback is
/// a first-class closure stored in a field, and also callable with method
/// syntax (`spec.id_of(contents)`).
pub struct Specification {
  /// A short human-readable name for the specification, used for debugging.
  name : String
  /// Find the ID of a given document.
  id_of : (Json) -> String?
  /// Retrieve the subresources of the given document (without traversing into
  /// the subresources themselves).
  subresources_of : (Json) -> Array[Json]
  /// While resolving a JSON pointer, conditionally enter a subresource
  /// (if e.g. we have just entered a keyword whose value is a subresource).
  /// Arguments are `(segments, resolver, subresource)`.
  maybe_in_subresource : (Array[Segment], Resolver, Resource) -> Resolver raise
  /// Retrieve the anchors contained in the given document. Arguments are
  /// `(specification, contents)`; see `Specification::anchors_in`.
  anchors_in : (Specification, Json) -> Array[Anchor]
}

///|
/// Create a new specification from its callbacks.
pub fn Specification::new(
  name~ : String,
  id_of~ : (Json) -> String?,
  subresources_of~ : (Json) -> Array[Json],
  anchors_in~ : (Specification, Json) -> Array[Anchor],
  maybe_in_subresource~ : (Array[Segment], Resolver, Resource) -> Resolver raise,
) -> Specification {
  { name, id_of, subresources_of, maybe_in_subresource, anchors_in, }
}

///|
/// Find the ID of a given document.
pub fn Specification::id_of(self : Specification, contents : Json) -> String? {
  (self.id_of)(contents)
}

///|
/// Retrieve the subresources of the given document.
pub fn Specification::subresources_of(
  self : Specification,
  contents : Json,
) -> Array[Json] {
  (self.subresources_of)(contents)
}

///|
/// Retrieve the anchors contained in the given document.
pub fn Specification::anchors_in(
  self : Specification,
  contents : Json,
) -> Array[Anchor] {
  (self.anchors_in)(self, contents)
}

///|
/// Conditionally enter a subresource while resolving a JSON pointer.
pub fn Specification::maybe_in_subresource(
  self : Specification,
  segments : Array[Segment],
  resolver : Resolver,
  subresource : Resource,
) -> Resolver raise {
  (self.maybe_in_subresource)(segments, resolver, subresource)
}

///|
/// Create a resource which is interpreted using this specification.
pub fn Specification::create_resource(
  self : Specification,
  contents : Json,
) -> Resource {
  { contents, specification: self, }
}

///|
/// An opaque specification where resources have no subresources nor internal
/// identifiers (Python's `Specification.OPAQUE`).
pub let opaque_specification : Specification = Specification::new(
  name="opaque",
  id_of=_ => None,
  subresources_of=_ => [],
  anchors_in=(_, _) => [],
  maybe_in_subresource=(_, resolver, _) => resolver,
)

///|
/// Attempt to discern which specification applies to the given contents.
///
/// Mirrors Python's `Specification.detect`, which may be called either as a
/// class method (here: no `default`) or as an instance method (here:
/// `default=that_specification`). Recall that not all contents contain enough
/// information about which specification they are written for -- the JSON
/// Schema `{}`, for instance, is valid under many different dialects.
///
/// * Without `default`, raises `CannotDetermineSpecification` if the contents
///   are not an object or have no string `$schema`, and `UnknownDialect` if
///   `$schema` names an unknown dialect.
/// * With `default`, that specification is returned for unidentifiable or
///   unknown-dialect contents.
pub fn Specification::detect(
  contents : Json,
  default? : Specification,
) -> Specification raise ReferencingError {
  match default {
    None => {
      guard contents is Object(obj) else {
        raise CannotDetermineSpecification(contents~)
      }
      guard obj.get("$schema") is Some(String(dialect_id)) else {
        raise CannotDetermineSpecification(contents~)
      }
      specification_with(dialect_id)
    }
    Some(default) => {
      guard contents is Object(obj) else { return default }
      match obj.get("$schema") {
        Some(String(dialect_id)) => specification_with(dialect_id, default~)
        // Python raises an AttributeError for a non-string `$schema` here;
        // we fall back to the default instead.
        _ => default
      }
    }
  }
}

///|
/// Specifications compare equal when they are the same object, or have the
/// same name and the very same callbacks (as with upstream's attrs equality,
/// where functions compare by identity).
pub impl Eq for Specification with fn equal(self, other) {
  physical_equal(self, other) ||
  (
    self.name == other.name &&
    physical_equal(self.id_of, other.id_of) &&
    physical_equal(self.subresources_of, other.subresources_of) &&
    physical_equal(self.maybe_in_subresource, other.maybe_in_subresource) &&
    physical_equal(self.anchors_in, other.anchors_in)
  )
}

///|
/// ``, as Python's `repr`.
pub impl Show for Specification with fn output(self, logger) {
  logger.write_string("")
}