///|
pub(all) enum TermKind {
  IriKind
  BlankKind
  LiteralKind
} derive(Eq, @debug.Debug)

///|
pub struct Term {
  priv kind : TermKind
  priv lexical : String
  priv datatype : String
  priv language : String?
} derive(Eq, @debug.Debug)

///|
pub fn xsd_namespace() -> String {
  "http://www.w3.org/2001/XMLSchema#"
}

///|
pub fn rdf_namespace() -> String {
  "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
}

///|
fn check_iri(value : String) -> Unit raise QueryError {
  if value.length() == 0 || !value.contains(":") {
    raise InvalidTerm("an absolute IRI is required")
  }
  for c in value.to_array() {
    let n = c.to_int()
    if n <= 32 ||
      n == 127 ||
      c == '<' ||
      c == '>' ||
      c == '"' ||
      c == '{' ||
      c == '}' ||
      c == '|' ||
      c == '^' ||
      c == '`' ||
      c == '\\' {
      raise InvalidTerm("forbidden IRI character")
    }
  }
}

///|
pub fn iri(value : String) -> Term raise QueryError {
  check_iri(value)
  { kind: IriKind, lexical: value, datatype: "", language: None, }
}

///|
pub fn blank(value : String) -> Term raise QueryError {
  if value.length() == 0 {
    raise InvalidTerm("empty blank node identifier")
  }
  for c in value.to_array() {
    if c.to_int() <= 32 || c == '<' || c == '>' || c == '"' || c == '\\' {
      raise InvalidTerm("invalid blank node identifier")
    }
  }
  { kind: BlankKind, lexical: value, datatype: "", language: None, }
}

///|
pub fn literal(value : String) -> Term {
  {
    kind: LiteralKind,
    lexical: value,
    datatype: xsd_namespace() + "string",
    language: None,
  }
}

///|
pub fn typed_literal(
  value : String,
  datatype : String,
) -> Term raise QueryError {
  check_iri(datatype)
  if datatype == rdf_namespace() + "langString" {
    raise InvalidTerm("rdf:langString requires a language tag")
  }
  { kind: LiteralKind, lexical: value, datatype, language: None, }
}

///|
pub fn language_literal(
  value : String,
  language : String,
) -> Term raise QueryError {
  let parts = language.split("-").to_array()
  if parts.length() == 0 || language.length() == 0 {
    raise InvalidTerm("empty language tag")
  }
  for i, part in parts {
    if part.length() == 0 {
      raise InvalidTerm("empty language subtag")
    }
    for c in part.to_array() {
      let n = c.to_int()
      let alpha = (n >= 65 && n <= 90) || (n >= 97 && n <= 122)
      if !alpha && !(i > 0 && n >= 48 && n <= 57) {
        raise InvalidTerm("invalid language subtag")
      }
    }
  }
  {
    kind: LiteralKind,
    lexical: value,
    datatype: rdf_namespace() + "langString",
    language: Some(language.to_lower()),
  }
}

///|
pub fn Term::kind(self : Term) -> TermKind {
  self.kind
}

///|
pub fn Term::lexical(self : Term) -> String {
  self.lexical
}

///|
pub fn Term::datatype(self : Term) -> String? {
  if self.kind == LiteralKind {
    Some(self.datatype)
  } else {
    None
  }
}

///|
pub fn Term::language(self : Term) -> String? {
  self.language
}

///|
fn key_part(s : String) -> String {
  s.length().to_string() + ":" + s
}

///|
fn Term::key(self : Term) -> String {
  match self.kind {
    IriKind => "I" + key_part(self.lexical)
    BlankKind => "B" + key_part(self.lexical)
    LiteralKind =>
      "L" +
      key_part(self.lexical) +
      key_part(self.datatype) +
      key_part(self.language.unwrap_or(""))
  }
}

///|
fn json_quote(s : String) -> String {
  Json::string(s).stringify()
}

///|
pub fn Term::ntriples(self : Term) -> String {
  match self.kind {
    IriKind => "<" + self.lexical + ">"
    BlankKind => "_:" + self.lexical
    LiteralKind => {
      let text = json_quote(self.lexical)
      match self.language {
        Some(lang) => text + "@" + lang
        None =>
          if self.datatype == xsd_namespace() + "string" {
            text
          } else {
            text + "^^<" + self.datatype + ">"
          }
      }
    }
  }
}

///|
pub struct Quad {
  priv subject : Term
  priv predicate : Term
  priv object : Term
  priv graph : Term?
} derive(Eq, @debug.Debug)

///|
pub fn quad(
  subject : Term,
  predicate : Term,
  object : Term,
  graph? : Term,
) -> Quad raise QueryError {
  if subject.kind == LiteralKind || predicate.kind != IriKind {
    raise InvalidTerm("subject must be resource and predicate must be IRI")
  }
  match graph {
    Some(g) =>
      if g.kind != IriKind {
        raise InvalidTerm("graph name must be IRI")
      }
    None => ()
  }
  { subject, predicate, object, graph, }
}

///|
pub fn Quad::subject(self : Quad) -> Term {
  self.subject
}

///|
pub fn Quad::predicate(self : Quad) -> Term {
  self.predicate
}

///|
pub fn Quad::object(self : Quad) -> Term {
  self.object
}

///|
pub fn Quad::graph(self : Quad) -> Term? {
  self.graph
}

///|
fn Quad::key(self : Quad) -> String {
  let g = match self.graph {
    None => "D"
    Some(g) => g.key()
  }
  self.subject.key() + self.predicate.key() + self.object.key() + g
}

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

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

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

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

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

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