///|
fn geometry_inventory_product(
  index : ModelIndex,
  product : Product,
) -> ProductGeometryReport raise ParseError {
  let geometries = geometries_for_product(index, product)
  let mesh = mesh_for_product_advanced(index, product)
  let bounds = if mesh.points.length() > 0 {
    bounds_of_mesh(mesh)
  } else {
    geometries.fold(init=BoundingBox::empty(), (result, geometry) => {
      merge_bounds(
        result,
        BoundingBox::from_points(
          transform_points(index, product, geometry.points),
        ),
      )
    })
  }
  let stats = mesh_stats(mesh)
  let estimated = geometries.fold(init=0.0, (result, geometry) => {
    result + estimated_volume(geometry).unwrap_or(0.0)
  })
  let volume = if mesh.triangles.length() > 0 {
    stats.volume().abs()
  } else {
    estimated
  }
  let valid_mesh_count = if mesh.triangles.length() > 0 &&
    mesh_is_renderable(mesh) {
    1
  } else {
    0
  }
  {
    product_id: product.id,
    geometry_count: geometries.length(),
    mesh_count: if mesh.triangles.length() > 0 {
      1
    } else {
      0
    },
    point_count: mesh.points.length(),
    triangle_count: mesh.triangles.length(),
    bounds,
    surface_area: stats.surface_area,
    volume,
    estimated_volume: mesh.triangles.length() == 0 && estimated > 0.0,
    valid_mesh_count,
  }
}

///|
/// Build a geometry inventory with one row per product.
pub fn geometry_inventory(model : Model) -> GeometryInventory raise ParseError {
  let index = model.index()
  let rows : Array[ProductGeometryReport] = []
  let mut represented = 0
  let mut empty = 0
  let mut invalid = 0
  let mut area = 0.0
  let mut volume = 0.0
  for product in model.products() {
    let row = geometry_inventory_product(index, product)
    rows.push(row)
    if row.geometry_count > 0 {
      represented += 1
    } else {
      empty += 1
    }
    if row.mesh_count > 0 && row.valid_mesh_count == 0 {
      invalid += 1
    }
    area += row.surface_area
    volume += row.volume
  }
  {
    products: rows,
    represented_product_count: represented,
    empty_representation_count: empty,
    invalid_mesh_count: invalid,
    total_surface_area: area,
    total_volume: volume,
  }
}

///|
pub fn geometry_report_for_product(
  model : Model,
  product_id : Int,
) -> ProductGeometryReport? raise ParseError {
  let index = model.index()
  match product_by_id(model.products(), product_id) {
    Some(product) => Some(geometry_inventory_product(index, product))
    None => None
  }
}

///|
pub fn ProductGeometryReport::has_mesh(self : ProductGeometryReport) -> Bool {
  self.mesh_count > 0 && self.triangle_count > 0
}

///|
pub fn ProductGeometryReport::is_valid(self : ProductGeometryReport) -> Bool {
  self.valid_mesh_count == self.mesh_count
}

///|
pub fn ProductGeometryReport::center(self : ProductGeometryReport) -> Point3 {
  self.bounds.center()
}

///|
pub fn ProductGeometryReport::headline(self : ProductGeometryReport) -> String {
  "#" +
  self.product_id.to_string() +
  " geometries=" +
  self.geometry_count.to_string() +
  " triangles=" +
  self.triangle_count.to_string() +
  " area=" +
  self.surface_area.to_string() +
  " volume=" +
  self.volume.to_string()
}

///|
pub fn GeometryInventory::product_count(self : GeometryInventory) -> Int {
  self.products.length()
}

///|
pub fn GeometryInventory::represented_ratio(self : GeometryInventory) -> Double {
  if self.products.length() == 0 {
    0.0
  } else {
    self.represented_product_count.to_double() /
    self.products.length().to_double()
  }
}

///|
pub fn GeometryInventory::valid_ratio(self : GeometryInventory) -> Double {
  let represented = self.products.filter(row => row.mesh_count > 0).length()
  if represented == 0 {
    1.0
  } else {
    (represented - self.invalid_mesh_count).to_double() /
    represented.to_double()
  }
}

///|
pub fn GeometryInventory::headline(self : GeometryInventory) -> String {
  "products=" +
  self.products.length().to_string() +
  " represented=" +
  self.represented_product_count.to_string() +
  " empty=" +
  self.empty_representation_count.to_string() +
  " invalid_mesh=" +
  self.invalid_mesh_count.to_string() +
  " area=" +
  self.total_surface_area.to_string() +
  " volume=" +
  self.total_volume.to_string()
}

///|
pub fn GeometryInventory::by_product(
  self : GeometryInventory,
  id : Int,
) -> ProductGeometryReport? {
  for row in self.products {
    if row.product_id == id {
      return Some(row)
    }
  }
  None
}

///|
pub fn GeometryInventory::largest_by_volume(
  self : GeometryInventory,
) -> ProductGeometryReport? {
  let mut result : ProductGeometryReport? = None
  for row in self.products {
    match result {
      Some(current) if current.volume >= row.volume => ()
      _ => result = Some(row)
    }
  }
  result
}

///|
pub fn GeometryInventory::largest_by_area(
  self : GeometryInventory,
) -> ProductGeometryReport? {
  let mut result : ProductGeometryReport? = None
  for row in self.products {
    match result {
      Some(current) if current.surface_area >= row.surface_area => ()
      _ => result = Some(row)
    }
  }
  result
}

///|
fn geometry_centroid(points : Array[Point3]) -> Point3 {
  if points.length() == 0 {
    return { x: 0.0, y: 0.0, z: 0.0 }
  }
  let (x, y, z) = points.fold(init=(0.0, 0.0, 0.0), (result, point) => {
    let (x, y, z) = result
    (x + point.x, y + point.y, z + point.z)
  })
  let count = points.length().to_double()
  { x: x / count, y: y / count, z: z / count }
}

///|
pub fn mesh_centroid(mesh : Mesh) -> Point3 {
  geometry_centroid(mesh.points)
}

///|
pub fn mesh_world_centroid(
  index : ModelIndex,
  product : Product,
) -> Point3 raise ParseError {
  mesh_centroid(mesh_for_product_advanced(index, product))
}

///|
pub fn mesh_diagonal(mesh : Mesh) -> Double {
  let bounds = bounds_of_mesh(mesh)
  Vector3::from_points(bounds.min, bounds.max).length()
}

///|
pub fn mesh_aspect_ratio(mesh : Mesh) -> Double {
  let bounds = bounds_of_mesh(mesh)
  let values = [bounds.width(), bounds.height(), bounds.depth()]
  let mut minimum = 1.0e300
  let mut maximum = 0.0
  for value in values {
    if value > 0.0 && value < minimum {
      minimum = value
    }
    if value > maximum {
      maximum = value
    }
  }
  if minimum == 1.0e300 || minimum == 0.0 {
    0.0
  } else {
    maximum / minimum
  }
}

///|
pub fn mesh_area_by_normal(
  mesh : Mesh,
  axis : Vector3,
  tolerance? : Double = 0.000001,
) -> Double {
  let direction = axis.normalized()
  mesh.triangles.fold(init=0.0, (result, triangle) => {
    let normal = triangle_normal(mesh, triangle)
    if normal.length() == 0.0 ||
      normal.normalized().dot(direction).abs() < tolerance {
      result
    } else {
      result + triangle_area(mesh, triangle)
    }
  })
}

///|
pub fn mesh_horizontal_area(mesh : Mesh) -> Double {
  mesh_area_by_normal(mesh, { x: 0.0, y: 0.0, z: 1.0 })
}

///|
pub fn mesh_vertical_area(mesh : Mesh) -> Double {
  mesh_area_by_normal(mesh, { x: 1.0, y: 0.0, z: 0.0 }) +
  mesh_area_by_normal(mesh, { x: 0.0, y: 1.0, z: 0.0 })
}

///|
pub fn mesh_triangle_quality(mesh : Mesh, triangle_index : Int) -> Double? {
  match mesh.triangles.get(triangle_index) {
    Some(triangle) => {
      let (a, b, c) = triangle
      match (mesh.points.get(a), mesh.points.get(b), mesh.points.get(c)) {
        (Some(left), Some(middle), Some(right)) => {
          let ab = Vector3::from_points(left, middle).length()
          let bc = Vector3::from_points(middle, right).length()
          let ca = Vector3::from_points(right, left).length()
          let longest = if ab > bc {
            if ab > ca {
              ab
            } else {
              ca
            }
          } else if bc > ca {
            bc
          } else {
            ca
          }
          let shortest = if ab < bc {
            if ab < ca {
              ab
            } else {
              ca
            }
          } else if bc < ca {
            bc
          } else {
            ca
          }
          if longest == 0.0 {
            Some(0.0)
          } else {
            Some(shortest / longest)
          }
        }
        _ => None
      }
    }
    None => None
  }
}

///|
pub fn mesh_quality_histogram(mesh : Mesh, bins : Int) -> Map[Int, Int] {
  let count = if bins <= 0 { 10 } else { bins }
  let result : Map[Int, Int] = Map([])
  for index in 0.. {
        let bucket = @math.floor(value * count.to_double()).to_int()
        let key = if bucket >= count { count - 1 } else { bucket }
        result.update_or_default(key, 1, old => old + 1)
      }
      None => ()
    }
  }
  result
}

///|
pub fn mesh_normal_histogram(mesh : Mesh, bins : Int) -> Map[String, Int] {
  let count = if bins <= 0 { 8 } else { bins }
  mesh.triangles.fold(init=Map([]), (result, triangle) => {
    let normal = triangle_normal(mesh, triangle).normalized()
    let key = @math.floor((normal.z + 1.0) * 0.5 * count.to_double())
      .to_int()
      .to_string()
    result.update_or_default(key, 1, old => old + 1)
    result
  })
}

///|
pub fn mesh_edge_length_statistics(mesh : Mesh) -> (Double, Double, Double) {
  let mut minimum = 1.0e300
  let mut maximum = 0.0
  let mut total = 0.0
  let mut count = 0
  for triangle in mesh.triangles {
    let (a, b, c) = triangle
    for pair in [(a, b), (b, c), (c, a)] {
      let (left, right) = pair
      match (mesh.points.get(left), mesh.points.get(right)) {
        (Some(start), Some(end)) => {
          let length = Vector3::from_points(start, end).length()
          if length < minimum {
            minimum = length
          }
          if length > maximum {
            maximum = length
          }
          total += length
          count += 1
        }
        _ => ()
      }
    }
  }
  (
    if count == 0 {
      0.0
    } else {
      minimum
    },
    maximum,
    if count == 0 {
      0.0
    } else {
      total / count.to_double()
    },
  )
}

///|
pub fn ProductGeometryReport::to_json(self : ProductGeometryReport) -> String {
  "{\"product_id\":" +
  self.product_id.to_string() +
  ",\"geometry_count\":" +
  self.geometry_count.to_string() +
  ",\"mesh_count\":" +
  self.mesh_count.to_string() +
  ",\"point_count\":" +
  self.point_count.to_string() +
  ",\"triangle_count\":" +
  self.triangle_count.to_string() +
  ",\"surface_area\":" +
  self.surface_area.to_string() +
  ",\"volume\":" +
  self.volume.to_string() +
  ",\"estimated_volume\":" +
  self.estimated_volume.to_string() +
  "}"
}

///|
pub fn GeometryInventory::to_json(self : GeometryInventory) -> String {
  "{\"represented_product_count\":" +
  self.represented_product_count.to_string() +
  ",\"empty_representation_count\":" +
  self.empty_representation_count.to_string() +
  ",\"invalid_mesh_count\":" +
  self.invalid_mesh_count.to_string() +
  ",\"total_surface_area\":" +
  self.total_surface_area.to_string() +
  ",\"total_volume\":" +
  self.total_volume.to_string() +
  ",\"products\":[" +
  self.products.map(row => row.to_json()).join(",") +
  "]}"
}

///|
pub fn GeometryInventory::to_csv(self : GeometryInventory) -> String {
  let builder = StringBuilder()
  builder <+
    "product_id,geometry_count,points,triangles,area,volume,estimated,valid\n"
  for row in self.products {
    builder <+
      "\{row.product_id},\{row.geometry_count},\{row.point_count},\{row.triangle_count},\{row.surface_area},\{row.volume},\{row.estimated_volume},\{row.is_valid()}\n"
  }
  builder.to_string()
}

///|
pub fn geometry_inventory_summary(
  inventory : GeometryInventory,
) -> Array[String] {
  [
    inventory.headline(),
    "represented_ratio=" + inventory.represented_ratio().to_string(),
    "valid_ratio=" + inventory.valid_ratio().to_string(),
  ]
}