// world/tube.js (Lane A) — chunked tube geometry + the streaming window.
//
// The canal is extruded as rings along the parallel-transport frames and cut into chunks of
// ~CHUNK_LEN. Chunks are built/disposed around playerS so a 3000-unit level costs the same as
// a 300-unit one. Chunk seams are aligned to segment (biome) boundaries so a chunk never spans
// two biomes: each chunk gets exactly one material, and the tint changes exactly at the
// anatomical join — which is where a sphincter is anyway (GDD §structure).

import * as THREE from 'three';

const TAU = Math.PI * 2;

export function createTube({ spline, materialFor, quality = 'high', skipSpans = [] }) {
  const Q = quality === 'low'
    ? { radial: 48, step: 0.75, ahead: 120, behind: 45 }
    : { radial: 64, step: 0.5, ahead: 180, behind: 60 };
  const CHUNK_LEN = 40;
  const BUILD_BUDGET = 2;          // chunks per update: a boost must not stall the frame

  const group = new THREE.Group();
  group.name = 'tube';

  // --- chunk plan: subdivide each segment span, never straddle a biome join --------------
  // Chunks whose midpoint falls inside an arena are dropped: the arena shell is the wall
  // there. The seam is therefore chunk-quantized (+/-CHUNK_LEN/2) — good enough for v0, and
  // round 2 replaces it with real sphincter joint geometry anyway (LANE_A_NOTES).
  const skipped = (s) => skipSpans.some(([a, b]) => s >= a && s <= b);
  const plan = [];
  for (const span of spline.spans) {
    const len = span.s1 - span.s0;
    const n = Math.max(1, Math.round(len / CHUNK_LEN));
    for (let i = 0; i < n; i++) {
      const s0 = span.s0 + (len * i) / n;
      const s1 = span.s0 + (len * (i + 1)) / n;
      if (skipped((s0 + s1) / 2)) continue;
      plan.push({ s0, s1, biomeId: span.seg.biome });
    }
  }

  const live = new Map();          // plan index -> THREE.Mesh
  const stats = { built: 0, disposed: 0, get live() { return live.size; } };

  function buildChunk(i) {
    const c = plan[i];
    const rings = Math.max(2, Math.round((c.s1 - c.s0) / Q.step) + 1);
    const cols = Q.radial + 1;     // duplicate the seam column so uv.x runs 0..1 without
                                   // wrapping backwards across the last quad (visible streak)
    const n = rings * cols;
    const position = new Float32Array(n * 3);
    const aInward = new Float32Array(n * 3);
    const aTangent = new Float32Array(n * 3);
    const uv = new Float32Array(n * 2);
    const aPhase = new Float32Array(n);
    const aK = new Float32Array(n);
    const aWaveA = new Float32Array(n);
    const aRadius = new Float32Array(n);      // the wall shader derives texel density from it

    let p = 0, q = 0, w = 0;
    for (let r = 0; r < rings; r++) {
      const s = c.s0 + ((c.s1 - c.s0) * r) / (rings - 1);
      const f = spline.frameAt(s);
      const phase = spline.phaseAt(s);
      const k = spline.kAt(s);
      const waveA = spline.waveAmpAt(s);
      for (let j = 0; j < cols; j++) {
        const th = (j / Q.radial) * TAU;
        const ct = Math.cos(th), st = Math.sin(th);
        const dx = f.nor.x * ct + f.bin.x * st;
        const dy = f.nor.y * ct + f.bin.y * st;
        const dz = f.nor.z * ct + f.bin.z * st;
        position[p] = f.pos.x + dx * f.radius;
        position[p + 1] = f.pos.y + dy * f.radius;
        position[p + 2] = f.pos.z + dz * f.radius;
        aInward[p] = -dx; aInward[p + 1] = -dy; aInward[p + 2] = -dz;
        aTangent[p] = f.tan.x; aTangent[p + 1] = f.tan.y; aTangent[p + 2] = f.tan.z;
        p += 3;
        uv[q++] = j / Q.radial; uv[q++] = s;
        aPhase[w] = phase; aK[w] = k; aWaveA[w] = waveA; aRadius[w] = f.radius; w++;
      }
    }

    const idx = new (n > 65535 ? Uint32Array : Uint16Array)((rings - 1) * Q.radial * 6);
    let t = 0;
    for (let r = 0; r < rings - 1; r++) {
      for (let j = 0; j < Q.radial; j++) {
        const a = r * cols + j, b = a + 1, cc = a + cols, d = b + cols;
        idx[t++] = a; idx[t++] = cc; idx[t++] = b;      // wound to face inward
        idx[t++] = b; idx[t++] = cc; idx[t++] = d;
      }
    }

    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(position, 3));
    geo.setAttribute('aInward', new THREE.BufferAttribute(aInward, 3));
    geo.setAttribute('aTangent', new THREE.BufferAttribute(aTangent, 3));
    geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
    geo.setAttribute('aPhase', new THREE.BufferAttribute(aPhase, 1));
    geo.setAttribute('aK', new THREE.BufferAttribute(aK, 1));
    geo.setAttribute('aWaveA', new THREE.BufferAttribute(aWaveA, 1));
    geo.setAttribute('aRadius', new THREE.BufferAttribute(aRadius, 1));
    geo.setIndex(new THREE.BufferAttribute(idx, 1));
    geo.computeBoundingSphere();
    geo.boundingSphere.radius += 2;   // the vertex shader displaces inward; keep culling honest

    const mesh = new THREE.Mesh(geo, materialFor(c.biomeId));
    mesh.name = `tube[${i}] ${c.biomeId} ${c.s0.toFixed(0)}..${c.s1.toFixed(0)}`;
    group.add(mesh);
    live.set(i, mesh);
    stats.built++;
  }

  function disposeChunk(i) {
    const mesh = live.get(i);
    if (!mesh) return;
    group.remove(mesh);
    mesh.geometry.dispose();          // material is shared per biome; index.js owns it
    live.delete(i);
    stats.disposed++;
  }

  function wanted(playerS) {
    const lo = playerS - Q.behind, hi = playerS + Q.ahead;
    const set = new Set();
    for (let i = 0; i < plan.length; i++) if (plan[i].s1 >= lo && plan[i].s0 <= hi) set.add(i);
    return set;
  }

  return {
    group,
    stats,
    chunkCount: plan.length,
    quality: Q,

    /** Synchronous full fill of the window — used once at load, inside the <2s budget. */
    prime(playerS = 0) {
      for (const i of wanted(playerS)) if (!live.has(i)) buildChunk(i);
    },

    update(playerS) {
      const want = wanted(playerS);
      for (const i of live.keys()) if (!want.has(i)) disposeChunk(i);
      let budget = BUILD_BUDGET;
      for (const i of want) {
        if (live.has(i)) continue;
        buildChunk(i);
        if (--budget <= 0) break;
      }
    },

    dispose() {
      for (const i of [...live.keys()]) disposeChunk(i);
    },
  };
}
