///|
// Internal helper to manage the repository and shared STEP entities.
pub struct StepContext {
  mut repo : @repository.Repository
  // Shared references
  mut length_unit_id : @step.EntityId
  mut plane_angle_unit_id : @step.EntityId
  mut solid_angle_unit_id : @step.EntityId
  mut uncertainty_id : @step.EntityId
  mut repo_ctx_id : @step.EntityId
  snapping : Double?

  // Entity index for O(1) read-back (glTF/CSG exporters).
  entities : Map[Int32, @repository.AnyEntity]

  // Deduplication caches
  directions : Map[String, @step.Ref]
  vectors : Map[String, @step.Ref]
  cartesian_points : Map[String, @step.Ref]
  vertex_points : Map[Int32, @step.Ref]
  lines : Map[String, @step.Ref]
  edge_curves : Map[String, @step.Ref]
  axis2_placements : Map[String, @step.Ref]
  oriented_edges : Map[String, @step.Ref]
  edge_loops : Map[String, @step.Ref]
  face_bounds : Map[String, @step.Ref]
  advanced_faces : Map[String, @step.Ref]
}

///|
/// near_zero_double clears floating-point residue without grid-snapping.
fn near_zero_double(v : Double) -> Double {
  if v.abs() < 1.0e-12 {
    0.0
  } else {
    v
  }
}

///|
pub fn snap_double(v : Double, snapping : Double?) -> Double {
  match snapping {
    None => near_zero_double(v)
    Some(eps) => {
      if eps <= 0.0 {
        return v
      }
      let sign = if v < 0.0 { -1.0 } else { 1.0 }
      let abs_v = v.abs()
      let scaled = (abs_v / eps + 0.5).to_int().to_double()
      sign * scaled * eps
    }
  }
}

///|
pub fn fmt_double(v : Double, snapping : Double?) -> String {
  match snapping {
    None => v.to_string()
    Some(eps) => {
      if eps <= 0.0 {
        return v.to_string()
      }
      let snapped = snap_double(v, Some(eps))
      let abs_snapped = snapped.abs()
      let mut prec = 0
      let mut temp_eps = eps
      while temp_eps < 0.999999 && prec < 10 {
        temp_eps = temp_eps * 10.0
        prec = prec + 1
      }
      let mut multiplier = 1
      for _ in 0.. 0 {
        "\{sign_str}\{whole}.\{frac_str}"
      } else {
        "\{sign_str}\{whole}.0"
      }
    }
  }
}

///|
pub fn StepContext::new(snapping? : Double) -> StepContext {
  {
    repo: @repository.new(),
    length_unit_id: { value: 0, },
    plane_angle_unit_id: { value: 0, },
    solid_angle_unit_id: { value: 0, },
    uncertainty_id: { value: 0, },
    repo_ctx_id: { value: 0, },
    snapping,
    entities: Map([]),
    directions: Map([]),
    vectors: Map([]),
    cartesian_points: Map([]),
    vertex_points: Map([]),
    lines: Map([]),
    edge_curves: Map([]),
    axis2_placements: Map([]),
    oriented_edges: Map([]),
    edge_loops: Map([]),
    face_bounds: Map([]),
    advanced_faces: Map([]),
  }
}

///|
fn StepContext::add(
  self : StepContext,
  entity : @repository.AnyEntity,
) -> @step.Ref {
  let (repo, id) = self.repo.add(entity)
  self.repo = repo
  self.entities.set(id.value, entity)
  @step.Ref::{ id, }
}

///|
pub fn StepContext::direction(
  self : StepContext,
  x : Double,
  y : Double,
  z : Double,
) -> @step.Ref {
  // Direction cosines define plane normals and line directions. Grid-snapping
  // them (to the coordinate snapping tolerance) tilts a PLANE away from its
  // snapped boundary and makes an otherwise flat face non-planar, so only
  // clear floating-point residue and deduplicate on the exact value.
  let nx = near_zero_double(x)
  let ny = near_zero_double(y)
  let nz = near_zero_double(z)
  let key = "DIR:\{@step.fmt_num(nx)},\{@step.fmt_num(ny)},\{@step.fmt_num(nz)}"
  match self.directions.get(key) {
    Some(r) => r
    None => {
      let r = self.add(
        @repository.any_entity_direction(
          @geometry.Direction::new("", [nx, ny, nz]),
        ),
      )
      self.directions.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::vector(
  self : StepContext,
  dir : @step.Ref,
  magnitude : Double,
) -> @step.Ref {
  let smag = snap_double(magnitude, self.snapping)
  let key = "VEC:\{dir.id.value},\{fmt_double(smag, self.snapping)}"
  match self.vectors.get(key) {
    Some(r) => r
    None => {
      let r = self.add(
        @repository.any_entity_vector(@geometry.Vector::new("", dir, smag)),
      )
      self.vectors.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::cartesian_point(
  self : StepContext,
  x : Double,
  y : Double,
  z : Double,
) -> @step.Ref {
  let sx = snap_double(x, self.snapping)
  let sy = snap_double(y, self.snapping)
  let sz = snap_double(z, self.snapping)
  let key = "PNT:\{fmt_double(sx, self.snapping)},\{fmt_double(sy, self.snapping)},\{fmt_double(sz, self.snapping)}"
  match self.cartesian_points.get(key) {
    Some(r) => r
    None => {
      let r = self.add(
        @repository.any_entity_cartesian_point(
          @geometry.CartesianPoint::new("", [sx, sy, sz]),
        ),
      )
      self.cartesian_points.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::axis2_placement_3d(
  self : StepContext,
  origin : @step.Ref,
  axis : @step.Ref,
  ref_direction : @step.Ref,
) -> @step.Ref {
  let key = "\{origin.id.value},\{axis.id.value},\{ref_direction.id.value}"
  match self.axis2_placements.get(key) {
    Some(r) => r
    None => {
      let a2p = @geometry.Axis2Placement3D::new("", origin, axis, ref_direction)
      let r = self.add(@repository.any_entity_axis2_placement_3d(a2p))
      self.axis2_placements.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::oriented_edge(
  self : StepContext,
  edge_element : @step.Ref,
  orientation : Bool,
) -> @step.Ref {
  let key = "\{edge_element.id.value},\{orientation}"
  match self.oriented_edges.get(key) {
    Some(r) => r
    None => {
      let oe = @topology.OrientedEdge::new(
        "", "*", "*", edge_element, orientation,
      )
      let r = self.add(@repository.any_entity_oriented_edge(oe))
      self.oriented_edges.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::edge_loop(
  self : StepContext,
  edge_list : Array[@step.Ref],
) -> @step.Ref {
  let key = edge_list.map(fn(r) { r.id.value.to_string() }).join(",")
  match self.edge_loops.get(key) {
    Some(r) => r
    None => {
      let el = @topology.EdgeLoop::new("", edge_list)
      let r = self.add(@repository.any_entity_edge_loop(el))
      self.edge_loops.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::face_bound(
  self : StepContext,
  bound : @step.Ref,
  orientation : Bool,
) -> @step.Ref {
  let key = "\{bound.id.value},\{orientation}"
  match self.face_bounds.get(key) {
    Some(r) => r
    None => {
      let fb = @topology.FaceBound::new("", bound, orientation)
      let r = self.add(@repository.any_entity_face_bound(fb))
      self.face_bounds.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::face_outer_bound(
  self : StepContext,
  bound : @step.Ref,
  orientation : Bool,
) -> @step.Ref {
  let key = "OUTER:\{bound.id.value},\{orientation}"
  match self.face_bounds.get(key) {
    Some(r) => r
    None => {
      let fob = @topology.FaceOuterBound::new("", bound, orientation)
      let r = self.add(@repository.any_entity_face_outer_bound(fob))
      self.face_bounds.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::advanced_face(
  self : StepContext,
  bounds : Array[@step.Ref],
  face_geometry : @step.Ref,
  same_sense : Bool,
) -> @step.Ref {
  let bounds_key = bounds.map(fn(r) { r.id.value.to_string() }).join(",")
  let key = "\{face_geometry.id.value},\{same_sense}:\{bounds_key}"
  match self.advanced_faces.get(key) {
    Some(r) => r
    None => {
      let af = @topology.AdvancedFace::new(
        "", bounds, face_geometry, same_sense,
      )
      let r = self.add(@repository.any_entity_advanced_face(af))
      self.advanced_faces.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::vertex_point(
  self : StepContext,
  cp_ref : @step.Ref,
) -> @step.Ref {
  match self.vertex_points.get(cp_ref.id.value) {
    Some(r) => r
    None => {
      let vp = @topology.VertexPoint::new("", cp_ref)
      let r = self.add(@repository.any_entity_vertex_point(vp))
      self.vertex_points.set(cp_ref.id.value, r)
      r
    }
  }
}

///|
pub fn StepContext::line(
  self : StepContext,
  pnt : @step.Ref,
  dir : @step.Ref,
) -> @step.Ref {
  let key = "\{pnt.id.value},\{dir.id.value}"
  match self.lines.get(key) {
    Some(r) => r
    None => {
      let line = @geometry.Line::new("", pnt, dir)
      let r = self.add(@repository.any_entity_line(line))
      self.lines.set(key, r)
      r
    }
  }
}

///|
pub fn StepContext::edge_curve(
  self : StepContext,
  start : @step.Ref,
  end : @step.Ref,
  curve : @step.Ref,
  same_sense : Bool,
) -> @step.Ref {
  let key = "\{start.id.value},\{end.id.value},\{curve.id.value},\{same_sense}"
  match self.edge_curves.get(key) {
    Some(r) => r
    None => {
      let ec = @topology.EdgeCurve::new("", start, end, curve, same_sense)
      let r = self.add(@repository.any_entity_edge_curve(ec))
      self.edge_curves.set(key, r)
      r
    }
  }
}

///|
// Initialize the standard STEP context (units, precision, etc.)
pub fn StepContext::init_standard_context(
  self : StepContext,
  name : String,
) -> Unit {
  let dim_exp = @units.DimensionalExponents::new(
    0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
  )
  // Kept as a root entity: files from this writer have always carried it, and
  // tests assert its presence. The radian plane-angle unit below needs no
  // dimensional exponents.
  let _ = self.add(@repository.any_entity_dimensional_exponents(dim_exp))

  // Radian
  let cx_radian = @context.ComplexInstance::new([
    @context.ComplexPart::NamedUnit(@units.NamedUnit::new("*")),
    @context.ComplexPart::PlaneAngleUnit(@units.PlaneAngleUnit::new()),
    @context.ComplexPart::SiUnit(@units.SiUnit::new("$", ".RADIAN.")),
  ])
  let radian_ref = self.add(@repository.any_entity_complex_instance(cx_radian))

  // The global context's plane-angle unit must be the radian: every angle this
  // writer emits (for example CONICAL_SURFACE's semi-angle) is in radians, and
  // ISO 10303-42 reads a plane_angle_measure in the context's declared unit.
  // Declaring DEGREE here made importers read 75 as 75 degrees and rebuild
  // every cone as a near cylinder, dropping cone-bounded faces.
  self.plane_angle_unit_id = radian_ref.id

  // Steradian
  let cx_steradian = @context.ComplexInstance::new([
    @context.ComplexPart::NamedUnit(@units.NamedUnit::new("*")),
    @context.ComplexPart::SiUnit(@units.SiUnit::new("$", ".STERADIAN.")),
    @context.ComplexPart::SolidAngleUnit(@units.SolidAngleUnit::new()),
  ])
  self.solid_angle_unit_id = self.add(
      @repository.any_entity_complex_instance(cx_steradian),
    ).id

  // Millimetre
  let cx_mm = @context.ComplexInstance::new([
    @context.ComplexPart::LengthUnit(@units.LengthUnit::new()),
    @context.ComplexPart::NamedUnit(@units.NamedUnit::new("*")),
    @context.ComplexPart::SiUnit(@units.SiUnit::new(".MILLI.", ".METRE.")),
  ])
  let mm_ref = self.add(@repository.any_entity_complex_instance(cx_mm))
  self.length_unit_id = mm_ref.id

  // Uncertainty
  let unc_val = match self.snapping {
    None => 2.e-5
    Some(eps) => eps * 2.0
  }
  let unc_str = fmt_double(unc_val, self.snapping)
  let unc = @units.UncertaintyMeasureWithUnit::new(
    unc_str, mm_ref, "DISTANCE_ACCURACY_VALUE", "Maximum Tolerance applied to model",
  )
  self.uncertainty_id = self.add(
      @repository.any_entity_uncertainty_measure_with_unit(unc),
    ).id

  // Global Context
  let cx_ctx = @context.ComplexInstance::new([
    @context.ComplexPart::GeomRepCtx(
      @context.GeometricRepresentationContext::new(3),
    ),
    @context.ComplexPart::GlobUncCtx(
      @context.GlobalUncertaintyAssignedContext::new([self.uncertainty_id]),
    ),
    @context.ComplexPart::GlobUnitCtx(
      @context.GlobalUnitAssignedContext::new([
        self.length_unit_id,
        self.plane_angle_unit_id,
        self.solid_angle_unit_id,
      ]),
    ),
    @context.ComplexPart::RepCtx(
      @context.RepresentationContext::new(name, "TOP_LEVEL_ASSEMBLY_PART"),
    ),
  ])
  self.repo_ctx_id = self.add(@repository.any_entity_complex_instance(cx_ctx)).id
}

///|
// Helper to create the product structure for a part.
pub fn StepContext::add_product_definition(
  self : StepContext,
  name : String,
) -> @step.Ref {
  let app_ctx = @product.ApplicationContext::new(
    "core data for automotive mechanical design processes",
  )
  let app_ctx_ref = self.add(
    @repository.any_entity_application_context(app_ctx),
  )
  let app_proto = @product.ApplicationProtocolDefinition::new(
    "international standard", "automotive_design", 2010, app_ctx_ref,
  )
  let _ = self.add(
    @repository.any_entity_application_protocol_definition(app_proto),
  )
  let prod_ctx = @product.ProductContext::new(" ", app_ctx_ref, "mechanical")
  let prod_ctx_ref = self.add(@repository.any_entity_product_context(prod_ctx))
  let product = @product.Product::new(name, name, " ", [prod_ctx_ref])
  let product_ref = self.add(@repository.any_entity_product(product))
  let cat = @product.ProductRelatedProductCategory::new("part", "", [
    product_ref,
  ])
  let _ = self.add(@repository.any_entity_product_related_product_category(cat))
  let form = @product.ProductDefinitionFormationWithSpecifiedSource::new(
    " ", " ", product_ref, ".NOT_KNOWN.",
  )
  let form_ref = self.add(
    @repository.any_entity_product_definition_formation_with_specified_source(
      form,
    ),
  )
  let def_ctx = @product.ProductDefinitionContext::new(
    "part definition", app_ctx_ref, "design",
  )
  let def_ctx_ref = self.add(
    @repository.any_entity_product_definition_context(def_ctx),
  )
  let prod_def = @product.ProductDefinition::new(" ", "", form_ref, def_ctx_ref)
  self.add(@repository.any_entity_product_definition(prod_def))
}

///|
/// StepContext::cartesian_point_2d emits a 2D parameter-space point.
pub fn StepContext::cartesian_point_2d(
  self : StepContext,
  x : Double,
  y : Double,
) -> @step.Ref {
  self.add(
    @repository.any_entity_cartesian_point(
      @geometry.CartesianPoint::new("", [x, y]),
    ),
  )
}

///|
/// StepContext::context_2d emits the 2D parametric representation context used
/// by pcurve definitional representations.
pub fn StepContext::context_2d(self : StepContext) -> @step.Ref {
  let instance = @context.ComplexInstance::new([
    @context.ComplexPart::Raw("GEOMETRIC_REPRESENTATION_CONTEXT", "(2)"),
    @context.ComplexPart::Raw("PARAMETRIC_REPRESENTATION_CONTEXT", "()"),
    @context.ComplexPart::Raw("REPRESENTATION_CONTEXT", "('2D SPACE','')"),
  ])
  self.add(@repository.any_entity_complex_instance(instance))
}

///|
/// StepContext::definitional_representation wraps pcurve 2D items.
pub fn StepContext::definitional_representation(
  self : StepContext,
  items : Array[@step.Ref],
  context_of_items : @step.Ref,
) -> @step.Ref {
  self.add(
    @repository.any_entity_definitional_representation(
      @geometry.DefinitionalRepresentation::new("", items, context_of_items),
    ),
  )
}

///|
/// StepContext::pcurve emits a PCURVE relating a 2D representation to a basis
/// surface.
pub fn StepContext::pcurve(
  self : StepContext,
  basis_surface : @step.Ref,
  reference_to_curve : @step.Ref,
) -> @step.Ref {
  self.add(
    @repository.any_entity_pcurve(
      @geometry.PCurve::new("", basis_surface, reference_to_curve),
    ),
  )
}

///|
/// StepContext::surface_curve emits a 3D curve with its pcurves.
pub fn StepContext::surface_curve(
  self : StepContext,
  curve_3d : @step.Ref,
  associated_geometry : Array[@step.Ref],
) -> @step.Ref {
  self.add(
    @repository.any_entity_surface_curve(
      @geometry.SurfaceCurve::new(
        "", curve_3d, associated_geometry, ".PCURVE_S1.",
      ),
    ),
  )
}