// world/arena.js (Lane A) — arena shells, v0. A displaced icosphere wearing the same wall
// shader as the tube, so the mouth/stomach/boss lairs are the same material world as the
// corridors (ART_BIBLE) and Lane B's 6DOF clamp has a real `arenaAt` to clamp against.
//
// v0 scope, stated plainly: the shell + its bounds. The stomach's animated acid plane
// (GDD §3, emissive #c8ff3a, height driven by C's events) is round 2 — see LANE_A_NOTES.

import * as THREE from 'three';

/** Seeded 3D value noise. Same lattice trick as spline.js: a table, not a hash function. */
function makeNoise3(rng) {
  const N = 64, tab = new Float32Array(N * N * N);
  const r = rng('world.arena');
  for (let i = 0; i < tab.length; i++) tab[i] = r() * 2 - 1;
  const at = (x, y, z) => tab[(((x & 63) * N + (y & 63)) * N + (z & 63))];
  const fade = (t) => t * t * (3 - 2 * t);
  const lerp = (a, b, t) => a + (b - a) * t;
  function vnoise(x, y, z) {
    const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
    const xf = fade(x - xi), yf = fade(y - yi), zf = fade(z - zi);
    const c00 = lerp(at(xi, yi, zi), at(xi + 1, yi, zi), xf);
    const c10 = lerp(at(xi, yi + 1, zi), at(xi + 1, yi + 1, zi), xf);
    const c01 = lerp(at(xi, yi, zi + 1), at(xi + 1, yi, zi + 1), xf);
    const c11 = lerp(at(xi, yi + 1, zi + 1), at(xi + 1, yi + 1, zi + 1), xf);
    return lerp(lerp(c00, c10, yf), lerp(c01, c11, yf), zf);
  }
  return (x, y, z) => {
    let sum = 0, amp = 1, f = 1, n = 0;
    for (let o = 0; o < 3; o++) { sum += amp * vnoise(x * f, y * f, z * f); n += amp; amp *= 0.5; f *= 2.07; }
    return sum / n;
  };
}

/**
 * @param {object} spec   level `arenas[]` entry: { at, radius, biome }
 * @param {object} spline
 * @param {THREE.Material} material  a wall material built for this arena's biome
 * @param {function} rng
 */
export function createArena({ spec, spline, material, rng, quality = 'high', waveAmpDefault = 0.7 }) {
  // three's polyhedron `detail` splits each edge into (detail+1) segments, so face count is
  // 20*(detail+1)^2 — NOT 20*4^detail. detail:5 is 720 tris, which on a 55-unit room is a
  // 10-unit facet and the fbm displacement has nothing to displace. Solve for ~3u spacing
  // instead (icosahedron edge ~ 1.05r), so arena cost tracks arena size.
  const spacing = quality === 'low' ? 6 : 3;
  const detail = Math.max(3, Math.min(24, Math.round((1.05 * spec.radius) / spacing) - 1));
  const noise = makeNoise3(rng);
  const f = spline.frameAt(spec.at);
  const center = new THREE.Vector3(f.pos.x, f.pos.y, f.pos.z);

  const geo = new THREE.IcosahedronGeometry(spec.radius, detail);   // non-indexed
  const pos = geo.attributes.position;
  const n = pos.count;
  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);

  // The churn wave crosses the room along the canal's own axis, slowly enough to read as a
  // room breathing rather than a corridor's transit wave.
  const k = 3.08 / Math.max(4, spec.radius / 5);
  const axis = new THREE.Vector3(f.tan.x, f.tan.y, f.tan.z);
  const ref = new THREE.Vector3(f.nor.x, f.nor.y, f.nor.z);
  const bin = new THREE.Vector3(f.bin.x, f.bin.y, f.bin.z);
  const amp = spec.radius * 0.09;
  // A room churns, it doesn't transit: the shell's wave amplitude comes from the arena's own
  // biome (or C's per-arena override), never from whatever segment happens to span it.
  const waveAmp = typeof spec.wave?.amp === 'number' ? spec.wave.amp : waveAmpDefault;
  const v = new THREE.Vector3();

  for (let i = 0; i < n; i++) {
    v.fromBufferAttribute(pos, i);
    const dir = v.clone().normalize();
    const r = spec.radius + amp * noise(dir.x * 2.3 + 11, dir.y * 2.3 + 5, dir.z * 2.3 + 3);
    const p = dir.clone().multiplyScalar(r);
    position[i * 3] = p.x; position[i * 3 + 1] = p.y; position[i * 3 + 2] = p.z;
    aInward[i * 3] = -dir.x; aInward[i * 3 + 1] = -dir.y; aInward[i * 3 + 2] = -dir.z;
    aTangent[i * 3] = axis.x; aTangent[i * 3 + 1] = axis.y; aTangent[i * 3 + 2] = axis.z;
    const along = p.dot(axis);
    uv[i * 2] = (Math.atan2(p.dot(bin), p.dot(ref)) / (Math.PI * 2)) + 0.5;
    uv[i * 2 + 1] = spec.at + along;              // keep uv.y in canal-s units, like the tube
    aPhase[i] = k * (spec.at + along);
    aK[i] = k;
    aWaveA[i] = waveAmp;
  }

  // Seam repair: uv.x comes from atan2, so a triangle straddling the -X axis interpolates it
  // from ~1 back to ~0 and the wall shader's fold pattern crams a full cycle into that one
  // triangle — a zigzag scar down the room. The geometry is non-indexed, so each triangle owns
  // its three vertices and we can just push the low ones past the wrap.
  for (let t = 0; t < n; t += 3) {
    let lo = Infinity, hi = -Infinity;
    for (let j = 0; j < 3; j++) { const x = uv[(t + j) * 2]; lo = Math.min(lo, x); hi = Math.max(hi, x); }
    if (hi - lo > 0.5) for (let j = 0; j < 3; j++) if (uv[(t + j) * 2] < 0.5) uv[(t + j) * 2] += 1;
  }

  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(position, 3));
  g.setAttribute('aInward', new THREE.BufferAttribute(aInward, 3));
  g.setAttribute('aTangent', new THREE.BufferAttribute(aTangent, 3));
  g.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
  g.setAttribute('aPhase', new THREE.BufferAttribute(aPhase, 1));
  g.setAttribute('aK', new THREE.BufferAttribute(aK, 1));
  g.setAttribute('aWaveA', new THREE.BufferAttribute(aWaveA, 1));
  g.computeBoundingSphere();
  geo.dispose();                                   // the source icosphere was scaffolding

  const mesh = new THREE.Mesh(g, material);        // material built with side: BackSide
  mesh.position.copy(center);
  mesh.name = `arena ${spec.biome} @${spec.at}`;

  return {
    spec,
    mesh,
    center,
    radius: spec.radius,
    /** Conservative inner surface: shell minus displacement peak minus the shader's wave. */
    innerRadius: spec.radius - amp - waveAmp - 0.6,
    covers: (s) => Math.abs(s - spec.at) <= spec.radius,
    dispose() { g.dispose(); },
  };
}
