// model.mbt — Core data model for moon-weblink.
//
// The model follows the abstract Web Linking model of RFC 8288 Section 2:
// a link has a context, a target (URI-reference), a relation type set, and
// optional target attributes. In the HTTP serialisation the context is the
// `anchor` parameter (defaulting to the current representation), the target
// is the URI-reference inside the angle brackets, and the target attributes
// are `hreflang`, `media`, `title`, `title*` and `type` plus arbitrary
// extension attributes.
//
// Design notes:
//   - A link can carry several relation types (RFC 8288 Section 3.3: the
//     `rel` value is a space separated list); establishing a separate link
//     per relation is left to consumers and to the JSON conversion.
//   - `RelationType::Registered` is the token form (RFC 8288 Section 3.3
//     `reg-rel-type`). Its string is stored exactly as parsed (including
//     case) so that parsing and serialising round-trips byte-for-byte;
//     comparison is case-insensitive per RFC 8288 Section 2.1.1.
//   - `RelationType::Extension` is an absolute URI (RFC 8288 `ext-rel-type`).
//   - `title_star` carries the RFC 8187 extended value. RFC 8288 says that
//     when both `title` and `title*` are present, applications SHOULD use
//     the `title*` value; the model preserves both.
//   - The deprecated `rev` parameter is preserved as an ordinary extension
//     parameter so no information is lost; the audit layer flags it.
//   - `extensions` keeps every parameter that is not a serialisation-defined
//     attribute, including duplicates, in input order.

///|
/// The relation type of a link. `Registered` is the token form; `Extension`
/// is an absolute URI.
pub enum RelationType {
  Registered(String)
  Extension(String)
}

///|
/// An RFC 8187 extended value (used by `title*` and `name*` parameters):
/// a charset, an optional language tag, and the decoded value.
pub struct ExtendedValue {
  charset : String
  language : String?
  value : String
}

///|
/// A single link parameter. `value` is the unquoted content; `quoted`
/// records whether the original serialisation used the quoted-string form
/// (used to preserve round-trip fidelity for extension parameters).
pub struct LinkParameter {
  name : String
  value : String?
  quoted : Bool
}

///|
/// One parsed link. `target` is the URI-reference inside the angle
/// brackets; `relations` the relation types of the `rel` parameter;
/// `anchor` the optional overriding context; `hreflang` the repeatable
/// language hint; `media`, `title`, `title_star` and `media_type` the
/// serialisation-defined target attributes; `extensions` every other
/// parameter in input order.
pub struct WebLink {
  target : String
  relations : Array[RelationType]
  anchor : String?
  hreflang : Array[String]
  media : String?
  title : String?
  title_star : ExtendedValue?
  media_type : String?
  extensions : Array[LinkParameter]
}

///|
/// A set of links. This is the model shared by the HTTP `Link` header
/// field, the `application/linkset` text format and the
/// `application/linkset+json` format.
pub struct LinkSet {
  links : Array[WebLink]
}

///|
/// Constructs an empty `LinkSet`.
pub fn LinkSet::new() -> LinkSet {
  { links: Array::new() }
}

///|
/// Constructs a `LinkSet` holding the given links, in order.
pub fn LinkSet::from_links(links : Array[WebLink]) -> LinkSet {
  { links, }
}

///|
/// Constructs a `WebLink` with the given target and no parameters.
pub fn web_link(target : String) -> WebLink {
  {
    target,
    relations: Array::new(),
    anchor: None,
    hreflang: Array::new(),
    media: None,
    title: None,
    title_star: None,
    media_type: None,
    extensions: Array::new(),
  }
}

///|
/// The links in this set, in order.
pub fn LinkSet::links(self : LinkSet) -> Array[WebLink] {
  self.links
}

///|
/// The number of links in this set.
pub fn LinkSet::link_count(self : LinkSet) -> Int {
  self.links.length()
}

///|
/// Whether the set contains no links.
pub fn LinkSet::is_empty(self : LinkSet) -> Bool {
  self.links.is_empty()
}

///|
/// Appends a link to the set.
pub fn LinkSet::add(self : LinkSet, link : WebLink) -> Unit {
  self.links.push(link)
}

///|
/// The link target (URI-reference).
pub fn WebLink::target(self : WebLink) -> String {
  self.target
}

///|
/// The relation types of this link.
pub fn WebLink::relations(self : WebLink) -> Array[RelationType] {
  self.relations
}

///|
/// The optional overriding link context.
pub fn WebLink::anchor(self : WebLink) -> String? {
  self.anchor
}

///|
/// The repeatable `hreflang` values.
pub fn WebLink::hreflang(self : WebLink) -> Array[String] {
  self.hreflang
}

///|
/// The `media` target attribute, if present.
pub fn WebLink::media(self : WebLink) -> String? {
  self.media
}

///|
/// The `title` target attribute, if present.
pub fn WebLink::title(self : WebLink) -> String? {
  self.title
}

///|
/// The `title*` target attribute, if present.
pub fn WebLink::title_star(self : WebLink) -> ExtendedValue? {
  self.title_star
}

///|
/// The `type` target attribute, if present.
pub fn WebLink::media_type(self : WebLink) -> String? {
  self.media_type
}

///|
/// The extension parameters, in input order.
pub fn WebLink::extensions(self : WebLink) -> Array[LinkParameter] {
  self.extensions
}

///|
/// The charset of this extended value.
pub fn ExtendedValue::charset(self : ExtendedValue) -> String {
  self.charset
}

///|
/// The optional language tag of this extended value.
pub fn ExtendedValue::language(self : ExtendedValue) -> String? {
  self.language
}

///|
/// The decoded value of this extended value.
pub fn ExtendedValue::value(self : ExtendedValue) -> String {
  self.value
}

///|
/// The parameter name.
pub fn LinkParameter::name(self : LinkParameter) -> String {
  self.name
}

///|
/// The parameter value (unquoted), if present.
pub fn LinkParameter::value(self : LinkParameter) -> String? {
  self.value
}

///|
/// Whether the original serialisation used the quoted-string form.
pub fn LinkParameter::quoted(self : LinkParameter) -> Bool {
  self.quoted
}

///|
/// The relation name of a registered (token) relation type.
pub fn RelationType::name(self : RelationType) -> String? {
  match self {
    Registered(name) => Some(name)
    Extension(_) => None
  }
}

///|
/// The extension URI of an extension relation type.
pub fn RelationType::extension_uri(self : RelationType) -> String? {
  match self {
    Registered(_) => None
    Extension(uri) => Some(uri)
  }
}

///|
/// Whether this is the registered (token) form.
pub fn RelationType::is_registered(self : RelationType) -> Bool {
  match self {
    Registered(_) => true
    Extension(_) => false
  }
}

///|
/// Whether this is the extension (absolute URI) form.
pub fn RelationType::is_extension(self : RelationType) -> Bool {
  match self {
    Registered(_) => false
    Extension(_) => true
  }
}

///|
/// Whether this relation type matches the given name or URI.
///
/// Registered relation types are compared case-insensitively
/// (RFC 8288 Section 2.1.1); extension relation types are compared as
/// exact strings (RFC 8288 Section 2.1.2).
pub fn RelationType::matches(self : RelationType, name : String) -> Bool {
  match self {
    Registered(n) => n.equal_ignore_ascii_case(name)
    Extension(uri) => uri == name
  }
}

///|
/// Whether any relation of this link matches the given name or URI.
pub fn WebLink::has_relation(self : WebLink, name : String) -> Bool {
  self.relations.any(fn(rt) { rt.matches(name) })
}

///|
/// The relation names/URIs of this link as plain strings, in order.
pub fn WebLink::relation_names(self : WebLink) -> Array[String] {
  let out = Array::new()
  for rt in self.relations {
    match rt {
      Registered(name) => out.push(name)
      Extension(uri) => out.push(uri)
    }
  }
  out
}