// Poster.jsx — renders a poster at exact mm dimensions, with layouts/patterns.
// Smart contrast: hero text recolors per-region where it overlaps a same-colored
// pattern shape, swapping to the OTHER pattern color (not changing the pattern itself).

const { useMemo } = React;

// --- Color helpers ----------------------------------------------------------

const _ctx = (() => {
  if (typeof document === "undefined") return null;
  const c = document.createElement("canvas");
  c.width = c.height = 1;
  return c.getContext("2d");
})();
function parseColor(c) {
  if (!_ctx) return { r: 0, g: 0, b: 0 };
  _ctx.clearRect(0, 0, 1, 1);
  _ctx.fillStyle = "#000";
  _ctx.fillStyle = c;
  _ctx.fillRect(0, 0, 1, 1);
  const d = _ctx.getImageData(0, 0, 1, 1).data;
  return { r: d[0], g: d[1], b: d[2] };
}
function colorDistance(a, b) {
  const ca = parseColor(a), cb = parseColor(b);
  return Math.sqrt((ca.r - cb.r) ** 2 + (ca.g - cb.g) ** 2 + (ca.b - cb.b) ** 2);
}
function colorsMatch(a, b) {
  return colorDistance(a, b) < 32;
}

// --- Pattern shape generators ------------------------------------------------
// Each returns { shapes: [{type, color, attrs}], cssNode? } in mm-space coords.

function arcShapes({ scale, density, palette, w, h, originX }) {
  const s = Math.max(2, scale);
  const d = Math.max(0, Math.min(100, density));
  const ox = typeof originX === "number" ? originX : 50;
  const cx = w * (ox / 100);
  const cy = h * 1.05;
  // Reach always covers the whole poster — density controls ring count.
  const maxR = Math.hypot(w, h) * 1.3;
  // Spacing between rings: low density = wide spacing (sparse), high density = tight spacing.
  // At d=0 spacing = scale * 3, at d=100 spacing = scale * 0.5.
  const ringSpacing = Math.max(2, s * (3 - (d / 100) * 2.5));
  const ringCount = Math.max(2, Math.round(maxR / ringSpacing));
  const colorA = palette.muted;
  const colorB = palette.highlight || palette.muted;
  const shapes = [];
  for (let i = ringCount; i >= 1; i--) {
    const r = (i / ringCount) * maxR;
    const color = i % 2 === 0 ? colorA : colorB;
    shapes.push({ type: "circle", color, attrs: { cx, cy, r } });
  }
  return { shapes, colors: [colorA, colorB] };
}

function rayShapes({ scale, density, palette, w, h, originX }) {
  const s = Math.max(2, scale);
  const d = Math.max(0, Math.min(100, density));
  const ox = typeof originX === "number" ? originX : 50;
  const cx = w * (ox / 100);
  const cy = h;
  // Density now controls wedge count (more density = more, narrower wedges).
  // Scale still nudges the base wedge width.
  const baseWedge = Math.max(4, Math.min(40, 90 - s * 0.6));
  // Map density 0..100 → wedge angle multiplier from 2.5x (sparse) down to 0.5x (dense).
  const wedgeAngle = baseWedge * (2.5 - (d / 100) * 2);
  const count = Math.max(4, Math.round(180 / wedgeAngle));
  // Reach always covers the whole poster.
  const R = Math.hypot(w, h) * 1.4;
  const colorA = palette.muted;
  const colorB = palette.highlight || palette.muted;
  const shapes = [];
  for (let i = 0; i < count; i++) {
    const a0 = (Math.PI * i) / count - Math.PI;
    const a1 = (Math.PI * (i + 1)) / count - Math.PI;
    const x0 = cx + R * Math.cos(a0);
    const y0 = cy + R * Math.sin(a0);
    const x1 = cx + R * Math.cos(a1);
    const y1 = cy + R * Math.sin(a1);
    const color = i % 2 === 0 ? colorA : colorB;
    shapes.push({ type: "path", color, attrs: { d: `M ${cx} ${cy} L ${x0} ${y0} L ${x1} ${y1} Z` } });
  }
  return { shapes, colors: [colorA, colorB] };
}

// Horizontal bands — shape-based (smart contrast compatible), alternating muted/highlight
function bandShapes({ scale, density, palette, w, h }) {
  const s = Math.max(2, scale);
  const d = Math.max(0, Math.min(100, density));
  // Low density = tall sparse bands, high density = tight bands (same logic as arcs)
  const bandH = Math.max(2, s * (3 - (d / 100) * 2.5));
  const count = Math.max(2, Math.ceil(h / bandH));
  const colorA = palette.muted;
  const colorB = palette.highlight || palette.muted;
  const shapes = [];
  for (let i = 0; i < count; i++) {
    const y0 = i * bandH;
    const y1 = Math.min(h, (i + 1) * bandH);
    const color = i % 2 === 0 ? colorA : colorB;
    shapes.push({ type: "path", color, attrs: { d: `M 0 ${y0} H ${w} V ${y1} H 0 Z` } });
  }
  return { shapes, colors: [colorA, colorB] };
}

// CSS-based patterns (no shape list — smartContrast won't activate for these)
function cssPatternNode(pattern, { scale, density, palette, w, h, thickness }) {
  const s = Math.max(2, scale);
  const d = Math.max(0, Math.min(100, density));
  const fg = palette.muted;
  const opacity = 0.5 + (d / 100) * 0.5;
  const common = { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0 };

  if (pattern === "stripes") {
    const stripe = s / 2;
    return <div style={{ ...common, background: `repeating-linear-gradient(45deg, ${fg} 0 ${stripe}mm, transparent ${stripe}mm ${s}mm)`, opacity: 0.18 + d / 300 }} />;
  }
  if (pattern === "dots") {
    const dot = Math.max(0.5, (s * (20 + d * 0.6)) / 100);
    return (
      <svg style={common} width="100%" height="100%" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <pattern id="dots-pat" width={`${s}mm`} height={`${s}mm`} patternUnits="userSpaceOnUse">
            <circle cx={`${s / 2}mm`} cy={`${s / 2}mm`} r={`${dot}mm`} fill={fg} />
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#dots-pat)" opacity={opacity * 0.6} />
      </svg>
    );
  }
  if (pattern === "grid") {
    return <div style={{ ...common, backgroundImage: `linear-gradient(${fg} 1px, transparent 1px), linear-gradient(90deg, ${fg} 1px, transparent 1px)`, backgroundSize: `${s}mm ${s}mm`, opacity: 0.18 + d / 400 }} />;
  }
  if (pattern === "checker") {
    return <div style={{ ...common, backgroundImage: `linear-gradient(45deg, ${fg} 25%, transparent 25%),linear-gradient(-45deg, ${fg} 25%, transparent 25%),linear-gradient(45deg, transparent 75%, ${fg} 75%),linear-gradient(-45deg, transparent 75%, ${fg} 75%)`, backgroundSize: `${s}mm ${s}mm`, backgroundPosition: `0 0, 0 ${s / 2}mm, ${s / 2}mm -${s / 2}mm, -${s / 2}mm 0`, opacity: 0.12 + d / 500 }} />;
  }
  if (pattern === "halftone") {
    // Dot grid where dot size shrinks from top to bottom
    const rows = Math.max(3, Math.round(h / s));
    const cols = Math.max(3, Math.round(w / s));
    const maxR = (s / 2) * (0.35 + (d / 100) * 0.6);
    const dots = [];
    for (let r = 0; r < rows; r++) {
      const frac = rows > 1 ? r / (rows - 1) : 0;
      const radius = Math.max(0.2, maxR * (1 - frac * 0.85));
      for (let c = 0; c <= cols; c++) {
        const x = c * s + (r % 2 === 0 ? 0 : s / 2);
        dots.push(<circle key={`${r}-${c}`} cx={x} cy={r * s + s / 2} r={radius} fill={fg} />);
      }
    }
    return (
      <svg style={common} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <g opacity={0.5 + (d / 100) * 0.4}>{dots}</g>
      </svg>
    );
  }
  if (pattern === "scales") {
    // Overlapping fish-scale arcs
    const sw = Math.max(0.2, 0.3 + (d / 100) * 1.4);
    return (
      <svg style={common} width="100%" height="100%" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <pattern id="scales-pat" width={`${s}mm`} height={`${s}mm`} patternUnits="userSpaceOnUse" viewBox="0 0 10 10">
            <circle cx="5" cy="0" r="5" fill="none" stroke={fg} strokeWidth={sw} />
            <circle cx="0" cy="5" r="5" fill="none" stroke={fg} strokeWidth={sw} />
            <circle cx="10" cy="5" r="5" fill="none" stroke={fg} strokeWidth={sw} />
            <circle cx="5" cy="10" r="5" fill="none" stroke={fg} strokeWidth={sw} />
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#scales-pat)" opacity={0.4 + (d / 100) * 0.4} />
      </svg>
    );
  }
  if (pattern === "triangles") {
    return (
      <svg style={common} width="100%" height="100%" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <pattern id="tri-pat" width={`${s * 2}mm`} height={`${s}mm`} patternUnits="userSpaceOnUse" viewBox="0 0 20 10">
            <path d="M 0 10 L 5 0 L 10 10 Z" fill={fg} />
            <path d="M 10 0 L 15 10 L 20 0 Z" fill={fg} />
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#tri-pat)" opacity={0.12 + (d / 100) * 0.35} />
      </svg>
    );
  }
  if (pattern === "crosses") {
    const t = 1.2 + (d / 100) * 1.6; // arm thickness in 0..10 tile space
    const a = 5 - t / 2;
    const b = 5 + t / 2;
    const inset = 1.5;
    return (
      <svg style={common} width="100%" height="100%" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <pattern id="cross-pat" width={`${s}mm`} height={`${s}mm`} patternUnits="userSpaceOnUse" viewBox="0 0 10 10">
            <path d={`M ${a} ${inset} H ${b} V ${a} H ${10 - inset} V ${b} H ${b} V ${10 - inset} H ${a} V ${b} H ${inset} V ${a} H ${a} Z`} fill={fg} />
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#cross-pat)" opacity={0.25 + (d / 100) * 0.4} />
      </svg>
    );
  }
  if (pattern === "aura") {
    // Soft atmospheric two-glow gradient. Scale moves the glows, density sets strength.
    const a = Math.round(0x28 + (d / 100) * 0x50).toString(16).padStart(2, "0"); // 28..78 alpha
    const b = Math.round(0x18 + (d / 100) * 0x40).toString(16).padStart(2, "0");
    const ox = 50 + Math.min(40, s - 30); // scale slider nudges glow position
    return (
      <div style={{
        ...common,
        background: `radial-gradient(90% 70% at ${ox}% 12%, ${palette.highlight}${a}, transparent 70%),` +
          `radial-gradient(80% 60% at ${100 - ox}% 88%, ${palette.accent}${b}, transparent 70%),` +
          `radial-gradient(60% 50% at 50% 50%, ${palette.muted}${b}, transparent 75%)`,
      }} />
    );
  }
  if (pattern === "wave") {
    const amp = s * 0.6;
    const period = s * 2;
    const sw = typeof thickness === "number" ? thickness : 0.5;
    const lines = Math.max(3, Math.round(h / s));
    const paths = [];
    for (let i = 0; i < lines; i++) {
      const y = (i / lines) * h + s / 2;
      let dStr = `M 0 ${y}`;
      for (let x = 0; x <= w; x += period) dStr += ` q ${period / 2} ${-amp} ${period} 0`;
      paths.push(<path key={i} d={dStr} stroke={fg} strokeWidth={sw} fill="none" />);
    }
    return (
      <svg style={common} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        <g opacity={0.3 + d / 300}>{paths}</g>
      </svg>
    );
  }
  return null;
}

// Build pattern data: { node, shapes, colors } — shapes only for shape-based patterns.
function buildPattern({ pattern, scale, density, palette, w, h, originX, thickness }) {
  if (pattern === "none") return { node: null, shapes: null, colors: null };
  if (pattern === "arcs") {
    const { shapes, colors } = arcShapes({ scale, density, palette, w, h, originX });
    const node = (
      <svg style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0 }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        {shapes.map((sh, i) => <circle key={i} cx={sh.attrs.cx} cy={sh.attrs.cy} r={sh.attrs.r} fill={sh.color} />)}
      </svg>
    );
    return { node, shapes, colors };
  }
  if (pattern === "rays") {
    const { shapes, colors } = rayShapes({ scale, density, palette, w, h, originX });
    const node = (
      <svg style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0 }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        {shapes.map((sh, i) => <path key={i} d={sh.attrs.d} fill={sh.color} />)}
      </svg>
    );
    return { node, shapes, colors };
  }
  if (pattern === "bands") {
    const { shapes, colors } = bandShapes({ scale, density, palette, w, h });
    const node = (
      <svg style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0 }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
        {shapes.map((sh, i) => <path key={i} d={sh.attrs.d} fill={sh.color} />)}
      </svg>
    );
    return { node, shapes, colors };
  }
  return { node: cssPatternNode(pattern, { scale, density, palette, w, h, thickness }), shapes: null, colors: null };
}

// --- Smart hero text wrapper -------------------------------------------------
// Strategy: render the hero TWICE as overlapping absolutely-positioned divs.
//  - Base layer (heroColor): masked so it's HIDDEN where matching shapes are.
//  - Top layer (contrastColor): masked so it's only VISIBLE where matching shapes are.
// Masks are inline SVG data URLs applied via CSS `mask-image`. Both layers
// occupy the same coordinate box (the poster), so the hero text stays in
// position and unbroken.

// Builds a luminance mask in SVG. Walks ALL pattern shapes in z-order (back to
// front), painting matching-color shapes as `fg` and non-matching shapes as `bg`.
// This way the mask reflects only the *visible* (top-most) matching regions —
// e.g. a large green arc that's partially covered by a smaller orange arc only
// contributes the visible green ring, not the whole disc.
function buildMaskDataUrl(allShapes, heroColor, vbX, vbY, vbW, vbH, mode) {
  // mode: "hide-matching" → matching=black (hide base layer there), bg=white
  //       "show-matching" → matching=white (show top layer there), bg=black
  const baseBg = mode === "hide-matching" ? "white" : "black";
  const matchFill = mode === "hide-matching" ? "black" : "white";
  const otherFill = baseBg; // overdraws to reveal the base again
  const layers = allShapes.map((sh) => {
    const isMatch = colorsMatch(sh.color, heroColor);
    const fill = isMatch ? matchFill : otherFill;
    return sh.type === "circle"
      ? `<circle cx="${sh.attrs.cx}" cy="${sh.attrs.cy}" r="${sh.attrs.r}" fill="${fill}"/>`
      : `<path d="${sh.attrs.d}" fill="${fill}"/>`;
  }).join("");
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vbX} ${vbY} ${vbW} ${vbH}" preserveAspectRatio="none"><rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="${baseBg}"/>${layers}</svg>`;
  return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
}

function maskStyleFor(url) {
  return {
    WebkitMaskImage: url,
    maskImage: url,
    WebkitMaskRepeat: "no-repeat",
    maskRepeat: "no-repeat",
    WebkitMaskSize: "100% 100%",
    maskSize: "100% 100%",
    WebkitMaskMode: "luminance",
    maskMode: "luminance",
  };
}

function SmartHeroLayers({ w, h, shapes, heroColor, contrastColor, renderText }) {
  const matchingShapes = shapes.filter((s) => colorsMatch(s.color, heroColor));
  if (matchingShapes.length === 0) {
    return renderText({ color: heroColor });
  }
  const hideMask = buildMaskDataUrl(shapes, heroColor, 0, 0, w, h, "hide-matching");
  const showMask = buildMaskDataUrl(shapes, heroColor, 0, 0, w, h, "show-matching");
  return (
    <>
      <div style={{ position: "absolute", inset: 0, ...maskStyleFor(hideMask) }}>
        {renderText({ color: heroColor })}
      </div>
      <div style={{ position: "absolute", inset: 0, ...maskStyleFor(showMask) }}>
        {renderText({ color: contrastColor })}
      </div>
    </>
  );
}

// --- Lyric block primitive ---------------------------------------------------

function LyricBlock({ text, color, font, size, lh = 1.2, align = "left", style = {} }) {
  const lines = text.split("\n");
  const selfMap = { left: "flex-start", center: "center", right: "flex-end" };
  return (
    <div style={{ color, fontFamily: font, fontSize: `${size}mm`, lineHeight: lh, textAlign: align, alignSelf: selfMap[align] || "flex-start", whiteSpace: "pre-wrap", ...style }}>
      {lines.map((l, i) => <div key={i}>{l || "\u00a0"}</div>)}
    </div>
  );
}

// --- Layouts ----------------------------------------------------------------
// Each layout exposes a renderHero(color) callback so SmartHeroSVG can do the
// dual-color mask. The rest of the layout renders normally.

function LayoutCentered({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, renderHero, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  // Cap the hero size so the word fits the poster width at 100% (slider can still push past)
  const len = Math.max(1, (hero || "").length);
  const widthCap = (w * 0.94) / (len * 0.5);
  const intrinsic = Math.min(w * 1.4, h * 0.5, widthCap);
  const fs = intrinsic * k;
  const heroEl = (color) => (
    <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, display: "flex", flexDirection: "column", pointerEvents: "none" }}>
      <div style={{ flex: 1 }} />
      <div
        style={{
          fontFamily: heroFont,
          fontSize: `${fs}mm`,
          lineHeight: 0.85,
          color,
          textAlign: "center",
          letterSpacing: "-0.02em",
          padding: `${padding * 0.4}mm 0`,
          wordBreak: "break-word",
        }}
      >
        {hero}
      </div>
      <div style={{ flex: 1 }} />
    </div>
  );

  return (
    <>
      {/* Lyrics (always normal) — split evenly above/below the hero */}
      <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", pointerEvents: "none" }}>
        <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "flex-start", alignItems: flexAlign, gap: `${bodySize * 1.5}mm` }}>
          {blocks.slice(0, Math.ceil(blocks.length / 2)).map((b, i) => (
            <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} style={{ maxWidth: `${w * 0.6}mm` }} />
          ))}
        </div>
        <div style={{ flex: 1 }} />
        <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "flex-end", alignItems: flexAlign, gap: `${bodySize * 1.5}mm` }}>
          {blocks.slice(Math.ceil(blocks.length / 2)).map((b, i) => (
            <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} style={{ maxWidth: `${w * 0.6}mm` }} />
          ))}
        </div>
      </div>
      {renderHero(heroEl)}
    </>
  );
}

function LayoutBleed({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, renderHero, lyricAlign, flexAlign }) {
  const heroEl = (color) => (
    <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", pointerEvents: "none" }}>
      <div style={{ flex: 1 }} />
      <div
        style={{
          fontFamily: heroFont,
          fontSize: `${w * 0.28 * (heroSize / 100)}mm`,
          lineHeight: 0.78,
          color,
          letterSpacing: "-0.04em",
          fontWeight: 900,
          marginLeft: `-${padding * 0.3}mm`,
          marginBottom: `-${padding * 0.4}mm`,
          whiteSpace: "nowrap",
          overflow: "visible",
        }}
      >
        {hero}
      </div>
    </div>
  );

  return (
    <>
      <div style={{ position: "absolute", top: 0, left: 0, right: 0, padding: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: flexAlign, gap: `${bodySize * 2}mm` }}>
        {blocks.map((b, i) => (
          <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.2} align={b.align || lyricAlign} />
        ))}
      </div>
      {renderHero(heroEl)}
    </>
  );
}

function LayoutTower({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, renderHero, lyricAlign, flexAlign }) {
  const word = hero;
  const fontSize = w * 0.22 * (heroSize / 100);
  const repeatCount = Math.max(2, Math.floor((h - padding * 2) / (fontSize * 0.95)));
  const heroEl = (color) => (
    <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, display: "flex", gap: `${padding * 0.4}mm`, pointerEvents: "none" }}>
      <div style={{ flex: "0 0 auto", fontFamily: heroFont, fontSize: `${fontSize}mm`, lineHeight: 0.92, letterSpacing: "-0.02em", fontWeight: 900, display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
        {Array.from({ length: repeatCount }).map((_, i) => (
          <div key={i} style={{ color }}>{word}</div>
        ))}
      </div>
      <div style={{ flex: 1 }} />
    </div>
  );
  return (
    <>
      <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, zIndex: 3, display: "flex", gap: `${padding * 0.4}mm` }}>
        <div style={{ flex: "0 0 auto", visibility: "hidden", fontFamily: heroFont, fontSize: `${fontSize}mm`, lineHeight: 0.92, fontWeight: 900, letterSpacing: "-0.02em", whiteSpace: "nowrap" }}>
          {/* Spacer so the right column lines up with the tower */}
          {Array.from({ length: repeatCount }).map((_, i) => <div key={i}>{word}</div>)}
        </div>
        <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: flexAlign, gap: `${bodySize * 1.8}mm`, paddingLeft: `${padding * 0.6}mm` }}>
          {blocks.map((b, i) => (
            <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.2} align={b.align || lyricAlign} />
          ))}
        </div>
      </div>
      {renderHero(heroEl)}
    </>
  );
}

function LayoutStamped({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, patternShapes, smartContrast, lyricAlign, flexAlign }) {
  const chars = (hero || "").split("");
  const n = Math.max(1, chars.length);
  // Letters live in a left band (40% of poster width). Lyrics get the right 60%
  // with proper padding — no overlap, predictable across any hero length.
  const letterBandFrac = 0.4;
  const letterBandW = w * letterBandFrac;
  const lyricsLeft = letterBandW;
  const lyricsW = w - letterBandW - padding;
  // One letter per row, stacked vertically. Row height fills the poster.
  const cellW = letterBandW;
  const cellH = (h - padding * 2) / n;
  const tints = [palette.muted, palette.ink, palette.highlight || palette.muted, palette.accent];

  let maskCounter = 0;

  const renderLetter = (ch, i) => {
    const k = heroSize / 100;
    const tint = tints[i % tints.length];
    const cellX = 0;
    const cellY = padding + i * cellH;

    // Glyph fits inside cell with margin so it never bleeds into lyrics column
    const fs = Math.min(cellH * 1.15, cellW * 1.1) * k;
    const baselineY = cellY + cellH * 0.88;
    // Anchor letters to the left edge of the band (with a small inset).
    // This matches the reference "belter" stack where each letter starts at the same x.
    const anchorX = cellX + padding * 0.5;
    const textAnchor = "start";

    const useMask = smartContrast && patternShapes && patternShapes.some((s) => colorsMatch(s.color, tint));

    if (useMask) {
      const candidates = [palette.accent, palette.ink, palette.highlight, palette.muted, palette.bg].filter(Boolean);
      const contrast = candidates.find((c) => !colorsMatch(c, tint)) || palette.bg;
      const maskId = `stamp-mask-${i}-${maskCounter++}`;
      return (
        <div key={i} style={{ position: "absolute", left: `${cellX}mm`, top: `${cellY}mm`, width: `${cellW}mm`, height: `${cellH}mm`, overflow: "hidden" }}>
          <svg xmlns="http://www.w3.org/2000/svg" width={`${w}mm`} height={`${h}mm`} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ position: "absolute", left: `${-cellX}mm`, top: `${-cellY}mm` }}>
            <defs>
              <mask id={maskId} maskUnits="userSpaceOnUse">
                <rect x="0" y="0" width={w} height={h} fill="black" />
                {patternShapes.map((sh, j) => {
                  const isMatch = colorsMatch(sh.color, tint);
                  const fill = isMatch ? "white" : "black";
                  return sh.type === "circle"
                    ? <circle key={j} cx={sh.attrs.cx} cy={sh.attrs.cy} r={sh.attrs.r} fill={fill} />
                    : <path key={j} d={sh.attrs.d} fill={fill} />;
                })}
              </mask>
            </defs>
            <text x={anchorX} y={baselineY} textAnchor={textAnchor} fontFamily={heroFont} fontWeight={900} fontSize={fs} fill={tint} style={{ }}>
              {ch}
            </text>
            <text x={anchorX} y={baselineY} textAnchor={textAnchor} fontFamily={heroFont} fontWeight={900} fontSize={fs} fill={contrast} mask={`url(#${maskId})`} style={{ }}>
              {ch}
            </text>
          </svg>
        </div>
      );
    }
    return (
      <div key={i} style={{ position: "absolute", left: `${cellX}mm`, top: `${cellY}mm`, width: `${cellW}mm`, height: `${cellH}mm`, overflow: "hidden", display: "flex", alignItems: "flex-end", justifyContent: "flex-start", paddingLeft: `${padding * 0.5}mm`, paddingBottom: `${cellH * 0.05}mm` }}>
        <div style={{
          fontFamily: heroFont,
          fontWeight: 900,
          fontSize: `${fs}mm`,
          lineHeight: 1,
          color: tint,
          whiteSpace: "nowrap",
        }}>
          {ch}
        </div>
      </div>
    );
  };

  return (
    <>
      <div style={{ position: "absolute", inset: 0, zIndex: 1 }}>
        {chars.map((ch, i) => renderLetter(ch, i))}
      </div>
      <div style={{ position: "absolute", left: `${lyricsLeft}mm`, top: `${padding}mm`, bottom: `${padding}mm`, width: `${lyricsW}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: flexAlign, gap: `${bodySize * 1.5}mm` }}>
        {blocks.map((b, i) => {
          const lyricTints = [palette.ink, palette.accent, palette.muted, palette.highlight || palette.ink];
          return <LyricBlock key={b.id || i} text={b.text} color={lyricTints[i % lyricTints.length]} font={bodyFont} size={bodySize} lh={1.15} align={b.align || lyricAlign} />;
        })}
      </div>
    </>
  );
}

function LayoutStack({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const chars = hero.split("");
  const k = heroSize / 100;
  return (
    <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, zIndex: 2, display: "flex", gap: `${padding * 0.6}mm` }}>
      <div style={{ flex: "0 0 auto", display: "flex", flexDirection: "column", justifyContent: "center" }}>
        {chars.map((ch, i) => (
          <div key={i} style={{ fontFamily: heroFont, fontSize: `${(h - padding * 2) / Math.max(chars.length, 1) * 1.05 * k}mm`, lineHeight: 0.9, color: i % 2 === 0 ? palette.accent : palette.ink, fontWeight: 900 }}>{ch}</div>
        ))}
      </div>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: flexAlign, gap: `${bodySize * 2}mm` }}>
        {blocks.map((b, i) => <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />)}
      </div>
    </div>
  );
}

function LayoutRepeat({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign }) {
  const wordSize = bodySize * 4.5 * (heroSize / 100);
  const rows = Math.ceil(h / (wordSize * 1.05)) + 1;
  const repeatedRow = " " + (hero + " ").repeat(20);
  return (
    <>
      <div aria-hidden style={{ position: "absolute", inset: 0, zIndex: 1, overflow: "hidden", fontFamily: heroFont, fontSize: `${wordSize}mm`, lineHeight: 1, color: palette.muted, letterSpacing: "-0.01em", opacity: 0.6, fontWeight: 900 }}>
        {Array.from({ length: rows }).map((_, i) => <div key={i} style={{ whiteSpace: "nowrap" }}>{repeatedRow}</div>)}
      </div>
      <div style={{ position: "absolute", inset: 0, padding: `${padding}mm`, zIndex: 3, display: "flex", alignItems: "center", justifyContent: "center" }}>
        <div style={{ background: palette.bg, padding: `${padding * 0.8}mm ${padding}mm`, maxWidth: `${w * 0.7}mm`, border: `1mm solid ${palette.accent}`, display: "flex", flexDirection: "column", gap: `${bodySize * 1.5}mm` }}>
          {blocks.map((b, i) => (
            <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} lh={1.3} />
          ))}
        </div>
      </div>
    </>
  );
}

function LayoutSplit({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, smartContrast, lyricAlign, flexAlign }) {
  const splitFrac = 0.55; // left bg-colored half spans 0..55% of width
  const splitX = w * splitFrac;
  // Fit the rotated hero within the poster height so it doesn't crop at the edges
  const splitLen = Math.max(1, (hero || "").length);
  const heightCap = (h * 0.92) / (splitLen * 0.55);
  const heroSizeMm = Math.min(h * 0.42, heightCap) * (heroSize / 100);
  // The hero is rendered in palette.bg (cream). Over the bg-colored left half it
  // disappears — when smart contrast is on, swap to palette.accent there.
  const heroEl = (color) => (
    <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "flex-end", paddingRight: `${padding * 0.5}mm` }}>
      <div style={{ fontFamily: heroFont, fontSize: `${heroSizeMm}mm`, lineHeight: 0.85, color, letterSpacing: "-0.03em", fontWeight: 900, transform: "rotate(-90deg)", transformOrigin: "center", whiteSpace: "nowrap" }}>{hero}</div>
    </div>
  );
  const maskId = `split-mask-${Math.random().toString(36).slice(2, 8)}`;
  return (
    <>
      <div style={{ position: "absolute", right: 0, top: 0, bottom: 0, width: `${(1 - splitFrac) * 100}%`, background: palette.accent, zIndex: 1 }} />
      <div style={{ position: "absolute", inset: 0, zIndex: 2 }}>
        {smartContrast ? (
          <>
            {/* Base: bg-color (cream) — visible where it sits over the accent-color half */}
            <div style={{ position: "absolute", inset: 0, WebkitMaskImage: `url("data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}' preserveAspectRatio='none'><rect x='0' y='0' width='${w}' height='${h}' fill='white'/><rect x='0' y='0' width='${splitX}' height='${h}' fill='black'/></svg>`)}")`, maskImage: `url("data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}' preserveAspectRatio='none'><rect x='0' y='0' width='${w}' height='${h}' fill='white'/><rect x='0' y='0' width='${splitX}' height='${h}' fill='black'/></svg>`)}")`, WebkitMaskSize: "100% 100%", maskSize: "100% 100%", WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat", WebkitMaskMode: "luminance", maskMode: "luminance" }}>
              {heroEl(palette.bg)}
            </div>
            {/* Top: accent (dark) — visible only where it sits over the bg-color half */}
            <div style={{ position: "absolute", inset: 0, WebkitMaskImage: `url("data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}' preserveAspectRatio='none'><rect x='0' y='0' width='${w}' height='${h}' fill='black'/><rect x='0' y='0' width='${splitX}' height='${h}' fill='white'/></svg>`)}")`, maskImage: `url("data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}' preserveAspectRatio='none'><rect x='0' y='0' width='${w}' height='${h}' fill='black'/><rect x='0' y='0' width='${splitX}' height='${h}' fill='white'/></svg>`)}")`, WebkitMaskSize: "100% 100%", maskSize: "100% 100%", WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat", WebkitMaskMode: "luminance", maskMode: "luminance" }}>
              {heroEl(palette.accent)}
            </div>
          </>
        ) : (
          heroEl(palette.bg)
        )}
      </div>
      <div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: `${splitFrac * 100}%`, padding: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: flexAlign, gap: `${bodySize * 2}mm` }}>
        {blocks.map((b, i) => <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.2} align={b.align || lyricAlign} />)}
      </div>
    </>
  );
}

function LayoutTape({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign, tapeCount = 1 }) {
  const n = Math.max(1, Math.min(8, tapeCount));
  // Tape height scales down as we add more bands so they don't all crash into each other.
  const bandHeight = (h * 0.32 * (heroSize / 100)) / (1 + (n - 1) * 0.45);
  const fontSize = bandHeight * 0.78;
  const tokenWord = hero + "  •  ";
  // Stagger: slice the repeated string so each band starts at a different
  // character of the cycle. This is font-independent — no width approximation.
  // Hand-picked offsets spread evenly around the token length.
  const offsetTable = [0, 0.37, 0.71, 0.18, 0.55, 0.84, 0.28, 0.62];
  // Vertical placement: spread bands evenly across the poster height, with margin top/bottom.
  const margin = 0.18;
  const tapeYs = n === 1
    ? [0.5]
    : Array.from({ length: n }, (_, i) => margin + (i / (n - 1)) * (1 - margin * 2));
  // Alternate rotation direction for visual rhythm when there's >1.
  const rotForIndex = (i) => (n === 1 ? -6 : (i % 2 === 0 ? -6 : 6));

  return (
    <>
      {tapeYs.map((y, i) => {
        const rot = rotForIndex(i);
        // Build a per-band repeated string, sliced to start at a different character.
        const shift = Math.floor(offsetTable[i % offsetTable.length] * tokenWord.length);
        const shifted = tokenWord.slice(shift) + tokenWord.slice(0, shift);
        const repeated = shifted.repeat(20);
        return (
          <div
            key={i}
            style={{
              position: "absolute",
              left: `${-padding}mm`,
              right: `${-padding}mm`,
              top: `${y * 100}%`,
              transform: `translateY(-50%) rotate(${rot}deg)`,
              background: palette.accent,
              height: `${bandHeight}mm`,
              zIndex: 2,
              display: "flex",
              alignItems: "center",
              overflow: "hidden",
            }}
          >
            <div
              style={{
                whiteSpace: "nowrap",
                fontFamily: heroFont,
                fontSize: `${fontSize}mm`,
                lineHeight: 1,
                color: palette.bg,
                fontWeight: 900,
                paddingLeft: `${padding}mm`,
              }}
            >
              {repeated}
            </div>
          </div>
        );
      })}
      <div style={{ position: "absolute", top: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: flexAlign, gap: `${bodySize * 1.2}mm` }}>
        {blocks.slice(0, Math.ceil(blocks.length / 2)).map((b, i) => (
          <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.2} align={b.align || lyricAlign} />
        ))}
      </div>
      <div style={{ position: "absolute", bottom: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: flexAlign, gap: `${bodySize * 1.2}mm` }}>
        {blocks.slice(Math.ceil(blocks.length / 2)).map((b, i) => (
          <LyricBlock key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.2} align={b.align || lyricAlign} />
        ))}
      </div>
    </>
  );
}

const LAYOUT_MAP = {
  centered: LayoutCentered,
  stamped: LayoutStamped,
  stack: LayoutStack,
  repeat: LayoutRepeat,
  bleed: LayoutBleed,
  split: LayoutSplit,
  tower: LayoutTower,
  tape: LayoutTape,
  ...(window.EXTRA_LAYOUTS || {}),
};

// Smart-hero capable layouts (have renderHero callback)
const SMART_LAYOUTS = new Set(["centered", "bleed", "tower"]);

function heroColorFor(layout, palette) {
  switch (layout) {
    case "split": return palette.bg;
    case "tape": return palette.bg;
    case "stamped": return palette.muted;
    default: return palette.accent;
  }
}

function Poster({ config, posterRef }) {
  const { width, height, palette, heroFont, bodyFont, hero, blocks, layout, pattern, patternScale, patternDensity, patternOriginX, patternThickness, heroSize, bodySize, padding, smartContrast, lyricAlign, tapeCount, photo } = config;
  const align = lyricAlign || "left";
  const flexAlign = align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
  // Skip empty lyric blocks so they don't leave stray gaps on the poster
  const visibleBlocks = (blocks || []).filter((b) => (b.text || "").trim().length > 0);
  const Layout = LAYOUT_MAP[layout] || LayoutCentered;

  const patternData = useMemo(
    () => buildPattern({ pattern, scale: patternScale, density: patternDensity, palette, w: width, h: height, originX: patternOriginX, thickness: patternThickness }),
    [pattern, patternScale, patternDensity, patternOriginX, patternThickness, palette, width, height]
  );

  const heroColor = heroColorFor(layout, palette);
  // Pick the contrast color: the OTHER pattern color (not the one matching hero)
  let contrastColor = heroColor;
  if (patternData.colors && patternData.colors.length === 2) {
    const [a, b] = patternData.colors;
    if (colorsMatch(a, heroColor)) contrastColor = b;
    else if (colorsMatch(b, heroColor)) contrastColor = a;
    else contrastColor = heroColor; // no clash
  }

  const renderHero = (heroElFn) => {
    const isSmart = smartContrast && SMART_LAYOUTS.has(layout) && patternData.shapes;
    if (!isSmart) {
      return heroElFn(heroColor);
    }
    return (
      <SmartHeroLayers
        w={width}
        h={height}
        shapes={patternData.shapes}
        heroColor={heroColor}
        contrastColor={contrastColor}
        renderText={({ color }) => heroElFn(color)}
      />
    );
  };

  return (
    <div
      ref={posterRef}
      data-poster="true"
      style={{ width: `${width}mm`, height: `${height}mm`, background: palette.bg, position: "relative", overflow: "hidden", boxShadow: "0 30px 60px rgba(0,0,0,.18), 0 8px 16px rgba(0,0,0,.12)" }}
    >
      {patternData.node}
      <Layout
        hero={hero}
        blocks={visibleBlocks}
        palette={palette}
        heroFont={heroFont}
        bodyFont={bodyFont}
        w={width}
        h={height}
        heroSize={heroSize}
        bodySize={bodySize}
        padding={padding}
        renderHero={renderHero}
        patternShapes={patternData.shapes}
        smartContrast={smartContrast}
        lyricAlign={align}
        flexAlign={flexAlign}
        tapeCount={tapeCount}
        photo={photo}
      />
    </div>
  );
}

window.Poster = Poster;
