///|
pub fn default_export_options() -> ExportOptions {
  {
    format: Obj,
    include_empty_products: false,
    include_normals: true,
    include_properties: true,
    precision: 6,
  }
}

///|
pub fn ExportOptions::with_format(
  self : ExportOptions,
  format : ExportFormat,
) -> ExportOptions {
  { ..self, format, }
}

///|
pub fn ExportOptions::including_empty(
  self : ExportOptions,
  enabled : Bool,
) -> ExportOptions {
  { ..self, include_empty_products: enabled }
}

///|
pub fn ExportOptions::including_normals(
  self : ExportOptions,
  enabled : Bool,
) -> ExportOptions {
  { ..self, include_normals: enabled }
}

///|
pub fn ExportOptions::including_properties(
  self : ExportOptions,
  enabled : Bool,
) -> ExportOptions {
  { ..self, include_properties: enabled }
}

///|
pub fn ExportOptions::with_precision(
  self : ExportOptions,
  precision : Int,
) -> ExportOptions {
  { ..self, precision: if precision < 0 { 0 } else { precision } }
}

///|
fn export_format_name(format : ExportFormat) -> String {
  match format {
    Obj => "obj"
    Ply => "ply"
    JsonLines => "jsonl"
    WebBim => "json"
  }
}

///|
fn export_file_name(format : ExportFormat) -> String {
  "model." + export_format_name(format)
}

///|
fn point_line(prefix : String, point : Point3) -> String {
  prefix +
  " " +
  point.x.to_string() +
  " " +
  point.y.to_string() +
  " " +
  point.z.to_string() +
  "\n"
}

///|
fn vector_line(prefix : String, vector : Vector3) -> String {
  prefix +
  " " +
  vector.x.to_string() +
  " " +
  vector.y.to_string() +
  " " +
  vector.z.to_string() +
  "\n"
}

///|
fn obj_face_line(triangle : (Int, Int, Int), offset : Int) -> String {
  let (a, b, c) = triangle
  "f " +
  (a + offset).to_string() +
  " " +
  (b + offset).to_string() +
  " " +
  (c + offset).to_string() +
  "\n"
}

///|
fn obj_product(
  index : ModelIndex,
  product : Product,
  options : ExportOptions,
  vertex_offset : Int,
  output : StringBuilder,
) -> (Int, Int, Int) raise ParseError {
  let mesh = mesh_for_product_advanced(index, product)
  if mesh.points.length() == 0 && !options.include_empty_products {
    return (0, 0, 0)
  }
  output <+ "o \{product.ifc_type}_\{product.id}\n"
  for point in mesh.points {
    let line = point_line("v", point)
    output <+ "\{line}"
  }
  if options.include_normals {
    let analysis = MeshAnalysis::from_mesh(mesh)
    for normal in analysis.normals {
      let line = vector_line("vn", normal)
      output <+ "\{line}"
    }
  }
  for triangle in mesh.triangles {
    output <+ "\{obj_face_line(triangle, vertex_offset + 1)}"
  }
  (mesh.points.length(), mesh.triangles.length(), 1)
}

///|
fn export_obj(
  index : ModelIndex,
  products : Array[Product],
  options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
  let output = StringBuilder()
  output <+ "# moonbit-ifc OBJ export\n"
  output <+ "# products=\{products.length()}\n"
  let mut vertex_offset = 0
  let mut product_count = 0
  let mut point_count = 0
  let mut triangle_count = 0
  for product in products {
    let (points, triangles, exported) = obj_product(
      index, product, options, vertex_offset, output,
    )
    vertex_offset += points
    point_count += points
    triangle_count += triangles
    product_count += exported
  }
  (output.to_string(), product_count, point_count, triangle_count)
}

///|
fn ply_header(
  point_count : Int,
  triangle_count : Int,
  include_normals : Bool,
) -> String {
  let normal_fields = if include_normals {
    "property float nx\nproperty float ny\nproperty float nz\n"
  } else {
    ""
  }
  "ply\nformat ascii 1.0\ncomment generated by moonbit-ifc\n" +
  "element vertex " +
  point_count.to_string() +
  "\n" +
  "property float x\nproperty float y\nproperty float z\n" +
  normal_fields +
  "element face " +
  triangle_count.to_string() +
  "\n" +
  "property list uchar int vertex_indices\nend_header\n"
}

///|
fn ply_product_meshes(
  index : ModelIndex,
  products : Array[Product],
  options : ExportOptions,
) -> (Array[Mesh], Int) raise ParseError {
  let meshes : Array[Mesh] = []
  let mut product_count = 0
  for product in products {
    let mesh = mesh_for_product_advanced(index, product)
    if mesh.points.length() > 0 || options.include_empty_products {
      meshes.push(mesh)
      product_count += 1
    }
  }
  (meshes, product_count)
}

///|
fn export_ply(
  index : ModelIndex,
  products : Array[Product],
  options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
  let (meshes, product_count) = ply_product_meshes(index, products, options)
  let point_count = meshes.fold(init=0, (count, mesh) => {
    count + mesh.points.length()
  })
  let triangle_count = meshes.fold(init=0, (count, mesh) => {
    count + mesh.triangles.length()
  })
  let output = StringBuilder()
  output <+
    "\{ply_header(point_count, triangle_count, options.include_normals)}"
  let all_points : Array[Point3] = []
  let all_normals : Array[Vector3] = []
  for mesh in meshes {
    all_points.append(mesh.points)
    if options.include_normals {
      all_normals.append(MeshAnalysis::from_mesh(mesh).normals)
    }
  }
  for index in 0.. String raise ParseError {
  let sets = property_sets_for_product(index, product)
  "\"properties\":[" + sets.map(property_set_to_json).join(",") + "]"
}

///|
fn export_json_lines(
  index : ModelIndex,
  products : Array[Product],
  options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
  let output = StringBuilder()
  let mut product_count = 0
  let mut point_count = 0
  let mut triangle_count = 0
  for product in products {
    let mesh = mesh_for_product_advanced(index, product)
    if mesh.points.length() == 0 && !options.include_empty_products {
      continue
    }
    let mesh_json = mesh_to_json_stats(MeshAnalysis::from_mesh(mesh))
    let properties = if options.include_properties {
      properties_for_product_json(index, product)
    } else {
      "\"properties\":[]"
    }
    let row = "{\"product\":" +
      product_to_json(product) +
      ",\"mesh\":" +
      mesh_json +
      "," +
      properties +
      "}\n"
    output <+ "\{row}"
    product_count += 1
    point_count += mesh.points.length()
    triangle_count += mesh.triangles.length()
  }
  (output.to_string(), product_count, point_count, triangle_count)
}

///|
fn export_webbim(
  model : Model,
  options : ExportOptions,
) -> (String, Int, Int, Int) {
  let scene = build_scene(model)
  let json = scene.to_json()
  let content = "{\"format\":\"webbim\",\"precision\":" +
    options.precision.to_string() +
    ",\"scene\":" +
    json +
    "}"
  (
    content,
    scene.items.length(),
    scene.items.fold(init=0, (count, item) => {
      count +
      item.geometries.fold(init=0, (inner, geometry) => {
        inner + geometry.points.length()
      })
    }),
    0,
  )
}

///|
pub fn export_model_asset(
  model : Model,
  options : ExportOptions,
) -> ExportedAsset raise ParseError {
  let index = model.index()
  let products = extract_products(index)
  let (content, product_count, point_count, triangle_count) = match
    options.format {
    Obj => export_obj(index, products, options)
    Ply => export_ply(index, products, options)
    JsonLines => export_json_lines(index, products, options)
    WebBim => export_webbim(model, options)
  }
  {
    format: options.format,
    file_name: export_file_name(options.format),
    content,
    product_count,
    point_count,
    triangle_count,
  }
}

///|
pub fn export_step_asset(
  source : String,
  options : ExportOptions,
) -> ExportedAsset raise ParseError {
  export_model_asset(parse(source), options)
}

///|
pub fn ExportedAsset::headline(self : ExportedAsset) -> String {
  self.file_name +
  ": products=" +
  self.product_count.to_string() +
  ", points=" +
  self.point_count.to_string() +
  ", triangles=" +
  self.triangle_count.to_string() +
  ", bytes=" +
  self.content.length().to_string()
}

///|
pub fn ExportedAsset::is_non_empty(self : ExportedAsset) -> Bool {
  self.content.length() > 0 && self.product_count >= 0
}

///|
pub fn ExportedAsset::line_count(self : ExportedAsset) -> Int {
  self.content.split("\n").length()
}

///|
pub fn export_supported_formats() -> Array[String] {
  ["obj", "ply", "jsonl", "webbim"]
}

///|
pub fn export_format_from_name(name : String) -> ExportFormat? {
  match name.to_lower() {
    "obj" => Some(Obj)
    "ply" => Some(Ply)
    "jsonl" | "json-lines" => Some(JsonLines)
    "json" | "webbim" => Some(WebBim)
    _ => None
  }
}

///|
pub fn export_preview(asset : ExportedAsset, maximum_lines : Int) -> String {
  if maximum_lines <= 0 {
    ""
  } else {
    asset.content.split("\n")[:maximum_lines].join("\n")
  }
}