///|
// Opt-in CSG export of boolean intent.
//
// The flat B-Rep export (`Design::to_step_file`) is nominal: a `Union` or
// `Difference` concatenates its children's faces and never trims them. This
// module instead emits a real STEP boolean tree (`BOOLEAN_RESULT` nodes wrapped
// in a `CSG_SOLID`), which a kernel that understands CSG can evaluate.
//
// The flat export is deliberately left untouched; `to_csg_step` is a separate,
// opt-in entry point.

///|
/// csg_boolean appends one BOOLEAN_RESULT and returns its reference.
fn csg_boolean(
  ctx : StepContext,
  name : String,
  op : @geometry.BooleanOperator,
  first : @step.Ref,
  second : @step.Ref,
) -> @step.Ref {
  let result = @geometry.BooleanResult::new(name, op, first, second)
  ctx.add(@repository.any_entity_boolean_result(result))
}

///|
/// union_operands folds an operand list left-associatively with `.UNION.`.
/// A single operand is returned unwrapped.
fn union_operands(
  ctx : StepContext,
  name : String,
  operands : Array[@step.Ref],
) -> @step.Ref? {
  if operands.is_empty() {
    return None
  }
  let mut acc = operands[0]
  for i in 1.. @step.Ref? {
  match node.shape {
    Union(u) => {
      if u.nodes.length() == 0 {
        return None
      }
      let child_parent = Some(node_transform(node, parent))
      let operands : Array[@step.Ref] = []
      for child in u.nodes {
        match compile_csg_operand(ctx, child, child_parent) {
          Some(r) => operands.push(r)
          None => ()
        }
      }
      union_operands(ctx, node.name, operands)
    }
    Difference(d) => {
      let child_parent = Some(node_transform(node, parent))
      let base = match compile_csg_operand(ctx, d.base, child_parent) {
        Some(r) => r
        None =>
          abort(
            "CSG difference '\{node.name}' has an empty base solid; there is nothing to subtract from",
          )
      }
      let cutters : Array[@step.Ref] = []
      for cutter in d.cutters {
        match compile_csg_operand(ctx, cutter, child_parent) {
          Some(r) => cutters.push(r)
          None => ()
        }
      }
      match union_operands(ctx, node.name + "-cutters", cutters) {
        None => Some(base)
        Some(cutters_ref) =>
          Some(
            csg_boolean(
              ctx,
              node.name + "-bool",
              @geometry.BooleanOperator::Difference,
              base,
              cutters_ref,
            ),
          )
      }
    }
    Fillet(f) => compile_edge_feature(ctx, node, f, "fillet", parent)
    Chamfer(c) => compile_edge_feature(ctx, node, c, "chamfer", parent)
    _ => Some(compile_node_to_brep(ctx, node, parent~))
  }
}

///|
/// step_quote escapes a single quote in a STEP string by doubling it.
fn step_quote(s : String) -> String {
  s.replace(old="'", new="''")
}

///|
fn edge_json_num(v : Double) -> String {
  v.to_string()
}

///|
fn edge_json_vec3(v : Vec3) -> String {
  "[" +
  edge_json_num(v.0) +
  "," +
  edge_json_num(v.1) +
  "," +
  edge_json_num(v.2) +
  "]"
}

///|
fn edge_selector_json(selector : EdgeSelector) -> String {
  match selector {
    AllEdges => "{\"kind\":\"all\"}"
    EdgesByDirection(dir, tolerance_deg) =>
      "{\"kind\":\"direction\",\"dir\":" +
      edge_json_vec3(dir) +
      ",\"tol_deg\":" +
      edge_json_num(tolerance_deg) +
      "}"
    EdgesNearPoint(point, radius_mm) =>
      "{\"kind\":\"near_point\",\"point\":" +
      edge_json_vec3(point) +
      ",\"radius\":" +
      edge_json_num(radius_mm) +
      "}"
    EdgeIndices(indices) =>
      "{\"kind\":\"indices\",\"indices\":[" +
      indices.map(fn(i) { i.to_string() }).join(",") +
      "]}"
  }
}

///|
fn edge_feature_payload(op : String, feat : EdgeFeature) -> String {
  let angle = match feat.angle_deg {
    Some(a) => edge_json_num(a)
    None => "null"
  }
  "{\"op\":\"" +
  op +
  "\",\"size\":" +
  edge_json_num(feat.size_mm) +
  ",\"angle\":" +
  angle +
  ",\"selector\":" +
  edge_selector_json(feat.selector) +
  "}"
}

///|
/// compile_edge_feature emits a `MOONBIT_EDGE_FEATURE` node that
/// `scripts/csg_eval.py` evaluates with OCCT.
///
/// The flat STEP writer refuses these nodes with an error diagnostic; only the
/// opt-in evaluated CSG path understands them. The base must be a single solid
/// (not a boolean), because the evaluator selects edges of one loaded leaf.
fn compile_edge_feature(
  ctx : StepContext,
  node : SceneNode,
  feat : EdgeFeature,
  op : String,
  parent : Transform?,
) -> @step.Ref? {
  let child_parent = Some(node_transform(node, parent))
  let base = match feat.base.shape {
    Union(_) | Difference(_) =>
      abort(
        "edge feature '\{node.name}' must wrap a single solid; wrap the boolean result first",
      )
    _ =>
      match compile_csg_operand(ctx, feat.base, child_parent) {
        Some(r) => r
        None => abort("edge feature '\{node.name}' has an empty base solid")
      }
  }
  let payload = edge_feature_payload(op, feat)
  let args = "('" +
    step_quote(node.name) +
    "',#" +
    base.id.value.to_string() +
    ",'" +
    step_quote(payload) +
    "')"
  let raw = @step.RawEntity::new("MOONBIT_EDGE_FEATURE", args)
  Some(ctx.add(@repository.any_entity_raw_entity(raw)))
}

///|
/// Design::to_csg_step compiles the design to ISO 10303-21 text with real
/// boolean trees.
///
/// Top-level boolean nodes whose root folds to a `BOOLEAN_RESULT` are wrapped
/// in a `CSG_SOLID` and referenced by a `CSG_SHAPE_REPRESENTATION`; leaves and
/// degenerate booleans keep the ordinary flat B-Rep path. Geometry is never
/// evaluated: the consuming kernel computes the boolean.
///
/// The flat export is unchanged; use `to_step_file` for the portable nominal
/// representation.
pub fn Design::to_csg_step(self : Design) -> String {
  let ctx = StepContext::new(snapping?=self.snapping)
  ctx.init_standard_context(self.name)
  let product_defs : Array[@step.Ref] = []
  let shape_reps : Array[@step.Ref] = []
  for node in self.nodes {
    match compile_csg_operand(ctx, node, None) {
      None => ()
      Some(root_ref) => {
        let root_is_boolean = match ctx.entities.get(root_ref.id.value) {
          Some(@repository.AnyEntity::BooleanResult(_)) => true
          // Edge features are emitted as a raw `MOONBIT_EDGE_FEATURE` entity
          // that the evaluator understands; treat them as CSG roots too.
          Some(@repository.AnyEntity::RawEntity(_)) => true
          _ => false
        }
        if root_is_boolean {
          let csg = @geometry.CsgSolid::new(node.name, root_ref)
          let csg_ref = ctx.add(@repository.any_entity_csg_solid(csg))
          add_solid_node_to_ctx(
            ctx,
            node,
            csg_ref,
            product_defs,
            shape_reps,
            is_csg=true,
          )
        } else {
          add_solid_node_to_ctx(
            ctx,
            node,
            root_ref,
            product_defs,
            shape_reps,
            is_csg=false,
          )
        }
      }
    }
  }
  ctx.repo = ctx.repo.set_header(
    default_header(
      self.name + ".step",
      self.description,
      self.documentation,
      nominal=false,
    ),
  )
  ctx.repo.to_step_file()
}

///|
/// Design::write_csg_step writes the CSG STEP document to `out_path`.
pub async fn Design::write_csg_step(self : Design, out_path : String?) -> Unit {
  write_text_file(self.to_csg_step(), out_path)
}