///|
fn product_kind(name : String) -> ProductKind {
  match name.to_upper() {
    "IFCPROJECT" => Project
    "IFCSITE" => Site
    "IFCBUILDING" => Building
    "IFCBUILDINGSTOREY" => Storey
    "IFCSPACE" => Space
    "IFCWALL" | "IFCWALLELEMENTEDCASE" | "IFCWALLSTANDARDCASE" => Wall
    "IFCDOOR" => Door
    "IFCWINDOW" => Window
    "IFCBEAM" => Beam
    "IFCCOLUMN" => Column
    "IFCSLAB" => Slab
    "IFCROOF" => Roof
    "IFCSTAIR" | "IFCSTAIRFLIGHT" => Stair
    "IFCRAMP" | "IFCRAMPFLIGHT" => Ramp
    "IFCOPENINGELEMENT" | "IFCVOIDINGFEATURE" => Opening
    _ => Generic
  }
}

///|
fn is_product_entity(name : String) -> Bool {
  let upper = name.to_upper()
  match product_kind(upper) {
    Generic =>
      upper == "IFCPRODUCT" ||
      upper == "IFCFURNISHINGELEMENT" ||
      upper == "IFCDISTRIBUTIONELEMENT"
    _ => true
  }
}

///|
fn optional_string_argument(entity : Entity, position : Int) -> String? {
  match entity_value(entity, position) {
    Some(value) => string_value(value)
    None => None
  }
}

///|
fn string_value(value : Value) -> String? {
  value.as_string()
}

///|
fn optional_reference_argument(entity : Entity, position : Int) -> Int? {
  value_reference(entity_value(entity, position))
}

///|
fn reference_list(value : Value?) -> Array[Int] {
  match value {
    Some(item) => references(item)
    None => []
  }
}

///|
fn relation_objects(entity : Entity, position : Int) -> Array[Int] {
  reference_list(entity_value(entity, position))
}

///|
fn property_relation_maps(
  index : ModelIndex,
) -> (Map[Int, Array[Int]], Map[Int, Int]) {
  let property_sets : Map[Int, Array[Int]] = Map([])
  let containers : Map[Int, Int] = Map([])
  for relation in index.all() {
    let name = relation.name.to_upper()
    if name == "IFCRELDEFINESBYPROPERTIES" {
      match optional_reference_argument(relation, 5) {
        Some(property_set_id) =>
          for object_id in relation_objects(relation, 4) {
            property_sets.update_or_default(object_id, [property_set_id], values => {
              values.push(property_set_id)
              values
            })
          }
        None => ()
      }
    } else if name == "IFCRELCONTAINEDINSPATIALSTRUCTURE" {
      match optional_reference_argument(relation, 5) {
        Some(container_id) =>
          for object_id in relation_objects(relation, 4) {
            containers[object_id] = container_id
          }
        None => ()
      }
    }
  }
  (property_sets, containers)
}

///|
fn make_product(
  entity : Entity,
  property_sets : Map[Int, Array[Int]],
  containers : Map[Int, Int],
) -> Product {
  let kind = product_kind(entity.name)
  {
    id: entity.id,
    ifc_type: entity.name,
    kind,
    global_id: optional_string_argument(entity, 0),
    name: optional_string_argument(entity, 2),
    description: optional_string_argument(entity, 3),
    object_type: optional_string_argument(entity, 4),
    placement: optional_reference_argument(entity, 5),
    representation: optional_reference_argument(entity, 6),
    tag: optional_string_argument(entity, 7),
    property_sets: property_sets.get(entity.id).unwrap_or([]),
    container: containers.get(entity.id),
  }
}

///|
/// Extract common IFC products while preserving the original generic entities.
pub fn extract_products(index : ModelIndex) -> Array[Product] {
  let (property_sets, containers) = property_relation_maps(index)
  index
  .all()
  .fold(init=Array::new(), (result, entity) => {
    if is_product_entity(entity.name) {
      result.push(make_product(entity, property_sets, containers))
    }
    result
  })
}

///|
pub fn product_by_id(products : Array[Product], id : Int) -> Product? {
  for product in products {
    if product.id == id {
      return Some(product)
    }
  }
  None
}

///|
pub fn products_of_kind(
  products : Array[Product],
  kind : ProductKind,
) -> Array[Product] {
  products.filter(product => product.kind == kind)
}