// stub/world_stub.js (Lane F) — executable spec of THE WORLD CONTRACT (docs/TECH.md).
// A straight-ish pulsing esophagus tube, analytic everywhere. Lane A replaces this with
// js/world/index.js; until then every lane builds against this. Keep contract-identical.

import * as THREE from 'three';

// Sample level — kept in sync with LEVEL SCHEMA v1 by Lane C (round 1, authorised by
// ROUND1_INSTRUCTIONS §Lane C.1). Schema of record: docs/TECH.md §Level data schema v1.
// This is the schema's shop window: if you are implementing against level data, this is the
// smallest complete example of it.
//
// Two deliberate restraints, both so this stays a stub and not a level:
//  - It keeps the BARE archetype ids ('floater'/'seeker'/'turret') rather than the fiction
//    ids that real levels use ('bolus_chunk'...). Lane B's pools are keyed by archetype and
//    B's dev harness (flight/dev.js) reads STUB_LEVEL directly, so renaming here would break
//    a live lane for nothing. levels/enemies.js resolves both (passthrough entries).
//  - Turrets are authored one-per-event with a scalar `theta`. Real levels may use a
//    `theta: [a, b]` array for count > 1; that form needs one line in B's spawn loop first
//    (snippet in LANE_C_NOTES.md §→ Lane B), so it is not demonstrated here yet.
export const STUB_LEVEL = {
  schema: 1,
  id: 'STUB', name: 'Stub Esophagus', seed: 20260716,
  par: { time: 30, score: 900, samples: 0 },
  segments: [{
    biome: 'esophagus', name: 'Stub Esophagus',
    length: 400, radius: { base: 10, wobble: 0.15 }, curviness: 0.3, flow: 14,
  }],
  arenas: [],
  events: [
    { s: 60,  type: 'spawn', enemy: 'floater', count: 4, spread: 60 },
    { s: 140, type: 'spawn', enemy: 'seeker', count: 3, spread: 40 },
    { s: 200, type: 'checkpoint', name: 'Stub Checkpoint' },
    { s: 260, type: 'spawn', enemy: 'turret', count: 1, theta: 0 },
    { s: 280, type: 'spawn', enemy: 'turret', count: 1, theta: 3.1416 },
    { s: 320, type: 'pickup', kind: 'nutrient_orb', count: 3, spread: 40 },
  ],
};

const BIOME = {   // mirrors ART_BIBLE esophagus row; A's biomes.js is the real registry
  id: 'esophagus', flow: 14, coatDrain: 0.5,
  palette: { tint: 0x1e6e64, rim: 0x5affd2, void: 0x02100d },
  fog: 0.028,
};

const WAVE_A = 0.9;        // peristalsis displacement amplitude (units, inward)
const WAVE_K = 0.22;       // spatial frequency (rad/unit along s)
const WAVE_W = 3.0;        // angular speed (rad/s)
const SKIN = 0.6;          // collision safety margin

export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
  const seg = levelData.segments[0];
  const LEN = seg.length, R = seg.radius.base;
  const AX = seg.curviness * 8, AY = seg.curviness * 5;   // gentle analytic centreline
  const KX = 0.020, KY = 0.031;

  // --- centreline & frames (analytic; s ≈ arclength for these gentle amplitudes) ---
  const centre = (s) => new THREE.Vector3(AX * Math.sin(s * KX), AY * Math.sin(s * KY), -s);
  const tangent = (s) => new THREE.Vector3(AX * KX * Math.cos(s * KX), AY * KY * Math.cos(s * KY), -1).normalize();
  function sample(s) {
    const tan = tangent(s);
    const up = new THREE.Vector3(0, 1, 0);
    const nor = up.clone().addScaledVector(tan, -up.dot(tan)).normalize();  // Gram-Schmidt (stable: tube never goes vertical)
    const bin = new THREE.Vector3().crossVectors(tan, nor);
    return { pos: centre(s), tan, nor, bin, radius: radiusAt(s) };
  }
  const radiusAt = (s) => R * (1 + seg.radius.wobble * Math.sin(s * 0.05) * Math.sin(s * 0.013 + 2.1));
  const flowPulse = (s, t) => Math.pow(Math.max(0, Math.sin(WAVE_K * s - WAVE_W * t)), 3);

  // --- geometry: rings along s, uv = (theta/2pi, s), inward-facing ---
  // seam ring vertex is duplicated (RADIAL+1 columns) so uv.x runs 0..1 without wrapping
  // backwards across the last quad (visible streak otherwise).
  const RADIAL = 64, COLS = RADIAL + 1, STEP = 0.5;
  const rings = Math.floor(LEN / STEP) + 1;
  const pos = new Float32Array(rings * COLS * 3);
  const inward = new Float32Array(rings * COLS * 3);
  const uv = new Float32Array(rings * COLS * 2);
  let p = 0, q = 0;
  for (let i = 0; i < rings; i++) {
    const s = i * STEP, f = sample(s);
    for (let j = 0; j < COLS; j++) {
      const th = (j / RADIAL) * Math.PI * 2;
      const dir = f.nor.clone().multiplyScalar(Math.cos(th)).addScaledVector(f.bin, Math.sin(th));
      const v = f.pos.clone().addScaledVector(dir, f.radius);
      pos[p] = v.x; pos[p + 1] = v.y; pos[p + 2] = v.z;
      inward[p] = -dir.x; inward[p + 1] = -dir.y; inward[p + 2] = -dir.z;
      p += 3;
      uv[q++] = j / RADIAL; uv[q++] = s;
    }
  }
  const idx = [];
  for (let i = 0; i < rings - 1; i++)
    for (let j = 0; j < RADIAL; j++) {
      const a = i * COLS + j, b = i * COLS + j + 1, c = a + COLS, d = b + COLS;
      idx.push(a, c, b, b, c, d);   // wound to face inward
    }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  geo.setAttribute('aInward', new THREE.BufferAttribute(inward, 3));
  geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
  geo.setIndex(idx);

  const mat = new THREE.ShaderMaterial({
    side: THREE.FrontSide,
    uniforms: {
      uTime: { value: 0 },
      uTint: { value: new THREE.Color(BIOME.palette.tint) },
      uRim: { value: new THREE.Color(BIOME.palette.rim) },
      uVoid: { value: new THREE.Color(BIOME.palette.void) },
      uFog: { value: BIOME.fog },
    },
    vertexShader: /* glsl */`
      attribute vec3 aInward;
      varying vec2 vUv; varying vec3 vN; varying vec3 vView;
      uniform float uTime;
      void main() {
        vUv = uv;
        float pulse = pow(max(0.0, sin(${WAVE_K.toFixed(3)} * uv.y - ${WAVE_W.toFixed(2)} * uTime)), 3.0);
        float breathe = 0.15 * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831 * 3.0);
        vec3 disp = position + aInward * (${WAVE_A.toFixed(2)} * pulse + breathe);
        vec4 mv = modelViewMatrix * vec4(disp, 1.0);
        vN = normalize(normalMatrix * aInward);
        vView = -mv.xyz;
        gl_Position = projectionMatrix * mv;
      }`,
    fragmentShader: /* glsl */`
      varying vec2 vUv; varying vec3 vN; varying vec3 vView;
      uniform vec3 uTint; uniform vec3 uRim; uniform vec3 uVoid; uniform float uFog; uniform float uTime;
      void main() {
        // fake SEM detail until Lane D textures land: ridged folds along theta + s striation
        float folds = 0.55 + 0.45 * sin(vUv.x * 6.2831 * 9.0 + sin(vUv.y * 0.9) * 2.0);
        float stria = 0.85 + 0.15 * sin(vUv.y * 2.2);
        vec3 base = uTint * folds * stria * 0.8;
        float fres = pow(1.0 - abs(dot(normalize(vN), normalize(vView))), 2.2);
        vec3 col = base + uRim * fres * 0.9;
        float d = length(vView);
        col = mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55));
        gl_FragColor = vec4(col, 1.0);
      }`,
  });

  const mesh = new THREE.Mesh(geo, mat);
  mesh.frustumCulled = false;   // one chunk; A's real world streams + culls properly
  const group = new THREE.Group();
  group.name = 'world';
  group.add(mesh);

  let time = 0;
  const world = {
    level: levelData,
    length: LEN,
    group,
    sample,
    flowPulse: (s, t = time) => flowPulse(s, t),
    project(v) {
      const s = THREE.MathUtils.clamp(-v.z, 0, LEN);
      const f = sample(s);
      const rel = v.clone().sub(f.pos);
      return { s, theta: Math.atan2(rel.dot(f.bin), rel.dot(f.nor)), rho: rel.length() };
    },
    wallRho: (s, _theta) => radiusAt(s) - WAVE_A - SKIN,
    collide(v, r = 0) {
      const { s, theta, rho } = world.project(v);
      const max = world.wallRho(s, theta) - r;
      if (rho <= max) return null;
      const f = sample(s);
      const dir = f.nor.clone().multiplyScalar(Math.cos(theta)).addScaledVector(f.bin, Math.sin(theta));
      return { push: dir.multiplyScalar(max - rho), kind: 'wall', biome: BIOME.id };
    },
    biomeAt: (_s) => BIOME,
    modeAt: (_s) => 'tube',
    arenaAt: (_s) => null,
    update(dt, _playerS) { time += dt; mat.uniforms.uTime.value = time; },
    hash() {
      let h = 2166136261 >>> 0;
      for (let s = 0; s <= LEN; s += 10) {
        const v = sample(s).pos;
        for (const x of [v.x, v.y, v.z, radiusAt(s)]) {
          h ^= Math.round(x * 1000) & 0xffff; h = Math.imul(h, 16777619);
        }
      }
      return (h >>> 0).toString(16);
    },
    dispose() { geo.dispose(); mat.dispose(); },
  };
  return world;
}
