///|
// Native smooth B-rep for a rectangular profile swept along a helical path.
//
// The four sides of the swept strip are ruled B-spline surfaces between the
// four corner helices (cubic Bezier spans with exact endpoint tangents), and
// the two ends are planar caps. The result is the six-face topology of a
// spring-washer STEP reference rather than a stack of planar facets.
//
// The corner paths are sampled from the sweep path; the curve control points
// use cubic Hermite spans, so the emitted surfaces deviate from the ideal
// helix by only a few 1e-5 mm at 24 spans per turn.

///|
priv struct SmoothCurve {
  curve_ref : @step.Ref
  cps : Array[@step.Ref]
  cps_pos : Array[Vec3]
  start_cp : @step.Ref
  end_cp : @step.Ref
}

///|
fn smooth_round(x : Double) -> Int32 {
  (x + 0.5).to_int()
}

///|
fn smooth_average(points : Array[Vec3]) -> Vec3 {
  let mut x = 0.0
  let mut y = 0.0
  let mut z = 0.0
  let n = points.length()
  for p in points {
    x = x + p.0
    y = y + p.1
    z = z + p.2
  }
  let d = n.to_double()
  (x / d, y / d, z / d)
}

///|
fn smooth_polygon_normal(points : Array[Vec3]) -> Vec3 {
  let mut nx = 0.0
  let mut ny = 0.0
  let mut nz = 0.0
  let n = points.length()
  for i in 0.. SmoothCurve {
  let num_spans = indices.length() - 1
  let points : Array[Vec3] = Array::make(num_spans + 1, (0.0, 0.0, 0.0))
  let tangents : Array[Vec3] = Array::make(num_spans + 1, (0.0, 0.0, 0.0))
  for k in 0..<=num_spans {
    let idx = indices[k]
    let p = corner_pts[idx * 4 + profile_index]
    points[k] = p
    let tangent = if idx == 0 {
      v_sub(corner_pts[(idx + 1) * 4 + profile_index], p)
    } else if idx == num_path_pts - 1 {
      v_sub(p, corner_pts[(idx - 1) * 4 + profile_index])
    } else {
      v_scale(
        v_sub(
          corner_pts[(idx + 1) * 4 + profile_index],
          corner_pts[(idx - 1) * 4 + profile_index],
        ),
        0.5,
      )
    }
    tangents[k] = tangent
  }
  let control_pts : Array[Vec3] = []
  for k in 0.. (@step.Ref, Bool) {
  let du = v_sub(row_b.cps_pos[0], row_a.cps_pos[0])
  let dv = v_sub(row_a.cps_pos[1], row_a.cps_pos[0])
  let natural_normal = v_cross(du, dv)
  // Orient the surface patch so its natural normal points outward; the face
  // then uses same_sense = .T. for every patch (the reference form).
  let (row0, row1) = if v_dot(natural_normal, expected_out) >= 0.0 {
    (row_a, row_b)
  } else {
    (row_b, row_a)
  }
  let cp_row = fn(row : Array[@step.Ref]) -> String {
    "(" + row.map(fn(r) { "#\{r.id.value}" }).join(",") + ")"
  }
  let rows = "(" + cp_row(row0.cps) + "," + cp_row(row1.cps) + ")"
  let v_mults_text = "(" + v_mults.map(fn(m) { "\{m}" }).join(",") + ")"
  let v_knots_text = "(" + v_knots.join(",") + ")"
  // Emit the AP214 simple-entity form used by the downloaded references:
  // B_SPLINE_SURFACE_WITH_KNOTS('',u_deg,v_deg,((row0),(row1)),...).
  let args = "('',1,3," +
    rows +
    ",.UNSPECIFIED.,.F.,.F.,.F.,(2,2)," +
    v_mults_text +
    ",(0.,1.)," +
    v_knots_text +
    ",.PIECEWISE_BEZIER_KNOTS.)"
  let raw = @step.RawEntity::new("B_SPLINE_SURFACE_WITH_KNOTS", args)
  let surface_ref = ctx.add(@repository.any_entity_raw_entity(raw))
  (surface_ref, true)
}

///|
fn smooth_line_edge(
  ctx : StepContext,
  a_cp : @step.Ref,
  a_vp : @step.Ref,
  _b_cp : @step.Ref,
  b_vp : @step.Ref,
  a_pos : Vec3,
  b_pos : Vec3,
) -> @step.Ref {
  let direction = v_norm(v_sub(b_pos, a_pos))
  let dir_ref = ctx.direction(direction.0, direction.1, direction.2)
  let vector_ref = ctx.vector(dir_ref, 1.0)
  let line_ref = ctx.line(a_cp, vector_ref)
  ctx.edge_curve(a_vp, b_vp, line_ref, true)
}

///|
fn smooth_cap_plane(
  ctx : StepContext,
  outward : Vec3,
  location : Vec3,
  reference : Vec3,
) -> @step.Ref {
  let location_ref = ctx.cartesian_point(location.0, location.1, location.2)
  let axis_ref = ctx.direction(outward.0, outward.1, outward.2)
  let ref_ref = ctx.direction(reference.0, reference.1, reference.2)
  let placement = ctx.axis2_placement_3d(location_ref, axis_ref, ref_ref)
  let plane = @geometry.Plane::new("", placement)
  ctx.add(@repository.any_entity_plane(plane))
}

///|
fn smooth_face(
  ctx : StepContext,
  edges : Array[(@step.Ref, Bool)],
  surface : @step.Ref,
  same_sense : Bool,
) -> @step.Ref {
  // Build the loop with the orientation that agrees with the surface normal;
  // reversing the caller's cycle keeps every shared edge opposite.
  let reversed = edges.rev().map(fn(e) { (e.0, !e.1) })
  let oriented = reversed.map(fn(e) { ctx.oriented_edge(e.0, e.1) })
  let edge_loop_ref = ctx.edge_loop(oriented)
  let bound = ctx.face_outer_bound(edge_loop_ref, true)
  ctx.advanced_face([bound], surface, same_sense)
}

///|
fn compile_smooth_helix(
  ctx : StepContext,
  node : SceneNode,
  s : SweptProfile,
  reverse~ : Bool = false,
  parent~ : Transform? = None,
) -> Array[@step.Ref] {
  let path = s.path
  let profile = s.profile
  let num_path_pts = path.points.length()
  if num_path_pts < 4 || profile.vertices.length() != 4 {
    return []
  }
  let xform = node_transform(node, parent)

  // Profile-local coordinates (the same mapping the faceted sweep uses).
  let prof_z = v_norm(profile.normal_dir)
  let prof_x = v_norm(profile.right_dir)
  let prof_y = v_norm(v_cross(prof_z, prof_x))
  let m_global_to_prof = Matrix3x3::{
    m00: prof_x.0,
    m01: prof_x.1,
    m02: prof_x.2,
    m10: prof_y.0,
    m11: prof_y.1,
    m12: prof_y.2,
    m20: prof_z.0,
    m21: prof_z.1,
    m22: prof_z.2,
  }
  let local_verts = profile.vertices.map(fn(v) {
    apply_matrix(m_global_to_prof, v_sub(v, profile.center))
  })

  // Corner positions at every path point, in world coordinates.
  let corner_pts : Array[Vec3] = Array::make(num_path_pts * 4, (0.0, 0.0, 0.0))
  for i in 0.. 0.0 { 0 } else { 1 }
    role_index[radial * 2 + axial] = j
  }
  for role in 0..<4 {
    if role_index[role] < 0 {
      return []
    }
  }

  // Optional reference corner endpoints: blend the sampled corner curves so
  // they meet the given high-Z and low-Z cap vertices exactly. The correction
  // is expressed in the local (radial, axial, tangential) sweep frame so it
  // rotates with the helix instead of distorting the far side of the ring.
  match s.corner_override {
    Some((start_ref, end_ref)) =>
      if start_ref.length() == 4 && end_ref.length() == 4 {
        let start_t = xform.apply_dir(v_norm(path.tangents[0]))
        let start_n = xform.apply_dir(v_norm(path.normals[0]))
        let start_r = v_norm(v_cross(start_t, start_n))
        let last = num_path_pts - 1
        let end_t = xform.apply_dir(v_norm(path.tangents[last]))
        let end_n = xform.apply_dir(v_norm(path.normals[last]))
        let end_r = v_norm(v_cross(end_t, end_n))
        for role in 0..<4 {
          let j = role_index[role]
          let start_delta = v_sub(xform.apply(start_ref[role]), corner_pts[j])
          let end_delta = v_sub(
            xform.apply(end_ref[role]),
            corner_pts[last * 4 + j],
          )
          let s_lx = v_dot(start_delta, start_r)
          let s_ly = v_dot(start_delta, start_n)
          let s_lz = v_dot(start_delta, start_t)
          let e_lx = v_dot(end_delta, end_r)
          let e_ly = v_dot(end_delta, end_n)
          let e_lz = v_dot(end_delta, end_t)
          for i in 0.. ()
  }

  // Total sweep angle, then one cubic span per 15 degrees (24 per turn).
  let mut total_angle = 0.0
  for i in 0..<(num_path_pts - 1) {
    let a = path.points[i]
    let b = path.points[i + 1]
    let cross_z = a.0 * b.1 - a.1 * b.0
    let dot = a.0 * b.0 + a.1 * b.1
    total_angle = total_angle + @math.atan2(cross_z, dot)
  }
  let turns = Double::abs(total_angle) / (2.0 * @math.PI)
  let mut num_spans = smooth_round(turns * 24.0)
  if num_spans < 4 {
    num_spans = 4
  }
  if num_spans > num_path_pts - 1 {
    num_spans = num_path_pts - 1
  }
  let indices : Array[Int32] = Array::make(num_spans + 1, 0)
  for k in 0..<=num_spans {
    indices[k] = smooth_round(
      k.to_double() * (num_path_pts - 1).to_double() / num_spans.to_double(),
    )
  }
  let v_mults : Array[Int32] = [4]
  for _ in 1.. {
      let last = it.cps_pos.length() - 1
      let s_cycle = [it.cps_pos[0], ot.cps_pos[0], ob.cps_pos[0], ib.cps_pos[0]]
      let e_cycle = [
        it.cps_pos[last],
        ot.cps_pos[last],
        ob.cps_pos[last],
        ib.cps_pos[last],
      ]
      let s_center = smooth_average(s_cycle)
      let e_center = smooth_average(e_cycle)
      let mut s_axis = smooth_polygon_normal(s_cycle)
      if v_dot(s_axis, v_sub(e_center, s_center)) < 0.0 {
        s_axis = v_scale(s_axis, -1.0)
      }
      let mut e_axis = smooth_polygon_normal(e_cycle)
      if v_dot(e_axis, v_sub(s_center, e_center)) < 0.0 {
        e_axis = v_scale(e_axis, -1.0)
      }
      let s_ref = v_norm(v_sub(s_cycle[1], s_cycle[0]))
      let e_ref = v_norm(v_sub(e_cycle[1], e_cycle[0]))
      (s_axis, s_center, s_ref, e_axis, e_center, e_ref)
    }
    None =>
      (
        start_out,
        start_location,
        xform.apply_dir(start_radial_local),
        end_out,
        end_location,
        xform.apply_dir(start_radial_local),
      )
  }
  let start_plane = smooth_cap_plane(
    ctx, start_axis, start_center, start_ref_dir,
  )
  let end_plane = smooth_cap_plane(ctx, end_axis, end_center, end_ref_dir)

  let flip = fn(same : Bool) -> Bool { if reverse { !same } else { same } }

  let faces : Array[@step.Ref] = []
  // Top: start radial, outer helix forward, end radial reversed, inner
  // helix reversed.
  faces.push(
    smooth_face(
      ctx,
      [(s_top_s, true), (he_ot, true), (s_top_e, false), (he_it, false)],
      top_surf,
      flip(top_same),
    ),
  )
  // Bottom.
  faces.push(
    smooth_face(
      ctx,
      [(he_ib, true), (s_bot_e, true), (he_ob, false), (s_bot_s, false)],
      bottom_surf,
      flip(bottom_same),
    ),
  )
  // Inner.
  faces.push(
    smooth_face(
      ctx,
      [(he_it, true), (s_it_e, true), (he_ib, false), (s_it_s, false)],
      inner_surf,
      flip(inner_same),
    ),
  )
  // Outer.
  faces.push(
    smooth_face(
      ctx,
      [(s_ot_s, true), (he_ob, true), (s_ot_e, false), (he_ot, false)],
      outer_surf,
      flip(outer_same),
    ),
  )
  // Start cap (outward opposite the path tangent).
  faces.push(
    smooth_face(
      ctx,
      [(s_it_s, true), (s_bot_s, true), (s_ot_s, false), (s_top_s, false)],
      start_plane,
      flip(true),
    ),
  )
  // End cap (outward along the path tangent).
  faces.push(
    smooth_face(
      ctx,
      [(s_top_e, true), (s_ot_e, true), (s_bot_e, false), (s_it_e, false)],
      end_plane,
      flip(true),
    ),
  )
  faces
}