///|
pub fn Transform3::identity() -> Transform3 {
  {
    m00: 1.0,
    m01: 0.0,
    m02: 0.0,
    m10: 0.0,
    m11: 1.0,
    m12: 0.0,
    m20: 0.0,
    m21: 0.0,
    m22: 1.0,
    tx: 0.0,
    ty: 0.0,
    tz: 0.0,
  }
}

///|
pub fn Transform3::translation(
  x~ : Double,
  y~ : Double,
  z~ : Double,
) -> Transform3 {
  {
    m00: 1.0,
    m01: 0.0,
    m02: 0.0,
    m10: 0.0,
    m11: 1.0,
    m12: 0.0,
    m20: 0.0,
    m21: 0.0,
    m22: 1.0,
    tx: x,
    ty: y,
    tz: z,
  }
}

///|
pub fn Transform3::apply(self : Transform3, point : Point3) -> Point3 {
  {
    x: self.m00 * point.x + self.m01 * point.y + self.m02 * point.z + self.tx,
    y: self.m10 * point.x + self.m11 * point.y + self.m12 * point.z + self.ty,
    z: self.m20 * point.x + self.m21 * point.y + self.m22 * point.z + self.tz,
  }
}

///|
pub fn Transform3::multiply(
  self : Transform3,
  other : Transform3,
) -> Transform3 {
  {
    m00: self.m00 * other.m00 + self.m01 * other.m10 + self.m02 * other.m20,
    m01: self.m00 * other.m01 + self.m01 * other.m11 + self.m02 * other.m21,
    m02: self.m00 * other.m02 + self.m01 * other.m12 + self.m02 * other.m22,
    m10: self.m10 * other.m00 + self.m11 * other.m10 + self.m12 * other.m20,
    m11: self.m10 * other.m01 + self.m11 * other.m11 + self.m12 * other.m21,
    m12: self.m10 * other.m02 + self.m11 * other.m12 + self.m12 * other.m22,
    m20: self.m20 * other.m00 + self.m21 * other.m10 + self.m22 * other.m20,
    m21: self.m20 * other.m01 + self.m21 * other.m11 + self.m22 * other.m21,
    m22: self.m20 * other.m02 + self.m21 * other.m12 + self.m22 * other.m22,
    tx: self.m00 * other.tx +
    self.m01 * other.ty +
    self.m02 * other.tz +
    self.tx,
    ty: self.m10 * other.tx +
    self.m11 * other.ty +
    self.m12 * other.tz +
    self.ty,
    tz: self.m20 * other.tx +
    self.m21 * other.ty +
    self.m22 * other.tz +
    self.tz,
  }
}

///|
fn placement_point(index : ModelIndex, entity : Entity) -> Transform3 {
  match entity_value(entity, 0) {
    Some(value) =>
      match references(value).get(0) {
        Some(point_id) =>
          match point_from_entity(index, point_id) {
            Some(point) =>
              Transform3::translation(x=point.x, y=point.y, z=point.z)
            None => Transform3::identity()
          }
        None => Transform3::identity()
      }
    None => Transform3::identity()
  }
}

///|
fn placement_transform_inner(
  index : ModelIndex,
  id : Int,
  stack : Array[Int],
) -> Transform3? {
  if stack.contains(id) {
    return None
  }
  let next_stack = stack.copy()
  next_stack.push(id)
  match index.entity(id) {
    Some(entity) =>
      match entity.name.to_upper() {
        "IFCLOCALPLACEMENT" => {
          let relative = match optional_reference_argument(entity, 1) {
            Some(relative_id) =>
              placement_transform_inner(index, relative_id, next_stack).unwrap_or(
                Transform3::identity(),
              )
            None => Transform3::identity()
          }
          match optional_reference_argument(entity, 0) {
            Some(parent_id) =>
              match placement_transform_inner(index, parent_id, next_stack) {
                Some(parent) => Some(parent.multiply(relative))
                None => Some(relative)
              }
            None => Some(relative)
          }
        }
        "IFCAXIS2PLACEMENT3D" | "IFCAXIS2PLACEMENT2D" =>
          Some(placement_point(index, entity))
        _ => None
      }
    None => None
  }
}

///|
pub fn placement_transform(index : ModelIndex, id : Int) -> Transform3? {
  placement_transform_inner(index, id, [])
}

///|
pub fn product_transform(index : ModelIndex, product : Product) -> Transform3 {
  match product.placement {
    Some(id) => placement_transform(index, id).unwrap_or(Transform3::identity())
    None => Transform3::identity()
  }
}

///|
pub fn transform_point(
  index : ModelIndex,
  product : Product,
  point : Point3,
) -> Point3 {
  product_transform(index, product).apply(point)
}

///|
pub fn transform_points(
  index : ModelIndex,
  product : Product,
  points : Array[Point3],
) -> Array[Point3] {
  let transform = product_transform(index, product)
  points.map(point => transform.apply(point))
}