/**
 * In-viewer float64 recenter for OBJ files with huge absolute coordinates
 * (CAD/survey, ~1e8). Done at LOAD time, before OBJLoader quantizes to float32.
 *
 * Why here and not after parse: WebGL vertex buffers are float32; at ~1e8 the
 * float32 ulp is 16-64 units, so vertices snap to that grid, merge, and faces
 * crack. Subtracting the bbox center from the *source text* (float64) keeps
 * coords small (~±3e4) so float32 holds full detail — no preprocessed files,
 * no shader-side double emulation (RTE/two-float), which a single offset of
 * this magnitude makes unnecessary (relative precision after centering ~3e-5
 * vs float32 ~1e-7).
 *
 * Streamed in two passes (bbox, then rewrite) to keep memory bounded on the
 * 100-200MB OBJ files this targets.
 */

const V = 118; // 'v'
const SP = 32; // ' '
const RECENTER_THRESHOLD = 1e4; // models nearer the origin than this are left as-is

/** Yield LF-normalized lines from a Blob's stream without buffering the whole file. */
async function* lines(blob: Blob): AsyncGenerator<string> {
  const reader = blob.stream().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 fmt(n: number): string {
  return Number.isInteger(n) ? String(n) : String(Math.round(n * 1000) / 1000);
}

/**
 * Return an origin-centered copy of `file` as a Blob, or the original `file`
 * unchanged when it already sits near the origin (small models pay only the
 * cheap first-pass scan). The returned Blob carries the same OBJ text minus a
 * per-file integer offset on every `v` line; vn/f/usemtl/mtllib are verbatim.
 */
export async function recenterObjFile(
  file: File,
  onProgress?: (loaded: number, total: number) => void,
): Promise<Blob> {
  // Two streamed passes (bbox, then rewrite) over the file's bytes; report
  // progress across both, throttled so the DOM isn't touched per line.
  const total = file.size * 2;
  let read = 0, tick = 0;
  const report = (line: string): void => {
    read += line.length + 1;
    if (onProgress && (++tick & 0x3fff) === 0) onProgress(read, total); // every ~16k lines
  };

  let xmin = Infinity, ymin = Infinity, zmin = Infinity;
  let xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
  for await (const line of lines(file)) {
    report(line);
    if (line.charCodeAt(0) !== V || line.charCodeAt(1) !== SP) continue;
    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;
  }
  if (!Number.isFinite(xmin)) return file; // no vertices — let OBJLoader handle it

  const cx = (xmin + xmax) / 2, cy = (ymin + ymax) / 2, cz = (zmin + zmax) / 2;
  if (Math.hypot(cx, cy, cz) < RECENTER_THRESHOLD) return file;

  const ox = Math.round(cx), oy = Math.round(cy), oz = Math.round(cz);
  const enc = new TextEncoder();
  const parts: BlobPart[] = [];
  let out = '';
  for await (const line of lines(file)) {
    report(line);
    if (line.charCodeAt(0) === V && line.charCodeAt(1) === SP) {
      const p = line.split(/\s+/);
      out += `v ${fmt(+p[1] - ox)} ${fmt(+p[2] - oy)} ${fmt(+p[3] - oz)}\n`;
    } else {
      out += line + '\n';
    }
    if (out.length > (1 << 23)) { parts.push(enc.encode(out)); out = ''; } // flush ~8MB
  }
  if (out) parts.push(enc.encode(out));
  if (onProgress) onProgress(total, total);
  return new Blob(parts, { type: 'text/plain' });
}
