///|
/// Export angular derivatives explicitly; time scaling is given in report JSON.
pub fn csv(points : Array[CamPoint]) -> String {
  let lines = [
    "theta_rad,lift,ds_dtheta,d2s_dtheta2,d3s_dtheta3,pitch_x,pitch_y,cam_x,cam_y,pressure_rad,pitch_curvature,offset_factor",
  ]
  for c in points {
    lines.push(
      "\{c.theta},\{c.motion.s},\{c.motion.v},\{c.motion.a},\{c.motion.j},\{c.pitch_x},\{c.pitch_y},\{c.x},\{c.y},\{c.pressure},\{c.curvature},\{c.offset_factor}",
    )
  }
  lines.join("\n") + "\n"
}

///|
/// Font-independent SVG. Model y is up; SVG y is down.
pub fn svg(points : Array[CamPoint]) -> String {
  let mut extent = 1.0
  let path = StringBuilder()
  for c in points {
    let roller = @math.hypot(c.pitch_x - c.x, c.pitch_y - c.y)
    extent = extent
      .max(c.x.abs())
      .max(c.y.abs())
      .max(c.pitch_x.abs() + roller)
      .max(c.pitch_y.abs() + roller)
    path.write_string("\{c.x},\{-c.y} ")
  }
  let margin = extent * 1.15
  let size = margin * 2.0
  let width = extent * 0.008
  ""
}

///|
/// ASCII DXF R2000, closed LWPOLYLINE in millimetres. Coordinates are sampled.
pub fn dxf_mm(points : Array[CamPoint]) -> String {
  let n = if points.length() > 1 { points.length() - 1 } else { 0 }
  let lines = [
    "0",
    "SECTION",
    "2",
    "HEADER",
    "9",
    "$ACADVER",
    "1",
    "AC1015",
    "9",
    "$INSUNITS",
    "70",
    "4",
    "0",
    "ENDSEC",
    "0",
    "SECTION",
    "2",
    "TABLES",
    "0",
    "TABLE",
    "2",
    "LTYPE",
    "70",
    "1",
    "0",
    "LTYPE",
    "100",
    "AcDbSymbolTableRecord",
    "100",
    "AcDbLinetypeTableRecord",
    "2",
    "CONTINUOUS",
    "70",
    "0",
    "3",
    "Solid line",
    "72",
    "65",
    "73",
    "0",
    "40",
    "0.0",
    "0",
    "ENDTAB",
    "0",
    "TABLE",
    "2",
    "LAYER",
    "70",
    "1",
    "0",
    "LAYER",
    "100",
    "AcDbSymbolTableRecord",
    "100",
    "AcDbLayerTableRecord",
    "2",
    "MOONCAM",
    "70",
    "0",
    "62",
    "7",
    "6",
    "CONTINUOUS",
    "0",
    "ENDTAB",
    "0",
    "ENDSEC",
    "0",
    "SECTION",
    "2",
    "ENTITIES",
    "0",
    "LWPOLYLINE",
    "100",
    "AcDbEntity",
    "8",
    "MOONCAM",
    "100",
    "AcDbPolyline",
    "90",
    n.to_string(),
    "70",
    "1",
  ]
  for i = 0; i < n; i = i + 1 {
    lines.push("10")
    lines.push(points[i].x.to_string())
    lines.push("20")
    lines.push(points[i].y.to_string())
  }
  lines.push("0")
  lines.push("ENDSEC")
  lines.push("0")
  lines.push("EOF")
  lines.join("\n") + "\n"
}

///|
/// Checked boundary for external point arrays. Raw formatters assume valid samples.
pub fn checked_export(
  points : Array[CamPoint],
  format : String,
) -> Result[String, String] {
  if points.length() < Profile_points_min ||
    points.length() > Profile_points_max {
    return Err(
      "export requires \{Profile_points_min}..\{Profile_points_max} closed profile points",
    )
  }
  for p in points {
    for
      x in [
        p.theta,
        p.motion.s,
        p.motion.v,
        p.motion.a,
        p.motion.j,
        p.x,
        p.y,
        p.pitch_x,
        p.pitch_y,
        p.pressure,
        p.curvature,
        p.offset_factor,
      ] {
      if !finite(x) || x.abs() > Coordinate_max {
        return Err("invalid export coordinate or metric")
      }
    }
  }
  if !profile_closed(points) {
    return Err("export polyline must be closed")
  }
  match format {
    "csv" => Ok(csv(points))
    "svg" => Ok(svg(points))
    "dxf" => Ok(dxf_mm(points))
    _ => Err("unknown export format")
  }
}