// Stage.jsx — the central visual stage. Canvas + per-mode renderers.
// Each mode is a pure function that draws into the supplied 2D context.

function Stage({ mode, state }) {
  const canvasRef = React.useRef(null);

  React.useEffect(() => {
    const c = canvasRef.current;
    if (!c) return;
    let alive = true;
    const start = performance.now();
    let bodyPts = null, sandPts = null, sandW = 0, sandH = 0;
    let lastPulseB = 0;

    function fit() {
      const r = c.getBoundingClientRect();
      const d = window.devicePixelRatio || 1;
      const w = Math.max(10, Math.round(r.width * d));
      const h = Math.max(10, Math.round(r.height * d));
      if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
      return [c.getContext('2d'), w, h, d];
    }

    function frame() {
      if (!alive) return;
      const [g, w, h, d] = fit();
      g.clearRect(0, 0, w, h);
      const now = (performance.now() - start) / 1000;
      const L = (window.MM && window.MM.live) || { bass:0, mid:0, top:0, energy:0 };
      const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      // Idle pulse so the dormant state still breathes unless the visitor asks for less motion.
      const idle = (state === 'standby' || state === 'ready' || state === 'paused');
      const idlePulse = (idle && !reduceMotion) ? (Math.sin(now / 2.1) * 0.5 + 0.5) * 0.18 : 0;
      const b = Math.max(L.bass,   idlePulse * 0.9);
      const m = Math.max(L.mid,    idlePulse * 0.55);
      const t = Math.max(L.top,    idlePulse * 0.42);
      const e = Math.max(L.energy, idlePulse * 0.7);

      // Always: dim stage wash.
      drawStageWash(g, w, h, d, now, e);

      if (mode === 'mirror')      drawMirror(g, w, h, d, now, b, m, t, e);
      else if (mode === 'sand')   drawSand(g, w, h, d, now, b, m, t, e);
      else if (mode === 'oil')    drawOil(g, w, h, d, now, b, m, t, e);
      else if (mode === 'laser')  drawLaser(g, w, h, d, now, b, m, t, e);
      else if (mode === 'pyramid')drawPyramid(g, w, h, d, now, b, m, t, e);
      else if (mode === 'signal') drawSignal(g, w, h, d, now, b, m, t, e);

      // Always: pressure ribbon along the horizon, when there's signal.
      if (window.MM && window.MM.timeData && mode !== 'signal' && mode !== 'laser')
        drawPressureRibbon(g, w, h, h*0.52, d, now);

      // Always: layered mode caption + readout (drawn in DOM, not canvas)
      requestAnimationFrame(frame);
    }

    function drawStageWash(g, w, h, d, now, e) {
      g.save();
      const grad = g.createLinearGradient(0, 0, 0, h);
      grad.addColorStop(0,   'rgba(80,214,255,0.03)');
      grad.addColorStop(0.45,'rgba(255,169,80,0.04)');
      grad.addColorStop(1,   'rgba(255,81,71,0.05)');
      g.fillStyle = grad; g.fillRect(0, 0, w, h);
      g.restore();
    }

    function drawMirror(g, w, h, d, now, b, m, t, energy) {
      const cx = w/2, cy = h*0.52, N = 220, base = Math.min(w, h)*0.18;
      if (!bodyPts || bodyPts.length !== N) {
        bodyPts = [];
        for (let i=0; i<N; i++) bodyPts.push({ a: i/N*Math.PI*2, r: base, v: 0, phase: Math.random()*Math.PI*2 });
      }
      // outer rings — the room
      g.save(); g.globalCompositeOperation = 'lighter';
      const rings = 7;
      for (let i=0; i<rings; i++) {
        const pulse = (i/rings + now*(0.05 + b*0.06)) % 1;
        const r = base*2 + pulse*Math.min(w, h)*0.32;
        const a = (1 - pulse) * (0.18 + b*0.25);
        g.strokeStyle = `rgba(255,169,80,${a})`;
        g.lineWidth = (0.5 + pulse*1.2) * d;
        g.beginPath();
        g.ellipse(cx, cy, r*(1+0.22*m), r*(0.62+0.15*t), Math.sin(now*0.25)*0.08, 0, Math.PI*2);
        g.stroke();
      }
      // tri-channel body
      const drive = b*1.3 + m*0.7 + t*0.4;
      const kick = Math.max(0, b - lastPulseB); lastPulseB = b;
      const layers = [
        { sx: 1.02, sy: 1.18, col: '255,81,71',   val: b, lag: 0   },
        { sx: 1.18, sy: 0.92, col: '248,233,215', val: m, lag: 1.7 },
        { sx: 1.34, sy: 0.64, col: '80,214,255',  val: t, lag: 3.1 },
      ];
      const grad = g.createRadialGradient(cx, cy, base*0.15, cx, cy, base*(2.9 + energy));
      grad.addColorStop(0,   `rgba(255,169,80,${0.10 + energy*0.18})`);
      grad.addColorStop(0.5, `rgba(255,81,71,${0.04 + b*0.12})`);
      grad.addColorStop(1,   'rgba(0,0,0,0)');
      g.fillStyle = grad; g.beginPath(); g.arc(cx, cy, base*(2.8 + energy*1.4), 0, Math.PI*2); g.fill();

      const td = window.MM?.timeData;
      layers.forEach((L, li) => {
        g.beginPath();
        for (let i=0; i<=N; i++) {
          const p = bodyPts[i % N];
          const a = p.a + Math.sin(now*0.12 + p.phase)*0.025 + li*0.035;
          const sample = td ? Math.abs(td[Math.floor(i/N*td.length)]||0) : 0.12;
          const target = base*(1 + L.val*1.25 + sample*(0.55 + drive*0.8) + kick*1.1)
                       + Math.sin(a*3 + now*(1.4 + b*2) + L.lag) * base * (0.10 + L.val*0.42)
                       + Math.sin(a*8 - now*(0.8 + t*2) + L.lag) * base * (0.035 + t*0.18);
          p.v += (target - p.r) * (0.035 + energy*0.018);
          p.v *= 0.84; p.r += p.v;
          const x = cx + Math.cos(a) * p.r * L.sx;
          const y = cy + Math.sin(a) * p.r * L.sy;
          i ? g.lineTo(x, y) : g.moveTo(x, y);
        }
        g.closePath();
        g.strokeStyle = `rgba(${L.col},${0.28 + L.val*0.42})`;
        g.lineWidth = (li === 1 ? 1.6 : 1.15) * d;
        g.stroke();
        if (li === 0) { g.fillStyle = `rgba(${L.col},${0.025 + b*0.045})`; g.fill(); }
      });
      g.restore();
    }

    function drawSand(g, w, h, d, now, b, m, t, energy) {
      const cx = w/2, cy = h*0.54, base = Math.min(w, h)*0.34, N = 1100;
      if (!sandPts || sandPts.length !== N || sandW !== w || sandH !== h) {
        sandPts = []; sandW = w; sandH = h;
        const R = Math.min(w, h)*0.36;
        for (let i=0; i<N; i++) {
          const a = Math.random()*Math.PI*2, r = Math.sqrt(Math.random())*R;
          sandPts.push({ a, r, phase: Math.random()*Math.PI*2, size: 0.45 + Math.random()*1.35 });
        }
      }
      g.save(); g.globalCompositeOperation = 'lighter';
      // rings
      const rings = 5 + Math.round((b+m+t)*4);
      for (let i=1; i<=rings; i++) {
        const rr = base*i/(rings+0.7), a = 0.05 + (1 - i/rings)*(0.12 + b*0.18);
        g.strokeStyle = `rgba(255,169,80,${a})`;
        g.lineWidth = (0.6 + i*0.05)*d;
        g.beginPath();
        g.ellipse(cx, cy, rr*(1 + m*0.16 + Math.sin(now*0.4 + i)*0.015), rr*(0.58 + b*0.12), Math.sin(now*0.12)*0.04, 0, Math.PI*2);
        g.stroke();
      }
      // sand grains
      for (const p of sandPts) {
        const k1 = 3 + Math.round(b*5), k2 = 5 + Math.round(m*7), k3 = 9 + Math.round(t*9);
        const a = p.a + Math.sin(now*0.12 + p.phase)*0.015;
        const field = Math.sin(k1*a + now*(0.35 + b*0.8))*b
                    + Math.sin(k2*a - now*(0.22 + m*0.7))*m*0.75
                    + Math.cos(k3*a + p.r*0.018 - now*(0.3 + t))*t*0.55;
        const node = 1 - Math.min(1, Math.abs(field));
        const pull = node*(0.35 + energy*0.8);
        const targetR = p.r + Math.sin(field*2.4 + p.phase)*base*0.02*pull;
        const squash = 0.58 + b*0.12 - t*0.04;
        const jitter = (1 - node)*energy*2.2*d;
        const x = cx + Math.cos(a)*targetR + Math.sin(now*3 + p.phase)*jitter;
        const y = cy + Math.sin(a)*targetR*squash + Math.cos(now*2.4 + p.phase)*jitter;
        const alpha = 0.16 + node*0.64, sz = (p.size + node*1.2 + b*0.7)*d;
        g.fillStyle = `rgba(248,210,150,${alpha})`;
        g.beginPath(); g.arc(x, y, sz, 0, Math.PI*2); g.fill();
      }
      g.restore();
    }

    function drawOil(g, w, h, d, now, b, m, t, energy) {
      // Liquid-light pressure wash. Layered radial blobs that flow and bloom.
      g.save(); g.globalCompositeOperation = 'lighter';
      const cx = w/2, cy = h*0.52, base = Math.min(w, h)*0.34;
      const blobs = [
        { col: '255,81,71',   a: now*0.12,                  r: base*(1.0 + b*0.6),  amp: 0.55 + b },
        { col: '255,169,80',  a: now*0.18 + 1.2,            r: base*(0.85 + m*0.4), amp: 0.45 + m },
        { col: '80,214,255',  a: -now*0.22 + 2.5,           r: base*(0.7 + t*0.5),  amp: 0.40 + t },
        { col: '255,122,58',  a: now*0.09 + 3.7,            r: base*(0.9 + b*0.3),  amp: 0.35 + (b+m)*0.5 },
      ];
      for (const blob of blobs) {
        const x = cx + Math.cos(blob.a)*base*0.45;
        const y = cy + Math.sin(blob.a*1.3)*base*0.22;
        const r = blob.r * (1 + Math.sin(now*0.4 + blob.amp)*0.12);
        const grad = g.createRadialGradient(x, y, 0, x, y, r);
        grad.addColorStop(0,   `rgba(${blob.col},${0.18 + blob.amp*0.10})`);
        grad.addColorStop(0.5, `rgba(${blob.col},${0.06 + blob.amp*0.05})`);
        grad.addColorStop(1,   'rgba(0,0,0,0)');
        g.fillStyle = grad; g.beginPath(); g.arc(x, y, r, 0, Math.PI*2); g.fill();
      }
      // turbulence streaks (spectral flux proxy)
      g.strokeStyle = `rgba(248,233,215,${0.04 + t*0.12})`;
      g.lineWidth = 1*d;
      for (let i=0; i<28; i++) {
        const ph = i*0.21 + now*(0.4 + t*1.2);
        const y = cy + Math.sin(ph)*base*0.35;
        g.beginPath();
        g.moveTo(0, y);
        for (let x=0; x<w; x+=12*d) {
          const yy = y + Math.sin(x*0.01 + ph)*base*0.08*(0.5 + m);
          g.lineTo(x, yy);
        }
        g.stroke();
      }
      // bloom on bass onset
      if (b > 0.4) {
        const grad = g.createRadialGradient(cx, cy, 0, cx, cy, base*1.8);
        grad.addColorStop(0,   `rgba(255,169,80,${b*0.20})`);
        grad.addColorStop(0.5, `rgba(255,81,71,${b*0.12})`);
        grad.addColorStop(1,   'rgba(0,0,0,0)');
        g.fillStyle = grad; g.beginPath(); g.arc(cx, cy, base*1.8, 0, Math.PI*2); g.fill();
      }
      g.restore();
    }

    function drawLaser(g, w, h, d, now, b, m, t, energy) {
      // The MEASURED mode. Pure waveform + a Lissajous trace.
      const cx = w/2, cy = h*0.5;
      const td = window.MM?.timeData;
      g.save(); g.globalCompositeOperation = 'lighter';
      // grid
      g.strokeStyle = 'rgba(255,169,80,0.08)';
      g.lineWidth = 1*d;
      for (let i=1; i<8; i++) {
        const y = h*i/8;
        g.beginPath(); g.moveTo(0, y); g.lineTo(w, y); g.stroke();
      }
      for (let i=1; i<14; i++) {
        const x = w*i/14;
        g.beginPath(); g.moveTo(x, 0); g.lineTo(x, h); g.stroke();
      }
      // center crosshair
      g.strokeStyle = 'rgba(255,169,80,0.22)';
      g.beginPath(); g.moveTo(0, cy); g.lineTo(w, cy); g.stroke();
      g.beginPath(); g.moveTo(cx, 0); g.lineTo(cx, h); g.stroke();
      // waveform trace
      if (td && td.length) {
        for (let pass=0; pass<3; pass++) {
          g.strokeStyle = pass===0 ? 'rgba(80,214,255,0.95)' : pass===1 ? 'rgba(80,214,255,0.45)' : 'rgba(80,214,255,0.18)';
          g.lineWidth = (pass===0 ? 1.6 : 3 + pass)*d;
          g.beginPath();
          for (let i=0; i<td.length; i++) {
            const x = i/(td.length-1) * w;
            const y = cy + td[i]*h*0.36;
            i ? g.lineTo(x, y) : g.moveTo(x, y);
          }
          g.stroke();
        }
        // Phase-style inset from delayed measured waveform. Not a true stereo L/R claim.
        const R = Math.min(w, h)*0.14, ox = w - R - 24*d, oy = h - R - 24*d;
        g.strokeStyle = 'rgba(80,214,255,0.6)';
        g.lineWidth = 1*d;
        g.beginPath();
        for (let i=0; i<td.length; i+=2) {
          const a = td[i] || 0, b2 = td[(i+13)%td.length] || 0;
          const x = ox + a*R, y = oy + b2*R;
          i ? g.lineTo(x, y) : g.moveTo(x, y);
        }
        g.stroke();
        g.strokeStyle = 'rgba(255,169,80,0.4)';
        g.strokeRect(ox - R, oy - R, R*2, R*2);
      } else {
        drawAwaiting(g, w, h, d, 'awaiting measured waveform');
      }
      g.restore();
    }

    function drawPyramid(g, w, h, d, now, b, m, t, energy) {
      // Sound as architecture. Stacked frames whose width is band-driven.
      const cx = w/2, baseY = h*0.82;
      g.save(); g.globalCompositeOperation = 'lighter';
      const floorWidth = Math.min(w, h)*0.72;
      const layers = 14;
      for (let i=0; i<layers; i++) {
        const u = i / (layers - 1);
        const band = u < 0.33 ? b : u < 0.7 ? m : t;
        const col  = u < 0.33 ? '255,81,71' : u < 0.7 ? '248,233,215' : '80,214,255';
        const widthFactor = (1 - u*0.92) + Math.sin(now*0.9 + i*0.4)*0.02*(1 + band*2);
        const yOff = i * 26*d * (1 - u*0.3);
        const y = baseY - yOff;
        const lw = floorWidth * widthFactor * (1 + band*0.18);
        g.strokeStyle = `rgba(${col},${0.18 + band*0.55})`;
        g.lineWidth = (1 + band*1.5)*d;
        g.beginPath();
        g.moveTo(cx - lw/2, y);
        g.lineTo(cx + lw/2, y);
        g.stroke();
        // verticals at the ends, like the silhouette of the chamber
        if (i < layers-1) {
          const nextLw = floorWidth * ((1 - (i+1)/(layers-1)*0.92) + Math.sin(now*0.9 + (i+1)*0.4)*0.02);
          g.strokeStyle = `rgba(${col},${0.10 + band*0.25})`;
          g.lineWidth = 1*d;
          g.beginPath();
          g.moveTo(cx - lw/2, y); g.lineTo(cx - nextLw/2, y - 26*d*(1 - u*0.3));
          g.moveTo(cx + lw/2, y); g.lineTo(cx + nextLw/2, y - 26*d*(1 - u*0.3));
          g.stroke();
        }
      }
      // apex light
      const apexY = baseY - layers*26*d*0.8;
      const grad = g.createRadialGradient(cx, apexY, 0, cx, apexY, Math.min(w, h)*0.3);
      grad.addColorStop(0,   `rgba(255,169,80,${0.30 + t*0.35})`);
      grad.addColorStop(0.5, `rgba(255,169,80,${0.08 + t*0.12})`);
      grad.addColorStop(1,   'rgba(0,0,0,0)');
      g.fillStyle = grad; g.beginPath(); g.arc(cx, apexY, Math.min(w, h)*0.3, 0, Math.PI*2); g.fill();
      // chamber pulse (rhythm)
      if (b > 0.35) {
        g.strokeStyle = `rgba(244,168,63,${b*0.4})`;
        g.lineWidth = 2*d;
        g.strokeRect(cx - floorWidth/2, baseY - layers*26*d, floorWidth, layers*26*d);
      }
      g.restore();
    }

    function drawSignal(g, w, h, d, now, b, m, t, energy) {
      // The proof mode. Three stacked panels inside the stage.
      const padL = 28*d, padR = 28*d, padT = 28*d, padB = 28*d;
      const innerW = w - padL - padR;
      const sectionH = (h - padT - padB - 24*d) / 3;
      g.save();
      // labels
      g.fillStyle = 'rgba(214,201,182,0.9)';
      g.font = `500 ${10*d}px 'IBM Plex Mono', monospace`;
      ['01 // WAVEFORM · MEASURED', '02 // SPECTRUM · MEASURED', '03 // BANDS · MEASURED'].forEach((label, i) => {
        const y = padT + i*(sectionH + 12*d);
        g.fillText(label, padL, y - 4*d);
      });
      const hasSignal = !!(window.MM?.timeData && window.MM?.freqData);
      if (!hasSignal) {
        for (let i=0; i<3; i++) {
          const y = padT + i*(sectionH + 12*d);
          g.strokeStyle = 'rgba(255,255,255,0.12)';
          g.strokeRect(padL, y, innerW, sectionH);
        }
        drawAwaiting(g, w, h, d, 'awaiting measured signal — no generated proof');
        g.restore();
        return;
      }
      // 1: waveform line
      {
        const y0 = padT + sectionH;
        const yMid = padT + sectionH*0.5;
        const td = window.MM?.timeData;
        g.strokeStyle = 'rgba(248,233,215,0.95)';
        g.lineWidth = 1.1*d;
        g.beginPath();
        if (td) {
          for (let i=0; i<td.length; i++) {
            const x = padL + i/(td.length-1)*innerW;
            const yy = yMid + td[i]*sectionH*0.4;
            i ? g.lineTo(x, yy) : g.moveTo(x, yy);
          }
        }
        g.stroke();
        g.strokeStyle = 'rgba(58,44,31,1)';
        g.strokeRect(padL, padT, innerW, sectionH);
      }
      // 2: spectrum bars
      {
        const y0 = padT + sectionH + 12*d;
        const fd = window.MM?.freqData;
        const bands = 64;
        const bw = innerW / bands;
        for (let i=0; i<bands; i++) {
          let v = 0;
          if (fd) {
            const a = Math.floor(i/bands*fd.length), bnd = Math.floor((i+1)/bands*fd.length);
            for (let k=a; k<bnd; k++) v = Math.max(v, fd[k]/255 || 0);
          }
          const bh = v * sectionH * 0.92;
          const x = padL + i*bw;
          g.fillStyle = i < 8 ? '#ff5147' : i < 36 ? '#f8e9d7' : '#50d6ff';
          g.globalAlpha = 0.35 + v*0.65;
          g.fillRect(x, y0 + sectionH - bh, bw - 1*d, bh);
        }
        g.globalAlpha = 1;
        g.strokeStyle = 'rgba(58,44,31,1)';
        g.strokeRect(padL, y0, innerW, sectionH);
      }
      // 3: band meters (b/m/t bars)
      {
        const y0 = padT + 2*(sectionH + 12*d);
        const cells = [
          { name: 'BOTTOM', v: b, col: '#ff5147' },
          { name: 'MIDDLE', v: m, col: '#f8e9d7' },
          { name: 'TOP',    v: t, col: '#50d6ff' },
        ];
        const cellW = innerW / 3 - 12*d;
        cells.forEach((c, i) => {
          const x = padL + i*(cellW + 18*d);
          g.strokeStyle = 'rgba(58,44,31,1)';
          g.strokeRect(x, y0, cellW, sectionH);
          g.fillStyle = 'rgba(214,201,182,0.9)';
          g.font = `500 ${9*d}px 'IBM Plex Mono', monospace`;
          g.fillText(c.name, x + 10*d, y0 + 18*d);
          g.fillStyle = c.col;
          g.globalAlpha = 0.85;
          const bw = (cellW - 20*d) * c.v;
          g.fillRect(x + 10*d, y0 + sectionH - 24*d, bw, 14*d);
          g.globalAlpha = 1;
          g.fillStyle = '#f8e9d7';
          g.font = `800 ${18*d}px 'Spectral', serif`;
          g.fillText(Math.round(c.v*100) + '%', x + cellW - 60*d, y0 + 32*d);
        });
      }
      g.restore();
    }


    function drawAwaiting(g, w, h, d, text) {
      g.save();
      g.fillStyle = 'rgba(239,231,255,0.58)';
      g.font = `600 ${13*d}px 'Spectral', serif`;
      g.textAlign = 'center';
      g.fillText(text, w/2, h*0.52);
      g.restore();
    }

    function drawPressureRibbon(g, w, h, cy, d, now) {
      const data = window.MM?.timeData;
      if (!data) return;
      g.save(); g.globalCompositeOperation = 'lighter';
      for (let pass=0; pass<3; pass++) {
        g.strokeStyle = pass===0 ? 'rgba(255,169,80,0.55)' : pass===1 ? 'rgba(248,233,215,0.22)' : 'rgba(80,214,255,0.15)';
        g.lineWidth = (pass===0 ? 1.2 : 1) * d;
        g.beginPath();
        for (let i=0; i<data.length; i++) {
          const x = i/(data.length-1) * w;
          const y = cy + data[i]*h*(0.14 + pass*0.04) + Math.sin(i*0.045 + now*5 + pass)*6*d;
          i ? g.lineTo(x, y) : g.moveTo(x, y);
        }
        g.stroke();
      }
      g.restore();
    }

    requestAnimationFrame(frame);
    const onResize = () => { /* fit() runs each frame */ };
    window.addEventListener('resize', onResize);
    return () => { alive = false; window.removeEventListener('resize', onResize); };
  }, [mode, state]);

  // Mode label + readout overlay
  const labels = {
    mirror:  'Mirror · the living body',
    sand:    'Sand · cymatic field',
    oil:     'Oil · liquid pressure',
    laser:   'Laser · oscilloscope',
    pyramid: 'Pyramid · architecture',
    signal:  'Signal · the proof layer',
  };
  const truths = {
    mirror:  ['interpreted', 'Interpreted'],
    sand:    ['modeled',     'Modeled'],
    oil:     ['interpreted', 'Interpreted'],
    laser:   ['measured',    'Measured'],
    pyramid: ['modeled',     'Int / Mod'],
    signal:  ['measured',    'Measured'],
  };
  const [readout, setReadout] = React.useState('awaiting signal');
  React.useEffect(() => {
    if (!window.MM) return;
    const off = window.MM.onFrame(() => {
      const L = window.MM.live;
      const pct = v => Math.round(v*100) + '%';
      if (state === 'live' || state === 'listening')
        setReadout(`bottom ${pct(L.bass)} · middle ${pct(L.mid)} · top ${pct(L.top)}`);
      else
        setReadout('awaiting signal');
    });
    return off;
  }, [state]);

  return (
    <section className="mm-stage" aria-label="Measured waveform listening stage">
      <canvas
        ref={canvasRef}
        role="img"
        aria-label="Measured waveform stage. Audio stays local in your browser."
      />
      <div className="mm-stage__labels">
        <div className="mm-stage__mode">
          <span>{labels[mode]}</span>
          <span className={"mm-truth mm-truth--" + truths[mode][0]}>{truths[mode][1]}</span>
        </div>
        <div className="mm-stage__readout">{readout}</div>
      </div>
    </section>
  );
}

Object.assign(window, { Stage });
