///|
/// Stable JSON encoding preserves RDF term identity, including ill-typed literals.
pub fn Term::to_json(self : Term) -> Json {
  match self {
    Iri(value) => { "termType": "NamedNode", "value": value }
    Blank(value) => { "termType": "BlankNode", "value": value }
    Literal(value, datatype, language) =>
      {
        "termType": "Literal",
        "value": value,
        "datatype": datatype,
        "language": match language {
          Some(s) => Json::string(s)
          None => Json::null()
        },
      }
  }
}

///|
pub fn Path::to_json(self : Path) -> Json {
  match self {
    Predicate(p) => { "predicate": p }
    Inverse(p) => { "inverse": p.to_json() }
    Sequence(ps) => { "sequence": Json::array(ps.map(p => p.to_json())) }
    Alternative(ps) => { "alternative": Json::array(ps.map(p => p.to_json())) }
    ZeroOrMore(p) => { "zeroOrMore": p.to_json() }
    OneOrMore(p) => { "oneOrMore": p.to_json() }
    ZeroOrOne(p) => { "zeroOrOne": p.to_json() }
  }
}

///|
pub fn ValidationResult::to_json(self : ValidationResult) -> Json {
  {
    "focusNode": self.focus_node.to_json(),
    "sourceShape": self.source_shape.to_json(),
    "sourceConstraintComponent": self.component,
    "resultSeverity": self.severity.to_json(),
    "resultPath": match self.path {
      Some(p) => p.to_json()
      None => Json::null()
    },
    "value": match self.value {
      Some(v) => v.to_json()
      None => Json::null()
    },
    "resultMessage": Json::array(self.messages.map(m => m.to_json())),
    "detail": Json::array(self.details.map(r => r.to_json())),
  }
}

///|
pub fn Report::to_json(self : Report) -> Json {
  {
    "conforms": self.conforms,
    "results": Json::array(self.results.map(r => r.to_json())),
    "work": { "checked": self.checked, "reused": self.reused },
  }
}

///|
pub fn Problem::to_json(self : Problem) -> Json {
  {
    "node": self.node.to_json(),
    "predicate": self.predicate,
    "message": self.message,
  }
}

///|
fn path_triples(
  path : Path,
  next : Array[Int],
  triples : Array[Triple],
) -> Term {
  match path {
    Predicate(p) => Iri(p)
    Inverse(p) | ZeroOrMore(p) | OneOrMore(p) | ZeroOrOne(p) => {
      let n = Blank("path" + next[0].to_string())
      next[0] = next[0] + 1
      let predicate = match path {
        Inverse(_) => "inversePath"
        ZeroOrMore(_) => "zeroOrMorePath"
        OneOrMore(_) => "oneOrMorePath"
        _ => "zeroOrOnePath"
      }
      let object = path_triples(p, next, triples)
      triples.push({ subject: n, predicate: sh + predicate, object, })
      n
    }
    Sequence(ps) | Alternative(ps) => {
      let head = Blank("path" + next[0].to_string())
      next[0] = next[0] + 1
      let items = ps.map(p => path_triples(p, next, triples))
      let mut current = head
      for i, item in items {
        let rest = if i + 1 == items.length() {
          Iri(rdf + "nil")
        } else {
          let node = Blank("path" + next[0].to_string())
          next[0] = next[0] + 1
          node
        }
        triples.push({
          subject: current,
          predicate: rdf + "first",
          object: item,
        })
        triples.push({
          subject: current,
          predicate: rdf + "rest",
          object: rest,
        })
        current = rest
      }
      if path is Alternative(_) {
        let node = Blank("path" + next[0].to_string())
        next[0] = next[0] + 1
        triples.push({
          subject: node,
          predicate: sh + "alternativePath",
          object: head,
        })
        node
      } else {
        head
      }
    }
  }
}

///| Reports are standalone RDF graphs; input blank nodes are relabelled consistently

///|
/// so generated report/path nodes cannot alias caller-supplied blank-node labels.
pub fn Report::to_graph(self : Report) -> Graph {
  let triples = []
  let renamed : Map[Term, Term] = Map([])
  let rename = fn(term : Term) {
    match term {
      Blank(_) =>
        renamed.get_or_init(term, () => {
          Blank("input" + renamed.length().to_string())
        })
      _ => term
    }
  }
  let report = Blank("report")
  triples.push({
    subject: report,
    predicate: rdf + "type",
    object: Iri(sh + "ValidationReport"),
  })
  triples.push({
    subject: report,
    predicate: sh + "conforms",
    object: Literal(self.conforms.to_string(), xsd + "boolean", None),
  })
  let next = [0]
  fn add_result(
    r : ValidationResult,
    parent : Term,
    predicate : String,
  ) -> Unit {
    let node = Blank("result" + next[0].to_string())
    next[0] = next[0] + 1
    triples.push({ subject: parent, predicate, object: node, })
    triples.push({
      subject: node,
      predicate: rdf + "type",
      object: Iri(sh + "ValidationResult"),
    })
    for
      (predicate, object) in [
        ("focusNode", r.focus_node),
        ("sourceShape", r.source_shape),
        ("sourceConstraintComponent", Iri(r.component)),
        ("resultSeverity", r.severity),
      ] {
      triples.push({
        subject: node,
        predicate: sh + predicate,
        object: rename(object),
      })
    }
    match r.path {
      Some(path) =>
        triples.push({
          subject: node,
          predicate: sh + "resultPath",
          object: path_triples(path, next, triples),
        })
      None => ()
    }
    match r.value {
      Some(value) =>
        triples.push({
          subject: node,
          predicate: sh + "value",
          object: rename(value),
        })
      None => ()
    }
    for message in r.messages {
      triples.push({
        subject: node,
        predicate: sh + "resultMessage",
        object: rename(message),
      })
    }
    for detail in r.details {
      add_result(detail, node, sh + "detail")
    }
  }
  for result in self.results {
    add_result(result, report, sh + "result")
  }
  Graph::new(triples)
}

///|
pub fn Graph::to_ntriples(self : Graph) -> String {
  self.triples
  .map(t => {
    t.subject.to_ntriples() +
    " <" +
    t.predicate +
    "> " +
    t.object.to_ntriples() +
    " ."
  })
  .join("\n")
}