'use client';

import { useEffect, useRef } from 'react';

/**
 * HeroAnimation — full-bleed Canvas 2D piece for the marketing hero.
 *
 * Four layers (back to front):
 *   1. Drifting indigo particle dots (texture)
 *   2. Code-block panel with typewriter cycling 3 snippets (centerpiece)
 *   3. Mouse-tracking glow ring (interactive, additive)
 *   4. Data packets shooting from the panel toward the canvas edges
 *
 * Hex colors are hardcoded here on purpose — Tailwind v4 beta oklch tokens
 * don't round-trip cleanly when read from JS, and we want pixel-stable
 * brand colors inside the canvas regardless of the design-token layer.
 */

const COLORS = {
  bg: '#0a0a0b',
  bgElevated: '#111114',
  border: '#1f1f22',
  borderStrong: '#2a2a2e',
  fg: '#fafafa',
  fgMuted: '#a1a1aa',
  fgSubtle: '#71717a',
  accent: '#6366f1',
  success: '#22c55e',
} as const;

const SNIPPETS: ReadonlyArray<ReadonlyArray<string>> = [
  [
    '// MCP server — auto-generated',
    'import { Server } from "@bmm/mcp";',
    '',
    'const server = new Server({',
    '  name: "data-bridge",',
    '  tools: [getRecord, search, write],',
    '});',
    '',
    'await server.listen();',
  ],
  [
    '> Generating spec...        OK',
    '> Static checks             OK',
    '> Building image            OK 17.2s',
    '> Deploying container       OK',
    '> Live at https://...mcp.bmm.io',
  ],
  [
    '// claude_desktop_config.json',
    '{',
    '  "mcpServers": {',
    '    "data-bridge": {',
    '      "url": "https://data.mcp.bmm.io",',
    '      "auth": "oauth2"',
    '    }',
    '  }',
    '}',
  ],
];

const KEYWORDS = new Set([
  'import',
  'from',
  'const',
  'await',
  'new',
  'return',
  'function',
  'let',
  'var',
  'if',
  'else',
  'export',
  'default',
]);

interface Particle {
  x: number;
  y: number;
  vx: number;
  vy: number;
  r: number;
  seed: number;
}

interface Packet {
  x: number;
  y: number;
  vx: number;
  vy: number;
  life: number; // 0..1, decreases
  maxLife: number;
}

interface HeroAnimationProps {
  className?: string;
}

export function HeroAnimation({ className }: HeroAnimationProps) {
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const wrap = wrapRef.current;
    const canvas = canvasRef.current;
    if (!wrap || !canvas) return;

    const ctx = canvas.getContext('2d', { alpha: true });
    if (!ctx) return;

    const reducedMotion =
      typeof window !== 'undefined' &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    // ----- sizing (DPR-aware) -----
    let cssW = 0;
    let cssH = 0;
    const setSize = () => {
      const rect = wrap.getBoundingClientRect();
      cssW = Math.max(1, Math.floor(rect.width));
      cssH = Math.max(1, Math.floor(rect.height));
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = cssW * dpr;
      canvas.height = cssH * dpr;
      canvas.style.width = `${cssW}px`;
      canvas.style.height = `${cssH}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      seedParticles();
    };

    // ----- particles -----
    const particles: Particle[] = [];
    const PARTICLE_COUNT = 60;
    const seedParticles = () => {
      particles.length = 0;
      for (let i = 0; i < PARTICLE_COUNT; i++) {
        const angle = Math.random() * Math.PI * 2;
        const speed = (0.15 + Math.random() * 0.25) * (reducedMotion ? 0.4 : 1);
        particles.push({
          x: Math.random() * cssW,
          y: Math.random() * cssH,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          r: 0.7 + Math.random() * 1.3,
          seed: Math.random() * 1000,
        });
      }
    };

    // ----- pointer state -----
    // Track in CSS pixels (post setTransform), -1,-1 means "not over".
    const pointer = {
      raw: { x: -1, y: -1 },
      smooth: { x: cssW / 2, y: cssH / 2 },
      inside: false,
      ringAlpha: 0, // eased
    };

    const onPointerMove = (e: PointerEvent) => {
      const rect = canvas.getBoundingClientRect();
      pointer.raw.x = e.clientX - rect.left;
      pointer.raw.y = e.clientY - rect.top;
      pointer.inside = true;
    };
    const onPointerLeave = () => {
      pointer.inside = false;
    };
    canvas.addEventListener('pointermove', onPointerMove, { passive: true });
    canvas.addEventListener('pointerleave', onPointerLeave, { passive: true });
    canvas.addEventListener('pointercancel', onPointerLeave, { passive: true });

    // ----- packets -----
    const packets: Packet[] = [];
    let lastPacketSpawn = 0;

    // ----- typewriter state -----
    // Snippet cycle:
    //   typing -> holding -> erasing -> pausing -> next snippet
    // Timings calibrated so each snippet runs ~4-5s.
    const FORWARD_MS = reducedMotion ? 45 : 25;
    const ERASE_MS = reducedMotion ? 28 : 15;
    const HOLD_MS = 2500;
    const PAUSE_MS = 500;

    let snippetIndex = 0;
    type Phase = 'typing' | 'holding' | 'erasing' | 'pausing';
    let phase: Phase = 'typing';
    let phaseStart = 0;
    // Characters revealed across the FULL snippet text including newlines.
    const fullText = (i: number): string =>
      (SNIPPETS[i] ?? []).join('\n');

    // ----- animation loop -----
    let rafId = 0;
    let startTime = performance.now();
    let lastFrame = startTime;
    let visible = !document.hidden;

    const onVisibility = () => {
      visible = !document.hidden;
      if (visible) {
        // Reset frame delta so paused time doesn't lurch animation.
        lastFrame = performance.now();
        rafId = requestAnimationFrame(frame);
      }
    };
    document.addEventListener('visibilitychange', onVisibility);

    // ----- drawing helpers -----
    const drawParticles = (elapsed: number, dt: number) => {
      ctx.save();
      ctx.fillStyle = COLORS.accent;
      ctx.globalAlpha = 0.2;
      const driftScale = reducedMotion ? 0.4 : 1;
      for (const p of particles) {
        // Cheap pseudo-simplex: layered sines indexed by seed + time.
        const t = elapsed * 0.0004;
        const nx = Math.sin(t + p.seed) * Math.cos(t * 0.7 + p.seed * 1.3);
        const ny = Math.cos(t * 0.9 + p.seed * 0.6) * Math.sin(t * 0.5 + p.seed);
        p.x += (p.vx + nx * 0.3) * dt * 0.06 * driftScale;
        p.y += (p.vy + ny * 0.3) * dt * 0.06 * driftScale;

        // Wrap around edges so density stays uniform.
        if (p.x < -4) p.x = cssW + 4;
        else if (p.x > cssW + 4) p.x = -4;
        if (p.y < -4) p.y = cssH + 4;
        else if (p.y > cssH + 4) p.y = -4;

        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    };

    // Returns { x, y, w, h } of the code panel in CSS pixels.
    const panelRect = () => {
      const w = cssW * 0.8;
      // Sized to comfortably fit the longest snippet (~9 lines @ ~18px line-height).
      const h = Math.min(cssH * 0.55, 240);
      return {
        x: (cssW - w) / 2,
        y: (cssH - h) / 2,
        w,
        h,
      };
    };

    const drawPanel = (rect: { x: number; y: number; w: number; h: number }) => {
      ctx.save();
      ctx.fillStyle = COLORS.bg;
      ctx.strokeStyle = COLORS.border;
      ctx.lineWidth = 1;
      const r = 8;
      const { x, y, w, h } = rect;
      ctx.beginPath();
      ctx.moveTo(x + r, y);
      ctx.arcTo(x + w, y, x + w, y + h, r);
      ctx.arcTo(x + w, y + h, x, y + h, r);
      ctx.arcTo(x, y + h, x, y, r);
      ctx.arcTo(x, y, x + w, y, r);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
      ctx.restore();
    };

    // Tokenize a line for syntax color. Cheap heuristic, not a real parser.
    type Token = { text: string; color: string };
    const tokenizeLine = (line: string, elapsed: number): Token[] => {
      // Comment lines.
      if (line.trim().startsWith('//')) {
        return [{ text: line, color: COLORS.fgSubtle }];
      }
      // Log lines: ">" prefix style, flash "OK" green briefly when it appears.
      if (line.startsWith('>')) {
        const tokens: Token[] = [];
        // Find OK occurrences and color them.
        const parts = line.split(/(\bOK\b)/g);
        for (const part of parts) {
          if (part === 'OK') {
            // Subtle flash effect: brighter for first ~500ms after fully typed.
            const flash = 0.65 + 0.35 * (0.5 + 0.5 * Math.sin(elapsed * 0.005));
            tokens.push({
              text: part,
              color: withAlpha(COLORS.success, flash),
            });
          } else {
            tokens.push({ text: part, color: COLORS.fgMuted });
          }
        }
        return tokens;
      }
      // Generic JS/JSON: split on word boundaries, color keywords + strings.
      const tokens: Token[] = [];
      // Match: string literals, words, or anything else.
      const re = /("[^"]*"|\b[A-Za-z_$][\w$]*\b|[^"\w]+)/g;
      let match: RegExpExecArray | null;
      while ((match = re.exec(line)) !== null) {
        const t = match[0];
        if (t.startsWith('"')) {
          tokens.push({ text: t, color: COLORS.fgMuted });
        } else if (KEYWORDS.has(t)) {
          tokens.push({ text: t, color: COLORS.accent });
        } else {
          tokens.push({ text: t, color: COLORS.fg });
        }
      }
      return tokens;
    };

    const drawCode = (
      rect: { x: number; y: number; w: number; h: number },
      visibleText: string,
      elapsed: number,
    ) => {
      ctx.save();
      // Clip to panel so text never bleeds over the rounded border.
      ctx.beginPath();
      ctx.rect(rect.x, rect.y, rect.w, rect.h);
      ctx.clip();

      const fontSize = 12.5;
      const lineHeight = 18;
      const padX = 16;
      const padY = 14;
      ctx.font = `${fontSize}px var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace`;
      ctx.textBaseline = 'top';

      const lines = visibleText.split('\n');
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        const y = rect.y + padY + i * lineHeight;
        if (y > rect.y + rect.h - padY) break;
        let x = rect.x + padX;
        const tokens = tokenizeLine(line, elapsed);
        for (const tok of tokens) {
          ctx.fillStyle = tok.color;
          ctx.fillText(tok.text, x, y);
          x += ctx.measureText(tok.text).width;
        }
      }

      // Blinking caret at the end of the visible text.
      const lastLine = lines[lines.length - 1] ?? '';
      const caretX =
        rect.x + padX + ctx.measureText(lastLine).width;
      const caretY = rect.y + padY + (lines.length - 1) * lineHeight;
      const blink = (elapsed % 1000) < 500;
      if (blink && caretY + lineHeight <= rect.y + rect.h - padY + lineHeight) {
        ctx.fillStyle = COLORS.accent;
        ctx.fillRect(caretX + 1, caretY + 2, 6, fontSize);
      }
      ctx.restore();
    };

    const drawRing = (elapsed: number) => {
      if (pointer.ringAlpha < 0.005) return;
      ctx.save();
      ctx.globalCompositeOperation = 'lighter';
      const cx = pointer.smooth.x;
      const cy = pointer.smooth.y;
      const baseRadii: ReadonlyArray<{ r: number; w: number; a: number }> = [
        { r: 60, w: 2.4, a: 0.4 },
        { r: 90, w: 1.6, a: 0.28 },
        { r: 130, w: 1.0, a: 0.18 },
      ];
      for (const band of baseRadii) {
        ctx.beginPath();
        const segments = 96;
        for (let i = 0; i <= segments; i++) {
          const theta = (i / segments) * Math.PI * 2;
          // Breathing distortion around the circumference.
          const wobble = Math.sin(theta * 3 + elapsed * 0.002) * 4;
          const r = band.r + wobble;
          const x = cx + Math.cos(theta) * r;
          const y = cy + Math.sin(theta) * r;
          if (i === 0) ctx.moveTo(x, y);
          else ctx.lineTo(x, y);
        }
        ctx.strokeStyle = withAlpha(
          COLORS.accent,
          band.a * pointer.ringAlpha,
        );
        ctx.lineWidth = band.w;
        ctx.stroke();
      }
      ctx.restore();
    };

    const spawnPacket = (rect: { x: number; y: number; w: number; h: number }) => {
      // Pick an edge of the panel.
      const edge = Math.floor(Math.random() * 4);
      let x = 0;
      let y = 0;
      let vx = 0;
      let vy = 0;
      const speed = 0.06 + Math.random() * 0.04; // CSS px / ms
      if (edge === 0) {
        // top
        x = rect.x + Math.random() * rect.w;
        y = rect.y;
        vx = (Math.random() - 0.5) * 0.02;
        vy = -speed;
      } else if (edge === 1) {
        // right
        x = rect.x + rect.w;
        y = rect.y + Math.random() * rect.h;
        vx = speed;
        vy = (Math.random() - 0.5) * 0.02;
      } else if (edge === 2) {
        // bottom
        x = rect.x + Math.random() * rect.w;
        y = rect.y + rect.h;
        vx = (Math.random() - 0.5) * 0.02;
        vy = speed;
      } else {
        // left
        x = rect.x;
        y = rect.y + Math.random() * rect.h;
        vx = -speed;
        vy = (Math.random() - 0.5) * 0.02;
      }
      // Distance to nearest canvas edge along direction → travel time.
      const distX = vx > 0 ? cssW - x : vx < 0 ? x : Infinity;
      const distY = vy > 0 ? cssH - y : vy < 0 ? y : Infinity;
      const tx = vx !== 0 ? distX / Math.abs(vx) : Infinity;
      const ty = vy !== 0 ? distY / Math.abs(vy) : Infinity;
      const maxLife = Math.min(tx, ty);
      packets.push({ x, y, vx, vy, life: 1, maxLife });
    };

    const drawPackets = (dt: number) => {
      ctx.save();
      for (let i = packets.length - 1; i >= 0; i--) {
        const p = packets[i];
        if (!p) continue;
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.life -= dt / p.maxLife;
        if (
          p.life <= 0 ||
          p.x < -4 ||
          p.x > cssW + 4 ||
          p.y < -4 ||
          p.y > cssH + 4
        ) {
          packets.splice(i, 1);
          continue;
        }
        const alpha = Math.max(0, p.life) * 0.85;
        ctx.fillStyle = withAlpha(COLORS.accent, alpha);
        ctx.beginPath();
        ctx.arc(p.x, p.y, 1.6, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    };

    // ----- main frame -----
    const frame = (now: number) => {
      if (!visible) return; // visibilitychange will restart us
      const dt = Math.min(48, now - lastFrame); // cap to avoid lurches
      lastFrame = now;
      const elapsed = now - startTime;

      // Pointer smoothing (EMA). When outside, ease ring alpha to 0 (~400ms).
      if (pointer.inside && pointer.raw.x >= 0) {
        pointer.smooth.x = 0.82 * pointer.smooth.x + 0.18 * pointer.raw.x;
        pointer.smooth.y = 0.82 * pointer.smooth.y + 0.18 * pointer.raw.y;
        // Ease alpha to 1 over ~250ms.
        pointer.ringAlpha = Math.min(1, pointer.ringAlpha + dt / 250);
      } else {
        pointer.ringAlpha = Math.max(0, pointer.ringAlpha - dt / 400);
      }

      // Clear (transparent — wrapper supplies bg color).
      ctx.clearRect(0, 0, cssW, cssH);

      // Layer 1: particles
      drawParticles(elapsed, dt);

      // Layer 2: panel + typewriter
      const rect = panelRect();
      drawPanel(rect);

      // Compute visible characters for the current snippet.
      const text = fullText(snippetIndex);
      const totalChars = text.length;
      let visibleChars = 0;
      const sincePhase = now - phaseStart;
      if (phase === 'typing') {
        visibleChars = Math.min(totalChars, Math.floor(sincePhase / FORWARD_MS));
        if (visibleChars >= totalChars) {
          phase = 'holding';
          phaseStart = now;
        }
      } else if (phase === 'holding') {
        visibleChars = totalChars;
        if (sincePhase >= HOLD_MS) {
          phase = 'erasing';
          phaseStart = now;
        }
      } else if (phase === 'erasing') {
        visibleChars = Math.max(
          0,
          totalChars - Math.floor(sincePhase / ERASE_MS),
        );
        if (visibleChars <= 0) {
          phase = 'pausing';
          phaseStart = now;
        }
      } else {
        visibleChars = 0;
        if (sincePhase >= PAUSE_MS) {
          snippetIndex = (snippetIndex + 1) % SNIPPETS.length;
          phase = 'typing';
          phaseStart = now;
        }
      }
      drawCode(rect, text.slice(0, visibleChars), elapsed);

      // Layer 4 (drawn before ring so the glow sits on top): packets.
      // Skipped under reduced-motion.
      if (!reducedMotion) {
        if (now - lastPacketSpawn > 600 && packets.length < 6) {
          spawnPacket(rect);
          lastPacketSpawn = now;
        }
        drawPackets(dt);
      }

      // Layer 3: glow ring (additive, on top)
      drawRing(elapsed);

      rafId = requestAnimationFrame(frame);
    };

    // ----- resize observer -----
    const ro = new ResizeObserver(() => {
      setSize();
    });
    ro.observe(wrap);
    setSize();

    // Kick off.
    startTime = performance.now();
    lastFrame = startTime;
    phaseStart = startTime;
    rafId = requestAnimationFrame(frame);

    return () => {
      cancelAnimationFrame(rafId);
      ro.disconnect();
      document.removeEventListener('visibilitychange', onVisibility);
      canvas.removeEventListener('pointermove', onPointerMove);
      canvas.removeEventListener('pointerleave', onPointerLeave);
      canvas.removeEventListener('pointercancel', onPointerLeave);
    };
  }, []);

  return (
    <div
      ref={wrapRef}
      className={
        className ??
        'relative aspect-square w-full overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated]'
      }
    >
      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="block h-full w-full"
      />
    </div>
  );
}

/**
 * Append an alpha channel to a 6-digit hex color.
 * `withAlpha('#6366f1', 0.4)` -> 'rgba(99, 102, 241, 0.4)'
 */
function withAlpha(hex: string, alpha: number): string {
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
