///|
pub struct Person {
  record : Int32
  id : String?
  names : Array[String]
  sex : String?
  child_families : Array[String]
  spouse_families : Array[String]
}

///|
pub struct Family {
  record : Int32
  id : String?
  husband : String?
  wife : String?
  children : Array[String]
}

///|
pub struct Event {
  node : Int32
  tag : String
  date : String?
  place : String?
  sources : Array[String]
  notes : Array[String]
}

///|
fn texts(doc : Document, parent : Int32, tag : String) -> Array[String] {
  let values = []
  for index in doc.select(parent, tag) {
    if doc.nodes[index].payload is Text(s) {
      values.push(s)
    }
  }
  values
}

///|
fn pointers(doc : Document, parent : Int32, tag : String) -> Array[String] {
  let values = []
  for index in doc.select(parent, tag) {
    if doc.nodes[index].payload is Pointer(s) {
      values.push(s)
    }
  }
  values
}

///|
fn person_view(doc : Document, index : Int32) -> Person {
  {
    record: index,
    id: doc.nodes[index].xref,
    names: texts(doc, index, "NAME"),
    sex: texts(doc, index, "SEX").get(0),
    child_families: pointers(doc, index, "FAMC"),
    spouse_families: pointers(doc, index, "FAMS"),
  }
}

///|
fn family_view(doc : Document, index : Int32) -> Family {
  {
    record: index,
    id: doc.nodes[index].xref,
    husband: pointers(doc, index, "HUSB").get(0),
    wife: pointers(doc, index, "WIFE").get(0),
    children: pointers(doc, index, "CHIL"),
  }
}

///|
pub fn people(doc : Document) -> Array[Person] {
  doc.records
  .filter(fn(i) { doc.nodes[i].tag == "INDI" })
  .map(fn(i) { person_view(doc, i) })
}

///|
pub fn families(doc : Document) -> Array[Family] {
  doc.records
  .filter(fn(i) { doc.nodes[i].tag == "FAM" })
  .map(fn(i) { family_view(doc, i) })
}

///|
fn require_record(
  doc : Document,
  id : String,
  tag : String,
) -> Int32 raise GedError {
  let index = match doc.ids.get(id) {
    Some(i) => i
    None => raise Query("unknown identifier " + id)
  }
  if doc.nodes[index].tag != tag || doc.nodes[index].level != 0 {
    raise Query("identifier does not name a " + tag + " record")
  }
  // A query must not silently resolve an ambiguous duplicate definition.
  let count = doc.nodes.filter(fn(n) { n.xref == Some(id) }).length()
  if count != 1 {
    raise Query("ambiguous duplicate identifier " + id)
  }
  index
}

///|
pub fn person(doc : Document, id : String) -> Person raise GedError {
  person_view(doc, require_record(doc, id, "INDI"))
}

///|
pub fn family(doc : Document, id : String) -> Family raise GedError {
  family_view(doc, require_record(doc, id, "FAM"))
}

///|
pub fn Person::display_name(self : Person) -> String {
  match self.names.get(0) {
    Some(name) => name
    None => "(unnamed)"
  }
}

///|
/// Events retain raw dates and source IDs. An event is a claim, not a verified fact.
pub fn events(doc : Document, record : Int32) -> Array[Event] {
  let tags = [
    "ADOP", "BAPM", "BARM", "BASM", "BIRT", "BLES", "BURI", "CENS", "CHR", "CHRA",
    "CONF", "CREM", "DEAT", "EMIG", "FCOM", "GRAD", "IMMI", "NATU", "ORDN", "RETI",
    "PROB", "WILL", "EVEN", "ANUL", "DIV", "DIVF", "ENGA", "MARB", "MARC", "MARL",
    "MARR", "MARS", "RESI",
  ]
  let out = []
  for index in doc.children(record) {
    let node = doc.nodes[index]
    if tags.contains(node.tag) {
      out.push({
        node: index,
        tag: node.tag,
        date: texts(doc, index, "DATE").get(0),
        place: texts(doc, index, "PLAC").get(0),
        sources: pointers(doc, index, "SOUR"),
        notes: texts(doc, index, "NOTE"),
      })
    }
  }
  out
}

///|
/// Case-insensitive ASCII search; Unicode names are preserved, without locale folding.
pub fn find_people(doc : Document, query : String) -> Array[Person] {
  let query = query.to_lower()
  people(doc).filter(fn(p) {
    p.names.any(fn(n) { n.to_lower().contains(query) })
  })
}