// 网格导出。
//
// 修完的模型得能存下来才有用,所以这里写两个格式的导出器。
//
// 二进制 STL 是默认选择:占空间小(每个三角形固定 50 字节,ASCII 要
// 两百多字节),而且所有切片软件都认。OBJ 用在需要人眼看的场合。

///|
/// 把一个 Double 写成 4 字节小端 float32,追加到缓冲区。
///
/// 用 `reinterpret_as_uint` 按位取,而不是做数值换算——IEEE 754 的位
/// 模式直接搬过去就行。注意这里会把双精度截成单精度,因为 STL 二进制
/// 格式里就是 32 位。这是格式本身的限制,不是我们丢精度。
fn push_f32(buf : Array[Byte], value : Double) -> Unit {
  let bits = Float::from_double(value)
    .reinterpret_as_uint()
    .reinterpret_as_int()
  buf.push((bits & 0xFF).to_byte())
  buf.push(((bits >> 8) & 0xFF).to_byte())
  buf.push(((bits >> 16) & 0xFF).to_byte())
  buf.push(((bits >> 24) & 0xFF).to_byte())
}

///|
/// 追加一个小端 32 位无符号整数。
fn push_u32(buf : Array[Byte], value : Int) -> Unit {
  buf.push((value & 0xFF).to_byte())
  buf.push(((value >> 8) & 0xFF).to_byte())
  buf.push(((value >> 16) & 0xFF).to_byte())
  buf.push(((value >> 24) & 0xFF).to_byte())
}

///|
/// 导出成二进制 STL。
///
/// 写之前会把法线重算一遍——文件里存的那份可能是错的,而导出的文件
/// 是要拿去打印的,不能把错误带出去。
pub fn write_stl_binary(mesh : Mesh) -> Bytes {
  let buf : Array[Byte] = []

  // 80 字节文件头。标准里说是「任意内容」,惯例是写一句来源说明。
  // 注意不能用 "solid" 开头——有些老软件靠这个判断是不是 ASCII 格式,
  // 会被骗到。
  let header = "stlkit - 由 MoonBit 生成"
  let header_bytes = @utf8.encode(header)
  let header_len = if header_bytes.length() > 80 {
    80
  } else {
    header_bytes.length()
  }
  for i = 0; i < header_len; i = i + 1 {
    buf.push(header_bytes[i])
  }
  for i = header_len; i < 80; i = i + 1 {
    buf.push(b'\x00')
  }

  push_u32(buf, mesh.triangle_count())

  for triangle in mesh.triangles {
    // 法线现算,不用文件里的
    let n = triangle.computed_normal()
    push_f32(buf, n.x)
    push_f32(buf, n.y)
    push_f32(buf, n.z)
    push_f32(buf, triangle.a.x)
    push_f32(buf, triangle.a.y)
    push_f32(buf, triangle.a.z)
    push_f32(buf, triangle.b.x)
    push_f32(buf, triangle.b.y)
    push_f32(buf, triangle.b.z)
    push_f32(buf, triangle.c.x)
    push_f32(buf, triangle.c.y)
    push_f32(buf, triangle.c.z)
    // 2 字节属性。标准里没有定义含义,惯例填 0。
    buf.push(b'\x00')
    buf.push(b'\x00')
  }

  Bytes::from_array(buf)
}

///|
/// 导出成 ASCII STL。人眼能读,但体积是二进制版的好几倍。
pub fn write_stl_ascii(mesh : Mesh, name : String) -> String {
  let parts : Array[String] = ["solid \{name}\n"]
  for triangle in mesh.triangles {
    let n = triangle.computed_normal()
    parts.push("  facet normal \{n.x} \{n.y} \{n.z}\n")
    parts.push("    outer loop\n")
    for v in triangle.vertices() {
      parts.push("      vertex \{v.x} \{v.y} \{v.z}\n")
    }
    parts.push("    endloop\n")
    parts.push("  endfacet\n")
  }
  parts.push("endsolid \{name}\n")
  parts.join("")
}

///|
/// 导出成 OBJ。
///
/// OBJ 有共享顶点表,所以这里要先按位置去重再写——不然就白费了 OBJ
/// 的格式优势,写出来比 STL 还大。
///
/// 注意:去重会让拓扑变成共享顶点的形式,这个文件重新读进来之后
/// 「有多少重复顶点」那条校验信息就跟原来不一样了。这是格式本身的
/// 差别,不是数据丢了。
pub fn write_obj(mesh : Mesh) -> String {
  let lookup : Map[String, Int] = Map([])
  // OBJ 的编号从 1 开始
  let vertex_lines : Array[String] = []
  let face_lines : Array[String] = []
  let mut next_id = 1

  let key_of = fn(v : Vec3) -> String { "\{v.x},\{v.y},\{v.z}" }

  for triangle in mesh.triangles {
    let ids : Array[Int] = []
    for v in triangle.vertices() {
      let key = key_of(v)
      let id = match lookup.get(key) {
        Some(id) => id
        None => {
          let id = next_id
          lookup.set(key, id)
          next_id = next_id + 1
          vertex_lines.push("v \{v.x} \{v.y} \{v.z}\n")
          id
        }
      }
      ids.push(id)
    }
    // 顶点的编号就是它在 vertex_lines 里的序号(从 1 开始),
    // 因为每遇到一个新顶点就立刻 push 一行。
    face_lines.push("f \{ids[0]} \{ids[1]} \{ids[2]}\n")
  }

  let parts : Array[String] = ["# 由 stlkit 生成\n"]
  for line in vertex_lines {
    parts.push(line)
  }
  for line in face_lines {
    parts.push(line)
  }
  parts.join("")
}

///|
/// 按文件名后缀选格式导出。
///
/// 这里是**唯一**一处看扩展名的地方,别处判断格式都看内容。因为导出
/// 必须由用户指定格式——同一个网格既可以写成 STL 也可以写成 OBJ,
/// 没有内容可看。
pub fn write_mesh(mesh : Mesh, path : String) -> Result[Bytes, String] {
  if ends_with_ignore_case(path, ".obj") {
    Ok(@utf8.encode(write_obj(mesh)))
  } else if ends_with_ignore_case(path, ".stl") {
    Ok(write_stl_binary(mesh))
  } else if ends_with_ignore_case(path, ".ply") {
    // 二进制 PLY。ASCII 版也有(write_ply_ascii),但那是给人看的,
    // 命令行导出走默认的二进制
    Ok(write_ply_binary(mesh))
  } else {
    Err(
      "认不出要导出成什么格式:文件名「\{path}」既不以 .stl、.obj 结尾,也不以 .ply 结尾",
    )
  }
}

///|
/// 判断字符串是否以某个后缀结尾(忽略大小写)。
///
/// 手写而不是用现成函数,是因为 `String` 的下标是按码元算的,
/// 直接比较尾部要小心;这里两个参数都是短字符串,逐字符比最省心。
fn ends_with_ignore_case(s : String, suffix : String) -> Bool {
  let n = s.length()
  let m = suffix.length()
  if m > n {
    return false
  }
  for i = 0; i < m; i = i + 1 {
    let a = lower(s.code_unit_at(n - m + i))
    let b = lower(suffix.code_unit_at(i))
    if a != b {
      return false
    }
  }
  true
}

///|
/// 把一个 UTF-16 码元转成小写(只处理 ASCII 字母,够用了)。
fn lower(c : UInt16) -> UInt16 {
  if c >= 65 && c <= 90 {
    c + 32
  } else {
    c
  }
}