///|
fn relation_role(name : String) -> String {
  match name.to_upper() {
    "IFCRELAGGREGATES" => "aggregates"
    "IFCRELCONTAINEDINSPATIALSTRUCTURE" => "contains"
    "IFCRELDEFINESBYPROPERTIES" => "defines_properties"
    "IFCRELASSOCIATESMATERIAL" => "associates_material"
    "IFCRELASSOCIATESCLASSIFICATION" => "associates_classification"
    "IFCRELCONNECTSELEMENTS" => "connects"
    "IFCRELVOIDSELEMENT" => "voids"
    "IFCRELFILLSELEMENT" => "fills"
    _ => "relationship"
  }
}

///|
fn relation_link_for(entity : Entity) -> RelationLink? {
  let name = entity.name.to_upper()
  let relating_position = match name {
    "IFCRELAGGREGATES"
    | "IFCRELCONTAINEDINSPATIALSTRUCTURE"
    | "IFCRELDEFINESBYPROPERTIES"
    | "IFCRELASSOCIATESMATERIAL"
    | "IFCRELASSOCIATESCLASSIFICATION" => 5
    "IFCRELCONNECTSELEMENTS" => 5
    "IFCRELVOIDSELEMENT" => 5
    _ => -1
  }
  let related_position = match name {
    "IFCRELAGGREGATES"
    | "IFCRELCONTAINEDINSPATIALSTRUCTURE"
    | "IFCRELDEFINESBYPROPERTIES"
    | "IFCRELASSOCIATESMATERIAL"
    | "IFCRELASSOCIATESCLASSIFICATION" => 4
    "IFCRELCONNECTSELEMENTS" | "IFCRELVOIDSELEMENT" => 4
    _ => -1
  }
  if relating_position < 0 || related_position < 0 {
    None
  } else {
    Some({
      relation_id: entity.id,
      relation_type: entity.name,
      relating_id: optional_reference_argument(entity, relating_position),
      related_ids: relation_objects(entity, related_position),
      role: relation_role(entity.name),
    })
  }
}

///|
fn relation_add_map_value(
  map : Map[Int, Array[Int]],
  key : Int,
  value : Int,
) -> Unit {
  map.update_or_default(key, [value], values => {
    if !values.contains(value) {
      values.push(value)
    }
    values
  })
}

///|
/// Build normalized relation maps from raw IFC relationship entities.
pub fn RelationIndex::new(index : ModelIndex) -> RelationIndex {
  let links : Array[RelationLink] = []
  let parents : Map[Int, Array[Int]] = Map([])
  let children : Map[Int, Array[Int]] = Map([])
  let containers : Map[Int, Int] = Map([])
  let property_sets : Map[Int, Array[Int]] = Map([])
  let classifications : Map[Int, Array[Int]] = Map([])
  for entity in index.all() {
    match relation_link_for(entity) {
      Some(link) => {
        links.push(link)
        match link.role {
          "aggregates" =>
            match link.relating_id {
              Some(parent) =>
                for child in link.related_ids {
                  relation_add_map_value(children, parent, child)
                  relation_add_map_value(parents, child, parent)
                }
              None => ()
            }
          "contains" =>
            match link.relating_id {
              Some(container) =>
                for product in link.related_ids {
                  containers[product] = container
                }
              None => ()
            }
          "defines_properties" =>
            match link.relating_id {
              Some(property_set) =>
                for product in link.related_ids {
                  relation_add_map_value(property_sets, product, property_set)
                }
              None => ()
            }
          "associates_classification" =>
            match link.relating_id {
              Some(classification) =>
                for product in link.related_ids {
                  relation_add_map_value(
                    classifications, product, classification,
                  )
                }
              None => ()
            }
          _ => ()
        }
      }
      None => ()
    }
  }
  { links, parents, children, containers, property_sets, classifications }
}

///|
pub fn relation_index(model : Model) -> RelationIndex {
  RelationIndex::new(model.index())
}

///|
pub fn RelationIndex::link_count(self : RelationIndex) -> Int {
  self.links.length()
}

///|
pub fn RelationIndex::links(self : RelationIndex) -> Array[RelationLink] {
  self.links.copy()
}

///|
pub fn RelationIndex::links_for(
  self : RelationIndex,
  id : Int,
) -> Array[RelationLink] {
  self.links.filter(link => {
    link.relating_id == Some(id) || link.related_ids.contains(id)
  })
}

///|
pub fn RelationIndex::parent_ids(self : RelationIndex, id : Int) -> Array[Int] {
  self.parents.get(id).unwrap_or([])
}

///|
pub fn RelationIndex::child_ids(self : RelationIndex, id : Int) -> Array[Int] {
  self.children.get(id).unwrap_or([])
}

///|
pub fn RelationIndex::container_id(
  self : RelationIndex,
  product_id : Int,
) -> Int? {
  self.containers.get(product_id)
}

///|
pub fn RelationIndex::property_set_ids(
  self : RelationIndex,
  product_id : Int,
) -> Array[Int] {
  self.property_sets.get(product_id).unwrap_or([])
}

///|
pub fn RelationIndex::classification_ids(
  self : RelationIndex,
  product_id : Int,
) -> Array[Int] {
  self.classifications.get(product_id).unwrap_or([])
}

///|
fn relation_unique_ids(values : Array[Int]) -> Array[Int] {
  let result : Array[Int] = []
  for value in values {
    if !result.contains(value) {
      result.push(value)
    }
  }
  result
}

///|
pub fn RelationIndex::ancestors(self : RelationIndex, id : Int) -> Array[Int] {
  let result : Array[Int] = []
  let queue : Array[Int] = [id]
  while queue.length() > 0 {
    let current = queue.remove(0)
    for parent in self.parent_ids(current) {
      if !result.contains(parent) {
        result.push(parent)
        queue.push(parent)
      }
    }
  }
  result
}

///|
pub fn RelationIndex::descendants(self : RelationIndex, id : Int) -> Array[Int] {
  let result : Array[Int] = []
  let queue : Array[Int] = [id]
  while queue.length() > 0 {
    let current = queue.remove(0)
    for child in self.child_ids(current) {
      if !result.contains(child) {
        result.push(child)
        queue.push(child)
      }
    }
  }
  result
}

///|
pub fn RelationIndex::root_ids(self : RelationIndex) -> Array[Int] {
  let result : Array[Int] = []
  for parent in self.children {
    if !self.parents.contains(parent.0) {
      result.push(parent.0)
    }
  }
  relation_unique_ids(result)
}

///|
pub fn RelationIndex::relationship_histogram(
  self : RelationIndex,
) -> Map[String, Int] {
  self.links.fold(init=Map([]), (result, link) => {
    result.update_or_default(link.role, 1, count => count + 1)
    result
  })
}

///|

///|
pub fn RelationIndex::products_without_container(
  self : RelationIndex,
  products : Array[Product],
) -> Array[Int] {
  products
  .filter(product => !self.containers.contains(product.id))
  .map(product => product.id)
}

///|

///|
pub fn RelationIndex::products_without_properties(
  self : RelationIndex,
  products : Array[Product],
) -> Array[Int] {
  products
  .filter(product => !self.property_sets.contains(product.id))
  .map(product => product.id)
}

///|
pub fn RelationLink::is_many_to_one(self : RelationLink) -> Bool {
  self.related_ids.length() > 1 && self.relating_id is Some(_)
}

///|
pub fn RelationLink::headline(self : RelationLink) -> String {
  self.relation_type +
  " #" +
  self.relation_id.to_string() +
  " " +
  self.relating_id.map(id => "#" + id.to_string()).unwrap_or("-") +
  " -> " +
  self.related_ids.map(id => "#" + id.to_string()).join(",")
}

///|
pub fn RelationIndex::headline(self : RelationIndex) -> String {
  "links=" +
  self.links.length().to_string() +
  " aggregates=" +
  self.children.length().to_string() +
  " containers=" +
  self.containers.length().to_string() +
  " property_products=" +
  self.property_sets.length().to_string()
}

///|
pub fn RelationIndex::to_json(self : RelationIndex) -> String {
  let links = self.links
    .map(link => {
      "{\"id\":" +
      link.relation_id.to_string() +
      ",\"type\":\"" +
      link.relation_type +
      "\",\"role\":\"" +
      link.role +
      "\",\"relating_id\":" +
      link.relating_id.map(id => id.to_string()).unwrap_or("null") +
      ",\"related_ids\":[" +
      link.related_ids.map(id => id.to_string()).join(",") +
      "]}"
    })
    .join(",")
  "{\"link_count\":" +
  self.links.length().to_string() +
  ",\"links\":[" +
  links +
  "]}"
}

///|
pub fn relation_links_for_product(
  model : Model,
  product_id : Int,
) -> Array[RelationLink] {
  relation_index(model).links_for(product_id)
}

///|
pub fn relation_ancestors(model : Model, id : Int) -> Array[Int] {
  relation_index(model).ancestors(id)
}

///|
pub fn relation_descendants(model : Model, id : Int) -> Array[Int] {
  relation_index(model).descendants(id)
}

///|
pub fn relation_histogram(model : Model) -> Map[String, Int] {
  relation_index(model).relationship_histogram()
}