import * as THREE from 'three';

/**
 * Streaming OBJ parser for files too large for OBJLoader.
 *
 * OBJLoader builds one giant string of the whole file; past ~512MB-1GB of text
 * that exceeds the V8 max string length and fails (empty geometry). This parses
 * the file in two streamed passes (count, then fill) so it never holds the whole
 * text, building an INDEXED BufferGeometry (positions indexed by vertex, normals
 * computed). Memory stays ~O(vertices + triangles) of typed arrays.
 *
 * Trade-offs vs OBJLoader (acceptable for huge structural CAD exports):
 *  - smooth (computed) normals, not per-face-corner — hard edges soften.
 *  - per-vertex color from the active material's Kd (one draw call, no textures).
 *  - float64 recenter baked in (huge absolute coords don't crack float32).
 */

const SP = 32; // ' '
const V = 118; // 'v'
const F = 102; // 'f'
const U = 117; // 'u' (usemtl)
const RECENTER_THRESHOLD = 1e4;

async function* streamLines(url: string): AsyncGenerator<string> {
  const resp = await fetch(url);
  if (!resp.ok || !resp.body) throw new Error('fetch failed: ' + resp.status);
  const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader();
  let buf = '';
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += value;
    let i: number;
    while ((i = buf.indexOf('\n')) >= 0) {
      let ln = buf.slice(0, i);
      if (ln.charCodeAt(ln.length - 1) === 13) ln = ln.slice(0, -1); // strip CR
      yield ln;
      buf = buf.slice(i + 1);
    }
  }
  if (buf.length) yield buf;
}

function parseMtlColors(text: string): Map<string, [number, number, number]> {
  const map = new Map<string, [number, number, number]>();
  let cur = '';
  for (const raw of text.split('\n')) {
    const line = raw.trim();
    if (line.startsWith('newmtl ')) cur = line.slice(7).trim();
    else if (cur && line.startsWith('Kd ')) {
      const p = line.split(/\s+/);
      map.set(cur, [+p[1], +p[2], +p[3]]);
    }
  }
  return map;
}

export async function loadObjStreaming(
  url: string,
  mtlText?: string,
  onProgress?: (loaded: number, total: number) => void,
): Promise<THREE.Object3D> {
  // PASS 1 — count vertices + triangles, accumulate bbox in float64.
  let nV = 0, nTri = 0;
  let xmin = Infinity, ymin = Infinity, zmin = Infinity;
  let xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
  for await (const line of streamLines(url)) {
    const c0 = line.charCodeAt(0);
    if (c0 === V && line.charCodeAt(1) === SP) {
      nV++;
      const p = line.split(/\s+/);
      const x = +p[1], y = +p[2], z = +p[3];
      if (x < xmin) xmin = x; if (x > xmax) xmax = x;
      if (y < ymin) ymin = y; if (y > ymax) ymax = y;
      if (z < zmin) zmin = z; if (z > zmax) zmax = z;
    } else if (c0 === F && line.charCodeAt(1) === SP) {
      let corners = 0;
      const p = line.split(/\s+/);
      for (let k = 1; k < p.length; k++) if (p[k]) corners++;
      if (corners >= 3) nTri += corners - 2;
    }
  }
  if (nV === 0) throw new Error('OBJ has no vertices');

  const cx = (xmin + xmax) / 2, cy = (ymin + ymax) / 2, cz = (zmin + zmax) / 2;
  const far = Math.hypot(cx, cy, cz) >= RECENTER_THRESHOLD;
  const ox = far ? Math.round(cx) : 0, oy = far ? Math.round(cy) : 0, oz = far ? Math.round(cz) : 0;

  // PASS 2 — fill typed arrays.
  const positions = new Float32Array(nV * 3);
  const index = new Uint32Array(nTri * 3);
  const colorMap = mtlText ? parseMtlColors(mtlText) : null;
  const colors = colorMap && colorMap.size ? new Float32Array(nV * 3) : null;
  let vi = 0, ii = 0;
  let cr = 0.8, cg = 0.8, cb = 0.8;
  const total = nV + nTri; let done = 0;

  for await (const line of streamLines(url)) {
    const c0 = line.charCodeAt(0), c1 = line.charCodeAt(1);
    if (c0 === V && c1 === SP) {
      const p = line.split(/\s+/);
      positions[vi * 3] = +p[1] - ox;
      positions[vi * 3 + 1] = +p[2] - oy;
      positions[vi * 3 + 2] = +p[3] - oz;
      vi++;
      if ((++done & 0x3ffff) === 0) onProgress?.(done, total);
    } else if (c0 === F && c1 === SP) {
      const p = line.split(/\s+/);
      const vs: number[] = [];
      for (let k = 1; k < p.length; k++) {
        if (!p[k]) continue;
        const slash = p[k].indexOf('/');
        let idx = parseInt(slash >= 0 ? p[k].slice(0, slash) : p[k], 10);
        idx = idx < 0 ? nV + idx : idx - 1; // negative = relative; OBJ is 1-based
        vs.push(idx);
      }
      if (colors) for (const v of vs) { colors[v * 3] = cr; colors[v * 3 + 1] = cg; colors[v * 3 + 2] = cb; }
      for (let t = 1; t + 1 < vs.length; t++) { // fan triangulate
        index[ii++] = vs[0]; index[ii++] = vs[t]; index[ii++] = vs[t + 1];
      }
      if ((++done & 0x3ffff) === 0) onProgress?.(done, total);
    } else if (c0 === U && line.startsWith('usemtl ')) {
      const c = colorMap?.get(line.slice(7).trim());
      if (c) { cr = c[0]; cg = c[1]; cb = c[2]; }
    }
  }

  const geom = new THREE.BufferGeometry();
  geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  if (colors) geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geom.setIndex(new THREE.BufferAttribute(index, 1));
  geom.computeVertexNormals();
  onProgress?.(total, total);

  const mat = new THREE.MeshStandardMaterial({
    color: colors ? 0xffffff : 0xcccccc,
    vertexColors: !!colors,
    metalness: 0.0,
    roughness: 1.0,
    side: THREE.DoubleSide,
  });
  return new THREE.Mesh(geom, mat);
}
