'use client';

/**
 * ParticleField — GPGPU particle simulation backing the marketing hero.
 *
 * Lean Three.js, no @react-three/fiber. Renders a 256×256 (or 128×128
 * on lower-end devices) float texture of positions, ping-ponged each
 * frame through a sim shader, then drawn as gl_Points with an
 * anti-aliased disc SDF and additive blending.
 *
 * Callers MUST gate this behind the capability checks in `index.tsx` —
 * this component assumes WebGL2 + float-render-target support exists
 * and will throw if they don't.
 */

import { useEffect, useRef } from 'react';
import * as THREE from 'three';

import {
  initFragment,
  renderFragment,
  renderVertex,
  simFragment,
  simVertex,
} from './shaders';

export interface ParticleFieldProps {
  /** Sqrt of particle count. 256 → 65,536 particles, 128 → 16,384. */
  textureSize: 128 | 256;
  /**
   * Global multiplier on drift + ring-push velocity. 1.0 default; 0.5
   * for reduced-motion users. Pointer position is NOT scaled — the ring
   * still tracks the cursor at full fidelity, only the ambient motion
   * and the gradient-push are damped.
   */
  motionScale?: number;
}

export function ParticleField({ textureSize, motionScale = 1 }: ParticleFieldProps) {
  const containerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    // ----- Renderer ---------------------------------------------------
    // alpha:true so the hero gradient/border behind the canvas shows
    // through where particles are sparse. premultipliedAlpha pairs with
    // the premultiplied output of the render fragment shader.
    const canvas = document.createElement('canvas');
    canvas.style.display = 'block';
    canvas.style.width = '100%';
    canvas.style.height = '100%';
    container.appendChild(canvas);

    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: false,
      alpha: true,
      premultipliedAlpha: true,
      powerPreference: 'high-performance',
    });
    renderer.setClearColor(0x000000, 0);

    // Clamp DPR — going above 2 on a 65k-particle field burns laptop GPUs
    // with negligible visual gain.
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    renderer.setPixelRatio(dpr);

    const initialRect = container.getBoundingClientRect();
    renderer.setSize(Math.max(1, initialRect.width), Math.max(1, initialRect.height), false);

    // ----- Float-texture support check --------------------------------
    // EXT_color_buffer_float is required to render INTO a float target
    // on WebGL2. Without it, ping-pong won't work — bail out and let the
    // wrapper fall back to the static gradient.
    const gl = renderer.getContext() as WebGL2RenderingContext;
    const floatExt = gl.getExtension('EXT_color_buffer_float');
    if (!floatExt) {
      // Tear down what we built and signal failure via the canvas
      // remaining empty. The wrapper checks this synchronously before
      // we even mount, so this is a belt-and-braces guard.
      canvas.remove();
      renderer.dispose();
      return;
    }

    // ----- Scenes & camera --------------------------------------------
    // Two scenes: one for the simulation pass (fullscreen quad), one
    // for the actual particle render. Both use the same OrthographicCamera
    // because everything is already in clip space.
    const simScene = new THREE.Scene();
    const renderScene = new THREE.Scene();
    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

    // ----- Ping-pong render targets ------------------------------------
    const rtParams: THREE.RenderTargetOptions = {
      minFilter: THREE.NearestFilter,
      magFilter: THREE.NearestFilter,
      format: THREE.RGBAFormat,
      type: THREE.FloatType,
      depthBuffer: false,
      stencilBuffer: false,
      generateMipmaps: false,
    };

    let rtA = new THREE.WebGLRenderTarget(textureSize, textureSize, rtParams);
    let rtB = new THREE.WebGLRenderTarget(textureSize, textureSize, rtParams);

    // ----- Init pass: seed both targets with the starting field -------
    const initMaterial = new THREE.ShaderMaterial({
      vertexShader: simVertex,
      fragmentShader: initFragment,
    });
    const fsQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), initMaterial);
    simScene.add(fsQuad);

    renderer.setRenderTarget(rtA);
    renderer.render(simScene, camera);
    renderer.setRenderTarget(rtB);
    renderer.render(simScene, camera);
    renderer.setRenderTarget(null);

    // Swap in the actual sim material on the same quad.
    const simUniforms = {
      uPrev: { value: rtA.texture },
      uTime: { value: 0 },
      uDelta: { value: 1 / 60 },
      uRingPos: { value: new THREE.Vector2(0, 0) },
      uRingRadius: { value: 0.22 },
      uRingWidth: { value: 0.05 },
      uRingActive: { value: 0 },
      uMotionScale: { value: motionScale },
    };
    const simMaterial = new THREE.ShaderMaterial({
      vertexShader: simVertex,
      fragmentShader: simFragment,
      uniforms: simUniforms,
    });
    fsQuad.material = simMaterial;
    initMaterial.dispose();

    // ----- Particle geometry: one vertex per texel --------------------
    const count = textureSize * textureSize;
    const indexUvs = new Float32Array(count * 2);
    const positionsAttr = new Float32Array(count * 3); // unused but required
    for (let y = 0; y < textureSize; y++) {
      for (let x = 0; x < textureSize; x++) {
        const i = y * textureSize + x;
        // Sample texels at their centers, not corners.
        indexUvs[i * 2 + 0] = (x + 0.5) / textureSize;
        indexUvs[i * 2 + 1] = (y + 0.5) / textureSize;
      }
    }
    const particleGeo = new THREE.BufferGeometry();
    particleGeo.setAttribute('position', new THREE.BufferAttribute(positionsAttr, 3));
    particleGeo.setAttribute('aIndexUv', new THREE.BufferAttribute(indexUvs, 2));
    // Tell Three.js never to frustum-cull this — positions live in
    // the texture, not the bounding box of the buffer geometry.
    particleGeo.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 10);

    // Brand colors — read from --color-accent (#6366f1) and
    // --color-success (#22c55e). Kept as constants here rather than
    // reading from CSS variables: those resolve to oklch in modern
    // Tailwind builds, which needs parsing. Hardcode the hex values
    // the design system already commits to.
    const colorCalm = new THREE.Color('#6366f1');
    const colorHot = new THREE.Color('#22c55e');

    const renderUniforms = {
      uPositions: { value: rtB.texture },
      uPointSize: { value: textureSize === 256 ? 1.8 : 2.4 },
      uDpr: { value: dpr },
      uColorCalm: { value: colorCalm },
      uColorHot: { value: colorHot },
      uBaseAlpha: { value: 0.42 },
    };
    const particleMat = new THREE.ShaderMaterial({
      vertexShader: renderVertex,
      fragmentShader: renderFragment,
      uniforms: renderUniforms,
      transparent: true,
      depthTest: false,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });
    const particles = new THREE.Points(particleGeo, particleMat);
    renderScene.add(particles);

    // ----- Pointer tracking ------------------------------------------
    // Raw target (last pointer event), smoothed via EMA into the
    // uniform each frame so the ring tracks fluidly even if events
    // are sparse (touch / pen / throttled mouse).
    const target = new THREE.Vector2(0, 0);
    const smoothed = new THREE.Vector2(0, 0);
    let hasPointer = false;

    const updatePointerFromClient = (clientX: number, clientY: number) => {
      const rect = container.getBoundingClientRect();
      const x = ((clientX - rect.left) / rect.width) * 2 - 1;
      // Flip Y so up is positive — matches clip space.
      const y = -(((clientY - rect.top) / rect.height) * 2 - 1);
      target.set(x, y);
      hasPointer = true;
    };

    const onPointerMove = (e: PointerEvent) => {
      updatePointerFromClient(e.clientX, e.clientY);
    };
    const onPointerLeave = () => {
      hasPointer = false;
    };

    // Listen on window so the ring tracks even when the cursor is over
    // the codeblocks/CTAs that sit above the canvas. The container is
    // pointer-events:none-friendly because we read clientX/clientY.
    window.addEventListener('pointermove', onPointerMove, { passive: true });
    container.addEventListener('pointerleave', onPointerLeave, { passive: true });

    // ----- Resize handling -------------------------------------------
    const onResize = () => {
      const rect = container.getBoundingClientRect();
      if (rect.width < 1 || rect.height < 1) return;
      renderer.setSize(rect.width, rect.height, false);
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(container);

    // ----- Animation loop --------------------------------------------
    const clock = new THREE.Clock();
    let raf = 0;
    let running = true;

    // Defer the first frame to idle to keep LCP clean — the hero text
    // is the LCP element and must paint before we start eating GPU.
    const startLoop = () => {
      const tick = () => {
        if (!running) return;
        raf = requestAnimationFrame(tick);

        const delta = Math.min(clock.getDelta(), 1 / 30); // tab-switch guard
        const t = clock.elapsedTime;

        // Smooth the pointer position (EMA, alpha=0.15).
        smoothed.x = smoothed.x * 0.85 + target.x * 0.15;
        smoothed.y = smoothed.y * 0.85 + target.y * 0.15;

        // Fade ring in/out when the pointer enters/leaves.
        const targetActive = hasPointer ? 1 : 0;
        simUniforms.uRingActive.value =
          simUniforms.uRingActive.value * 0.92 + targetActive * 0.08;

        simUniforms.uTime.value = t;
        simUniforms.uDelta.value = delta;
        simUniforms.uRingPos.value.copy(smoothed);

        // Sim pass: read rtA, write rtB.
        simUniforms.uPrev.value = rtA.texture;
        renderer.setRenderTarget(rtB);
        renderer.render(simScene, camera);
        renderer.setRenderTarget(null);

        // Render pass: draw particles sampling rtB.
        renderUniforms.uPositions.value = rtB.texture;
        renderer.render(renderScene, camera);

        // Swap.
        const tmp = rtA;
        rtA = rtB;
        rtB = tmp;
      };
      tick();
    };

    let idleHandle: number | null = null;
    const w = window as Window & {
      requestIdleCallback?: (cb: IdleRequestCallback) => number;
      cancelIdleCallback?: (h: number) => void;
    };
    if (typeof w.requestIdleCallback === 'function') {
      idleHandle = w.requestIdleCallback(() => startLoop());
    } else {
      idleHandle = window.setTimeout(startLoop, 80);
    }

    // ----- Cleanup ----------------------------------------------------
    // Three.js leaks GPU memory aggressively if we skip any of this.
    return () => {
      running = false;
      if (raf) cancelAnimationFrame(raf);
      if (idleHandle !== null) {
        if (typeof w.cancelIdleCallback === 'function') {
          w.cancelIdleCallback(idleHandle);
        } else {
          window.clearTimeout(idleHandle);
        }
      }
      window.removeEventListener('pointermove', onPointerMove);
      container.removeEventListener('pointerleave', onPointerLeave);
      ro.disconnect();

      particleGeo.dispose();
      particleMat.dispose();
      simMaterial.dispose();
      (fsQuad.geometry as THREE.BufferGeometry).dispose();
      rtA.dispose();
      rtB.dispose();
      renderer.dispose();
      if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
    };
  }, [textureSize, motionScale]);

  // The container is the surface that receives pointer events.
  // Visually transparent — the canvas it owns paints the field.
  return (
    <div
      ref={containerRef}
      aria-hidden="true"
      className="absolute inset-0 size-full"
    />
  );
}
