// STL 解析。
//
// STL 是 3D 打印的事实标准格式,有两种形态,内容完全一样,只是写法不同:
//
// ASCII:纯文本,一个三角形写成一坨 facet/vertex 关键字。
//        文件大(是二进制版的好几倍),但人眼能读。
//
// 二进制:80 字节文件头(内容随意,很多导出器塞一段说明文字)
//        + 4 字节小端三角形数量
//        + 每个三角形 50 字节:12 个 float32(法线 3 + 三个顶点 9)+ 2 字节属性
//
// 属性那 2 字节没有标准含义,多数导出器填 0,直接丢掉。

///|
/// 从 STL 数据解析出网格,自动判断是 ASCII 还是二进制。
///
/// 判断方法:先按二进制试——第 80 字节处读出的三角形数量如果能让总长度
/// 刚好对上(84 + 数量 × 50),那就是二进制。对不上再按文本解析。
///
/// 为什么不用「开头是不是 solid」判断?因为二进制 STL 的 80 字节文件头
/// 是随便写的,很多导出器就塞一句 "solid ...",靠这个判断会误判。
/// 反过来,长度校验几乎不会误判。
pub fn parse_stl(data : Bytes) -> Result[Mesh, String] {
  match detect_format(data) {
    Binary => parse_stl_binary(data)
    Ascii =>
      // 用 @utf8.decode_lossy 正经解码。两个坑:
      //
      // 1. 不能用 Bytes::to_unchecked_string——那个是把字节直接当成 MoonBit
      //    字符串的内部表示(UTF-16)来重新解释,拿它解 UTF-8 会得到一堆
      //    乱码(512 字节会变成 256 个乱字符)。
      // 2. 用 lossy 而不是 decode:文件损坏时坏字节会被换成 U+FFFD,
      //    继续往下走,最后由坐标解析报出「不是一个合法的数字」,
      //    比在这里抛一个编码错误更有指向性。
      parse_stl_ascii(@utf8.decode_lossy(data))
    ThreeMF =>
      Err("这是一个 3MF 文件(ZIP 包),不是 STL——请用 parse_mesh 或 parse_3mf")
    Ply => Err("这是一个 PLY 文件,不是 STL——请用 parse_mesh 或 parse_ply")
    Unknown =>
      Err(
        "认不出这是 ASCII 还是二进制 STL:文件 \{data.length()} 字节," +
        "既不是合法的二进制 STL(长度对不上三角形数量),开头也不像文本",
      )
  }
}

///|
/// 按内容自动判断格式并解析。STL、OBJ、3MF 都能读。
///
/// 判断靠内容不靠扩展名——从别处拷来的文件经常带着错的扩展名,
/// 而且二进制 STL 根本没有可靠的文本特征。
///
/// 顺序是:3MF(ZIP 魔数)→ STL 的两种形态(靠长度校验和关键字)→
/// 都不认再当 OBJ 试。
pub fn parse_mesh(data : Bytes) -> Result[Mesh, String] {
  match detect_format(data) {
    ThreeMF => parse_3mf(data)
    Ply => parse_ply(data)
    Unknown => parse_obj(@utf8.decode_lossy(data))
    _ => parse_stl(data)
  }
}

///|
/// 支持的格式。
///
/// `Unknown` 不是「认不出来」,而是「不是 STL,去试试 OBJ」——
/// 调用方看到它应该走 OBJ 那条路,见 `parse_mesh`。
pub(all) enum MeshFormat {
  Ascii
  Binary
  /// 3MF:ZIP 包 + XML,现代切片软件用的格式
  ThreeMF
  /// PLY:三维扫描仪常用的格式
  Ply
  Unknown
}

///|
/// 判断数据是哪种格式。
///
/// 顺序有讲究:**带魔数的排前面,靠长度算术的排后面**。
/// 二进制 STL 的判据是「文件长度正好等于 84 + 数量 × 50」,
/// 一个随便什么文件撞上这个式子的概率大约是 1/50——所以不能在它前面
/// 留一个「可能会被误判成二进制 STL」的岔路。3MF 和 PLY 都只看开头
/// 几个固定字节,几乎不会误判,放前面最稳。
pub fn detect_format(data : Bytes) -> MeshFormat {
  // 3MF 是个 ZIP 包,按魔数认。放在最前面判断是最省的——
  // 只看 4 个字节,而下面的二进制 STL 判断要读长度做算术
  if is_3mf(data) {
    return ThreeMF
  }
  // PLY 的第一行必须是 `ply`
  if is_ply(data) {
    return Ply
  }
  // 二进制:长度必须正好是 84 + 数量 × 50
  if data.length() >= 84 {
    let count = read_u32_le(data, 80)
    // 用 64 位算,避免 count 很大时 32 位溢出反而「碰巧对上」
    let expected = 84UL + count.to_uint64() * 50UL
    if expected == data.length().to_uint64() {
      return Binary
    }
  }
  // ASCII:开头有 solid、里面有 facet 关键字。
  //
  // 直接在字节上找,不解码成字符串——关键字都是 ASCII,按字节比就够了。
  // 而且这样不怕把开头切在多字节字符中间(中文字节被截断会让解码失败)。
  // 只看开头 512 字节就够:关键字必然在最前面,没必要为了判断把几十 MB
  // 的整个扫一遍。
  let head_len = if data.length() < 512 { data.length() } else { 512 }
  let has_solid = bytes_contain_ascii(data, head_len, "solid")
  let has_facet = bytes_contain_ascii(data, head_len, "facet")
  let has_vertex = bytes_contain_ascii(data, head_len, "vertex")
  if has_solid && (has_facet || has_vertex) {
    return Ascii
  }
  // 有的 ASCII STL 开头不写 solid,那就只看有没有 facet/vertex 关键字
  if has_facet && has_vertex {
    return Ascii
  }
  Unknown
}

///|
/// 去掉结尾的 `\r`。
///
/// Windows 的换行是 `\r\n`,按 `\n` 切完每行末尾会剩一个 `\r`。
/// 不处理的话,行尾那个数字会变成 `-1\r` 这种没法解析的东西——
/// 而 Windows 上导出的 STL 几乎全是 CRLF,真实文件一读就挂。
fn strip_cr(s : StringView) -> StringView {
  let n = s.length()
  if n > 0 && s.code_unit_at(n - 1) == 0x0D {
    s[:n - 1]
  } else {
    s
  }
}

///|
/// 在前 `limit` 个字节里找 ASCII 子串。
///
/// 整个文件扫一遍是 O(n×m),但这些关键字都很短、limit 也小,
/// 真正调用时只在前 512 字节里找,开销可以忽略。
fn bytes_contain_ascii(data : Bytes, limit : Int, needle : String) -> Bool {
  let target = @utf8.encode(needle)
  let n = target.length()
  if n == 0 {
    return true
  }
  let end = if limit < data.length() { limit } else { data.length() }
  if end < n {
    return false
  }
  for start = 0; start <= end - n; start = start + 1 {
    let mut matched = true
    for j = 0; j < n; j = j + 1 {
      if data[start + j] != target[j] {
        matched = false
        break
      }
    }
    if matched {
      return true
    }
  }
  false
}

///|
/// 解析 ASCII STL。
///
/// 逐行扫,认三种关键字:
///   `facet normal x y z`  开一个新面,记下法线
///   `vertex x y z`        往当前面里加一个顶点
///   `endfacet`            当前面结束,凑够三个顶点就收一个三角形
///
/// 其余的行(`solid`、`outer`、`loop`、`endloop`、`endsolid`)直接跳过——
/// 它们不影响几何信息。
pub fn parse_stl_ascii(text : String) -> Result[Mesh, String] {
  let triangles : Array[Triangle] = []
  let mut normal = Vec3::zero()
  let pending : Array[Vec3] = []
  let mut line_no = 0

  for raw_line in text.split("\n") {
    line_no = line_no + 1
    // 按空格切,顺便滤掉空串(行首缩进、连续空格都会产生空 token)
    let tokens : Array[StringView] = []
    for raw_token in raw_line.split(" ") {
      let token = strip_cr(raw_token)
      if token.length() > 0 {
        tokens.push(token)
      }
    }
    match tokens {
      ["facet", "normal", x, y, z] =>
        match parse_vec3(x, y, z, line_no) {
          Ok(v) => {
            normal = v
            pending.clear()
          }
          Err(msg) => return Err(msg)
        }
      ["vertex", x, y, z] =>
        match parse_vec3(x, y, z, line_no) {
          Ok(v) => pending.push(v)
          Err(msg) => return Err(msg)
        }
      ["endfacet", ..] =>
        if pending.length() == 3 {
          triangles.push({
            normal,
            a: pending[0],
            b: pending[1],
            c: pending[2],
          })
          pending.clear()
        } else {
          return Err(
            "第 \{line_no} 行:一个面结束了,但里面只有 \{pending.length()} 个顶点,应该是 3 个",
          )
        }
      _ => ()
    }
  }

  if triangles.length() == 0 {
    return Err(
      "这个 ASCII STL 里一个三角形都没有,可能不是 STL 文件",
    )
  }
  Ok(Mesh::new(triangles))
}

///|
/// 解析二进制 STL。
pub fn parse_stl_binary(data : Bytes) -> Result[Mesh, String] {
  if data.length() < 84 {
    return Err(
      "二进制 STL 至少要 84 字节(80 字节文件头 + 4 字节三角形数)," +
      "这个文件只有 \{data.length()} 字节",
    )
  }
  let count_u = read_u32_le(data, 80)
  let expected = 84UL + count_u.to_uint64() * 50UL
  if expected > data.length().to_uint64() {
    return Err(
      "二进制 STL 头部声称有 \{count_u} 个三角形(需要 \{expected} 字节)," +
      "但文件只有 \{data.length()} 字节,文件被截断了",
    )
  }
  let count = count_u.reinterpret_as_int()
  let triangles : Array[Triangle] = []
  for i = 0; i < count; i = i + 1 {
    let base = 84 + i * 50
    triangles.push({
      normal: read_vec3_f32(data, base),
      a: read_vec3_f32(data, base + 12),
      b: read_vec3_f32(data, base + 24),
      c: read_vec3_f32(data, base + 36),
    })
  }
  if triangles.length() == 0 {
    return Err("这个二进制 STL 里一个三角形都没有")
  }
  Ok(Mesh::new(triangles))
}

///|
/// 把三个坐标 token 解析成向量,失败时给出带行号的中文错误。
///
/// 逐个解析、出错就返回,而不是三个一起解再合并错误——后者要么写得很绕,
/// 要么丢失「是哪个坐标出的错」。
fn parse_vec3(
  x : StringView,
  y : StringView,
  z : StringView,
  line_no : Int,
) -> Result[Vec3, String] {
  let x = match parse_coord(x, line_no) {
    Ok(v) => v
    Err(msg) => return Err(msg)
  }
  let y = match parse_coord(y, line_no) {
    Ok(v) => v
    Err(msg) => return Err(msg)
  }
  let z = match parse_coord(z, line_no) {
    Ok(v) => v
    Err(msg) => return Err(msg)
  }
  Ok(Vec3::new(x, y, z))
}

///|
/// 解析一个坐标。解析器的错误是英文的,这里换成带行号的中文提示。
fn parse_coord(s : StringView, line_no : Int) -> Result[Double, String] {
  Ok(@string.parse_double(s)) catch {
    _ => Err("第 \{line_no} 行:『\{s}』不是一个合法的数字")
  }
}

///|
/// 从字节里读一个小端 32 位无符号整数。
fn read_u32_le(data : Bytes, offset : Int) -> UInt {
  let b0 = data[offset].to_int() & 0xFF
  let b1 = data[offset + 1].to_int() & 0xFF
  let b2 = data[offset + 2].to_int() & 0xFF
  let b3 = data[offset + 3].to_int() & 0xFF
  (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)).reinterpret_as_uint()
}

///|
/// 从字节里读一个小端 float32,转成 Double。
///
/// 用 `Float::reinterpret_from_uint` 按位重新解释,而不是做数值转换——
/// 文件里存的就是 IEEE 754 的位模式,直接搬过来就行,不需要任何换算。
fn read_f32_le(data : Bytes, offset : Int) -> Double {
  let bits = read_u32_le(data, offset)
  Float::reinterpret_from_uint(bits).to_double()
}

///|
/// 连读三个 float32 组成一个向量。
fn read_vec3_f32(data : Bytes, offset : Int) -> Vec3 {
  Vec3::new(
    read_f32_le(data, offset),
    read_f32_le(data, offset + 4),
    read_f32_le(data, offset + 8),
  )
}