///|
/// A plane used to extract a coordination or quantity-takeoff section.
pub(all) struct SectionPlane {
  origin : Point3
  normal : Vector3
} derive(Debug)

///|
/// Controls numerical tolerance and segment limits for a section cut.
pub(all) struct SectionOptions {
  tolerance : Double
  include_coplanar_edges : Bool
  maximum_segments : Int
  join_tolerance : Double
} derive(Debug)

///|
/// One line segment produced by intersecting a triangle with a plane.
pub(all) struct SectionSegment {
  product_id : Int
  source_triangle : Int
  start : Point3
  end : Point3
  length : Double
  coplanar : Bool
} derive(Debug)

///|
/// A connected sequence of section points assembled from raw segments.
pub(all) struct SectionPolyline {
  product_id : Int
  points : Array[Point3]
  closed : Bool
  length : Double
  area : Double
} derive(Debug)

///|
/// Aggregate output of a section extraction.
pub(all) struct SectionResult {
  plane : SectionPlane
  segments : Array[SectionSegment]
  polylines : Array[SectionPolyline]
  product_ids : Array[Int]
  intersected_triangle_count : Int
  total_length : Double
  bounds : BoundingBox
} derive(Debug)

///|
pub fn SectionPlane::new(origin~ : Point3, normal~ : Vector3) -> SectionPlane {
  { origin, normal: normal.normalized() }
}

///|
pub fn SectionPlane::horizontal(height : Double) -> SectionPlane {
  SectionPlane::new(origin={ x: 0.0, y: 0.0, z: height }, normal=Vector3::{
    x: 0.0,
    y: 0.0,
    z: 1.0,
  })
}

///|
pub fn SectionPlane::vertical_x(offset : Double) -> SectionPlane {
  SectionPlane::new(origin={ x: offset, y: 0.0, z: 0.0 }, normal=Vector3::{
    x: 1.0,
    y: 0.0,
    z: 0.0,
  })
}

///|
pub fn SectionPlane::vertical_y(offset : Double) -> SectionPlane {
  SectionPlane::new(origin={ x: 0.0, y: offset, z: 0.0 }, normal=Vector3::{
    x: 0.0,
    y: 1.0,
    z: 0.0,
  })
}

///|
pub fn SectionPlane::is_valid(self : SectionPlane) -> Bool {
  self.normal.length() > 0.0
}

///|
pub fn SectionPlane::signed_distance(
  self : SectionPlane,
  point : Point3,
) -> Double {
  Vector3::from_points(self.origin, point).dot(self.normal)
}

///|
pub fn SectionPlane::project(self : SectionPlane, point : Point3) -> Point3 {
  let distance = self.signed_distance(point)
  {
    x: point.x - self.normal.x * distance,
    y: point.y - self.normal.y * distance,
    z: point.z - self.normal.z * distance,
  }
}

///|
pub fn SectionPlane::headline(self : SectionPlane) -> String {
  "origin=(" +
  self.origin.x.to_string() +
  "," +
  self.origin.y.to_string() +
  "," +
  self.origin.z.to_string() +
  ") normal=(" +
  self.normal.x.to_string() +
  "," +
  self.normal.y.to_string() +
  "," +
  self.normal.z.to_string() +
  ")"
}

///|
pub fn default_section_options() -> SectionOptions {
  {
    tolerance: 0.000001,
    include_coplanar_edges: false,
    maximum_segments: 0,
    join_tolerance: 0.0001,
  }
}

///|
pub fn SectionOptions::with_tolerance(
  self : SectionOptions,
  tolerance : Double,
) -> SectionOptions {
  let value = if tolerance < 0.0 { 0.0 } else { tolerance }
  {
    ..self,
    tolerance: value,
    join_tolerance: if self.join_tolerance < value {
      value
    } else {
      self.join_tolerance
    },
  }
}

///|
pub fn SectionOptions::including_coplanar_edges(
  self : SectionOptions,
  enabled : Bool,
) -> SectionOptions {
  { ..self, include_coplanar_edges: enabled }
}

///|
pub fn SectionOptions::with_maximum_segments(
  self : SectionOptions,
  maximum : Int,
) -> SectionOptions {
  { ..self, maximum_segments: if maximum < 0 { 0 } else { maximum } }
}

///|
pub fn SectionOptions::with_join_tolerance(
  self : SectionOptions,
  tolerance : Double,
) -> SectionOptions {
  { ..self, join_tolerance: if tolerance < 0.0 { 0.0 } else { tolerance } }
}

///|
fn section_point_distance(left : Point3, right : Point3) -> Double {
  Vector3::from_points(left, right).length()
}

///|
fn section_interpolate(
  left : Point3,
  right : Point3,
  amount : Double,
) -> Point3 {
  {
    x: left.x + (right.x - left.x) * amount,
    y: left.y + (right.y - left.y) * amount,
    z: left.z + (right.z - left.z) * amount,
  }
}

///|
fn section_add_unique(
  points : Array[Point3],
  point : Point3,
  tolerance : Double,
) -> Unit {
  if !points.any(item => section_point_distance(item, point) <= tolerance) {
    points.push(point)
  }
}

///|
fn section_edge_point(
  left : Point3,
  right : Point3,
  left_distance : Double,
  right_distance : Double,
  tolerance : Double,
) -> Point3? {
  let left_on = left_distance.abs() <= tolerance
  let right_on = right_distance.abs() <= tolerance
  if left_on && right_on {
    Some(left)
  } else if left_on {
    Some(left)
  } else if right_on {
    Some(right)
  } else if (left_distance < 0.0 && right_distance > 0.0) ||
    (left_distance > 0.0 && right_distance < 0.0) {
    let amount = left_distance.abs() /
      (left_distance.abs() + right_distance.abs())
    Some(section_interpolate(left, right, amount))
  } else {
    None
  }
}

///|
fn section_triangle_points(
  plane : SectionPlane,
  a : Point3,
  b : Point3,
  c : Point3,
  options : SectionOptions,
) -> (Array[Point3], Bool) {
  let da = plane.signed_distance(a)
  let db = plane.signed_distance(b)
  let dc = plane.signed_distance(c)
  let points : Array[Point3] = []
  for
    item in [
      section_edge_point(a, b, da, db, options.tolerance),
      section_edge_point(b, c, db, dc, options.tolerance),
      section_edge_point(c, a, dc, da, options.tolerance),
    ] {
    match item {
      Some(point) => section_add_unique(points, point, options.tolerance)
      None => ()
    }
  }
  let coplanar = da.abs() <= options.tolerance &&
    db.abs() <= options.tolerance &&
    dc.abs() <= options.tolerance
  if coplanar && !options.include_coplanar_edges {
    ([], true)
  } else {
    (points, coplanar)
  }
}

///|
fn section_farthest_pair(points : Array[Point3]) -> (Point3, Point3)? {
  if points.length() < 2 {
    return None
  }
  let mut first = points[0]
  let mut second = points[1]
  let mut longest = section_point_distance(first, second)
  for i in 0.. longest {
        longest = distance
        first = points[i]
        second = points[j]
      }
    }
  }
  Some((first, second))
}

///|
/// Intersect a standalone mesh, retaining the source triangle index.
pub fn section_mesh(
  product_id : Int,
  mesh : Mesh,
  plane : SectionPlane,
  options? : SectionOptions = default_section_options(),
) -> Array[SectionSegment] {
  let result : Array[SectionSegment] = []
  if !plane.is_valid() {
    return result
  }
  for index in 0.. {
        let (points, coplanar) = section_triangle_points(
          plane, a, b, c, options,
        )
        match section_farthest_pair(points) {
          Some((start, end)) if section_point_distance(start, end) >
            options.tolerance =>
            result.push({
              product_id,
              source_triangle: index,
              start,
              end,
              length: section_point_distance(start, end),
              coplanar,
            })
          _ => ()
        }
      }
      _ => ()
    }
    if options.maximum_segments > 0 &&
      result.length() >= options.maximum_segments {
      break
    }
  }
  result
}

///|
fn section_append_orient(
  points : Array[Point3],
  segment : SectionSegment,
  tolerance : Double,
) -> Bool {
  if points.length() == 0 {
    points.push(segment.start)
    points.push(segment.end)
    return true
  }
  let first = points[0]
  let last = points[points.length() - 1]
  if section_point_distance(last, segment.start) <= tolerance {
    points.push(segment.end)
    true
  } else if section_point_distance(last, segment.end) <= tolerance {
    points.push(segment.start)
    true
  } else if section_point_distance(first, segment.end) <= tolerance {
    points.insert(0, segment.start)
    true
  } else if section_point_distance(first, segment.start) <= tolerance {
    points.insert(0, segment.end)
    true
  } else {
    false
  }
}

///|
fn section_polyline_length(points : Array[Point3]) -> Double {
  let mut total = 0.0
  for index in 1.. Double {
  if points.length() < 3 {
    return 0.0
  }
  let axis = plane.normal
  let mut area = 0.0
  for index in 0.. Array[SectionPolyline] {
  let remaining = segments.copy()
  let result : Array[SectionPolyline] = []
  while remaining.length() > 0 {
    let seed = remaining.remove(0)
    let points : Array[Point3] = [seed.start, seed.end]
    let mut changed = true
    while changed {
      changed = false
      let mut index = 0
      while index < remaining.length() {
        if remaining[index].product_id == seed.product_id &&
          section_append_orient(points, remaining[index], tolerance) {
          ignore(remaining.remove(index))
          changed = true
        } else {
          index += 1
        }
      }
    }
    let closed = points.length() > 2 &&
      section_point_distance(points[0], points[points.length() - 1]) <=
      tolerance
    result.push({
      product_id: seed.product_id,
      points,
      closed,
      length: section_polyline_length(points),
      area: section_projected_area(points, plane),
    })
  }
  result
}

///|
fn section_add_product_id(ids : Array[Int], product_id : Int) -> Unit {
  if !ids.contains(product_id) {
    ids.push(product_id)
  }
}

///|
/// Extract a section across all products with supported mesh geometry.
pub fn section_model(
  model : Model,
  plane : SectionPlane,
  options? : SectionOptions = default_section_options(),
) -> SectionResult raise ParseError {
  let index = model.index()
  let segments : Array[SectionSegment] = []
  for product in model.products() {
    let mesh = mesh_for_product_advanced(index, product)
    segments.append(section_mesh(product.id, mesh, plane, options~))
    if options.maximum_segments > 0 &&
      segments.length() >= options.maximum_segments {
      break
    }
  }
  let polylines = section_build_polylines(
    segments,
    plane,
    options.join_tolerance,
  )
  let ids : Array[Int] = []
  for segment in segments {
    section_add_product_id(ids, segment.product_id)
  }
  {
    plane,
    segments,
    polylines,
    product_ids: ids,
    intersected_triangle_count: segments.length(),
    total_length: segments.fold(init=0.0, (sum, item) => sum + item.length),
    bounds: segments.fold(init=BoundingBox::empty(), (result, item) => {
      merge_bounds(result, BoundingBox::from_points([item.start, item.end]))
    }),
  }
}

///|
pub fn SectionResult::segment_count(self : SectionResult) -> Int {
  self.segments.length()
}

///|
pub fn SectionResult::polyline_count(self : SectionResult) -> Int {
  self.polylines.length()
}

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

///|
pub fn SectionResult::closed_count(self : SectionResult) -> Int {
  self.polylines.fold(init=0, (count, line) => {
    if line.closed {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn SectionResult::total_area(self : SectionResult) -> Double {
  self.polylines.fold(init=0.0, (sum, line) => sum + line.area)
}

///|
pub fn SectionResult::segments_for_product(
  self : SectionResult,
  product_id : Int,
) -> Array[SectionSegment] {
  self.segments.filter(segment => segment.product_id == product_id)
}

///|
pub fn SectionResult::polylines_for_product(
  self : SectionResult,
  product_id : Int,
) -> Array[SectionPolyline] {
  self.polylines.filter(line => line.product_id == product_id)
}

///|
pub fn SectionResult::headline(self : SectionResult) -> String {
  "products=" +
  self.product_count().to_string() +
  " triangles=" +
  self.intersected_triangle_count.to_string() +
  " segments=" +
  self.segment_count().to_string() +
  " polylines=" +
  self.polyline_count().to_string() +
  " length=" +
  self.total_length.to_string() +
  " area=" +
  self.total_area().to_string()
}

///|
fn section_point_json(point : Point3) -> String {
  "[" +
  point.x.to_string() +
  "," +
  point.y.to_string() +
  "," +
  point.z.to_string() +
  "]"
}

///|
pub fn SectionResult::to_json(self : SectionResult) -> String {
  let segments = self.segments
    .map(segment => {
      "{\"product_id\":" +
      segment.product_id.to_string() +
      ",\"triangle\":" +
      segment.source_triangle.to_string() +
      ",\"start\":" +
      section_point_json(segment.start) +
      ",\"end\":" +
      section_point_json(segment.end) +
      ",\"length\":" +
      segment.length.to_string() +
      "}"
    })
    .join(",")
  let lines = self.polylines
    .map(line => {
      "{\"product_id\":" +
      line.product_id.to_string() +
      ",\"closed\":" +
      line.closed.to_string() +
      ",\"length\":" +
      line.length.to_string() +
      ",\"area\":" +
      line.area.to_string() +
      ",\"points\":[" +
      line.points.map(section_point_json).join(",") +
      "]}"
    })
    .join(",")
  "{\"product_ids\":[" +
  self.product_ids.map(id => id.to_string()).join(",") +
  "],\"segment_count\":" +
  self.segment_count().to_string() +
  ",\"polyline_count\":" +
  self.polyline_count().to_string() +
  ",\"total_length\":" +
  self.total_length.to_string() +
  ",\"total_area\":" +
  self.total_area().to_string() +
  ",\"segments\":[" +
  segments +
  "],\"polylines\":[" +
  lines +
  "]}"
}

///|
pub fn SectionResult::to_csv(self : SectionResult) -> String {
  let builder = StringBuilder()
  builder <+
    "product_id,triangle,start_x,start_y,start_z,end_x,end_y,end_z,length,coplanar\n"
  for segment in self.segments {
    builder <+
      "\{segment.product_id},\{segment.source_triangle},\{segment.start.x},\{segment.start.y},\{segment.start.z},\{segment.end.x},\{segment.end.y},\{segment.end.z},\{segment.length},\{segment.coplanar}\n"
  }
  builder.to_string()
}

///|
pub fn SectionResult::to_markdown(self : SectionResult) -> String {
  let builder = StringBuilder()
  builder <+ "# Section analysis\n\n"
  builder <+ "- \{self.headline()}\n"
  builder <+ "- Plane: \{self.plane.headline()}\n"
  builder <+ "- Closed polylines: \{self.closed_count()}\n\n"
  builder <+ "| Product | Triangle | Length | Coplanar |\n"
  builder <+ "| ---: | ---: | ---: | --- |\n"
  for segment in self.segments {
    builder <+
      "| \{segment.product_id} | \{segment.source_triangle} | \{segment.length} | \{segment.coplanar} |\n"
  }
  builder.to_string()
}

///|
pub fn SectionPolyline::point_count(self : SectionPolyline) -> Int {
  self.points.length()
}

///|
pub fn SectionPolyline::is_degenerate(
  self : SectionPolyline,
  tolerance? : Double = 0.000001,
) -> Bool {
  self.length <= tolerance || self.points.length() < 2
}

///|
pub fn SectionPolyline::first(self : SectionPolyline) -> Point3? {
  self.points.get(0)
}

///|
pub fn SectionPolyline::last(self : SectionPolyline) -> Point3? {
  if self.points.length() == 0 {
    None
  } else {
    Some(self.points[self.points.length() - 1])
  }
}

///|
pub fn section_result_product_histogram(
  result : SectionResult,
) -> Map[Int, Int] {
  let histogram : Map[Int, Int] = Map([])
  for segment in result.segments {
    histogram.update_or_default(segment.product_id, 1, count => count + 1)
  }
  histogram
}

///|
pub fn section_result_long_segments(
  result : SectionResult,
  minimum_length : Double,
) -> Array[SectionSegment] {
  result.segments.filter(segment => segment.length >= minimum_length)
}

///|
pub fn section_result_bounds_for(
  result : SectionResult,
  product_id : Int,
) -> BoundingBox {
  result
  .segments_for_product(product_id)
  .fold(init=BoundingBox::empty(), (bounds, segment) => {
    merge_bounds(bounds, BoundingBox::from_points([segment.start, segment.end]))
  })
}

///|
pub fn section_result_summary(result : SectionResult) -> Array[String] {
  [
    result.headline(),
    "closed=" + result.closed_count().to_string(),
    "products=" + result.product_ids.map(id => id.to_string()).join(","),
  ]
}