///|
/// The version of the query language that this package compiles.
pub let language_version : Int = 1

///|
/// An expanded name: a namespace URI and a local name. The empty URI means
/// no namespace. Source prefixes are never part of a name.
pub struct Name {
  namespace_uri : String
  local_name : String
} derive(Eq, Hash, Compare, Debug)

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

///|
pub extend Name with Compare::{op_lt, op_le, op_ge, compare, op_gt}

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

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

///|
pub fn Name::new(namespace_uri : String, local_name : String) -> Name {
  { namespace_uri, local_name }
}

///|
/// Clark notation, `{uri}local`, or `local` with no namespace.
pub fn Name::clark(self : Name) -> String {
  if self.namespace_uri == "" {
    self.local_name
  } else {
    "{\{self.namespace_uri}}\{self.local_name}"
  }
}

///|
/// The published contract of a tree adapter.
///
/// `kinds`, `fields` and `categories` are the inventory that queries can
/// name. A kind or field in a namespace of `reserved_namespaces` must be in the
/// inventory. A kind or field in another namespace is valid when
/// `open_extensions` is true, also when no document contains it. With
/// `open_extensions` false, every kind and field must be in the inventory.
/// `scalar_tags` lists the tags of attribute values. `extra_fields` lists the
/// adapter-specific projection fields, for example `id`.
pub struct Schema {
  id : String
  version : Int
  kinds : ReadOnlyArray[Name]
  categories : ReadOnlyArray[String]
  fields : ReadOnlyArray[Name]
  reserved_namespaces : ReadOnlyArray[String]
  open_extensions : Bool
  scalar_tags : ReadOnlyArray[String]
  extra_fields : ReadOnlyArray[String]
  priv kind_set : Map[Name, Bool]
  priv field_set : Map[Name, Bool]
  priv category_set : Map[String, Bool]
}

///|
pub impl Eq for Schema with equal(a, b) {
  a.id == b.id &&
  a.version == b.version &&
  a.kinds == b.kinds &&
  a.categories == b.categories &&
  a.fields == b.fields &&
  a.reserved_namespaces == b.reserved_namespaces &&
  a.open_extensions == b.open_extensions &&
  a.scalar_tags == b.scalar_tags &&
  a.extra_fields == b.extra_fields
}

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

///|
pub impl @debug.Debug for Schema with to_repr(self) {
  @debug.Repr::Repr(("Schema", self.id, self.version))
}

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

///|
pub fn Schema::new(
  id~ : String,
  version~ : Int,
  kinds~ : ArrayView[Name],
  categories~ : ArrayView[String],
  fields~ : ArrayView[Name],
  reserved_namespaces? : ArrayView[String] = [],
  open_extensions? : Bool = false,
  scalar_tags? : ArrayView[String] = ["text"],
  extra_fields? : ArrayView[String] = [],
) -> Schema {
  let kind_set : Map[Name, Bool] = Map([])
  for kind in kinds {
    kind_set[kind] = true
  }
  let field_set : Map[Name, Bool] = Map([])
  for field in fields {
    field_set[field] = true
  }
  let category_set : Map[String, Bool] = Map([])
  for category in categories {
    category_set[category] = true
  }
  {
    id,
    version,
    kinds: ReadOnlyArray::from_array(kinds.to_owned()),
    categories: ReadOnlyArray::from_array(categories.to_owned()),
    fields: ReadOnlyArray::from_array(fields.to_owned()),
    reserved_namespaces: ReadOnlyArray::from_array(reserved_namespaces.to_owned()),
    open_extensions,
    scalar_tags: ReadOnlyArray::from_array(scalar_tags.to_owned()),
    extra_fields: ReadOnlyArray::from_array(extra_fields.to_owned()),
    kind_set,
    field_set,
    category_set,
  }
}

///|
fn Schema::admits(
  self : Schema,
  name : Name,
  declared : Map[Name, Bool],
) -> Bool {
  if declared.contains(name) {
    return true
  }
  self.open_extensions &&
  !self.reserved_namespaces.iter().any(ns => ns == name.namespace_uri)
}

///|
/// True when queries can name `kind` under this schema.
pub fn Schema::admits_kind(self : Schema, kind : Name) -> Bool {
  self.admits(kind, self.kind_set)
}

///|
/// True when queries can name `field` under this schema.
pub fn Schema::admits_field(self : Schema, field : Name) -> Bool {
  self.admits(field, self.field_set)
}

///|
pub fn Schema::has_category(self : Schema, category : String) -> Bool {
  self.category_set.contains(category)
}

///|
/// True when a plan compiled for `self` can execute on a tree of `other`.
/// Compatibility depends on the declared identifier and version, not on the
/// contents of a document.
pub fn Schema::compatible(self : Schema, other : Schema) -> Bool {
  self.id == other.id && self.version == other.version
}