///|
fn optional_text(value : String?) -> Json {
  match value {
    Some(s) => Json::string(s)
    None => Json::null()
  }
}

///|
fn strings_json(values : Array[String]) -> Json {
  Json::array(values.map(Json::string))
}

///|
fn person_json(p : Person) -> Json {
  Json::object(
    Map([
      ("id", optional_text(p.id)),
      ("name", Json::string(p.display_name())),
      ("names", strings_json(p.names)),
      ("sex", optional_text(p.sex)),
      ("child_families", strings_json(p.child_families)),
      ("spouse_families", strings_json(p.spouse_families)),
    ]),
  )
}

///|
fn family_json(f : Family) -> Json {
  Json::object(
    Map([
      ("id", optional_text(f.id)),
      ("husband", optional_text(f.husband)),
      ("wife", optional_text(f.wife)),
      ("children", strings_json(f.children)),
    ]),
  )
}

///|
fn node_json(n : Node) -> Json {
  let (kind, value) = match n.payload {
    Empty => ("empty", Json::null())
    Text(t) => ("text", Json::string(t))
    Pointer(id) => ("pointer", Json::string(id))
    Void => ("void", Json::null())
  }
  Json::object(
    Map([
      ("line", Json::number(n.line.to_double())),
      ("level", Json::number(n.level.to_double())),
      ("xref", optional_text(n.xref)),
      ("tag", Json::string(n.tag)),
      ("kind", Json::string(kind)),
      ("value", value),
    ]),
  )
}

///|
fn diagnostic_json(d : Diagnostic) -> Json {
  Json::object(
    Map([
      (
        "severity",
        Json::string(if d.severity == Error { "error" } else { "warning" }),
      ),
      ("code", Json::string(d.code)),
      ("line", Json::number(d.line.to_double())),
      (
        "related_line",
        match d.related_line {
          Some(n) => Json::number(n.to_double())
          None => Json::null()
        },
      ),
      ("message", Json::string(d.message)),
    ]),
  )
}

///|
fn relation_text(relation : Relation) -> String {
  match relation {
    Parent => "parent"
    Child => "child"
    Partner => "partner"
  }
}

///|
fn edge_json(edge : Edge) -> Json {
  Json::object(
    Map([
      ("from", Json::string(edge.from)),
      ("to", Json::string(edge.to)),
      ("relation", Json::string(relation_text(edge.relation))),
      ("family", optional_text(edge.family)),
      ("pedigree", optional_text(edge.pedigree)),
    ]),
  )
}

///|
pub fn full_audit(doc : Document) -> Array[Diagnostic] {
  let out = audit(doc)
  if !has_errors(out) {
    try audit_relationships(doc) catch {
      _ => ()
    } noraise {
      more => out.append(more)
    }
  }
  out.append(audit_dates(doc))
  out.sort_by(fn(a, b) {
    if a.line != b.line {
      a.line - b.line
    } else {
      a.code.compare(b.code)
    }
  })
  out
}

///|
pub fn inspect_json(doc : Document) -> Json {
  let diagnostics = full_audit(doc)
  Json::object(
    Map([
      ("valid", Json::boolean(!has_errors(diagnostics))),
      (
        "validation_scope",
        Json::string(
          "GEDCOM 7.0.18 chapters 1–3: standard structures, cardinalities, enums, datatypes and machine-checkable cross-record rules; extension meanings and external resources are unassessed",
        ),
      ),
      ("node_count", Json::number(doc.length().to_double())),
      ("specification", Json::string("7.0.18")),
      ("people", Json::array(people(doc).map(person_json))),
      ("families", Json::array(families(doc).map(family_json))),
      ("diagnostics", Json::array(diagnostics.map(diagnostic_json))),
    ]),
  )
}

///|
fn csv_cell(value : String, spreadsheet_safe : Bool) -> String {
  let value = if spreadsheet_safe &&
    (
      value.has_prefix("=") ||
      value.has_prefix("+") ||
      value.has_prefix("-") ||
      value.has_prefix("@") ||
      value.has_prefix("\t") ||
      value.has_prefix("\r")
    ) {
    "'" + value
  } else {
    value
  }
  "\"" + value.replace_all(old="\"", new="\"\"") + "\""
}

///|
/// RFC 4180 quoting and CRLF rows. Spreadsheet protection may prefix an apostrophe;
/// disable it explicitly for raw data interchange rather than silently changing data.
pub fn people_csv(doc : Document, spreadsheet_safe~ : Bool = true) -> String {
  let out = StringBuilder()
  out.write_string("id,name,sex,child_families,spouse_families\r\n")
  for p in people(doc) {
    let fields = [
      p.id.unwrap_or(""),
      p.display_name(),
      p.sex.unwrap_or(""),
      p.child_families.join(";"),
      p.spouse_families.join(";"),
    ]
    out.write_string(
      fields.map(fn(s) { csv_cell(s, spreadsheet_safe) }).join(","),
    )
    out.write_string("\r\n")
  }
  out.to_string()
}