///|
/// Number of line segments used to approximate each cubic Bezier span when
/// flattening an `ExtrudedBezierProfile` for STEP output.
///
/// STEP cannot express the glyph outlines directly, so the curves are sampled
/// into polylines before the profile is extruded. Eight segments per span keeps
/// the resulting files comparable in size to the `segments=8` polyline path.
let bezier_segments_per_span : Int32 = 8

///|
/// cubic_bezier_point evaluates the cubic Bezier defined by the four control
/// points at parameter `t`.
fn cubic_bezier_point(
  p0 : Vec2,
  c1 : Vec2,
  c2 : Vec2,
  p3 : Vec2,
  t : Double,
) -> Vec2 {
  let u = 1.0 - t
  let uu = u * u
  let tt = t * t
  let uuu = uu * u
  let ttt = tt * t
  let ut3 = 3.0 * uu * t
  let tu3 = 3.0 * u * tt
  (
    uuu * p0.0 + ut3 * c1.0 + tu3 * c2.0 + ttt * p3.0,
    uuu * p0.1 + ut3 * c1.1 + tu3 * c2.1 + ttt * p3.1,
  )
}

///|
/// flatten_bezier_path samples a `BezierPath` into a polyline.
///
/// Handles are stored as absolute positions, so the cubic span from anchor `i`
/// to anchor `i+1` uses `a[i].position`, `a[i].handle_out`,
/// `a[i+1].handle_in`, and `a[i+1].position`. Straight spans (zero-length
/// offsets) contribute only their start point, which avoids bloating glyphs
/// whose stems are simple lines. Closed paths include the wrap-around span.
fn flatten_bezier_path(path : BezierPath) -> Array[Vec2] {
  let anchors = path.anchors
  let n = anchors.length()
  if n < 2 {
    return []
  }
  let points : Array[Vec2] = []
  let span_count = if path.closed { n } else { n - 1 }
  for i in 0.. Array[Vec2] {
  let eps_sq = 1.0e-18
  let result : Array[Vec2] = []
  for pt in points {
    let duplicate = match result.last() {
      Some(prev) => {
        let dx = pt.0 - prev.0
        let dy = pt.1 - prev.1
        dx * dx + dy * dy <= eps_sq
      }
      None => false
    }
    if !duplicate {
      result.push(pt)
    }
  }
  if result.length() > 1 {
    let first = result[0]
    let last = result[result.length() - 1]
    let dx = first.0 - last.0
    let dy = first.1 - last.1
    if dx * dx + dy * dy <= eps_sq {
      let _ = result.pop()
    }
  }
  result
}

///|
/// bezier_path_groups splits `BezierPath`s into (outer, holes) groups while
/// keeping the original curves.
///
/// Disconnected glyph components (for example the dot of an "i", or the pieces
/// of a "%") become separate groups so each can be emitted as its own closed
/// solid. `path.clear == true` marks a hole (see `BezierPath.clear`), and a
/// hole is attached to every outer loop that contains it. Degenerate paths with
/// fewer than three flattened points are skipped.
fn bezier_path_groups(paths : Array[BezierPath]) -> Array[Array[BezierPath]] {
  let outers : Array[(BezierPath, Array[Vec2])] = []
  let holes : Array[(BezierPath, Array[Vec2])] = []
  for path in paths {
    let flat = flatten_bezier_path(path)
    if flat.length() < 3 {
      continue
    }
    if path.clear {
      holes.push((path, flat))
    } else {
      outers.push((path, flat))
    }
  }
  let groups : Array[Array[BezierPath]] = []
  for outer_pair in outers {
    let outer = outer_pair.0
    let outer_poly = outer_pair.1
    let group : Array[BezierPath] = [outer]
    for hole_pair in holes {
      let hole = hole_pair.0
      let hole_poly = hole_pair.1
      if hole_poly.length() > 0 && is_point_in_polygon(hole_poly[0], outer_poly) {
        group.push(hole)
      }
    }
    groups.push(group)
  }
  groups
}

///|
/// flatten_group_profile flattens one (outer, holes) group into a `Profile2D`.
///
/// Outers are wound counter-clockwise and holes clockwise, matching
/// `collect_profiles`.
fn flatten_group_profile(group : Array[BezierPath]) -> Profile2D {
  let outer = force_ccw(flatten_bezier_path(group[0]))
  let holes = group
    .iter()
    .drop(1)
    .map(fn(path) { force_cw(flatten_bezier_path(path)) })
    .collect()
  Profile2D::{ outer, holes, }
}

///|
/// compile_extruded_bezier_profile writes a B-Rep for an
/// `ExtrudedBezierProfile`.
///
/// Contours with only straight spans keep the cheap polyline/line path used by
/// `compile_extruded_profile`. A group with any curved span is emitted with
/// exact B-spline geometry: one closed cubic `B_SPLINE_CURVE_WITH_KNOTS` per
/// contour, two planar caps, and a `SURFACE_OF_LINEAR_EXTRUSION` lateral face.
pub fn compile_extruded_bezier_profile(
  ctx : StepContext,
  node : SceneNode,
  extrusion : ExtrudedBezierProfile,
  reverse~ : Bool = false,
  parent~ : Transform? = None,
) -> Array[@step.Ref] {
  if extrusion.height_mm == 0.0 {
    return []
  }
  let faces : Array[@step.Ref] = []
  for group in bezier_path_groups(extrusion.paths) {
    if group.is_empty() {
      continue
    }
    if group.any(fn(path) { path_has_curved_span(path) }) {
      faces.push_iter(
        compile_curved_bezier_group(
          ctx,
          node,
          group,
          extrusion.height_mm,
          reverse~,
          parent~,
        ).iter(),
      )
    } else {
      let profile = flatten_group_profile(group)
      if profile.outer.length() < 3 {
        continue
      }
      let ext = ExtrudedProfile::{
        profile,
        height_mm: extrusion.height_mm,
        round_bore: None,
      }
      faces.push_iter(
        compile_extruded_profile(ctx, node, ext, reverse~, parent~).iter(),
      )
    }
  }
  faces
}