///|
pub fn BoundingBox::empty() -> BoundingBox {
  {
    min: { x: 0.0, y: 0.0, z: 0.0 },
    max: { x: 0.0, y: 0.0, z: 0.0 },
    empty: true,
  }
}

///|
pub fn BoundingBox::from_points(points : Array[Point3]) -> BoundingBox {
  if points.length() == 0 {
    return BoundingBox::empty()
  }
  let first = points[0]
  let mut min_x = first.x
  let mut min_y = first.y
  let mut min_z = first.z
  let mut max_x = first.x
  let mut max_y = first.y
  let mut max_z = first.z
  for point in points[1:] {
    if point.x < min_x {
      min_x = point.x
    }
    if point.y < min_y {
      min_y = point.y
    }
    if point.z < min_z {
      min_z = point.z
    }
    if point.x > max_x {
      max_x = point.x
    }
    if point.y > max_y {
      max_y = point.y
    }
    if point.z > max_z {
      max_z = point.z
    }
  }
  {
    min: { x: min_x, y: min_y, z: min_z },
    max: { x: max_x, y: max_y, z: max_z },
    empty: false,
  }
}

///|
pub fn BoundingBox::width(self : BoundingBox) -> Double {
  if self.empty {
    0.0
  } else {
    self.max.x - self.min.x
  }
}

///|
pub fn BoundingBox::height(self : BoundingBox) -> Double {
  if self.empty {
    0.0
  } else {
    self.max.y - self.min.y
  }
}

///|
pub fn BoundingBox::depth(self : BoundingBox) -> Double {
  if self.empty {
    0.0
  } else {
    self.max.z - self.min.z
  }
}

///|
pub fn BoundingBox::volume(self : BoundingBox) -> Double {
  self.width() * self.height() * self.depth()
}

///|
pub fn BoundingBox::center(self : BoundingBox) -> Point3 {
  {
    x: (self.min.x + self.max.x) / 2.0,
    y: (self.min.y + self.max.y) / 2.0,
    z: (self.min.z + self.max.z) / 2.0,
  }
}

///|
pub fn bounds_of_mesh(mesh : Mesh) -> BoundingBox {
  BoundingBox::from_points(mesh.points)
}

///|
pub fn bounds_of_geometry(geometry : Geometry) -> BoundingBox {
  BoundingBox::from_points(geometry.points)
}

///|
pub fn merge_bounds(left : BoundingBox, right : BoundingBox) -> BoundingBox {
  if left.empty {
    return right
  }
  if right.empty {
    return left
  }
  BoundingBox::{
    min: {
      x: if left.min.x < right.min.x {
        left.min.x
      } else {
        right.min.x
      },
      y: if left.min.y < right.min.y {
        left.min.y
      } else {
        right.min.y
      },
      z: if left.min.z < right.min.z {
        left.min.z
      } else {
        right.min.z
      },
    },
    max: {
      x: if left.max.x > right.max.x {
        left.max.x
      } else {
        right.max.x
      },
      y: if left.max.y > right.max.y {
        left.max.y
      } else {
        right.max.y
      },
      z: if left.max.z > right.max.z {
        left.max.z
      } else {
        right.max.z
      },
    },
    empty: false,
  }
}

///|
pub fn transform_mesh(transform : Transform3, mesh : Mesh) -> Mesh {
  {
    points: mesh.points.map(point => transform.apply(point)),
    triangles: mesh.triangles,
  }
}

///|
pub fn merge_meshes(meshes : Array[Mesh]) -> Mesh {
  let points : Array[Point3] = []
  let triangles : Array[(Int, Int, Int)] = []
  for mesh in meshes {
    let offset = points.length()
    points.append(mesh.points)
    for triangle in mesh.triangles {
      let (a, b, c) = triangle
      triangles.push((a + offset, b + offset, c + offset))
    }
  }
  { points, triangles }
}

///|
pub fn estimated_volume(geometry : Geometry) -> Double? {
  match (geometry.profile_width, geometry.profile_height, geometry.depth) {
    (Some(width), Some(height), Some(depth)) => Some(width * height * depth)
    (Some(radius), _, Some(depth)) if geometry.profile_radius is Some(_) =>
      Some(3.141592653589793 * radius * radius * depth)
    _ => None
  }
}

///|
pub fn model_bounds(
  index : ModelIndex,
  products : Array[Product],
) -> BoundingBox {
  products.fold(init=BoundingBox::empty(), (result, product) => {
    let product_bounds = geometries_for_product(index, product).fold(
      init=BoundingBox::empty(),
      (geometry_result, geometry) => {
        merge_bounds(geometry_result, bounds_of_geometry(geometry))
      },
    )
    merge_bounds(result, product_bounds)
  })
}