/* Phasor — six procedural mini-visualizers + small helpers */

const useCanvasAnim = (drawRef, deps = []) => {
  const ref = React.useRef(null);
  const stateRef = React.useRef({ visible: true, paused: false, t0: performance.now() });
  React.useEffect(() => {
    const c = ref.current; if (!c) return;
    const io = new IntersectionObserver(([e]) => { stateRef.current.visible = e.isIntersecting; }, { threshold: 0.01 });
    io.observe(c);

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
      c.width = Math.floor(c.clientWidth * dpr);
      c.height = Math.floor(c.clientHeight * dpr);
    };
    resize();
    const ro = new ResizeObserver(resize); ro.observe(c);

    let raf = 0;
    const loop = (t) => {
      raf = requestAnimationFrame(loop);
      if (!stateRef.current.visible || stateRef.current.paused) return;
      drawRef.current && drawRef.current(c, t - stateRef.current.t0);
    };
    raf = requestAnimationFrame(loop);
    return () => { cancelAnimationFrame(raf); io.disconnect(); ro.disconnect(); };
  }, deps);
  return ref;
};

// 1. PARTICLE FIELD ----------------------------------------------------
const ParticleField = ({ active = false }) => {
  const drawRef = React.useRef();
  const stateRef = React.useRef({ ps: null });
  drawRef.current = (c, t) => {
    const ctx = c.getContext('2d');
    const W = c.width, H = c.height;
    if (!stateRef.current.ps) {
      const N = 240;
      const ps = [];
      for (let i = 0; i < N; i++) {
        ps.push({ x: Math.random()*W, y: Math.random()*H, vx: 0, vy: 0, life: Math.random()*1 });
      }
      stateRef.current.ps = ps;
    }
    ctx.fillStyle = 'rgba(0,0,0,0.18)'; ctx.fillRect(0,0,W,H);
    const ps = stateRef.current.ps;
    const cx = W/2 + Math.cos(t/1200) * W*0.18;
    const cy = H/2 + Math.sin(t/900) * H*0.18;
    for (const p of ps) {
      const dx = cx - p.x, dy = cy - p.y;
      const d2 = dx*dx + dy*dy + 200;
      const f = (active ? 800 : 280) / d2;
      p.vx += dx * f * 0.001; p.vy += dy * f * 0.001;
      p.vx *= 0.96; p.vy *= 0.96;
      p.x += p.vx; p.y += p.vy;
      if (p.x < 0) p.x += W; if (p.x > W) p.x -= W;
      if (p.y < 0) p.y += H; if (p.y > H) p.y -= H;
      const sp = Math.min(1, Math.hypot(p.vx, p.vy) / 5);
      const hue = 180 + sp * 100;
      ctx.fillStyle = `hsla(${hue}, 90%, ${50 + sp*30}%, ${0.5 + sp*0.5})`;
      ctx.fillRect(p.x, p.y, 1.5, 1.5);
    }
  };
  const ref = useCanvasAnim(drawRef, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

// 2. OSCILLOSCOPE / LISSAJOUS -----------------------------------------
const Oscilloscope = ({ active = false }) => {
  const drawRef = React.useRef();
  drawRef.current = (c, t) => {
    const ctx = c.getContext('2d');
    const W = c.width, H = c.height;
    ctx.fillStyle = 'rgba(0,0,0,0.10)'; ctx.fillRect(0,0,W,H);
    const cx = W/2, cy = H/2;
    const R = Math.min(W,H) * 0.38;
    const ratio = 3 + Math.sin(t/4000) * 2;
    const phase = t / 1400;
    ctx.strokeStyle = active ? 'rgba(120,255,170,0.9)' : 'rgba(120,255,170,0.55)';
    ctx.lineWidth = active ? 1.4 : 1.0;
    ctx.beginPath();
    const N = 600;
    for (let i = 0; i < N; i++) {
      const a = (i/N) * Math.PI * 2;
      const x = cx + Math.sin(a * 3 + phase) * R;
      const y = cy + Math.sin(a * ratio + phase * 0.7) * R;
      if (i === 0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
    }
    ctx.stroke();
    // grid
    ctx.strokeStyle = 'rgba(255,255,255,0.06)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let i = 1; i < 6; i++) {
      ctx.moveTo(W*i/6, 0); ctx.lineTo(W*i/6, H);
      ctx.moveTo(0, H*i/6); ctx.lineTo(W, H*i/6);
    }
    ctx.stroke();
  };
  const ref = useCanvasAnim(drawRef, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

// 3. HEX FLOOR WIREFRAME ----------------------------------------------
const HexFloor = ({ active = false }) => {
  const drawRef = React.useRef();
  drawRef.current = (c, t) => {
    const ctx = c.getContext('2d');
    const W = c.width, H = c.height;
    ctx.fillStyle = '#000'; ctx.fillRect(0,0,W,H);
    // perspective-ish: rows of hex tiles receding
    const rows = 14;
    const cols = 18;
    const horizon = H * 0.42;
    for (let r = rows-1; r >= 0; r--) {
      const tt = r / rows;
      const y = horizon + (H - horizon) * Math.pow(tt, 1.6);
      const scale = Math.pow(tt, 1.4);
      const size = 6 + scale * 26;
      for (let i = 0; i < cols; i++) {
        const phase = t/600 + r*0.4 + i*0.18;
        const lit = (Math.sin(phase) > 0.92) ? 1 : 0;
        const x = (i - cols/2 + ((r%2) ? 0.5 : 0)) * size * 1.7 + W/2;
        if (x < -size || x > W+size) continue;
        const alpha = (active ? 0.5 : 0.32) * scale + lit * 0.5;
        ctx.strokeStyle = lit ? `rgba(255, 60, 180, ${alpha})` : `rgba(220,220,255,${0.2 * scale})`;
        ctx.lineWidth = lit ? 1.6 : 0.8;
        ctx.beginPath();
        for (let k = 0; k < 6; k++) {
          const a = k * Math.PI / 3 + Math.PI/6;
          const px = x + Math.cos(a) * size * 0.95;
          const py = y + Math.sin(a) * size * 0.55;
          if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
        }
        ctx.closePath();
        ctx.stroke();
      }
    }
    // fog gradient
    const grad = ctx.createLinearGradient(0, horizon-20, 0, H);
    grad.addColorStop(0, 'rgba(0,0,0,0.95)');
    grad.addColorStop(0.4, 'rgba(0,0,0,0)');
    ctx.fillStyle = grad; ctx.fillRect(0,0,W,H);
  };
  const ref = useCanvasAnim(drawRef, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

// 4. CHLADNI PLATE -----------------------------------------------------
const Chladni = ({ active = false }) => {
  const drawRef = React.useRef();
  const stateRef = React.useRef({ pts: null });
  drawRef.current = (c, t) => {
    const ctx = c.getContext('2d');
    const W = c.width, H = c.height;
    if (!stateRef.current.pts) {
      const N = 1400; const pts = [];
      for (let i = 0; i < N; i++) pts.push({ x: Math.random()*W, y: Math.random()*H });
      stateRef.current.pts = pts;
    }
    ctx.fillStyle = 'rgba(0,0,0,0.18)'; ctx.fillRect(0,0,W,H);
    const m = 3 + Math.sin(t/3000) * 2;
    const n = 4 + Math.cos(t/2200) * 2;
    const a = 1, b = 1;
    const pts = stateRef.current.pts;
    for (const p of pts) {
      const u = p.x / W, v = p.y / H;
      const f = a * Math.sin(Math.PI * m * u) * Math.sin(Math.PI * n * v) +
                b * Math.sin(Math.PI * n * u) * Math.sin(Math.PI * m * v);
      const amp = Math.abs(f);
      const jitter = 0.02 + amp * 0.015;
      p.x += (Math.random() - 0.5) * W * jitter;
      p.y += (Math.random() - 0.5) * H * jitter;
      if (p.x < 0) p.x = 0; if (p.x > W-1) p.x = W-1;
      if (p.y < 0) p.y = 0; if (p.y > H-1) p.y = H-1;
      ctx.fillStyle = `rgba(255,255,255,${active ? 0.85 : 0.55})`;
      ctx.fillRect(p.x, p.y, 1, 1);
    }
  };
  const ref = useCanvasAnim(drawRef, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

// 5. PLASMA SPHERE -----------------------------------------------------
const PlasmaSphere = ({ active = false }) => {
  const ref = React.useRef(null);
  const stateRef = React.useRef({ visible: true });
  React.useEffect(() => {
    const c = ref.current; if (!c) return;
    const gl = c.getContext('webgl2'); if (!gl) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 1.25);
    const resize = () => {
      c.width = Math.floor(c.clientWidth * dpr);
      c.height = Math.floor(c.clientHeight * dpr);
      gl.viewport(0,0,c.width,c.height);
    };
    resize();
    const ro = new ResizeObserver(resize); ro.observe(c);
    const io = new IntersectionObserver(([e]) => { stateRef.current.visible = e.isIntersecting; }, { threshold: 0.01 });
    io.observe(c);

    const vs = `#version 300 es
      in vec2 p; out vec2 uv;
      void main(){ uv = p*0.5+0.5; gl_Position = vec4(p,0.0,1.0); }`;
    const fs = `#version 300 es
      precision highp float; in vec2 uv; out vec4 fc;
      uniform float t; uniform float uActive; uniform vec2 res;
      // sphere raymarch with internal plasma
      float sdSphere(vec3 p, float r){ return length(p) - r; }
      vec3 plasma(vec3 p, float t) {
        float v = 0.0;
        v += sin(p.x*3.0 + t*1.2);
        v += sin(p.y*3.4 - t*1.0);
        v += sin(p.z*2.6 + t*1.4);
        v += sin(length(p.xy)*4.0 - t*1.7);
        v *= 0.25;
        return vec3(0.5 + 0.5*sin(v*3.14 + vec3(0.0, 2.0, 4.0)));
      }
      void main(){
        vec2 q = (uv - 0.5) * vec2(res.x/res.y, 1.0) * 2.0;
        vec3 ro = vec3(0.0, 0.0, -2.4);
        vec3 rd = normalize(vec3(q, 1.5));
        float r = 0.95;
        // sphere intersection
        float b = dot(ro, rd);
        float c = dot(ro, ro) - r*r;
        float h = b*b - c;
        vec3 col = vec3(0.0);
        if (h > 0.0) {
          h = sqrt(h);
          float t0 = -b - h;
          float t1 = -b + h;
          // raymarch internal
          int steps = 22;
          float dt = (t1 - t0) / float(steps);
          float acc = 0.0;
          vec3 sum = vec3(0.0);
          for (int i = 0; i < 22; i++) {
            float ti = t0 + float(i) * dt;
            vec3 p = ro + rd * ti;
            // rotate
            float ca = cos(t*0.4), sa = sin(t*0.4);
            p.xz = mat2(ca,-sa,sa,ca) * p.xz;
            vec3 plc = plasma(p * 1.6, t);
            float d = max(0.0, 1.0 - length(p)/r);
            sum += plc * d * 0.18 * (1.0 + uActive*0.4);
          }
          col = sum;
          // rim
          vec3 hitp = ro + rd * t0;
          vec3 nrm = normalize(hitp);
          float rim = pow(1.0 - max(dot(nrm, -rd), 0.0), 2.5);
          col += rim * vec3(1.0, 0.4, 0.9) * 0.6;
        }
        // background subtle
        col += vec3(0.02, 0.0, 0.04) * (1.0 - length(q)*0.4);
        fc = vec4(col, 1.0);
      }`;

    function compile(type, src){ const s=gl.createShader(type); gl.shaderSource(s,src); gl.compileShader(s); if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s)); return s; }
    const prog = gl.createProgram();
    gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
    gl.linkProgram(prog);
    gl.useProgram(prog);
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
    const ploc = gl.getAttribLocation(prog, 'p');
    gl.enableVertexAttribArray(ploc);
    gl.vertexAttribPointer(ploc, 2, gl.FLOAT, false, 0, 0);
    const uT = gl.getUniformLocation(prog, 't');
    const uA = gl.getUniformLocation(prog, 'uActive');
    const uR = gl.getUniformLocation(prog, 'res');
    let raf = 0;
    const start = performance.now();
    const loop = (now) => {
      raf = requestAnimationFrame(loop);
      if (!stateRef.current.visible) return;
      gl.useProgram(prog);
      gl.uniform1f(uT, (now - start)/1000);
      gl.uniform1f(uA, stateRef.current.active ? 1.0 : 0.0);
      gl.uniform2f(uR, c.width, c.height);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };
    raf = requestAnimationFrame(loop);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); io.disconnect(); };
  }, []);
  React.useEffect(() => { stateRef.current.active = active; }, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

// 6. MARBLED FLUID (cheap noise-warp shader) ---------------------------
const Marbled = ({ active = false }) => {
  const ref = React.useRef(null);
  const stateRef = React.useRef({ visible: true });
  React.useEffect(() => {
    const c = ref.current; if (!c) return;
    const gl = c.getContext('webgl2'); if (!gl) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 1.25);
    const resize = () => {
      c.width = Math.floor(c.clientWidth * dpr);
      c.height = Math.floor(c.clientHeight * dpr);
      gl.viewport(0,0,c.width,c.height);
    };
    resize();
    const ro = new ResizeObserver(resize); ro.observe(c);
    const io = new IntersectionObserver(([e]) => { stateRef.current.visible = e.isIntersecting; }, { threshold: 0.01 });
    io.observe(c);

    const vs = `#version 300 es
      in vec2 p; out vec2 uv;
      void main(){ uv = p*0.5+0.5; gl_Position = vec4(p,0.0,1.0); }`;
    const fs = `#version 300 es
      precision highp float; in vec2 uv; out vec4 fc;
      uniform float t; uniform float uActive;
      // hash + value noise
      float h(vec2 p){ p = fract(p*vec2(123.34, 456.21)); p += dot(p, p+45.32); return fract(p.x * p.y); }
      float vnoise(vec2 p){
        vec2 i = floor(p), f = fract(p);
        float a = h(i), b = h(i+vec2(1,0)), c2 = h(i+vec2(0,1)), d = h(i+vec2(1,1));
        vec2 u = f*f*(3.0-2.0*f);
        return mix(mix(a,b,u.x), mix(c2,d,u.x), u.y);
      }
      float fbm(vec2 p){ float s=0.0, a=0.5; for (int i=0;i<5;i++){ s+=a*vnoise(p); p*=2.04; a*=0.5; } return s; }
      void main(){
        vec2 q = uv * 3.0;
        vec2 w = vec2(fbm(q + t*0.15), fbm(q + 5.2 + t*0.13));
        vec2 r = vec2(fbm(q + 4.0*w + t*0.2), fbm(q + 8.0*w - t*0.18));
        float n = fbm(q + 4.0*r);
        // warm-cool palette: violet -> magenta -> cyan
        vec3 a = vec3(0.05, 0.0, 0.15);
        vec3 b = vec3(0.6, 0.05, 0.55);
        vec3 cc = vec3(0.0, 0.55, 0.85);
        vec3 col = mix(a, b, smoothstep(0.2, 0.7, n));
        col = mix(col, cc, smoothstep(0.55, 0.95, n) * 0.7);
        col *= 0.6 + 0.5 * length(r);
        col *= 1.0 + uActive*0.4;
        // subtle grain
        col += (h(uv*1024.0 + t)-0.5) * 0.02;
        fc = vec4(col, 1.0);
      }`;
    function compile(type, src){ const s=gl.createShader(type); gl.shaderSource(s,src); gl.compileShader(s); return s; }
    const prog = gl.createProgram();
    gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
    gl.linkProgram(prog);
    gl.useProgram(prog);
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
    const ploc = gl.getAttribLocation(prog, 'p');
    gl.enableVertexAttribArray(ploc);
    gl.vertexAttribPointer(ploc, 2, gl.FLOAT, false, 0, 0);
    const uT = gl.getUniformLocation(prog, 't');
    const uA = gl.getUniformLocation(prog, 'uActive');
    let raf = 0;
    const start = performance.now();
    const loop = (now) => {
      raf = requestAnimationFrame(loop);
      if (!stateRef.current.visible) return;
      gl.useProgram(prog);
      gl.uniform1f(uT, (now - start)/1000);
      gl.uniform1f(uA, stateRef.current.active ? 1.0 : 0.0);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };
    raf = requestAnimationFrame(loop);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); io.disconnect(); };
  }, []);
  React.useEffect(() => { stateRef.current.active = active; }, [active]);
  return <canvas ref={ref} className="block w-full h-full cursor-cross" />;
};

window.MiniViz = {
  ParticleField, Oscilloscope, HexFloor, Chladni, PlasmaSphere, Marbled,
};
