/**
 * GLSL shaders for the particle-field hero.
 *
 * Shaders are exported as tagged-template strings with a leading
 * `/* glsl *\/` comment marker so future syntax highlighters or
 * static analysers can pick them up without us adding a webpack loader.
 *
 * Conventions:
 *   - All positions live in clip-space-like coordinates: x, y ∈ [-1, +1].
 *   - Position texture is RGBA32F:
 *       r = x
 *       g = y
 *       b = scale (per-particle render size jitter)
 *       a = velocity magnitude (used for color tint)
 *   - Simulation runs in a fullscreen quad pass — each fragment = one particle.
 */

const simplexNoise = /* glsl */ `
  // 2D simplex noise by Ian McEwan / Ashima Arts — public domain.
  // Used both for idle drift in the sim and for organic distortion of
  // the cursor-tracking ring.
  vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  vec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }

  float snoise(vec2 v) {
    const vec4 C = vec4(
      0.211324865405187,
      0.366025403784439,
     -0.577350269189626,
      0.024390243902439
    );
    vec2 i  = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);
    vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i);
    vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0))
                          + i.x + vec3(0.0, i1.x, 1.0));
    vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
    m = m * m;
    m = m * m;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
  }
`;

/**
 * Sim vertex shader — trivial fullscreen pass.
 * Writes through clip-space UVs so the fragment shader receives one
 * fragment per particle in the position texture.
 */
export const simVertex = /* glsl */ `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0);
  }
`;

/**
 * Sim fragment shader — the actual integrator.
 *
 * Inputs:
 *   uPrev      — previous-frame position texture (ping-pong source)
 *   uTime      — elapsed seconds
 *   uDelta     — clamped frame delta (seconds), guards against tab-switch spikes
 *   uRingPos   — mouse position in clip space, smoothed
 *   uRingRadius— current ring radius (clip-space units)
 *   uRingWidth — base ring thickness (clip-space units)
 *   uRingActive— 0..1 fade so the ring softly vanishes when the mouse leaves
 *   uMotionScale— global multiplier on drift + ring-push velocity. 1.0 is
 *                 default; the prefers-reduced-motion path passes 0.5 so
 *                 the field reads as calm without removing interaction.
 *                 Pointer *position* is not scaled — the ring still
 *                 tracks the cursor at full fidelity.
 *
 * Per-particle dynamics:
 *   1. Idle drift: rotational simplex-noise velocity field — feels like
 *      slow oceanic currents rather than random brownian jitter.
 *   2. Ring push: three overlapping smoothstep bands at slightly offset
 *      radii, with the radius input distorted by simplex noise and a
 *      polar sin/cos wave. The gradient of the resulting field is
 *      applied as an outward push, so particles get gently shoved as
 *      the ring sweeps over them.
 *   3. Containment: a very soft spring pulls particles back toward the
 *      origin if they drift past the field edge — prevents particles
 *      from escaping to infinity on long sessions.
 *   4. Damping: every frame velocity decays so the field returns to a
 *      calm steady state when the mouse is idle.
 */
export const simFragment = /* glsl */ `
  precision highp float;

  uniform sampler2D uPrev;
  uniform float uTime;
  uniform float uDelta;
  uniform vec2  uRingPos;
  uniform float uRingRadius;
  uniform float uRingWidth;
  uniform float uRingActive;
  uniform float uMotionScale;

  varying vec2 vUv;

  ${simplexNoise}

  // Organic ring field — value peaks ON the ring, falls off either side.
  // Three overlapping smoothstep bands with simplex-noise + polar-wave
  // distortion to keep the boundary breathing instead of geometric.
  float ringField(vec2 p) {
    vec2 d = p - uRingPos;
    float r = length(d);
    float ang = atan(d.y, d.x);

    // Breathing distortion of the radius itself. Time scale tuned
    // down (was 0.35 → 0.22) so the ring "breathes" rather than
    // pulsates, which used to read as visual stutter on slow drift.
    float noise = snoise(p * 4.0 + uTime * 0.22) * 0.05;
    // Polar wave — a slow rippling around the circumference.
    // Same calming pass on both phases.
    float wave  = sin(ang * 5.0 + uTime * 0.7) * 0.012
                + cos(ang * 3.0 - uTime * 0.42) * 0.010;
    float rr = r + noise + wave;

    // Three bands of different thickness at slightly offset radii.
    float w = uRingWidth;
    float b1 = smoothstep(uRingRadius - w * 0.30, uRingRadius, rr)
             * (1.0 - smoothstep(uRingRadius, uRingRadius + w * 0.30, rr));
    float b2 = smoothstep(uRingRadius - w * 0.80, uRingRadius - w * 0.15, rr)
             * (1.0 - smoothstep(uRingRadius - w * 0.15, uRingRadius + w * 0.65, rr));
    float b3 = smoothstep(uRingRadius - w * 1.40, uRingRadius - w * 0.50, rr)
             * (1.0 - smoothstep(uRingRadius - w * 0.50, uRingRadius + w * 1.20, rr));

    return (b1 * 1.0 + b2 * 0.55 + b3 * 0.30) * uRingActive;
  }

  void main() {
    vec4 prev = texture2D(uPrev, vUv);
    vec2 pos  = prev.xy;
    float scale = prev.z;
    float velPrev = prev.w;

    // --- Idle drift: rotational simplex-noise current ---
    // Time is scaled by uMotionScale so reduced-motion users get a
    // calmer field that evolves at half speed.
    // Noise-evolution speed dropped from 0.08 → 0.045 (almost halved)
    // and velocity magnitude from 0.045 → 0.028. The combination kills
    // the perceived stutter — each particle now moves slowly enough
    // that the eye tracks individual motion as smooth drift rather
    // than as jerky per-frame teleportation.
    float driftTime = uTime * uMotionScale;
    float n1 = snoise(pos * 1.6 + vec2(driftTime * 0.045, 0.0));
    float n2 = snoise(pos * 1.6 + vec2(0.0, driftTime * 0.045) + 53.7);
    vec2 driftVel = vec2(-n2, n1) * 0.028 * uMotionScale; // curl-like rotation

    // --- Ring push: gradient of the ring field, pointing outward ---
    float h = 0.003;
    float fx0 = ringField(pos - vec2(h, 0.0));
    float fx1 = ringField(pos + vec2(h, 0.0));
    float fy0 = ringField(pos - vec2(0.0, h));
    float fy1 = ringField(pos + vec2(0.0, h));
    vec2 grad = vec2(fx1 - fx0, fy1 - fy0) / (2.0 * h);
    float fieldHere = ringField(pos);
    // Push along gradient — particles get nudged away from the ring crest.
    // Magnitude is scaled by uMotionScale so reduced-motion users get a
    // softer shove while the ring position still tracks at full fidelity.
    vec2 ringVel = grad * fieldHere * 0.55 * uMotionScale;

    // --- Soft containment toward origin if particle escaped ---
    float r = length(pos);
    vec2 containVel = vec2(0.0);
    if (r > 1.05) {
      containVel = -normalize(pos) * (r - 1.05) * 0.6;
    }

    // --- Integrate ---
    vec2 vel = driftVel + ringVel + containVel;
    vec2 next = pos + vel * uDelta * 60.0; // normalise to 60fps reference

    // Velocity magnitude for color tint — EMA so flash decays gracefully.
    float velMag = length(vel);
    float velOut = mix(velPrev, velMag, 0.20);

    gl_FragColor = vec4(next, scale, velOut);
  }
`;

/**
 * Render vertex shader — one vertex per particle, sampled from the
 * position texture. The vertex's `position` attribute is unused;
 * instead `aIndexUv` carries the (u, v) coordinate of this particle
 * inside the position texture, and we read the actual position from
 * `uPositions`.
 *
 * `gl_PointSize` is scaled by per-particle `scale` (z channel) and the
 * device pixel ratio so the disc stays the same physical size on
 * retina displays.
 */
export const renderVertex = /* glsl */ `
  precision highp float;

  uniform sampler2D uPositions;
  uniform float uPointSize;
  uniform float uDpr;

  attribute vec2 aIndexUv;

  varying float vVel;
  varying float vScale;

  void main() {
    vec4 p = texture2D(uPositions, aIndexUv);
    vScale = p.z;
    vVel   = p.w;

    // Position is already clip-space xy in [-1, +1]; pin z = 0.
    gl_Position = vec4(p.xy, 0.0, 1.0);
    gl_PointSize = uPointSize * p.z * uDpr;
  }
`;

/**
 * Render fragment shader — anti-aliased disc + velocity-based tint.
 *
 * Color: most of the field stays calm indigo at low opacity; particles
 * that just got shoved by the ring (high velocity) flash toward a
 * success-green tint. Output is premultiplied so additive blending
 * gives the bloom-like glow without needing a post-process pass.
 */
export const renderFragment = /* glsl */ `
  precision highp float;

  uniform vec3 uColorCalm;     // indigo
  uniform vec3 uColorHot;      // success-green
  uniform float uBaseAlpha;

  varying float vVel;
  varying float vScale;

  void main() {
    // Disc SDF — anti-aliased round dot.
    float d = length(gl_PointCoord - 0.5);
    float a = smoothstep(0.5, 0.42, d);
    if (a <= 0.001) discard;

    // Velocity-driven mix: pin to indigo for typical drift, lerp toward
    // green only on real shoves. The 0.04..0.18 band is roughly where
    // ring pushes live; idle drift stays below 0.03.
    float t = smoothstep(0.04, 0.18, vVel);
    vec3 col = mix(uColorCalm, uColorHot, t);

    float alpha = uBaseAlpha * a * (0.6 + 0.4 * vScale);
    // Premultiplied alpha — pairs with THREE.AdditiveBlending.
    gl_FragColor = vec4(col * alpha, alpha);
  }
`;

/**
 * Init fragment — runs once into both ping-pong targets to seed the
 * starting field. Uses a tempered random distribution: uniform in the
 * disc, with a small radial bias toward the edges so the field doesn't
 * look like a bullseye on first frame.
 */
export const initFragment = /* glsl */ `
  precision highp float;

  varying vec2 vUv;

  ${simplexNoise}

  // Tiny hash for per-particle deterministic randoms.
  float hash(vec2 p) {
    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
  }

  void main() {
    float r1 = hash(vUv);
    float r2 = hash(vUv + 17.3);
    float r3 = hash(vUv + 91.7);

    // Polar-uniform disc with a soft outward bias.
    float angle = r1 * 6.28318;
    float radius = sqrt(r2) * 1.0;
    vec2 pos = vec2(cos(angle), sin(angle)) * radius;

    // Slight horizontal stretch so the field reads as a wide hero band,
    // not a perfect circle.
    pos.x *= 1.25;

    float scale = 0.55 + r3 * 0.85;

    gl_FragColor = vec4(pos, scale, 0.0);
  }
`;
