///|
fn distance_between(left : Point3, right : Point3) -> Double {
  let dx = right.x - left.x
  let dy = right.y - left.y
  let dz = right.z - left.z
  (dx * dx + dy * dy + dz * dz).sqrt()
}

///|
pub fn polyline_length(
  points : Array[Point3],
  closed? : Bool = false,
) -> Double {
  if points.length() < 2 {
    return 0.0
  }
  let mut length = 0.0
  for index in 0..<(points.length() - 1) {
    length += distance_between(points[index], points[index + 1])
  }
  if closed && points.length() > 2 {
    length += distance_between(points[points.length() - 1], points[0])
  }
  length
}

///|
fn curve_points_inner(
  index : ModelIndex,
  id : Int,
  stack : Array[Int],
) -> Array[Point3] {
  if stack.contains(id) {
    return []
  }
  let next_stack = stack.copy()
  next_stack.push(id)
  match index.entity(id) {
    Some(entity) =>
      match entity.name.to_upper() {
        "IFCPOLYLINE" | "IFCPOLYLOOP" => points_from_entity(index, entity)
        "IFCINDEXEDPOLYCURVE" =>
          match optional_reference_argument(entity, 0) {
            Some(points_id) =>
              match index.entity(points_id) {
                Some(point_list) => points_from_entity(index, point_list)
                None => []
              }
            None => []
          }
        "IFCTRIMMEDCURVE" =>
          match optional_reference_argument(entity, 0) {
            Some(base_id) => curve_points_inner(index, base_id, next_stack)
            None => []
          }
        "IFCCOMPOSITECURVE" => {
          let result : Array[Point3] = []
          for segment_id in reference_list(entity_value(entity, 0)) {
            match index.entity(segment_id) {
              Some(segment) => {
                let candidates = entity_references(segment)
                for candidate in candidates {
                  let points = curve_points_inner(index, candidate, next_stack)
                  if points.length() > 0 {
                    for point in points {
                      result.push(point)
                    }
                    break
                  }
                }
              }
              None => ()
            }
          }
          result
        }
        "IFCCIRCLE" | "IFCELLIPSE" => []
        _ => []
      }
    None => []
  }
}

///|
pub fn curve_points(index : ModelIndex, id : Int) -> Array[Point3] {
  curve_points_inner(index, id, [])
}

///|
pub fn curve_length(
  index : ModelIndex,
  id : Int,
  closed? : Bool = false,
) -> Double {
  polyline_length(curve_points(index, id), closed~)
}

///|
pub fn resample_polyline(
  points : Array[Point3],
  maximum_segment : Double,
) -> Array[Point3] {
  if points.length() < 2 || maximum_segment <= 0.0 {
    return points
  }
  let result : Array[Point3] = [points[0]]
  for index in 0..<(points.length() - 1) {
    let left = points[index]
    let right = points[index + 1]
    let length = distance_between(left, right)
    let pieces = if length <= maximum_segment {
      1
    } else {
      (length / maximum_segment).ceil().to_int()
    }
    for piece in 1..<=pieces {
      let ratio = piece.to_double() / pieces.to_double()
      result.push({
        x: left.x + (right.x - left.x) * ratio,
        y: left.y + (right.y - left.y) * ratio,
        z: left.z + (right.z - left.z) * ratio,
      })
    }
  }
  result
}

///|
pub fn curve_bounds(index : ModelIndex, id : Int) -> BoundingBox {
  BoundingBox::from_points(curve_points(index, id))
}

///|
pub fn curves_in_entity(index : ModelIndex, entity : Entity) -> Array[Int] {
  entity_references(entity).filter(id => {
    match index.entity(id) {
      Some(candidate) => {
        let name = candidate.name.to_upper()
        name == "IFCPOLYLINE" ||
        name == "IFCINDEXEDPOLYCURVE" ||
        name == "IFCTRIMMEDCURVE" ||
        name == "IFCCOMPOSITECURVE"
      }
      None => false
    }
  })
}