// 流式扫描:不构造网格,单遍算出所有统计量。
//
// 为什么值得单独写一遍:**一个三角形在内存里的开销远大于它在文件里的开销**。
//
//   文件里(二进制 STL)    3 个坐标 + 1 个法线 = 12 个 float32 = 50 字节
//   内存里(Triangle)      4 个 Vec3 = 12 个 Double      = 96 字节
//
// 也就是说解析一个 480 MB 的模型,光是"把三角形存下来"就要再吃掉 960 MB。
// 渲染库必须存下来——它们的顶点最终要上传到 GPU,所以这是结构性的,
// 不是"它们没做优化"。
//
// 但做**统计**不需要存:体积、表面积、包围盒都能一边读一边累加。
// 只有需要拓扑的两项(水密性、绕向)才麻烦,下面单独说怎么办。

///|
/// 流式扫描的结果。
///
/// 字段和 `ValidationReport` 有意保持一致,方便并排显示。
pub(all) struct StreamReport {
  /// 三角形总数
  triangle_count : Int
  /// 文件里写了多少个顶点(二进制 STL 就是三角形数 × 3)
  raw_vertex_count : Int
  /// 去重之后剩下多少个不同的顶点
  unique_vertex_count : Int
  /// 只被一个面用到的边。大于零说明有破洞
  boundary_edge_count : Int
  /// 被三个及以上面共用的边
  non_manifold_edge_count : Int
  /// 体积
  volume : Double
  /// 表面积
  surface_area : Double
  /// 轴对齐包围盒
  bbox : Aabb?
}

///|
/// 网格是不是水密的。判据和全量校验一致。
pub fn StreamReport::is_watertight(self : StreamReport) -> Bool {
  self.boundary_edge_count == 0 && self.non_manifold_edge_count == 0
}

///|
/// 流式扫描二进制 STL。
///
/// 二进制格式天生适合流式:每个三角形固定 50 字节,顺序读就行,
/// 不需要回头。ASCII 格式要逐行扫,见 `stream_stl_ascii`。
pub fn stream_stl_binary(data : Bytes) -> Result[StreamReport, 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()
  if count == 0 {
    return Err("这个二进制 STL 里一个三角形都没有")
  }

  // 累加器。这些都不需要保留中间结果,扫到哪算到哪
  let mut volume = 0.0
  let mut area = 0.0
  let mut min : Vec3? = None
  let mut max : Vec3? = None

  // 需要保留的两样东西:
  //   1. 顶点去重表 —— 只存「量化坐标 → 编号」,不存三角形本身
  //   2. 边表 —— 只存每条边的两个端点编号,最后排序统计
  let weld : Map[UInt64, Int] = Map([])
  let edges : Array[UInt64] = []
  let mut unique_vertices = 0

  for i = 0; i < count; i = i + 1 {
    let base = 84 + i * 50
    // 法线那 12 字节直接跳过 —— 统计用不上,我们自己算
    let a = read_vec3_f32(data, base + 12)
    let b = read_vec3_f32(data, base + 24)
    let c = read_vec3_f32(data, base + 36)

    // 体积:三角形与原点构成的四面体的有符号体积,累加
    volume = volume + a.dot(b.cross(c)) / 6.0
    // 表面积:两条边叉乘模长的一半
    area = area + b.sub(a).cross(c.sub(a)).length() / 2.0
    // 包围盒:逐个比大小
    for v in [a, b, c] {
      let (lo, hi) = expand_bbox(min, max, v)
      min = lo
      max = hi
    }

    // 顶点焊接。三个顶点各自查表拿编号
    let ia = weld_vertex(weld, a)
    let ib = weld_vertex(weld, b)
    let ic = weld_vertex(weld, c)
    // 查表时新分配了编号就说明是个新顶点
    let after = weld.length()
    if after > unique_vertices {
      unique_vertices = after
    }

    // 三条边压成 UInt64 存进数组,最后统一排序统计。
    //
    // 用「排序 + 扫描」而不是哈希表:哈希表每项的开销(键 + 值 + 桶)
    // 比一个 8 字节的整数大好几倍,而这个数组是紧凑的。
    // 1500 万条边只需要 120 MB,哈希表要 400 MB 以上。
    if ia != ib {
      edges.push(pack_edge(ia, ib))
    }
    if ib != ic {
      edges.push(pack_edge(ib, ic))
    }
    if ic != ia {
      edges.push(pack_edge(ic, ia))
    }
  }

  let (boundary, non_manifold) = count_edges(edges)

  Ok(StreamReport::{
    triangle_count: count,
    raw_vertex_count: count * 3,
    unique_vertex_count: unique_vertices,
    boundary_edge_count: boundary,
    non_manifold_edge_count: non_manifold,
    volume: volume.abs(),
    surface_area: area,
    bbox: match (min, max) {
      (Some(lo), Some(hi)) => Some(Aabb::{ min: lo, max: hi, })
      _ => None
    },
  })
}

///|
/// 把一个顶点查表拿编号,没有就分配一个新的。
///
/// 量化方式和全量校验那边一致(`validate.mbt` 的 `quantize`),
/// 这样两条路径对「两个点算不算同一个」的判断是一样的。
fn weld_vertex(table : Map[UInt64, Int], v : Vec3) -> Int {
  let key = pack_position(v)
  match table.get(key) {
    Some(id) => id
    None => {
      let id = table.length()
      table.set(key, id)
      id
    }
  }
}

///|
/// 逐分量取小。
fn vec_min(a : Vec3, b : Vec3) -> Vec3 {
  Vec3::{ x: a.x.min(b.x), y: a.y.min(b.y), z: a.z.min(b.z) }
}

///|
/// 逐分量取大。
fn vec_max(a : Vec3, b : Vec3) -> Vec3 {
  Vec3::{ x: a.x.max(b.x), y: a.y.max(b.y), z: a.z.max(b.z) }
}

///|
/// 把一个点并进包围盒。
///
/// 抽成函数是因为「在循环里写嵌套的结构体字面量」MoonBit 会报歧义
/// (`ambiguous_block`),写成命名函数最省事。
fn expand_bbox(min : Vec3?, max : Vec3?, v : Vec3) -> (Vec3?, Vec3?) {
  match (min, max) {
    (Some(lo), Some(hi)) => (Some(vec_min(lo, v)), Some(vec_max(hi, v)))
    _ => (Some(v), Some(v))
  }
}
///|
/// 容差,和全量校验保持一致。
const WELD_QUANTUM : Double = 1.0e-6

///|
/// 把三个量化后的坐标打包成一个 64 位的键。
///
/// 每个坐标量化成 21 位整数(约 ±100 万,按 1e-6 毫米的精度够覆盖
/// ±1 米的模型),三个拼起来 63 位,正好塞进 UInt64。
///
/// 用整数当键比字符串省得多——字符串键光是每个字符就要一个字节,
/// 加上对象头,一个键轻松超过 20 字节;这里只要 8 字节。
fn pack_position(v : Vec3) -> UInt64 {
  let x = (v.x / WELD_QUANTUM).round().to_int()
  let y = (v.y / WELD_QUANTUM).round().to_int()
  let z = (v.z / WELD_QUANTUM).round().to_int()
  // 每个坐标取低 21 位,避免负数扩展把邻位污染
  let mask = 0x1FFFFFUL
  ((x.reinterpret_as_uint().to_uint64() & mask) << 42) |
  ((y.reinterpret_as_uint().to_uint64() & mask) << 21) |
  (z.reinterpret_as_uint().to_uint64() & mask)
}

///|
/// 把一条边的两个端点编号打包成一个 64 位的整数。
///
/// 无向边:小的编号放高位,这样 (a,b) 和 (b,a) 得到同一个键。
fn pack_edge(a : Int, b : Int) -> UInt64 {
  let lo = if a < b { a } else { b }
  let hi = if a < b { b } else { a }
  (lo.to_uint64() << 32) | hi.to_uint64()
}

///|
/// 把边数组排序后扫描一遍,数出边界边和非流形边。
///
/// 排序之后同一条边必然挨在一起,扫一遍数连续相同的个数就行,
/// 不需要哈希表。
fn count_edges(edges : Array[UInt64]) -> (Int, Int) {
  if edges.length() == 0 {
    return (0, 0)
  }
  edges.sort()
  let mut boundary = 0
  let mut non_manifold = 0
  let mut run = 1
  for i = 1; i < edges.length(); i = i + 1 {
    if edges[i] == edges[i - 1] {
      run = run + 1
    } else {
      if run == 1 {
        boundary = boundary + 1
      } else if run > 2 {
        non_manifold = non_manifold + 1
      }
      run = 1
    }
  }
  // 最后一段
  if run == 1 {
    boundary = boundary + 1
  } else if run > 2 {
    non_manifold = non_manifold + 1
  }
  (boundary, non_manifold)
}

///|
/// 流式扫描 ASCII STL。
///
/// 逐行扫,同样是累加不保留。ASCII 文件本身比二进制大好几倍,
/// 流式的意义反而更大。
pub fn stream_stl_ascii(text : String) -> Result[StreamReport, String] {
  let mut volume = 0.0
  let mut area = 0.0
  let mut min : Vec3? = None
  let mut max : Vec3? = None
  let weld : Map[UInt64, Int] = Map([])
  let edges : Array[UInt64] = []
  let mut unique_vertices = 0
  let mut triangles = 0
  let mut raw_vertices = 0

  // 当前面攒到的三个顶点
  let mut va = Vec3::zero()
  let mut vb = Vec3::zero()
  let mut vc = Vec3::zero()
  let mut have = 0
  let mut line_no = 0

  for raw_line in text.split("\n") {
    line_no = line_no + 1
    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 {
      ["vertex", x, y, z] => {
        let v = match parse_vec3(x, y, z, line_no) {
          Ok(v) => v
          Err(msg) => return Err(msg)
        }
        if have == 0 {
          va = v
        } else if have == 1 {
          vb = v
        } else if have == 2 {
          vc = v
        }
        have = have + 1
        raw_vertices = raw_vertices + 1
      }
      ["endfacet", ..] => {
        if have == 3 {
          triangles = triangles + 1
          volume = volume + va.dot(vb.cross(vc)) / 6.0
          area = area + vb.sub(va).cross(vc.sub(va)).length() / 2.0
          for v in [va, vb, vc] {
            let (lo, hi) = expand_bbox(min, max, v)
            min = lo
            max = hi
          }
          let ia = weld_vertex(weld, va)
          let ib = weld_vertex(weld, vb)
          let ic = weld_vertex(weld, vc)
          if weld.length() > unique_vertices {
            unique_vertices = weld.length()
          }
          if ia != ib {
            edges.push(pack_edge(ia, ib))
          }
          if ib != ic {
            edges.push(pack_edge(ib, ic))
          }
          if ic != ia {
            edges.push(pack_edge(ic, ia))
          }
        }
        have = 0
      }
      _ => ()
    }
  }

  if triangles == 0 {
    return Err("这个 ASCII STL 里一个三角形都没有,可能不是 STL 文件")
  }
  let (boundary, non_manifold) = count_edges(edges)
  Ok(StreamReport::{
    triangle_count: triangles,
    raw_vertex_count: raw_vertices,
    unique_vertex_count: unique_vertices,
    boundary_edge_count: boundary,
    non_manifold_edge_count: non_manifold,
    volume: volume.abs(),
    surface_area: area,
    bbox: match (min, max) {
      (Some(lo), Some(hi)) => Some(Aabb::{ min: lo, max: hi, })
      _ => None
    },
  })
}

// ── 分块扫描 ────────────────────────────────────────────────────────────
//
// 上面那两个 `stream_stl_*` 仍然是「整个文件都在内存里」。要真正把文件
// 那份内存也省掉,得能**分块读**——这需要 `moonbitlang/async/fs` 的
// `File::read_at`,而那是异步的。
//
// 所以这里把解析逻辑抽成一个「喂数据块」的扫描器:它本身是纯同步的、
// 不依赖任何外部库,调用方(命令行工具)负责用异步 IO 把文件一块块读进来。
// 这样库本体仍然是零第三方依赖。

///|
/// 分块扫描二进制 STL 的状态机。
///
/// 用法:反复 `feed` 数据块,最后 `finish` 出报告。
/// 内部只保留「没凑够一个完整记录的零头」和统计需要的那两张表,
/// **不保留三角形本身**,所以内存不随模型规模增长。
pub(all) struct StlScanner {
  /// 上一次喂进来、还没凑够一个完整记录的零头
  priv tail : Array[Byte]
  /// 文件头读完了没有
  priv mut started : Bool
  /// 头部声称的三角形数
  priv mut declared : Int
  /// 已经处理了多少个三角形
  priv mut done : Int
  priv mut volume : Double
  priv mut area : Double
  priv mut min : Vec3?
  priv mut max : Vec3?
  /// 顶点去重表
  priv weld : Map[UInt64, Int]
  /// 边表,最后统一排序统计
  priv edges : Array[UInt64]
  /// 读到的总字节数,用来核对文件长度
  priv mut consumed : UInt64
}

///|
/// 二进制 STL 的文件头长度:80 字节说明 + 4 字节三角形数。
const STL_HEADER_SIZE : Int = 84

///|
/// 二进制 STL 每个三角形固定 50 字节。
const STL_RECORD_SIZE : Int = 50

///|
pub fn StlScanner::new() -> StlScanner {
  StlScanner::{
    tail: [],
    started: false,
    declared: 0,
    done: 0,
    volume: 0.0,
    area: 0.0,
    min: None,
    max: None,
    weld: Map([]),
    edges: [],
    consumed: 0UL,
  }
}

///|
/// 喂一块数据进去。
///
/// 块的大小随意,不要求对齐到 50 字节——零头会在内部攒着,
/// 和下一块拼起来。调用方按 64 KB 之类的尺寸读就行。
pub fn StlScanner::feed(self : StlScanner, chunk : Bytes) -> Unit {
  self.consumed = self.consumed + chunk.length().to_uint64()
  for i = 0; i < chunk.length(); i = i + 1 {
    self.tail.push(chunk[i])
  }
  self.drain()
}

///|
/// 把 tail 里已经凑够整记录的部分处理掉。
fn StlScanner::drain(self : StlScanner) -> Unit {
  let mut pos = 0
  // 先处理文件头
  if !self.started {
    if self.tail.length() < STL_HEADER_SIZE {
      return
    }
    // 三角形数量在偏移 80 处,小端
    let head = Bytes::from_array(self.tail[:STL_HEADER_SIZE])
    self.declared = read_u32_le(head, 80).reinterpret_as_int()
    self.started = true
    pos = STL_HEADER_SIZE
  }
  // 再逐个处理完整的记录
  while self.tail.length() - pos >= STL_RECORD_SIZE {
    let rec = Bytes::from_array(self.tail[pos:pos + STL_RECORD_SIZE])
    self.absorb(rec)
    self.done = self.done + 1
    pos = pos + STL_RECORD_SIZE
  }
  if pos > 0 {
    // 把已处理的部分丢掉,只留零头。
    // 这里不能直接 `self.tail = self.tail[pos:]`——那样每块都要重建数组,
    // 攒够一次再截。
    let rest : Array[Byte] = []
    for i = pos; i < self.tail.length(); i = i + 1 {
      rest.push(self.tail[i])
    }
    self.tail.clear()
    for b in rest {
      self.tail.push(b)
    }
  }
}

///|
/// 处理一个 50 字节的三角形记录。
fn StlScanner::absorb(self : StlScanner, rec : Bytes) -> Unit {
  // 前 12 字节是法线,跳过——统计用不上
  let a = read_vec3_f32(rec, 12)
  let b = read_vec3_f32(rec, 24)
  let c = read_vec3_f32(rec, 36)

  self.volume = self.volume + a.dot(b.cross(c)) / 6.0
  self.area = self.area + b.sub(a).cross(c.sub(a)).length() / 2.0
  for v in [a, b, c] {
    let (lo, hi) = expand_bbox(self.min, self.max, v)
    self.min = lo
    self.max = hi
  }
  let ia = weld_vertex(self.weld, a)
  let ib = weld_vertex(self.weld, b)
  let ic = weld_vertex(self.weld, c)
  if ia != ib {
    self.edges.push(pack_edge(ia, ib))
  }
  if ib != ic {
    self.edges.push(pack_edge(ib, ic))
  }
  if ic != ia {
    self.edges.push(pack_edge(ic, ia))
  }
}

///|
/// 结束扫描,出报告。
pub fn StlScanner::finish(self : StlScanner) -> Result[StreamReport, String] {
  if !self.started {
    return Err(
      "文件太短,连 84 字节的文件头都没读全(只读到 \{self.consumed} 字节)",
    )
  }
  if self.done != self.declared {
    return Err(
      "文件被截断了:头部声称有 \{self.declared} 个三角形,实际只读到 \{self.done} 个",
    )
  }
  if self.done == 0 {
    return Err("这个二进制 STL 里一个三角形都没有")
  }
  let (boundary, non_manifold) = count_edges(self.edges)
  Ok(StreamReport::{
    triangle_count: self.done,
    raw_vertex_count: self.done * 3,
    unique_vertex_count: self.weld.length(),
    boundary_edge_count: boundary,
    non_manifold_edge_count: non_manifold,
    volume: self.volume.abs(),
    surface_area: self.area,
    bbox: match (self.min, self.max) {
      (Some(lo), Some(hi)) => Some(Aabb::{ min: lo, max: hi, })
      _ => None
    },
  })
}