// Export.jsx — Export the current scene as transparent PNG or SVG.
//
// The live scene is rendered in a single <svg> inside .stage-inner. To export
// we:
//   1. Locate that SVG and the inner translation <g> that holds the scene.
//   2. Measure that <g>'s bounding box (in SVG coords) — that's the tight
//      bounds of all rendered geometry, regardless of pan/zoom.
//   3. Clone the SVG, retarget its viewBox/width/height to the bbox + a small
//      pad, strip transient overlays (hover ghost, selection halos), and
//      embed @font-face declarations with the actual woff2 fonts as data
//      URLs so type renders correctly when opened standalone OR rasterized
//      to PNG via Image+canvas (which can't fetch external resources).
//   4. Resolve any `var(--font-...)` references on <text> nodes to concrete
//      family names (the SVG-as-Image render context has no CSS variables).
//   5. Serialize to an SVG string.
//   6. PNG export rasterizes the same SVG at hi-DPI through an Image+canvas.

// Map of font-family name -> array of {weight, style, url} entries.
// Built by ENUMERATING the document's @font-face rules at export time so we
// always pick up exactly what the page is using (no manual mirror to drift).
async function collectFontFaces() {
  const out = []; // [{ family, weight, style, dataUrl }]
  for (const sheet of Array.from(document.styleSheets)) {
    let rules;
    try { rules = sheet.cssRules; } catch (_) { continue; } // CORS guard
    if (!rules) continue;
    for (const rule of Array.from(rules)) {
      if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
      const family = (rule.style.getPropertyValue('font-family') || '').trim().replace(/^['"]|['"]$/g, '');
      const weight = (rule.style.getPropertyValue('font-weight') || '400').trim();
      const style = (rule.style.getPropertyValue('font-style') || 'normal').trim();
      const src = rule.style.getPropertyValue('src') || '';
      // Pull the first url(...) from the src list.
      const m = src.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/);
      if (!m) continue;
      let url = m[1];
      // Resolve relative URLs against the STYLESHEET's href (so `../fonts/`
      // in styles/colors_and_type.css correctly resolves to /fonts/), not
      // the document URL. Fall back to document base if the sheet has no href
      // (e.g. inline <style>).
      const base = sheet.href || document.baseURI;
      try { url = new URL(url, base).href; } catch (_) {}
      out.push({ family, weight, style, url });
    }
  }
  // Fetch each font and base64-encode.
  const results = await Promise.all(out.map(async (f) => {
    try {
      const r = await fetch(f.url);
      if (!r.ok) return null;
      const buf = await r.arrayBuffer();
      const b64 = arrayBufferToBase64(buf);
      const mime = f.url.endsWith('.woff2') ? 'font/woff2' : f.url.endsWith('.woff') ? 'font/woff' : 'font/ttf';
      return { ...f, dataUrl: `data:${mime};base64,${b64}` };
    } catch (_) { return null; }
  }));
  return results.filter(Boolean);
}

function arrayBufferToBase64(buf) {
  // Chunked conversion to avoid call-stack limits on large fonts.
  const bytes = new Uint8Array(buf);
  let bin = '';
  const CHUNK = 0x8000;
  for (let i = 0; i < bytes.length; i += CHUNK) {
    bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
  }
  return btoa(bin);
}

function buildFontFaceCss(faces) {
  return faces.map((f) => `
    @font-face {
      font-family: '${f.family}';
      font-weight: ${f.weight};
      font-style: ${f.style};
      src: url('${f.dataUrl}') format('${f.url.endsWith('.woff2') ? 'woff2' : 'woff'}');
    }
  `).join('\n');
}

// Resolve a `font-family` attribute that may contain CSS var() references
// against the page's :root computed styles. var() is invalid in raw SVG,
// so we expand it to the underlying value at export time.
function resolveFontFamilyValue(value, rootStyle) {
  if (!value) return value;
  // Replace each var(--name[, fallback]) with the resolved value.
  return value.replace(/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\)/g, (_m, name, fallback) => {
    const v = rootStyle.getPropertyValue(name).trim();
    if (v) return v;
    return (fallback || '').trim();
  });
}

// Resolve ALL var(--...) references inside an attribute string (used for
// fill/stroke too — the live SVG sets fill="var(--teal-400)" in some places).
function resolveAllVars(value, rootStyle) {
  if (!value || value.indexOf('var(') === -1) return value;
  return value.replace(/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\)/g, (_m, name, fallback) => {
    const v = rootStyle.getPropertyValue(name).trim();
    if (v) return v;
    return (fallback || '').trim();
  });
}

// Compute the BUILD-AREA viewBox: the iso-projected bounding box of the
// MAXIMUM possible scene for the current canvas size — canvas pedestal +
// walls + accent slot, plus a worst-case 2×2×2 external block on each of
// the four sides. Independent of which externals are actually placed, so
// successive exports of the same canvas size always land on the same
// viewBox and can be stacked / compared pixel-aligned.
//
// All coordinates returned are in the SAME SVG space the live scene uses,
// i.e. AFTER the gridCenter offset that App.jsx applies on its inner <g>.
// We pull W/D/H/accentSlotH off `window.__bbExportCtx` (App.jsx writes it
// every render — see App.jsx).
function _buildAreaSvgBBox() {
  const ctx = window.__bbExportCtx;
  if (!ctx || typeof window.iso !== 'function') return null;
  const { W, D, H, accentSlotH = 0, gridCenter } = ctx;

  // Constants must match the rendering modules:
  //   - Container.jsx: _CEIL_LIFT = 0.16, _PEDESTAL = 0.16, FRONT_BUFFER = 0.12
  //   - ExternalBlock.jsx: EXT_OFFSET = 3
  // We deliberately re-declare them here (rather than import) so this
  // function stays self-contained — if any of these change, update both.
  const CEIL_LIFT = 0.16;
  const PEDESTAL  = 0.16;
  const FRONT_BUF = 0.12;
  const EXT_OFF   = 3;

  // Worst-case external footprint: any axis can be up to 2 (see EXT_SIZES
  // in ExternalBlock.jsx). z up to 2 as well to cover taller variants.
  const MAX = 2;

  // Wireframe extends slightly past the canvas on the FRONT side.
  const Wf = W + FRONT_BUF;
  const Df = D + FRONT_BUF;

  // World-space corner candidates we want inside the bbox.
  const pts = [];

  // Canvas volume — pedestal underside up through the accent slot ceiling.
  const zLo = -PEDESTAL;
  const zHi = H + CEIL_LIFT + accentSlotH;
  for (const x of [0, Wf]) for (const y of [0, Df]) for (const z of [zLo, zHi]) {
    pts.push({ x, y, z });
  }

  // Worst-case externals: 2×2×2 block at the far extent of every side.
  // Each side spans the full canvas tangent dimension since slot positions
  // can shift along that axis when one of two blocks is placed.
  // back-left:  -EXT_OFF-MAX ≤ x ≤ -EXT_OFF, 0 ≤ y ≤ D
  for (const x of [-EXT_OFF - MAX, -EXT_OFF])
    for (const y of [0, D])
      for (const z of [0, MAX]) pts.push({ x, y, z });
  // back-right: 0 ≤ x ≤ W, -EXT_OFF-MAX ≤ y ≤ -EXT_OFF
  for (const x of [0, W])
    for (const y of [-EXT_OFF - MAX, -EXT_OFF])
      for (const z of [0, MAX]) pts.push({ x, y, z });
  // front-right: W+EXT_OFF ≤ x ≤ W+EXT_OFF+MAX, 0 ≤ y ≤ D
  for (const x of [W + EXT_OFF, W + EXT_OFF + MAX])
    for (const y of [0, D])
      for (const z of [0, MAX]) pts.push({ x, y, z });
  // front-left: 0 ≤ x ≤ W, D+EXT_OFF ≤ y ≤ D+EXT_OFF+MAX
  for (const x of [0, W])
    for (const y of [D + EXT_OFF, D + EXT_OFF + MAX])
      for (const z of [0, MAX]) pts.push({ x, y, z });

  // Project all points to iso-screen, then offset by -gridCenter to match
  // the inner <g transform="translate(-cx,-cy)"> that App.jsx applies.
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const p of pts) {
    const s = window.iso(p.x, p.y, p.z);
    const sx = s.x - gridCenter.x;
    const sy = s.y - gridCenter.y;
    if (sx < minX) minX = sx;
    if (sy < minY) minY = sy;
    if (sx > maxX) maxX = sx;
    if (sy > maxY) maxY = sy;
  }
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}

// Build a sanitized clone of the live SVG with a tight viewBox.
//
// `mode`:
//   'visible'    — bbox of the rendered scene group (default; tracks
//                  whatever blocks are placed)
//   'build-area' — bbox of the maximum possible scene for the current
//                  canvas size (stable across exports — useful for
//                  layering multiple exports in the same position)
async function buildExportSvg({ padding = 24, mode = 'visible' } = {}) {
  const liveSvg = document.querySelector('.stage-inner svg');
  if (!liveSvg) throw new Error('Could not find scene SVG');

  const sceneG = liveSvg.querySelector('g');
  if (!sceneG) throw new Error('Could not find scene group');

  let bbox;
  if (mode === 'build-area') {
    bbox = _buildAreaSvgBBox();
    if (!bbox) bbox = sceneG.getBBox(); // fallback if ctx missing
  } else {
    bbox = sceneG.getBBox();
  }
  const x = bbox.x - padding;
  const y = bbox.y - padding;
  const w = bbox.width + padding * 2;
  const h = bbox.height + padding * 2;

  const clone = liveSvg.cloneNode(true);

  // Remove transient hover-ghost / selection chrome (teal-tinted polygons).
  clone.querySelectorAll('polygon').forEach((el) => {
    const fill = el.getAttribute('fill') || '';
    if (fill.startsWith('rgba(45, 212, 160')) el.remove();
  });

  // Resolve var(--...) on common attributes for every element. This covers
  // fill="var(--teal-400)" etc. in source, which raw SVG renderers ignore.
  const rootStyle = getComputedStyle(document.documentElement);
  const ATTRS_WITH_VARS = ['fill', 'stroke', 'color', 'stop-color'];
  clone.querySelectorAll('*').forEach((el) => {
    for (const a of ATTRS_WITH_VARS) {
      const v = el.getAttribute(a);
      if (v && v.indexOf('var(') !== -1) {
        el.setAttribute(a, resolveAllVars(v, rootStyle));
      }
    }
    // Inline style may also contain var()
    const inline = el.getAttribute('style');
    if (inline && inline.indexOf('var(') !== -1) {
      el.setAttribute('style', resolveAllVars(inline, rootStyle));
    }
  });

  // Resolve font-family on every <text>. Also default text fill to white if
  // missing (CSS class .block-label sets fill but a static SVG has no class
  // styles). We keep existing fill if set explicitly.
  clone.querySelectorAll('text').forEach((el) => {
    const ff = el.getAttribute('font-family');
    if (ff) {
      el.setAttribute('font-family', resolveFontFamilyValue(ff, rootStyle));
    }
    // If the text has class="block-label" but no inline fill, copy from
    // the live element's computed style so the export matches what the
    // user sees (typically white).
    if (!el.getAttribute('fill')) {
      // Find the matching node in the live SVG by cloning index — easier
      // path: check class and apply a sensible default.
      const cls = (el.getAttribute('class') || '');
      if (cls.split(/\s+/).includes('block-label')) {
        el.setAttribute('fill', '#ffffff');
        if (!el.getAttribute('font-family')) {
          el.setAttribute('font-family', resolveFontFamilyValue('var(--font-sans)', rootStyle));
        }
        if (!el.getAttribute('font-weight')) {
          el.setAttribute('font-weight', '700');
        }
      }
    }
  });

  // Build & embed a <style> block with @font-face data-URL fonts, so both
  // SVG-opened-standalone and PNG-rasterized cases render with correct type.
  const faces = await collectFontFaces();
  const fontCss = buildFontFaceCss(faces);
  const styleEl = document.createElementNS('http://www.w3.org/2000/svg', 'style');
  styleEl.textContent = fontCss;
  clone.insertBefore(styleEl, clone.firstChild);

  // Tight viewBox + explicit pixel size.
  clone.setAttribute('viewBox', `${x} ${y} ${w} ${h}`);
  clone.setAttribute('width', String(Math.round(w)));
  clone.setAttribute('height', String(Math.round(h)));
  clone.removeAttribute('style');
  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');

  return { svgEl: clone, width: Math.round(w), height: Math.round(h) };
}

function serializeSvg(svgEl) {
  const ser = new XMLSerializer();
  let str = ser.serializeToString(svgEl);
  if (!str.startsWith('<?xml')) {
    str = '<?xml version="1.0" encoding="UTF-8"?>\n' + str;
  }
  return str;
}

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function timestampStub() {
  const d = new Date();
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
}

async function exportSVG({ mode = 'visible' } = {}) {
  // Make sure fonts are loaded so @font-face URL fetches resolve.
  if (document.fonts && document.fonts.ready) {
    try { await document.fonts.ready; } catch (_) {}
  }
  const { svgEl } = await buildExportSvg({ mode });
  const str = serializeSvg(svgEl);
  const blob = new Blob([str], { type: 'image/svg+xml;charset=utf-8' });
  downloadBlob(blob, `block-builder-${timestampStub()}.svg`);
}

async function exportPNG({ scale = 2, mode = 'visible' } = {}) {
  if (document.fonts && document.fonts.ready) {
    try { await document.fonts.ready; } catch (_) {}
  }
  const { svgEl, width, height } = await buildExportSvg({ mode });
  const str = serializeSvg(svgEl);

  const svgBlob = new Blob([str], { type: 'image/svg+xml;charset=utf-8' });
  const url = URL.createObjectURL(svgBlob);

  try {
    const img = await new Promise((resolve, reject) => {
      const im = new Image();
      im.onload = () => resolve(im);
      im.onerror = () => reject(new Error('Failed to load SVG into Image'));
      im.src = url;
    });

    const canvas = document.createElement('canvas');
    canvas.width = Math.max(1, Math.round(width * scale));
    canvas.height = Math.max(1, Math.round(height * scale));
    const ctx = canvas.getContext('2d');
    ctx.scale(scale, scale);
    ctx.drawImage(img, 0, 0, width, height);

    const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
    if (!blob) throw new Error('Canvas toBlob returned null');
    downloadBlob(blob, `block-builder-${timestampStub()}.png`);
  } finally {
    URL.revokeObjectURL(url);
  }
}

window.BlockBuilderExport = { exportPNG, exportSVG };
