///|
fn number_from_value(value : Value) -> Double? {
  value.as_double()
}

///|
fn optional_double(value : Value?) -> Double? {
  match value {
    Some(item) => number_from_value(item)
    None => None
  }
}

///|
fn coordinates_from_value(value : Value) -> Array[Double] {
  match value {
    List(values) =>
      values.fold(init=Array::new(), (result, item) => {
        match number_from_value(item) {
          Some(number) => {
            result.push(number)
            result
          }
          None => result
        }
      })
    Typed(name~, value~) => {
      ignore(name)
      coordinates_from_value(value)
    }
    _ => []
  }
}

///|
fn point_from_coordinates(values : Array[Double]) -> Point3 {
  {
    x: values.get(0).unwrap_or(0.0),
    y: values.get(1).unwrap_or(0.0),
    z: values.get(2).unwrap_or(0.0),
  }
}

///|
fn point_from_entity(index : ModelIndex, id : Int) -> Point3? {
  match index.entity(id) {
    Some(entity) if entity.name.to_upper() == "IFCCARTESIANPOINT" =>
      match entity_value(entity, 0) {
        Some(value) =>
          Some(point_from_coordinates(coordinates_from_value(value)))
        None => None
      }
    _ => None
  }
}

///|
fn points_from_entity(index : ModelIndex, entity : Entity) -> Array[Point3] {
  let upper = entity.name.to_upper()
  if upper == "IFCCARTESIANPOINT" {
    match entity_value(entity, 0) {
      Some(value) => [point_from_coordinates(coordinates_from_value(value))]
      None => []
    }
  } else if upper == "IFCCARTESIANPOINTLIST3D" ||
    upper == "IFCCARTESIANPOINTLIST2D" {
    match entity_value(entity, 0) {
      Some(List(rows)) =>
        rows.fold(init=Array::new(), (result, row) => {
          result.push(point_from_coordinates(coordinates_from_value(row)))
          result
        })
      _ => []
    }
  } else if upper == "IFCPOLYLINE" {
    match entity_value(entity, 0) {
      Some(value) =>
        references(value).fold(init=Array::new(), (result, id) => {
          match point_from_entity(index, id) {
            Some(point) => {
              result.push(point)
              result
            }
            None => result
          }
        })
      None => []
    }
  } else {
    []
  }
}

///|
fn polygon_count(value : Value) -> Int {
  match value {
    List(values) =>
      values.fold(init=0, (count, item) => count + polygon_count(item))
    _ => 1
  }
}

///|
fn geometry_for_entity(index : ModelIndex, entity : Entity) -> Geometry {
  let upper = entity.name.to_upper()
  if upper == "IFCEXTRUDEDAREASOLID" {
    let profile_id = optional_reference_argument(entity, 0)
    let profile = match profile_id {
      Some(id) => index.entity(id)
      None => None
    }
    let profile_width = match profile {
      Some(profile_entity) if profile_entity.name.to_upper() ==
        "IFCRECTANGLEPROFILEDEF" =>
        optional_double(entity_value(profile_entity, 3))
      _ => None
    }
    let profile_height = match profile {
      Some(profile_entity) if profile_entity.name.to_upper() ==
        "IFCRECTANGLEPROFILEDEF" =>
        optional_double(entity_value(profile_entity, 4))
      _ => None
    }
    {
      source_id: entity.id,
      kind: Extrusion,
      points: match profile {
        Some(profile_entity) => points_from_entity(index, profile_entity)
        None => []
      },
      references: entity_references(entity),
      depth: optional_double(entity_value(entity, 3)),
      profile_width,
      profile_height,
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  } else if upper == "IFCFACETEDBREP" || upper == "IFCMANIFOLDSOLIDBREP" {
    let outer = optional_reference_argument(entity, 0)
    let face_count = match outer {
      Some(outer_id) =>
        match index.entity(outer_id) {
          Some(shell) =>
            match entity_value(shell, 0) {
              Some(value) => references(value).length()
              None => 0
            }
          None => 0
        }
      None => 0
    }
    {
      source_id: entity.id,
      kind: FacetedBrep,
      points: [],
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count,
      triangle_count: 0,
    }
  } else if upper == "IFCTRIANGULATEDFACESET" || upper == "IFCPOLYGONALFACESET" {
    let coordinate_ref = optional_reference_argument(entity, 0)
    let points = match coordinate_ref {
      Some(id) =>
        match index.entity(id) {
          Some(coordinates) => points_from_entity(index, coordinates)
          None => []
        }
      None => []
    }
    let triangle_count = match entity_value(entity, 2) {
      Some(value) => polygon_count(value) / 3
      None => 0
    }
    {
      source_id: entity.id,
      kind: TriangulatedMesh,
      points,
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count: 0,
      triangle_count,
    }
  } else if upper == "IFCCSGSOLID" || upper == "IFCBOOLEANCLIPPINGRESULT" {
    {
      source_id: entity.id,
      kind: CsgSolid,
      points: [],
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  } else if upper == "IFCRECTANGLEPROFILEDEF" {
    {
      source_id: entity.id,
      kind: Profile,
      points: [],
      references: entity_references(entity),
      depth: None,
      profile_width: optional_double(entity_value(entity, 3)),
      profile_height: optional_double(entity_value(entity, 4)),
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  } else if upper == "IFCCIRCLEPROFILEDEF" {
    {
      source_id: entity.id,
      kind: Profile,
      points: [],
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: optional_double(entity_value(entity, 3)),
      face_count: 0,
      triangle_count: 0,
    }
  } else if upper == "IFCPOLYLINE" {
    {
      source_id: entity.id,
      kind: Polyline,
      points: points_from_entity(index, entity),
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  } else if upper == "IFCCARTESIANPOINT" || upper == "IFCCARTESIANPOINTLIST3D" {
    {
      source_id: entity.id,
      kind: PointCloud,
      points: points_from_entity(index, entity),
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  } else {
    {
      source_id: entity.id,
      kind: Unknown,
      points: [],
      references: entity_references(entity),
      depth: None,
      profile_width: None,
      profile_height: None,
      profile_radius: None,
      face_count: 0,
      triangle_count: 0,
    }
  }
}

///|
pub fn geometry(index : ModelIndex, id : Int) -> Geometry raise ParseError {
  let entity = index.require(id)
  geometry_for_entity(index, entity)
}

///|
fn representation_items(
  index : ModelIndex,
  representation_id : Int,
) -> Array[Int] {
  match index.entity(representation_id) {
    Some(shape) if shape.name.to_upper() == "IFCPRODUCTDEFINITIONSHAPE" =>
      match entity_value(shape, 2) {
        Some(value) =>
          references(value).fold(init=Array::new(), (result, shape_id) => {
            for item_id in representation_items(index, shape_id) {
              result.push(item_id)
            }
            result
          })
        None => []
      }
    Some(shape) if shape.name.to_upper() == "IFCSHAPEREPRESENTATION" =>
      reference_list(entity_value(shape, 3))
    _ => [representation_id]
  }
}

///|
pub fn geometries_for_product(
  index : ModelIndex,
  product : Product,
) -> Array[Geometry] {
  match product.representation {
    Some(representation_id) =>
      representation_items(index, representation_id).fold(init=Array::new(), (
        result,
        id,
      ) => {
        match index.entity(id) {
          Some(entity) => result.push(geometry_for_entity(index, entity))
          None => ()
        }
        result
      })
    None => []
  }
}

///|
pub fn geometry_for_product(index : ModelIndex, product : Product) -> Geometry? {
  geometries_for_product(index, product).get(0)
}

///|
pub fn geometry_entities(index : ModelIndex) -> Array[Entity] {
  index
  .all()
  .filter(entity => {
    let upper = entity.name.to_upper()
    upper == "IFCEXTRUDEDAREASOLID" ||
    upper == "IFCFACETEDBREP" ||
    upper == "IFCTRIANGULATEDFACESET" ||
    upper == "IFCPOLYGONALFACESET" ||
    upper == "IFCCSGSOLID" ||
    upper == "IFCBOOLEANCLIPPINGRESULT"
  })
}