///|
/// ChamferedPrism is a convex prism whose outer boundary is a stack of
/// parallel-inset rings, with an optional prismatic through-hole.
///
/// It is the generalisation of `CalibrationChamferBlock` needed for real
/// chamfers and fillets on hex nuts, screw heads, washers, and plates: each
/// ring is the base polygon offset inward by `inset`, placed at `z`. Consecutive
/// rings are joined by planar ruled quads (edges stay parallel under a uniform
/// parallel offset), and the ends are planar caps. With a through-hole the caps
/// become annuli and the hole gets vertical walls, so a blind socket is built
/// by placing this shape over a solid floor layer.
pub struct ChamferedPrism {
  base : Array[Vec2]
  levels : Array[(Double, Double)]
  hole : Array[Vec2]?
  /// Optional per-level hole profile. Each entry is `(z, inward_inset)` applied
  /// to `hole`, which is the largest (outermost) hole profile. The first and
  /// last `z` must equal the first and last outer level `z`, so the end caps
  /// close. This is how a real tapered hole (a socket lead-in chamfer or a
  /// countersink) is represented: the hole ring shrinks inward with depth.
  hole_levels : Array[(Double, Double)]?
  /// Optional analytic circular through-bore `(center, diameter)`. Unlike the
  /// polygonal `hole`, this compiles to a real `CYLINDRICAL_SURFACE` with
  /// circular bounding edges, so a hex nut's bore never becomes a polygon.
  round_bore : (Vec2, Double)?
}

///|
/// ChamferedPrism::new stores a ring stack without validating it. `levels` are
/// `(z, inset)` pairs with strictly increasing `z` and non-negative insets.
///
/// `hole_levels` optionally gives a `(z, inward_inset)` ring stack for the
/// hole; it makes a tapered hole such as a socket lead-in chamfer. Without it
/// the hole has vertical walls.
pub fn ChamferedPrism::new(
  base : Array[Vec2],
  levels : Array[(Double, Double)],
  hole? : Array[Vec2],
  hole_levels? : Array[(Double, Double)],
  round_bore? : (Vec2, Double),
) -> ChamferedPrism {
  { base, levels, hole, hole_levels, round_bore, }
}

///|
/// ChamferedPrism::from_chamfer builds a prism of `height` with an optional
/// chamfer at the bottom and top outer edges. `chamfer` is the parallel inset
/// at each face; `chamfer_depth` is the axial length of the chamfer.
pub fn ChamferedPrism::from_chamfer(
  base : Array[Vec2],
  height : Double,
  bottom_chamfer~ : Double = 0.0,
  top_chamfer~ : Double = 0.0,
  chamfer_depth~ : Double = 0.0,
  hole? : Array[Vec2],
  round_bore? : (Vec2, Double),
) -> ChamferedPrism {
  let levels : Array[(Double, Double)] = []
  if bottom_chamfer > 0.0 && chamfer_depth > 0.0 {
    levels.push((0.0, bottom_chamfer))
    levels.push((chamfer_depth, 0.0))
  } else {
    levels.push((0.0, 0.0))
  }
  if top_chamfer > 0.0 && chamfer_depth > 0.0 {
    levels.push((height - chamfer_depth, 0.0))
    levels.push((height, top_chamfer))
  } else {
    levels.push((height, 0.0))
  }
  { base, levels, hole, hole_levels: None, round_bore, }
}

///|
/// PrismError reports why a `ChamferedPrism` cannot be compiled.
pub(all) enum PrismError {
  PrismTooFewVertices
  PrismBadLevels
  PrismInsetTooLarge(Int32)
  PrismSelfIntersection
  PrismBadHole
}

///|
/// PrismError::message returns a human-readable diagnostic.
pub fn PrismError::message(self : PrismError) -> String {
  match self {
    PrismTooFewVertices => "chamfered prism base needs at least three vertices"
    PrismBadLevels => "chamfered prism levels must have increasing z"
    PrismInsetTooLarge(i) => "chamfered prism inset \{i} does not fit the base"
    PrismSelfIntersection => "chamfered prism inset ring self-intersects"
    PrismBadHole => "chamfered prism hole is invalid"
  }
}

///|
/// ChamferedPrism::validate checks the ring stack.
pub fn ChamferedPrism::validate(
  self : ChamferedPrism,
) -> Result[Unit, PrismError] {
  let n = self.base.length()
  if n < 3 || self.levels.length() < 2 {
    return Err(PrismTooFewVertices)
  }
  let mut prev_z = -1.0e300
  for i in 0.. r
      None => return Err(PrismInsetTooLarge(i))
    }
    if !Profile2D::is_simple(ring) {
      return Err(PrismSelfIntersection)
    }
  }
  match self.hole {
    Some(h) =>
      if h.length() < 3 || !Profile2D::is_simple(h) {
        return Err(PrismBadHole)
      }
    None => ()
  }
  match self.round_bore {
    Some((center, diameter)) => {
      if diameter <= 0.0 || self.hole is Some(_) {
        return Err(PrismBadHole)
      }
      if !is_point_in_polygon(center, self.base) {
        return Err(PrismBadHole)
      }
    }
    None => ()
  }
  match self.hole_levels {
    Some(hls) =>
      match self.hole {
        None => return Err(PrismBadHole)
        Some(h) => {
          if hls.length() < 2 {
            return Err(PrismBadHole)
          }
          let z_first = self.levels[0].0
          let z_last = self.levels[self.levels.length() - 1].0
          if (hls[0].0 - z_first).abs() > 1.0e-9 ||
            (hls[hls.length() - 1].0 - z_last).abs() > 1.0e-9 {
            return Err(PrismBadHole)
          }
          let mut prev_z = -1.0e300
          let hccw = if polygon_area_2d(h) < 0.0 { h.rev() } else { h }
          for i in 0.. return Err(PrismBadHole)
              Some(ring) =>
                if !Profile2D::is_simple(ring) {
                  return Err(PrismBadHole)
                }
            }
          }
        }
      }
    None => ()
  }
  Ok(())
}

///|
/// chamfer_inset offsets a convex polygon inward by `c` along each edge.
fn chamfer_inset(profile : Array[Vec2], c : Double) -> Array[Vec2]? {
  if c <= 0.0 {
    return Some(profile)
  }
  let n = profile.length()
  if n < 3 {
    return None
  }
  let ccw = if polygon_area_2d(profile) < 0.0 { profile.rev() } else { profile }
  let out : Array[Vec2] = []
  for i in 0..= @math.PI - 1.0e-9 {
      return None
    }
    let half = theta / 2.0
    let inset_dist = c / @math.sin(half)
    let bis = v2_norm((u1.0 + u2.0, u1.1 + u2.1))
    out.push((cur.0 + bis.0 * inset_dist, cur.1 + bis.1 * inset_dist))
  }
  if !is_convex_ccw(out) || polygon_area_2d(out) <= 0.0 {
    return None
  }
  // A valid inward offset must shrink the polygon and keep every point inside.
  if polygon_area_2d(out) >= polygon_area_2d(ccw) - 1.0e-12 {
    return None
  }
  for p in out {
    if !is_point_in_polygon(p, ccw) {
      return None
    }
  }
  Some(out)
}

///|
/// is_convex_ccw reports whether a counter-clockwise polygon is convex.
fn is_convex_ccw(pts : Array[Vec2]) -> Bool {
  let n = pts.length()
  if n < 3 {
    return false
  }
  for i in 0.. Array[@step.Ref] {
  let xform = node_transform(node, parent)
  match prism.validate() {
    Err(_) => return []
    Ok(_) => ()
  }
  match prism.round_bore {
    Some((center, diameter)) =>
      return compile_chamfered_prism_round_bore(
        ctx,
        node,
        prism,
        center,
        diameter,
        reverse~,
        parent~,
      )
    None => ()
  }
  let base_raw = prism.base
  let base = if polygon_area_2d(base_raw) < 0.0 {
    base_raw.rev()
  } else {
    base_raw
  }
  let nc = base.length()
  let nl = prism.levels.length()
  // Build points: every level ring, then the hole rings at the first/last z.
  let pts_raw : Array[Vec3] = []
  let rings : Array[Array[Int32]] = []
  for li in 0.. r
      None => return []
    }
    let idxs : Array[Int32] = []
    for p in ring {
      idxs.push(pts_raw.length())
      pts_raw.push((p.0, p.1, z))
    }
    rings.push(idxs)
  }
  let z0 = prism.levels[0].0
  let z1 = prism.levels[nl - 1].0
  let hole_rings : Array[Array[Int32]] = []
  match prism.hole {
    Some(h) =>
      match prism.hole_levels {
        Some(hls) => {
          let hccw = if polygon_area_2d(h) < 0.0 { h.rev() } else { h }
          for level in hls {
            let (z, inset) = level
            let ring = match chamfer_inset(hccw, inset) {
              Some(r) => r.rev()
              None => return []
            }
            let idxs : Array[Int32] = []
            for p in ring {
              idxs.push(pts_raw.length())
              pts_raw.push((p.0, p.1, z))
            }
            hole_rings.push(idxs)
          }
        }
        None => {
          let hcw = if polygon_area_2d(h) > 0.0 { h.rev() } else { h }
          let idx0 : Array[Int32] = []
          for p in hcw {
            idx0.push(pts_raw.length())
            pts_raw.push((p.0, p.1, z0))
          }
          let idx1 : Array[Int32] = []
          for p in hcw {
            idx1.push(pts_raw.length())
            pts_raw.push((p.0, p.1, z1))
          }
          hole_rings.push(idx0)
          hole_rings.push(idx1)
        }
      }
    None => ()
  }
  let pts = pts_raw.map(fn(pt) { xform.apply(pt) })
  let cp_refs = pts.map(fn(pt) {
    let (px, py, pz) = pt
    ctx.cartesian_point(px, py, pz)
  })
  let vp_refs = cp_refs.map(fn(cp) { ctx.vertex_point(cp) })
  let edges : Array[EdgeInfo] = []
  let seen : Map[String, Bool] = Map([])
  let add_edge = fn(a : Int32, b : Int32) -> Unit {
    let key = if a < b { "\{a}:\{b}" } else { "\{b}:\{a}" }
    if seen.get(key).unwrap_or(false) {
      return
    }
    seen.set(key, true)
    let start = if a < b { a } else { b }
    let end = if a < b { b } else { a }
    let dir_raw = v_norm(v_sub(pts[end], pts[start]))
    let dir_ref = ctx.direction(dir_raw.0, dir_raw.1, dir_raw.2)
    let vec_ref = ctx.vector(dir_ref, 1.0)
    let line_ref = ctx.line(cp_refs[start], vec_ref)
    let ec_ref = ctx.edge_curve(vp_refs[start], vp_refs[end], line_ref, true)
    edges.push(EdgeInfo::{ start, end, edge: ec_ref, })
  }
  let connect_ring = fn(a : Array[Int32], b : Array[Int32]) -> Unit {
    let n = a.length()
    for k in 0.. 0 {
        let prev = hole_rings[hi - 1]
        for k in 0.. Array[@step.Ref] {
  let xform = node_transform(node, parent)
  let base_raw = prism.base
  let base = if polygon_area_2d(base_raw) < 0.0 {
    base_raw.rev()
  } else {
    base_raw
  }
  let nc = base.length()
  let nl = prism.levels.length()
  let pts_raw : Array[Vec3] = []
  let rings : Array[Array[Int32]] = []
  for li in 0.. r
      None => return []
    }
    let idxs : Array[Int32] = []
    for p in ring {
      idxs.push(pts_raw.length())
      pts_raw.push((p.0, p.1, z))
    }
    rings.push(idxs)
  }
  let pts = pts_raw.map(fn(pt) { xform.apply(pt) })
  let cp_refs = pts.map(fn(pt) {
    let (px, py, pz) = pt
    ctx.cartesian_point(px, py, pz)
  })
  let vp_refs = cp_refs.map(fn(cp) { ctx.vertex_point(cp) })
  let edges : Array[EdgeInfo] = []
  let seen : Map[String, Bool] = Map([])
  let add_edge = fn(a : Int32, b : Int32) -> Unit {
    let key = if a < b { "\{a}:\{b}" } else { "\{b}:\{a}" }
    if seen.get(key).unwrap_or(false) {
      return
    }
    seen.set(key, true)
    let start = if a < b { a } else { b }
    let end = if a < b { b } else { a }
    let dir_raw = v_norm(v_sub(pts[end], pts[start]))
    let dir_ref = ctx.direction(dir_raw.0, dir_raw.1, dir_raw.2)
    let vec_ref = ctx.vector(dir_ref, 1.0)
    let line_ref = ctx.line(cp_refs[start], vec_ref)
    let ec_ref = ctx.edge_curve(vp_refs[start], vp_refs[end], line_ref, true)
    edges.push(EdgeInfo::{ start, end, edge: ec_ref, })
  }
  let connect_ring = fn(a : Array[Int32], b : Array[Int32]) -> Unit {
    let n = a.length()
    for k in 0.. Vec3 {
  let centroid = ring_quad_centroid(pts, cycle)
  let av = cycle_area_3d(pts, cycle)
  let mut n = v_norm(av)
  let radial_dot = n.0 * centroid.0 + n.1 * centroid.1
  let want_outward = !toward_axis
  if want_outward && radial_dot < 0.0 {
    n = (-n.0, -n.1, -n.2)
  } else if !want_outward && radial_dot > 0.0 {
    n = (-n.0, -n.1, -n.2)
  }
  n
}

///|
fn ring_quad_centroid(pts : Array[Vec3], cycle : Array[Int32]) -> Vec3 {
  let n = cycle.length()
  let mut x = 0.0
  let mut y = 0.0
  let mut z = 0.0
  for i in cycle {
    let p = pts[i]
    x = x + p.0
    y = y + p.1
    z = z + p.2
  }
  let d = Double::from_int(n)
  (x / d, y / d, z / d)
}

///|
/// chamfered_prism_mesh builds a polygonal mesh approximation for the optional
/// Blender export. STEP output uses the analytic compiler, not this mesh.
pub fn chamfered_prism_mesh(
  prism : ChamferedPrism,
) -> (Array[Vec3], Array[Array[Int32]]) {
  let base_raw = prism.base
  let base = if polygon_area_2d(base_raw) < 0.0 {
    base_raw.rev()
  } else {
    base_raw
  }
  let nc = base.length()
  let verts : Array[Vec3] = []
  let rings : Array[Array[Int32]] = []
  for level in prism.levels {
    let (z, inset) = level
    let ring = match chamfer_inset(base, inset) {
      Some(r) => r
      None => base
    }
    let idxs : Array[Int32] = []
    for p in ring {
      idxs.push(verts.length())
      verts.push((p.0, p.1, z))
    }
    rings.push(idxs)
  }
  let faces : Array[Array[Int32]] = []
  let nl = rings.length()
  for li in 0..<(nl - 1) {
    let a = rings[li]
    let b = rings[li + 1]
    for k in 0.. Shape {
  Shape::ChamferedPrism(self)
}