// rain.jsx — Matrix-style digital rain canvas, themed.
// Reads theme + tweak overrides (color/glitch/scanlines), renders a
// requestAnimationFrame loop scoped to the artboard, pauses when offscreen
// to keep 3 concurrent rains lightweight.

const { useEffect, useRef } = React;

function MatrixRain({
  theme,
  width,
  height,
  intensity = 1,      // 0..1, scales drop density
  speed = 1,          // 0..2
  glitch = 0.4,       // 0..1, occasional char-flip rate
  colorOverride,      // optional hex from tweaks
  paused = false,
  reverse = false,    // true makes drops climb (used during blue-pill exit)
  burst = 0,          // 0..1 — flashes brighter during transitions
  variant = 'classic',// visual style: classic / neon / depth / tail / bloom / mix
}) {
  const canvasRef = useRef(null);
  const rafRef = useRef(0);
  const stateRef = useRef(null);

  // Re-init drops when size or theme changes.
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.floor(width * dpr);
    canvas.height = Math.floor(height * dpr);
    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';

    // ── Variant config ──────────────────────────────────────────────
    // Each variant flips a small set of knobs to produce a visually
    // distinct rain feel. The render loop checks these fields directly.
    const cfg = ((v) => {
      // Density + glow + per-variant fall-speed multiplier. All ultra-*
      // variants share the 8px dense base; pacing and glow vary so the
      // user can pick a tempo that doesn't feel slow OR rushed.
      if (v === 'dense')      return { fontSize: 12, glow: 22, fadeMul: 0.40, trailRows: 0, sizeRange: [10, 18], speedMul: 1.0 };
      if (v === 'denser')     return { fontSize: 10, glow: 22, fadeMul: 0.40, trailRows: 0, sizeRange: [ 8, 15], speedMul: 1.0 };
      if (v === 'cinematic')  return { fontSize: 13, glow: 30, fadeMul: 0.30, trailRows: 0, sizeRange: [11, 20], speedMul: 0.95 };
      if (v === 'ultra')      return { fontSize:  8, glow: 28, fadeMul: 0.33, trailRows: 0, sizeRange: [ 7, 12], speedMul: 0.80 };
      if (v === 'ultra-mod')  return { fontSize:  8, glow: 28, fadeMul: 0.33, trailRows: 0, sizeRange: [ 7, 12], speedMul: 0.75 };
      if (v === 'ultra-fast') return { fontSize:  8, glow: 30, fadeMul: 0.32, trailRows: 0, sizeRange: [ 7, 12], speedMul: 0.95 };
      if (v === 'ultra-neon') return { fontSize:  8, glow: 36, fadeMul: 0.28, trailRows: 0, sizeRange: [ 7, 12], speedMul: 0.80 };
      if (v === 'ultra-slow') return { fontSize:  8, glow: 26, fadeMul: 0.34, trailRows: 0, sizeRange: [ 7, 12], speedMul: 0.65 };
      // Legacy variants kept for back-compat.
      if (v === 'neon')       return { fontSize: 12, glow: 20, fadeMul: 1.0,  trailRows: 0, sizeRange: null,    speedMul: 1.0 };
      if (v === 'depth')      return { fontSize: 16, glow: 12, fadeMul: 1.0,  trailRows: 0, sizeRange: [10, 22], speedMul: 1.0 };
      if (v === 'tail')       return { fontSize: 15, glow: 14, fadeMul: 1.0,  trailRows: 3, sizeRange: null,    speedMul: 1.0 };
      if (v === 'bloom')      return { fontSize: 17, glow: 26, fadeMul: 0.5,  trailRows: 0, sizeRange: null,    speedMul: 1.0 };
      if (v === 'mix')        return { fontSize: 14, glow: 16, fadeMul: 0.9,  trailRows: 2, sizeRange: [11, 19], speedMul: 1.0 };
      return                         { fontSize: 16, glow: theme.rainGlowAmount, fadeMul: 1.0, trailRows: 0, sizeRange: null, speedMul: 1.0 };
    })(variant);

    const fontSize = cfg.fontSize;
    const cols = Math.ceil(width / fontSize);
    // Initialise drops slightly ABOVE the top of the screen so the very
    // first wave of glyphs enters from row 0 instead of spawning mid-
    // column. Each column gets its own small negative offset so the wave
    // isn't a perfect guillotine — the natural speed variance below
    // (0.6..1.5 rows/frame) takes over after a couple of seconds and the
    // pattern becomes chaotic on its own. This matches the original
    // "rain starts from the top, then desynchronises" feel.
    const drops = new Array(cols).fill(0).map(() => -Math.random() * 8);
    const speeds = new Array(cols).fill(0).map(() => 0.6 + Math.random() * 0.9);
    const isAccent = new Array(cols).fill(0).map(() => Math.random() < 0.12);
    const charset = (RAIN_CHARS[theme.chars] || RAIN_CHARS.classic).split('');
    // Per-column glyph size (for 'depth' / 'mix' variants). Uniform cellPx
    // still spaces columns — only the rendered glyph size varies. Each
    // column gets a depth score (0=far, 1=near) used to modulate per-
    // glyph brightness so smaller glyphs read as further away.
    const colSizes  = new Array(cols).fill(0).map(() => {
      if (!cfg.sizeRange) return fontSize;
      const [lo, hi] = cfg.sizeRange;
      return lo + Math.random() * (hi - lo);
    });
    const colDepths = colSizes.map((s) => {
      if (!cfg.sizeRange) return 1;
      const [lo, hi] = cfg.sizeRange;
      return (s - lo) / Math.max(1, hi - lo);
    });
    // Per-column "last row painted" — we only paint a fresh char when the
    // drop's floor() row advances by at least one cell, so chars never
    // stack on top of each other in the same cell.
    const lastRow = new Array(cols).fill(-1);
    // Word-streams: each entry, if set, hijacks a column to paint a real word
    // one char per row instead of random katakana. Cleared once the head
    // drifts off-screen, then the column resumes normal rain.
    const wordCols = new Array(cols).fill(null);
    stateRef.current = { dpr, fontSize, cols, drops, speeds, isAccent, charset, lastRow, wordCols, frameCount: 0, cfg, colSizes, colDepths };
  }, [width, height, theme]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !stateRef.current) return;
    const ctx = canvas.getContext('2d', { alpha: false });
    let lastFrame = 0;
    const targetFPS = 30;
    const frameInterval = 1000 / targetFPS;

    const step = (t) => {
      rafRef.current = requestAnimationFrame(step);
      if (paused) return;
      if (t - lastFrame < frameInterval) return;
      lastFrame = t;

      const s = stateRef.current;
      const { dpr, fontSize, cols, drops, speeds, isAccent, charset, wordCols, lastRow, cfg, colSizes, colDepths } = s;
      s.frameCount = (s.frameCount || 0) + 1;
      const w = canvas.width, h = canvas.height;

      // Background fade — leaves trails behind drops. fadeMul lets 'bloom'
      // fade slower so trails are visibly longer.
      ctx.fillStyle = `rgba(${hexToRgb(theme.bgDeep || theme.bg)}, ${theme.rainAlpha * cfg.fadeMul})`;
      ctx.fillRect(0, 0, w, h);

      ctx.font = `${fontSize * dpr}px ${theme.fontMono}`;
      ctx.textBaseline = 'top';

      const bodyColor = colorOverride || theme.rainBody;
      const accentColor = theme.rainAccent || theme.fgGlow;
      // When a pill is hovered, colorOverride is set to that pill's hex.
      // Brighten it for the head so the rain reads as vivid neon-red /
      // neon-blue instead of a dull tinted ghost. Null when no override
      // — falls through to the theme's normal rainHead.
      const overrideHead = colorOverride ? brighten(colorOverride, 0.55) : null;

      // Maybe spawn a new word-stream. Bumped pacing so 'WAKE_UP' makes
      // itself seen: check every ~3 frames, generous max-active (4–6
      // streams co-exist), 80% spawn probability.
      const WORDS = (window.RAIN_WORDS || []);
      if (WORDS.length && s.frameCount % 3 === 0) {
        const active = wordCols.reduce((n, w) => n + (w ? 1 : 0), 0);
        const maxActive = Math.max(4, Math.floor(cols / 15));
        if (active < maxActive && Math.random() < 0.80) {
          const word = WORDS[Math.floor(Math.random() * WORDS.length)];
          const rowsInCol = Math.floor(h / cell);
          const maxStartRow = rowsInCol - word.length - 2;
          const candidates = [];
          for (let i = 0; i < cols; i++) {
            if (wordCols[i]) continue;
            const r = drops[i];
            if (r >= 0 && r <= maxStartRow) candidates.push(i);
          }
          if (candidates.length) {
            const col = candidates[Math.floor(Math.random() * candidates.length)];
            wordCols[col] = {
              chars: word.split(''),
              startRow: Math.floor(drops[col]),
              lastPainted: -1,
            };
          }
        }
      }

      // Grid-aligned cell size so chars never overlap each other.
      const cell = fontSize * dpr;

      for (let i = 0; i < cols; i++) {
        const x = i * cell;

        // ── Word-stream branch ──
        // While a word is active on this column, it owns the column — we
        // skip the regular random-char render so the word stays readable.
        if (wordCols[i]) {
          const wc = wordCols[i];
          const newRow = Math.floor(drops[i]);
          if (newRow > wc.lastPainted) {
            const idx = newRow - wc.startRow;
            if (idx >= 0 && idx < wc.chars.length) {
              const ch = wc.chars[idx];
              // CRITICAL: explicitly set the font for word glyphs. Without
              // this the canvas falls back to its default 10px sans-serif
              // and 'WAKE_UP' renders too tiny to spot. The size matches
              // the variant's base font so the word reads as part of the
              // rain.
              const wSize = fontSize * dpr;
              ctx.font = `${wSize}px ${theme.fontMono}`;
              ctx.textBaseline = 'top';
              ctx.shadowBlur = cfg.glow * 1.4;
              ctx.shadowColor = theme.fg;
              ctx.fillStyle = theme.rainHead;
              ctx.fillText(ch, x, newRow * cell);
            }
            wc.lastPainted = newRow;
          }
          drops[i] += speeds[i] * speed;
          // Word expires once its last char has fully scrolled off-screen.
          const endRow = wc.startRow + wc.chars.length;
          if (newRow > endRow + 6 || newRow * cell > h + 4 * cell) {
            wordCols[i] = null;
            drops[i] = 0;
          }
          continue;
        }

        // floor onto the row grid — drops[i] is fractional for smooth motion,
        // but we render only at integer rows so chars never bleed into
        // neighbouring rows.
        const row = Math.floor(drops[i]);
        const y = row * cell;

        // ⚠️ Only paint when the column's head has actually advanced into a
        // new row — this is THE fix for the stacked-chars-in-one-cell bug.
        // Speeds < 1 mean a column stays on the same row for multiple frames;
        // without this guard we'd paint a NEW random char in that cell every
        // frame and the cell would show a soup of overlapping glyphs.
        const movedToNewRow = row > lastRow[i];

        if (movedToNewRow) {
          const ch = charset[(Math.floor(Math.random() * charset.length))];

          // Per-column glyph size + depth modulation. Smaller glyphs read
          // as further away — dimmer + less glow.
          const glyphSize = colSizes[i] * dpr;
          const depth = colDepths[i];
          ctx.font = `${glyphSize}px ${theme.fontMono}`;
          ctx.textBaseline = 'top';

          const headColor = overrideHead || (isAccent[i] ? accentColor : theme.rainHead);
          const headGlow = cfg.glow * (0.7 + 0.3 * depth);
          const dropColor = colorOverride || bodyColor;

          // ── Drop-bulb pass ────────────────────────────────────────
          // Paint the head TWICE: first with a wider, low-alpha halo so
          // the head reads as a "wet bulb" of green light, then a crisp
          // glyph on top. Multiplier kept moderate (1.5×) so two passes
          // per fresh head don't crater performance in dense variants
          // where dozens of cells paint each frame.
          ctx.shadowBlur = headGlow * 1.5;
          ctx.shadowColor = dropColor;
          ctx.fillStyle = withAlpha(dropColor, 0.35);
          ctx.fillText(ch, x, y);

          // Sharp head on top of the halo.
          ctx.shadowBlur = burst > 0 ? headGlow * 1.6 : headGlow * 0.8;
          ctx.shadowColor = isAccent[i] ? accentColor : bodyColor;
          ctx.fillStyle = depth < 1
            ? withAlpha(headColor, 0.55 + 0.45 * depth)
            : headColor;
          ctx.fillText(ch, x, y);

          lastRow[i] = row;
        }

        // Advance / wrap. We keep advancing even on frames we don't paint,
        // so the column accumulates fractional progress smoothly.
        const stepSize = speeds[i] * speed * (cfg.speedMul || 1) * (reverse ? -1 : 1);
        drops[i] += stepSize;
        if (!reverse && y > h && Math.random() > 0.975) {
          drops[i] = 0;
          lastRow[i] = -1;
        }
        if (reverse && y < -cell && Math.random() > 0.975) {
          drops[i] = h / cell;
          lastRow[i] = Math.floor(drops[i]);
        }

        // Glitch: occasional column flicker — bright flash on the same cell.
        // Only happens on "new row" frames so the flash doesn't double-paint.
        if (movedToNewRow && glitch > 0 && Math.random() < 0.0015 * glitch) {
          ctx.shadowBlur = 24;
          ctx.shadowColor = accentColor;
          ctx.fillStyle = '#ffffff';
          ctx.fillText(
            charset[(Math.floor(Math.random() * charset.length))],
            x,
            y,
          );
        }
      }
      ctx.shadowBlur = 0;
    };

    rafRef.current = requestAnimationFrame(step);
    return () => cancelAnimationFrame(rafRef.current);
  }, [theme, intensity, speed, glitch, colorOverride, paused, reverse, burst]);

  return (
    <canvas
      ref={canvasRef}
      style={{
        position: 'absolute',
        inset: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none',
        zIndex: 0,
      }}
    />
  );
}

// hex (#rrggbb) → "r, g, b" for use inside rgba()
function hexToRgb(hex) {
  const h = hex.replace('#', '');
  const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
  const n = parseInt(x, 16);
  return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
}

// `#rrggbb` (or `rgb(...)`) + alpha → `rgba(r,g,b,a)` for inline fills.
function withAlpha(color, alpha) {
  if (typeof color !== 'string') return color;
  if (color.startsWith('#')) return `rgba(${hexToRgb(color)},${alpha})`;
  if (color.startsWith('rgb(')) return color.replace('rgb(', 'rgba(').replace(')', `,${alpha})`);
  return color;
}

// Lighten a hex (or rgb()) colour toward white by `amount` (0..1). Used to
// derive a vivid head colour from the pill's saturated rim hex when the
// rain is colour-overridden by a hover.
function brighten(color, amount = 0.4) {
  if (typeof color !== 'string') return color;
  let r, g, b;
  if (color.startsWith('#')) {
    const h = color.replace('#', '');
    const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
    const n = parseInt(x, 16);
    r = (n >> 16) & 255; g = (n >> 8) & 255; b = n & 255;
  } else if (color.startsWith('rgb')) {
    const m = color.match(/\d+/g);
    if (!m) return color;
    [r, g, b] = m.map(Number);
  } else return color;
  const k = Math.max(0, Math.min(1, amount));
  const lerp = (v) => Math.round(v + (255 - v) * k);
  return `rgb(${lerp(r)},${lerp(g)},${lerp(b)})`;
}

// ── CRT overlay ──────────────────────────────────────────────────────────
// Scanlines + vignette + optional curvature + flicker. Pure CSS overlay, sits
// on top of the prototype content. Driven by the theme.

function CRTOverlay({ theme, scanlines = true, intensity = 1 }) {
  if (!scanlines && !theme.vignette) return null;
  return (
    <>
      {scanlines && (
        <div
          style={{
            position: 'absolute',
            inset: 0,
            pointerEvents: 'none',
            zIndex: 50,
            background: `repeating-linear-gradient(
              to bottom,
              rgba(0,0,0,0) 0px,
              rgba(0,0,0,0) 2px,
              rgba(0,0,0,${theme.scanlines * intensity}) 2px,
              rgba(0,0,0,${theme.scanlines * intensity}) 4px
            )`,
            mixBlendMode: 'multiply',
          }}
        />
      )}
      <div
        style={{
          position: 'absolute',
          inset: 0,
          pointerEvents: 'none',
          zIndex: 51,
          background: `radial-gradient(
            ellipse at center,
            rgba(0,0,0,0) 40%,
            rgba(0,0,0,${theme.vignette * 0.4}) 75%,
            rgba(0,0,0,${theme.vignette}) 100%
          )`,
        }}
      />
      {theme.flicker > 0 && (
        <div
          style={{
            position: 'absolute',
            inset: 0,
            pointerEvents: 'none',
            zIndex: 52,
            background: '#fff',
            opacity: 0,
            animation: `crt-flicker ${3 / (1 + theme.flicker * 4)}s steps(2, end) infinite`,
            mixBlendMode: 'overlay',
          }}
        />
      )}
    </>
  );
}

// Glitch-cursor: a faint duplicate cursor offset & desaturated, tracks the real
// pointer with jittery delay to give a "the system is watching" feel.
function GlitchCursor({ color, intensity = 0.5 }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const parent = el.parentElement;
    if (!parent) return;
    let raf = 0;
    let tx = 0, ty = 0, cx = 0, cy = 0;
    const onMove = (e) => {
      // CRITICAL: the parent may be CSS-scaled (e.g. design-canvas focus
      // mode scales the artboard to fit the viewport). getBoundingClientRect
      // returns the SCALED bounds, but the cursor's own transform lives in
      // the parent's unscaled coordinate space — so we must divide the
      // pointer offset by the parent's current scale, or the cursor will
      // visually lag the mouse by a factor of (scale - 1) at the far corner.
      const r = parent.getBoundingClientRect();
      const scale = r.width / parent.offsetWidth || 1;
      tx = (e.clientX - r.left) / scale;
      ty = (e.clientY - r.top) / scale;
    };
    const loop = () => {
      // lag + jitter — heavier lerp now that the scale is correct so the
      // cursor still feels alive but doesn't drag visibly behind.
      cx += (tx - cx) * 0.45;
      cy += (ty - cy) * 0.45;
      const jx = (Math.random() - 0.5) * 6 * intensity;
      const jy = (Math.random() - 0.5) * 6 * intensity;
      el.style.transform = `translate(${cx + jx - 6}px, ${cy + jy - 6}px)`;
      el.style.opacity = String(0.35 + Math.random() * 0.3);
      raf = requestAnimationFrame(loop);
    };
    parent.addEventListener('pointermove', onMove);
    loop();
    return () => {
      parent.removeEventListener('pointermove', onMove);
      cancelAnimationFrame(raf);
    };
  }, [intensity]);
  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        top: 0, left: 0,
        width: 12, height: 12,
        borderRadius: 1,
        background: color,
        boxShadow: `0 0 8px ${color}, 0 0 14px ${color}`,
        pointerEvents: 'none',
        zIndex: 60,
        mixBlendMode: 'screen',
        willChange: 'transform',
      }}
    />
  );
}

Object.assign(window, { MatrixRain, CRTOverlay, GlitchCursor });
