import { useEffect, useRef, useState, useCallback } from 'react';
import * as d3 from 'd3';

interface Stage {
  title: string;
  description: string;
  explanation: string[];
  draw: (svg: d3.Selection<SVGGElement, unknown, null, undefined>, w: number, h: number) => void;
}

// ── Color tokens ──────────────────────────────────────────────
const C = {
  bg: '#09090b',       // zinc-950
  surface: '#18181b',  // zinc-900
  border: '#27272a',   // zinc-800
  muted: '#3f3f46',    // zinc-700
  dim: '#52525b',      // zinc-600
  text: '#a1a1aa',     // zinc-400
  bright: '#e4e4e7',   // zinc-200
  white: '#fafafa',    // zinc-50
  blue: '#3b82f6',
  violet: '#8b5cf6',
  amber: '#f59e0b',
  emerald: '#10b981',
  red: '#ef4444',
};

// ── Shared drawing helpers ────────────────────────────────────
function arrow(
  g: d3.Selection<SVGGElement, unknown, null, undefined>,
  x1: number, y1: number, x2: number, y2: number,
  color = C.text, strokeWidth = 1.5,
) {
  const id = `ah-${Math.random().toString(36).slice(2, 8)}`;
  g.append('defs').append('marker')
    .attr('id', id)
    .attr('viewBox', '0 0 10 10')
    .attr('refX', 9).attr('refY', 5)
    .attr('markerWidth', 6).attr('markerHeight', 6)
    .attr('orient', 'auto-start-reverse')
    .append('path').attr('d', 'M 0 0 L 10 5 L 0 10 z').attr('fill', color);

  g.append('line')
    .attr('x1', x1).attr('y1', y1).attr('x2', x2).attr('y2', y2)
    .attr('stroke', color).attr('stroke-width', strokeWidth)
    .attr('marker-end', `url(#${id})`);
}

function labelText(
  g: d3.Selection<SVGGElement, unknown, null, undefined>,
  x: number, y: number, text: string,
  opts: { size?: number; color?: string; anchor?: string; weight?: string } = {},
) {
  g.append('text')
    .attr('x', x).attr('y', y)
    .attr('text-anchor', opts.anchor ?? 'middle')
    .attr('fill', opts.color ?? C.text)
    .attr('font-size', opts.size ?? 11)
    .attr('font-family', 'ui-monospace, monospace')
    .attr('font-weight', opts.weight ?? '400')
    .text(text);
}

function roundedRect(
  g: d3.Selection<SVGGElement, unknown, null, undefined>,
  x: number, y: number, w: number, h: number,
  opts: { fill?: string; stroke?: string; rx?: number; opacity?: number } = {},
) {
  g.append('rect')
    .attr('x', x).attr('y', y).attr('width', w).attr('height', h)
    .attr('rx', opts.rx ?? 4)
    .attr('fill', opts.fill ?? 'none')
    .attr('stroke', opts.stroke ?? C.border)
    .attr('stroke-width', 1)
    .attr('opacity', opts.opacity ?? 1);
}

// ── Stage definitions ─────────────────────────────────────────

const stages: Stage[] = [

  // ──────────── 1. Input Y Plane ────────────
  {
    title: 'Input Y Plane',
    description: 'Luminance extraction from video frame',
    explanation: [
      'Each video frame is converted from RGB to YCbCr color space. We extract only the Y (luminance) channel — the brightness of each pixel, ignoring color information.',
      'Human vision is far more sensitive to luminance than to chrominance. By embedding exclusively in the Y channel, we can exploit this: small changes to brightness values are imperceptible, while the watermark survives color adjustments and chroma subsampling during compression.',
    ],
    draw(g, w, h) {
      const gridSize = 10;
      const cellSize = 28;
      const offsetX = (w - gridSize * cellSize) / 2;
      const offsetY = (h - gridSize * cellSize) / 2 + 10;

      roundedRect(g, offsetX - 16, offsetY - 16, gridSize * cellSize + 32, gridSize * cellSize + 32, {
        stroke: C.muted, rx: 8, fill: C.surface + '80',
      });

      const rng = d3.randomLcg(42);
      for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
          const val = Math.floor(rng() * 220 + 20);
          const lum = d3.interpolateGreys(1 - val / 255);
          g.append('rect')
            .attr('x', offsetX + c * cellSize)
            .attr('y', offsetY + r * cellSize)
            .attr('width', cellSize - 1).attr('height', cellSize - 1)
            .attr('fill', lum).attr('rx', 2);

          if ((r + c) % 3 === 0) {
            g.append('text')
              .attr('x', offsetX + c * cellSize + cellSize / 2)
              .attr('y', offsetY + r * cellSize + cellSize / 2 + 4)
              .attr('text-anchor', 'middle')
              .attr('fill', val > 140 ? '#000' : '#fff')
              .attr('font-size', 8)
              .attr('font-family', 'ui-monospace, monospace')
              .attr('opacity', 0.7)
              .text(val);
          }
        }
      }

      labelText(g, w / 2, offsetY - 28, 'Y (luminance) channel', { color: C.bright, size: 13, weight: '600' });

      // RGB stack → arrow → Y plane
      const arrowG = g.append('g');
      const frameLeft = offsetX - 50;
      roundedRect(arrowG, frameLeft - 30, h / 2 - 20, 24, 18, { fill: C.blue + '20', stroke: C.blue + '50', rx: 3 });
      roundedRect(arrowG, frameLeft - 28, h / 2 - 18, 24, 18, { fill: C.violet + '20', stroke: C.violet + '50', rx: 3 });
      roundedRect(arrowG, frameLeft - 26, h / 2 - 16, 24, 18, { fill: C.emerald + '20', stroke: C.emerald + '50', rx: 3 });
      labelText(arrowG, frameLeft - 14, h / 2 + 20, 'RGB', { color: C.dim, size: 9 });
      arrow(arrowG, frameLeft + 4, h / 2 - 6, offsetX - 20, h / 2 - 6, C.blue);
    },
  },

  // ──────────── 2. Haar DWT ────────────
  {
    title: 'Haar DWT',
    description: '2-level Haar discrete wavelet transform',
    explanation: [
      'The Haar wavelet transform decomposes the Y plane into frequency subbands. Each level splits the image into four quadrants: LL (approximation), LH (vertical detail), HL (horizontal detail), and HH (diagonal detail). We apply this twice, recursively subdividing the LL quadrant.',
      'We embed in the HL (horizontal detail) subband at the deepest decomposition level. HL captures horizontal edge and texture energy — changes here are masked by the image content itself. The deepest level provides maximum downsampling, concentrating energy so that each modified coefficient affects a larger spatial region, improving robustness against compression and rescaling.',
      'LH or HH could also work, but HL empirically gives the best balance of imperceptibility and robustness for typical video content with horizontal motion.',
    ],
    draw(g, w, h) {
      const size = 260;
      const ox = (w - size) / 2;
      const oy = (h - size) / 2 + 8;
      const half = size / 2;
      const quarter = size / 4;

      // Glow filter (must be defined before use)
      const defs = g.append('defs');
      const filter = defs.append('filter').attr('id', 'glow');
      filter.append('feGaussianBlur').attr('stdDeviation', 4).attr('result', 'coloredBlur');
      const merge = filter.append('feMerge');
      merge.append('feMergeNode').attr('in', 'coloredBlur');
      merge.append('feMergeNode').attr('in', 'SourceGraphic');

      // Full square
      g.append('rect')
        .attr('x', ox).attr('y', oy).attr('width', size).attr('height', size)
        .attr('fill', C.surface).attr('stroke', C.border).attr('stroke-width', 1.5).attr('rx', 4);

      // First level quadrants
      const quads = [
        { x: 0, y: 0, w: half, h: half, label: '', fill: C.muted + '30' },
        { x: half, y: 0, w: half, h: half, label: 'HL₁', fill: C.blue + '15', sub: 'horiz. detail' },
        { x: 0, y: half, w: half, h: half, label: 'LH₁', fill: C.muted + '20', sub: 'vert. detail' },
        { x: half, y: half, w: half, h: half, label: 'HH₁', fill: C.muted + '15', sub: 'diagonal' },
      ];

      quads.forEach(q => {
        g.append('rect')
          .attr('x', ox + q.x).attr('y', oy + q.y)
          .attr('width', q.w).attr('height', q.h)
          .attr('fill', q.fill).attr('stroke', C.border);
        if (q.label) {
          labelText(g, ox + q.x + q.w / 2, oy + q.y + q.h / 2 - 2, q.label, { color: C.dim, size: 12 });
          labelText(g, ox + q.x + q.w / 2, oy + q.y + q.h / 2 + 12, (q as { sub?: string }).sub ?? '', { color: C.muted, size: 8 });
        }
      });

      // Second level (subdivide LL₁)
      const sub = [
        { x: 0, y: 0, w: quarter, h: quarter, label: 'LL₂', fill: C.muted + '40', sub: 'approx.' },
        { x: quarter, y: 0, w: quarter, h: quarter, label: 'HL₂', fill: C.blue + '25', sub: '' },
        { x: 0, y: quarter, w: quarter, h: quarter, label: 'LH₂', fill: C.muted + '25', sub: '' },
        { x: quarter, y: quarter, w: quarter, h: quarter, label: 'HH₂', fill: C.muted + '20', sub: '' },
      ];

      sub.forEach(q => {
        g.append('rect')
          .attr('x', ox + q.x).attr('y', oy + q.y)
          .attr('width', q.w).attr('height', q.h)
          .attr('fill', q.fill).attr('stroke', C.border);
        labelText(g, ox + q.x + q.w / 2, oy + q.y + q.h / 2 + 4, q.label, {
          color: q.label.startsWith('HL') ? C.blue : C.dim, size: 10,
        });
      });

      // Highlight HL subbands with glow
      [{ x: half, y: 0, w: half, h: half }, { x: quarter, y: 0, w: quarter, h: quarter }].forEach(hl => {
        g.append('rect')
          .attr('x', ox + hl.x).attr('y', oy + hl.y)
          .attr('width', hl.w).attr('height', hl.h)
          .attr('fill', 'none').attr('stroke', C.blue)
          .attr('stroke-width', 2.5)
          .attr('filter', 'url(#glow)');
      });

      // Annotations
      labelText(g, ox + size + 14, oy + quarter / 2, 'embed', { color: C.blue, size: 10, anchor: 'start', weight: '600' });
      labelText(g, ox + size + 14, oy + quarter / 2 + 13, 'here', { color: C.blue, size: 10, anchor: 'start', weight: '600' });
      arrow(g, ox + size + 10, oy + quarter / 2 + 2, ox + half + half + 4, oy + quarter / 2 + 2, C.blue + '60');

      labelText(g, w / 2, oy - 16, 'Wavelet decomposition (2 levels)', { color: C.bright, size: 13, weight: '600' });

      // Level annotations on left
      labelText(g, ox - 10, oy + quarter / 2 + 4, 'L2', { color: C.dim, size: 9, anchor: 'end' });
      labelText(g, ox - 10, oy + half + half / 2 + 4, 'L1', { color: C.dim, size: 9, anchor: 'end' });
    },
  },

  // ──────────── 3. Tile Grid ────────────
  {
    title: 'Tile Grid',
    description: 'Periodic tile overlay for redundancy',
    explanation: [
      'The HL subband is partitioned into a periodic grid of equally-sized tiles. Each tile independently carries a complete copy of the entire coded payload. This is the core redundancy mechanism.',
      'If parts of the frame are cropped or heavily damaged, the surviving tiles still contain the full message. During detection, soft-bit estimates from all tiles are averaged together — more tiles means a stronger signal. For a typical 1080p frame with the "moderate" preset, this yields ~24 tiles with ~37x bit replication per tile.',
    ],
    draw(g, w, h) {
      const rectW = 420;
      const rectH = 220;
      const ox = (w - rectW) / 2;
      const oy = (h - rectH) / 2 + 4;
      const tileW = 70;
      const tileH = 55;
      const cols = Math.floor(rectW / tileW);
      const rows = Math.floor(rectH / tileH);

      // HL subband background
      g.append('rect')
        .attr('x', ox).attr('y', oy).attr('width', rectW).attr('height', rectH)
        .attr('fill', C.blue + '08').attr('stroke', C.blue + '40').attr('stroke-width', 1.5).attr('rx', 4);

      labelText(g, w / 2, oy - 18, 'HL subband tile partitioning', { color: C.bright, size: 13, weight: '600' });

      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          const tx = ox + c * tileW;
          const ty = oy + r * tileH;
          const isFilled = (r + c) % 2 === 0;

          g.append('rect')
            .attr('x', tx + 1).attr('y', ty + 1)
            .attr('width', tileW - 2).attr('height', tileH - 2)
            .attr('fill', isFilled ? C.violet + '12' : 'none')
            .attr('stroke', C.muted)
            .attr('stroke-dasharray', '3,3')
            .attr('rx', 2);

          if (isFilled) {
            labelText(g, tx + tileW / 2, ty + tileH / 2 + 3, 'P', {
              color: C.violet + '60', size: 16, weight: '700',
            });
          }
        }
      }

      // Legend — positioned clearly below with no overlap
      const ly = oy + rectH + 16;
      g.append('rect').attr('x', w / 2 - 140).attr('y', ly - 5).attr('width', 12).attr('height', 12)
        .attr('fill', C.violet + '12').attr('stroke', C.muted).attr('rx', 2);
      labelText(g, w / 2 - 122, ly + 5, 'P', { color: C.violet + '60', size: 10, anchor: 'start', weight: '700' });
      labelText(g, w / 2 - 110, ly + 5, '= full payload copy', { color: C.dim, size: 10, anchor: 'start' });

      labelText(g, w / 2 + 60, ly + 5, `${cols * rows} tiles`, { color: C.text, size: 10, anchor: 'start' });
    },
  },

  // ──────────── 4. 8×8 DCT Blocks ────────────
  {
    title: '8×8 DCT Blocks',
    description: 'Block DCT frequency decomposition',
    explanation: [
      'Within each tile, the subband coefficients are divided into non-overlapping 8x8 blocks — the same block size used by JPEG and H.264. Each block is transformed from the spatial domain into the frequency domain using the Discrete Cosine Transform (DCT).',
      'The DCT concentrates energy into a few low-frequency coefficients (top-left), while mid and high frequencies (bottom-right) carry texture detail. We embed watermark bits into mid-frequency coefficients — they carry enough energy to survive lossy compression, but are perceptually less salient than the dominant low-frequency components.',
    ],
    draw(g, w, h) {
      const tileSize = 190;
      const ox = w / 2 - tileSize - 30;
      const oy = (h - tileSize) / 2 + 14;
      const blockCount = 5;
      const blockSize = tileSize / blockCount;

      labelText(g, w / 2, oy - 20, 'One tile → 8x8 block grid → DCT', { color: C.bright, size: 13, weight: '600' });

      g.append('rect')
        .attr('x', ox).attr('y', oy).attr('width', tileSize).attr('height', tileSize)
        .attr('fill', C.surface).attr('stroke', C.border).attr('rx', 4);

      const highlightR = 1, highlightC = 2;
      for (let r = 0; r < blockCount; r++) {
        for (let c = 0; c < blockCount; c++) {
          const isHighlight = r === highlightR && c === highlightC;
          g.append('rect')
            .attr('x', ox + c * blockSize + 1)
            .attr('y', oy + r * blockSize + 1)
            .attr('width', blockSize - 2).attr('height', blockSize - 2)
            .attr('fill', isHighlight ? C.amber + '25' : 'none')
            .attr('stroke', isHighlight ? C.amber : C.muted)
            .attr('stroke-width', isHighlight ? 2 : 0.5)
            .attr('rx', 1);
        }
      }

      // Arrow to DCT heatmap
      const dctOx = w / 2 + 40;
      const dctOy = oy + 16;
      const dctSize = 160;
      arrow(g, ox + (highlightC + 1) * blockSize + 8, oy + (highlightR + 0.5) * blockSize, dctOx - 10, dctOy + dctSize / 2, C.amber);

      // Frequency-domain heatmap
      const cells = 8;
      const cs = dctSize / cells;
      g.append('rect')
        .attr('x', dctOx - 2).attr('y', dctOy - 2)
        .attr('width', dctSize + 4).attr('height', dctSize + 4)
        .attr('fill', 'none').attr('stroke', C.amber + '60').attr('rx', 4);

      const colorScale = d3.scaleSequential(d3.interpolateInferno).domain([0, 12]);
      for (let r = 0; r < cells; r++) {
        for (let c = 0; c < cells; c++) {
          const energy = Math.max(0, 10 - (r + c) * 0.9 + (Math.sin(r * c) * 1.5));
          g.append('rect')
            .attr('x', dctOx + c * cs).attr('y', dctOy + r * cs)
            .attr('width', cs - 1).attr('height', cs - 1)
            .attr('fill', colorScale(energy)).attr('rx', 1);
        }
      }

      // Frequency labels
      labelText(g, dctOx + dctSize / 2, dctOy + dctSize + 18, 'DCT coefficients', { color: C.amber, size: 10 });
      labelText(g, dctOx - 8, dctOy + 8, 'DC', { color: C.dim, size: 8, anchor: 'end' });
      labelText(g, dctOx + dctSize + 4, dctOy + dctSize - 4, 'HF', { color: C.dim, size: 8, anchor: 'start' });
      arrow(g, dctOx + dctSize * 0.15, dctOy + dctSize + 28, dctOx + dctSize * 0.85, dctOy + dctSize + 28, C.dim, 1);
      labelText(g, dctOx + dctSize / 2, dctOy + dctSize + 40, 'increasing frequency →', { color: C.muted, size: 8 });

      labelText(g, ox + tileSize / 2, oy + tileSize + 18, 'spatial domain', { color: C.dim, size: 9 });
    },
  },

  // ──────────── 5. Coefficient Selection + Masking ────────────
  {
    title: 'Coefficient Selection',
    description: 'Zigzag scan with perceptual masking',
    explanation: [
      'DCT coefficients are traversed in zigzag order — a diagonal path from low to high frequency. We select mid-frequency positions (indices ~10-25): low enough to survive quantization in lossy codecs, high enough to avoid visible artifacts in flat areas.',
      'Each block also gets a perceptual masking factor based on its AC energy relative to the tile median, using a square-root curve. Flat/smooth blocks (low energy) are barely embedded at all (factor as low as 0.1x) — any modification there would be visible. Textured and noisy blocks tolerate much stronger embedding (up to 2.0x). No blocks are skipped entirely, but flat areas contribute almost nothing while busy areas carry the bulk of the watermark signal.',
    ],
    draw(g, w, h) {
      const cells = 8;
      const cellSize = 30;
      const gridSize = cells * cellSize;
      const ox = (w - gridSize) / 2 - 80;
      const oy = (h - gridSize) / 2 + 14;

      labelText(g, w / 2, oy - 20, 'Zigzag scan + perceptual masking', { color: C.bright, size: 13, weight: '600' });

      // Zigzag order
      const zigzag: [number, number][] = [];
      for (let sum = 0; sum < 2 * cells - 1; sum++) {
        if (sum % 2 === 0) {
          for (let r = Math.min(sum, cells - 1); r >= Math.max(0, sum - cells + 1); r--) {
            zigzag.push([r, sum - r]);
          }
        } else {
          for (let r = Math.max(0, sum - cells + 1); r <= Math.min(sum, cells - 1); r++) {
            zigzag.push([r, sum - r]);
          }
        }
      }

      const midStart = 10, midEnd = 26;
      zigzag.forEach(([r, c], idx) => {
        const isMid = idx >= midStart && idx < midEnd;
        const isDc = idx === 0;
        const isLow = idx > 0 && idx < midStart;
        g.append('rect')
          .attr('x', ox + c * cellSize + 1)
          .attr('y', oy + r * cellSize + 1)
          .attr('width', cellSize - 2).attr('height', cellSize - 2)
          .attr('fill', isMid ? C.amber + '30' : isDc ? C.blue + '20' : isLow ? C.blue + '08' : C.surface)
          .attr('stroke', isMid ? C.amber + '80' : C.muted)
          .attr('stroke-width', isMid ? 1.5 : 0.5)
          .attr('rx', 2);

        if (isMid || idx < 4 || idx > 60) {
          g.append('text')
            .attr('x', ox + c * cellSize + cellSize / 2)
            .attr('y', oy + r * cellSize + cellSize / 2 + 3.5)
            .attr('text-anchor', 'middle')
            .attr('fill', isMid ? C.amber : C.dim)
            .attr('font-size', 8)
            .attr('font-family', 'ui-monospace, monospace')
            .text(idx);
        }
      });

      // Zigzag path
      const pathPoints = zigzag.slice(0, 30).map(([r, c]) => [
        ox + c * cellSize + cellSize / 2,
        oy + r * cellSize + cellSize / 2,
      ] as [number, number]);

      g.append('path')
        .attr('d', d3.line().curve(d3.curveLinear)(pathPoints)!)
        .attr('fill', 'none')
        .attr('stroke', C.text + '40')
        .attr('stroke-width', 1)
        .attr('stroke-dasharray', '3,2');

      // Legend below zigzag grid
      const ly = oy + gridSize + 14;
      g.append('rect').attr('x', ox).attr('y', ly - 4).attr('width', 12).attr('height', 12)
        .attr('fill', C.amber + '30').attr('stroke', C.amber + '80').attr('rx', 2);
      labelText(g, ox + 16, ly + 6, `positions ${midStart}–${midEnd - 1}`, { color: C.amber, size: 9, anchor: 'start' });

      // Masking diagram on the right
      const mx = ox + gridSize + 40;
      const my = oy + 10;
      const barW = 24;
      const barGap = 6;
      const maxBarH = 120;

      labelText(g, mx + 70, my - 4, 'masking factor', { color: C.bright, size: 10, weight: '600' });

      const blockTypes = [
        { label: 'flat', energy: 0.01, factor: 0.1, color: C.red },
        { label: 'smooth', energy: 0.25, factor: 0.5, color: C.blue },
        { label: 'texture', energy: 1.0, factor: 1.0, color: C.emerald },
        { label: 'noisy', energy: 4.0, factor: 2.0, color: C.amber },
      ];

      blockTypes.forEach((bt, i) => {
        const bx = mx + i * (barW + barGap);
        const barH = (bt.factor / 2.0) * maxBarH;
        const by = my + 16 + maxBarH - barH;

        // Bar
        g.append('rect')
          .attr('x', bx).attr('y', by)
          .attr('width', barW).attr('height', barH)
          .attr('fill', bt.color + '30')
          .attr('stroke', bt.color + '80')
          .attr('rx', 3);

        // Factor label
        labelText(g, bx + barW / 2, by - 6, `${bt.factor}x`, { color: bt.color, size: 9, weight: '600' });

        // Block type label
        labelText(g, bx + barW / 2, my + 16 + maxBarH + 14, bt.label, { color: C.dim, size: 8 });
      });

      // Baseline at 1.0x
      const baselineY = my + 16 + maxBarH - (1.0 / 2.0) * maxBarH;
      g.append('line')
        .attr('x1', mx - 4).attr('y1', baselineY)
        .attr('x2', mx + blockTypes.length * (barW + barGap)).attr('y2', baselineY)
        .attr('stroke', C.dim).attr('stroke-dasharray', '4,3').attr('stroke-width', 1);
      labelText(g, mx + blockTypes.length * (barW + barGap) + 4, baselineY + 4, '1.0x', { color: C.dim, size: 8, anchor: 'start' });

      labelText(g, mx + 70, my + 16 + maxBarH + 30, 'Δ_eff = Δ × factor', { color: C.muted, size: 9 });
    },
  },

  // ──────────── 6. DM-QIM Embedding ────────────
  {
    title: 'DM-QIM Embedding',
    description: 'Dithered quantization index modulation',
    explanation: [
      'To embed a single bit into a DCT coefficient, DM-QIM defines two interleaved quantization lattices spaced Δ apart. The lattice for bit=0 has points at even multiples of Δ (…, -2Δ, 0, 2Δ, …). The lattice for bit=1 is shifted by Δ/2 (…, -Δ/2, Δ/2, 3Δ/2, …).',
      'To embed: subtract a secret dither value d, quantize the shifted coefficient to the nearest point on the target bit\'s lattice, then add d back. The dither is derived from the secret key — without it, an attacker cannot determine which lattice a coefficient belongs to.',
      'During detection, we subtract d and measure which lattice the coefficient is closer to. The signed distance gives a "soft bit" — its magnitude indicates confidence. Larger Δ means more robust (bigger lattice spacing) but more visible distortion.',
    ],
    draw(g, w, h) {
      const lineY = h / 2 - 15;
      const lineLeft = 70;
      const lineRight = w - 70;
      const lineW = lineRight - lineLeft;

      labelText(g, w / 2, 30, 'Two interleaved quantization lattices', { color: C.bright, size: 13, weight: '600' });

      // Number line
      g.append('line')
        .attr('x1', lineLeft - 10).attr('y1', lineY)
        .attr('x2', lineRight + 10).attr('y2', lineY)
        .attr('stroke', C.muted).attr('stroke-width', 1.5);

      // Lattice points — bit 0 (even multiples of Δ)
      const delta = lineW / 6;
      const lattice0: number[] = [];
      const lattice1: number[] = [];

      for (let i = 0; i <= 6; i++) {
        const x = lineLeft + i * delta;
        if (i % 2 === 0) {
          lattice0.push(x);
          // Bit=0 markers (triangles pointing up)
          g.append('path')
            .attr('d', `M${x - 5},${lineY + 4} L${x},${lineY - 6} L${x + 5},${lineY + 4}Z`)
            .attr('fill', C.blue + '60').attr('stroke', C.blue).attr('stroke-width', 1.5);
        } else {
          lattice1.push(x);
          // Bit=1 markers (circles)
          g.append('circle')
            .attr('cx', x).attr('cy', lineY)
            .attr('r', 5)
            .attr('fill', C.emerald + '40').attr('stroke', C.emerald).attr('stroke-width', 1.5);
        }
      }

      // Δ bracket between first two points
      const bY = lineY + 20;
      g.append('line').attr('x1', lineLeft).attr('y1', bY).attr('x2', lineLeft + delta).attr('y2', bY)
        .attr('stroke', C.text).attr('stroke-width', 1);
      g.append('line').attr('x1', lineLeft).attr('y1', bY - 4).attr('x2', lineLeft).attr('y2', bY + 4)
        .attr('stroke', C.text).attr('stroke-width', 1);
      g.append('line').attr('x1', lineLeft + delta).attr('y1', bY - 4).attr('x2', lineLeft + delta).attr('y2', bY + 4)
        .attr('stroke', C.text).attr('stroke-width', 1);
      labelText(g, lineLeft + delta / 2, bY + 14, 'Δ', { color: C.bright, size: 11, weight: '600' });

      // Δ/2 bracket
      g.append('line').attr('x1', lineLeft).attr('y1', bY + 28).attr('x2', lineLeft + delta / 2).attr('y2', bY + 28)
        .attr('stroke', C.dim).attr('stroke-width', 1);
      g.append('line').attr('x1', lineLeft).attr('y1', bY + 24).attr('x2', lineLeft).attr('y2', bY + 32)
        .attr('stroke', C.dim).attr('stroke-width', 1);
      g.append('line').attr('x1', lineLeft + delta / 2).attr('y1', bY + 24).attr('x2', lineLeft + delta / 2).attr('y2', bY + 32)
        .attr('stroke', C.dim).attr('stroke-width', 1);
      labelText(g, lineLeft + delta / 4, bY + 42, 'Δ/2', { color: C.dim, size: 10 });

      // Original coefficient and snap animation
      const origX = lineLeft + 2.3 * delta;
      const snapTarget = lineLeft + 2 * delta; // snap to nearest bit=0 point (even)
      const snapTarget1 = lineLeft + 3 * delta; // or bit=1 point (odd)

      // Original value
      g.append('line')
        .attr('x1', origX).attr('y1', lineY - 40)
        .attr('x2', origX).attr('y2', lineY - 10)
        .attr('stroke', C.amber).attr('stroke-width', 1.5)
        .attr('stroke-dasharray', '3,2');
      g.append('circle')
        .attr('cx', origX).attr('cy', lineY - 44)
        .attr('r', 3).attr('fill', C.amber);
      labelText(g, origX, lineY - 54, 'c (original)', { color: C.amber, size: 9 });

      // Snap arrows — show both options
      // To bit=0
      arrow(g, origX - 4, lineY - 28, snapTarget + 6, lineY - 10, C.blue, 1.5);
      labelText(g, (origX + snapTarget) / 2 - 14, lineY - 36, 'if bit=0', { color: C.blue, size: 8 });

      // To bit=1
      arrow(g, origX + 4, lineY - 28, snapTarget1 - 6, lineY - 10, C.emerald, 1.5);
      labelText(g, (origX + snapTarget1) / 2 + 14, lineY - 36, 'if bit=1', { color: C.emerald, size: 8 });

      // Legend
      const ly = lineY + 62;
      // Bit 0
      g.append('path')
        .attr('d', `M${w / 2 - 120 - 5},${ly + 4} L${w / 2 - 120},${ly - 4} L${w / 2 - 115},${ly + 4}Z`)
        .attr('fill', C.blue + '60').attr('stroke', C.blue).attr('stroke-width', 1);
      labelText(g, w / 2 - 105, ly + 3, 'bit = 0 lattice (even·Δ)', { color: C.blue, size: 9, anchor: 'start' });
      // Bit 1
      g.append('circle').attr('cx', w / 2 + 50).attr('cy', ly).attr('r', 4)
        .attr('fill', C.emerald + '40').attr('stroke', C.emerald);
      labelText(g, w / 2 + 60, ly + 3, 'bit = 1 lattice (odd·Δ + Δ/2)', { color: C.emerald, size: 9, anchor: 'start' });

      // Dither note
      labelText(g, w / 2, ly + 24, 'Dither d (from secret key) shifts both lattices — prevents unauthorized detection', { color: C.muted, size: 9 });
    },
  },

  // ──────────── 7. Payload Encoding ────────────
  {
    title: 'Payload Encoding',
    description: 'Error-correcting payload encoding',
    explanation: [
      'The raw 32-bit payload (e.g. 0xDEADBEEF) is first protected with a 4-bit CRC checksum for integrity verification, expanding to 36 bits. Then BCH(63,36,5) error-correcting coding adds parity bits, yielding 63 coded bits that can correct up to 5 bit errors.',
      'Finally, the 63 bits are pseudo-randomly interleaved using a key-derived permutation. This spreads burst errors (from localized damage) across the codeword, converting them into scattered errors that BCH can correct more effectively.',
    ],
    draw(g, w, h) {
      const cy = h / 2;
      const steps = [
        { label: '32-bit', label2: 'payload', bits: '32', color: C.blue, desc: '0xDEADBEEF' },
        { label: 'CRC-4', label2: 'append', bits: '36', color: C.violet, desc: '+4 check bits' },
        { label: 'BCH', label2: '(63,36,t=5)', bits: '63', color: C.emerald, desc: 'correct 5 errors' },
        { label: 'Keyed', label2: 'interleave', bits: '63', color: C.amber, desc: 'spread burst errors' },
      ];
      const pad = 30;
      const availW = w - pad * 2;
      const gap = 30;
      const boxW = Math.min(110, (availW - (steps.length - 1) * gap) / steps.length);
      const totalW = steps.length * boxW + (steps.length - 1) * gap;
      const startX = (w - totalW) / 2 + boxW / 2;

      labelText(g, w / 2, 36, 'Payload encoding pipeline', { color: C.bright, size: 13, weight: '600' });

      steps.forEach((step, i) => {
        const cx = startX + i * (boxW + gap);

        roundedRect(g, cx - boxW / 2, cy - 36, boxW, 72, {
          fill: step.color + '12', stroke: step.color + '50', rx: 8,
        });

        labelText(g, cx, cy - 14, step.label, { color: C.bright, size: 12, weight: '600' });
        labelText(g, cx, cy + 2, step.label2, { color: C.bright, size: 11, weight: '600' });
        labelText(g, cx, cy + 18, step.desc, { color: C.dim, size: 8 });

        // Bit count badge
        g.append('rect')
          .attr('x', cx - 18).attr('y', cy + 40)
          .attr('width', 36).attr('height', 18)
          .attr('fill', step.color + '20')
          .attr('stroke', step.color + '40')
          .attr('rx', 9);
        labelText(g, cx, cy + 52, `${step.bits} bits`, { color: step.color, size: 8, weight: '600' });

        if (i < steps.length - 1) {
          arrow(g, cx + boxW / 2 + 4, cy, cx + boxW / 2 + gap - 4, cy, step.color + 'a0', 1.5);
        }
      });
    },
  },

  // ──────────── 8. Reconstruction ────────────
  {
    title: 'Reconstruction',
    description: 'Inverse transform back to pixel domain',
    explanation: [
      'After embedding modifies DCT coefficients within the HL subband, we reverse the transforms to reconstruct a full video frame. Inverse DCT converts frequency-domain blocks back to spatial samples. Inverse DWT recombines all subbands (including the modified HL) into the full-resolution Y plane.',
      'The resulting watermarked Y plane is nearly identical to the original — typical PSNR is above 38 dB (the "moderate" preset), meaning the per-pixel difference is invisible to the human eye.',
    ],
    draw(g, w, h) {
      const cy = h * 0.32;
      // Correct direction: embedded coefficients → iDCT → write HL → iDWT → watermarked Y
      const steps = [
        { label: 'Modified', label2: 'DCT coeffs', color: C.amber },
        { label: 'iDCT →', label2: 'write HL', color: C.emerald },
        { label: 'inverse', label2: 'DWT', color: C.blue },
        { label: 'Watermarked', label2: 'Y plane', color: C.violet },
      ];
      const boxW = 110;
      const gap = 34;
      const totalW = steps.length * boxW + (steps.length - 1) * gap;
      const startX = (w - totalW) / 2 + boxW / 2;

      labelText(g, w / 2, 30, 'Reconstruction: frequency → spatial', { color: C.bright, size: 13, weight: '600' });

      steps.forEach((step, i) => {
        const cx = startX + i * (boxW + gap);

        roundedRect(g, cx - boxW / 2, cy - 28, boxW, 56, {
          fill: step.color + '12', stroke: step.color + '50', rx: 8,
        });

        labelText(g, cx, cy - 6, step.label, { color: C.bright, size: 11, weight: '600' });
        labelText(g, cx, cy + 10, step.label2, { color: C.bright, size: 11, weight: '600' });

        // Step number
        g.append('circle')
          .attr('cx', cx - boxW / 2 + 10).attr('cy', cy - 28 + 10)
          .attr('r', 8).attr('fill', step.color + '30').attr('stroke', step.color + '60');
        labelText(g, cx - boxW / 2 + 10, cy - 28 + 13, `${i + 1}`, { color: step.color, size: 8, weight: '700' });

        if (i < steps.length - 1) {
          arrow(g, cx + boxW / 2 + 4, cy, cx + boxW / 2 + gap - 4, cy, C.dim + 'a0', 1.5);
        }
      });

      // Before/after comparison — centered below with generous spacing
      const compY = cy + 66;
      const compSize = 80;
      const compGap = 70;
      const compOx = w / 2 - compSize - compGap / 2;
      const cells = 8;
      const cs = compSize / cells;

      const rng1 = d3.randomLcg(42);
      for (let r = 0; r < cells; r++) {
        for (let c = 0; c < cells; c++) {
          const val = Math.floor(rng1() * 200 + 30);
          g.append('rect')
            .attr('x', compOx + c * cs).attr('y', compY + r * cs)
            .attr('width', cs - 0.5).attr('height', cs - 0.5)
            .attr('fill', d3.interpolateGreys(1 - val / 255)).attr('rx', 1);
        }
      }
      labelText(g, compOx + compSize / 2, compY - 8, 'original', { color: C.dim, size: 9 });

      const compOx2 = w / 2 + compGap / 2;
      const rng2 = d3.randomLcg(42);
      for (let r = 0; r < cells; r++) {
        for (let c = 0; c < cells; c++) {
          const val = Math.floor(rng2() * 200 + 30) + (Math.random() > 0.6 ? 1 : 0);
          g.append('rect')
            .attr('x', compOx2 + c * cs).attr('y', compY + r * cs)
            .attr('width', cs - 0.5).attr('height', cs - 0.5)
            .attr('fill', d3.interpolateGreys(1 - Math.min(255, val) / 255)).attr('rx', 1);
        }
      }
      labelText(g, compOx2 + compSize / 2, compY - 8, 'watermarked', { color: C.violet, size: 9 });

      labelText(g, w / 2, compY + compSize / 2 + 4, '≈', { color: C.emerald, size: 22, weight: '700' });
      labelText(g, w / 2, compY + compSize + 16, 'PSNR > 38 dB — imperceptible', { color: C.dim, size: 9 });
    },
  },

  // ──────────── 9. Detection ────────────
  {
    title: 'Detection',
    description: 'Multi-frame soft detection and decoding',
    explanation: [
      'Detection reverses the embedding path but never needs the original frame. Each frame is independently DWT-transformed, tiles are located in the HL subband, and DCT + DM-QIM soft extraction produces a signed confidence value per bit per tile.',
      'The real power comes from sheer repetition. Each of the 63 coded bits is embedded into ~12 mid-frequency coefficients per block, across ~196 blocks per tile, across ~24 tiles per frame, across multiple frames. That\'s thousands of independent readings per bit. By the law of large numbers, averaging this many noisy soft-bit estimates drives the error rate exponentially toward zero — even when each individual reading is heavily corrupted by compression or noise.',
      'After averaging, the combined soft bits are de-interleaved, BCH-decoded (correcting any residual errors), and CRC-verified. The combination of massive statistical redundancy from tiling + frames, algebraic error correction from BCH, and integrity checking from CRC makes the system robust even under severe degradation.',
    ],
    draw(g, w, h) {
      const pad = 20; // horizontal padding from edges
      labelText(g, w / 2, 28, 'Multi-frame detection pipeline', { color: C.bright, size: 13, weight: '600' });

      // Frame icons — vertically use top third
      const frameY = 56;
      const frameCount = 5;
      const frameW = 44;
      const frameH = 32;
      const frameGap = 16;
      const framesW = frameCount * frameW + (frameCount - 1) * frameGap;
      const framesOx = (w - framesW) / 2;

      for (let i = 0; i < frameCount; i++) {
        const fx = framesOx + i * (frameW + frameGap);
        roundedRect(g, fx, frameY, frameW, frameH, {
          fill: C.blue + '15', stroke: C.blue + '50', rx: 4,
        });
        for (let r = 0; r < 3; r++) {
          for (let c = 0; c < 4; c++) {
            g.append('rect')
              .attr('x', fx + 4 + c * 9).attr('y', frameY + 4 + r * 8)
              .attr('width', 7).attr('height', 6)
              .attr('fill', C.blue + (10 + Math.floor(Math.random() * 20)).toString(16))
              .attr('rx', 1);
          }
        }
        labelText(g, fx + frameW / 2, frameY + frameH + 13, `f${i + 1}`, { color: C.dim, size: 8 });
      }

      labelText(g, w / 2, frameY + frameH + 28, 'each: DWT → HL → DCT → DM-QIM soft extract', { color: C.muted, size: 8 });

      // Funnel arrows — use proportional spacing
      const combineX = w / 2;
      const combineY = frameY + frameH + 56;

      for (let i = 0; i < frameCount; i++) {
        const fx = framesOx + i * (frameW + frameGap) + frameW / 2;
        arrow(g, fx, frameY + frameH + 32, combineX + (i - 2) * 6, combineY - 10, C.blue + '50');
      }

      // Soft-combine box
      roundedRect(g, combineX - 75, combineY - 8, 150, 30, {
        fill: C.blue + '15', stroke: C.blue + '50', rx: 6,
      });
      labelText(g, combineX, combineY + 11, 'average soft bits', { color: C.blue, size: 10, weight: '600' });

      // Pipeline — use available width with padding
      const pipeY = combineY + 52;
      const pipeSteps = [
        { label: 'De-interleave', color: C.amber },
        { label: 'BCH decode', color: C.emerald },
        { label: 'CRC verify', color: C.violet },
      ];
      const availW = w - pad * 2;
      const pStepW = Math.min(120, (availW - 60) / 3);
      const pGap = (availW - pipeSteps.length * pStepW) / (pipeSteps.length - 1);
      const pStartX = pad + pStepW / 2;

      arrow(g, combineX, combineY + 22, pStartX, pipeY - 12, C.blue + '60');

      pipeSteps.forEach((step, i) => {
        const px = pStartX + i * (pStepW + pGap);
        roundedRect(g, px - pStepW / 2, pipeY - 14, pStepW, 28, {
          fill: step.color + '12', stroke: step.color + '50', rx: 6,
        });
        labelText(g, px, pipeY + 4, step.label, { color: step.color, size: 10, weight: '600' });

        if (i < pipeSteps.length - 1) {
          arrow(g, px + pStepW / 2 + 2, pipeY, px + pStepW / 2 + pGap - 2, pipeY, step.color + '80');
        }
      });

      // Result box — generous spacing below
      const outY = pipeY + 38;
      const lastPipeX = pStartX + (pipeSteps.length - 1) * (pStepW + pGap);
      arrow(g, lastPipeX, pipeY + 14, w / 2, outY, C.violet + '80');

      const resultW = 220;
      roundedRect(g, w / 2 - resultW / 2, outY + 4, resultW, 58, {
        fill: C.emerald + '08', stroke: C.emerald + '40', rx: 8,
      });
      labelText(g, w / 2, outY + 22, 'payload: 0xDEADBEEF', { color: C.emerald, size: 11, weight: '600' });
      labelText(g, w / 2, outY + 38, 'confidence: 0.97', { color: C.dim, size: 9 });
      labelText(g, w / 2, outY + 52, '12 / 16 tiles decoded · CRC ✓', { color: C.dim, size: 9 });
    },
  },
];

// ── Main component ────────────────────────────────────────────

export default function HowItWorks() {
  const svgRef = useRef<SVGSVGElement>(null);
  const [stage, setStage] = useState(0);
  const [svgDims, setSvgDims] = useState({ w: 800, h: 470 });

  const total = stages.length;

  const prev = useCallback(() => setStage(s => Math.max(0, s - 1)), []);
  const next = useCallback(() => setStage(s => Math.min(total - 1, s + 1)), [total]);

  // Keyboard navigation
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'ArrowLeft') prev();
      if (e.key === 'ArrowRight') next();
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [prev, next]);

  // Responsive sizing
  useEffect(() => {
    const el = svgRef.current?.parentElement;
    if (!el) return;
    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        const w = Math.min(entry.contentRect.width, 860);
        setSvgDims({ w, h: Math.max(400, Math.min(500, w * 0.58)) });
      }
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);

  // D3 render
  useEffect(() => {
    const svg = d3.select(svgRef.current);
    if (!svg.node()) return;

    svg.selectAll('*').remove();

    const { w, h } = svgDims;
    svg.attr('width', w).attr('height', h)
      .attr('viewBox', `0 0 ${w} ${h}`);

    const root = svg.append('g');

    root.attr('opacity', 0)
      .transition()
      .duration(350)
      .ease(d3.easeCubicOut)
      .attr('opacity', 1);

    stages[stage].draw(root as unknown as d3.Selection<SVGGElement, unknown, null, undefined>, w, h);
  }, [stage, svgDims]);

  const current = stages[stage];

  return (
    <div className="space-y-6">
      {/* Stage header */}
      <div className="text-center">
        <span className="inline-block rounded-full bg-blue-500/10 px-3 py-0.5 text-[11px] font-semibold tracking-wide text-blue-400 ring-1 ring-blue-500/20">
          {stage + 1} / {total}
        </span>
        <h3 className="mt-3 text-xl font-bold tracking-tight text-zinc-100">
          {current.title}
        </h3>
        <p className="mt-1 text-sm text-zinc-500">{current.description}</p>
      </div>

      {/* SVG canvas */}
      <div className="flex justify-center overflow-hidden rounded-xl bg-zinc-900/60 ring-1 ring-zinc-800/50">
        <svg ref={svgRef} className="block" />
      </div>

      {/* Explanation text */}
      <div className="space-y-2 rounded-lg bg-zinc-900/40 px-4 py-3 ring-1 ring-zinc-800/40">
        {current.explanation.map((para, i) => (
          <p key={i} className="text-xs leading-relaxed text-zinc-400">
            {para}
          </p>
        ))}
      </div>

      {/* Navigation */}
      <div className="flex items-center justify-between">
        <button
          onClick={prev}
          disabled={stage === 0}
          className="flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium text-zinc-400 transition-all hover:bg-zinc-800 hover:text-zinc-200 disabled:pointer-events-none disabled:opacity-30"
        >
          <span className="text-xs">←</span> Prev
        </button>

        {/* Dots */}
        <div className="flex gap-1.5">
          {stages.map((_, i) => (
            <button
              key={i}
              onClick={() => setStage(i)}
              className={`h-2 rounded-full transition-all ${
                i === stage
                  ? 'w-6 bg-blue-500'
                  : 'w-2 bg-zinc-700 hover:bg-zinc-500'
              }`}
              aria-label={`Stage ${i + 1}`}
            />
          ))}
        </div>

        <button
          onClick={next}
          disabled={stage === total - 1}
          className="flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium text-zinc-400 transition-all hover:bg-zinc-800 hover:text-zinc-200 disabled:pointer-events-none disabled:opacity-30"
        >
          Next <span className="text-xs">→</span>
        </button>
      </div>
    </div>
  );
}
