// Decorative U-Net diagram. Canvas tiles are generated once per tier and
// cached; geometry and shading come from window.UNetGeo.
//
// The whole file is wrapped in an IIFE so it contributes NO top-level bindings.
// Files here share one global scope, and `const {...} = React` at top level only
// survives today because Babel-standalone's default `env` preset downlevels
// const to var. Adding data-presets="react" or data-type="module" would turn a
// duplicate destructure into a fatal redeclaration SyntaxError. The body is left
// un-indented deliberately, to keep this a two-line diff.
(function () {
const { useState, useEffect, useMemo } = React;

function makeTile(res) {
  const geo = window.UNetGeo;
  const c = document.createElement('canvas');
  c.width = c.height = res;
  const g = c.getContext('2d');
  for (let y = 0; y < res; y++) {
    for (let x = 0; x < res; x++) {
      g.fillStyle = geo.shade(geo.sceneValue((x + 0.5) / res, (y + 0.5) / res));
      g.fillRect(x, y, 1, 1);
    }
  }
  return c.toDataURL();
}

// The latent strip is accent-coloured, unlike the six node tiles, which stay
// photographic scraps on shade()'s grey ramp. Emphasis comes from the stat
// roster in stats-zoom.js rather than being duplicated here, so there is one
// place that decides which cells carry a figure. Guarded because unet.jsx must
// still render on a page that has not loaded stats-zoom.js.
function isStatCell(i) {
  const zg = window.StatsZoomGeo;
  return !!(zg && zg.statForCell(i));
}

function makeLatentTile(n) {
  const geo = window.UNetGeo;
  const c = document.createElement('canvas');
  c.width = n; c.height = 1;
  const g = c.getContext('2d');
  for (let i = 0; i < n; i++) {
    g.fillStyle = geo.latentColor(i, isStatCell(i));
    g.fillRect(i, 0, 1, 1);
  }
  return c.toDataURL();
}

function UNetDiagram({ zoom = null }) {
  const geo = window.UNetGeo;
  const zg = window.StatsZoomGeo;
  const [tier, setTier] = useState(() => geo.tierFor(window.innerWidth));

  useEffect(() => {
    const onResize = () => setTier(geo.tierFor(window.innerWidth));
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  const nodes = useMemo(() => geo.nodesFor(tier), [tier]);
  const tiles = useMemo(() => nodes.map((n) => makeTile(n.res)), [tier]);
  const skips = useMemo(() => geo.skipsFor(tier), [tier]);
  const latentLinks = useMemo(() => geo.latentLinksFor(tier), [tier]);
  const latentSrc = useMemo(() => makeLatentTile(geo.LATENT.n), []);

  // Discrete drivers, safe to read off the prop with no geo call, so they can be
  // computed before the early return and feed the memos below.
  const level = zoom ? zoom.level : 'plain';
  const L = geo.LATENT;

  // The scroll zoom re-renders this component on EVERY animation frame (stats.jsx
  // pushes a new progress value each rAF). Only two things actually change per
  // frame -- the svg's viewBox and a handful of opacities -- so everything that
  // does NOT depend on those is memoized here. Because each memo returns a stable
  // element reference between frames, React reconciles the wrapper groups (which
  // carry the changing opacity) but skips the subtrees entirely, which is what
  // keeps the deep zoom smooth: the 6 pixel tiles, 40 cells, 40 numerals and the
  // four figures are built once per tier/level, not 60 times a second.

  // Skip lines and the elbowed latent links. Static per tier; the group wrapper
  // below carries the per-frame chrome opacity, not these.
  const skipChildren = useMemo(() => ([
    skips.map((s) => <line key={`skip-${s.res}`} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} />),
    latentLinks.map((l) =>
      <polyline className="unet-latent-link" key={`lat-${l.side}`}
        points={l.points.map((p) => p.join(',')).join(' ')} />)
  ]), [skips, latentLinks]);

  // The inner content of each encoder/decoder node -- tile, ring, two labels.
  // The node <g> that wraps this (below) is what fades and drops out of the tab
  // order, so its opacity/tabIndex stay outside the memo while this heavy part,
  // including the pixelated tile <image>, is reused across frames.
  const nodeInner = useMemo(() => nodes.map((n, i) => {
    const right = n.side === 'r';
    const edge = right ? n.x + n.s : n.x;
    return [
      <image key="img" href={tiles[i]} x={n.x} y={n.y} width={n.s} height={n.s}
        imageRendering="pixelated" preserveAspectRatio="none" />,
      <rect key="ring" className="unet-ring" x={n.x - 3} y={n.y - 3} width={n.s + 6} height={n.s + 6} fill="none" />,
      <text key="lbl-ch" className="unet-lbl" x={edge} y={n.y - 12} textAnchor={right ? 'end' : 'start'}>{n.ch}</text>,
      <text key="lbl-op" className="unet-lbl" x={edge} y={n.y + n.s + 20} textAnchor={right ? 'end' : 'start'}>{n.op}</text>
    ];
  }), [nodes, tiles]);

  // The latent payload: the raster strip at the plain level, or the vector cells
  // and their numerals once the zoom is close enough to resolve them. Keyed on
  // `level` so the plain->numerals swap and the numerals' own opacity flip both
  // recompute exactly when the level changes and never on an ordinary zoom frame.
  // The .unet-cell-num opacity is a hard switch here that theme.css fades via a
  // CSS transition, so the reveal stays a fade rather than a pop.
  const latentBody = useMemo(() => {
    if (level === 'plain') {
      return (
        <image className="unet-latent-img" href={latentSrc}
          x={L.x} y={L.y} width={L.w} height={L.h}
          imageRendering="pixelated" preserveAspectRatio="none" />);
    }
    // Vector cells, so the numerals can be real text. Fill comes from the same
    // geo.latentColor(i, isStatCell(i)) the raster tile used, or the swap would
    // show a colour pop -- latentColor's hexes are baked for the canvas path and
    // so cannot be matched by a CSS variable.
    const cw = L.w / L.n;
    return (
      <g className="unet-cells">
        {Array.from({ length: L.n }, (_, i) => (
          <rect key={`cell-${i}`} className="unet-cell"
            x={L.x + i * cw} y={L.y} width={cw} height={L.h}
            fill={geo.latentColor(i, isStatCell(i))} />
        ))}
        {Array.from({ length: L.n }, (_, i) => (
          <text key={`num-${i}`} className="unet-cell-num"
            x={L.x + (i + 0.5) * cw} y={L.y + L.h / 2}
            textAnchor="middle" dominantBaseline="central"
            style={{ opacity: level === 'numerals' ? 1 : 0 }}>
            {zg ? zg.cellNumeral(i) : ''}
          </text>
        ))}
      </g>);
  }, [level, latentSrc]);

  // The four figures. Static; the .unet-stats wrapper below carries the level
  // opacity (a CSS-transitioned fade). `label` is pre-split into lines because
  // SVG text does not wrap.
  const statChildren = useMemo(() => (zg ? zg.STATS : []).map((s) => {
    const cw = L.w / L.n;
    const cx = L.x + (s.cell + 0.5) * cw;
    return (
      <g key={s.figure + s.cell}>
        <text className="unet-stat-fig" x={cx} y={L.y + L.h * 0.42}
          textAnchor="middle" dominantBaseline="central">{s.figure}</text>
        {s.label.map((line, li) => (
          <text key={line} className="unet-stat-lbl" x={cx}
            y={L.y + L.h * 0.68 + li * (L.h * 0.11)}
            textAnchor="middle" dominantBaseline="central">{line}</text>
        ))}
      </g>);
  }), []);

  // Phone tier draws nothing at all -- not even the latent bar, which on its
  // own read as a stray pixel strip rather than a bottleneck. Placed after every
  // hook on purpose: bailing earlier would change the hook order between tiers
  // and break the resize path.
  if (!geo.hasDiagram(tier)) return null;

  // With no zoom prop this renders the old diagram at the old viewBox, so every
  // other consumer of window.UNet keeps working -- but it is no longer literally
  // the old markup: the latent group now also carries the (fully transparent,
  // aria-hidden) stats layer. That layer is the only thing here that reaches for
  // window.StatsZoomGeo, and pages other than the home page do not load
  // stats-zoom.js, so every `zg` read below is guarded.
  const viewBox = zoom ? zg.rectToViewBox(zoom.rect) : geo.viewBoxFor(tier);
  const chrome = zoom ? zoom.chrome : 1;

  return (
    // yMax, not yMid: the box bottom is pinned to the frame's bottom rule (see
    // .unet in theme.css), so bottom-aligning the content hangs the latent bar
    // off that rule at a fixed offset -- viewBoxFor's LABEL_PAD, scaled.
    // Centering instead let the latent drift upward into the hero CTAs whenever
    // the box grew taller than the viewBox aspect, which is what happens at the
    // mid tier: dropping the outer tiles makes the viewBox proportionally wider.
    <svg className="unet"
      viewBox={viewBox} preserveAspectRatio="xMidYMax meet"
      role="img"
      aria-label="Decorative U-Net diagram: an image is progressively downsampled on the left, compressed to a latent vector at the bottom, then upsampled back on the right.">
      {/* First child, so the skips paint behind every tile. The wrapper carries
          the per-frame chrome opacity; its children are memoized above. The
          latent links are elbows, drawn as polylines from a geo vertex list. */}
      <g className="unet-skips" aria-hidden="true" style={{ opacity: chrome }}>
        {skipChildren}
      </g>
      {nodes.map((n, i) => (
        // tabIndex tracks the fade: once the tiles are invisible they must leave
        // the tab order too, or the zoomed hero parks a keyboard user on six
        // groups they cannot see. Only opacity/tabIndex live here -- the tile,
        // ring and labels come memoized so the zoom frame does not rebuild them.
        <g className="unet-node" key={n.ch} tabIndex={chrome < 0.5 ? -1 : 0} role="img"
          style={{ opacity: chrome, pointerEvents: chrome < 0.5 ? 'none' : undefined }}
          aria-label={`${n.op}, tensor ${n.ch}`}>
          {nodeInner[i]}
        </g>
      ))}
      <g className="unet-node" tabIndex={0} role="img"
        aria-label={`Bottleneck, ${L.op}, latent vector z`}>
        {latentBody}
        {/* The figures are rendered at EVERY level, with opacity carrying the
            reveal, so the reveal is a fade and not a pop-in.
            aria-hidden, though: this layer sits under two nested role="img"
            ancestors, and ARIA specifies Children Presentational: True for that
            role, so assistive tech never reached it in the first place. The
            accessible copy of these numbers is the visually-hidden StatsBand
            that stats.jsx renders next to this diagram; marking the SVG copy
            decorative keeps them from being announced twice wherever a browser
            does expose it. */}
        <g className="unet-stats" aria-hidden="true" style={{ opacity: level === 'figures' ? 1 : 0 }}>
          {statChildren}
        </g>
        <rect className="unet-ring" x={L.x - 3} y={L.y - 3} width={L.w + 6} height={L.h + 6} fill="none" />
        {/* No label ABOVE the strip. The channel count sat directly in the path
            the zoom travels through and read as debris the moment the frame
            started closing on the cells; L.ch survives in the aria-label only.
            The op label below is kept -- it is out of the zoom's way. */}
        <text className="unet-lbl" x={L.x + L.w / 2} y={L.y + L.h + 20} textAnchor="middle"
          style={{ opacity: chrome === 1 ? undefined : chrome }}>{L.op}</text>
      </g>
    </svg>);

}

window.UNet = UNetDiagram;
})();
