// world/acid.js (Lane A) — the acid sea's surface. GDD §3: the stomach's animated `#c8ff3a`
// level, the thing that makes the acid sea a SEA and not just a big room.
//
// PROTOTYPE, round 2. It renders and it animates; it does not yet drain your coat, because the
// height it should sit at is Lane C's to drive (`level:event` → a height, per their L3 design)
// and Lane B owns what touching it costs. Both hooks are here and unclaimed:
//   world.acid.height          read it; it's the y of the surface in world space
//   world.acid.set(y, dt)      drive it; C's event pump or DBG can call this
//   world.acid.depthAt(pos)    >0 means submerged, in units. B: this is your drain test.
//
// Why a horizontal plane and not a shell: acid is a fluid, so it is level — it does NOT follow
// the tube. That is the entire point of it as a mechanic. The canal dives and climbs through
// its own bends (the greater curvature sinks, the lesser rises), so a single flat plane at a
// fixed y makes some of the room lethal and some of it safe **for free, out of the geometry
// C already authored** — no lanes, no volumes, no authoring. Fly low and you swim; hug the
// ceiling of the sea and you don't. The mucus shallows are then just "where the wall rises
// above the surface", which is a fact about the spline, not a thing anyone has to place.
//
// Colorspace: `#include <colorspace_fragment>` at the end of main() is LAW (TECH §Shader law).

import * as THREE from 'three';

const ACID = 0xc8ff3a;        // ART_BIBLE §Colour: acid / corrosive zones

/**
 * @param {object} spec   `level.acid`: { from, to, height, biome }
 * @param {object} spline
 * @param {object} opts   { fog, void: hex } — matched to the room's material by index.js
 */
export function createAcid({ spec, spline, fog = 0.02, voidColor = 0x120801, quality = 'high' }) {
  // Size the plane to the span it covers, plus the widest wall in it, so the surface always
  // reaches the rock. Sampling the radius rather than trusting `radius.base`: wobble is real
  // and a plane that stops short of the wall shows a seam of void at the waterline.
  let halfWidth = 0;
  const step = 4;
  for (let s = spec.from; s <= spec.to; s += step) halfWidth = Math.max(halfWidth, spline.radiusAt(s) * 1.35);
  const pts = [];
  for (let s = spec.from; s <= spec.to; s += step) {
    const f = spline.frameAt(s);
    pts.push(new THREE.Vector3(f.pos.x, f.pos.y, f.pos.z));
  }
  const box = new THREE.Box3().setFromPoints(pts).expandByScalar(halfWidth);

  // A plane in XZ. Segments are for the vertex ripple, not for shape: 1 unit per segment is
  // ample for a wave whose wavelength is ~14 units, and it keeps a 300x300u sea under 2k tris.
  const w = box.max.x - box.min.x, d = box.max.z - box.min.z;
  const seg = quality === 'low' ? 24 : Math.min(96, Math.max(16, Math.round(Math.max(w, d) / 6)));
  const geo = new THREE.PlaneGeometry(w, d, seg, seg);
  geo.rotateX(-Math.PI / 2);                       // PlaneGeometry is XY; we want a floor

  const mat = new THREE.ShaderMaterial({
    transparent: true,
    side: THREE.DoubleSide,                        // you will be under it, and that must read
    depthWrite: false,                             // it's a fluid surface, not an occluder
    uniforms: {
      uTime: { value: 0 },
      uAcid: { value: new THREE.Color(ACID) },
      uVoid: { value: new THREE.Color(voidColor) },
      uFog: { value: fog },
      uOpacity: { value: 0.62 },
    },
    vertexShader: /* glsl */`
      uniform float uTime;
      varying vec2 vP; varying vec3 vView; varying float vRipple;
      void main() {
        vP = position.xz;
        // Two crossing swells + a fine chop. Cheap, and it never repeats visibly because the
        // three periods are mutually irrational-ish.
        float a = sin(position.x * 0.09 + uTime * 0.8);
        float b = sin(position.z * 0.13 - uTime * 0.6);
        float c = sin((position.x + position.z) * 0.31 + uTime * 1.7);
        vRipple = a * 0.5 + b * 0.4 + c * 0.18;
        vec3 p = position + vec3(0.0, vRipple * 0.55, 0.0);
        vec4 mv = modelViewMatrix * vec4(p, 1.0);
        vView = -mv.xyz;
        gl_Position = projectionMatrix * mv;
      }`,
    fragmentShader: /* glsl */`
      uniform vec3 uAcid, uVoid;
      uniform float uFog, uOpacity, uTime;
      varying vec2 vP; varying vec3 vView; varying float vRipple;
      void main() {
        // Caustic-ish banding: the surface is emissive (no lights in GUTS), so the "light" has
        // to be in the colour itself. Bright where the swells pinch, dark in the troughs.
        float band = 0.5 + 0.5 * sin(vRipple * 3.4 + uTime * 0.9);
        float foam = smoothstep(0.72, 1.0, abs(vRipple));      // crests catch a hot rim
        vec3 col = uAcid * (0.42 + band * 0.5) + vec3(0.85, 1.0, 0.55) * foam * 0.5;

        // Same exponential fog to the same void as the wall, or the sea would float free of
        // the room it is in — the far shore must dissolve exactly like the far wall does.
        float dist = length(vView);
        float f = 1.0 - exp(-uFog * dist * 0.55);
        col = mix(col, uVoid, f);

        // Grazing angles read as a slab of fluid; straight down you see through it. Without
        // this it looks like a sheet of paper laid in the room.
        float graze = 1.0 - abs(normalize(vView).y);
        float alpha = uOpacity * mix(0.55, 1.0, graze) * (1.0 - f * 0.85);
        gl_FragColor = vec4(col, alpha);
        #include <colorspace_fragment>
      }`,
  });

  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set((box.min.x + box.max.x) / 2, spec.height ?? 0, (box.min.z + box.max.z) / 2);
  mesh.renderOrder = 2;                            // after the walls: it's transparent
  mesh.name = `acid ${spec.from}..${spec.to}`;

  let time = 0;
  return {
    spec,
    mesh,
    get height() { return mesh.position.y; },

    /** C's event pump (or DBG) drives this. dt > 0 eases; dt = 0 snaps. */
    set(y, dt = 0) {
      mesh.position.y = dt > 0 ? THREE.MathUtils.damp(mesh.position.y, y, 1.6, dt) : y;
    },

    /** B: >0 means submerged, in units. Outside the acid's s-span it is always <= 0. */
    depthAt(pos) {
      const s = spline.project(pos).s;
      if (s < spec.from || s > spec.to) return -1;
      return mesh.position.y - pos.y;
    },

    update(dt) {
      time += dt;
      mat.uniforms.uTime.value = time;
    },

    dispose() { geo.dispose(); mat.dispose(); },
  };
}
