'use strict';

// The Device — scope canvas (leaf). BOTH the waveform display and the XY
// performance surface: pointer down starts the voice, drag updates pitch
// (X, snapped to scale) and filter cutoff (Y, log), pointer up releases.
// Renders the AnalyserNode time-domain trace every frame.

const { useEffect, useRef, useState } = React;

const TRACE_COLOR = '#46ffc0';
const TRACE_GLOW  = 'rgba(70, 255, 192, 0.85)';
const GRID_COLOR  = 'rgba(64, 200, 150, 0.10)';
const BG_COLOR    = '#03100b';
const GLASS_COLOR = '#020806'; // dead glass outside the readout margin

// Percent margin on each edge between the canvas bounds and the DRAWN
// readout (grid/trace/crosshair/vignette), so the display sits clear of the
// cutout's rounded/ragged corners. TOUCH BEHAVIOR IS DELIBERATELY (a): the
// XY field still maps across the FULL canvas — this inset is purely cosmetic
// framing and the playable feel is byte-for-byte what it was at inset 0. The
// crosshair is the one bridge: its position is re-projected from full-canvas
// touch coords into the inset rect so the readout shows where you are.
const DRAW_INSET_PCT = 5;

function ScopeCanvas() {
  const canvasRef  = useRef(null);
  const pointerRef = useRef({ down: false, x: 0, y: 0 }); // canvas-CSS-px coords
  const [awake, setAwake] = useState(false);

  // First pointerdown ANYWHERE unlocks the AudioContext (iOS gesture rule)
  // and dismisses the wake prompt.
  useEffect(() => {
    const wake = () => {
      if (window.DeviceControls) window.DeviceControls.ensure();
      else window.DeviceAudio.ensure();
      setAwake(true);
    };
    window.addEventListener('pointerdown', wake, { once: true });
    return () => window.removeEventListener('pointerdown', wake);
  }, []);

  // Render loop + backing-store sizing.
  useEffect(() => {
    const canvas = canvasRef.current;
    const g = canvas.getContext('2d');
    let raf = 0;
    let timeDomain = null;

    const fit = () => {
      const r = canvas.getBoundingClientRect();
      const dpr = window.devicePixelRatio || 1;
      const w = Math.max(1, Math.round(r.width * dpr));
      const h = Math.max(1, Math.round(r.height * dpr));
      if (canvas.width !== w || canvas.height !== h) {
        canvas.width = w;
        canvas.height = h;
      }
    };
    const ro = new ResizeObserver(fit);
    ro.observe(canvas);
    fit();

    const draw = () => {
      raf = requestAnimationFrame(draw);
      const W = canvas.width, H = canvas.height;
      const dpr = window.devicePixelRatio || 1;

      // readout rect — everything below draws into this, not the full canvas
      const RX = (W * DRAW_INSET_PCT) / 100;
      const RY = (H * DRAW_INSET_PCT) / 100;
      const RW = W - 2 * RX;
      const RH = H - 2 * RY;

      // dead glass across the whole canvas (incl. the margin) so the cutout
      // never shows flesh through it, then clip the live readout to the rect
      g.globalCompositeOperation = 'source-over';
      g.fillStyle = GLASS_COLOR;
      g.fillRect(0, 0, W, H);
      g.save();
      g.beginPath();
      g.rect(RX, RY, RW, RH);
      g.clip();

      // bed
      g.fillStyle = BG_COLOR;
      g.fillRect(RX, RY, RW, RH);

      // faint grid — one column per scale degree so the snap zones read
      const cols = window.DeviceScale.degreeCount;
      g.strokeStyle = GRID_COLOR;
      g.lineWidth = 1;
      g.beginPath();
      for (let i = 1; i < cols; i++) {
        const x = RX + (i / cols) * RW;
        g.moveTo(x, RY); g.lineTo(x, RY + RH);
      }
      for (let j = 1; j < 5; j++) {
        const y = RY + (j / 5) * RH;
        g.moveTo(RX, y); g.lineTo(RX + RW, y);
      }
      g.stroke();

      // center line, slightly brighter
      g.strokeStyle = 'rgba(64, 200, 150, 0.18)';
      g.beginPath();
      g.moveTo(RX, RY + RH / 2); g.lineTo(RX + RW, RY + RH / 2);
      g.stroke();

      // ── recording indicator (Change 1): loop-position sweep ─────────
      // Only while armed/recording. Reads loop phase from DeviceLoop (no new
      // timing source). Drawn behind the trace so it never obscures it; the
      // line resets to the left at each loop boundary.
      const loopState = (window.DeviceLoop && window.DeviceLoop.getState) ? window.DeviceLoop.getState() : null;
      const recActive = loopState && (loopState.recordState === 'armed' || loopState.recordState === 'recording');
      if (recActive) {
        const sx = RX + loopState.phase01 * RW;
        g.strokeStyle = 'rgba(120, 255, 210, 0.40)';
        g.lineWidth = 1.5 * dpr;
        g.shadowColor = 'rgba(70, 255, 192, 0.7)';
        g.shadowBlur = 6 * dpr;
        g.beginPath(); g.moveTo(sx, RY); g.lineTo(sx, RY + RH); g.stroke();
        g.shadowBlur = 0;
      }

      // waveform (or idle flat line before the context exists)
      const analyser = window.DeviceAudio.getAnalyser();
      g.lineJoin = 'round';
      g.shadowColor = TRACE_GLOW;
      g.shadowBlur = 14 * dpr;
      g.strokeStyle = TRACE_COLOR;
      g.lineWidth = 2 * dpr;
      g.beginPath();
      if (analyser) {
        if (!timeDomain || timeDomain.length !== analyser.fftSize) {
          timeDomain = new Uint8Array(analyser.fftSize);
        }
        analyser.getByteTimeDomainData(timeDomain);
        const n = timeDomain.length;
        for (let i = 0; i < n; i++) {
          const x = RX + (i / (n - 1)) * RW;
          const y = RY + (timeDomain[i] / 255) * RH;
          i === 0 ? g.moveTo(x, y) : g.lineTo(x, y);
        }
      } else {
        // resting trace: flat with a hair of phosphor noise
        const mid = RY + RH / 2;
        for (let x = RX; x <= RX + RW; x += 4 * dpr) {
          const y = mid + (Math.random() - 0.5) * 1.2 * dpr;
          x === RX ? g.moveTo(x, y) : g.lineTo(x, y);
        }
      }
      g.stroke();
      // hot core pass
      g.shadowBlur = 0;
      g.strokeStyle = 'rgba(220, 255, 242, 0.55)';
      g.lineWidth = 0.8 * dpr;
      g.stroke();

      // crosshair at the touch point while a note is held. Touch coords are
      // full-canvas (behavior (a)); re-project them into the readout rect so
      // the marker stays inside the frame.
      const p = pointerRef.current;
      if (p.down) {
        const px = RX + ((p.x * dpr) / W) * RW;
        const py = RY + ((p.y * dpr) / H) * RH;
        g.strokeStyle = 'rgba(170, 255, 224, 0.35)';
        g.lineWidth = 1 * dpr;
        g.beginPath();
        g.moveTo(px - 10 * dpr, py); g.lineTo(px + 10 * dpr, py);
        g.moveTo(px, py - 10 * dpr); g.lineTo(px, py + 10 * dpr);
        g.stroke();
        g.beginPath();
        g.arc(px, py, 5 * dpr, 0, Math.PI * 2);
        g.fillStyle = 'rgba(170, 255, 224, 0.25)';
        g.fill();
      }

      // ── recording indicator (Change 1): REC glyph ───────────────────
      // BLINKS while armed (waiting for the loop top — explains the delay),
      // goes SOLID while actively recording, off when idle.
      if (recActive) {
        const recording = loopState.recordState === 'recording';
        const lit = recording ? true : (performance.now() % 760 < 460); // blink while armed
        if (lit) {
          const pad = 9 * dpr, dotR = 4.5 * dpr;
          const dotX = RX + RW - pad - dotR;
          const dotY = RY + pad + dotR;
          g.shadowColor = 'rgba(70, 255, 192, 0.85)';
          g.shadowBlur = 8 * dpr;
          g.fillStyle = recording ? 'rgba(150, 255, 220, 0.98)' : 'rgba(120, 255, 210, 0.8)';
          g.font = `${13 * dpr}px "VT323", monospace`;
          g.textAlign = 'right'; g.textBaseline = 'middle';
          g.fillText('REC', dotX - dotR - 5 * dpr, dotY + dpr);
          g.beginPath(); g.arc(dotX, dotY, dotR, 0, Math.PI * 2); g.fill();
          g.shadowBlur = 0;
          g.textAlign = 'left'; g.textBaseline = 'alphabetic';
        }
      }

      // scanlines
      g.fillStyle = 'rgba(0, 0, 0, 0.16)';
      const step = Math.max(3, Math.round(3 * dpr));
      for (let y = RY; y < RY + RH; y += step) g.fillRect(RX, y, RW, 1);

      // vignette
      const v = g.createRadialGradient(RX + RW / 2, RY + RH / 2, Math.min(RW, RH) * 0.35, RX + RW / 2, RY + RH / 2, Math.max(RW, RH) * 0.72);
      v.addColorStop(0, 'rgba(0,0,0,0)');
      v.addColorStop(1, 'rgba(0,0,0,0.5)');
      g.fillStyle = v;
      g.fillRect(RX, RY, RW, RH);

      g.restore();
    };
    draw();

    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, []);

  // ── XY voice control ────────────────────────────────────────────────
  const applyXY = (e, rect) => {
    const xNorm = window.DeviceScale.clamp01((e.clientX - rect.left) / rect.width);
    const yNorm = window.DeviceScale.clamp01((e.clientY - rect.top) / rect.height);
    pointerRef.current.x = e.clientX - rect.left;
    pointerRef.current.y = e.clientY - rect.top;
    pointerRef.current.xNorm = xNorm;
    pointerRef.current.yNorm = yNorm;
    return {
      freq:   window.DeviceScale.freqForX(xNorm).freq,
      cutoff: window.DeviceScale.cutoffForY(yNorm),
      xNorm, yNorm,
    };
  };

  // Phase 4: all live input flows through DeviceControls.input, which drives
  // the voice (sustained, or arpeggiated when Knob 5 is up) AND feeds the loop
  // recorder. Behavior is identical to Phase 1 when the arp is off.
  const onPointerDown = (e) => {
    e.preventDefault();
    const canvas = canvasRef.current;
    // Capture can throw on an already-released pointer (fast taps) — the
    // note must still fire.
    try { canvas.setPointerCapture(e.pointerId); } catch (_) {}
    const { xNorm, yNorm } = applyXY(e, canvas.getBoundingClientRect());
    pointerRef.current.down = true;
    window.DeviceControls.input(xNorm, yNorm, true);
  };

  const onPointerMove = (e) => {
    if (!pointerRef.current.down) return;
    const { xNorm, yNorm } = applyXY(e, canvasRef.current.getBoundingClientRect());
    window.DeviceControls.input(xNorm, yNorm, true);
  };

  const endNote = (e) => {
    if (!pointerRef.current.down) return;
    pointerRef.current.down = false;
    window.DeviceControls.input(pointerRef.current.xNorm, pointerRef.current.yNorm, false);
    try { canvasRef.current.releasePointerCapture(e.pointerId); } catch (_) {}
  };

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <canvas
        ref={canvasRef}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={endNote}
        onPointerCancel={endNote}
        style={{
          position: 'absolute', inset: 0, width: '100%', height: '100%',
          touchAction: 'none', cursor: 'crosshair', display: 'block',
        }}
      />
      {!awake && (
        <div style={{
          position: 'absolute', inset: `${DRAW_INSET_PCT}%`, display: 'flex',
          alignItems: 'center', justifyContent: 'center',
          pointerEvents: 'none',
          fontFamily: '"VT323", monospace',
          fontSize: 'clamp(14px, 2.6vmin, 28px)',
          letterSpacing: '0.35em',
          color: 'rgba(70, 255, 192, 0.55)',
          textShadow: '0 0 12px rgba(70, 255, 192, 0.6)',
          animation: 'device-wake-blink 1.6s steps(1) infinite',
        }}>
          TOUCH TO WAKE
        </div>
      )}
    </div>
  );
}

window.ScopeCanvas = ScopeCanvas;
