// world/wall_material.js (Lane A) — the synthetic-scanner wall (ART_BIBLE.md §Rendering recipe).
// No real-time lights anywhere in GUTS: the wall is biome tint x detail luminance + a fresnel
// rim (the SEM edge glow) + exponential fog to the biome void. Peristalsis is vertex work.
//
// Attributes the geometry must supply (tube.js and arena.js both do):
//   position  vec3   baked ring/shell vertex
//   aInward   vec3   unit normal pointing into the playable space
//   aTangent  vec3   unit centreline tangent at this vertex (for the wave's normal tilt)
//   uv        vec2   (theta/2pi, s) — s in world units, NOT normalized: it's the wave's phase
//                    axis and the tiling axis, and it must stay continuous across chunks
//   aPhase    float  K(s) = ∫k ds, baked (see spline.js)
//   aK        float  local wavenumber, for the analytic d(pulse)/ds
//
// Colorspace: like the stub, this writes its computed color straight out with no
// <colorspace_fragment> conversion. That's a whole-game decision (it moves every color at
// once) so it stays matched to F's round-0 look until F rules on it — see LANE_A_NOTES.

import * as THREE from 'three';

export function createWallMaterial({
  biome,
  omega,
  fog = biome.fog,        // overridable: arenas size their own fog to the room (see index.js)
  waveAmp = biome.wave.amp,
  breatheAmp = biome.wave.breathe,
  detail = null,          // THREE.Texture | null — Lane D's grayscale detail map
  tile = null,            // [repeatsAroundCircumference, unitsOfSPerRepeat]
  radiusHint = 10,        // used only to pick a square-ish default tiling
  side = THREE.FrontSide,
}) {
  if (detail) {           // defensive: we consume D's texture, so we set what we depend on
    detail.wrapS = detail.wrapT = THREE.RepeatWrapping;
    detail.needsUpdate = true;
  }
  const tileAround = tile ? tile[0] : 3;
  const tileAlong = tile ? tile[1] : Math.max(4, (2 * Math.PI * radiusHint) / tileAround);

  return new THREE.ShaderMaterial({
    side,
    defines: detail ? { USE_DETAIL: '' } : {},
    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: fog },
      uWaveA: { value: waveAmp },
      uBreatheA: { value: breatheAmp },
      uOmega: { value: omega },
      uRimPow: { value: 2.2 },
      uRimGain: { value: 0.9 },
      uDetail: { value: detail },
      uTile: { value: new THREE.Vector2(tileAround, tileAlong) },
    },
    vertexShader: /* glsl */`
      attribute vec3 aInward;
      attribute vec3 aTangent;
      attribute float aPhase;
      attribute float aK;
      uniform float uTime, uWaveA, uBreatheA, uOmega;
      varying vec2 vUv; varying vec3 vN; varying vec3 vView; varying float vPulse;

      void main() {
        vUv = uv;
        float phi = aPhase - uOmega * uTime;
        float sn = max(0.0, sin(phi));
        float pulse = sn * sn * sn;                     // sharp crest, long trough: a muscle, not a sine
        float breathe = uBreatheA * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831853 * 3.0);
        float disp = uWaveA * pulse + breathe;

        // Tilt the normal with the wave. Without this the crests are silhouette-only and the
        // rim light slides over them as if the wall were flat — the effective wall radius is
        // rho(s) = radius - disp, so the inward normal leans along the tangent by d(rho)/ds.
        float dPulse_ds = 3.0 * sn * sn * cos(phi) * aK;
        vec3 nIn = normalize(aInward - aTangent * (uWaveA * dPulse_ds));

        vec4 mv = modelViewMatrix * vec4(position + aInward * disp, 1.0);
        vN = normalize(normalMatrix * nIn);
        vView = -mv.xyz;
        vPulse = pulse;
        gl_Position = projectionMatrix * mv;
      }`,
    fragmentShader: /* glsl */`
      uniform vec3 uTint, uRim, uVoid;
      uniform float uFog, uRimPow, uRimGain;
      #ifdef USE_DETAIL
        uniform sampler2D uDetail;
        uniform vec2 uTile;
      #endif
      varying vec2 vUv; varying vec3 vN; varying vec3 vView; varying float vPulse;

      void main() {
        #ifdef USE_DETAIL
          // Lane D authors grayscale; the biome tint is applied here, so one texture can serve
          // two biomes at different tints (ART_BIBLE §FLUX prompt kit).
          float detail = texture2D(uDetail, vec2(vUv.x * uTile.x, vUv.y / uTile.y)).r;
          detail = mix(0.5, 1.2, detail);
        #else
          // Assets-optional law: no texture is the *shipping* look until D lands, not an error
          // state. Ridged folds around theta + striation along s = a passable SEM stand-in.
          float folds = 0.55 + 0.45 * sin(vUv.x * 6.2831853 * 9.0 + sin(vUv.y * 0.9) * 2.0);
          float detail = folds * (0.85 + 0.15 * sin(vUv.y * 2.2));
        #endif

        vec3 base = uTint * detail * 0.8;
        float fres = pow(1.0 - abs(dot(normalize(vN), normalize(vView))), uRimPow);
        // crest sheen: the wave is a gameplay tell (ride it for boost), so it gets a little
        // help beyond what its own geometry earns from the rim term
        vec3 col = base + uRim * fres * uRimGain + uRim * vPulse * 0.10;
        float d = length(vView);
        gl_FragColor = vec4(mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55)), 1.0);
      }`,
  });
}
