// Block.jsx — renders a single isometric cube (solid or empty/outline)
// plus a shared palette utility and iso-projection helpers.

// Isometric projection (classic 2:1 dimetric-style but using 30° angles).
// Grid cell size in world units = 1. One world unit projects to:
//   x:  (cos30 * unit)
//   y:  (sin30 * unit)
// We choose UNIT = 46 px for base tile edge.
const UNIT = 46;
const ANGLE_DEG = 22.5;
const ANGLE = (ANGLE_DEG * Math.PI) / 180;
const COS30 = Math.cos(ANGLE);
const SIN30 = Math.sin(ANGLE);

// Project a 3D world point (gx, gy, gz) to screen (sx, sy).
// gx goes right-depth (into the page on the right), gy goes left-depth,
// gz is vertical. Origin (0,0,0) maps to (0,0).
function iso(gx, gy, gz) {
  return {
    x: (gx - gy) * UNIT * COS30,
    y: (gx + gy) * UNIT * SIN30 - gz * UNIT,
  };
}

// Palette — for each named color, the three face shades (top lightest,
// right medium, left darkest), plus any glow / fill tint used for "empty".
const PALETTE = {
  magenta: {
    name: 'Magenta',
    top:   '#ff8ad6',
    right: '#f553c3',
    left:  '#b01f8c',
    edge:  '#7d1463',
    solidSwatch: '#f553c3',
  },
  purple: {
    name: 'Purple',
    top:   '#b392ff',
    right: '#8b5cff',
    left:  '#4d18c4',
    edge:  '#2f0b8a',
    solidSwatch: '#8b5cff',
  },
  blue: {
    name: 'Blue',
    top:   '#5ea8ff',
    right: '#1f6ee0',
    left:  '#0a3a8a',
    edge:  '#071f52',
    solidSwatch: '#3d8cff',
  },
  green: {
    name: 'Green',
    top:   '#7df0c4',
    right: '#2dd4a0',
    left:  '#0b9a73',
    edge:  '#075744',
    solidSwatch: '#2dd4a0',
  },
  agent: {
    name: 'Agent',
    // Translucent — outline + inner glow, per reference
    top:   'rgba(139, 92, 255, 0.18)',
    right: 'rgba(139, 92, 255, 0.10)',
    left:  'rgba(106, 43, 240, 0.14)',
    edge:  'rgba(179, 146, 255, 0.9)',
    solidSwatch: 'transparent',
    translucent: true,
  },
  // Warm gray for "external" nodes — systems connected to Harper that live
  // outside the canvas. Sharp contrast against the dark stage; warmer than
  // the cool teals/blues used inside the canvas.
  slate: {
    name: 'Warm Gray',
    top:   '#d8d2c6',
    right: '#a89e8c',
    left:  '#6b6253',
    edge:  '#3d352a',
    solidSwatch: '#a89e8c',
  },
  // Neutral gray — same character (light/medium/dark with hard edge), no
  // warm or cool cast. Sits between the warm and cool slate variants.
  slateNeutral: {
    name: 'Neutral Gray',
    top:   '#d4d4d4',
    right: '#a3a3a3',
    left:  '#666666',
    edge:  '#363636',
    solidSwatch: '#a3a3a3',
  },
  // Cool gray — same character with a subtle blue undertone for systems
  // we want to read as cooler than the warm-gray default.
  slateCool: {
    name: 'Cool Gray',
    top:   '#cdd2d8',
    right: '#9aa1ab',
    left:  '#5e6570',
    edge:  '#2f3540',
    solidSwatch: '#9aa1ab',
  },
};

const COLOR_KEYS = ['magenta', 'purple', 'blue', 'green'];
const EXT_COLOR_KEYS = ['slate', 'slateNeutral', 'slateCool'];

// Build the SVG <g> contents for one block occupying size `s` world units
// (default 1), positioned with its NEAR-BOTTOM corner at world (0,0,0).
// type: 'solid' | 'empty'
// dim: { x, y, z } in world units (each axis can shrink via PAD inset).
// Default 1x1x1 unit cube.
function BlockShape({ color, type, dim, size, label, labelFace, labelScale = 1 }) {
  const pal = PALETTE[color] || PALETTE.green;
  // Backward-compat: if `size` (a single number) is passed, treat as cubic dim.
  const dx = (dim?.x ?? size ?? 1);
  const dy = (dim?.y ?? size ?? 1);
  const dz = (dim?.z ?? size ?? 1);

  // 8 corners of the box in world coords:
  // Base at z=0, block fills x:[0,dx], y:[0,dy], z:[0,dz]
  const pts = {
    // bottom
    B000: iso(0,  0,  0),
    B100: iso(dx, 0,  0),
    B110: iso(dx, dy, 0),
    B010: iso(0,  dy, 0),
    // top
    T001: iso(0,  0,  dz),
    T101: iso(dx, 0,  dz),
    T111: iso(dx, dy, dz),
    T011: iso(0,  dy, dz),
  };

  const poly = (a, b, c, d) => `${a.x},${a.y} ${b.x},${b.y} ${c.x},${c.y} ${d.x},${d.y}`;

  // Face assembly — for a cube viewed from the standard iso angle:
  // top face:   T001, T101, T111, T011
  // right face: T101, B100, B110, T111   (the face on +x side)
  // left face:  T001, B000, B110?  actually +y is "left"
  // Let's recompute: in our iso, +x projects right/down, +y projects left/down.
  // So the visible right-wall is the face where x is max (x=s).
  // Visible left-wall is the face where y is max (y=s).
  // Top face is z=s.

  const topFace   = poly(pts.T001, pts.T101, pts.T111, pts.T011);
  const rightFace = poly(pts.T101, pts.B100, pts.B110, pts.T111); // x = s
  const leftFace  = poly(pts.T011, pts.T111, pts.B110, pts.B010); // y = s

  // For labels, we place them on the front-facing right face (more readable)
  // unless translucent, in which case centered on top.
  const topCenter = {
    x: (pts.T001.x + pts.T101.x + pts.T111.x + pts.T011.x) / 4,
    y: (pts.T001.y + pts.T101.y + pts.T111.y + pts.T011.y) / 4,
  };

  // Build an affine matrix that maps face-local (u, v) coords — where u
  // runs along the face's horizontal edge in pixel units (0 → faceWidthPx)
  // and v runs along the vertical edge (0 → faceHeightPx) — onto the
  // projected face quad. We pass face corners in order: TL, TR, BL.
  // The resulting `matrix(a,b,c,d,e,f)` transform on a <g> lets us render
  // text in unsheared local space and have it land in iso perspective.
  const faceMatrix = (TL, TR, BL, widthUnits, heightUnits) => {
    const wPx = widthUnits * UNIT;
    const hPx = heightUnits * UNIT;
    const a = (TR.x - TL.x) / wPx;
    const b = (TR.y - TL.y) / wPx;
    const c = (BL.x - TL.x) / hPx;
    const d = (BL.y - TL.y) / hPx;
    const e = TL.x;
    const f = TL.y;
    return { matrix: `matrix(${a},${b},${c},${d},${e},${f})`, wPx, hPx };
  };

  // Pick which face the label should sit on. Both right (x=dx) and left
  // (y=dy) faces are visible in our iso view. Prefer the wider one so longer
  // labels read better; tie-break to the left face since it's slightly
  // closer to the viewer in this projection.
  // labelFace prop: legacy string ('left'/'right'/'hidden') OR object
  // {face, vLo, vHi} where vLo/vHi are face-local vertical bounds
  // (v=0 BOTTOM, v=1 TOP).
  const labelOnLeft = dy <= dx;
  let chosenFace, vLo, vHi;
  if (labelFace && typeof labelFace === 'object') {
    chosenFace = labelFace.face;
    vLo = labelFace.vLo ?? 0;
    vHi = labelFace.vHi ?? 1;
  } else {
    chosenFace = labelFace ?? (labelOnLeft ? 'left' : 'right');
    vLo = 0;
    vHi = 1;
  }
  const labelFaceData = chosenFace === 'left'
    ? faceMatrix(pts.T011, pts.T111, pts.B010, dx, dz)
    : (chosenFace === 'right'
      // Right face: TL = T111 (front-top, screen-LEFT in iso), TR = T101
      // (back-top, screen-RIGHT). u runs screen-left → screen-right so text
      // reads naturally instead of mirrored.
      ? faceMatrix(pts.T111, pts.T101, pts.B110, dy, dz)
      : null);

  // ---- Word-only wrap (no hyphenation). Returns array of lines, each
  // ≤ maxCharsPerLine. Words longer than maxCharsPerLine are placed on a
  // line of their own (they'll force a smaller font but won't be broken).
  const wrapWords = (text, maxCharsPerLine, maxLines) => {
    const words = text.split(/\s+/).filter(Boolean);
    if (!words.length) return [text];
    const lines = [];
    let current = '';
    for (const word of words) {
      if (lines.length >= maxLines) break;
      if (!current) {
        current = word;
        continue;
      }
      const candidate = current + ' ' + word;
      if (candidate.length <= maxCharsPerLine) {
        current = candidate;
      } else {
        lines.push(current);
        current = word;
      }
    }
    if (current && lines.length < maxLines) lines.push(current);
    return lines;
  };

  // ---- Hyphenated mid-word wrap (LAST RESORT). Used only when wrapWords
  // can't avoid a line that overflows the strip's height-budgeted font.
  const wrapHyphenated = (text, maxCharsPerLine, maxLines) => {
    if (text.length <= maxCharsPerLine) return [text];
    const words = text.split(/\s+/).filter(Boolean);
    const lines = [];
    let current = '';
    const tryFlush = () => {
      if (current) lines.push(current);
      current = '';
    };
    const breakWord = (word) => {
      let rest = word;
      while (rest.length > maxCharsPerLine) {
        if (lines.length >= maxLines) return '';
        const seg = rest.slice(0, maxCharsPerLine - 1);
        lines.push(seg + '-');
        rest = rest.slice(maxCharsPerLine - 1);
      }
      return rest;
    };
    for (const word of words) {
      if (lines.length >= maxLines) break;
      if (word.length > maxCharsPerLine) {
        tryFlush();
        current = breakWord(word);
        continue;
      }
      const candidate = current ? current + ' ' + word : word;
      if (candidate.length <= maxCharsPerLine) {
        current = candidate;
      } else {
        tryFlush();
        current = word;
      }
    }
    if (current && lines.length < maxLines) lines.push(current);
    if (lines.length === maxLines) {
      const last = lines[maxLines - 1];
      if (last.length > maxCharsPerLine) {
        lines[maxLines - 1] = last.slice(0, maxCharsPerLine - 1) + '…';
      }
    }
    return lines;
  };

  // Render label inside the face's matrix transform.
  const renderFaceLabel = () => {
    if (!label) return null;
    if (!labelFaceData) return null;
    const { matrix, wPx, hPx } = labelFaceData;

    // Strip bounds in face-local pixel space. The face's matrix has v=0 at
    // top of the rect and v=hPx at bottom — but our (vLo, vHi) come from a
    // coordinate where v=0 is the BOTTOM of the face. Flip to get pixel y.
    const yStripTopPx    = hPx * (1 - vHi); // higher v = lower pixel y
    const yStripBottomPx = hPx * (1 - vLo);
    const stripCenterY   = (yStripTopPx + yStripBottomPx) / 2;
    const stripHeightPx  = yStripBottomPx - yStripTopPx;

    // Comfortable horizontal padding so labels never look edge-glued.
    const padX = wPx * 0.14;
    const usableW = Math.max(wPx - padX * 2, 12);

    const charWidth = 0.55; // average glyph width as fraction of font size

    // Caps: font size never exceeds ~50% of strip height (so it doesn't
    // crowd vertically). Hard cap at 16 — anything larger looks oversized
    // even on a 2u or 3u face.
    const FONT_CAP = 16;
    const maxFontFromHeight = (n) => Math.min(FONT_CAP, (stripHeightPx / n) * 0.78);

    // ---- Strategy:
    //   (a) ONE LINE: shrink down to MIN_FONT (8) — accept anything ≥ 8px.
    //   (b) Only when one line is forced below 8px do we wrap on word
    //       boundaries. Try 2 lines, then 3 lines.
    //   (c) Hyphenated mid-word breaks are a final fallback if word-only
    //       wrapping can't get to ≥ 8px either.
    const MIN_FONT = 8;

    const fitOneLine = () => {
      const fitFromW = usableW / Math.max(label.length, 1) / charWidth;
      const fs = Math.min(maxFontFromHeight(1), fitFromW);
      return { lines: [label], fs };
    };
    const fitNLinesWords = (n) => {
      // Iterate twice: pick a chars-per-line guess, wrap, refit, re-wrap.
      let cpl = Math.max(3, Math.ceil(label.length / n));
      let lines = wrapWords(label, cpl, n);
      let longest = lines.reduce((m, l) => Math.max(m, l.length), 1);
      let fitFromW = usableW / longest / charWidth;
      let fs = Math.min(maxFontFromHeight(n), fitFromW);
      const cplAtFs = Math.max(1, Math.floor(usableW / (fs * charWidth)));
      if (cplAtFs !== cpl) {
        cpl = cplAtFs;
        lines = wrapWords(label, cpl, n);
        longest = lines.reduce((m, l) => Math.max(m, l.length), 1);
        fitFromW = usableW / longest / charWidth;
        fs = Math.min(maxFontFromHeight(n), fitFromW);
      }
      return { lines, fs };
    };
    const fitNLinesHyphen = (n) => {
      let cpl = Math.max(3, Math.ceil(label.length / n));
      let lines = wrapHyphenated(label, cpl, n);
      let longest = lines.reduce((m, l) => Math.max(m, l.length), 1);
      let fitFromW = usableW / longest / charWidth;
      let fs = Math.min(maxFontFromHeight(n), fitFromW);
      const cplAtFs = Math.max(1, Math.floor(usableW / (fs * charWidth)));
      if (cplAtFs !== cpl) {
        cpl = cplAtFs;
        lines = wrapHyphenated(label, cpl, n);
        longest = lines.reduce((m, l) => Math.max(m, l.length), 1);
        fitFromW = usableW / longest / charWidth;
        fs = Math.min(maxFontFromHeight(n), fitFromW);
      }
      return { lines, fs };
    };

    // Always compute all wrap candidates so the slider can re-pick a
    // wrap that supports a larger user-requested font size.
    const attempts = [];
    attempts.push({ ...fitOneLine(), kind: 'single' });
    attempts.push({ ...fitNLinesWords(2), kind: 'word2' });
    attempts.push({ ...fitNLinesWords(3), kind: 'word3' });
    attempts.push({ ...fitNLinesHyphen(2), kind: 'hyph2' });
    attempts.push({ ...fitNLinesHyphen(3), kind: 'hyph3' });

    // Default pick (labelScale == 1): preserve original priority —
    // single line if it reads at MIN_FONT or larger; otherwise the
    // first wrap that does; otherwise whichever wrap has the largest fs.
    let chosen;
    if (attempts[0].fs >= MIN_FONT) {
      chosen = attempts[0];
    } else {
      const fallback = attempts.slice(1).find((a) => a.fs >= MIN_FONT);
      chosen = fallback || attempts.reduce((b, a) => (a.fs > b.fs ? a : b));
    }

    // User-set scale: target font size = baseline × labelScale. We re-pick
    // the wrap (single → 2-word → 3-word → 2-hyph → 3-hyph) so the
    // LEAST-WRAPPED option that can render at the requested font wins.
    //  - Slider UP (>1): a tighter wrap may overflow at the bigger font,
    //    so step into a more-wrapped option that fits.
    //  - Slider DOWN (<1): the previously-chosen wrap may be wrapped MORE
    //    than necessary (e.g. "GraphQL" was on 2 lines because single-line
    //    was below MIN_FONT — but at 50% the user has accepted a smaller
    //    font, and single-line now fits comfortably). Step BACK to a
    //    less-wrapped option.
    const baselineFs = Math.max(MIN_FONT, chosen.fs);
    const targetFs = baselineFs * labelScale;
    if (labelScale !== 1) {
      const supported = attempts.find((a) => a.fs >= targetFs);
      chosen = supported || attempts.reduce((b, a) => (a.fs > b.fs ? a : b));
    }
    // Clamp final font size to the chosen wrap's max — no overflow.
    const fontSize = Math.min(targetFs, chosen.fs);
    const chosenLines = chosen.lines;
    const lineHeight = fontSize * 1.1;
    const totalTextH = lineHeight * chosenLines.length;

    // Vertically center the text block on the strip.
    const firstBaselineY = stripCenterY - totalTextH / 2 + fontSize * 0.85;
    const cx = wPx / 2;

    return (
      <g transform={matrix}>
        {chosenLines.map((line, i) => (
          <text
            key={i}
            x={cx}
            y={firstBaselineY + i * lineHeight}
            textAnchor="middle"
            fontSize={fontSize}
            className="block-label"
            style={{ fill: '#ffffff' }}
          >
            {line}
          </text>
        ))}
      </g>
    );
  };

  // Empty blocks: outlined frame (edges only) + faintly tinted translucent faces.
  if (type === 'empty') {
    const faceOpacity = pal.translucent ? 1 : 0.22;
    const edgeColor = pal.edge;
    // All 12 edges of the cube
    const edges = [
      // bottom square
      [pts.B000, pts.B100], [pts.B100, pts.B110], [pts.B110, pts.B010], [pts.B010, pts.B000],
      // top square
      [pts.T001, pts.T101], [pts.T101, pts.T111], [pts.T111, pts.T011], [pts.T011, pts.T001],
      // verticals
      [pts.B000, pts.T001], [pts.B100, pts.T101], [pts.B110, pts.T111], [pts.B010, pts.T011],
    ];

    return (
      <g>
        {/* Translucent faces (visible three only) */}
        <polygon points={topFace}   fill={pal.top}   opacity={faceOpacity} />
        <polygon points={leftFace}  fill={pal.left}  opacity={faceOpacity} />
        <polygon points={rightFace} fill={pal.right} opacity={faceOpacity} />
        {/* Inner glow for agent-style */}
        {pal.translucent && (
          <circle
            cx={topCenter.x}
            cy={topCenter.y + UNIT * 0.35}
            r={UNIT * 0.9}
            fill="url(#agent-glow)"
            opacity="0.65"
          />
        )}
        {/* All 12 edges */}
        {edges.map(([a, b], i) => (
          <line
            key={i}
            x1={a.x} y1={a.y} x2={b.x} y2={b.y}
            stroke={edgeColor}
            strokeWidth="2"
            strokeLinecap="round"
            opacity={i >= 8 || i < 4 ? 0.55 : 1}
          />
        ))}
        {label && renderFaceLabel()}
      </g>
    );
  }

  // Solid block: three filled faces, bold color contrast per reference.
  return (
    <g>
      <polygon points={leftFace}  fill={pal.left} />
      <polygon points={rightFace} fill={pal.right} />
      <polygon points={topFace}   fill={pal.top} />
      {/* Subtle edge highlights on top */}
      <polyline
        points={`${pts.T001.x},${pts.T001.y} ${pts.T101.x},${pts.T101.y} ${pts.T111.x},${pts.T111.y} ${pts.T011.x},${pts.T011.y} ${pts.T001.x},${pts.T001.y}`}
        fill="none"
        stroke="rgba(255,255,255,0.12)"
        strokeWidth="1"
      />
      {/* label sheared onto the more-visible side face */}
      {renderFaceLabel()}
    </g>
  );
}

// Expose
window.PALETTE = PALETTE;
window.COLOR_KEYS = COLOR_KEYS;
window.EXT_COLOR_KEYS = EXT_COLOR_KEYS;
window.UNIT = UNIT;
window.iso = iso;
window.BlockShape = BlockShape;
