/**
 * Synchronous text-width measurement.
 *
 * Uses a single shared offscreen canvas's 2D context to measure exactly how
 * wide a piece of text will render at a given font size and family. This is
 * the same pixel-perfect measurement the browser uses for `<canvas>` text
 * rendering, so it matches what Konva ends up drawing.
 *
 * Why we need this
 * ────────────────
 * Konva's `<TextPath>` renders text along a path. If the path is shorter
 * than the rendered text width, Konva silently clips the trailing
 * characters; if the path is longer, the visual midpoint of the path (and
 * any curvature peak) lands past the visual midpoint of the text.
 *
 * Both failure modes are font-dependent: a wide serif overflows, a
 * condensed display face like Bebas Neue under-flows. The fix is to ask the
 * browser for the actual rendered width at the given font and use that as
 * the path length.
 *
 * Caching
 * ───────
 * The canvas + 2D context is created lazily on first call and reused for
 * every subsequent call — measureText is a hot-path operation in some text
 * components, and creating canvases per call would be wasteful.
 *
 * SSR / non-DOM safety
 * ────────────────────
 * In environments without `document` (SSR, tests), we fall back to a rough
 * geometric estimate (`fontSize × length × 0.55`). The estimate is good
 * enough to hydrate without layout-shift and is overwritten by the real
 * measurement on the first client render.
 */

let _measureCtx = null;

function getMeasureCtx() {
  if (_measureCtx) return _measureCtx;
  if (typeof document === 'undefined') return null;
  const canvas = document.createElement('canvas');
  // Tiny canvas — we only use it for measureText, never for drawing.
  canvas.width = 1;
  canvas.height = 1;
  _measureCtx = canvas.getContext('2d');
  return _measureCtx;
}

/**
 * Wrap a font-family string for use as the family slot in CSS's font
 * shorthand. Multi-word names (e.g. "Bebas Neue", "Roboto Slab") must
 * be quoted or the parser treats them as two separate identifiers, and
 * canvas `ctx.font` is stricter about this than `style.fontFamily`.
 * Names that already contain quotes are passed through unchanged so
 * callers who pre-quote don't end up double-wrapped. Single-word
 * generic families like `serif` / `sans-serif` / `monospace` are
 * passed through as-is so they keep their generic-family semantics.
 */
const GENERIC_FAMILIES = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui']);
function quoteFontFamily(family) {
  if (typeof family !== 'string') return family;
  const trimmed = family.trim();
  if (!trimmed) return trimmed;
  // Already quoted (caller wrapped it). Leave alone.
  if (trimmed.startsWith('"') || trimmed.startsWith("'")) return trimmed;
  // Generic CSS family keywords — don't quote, or canvas treats them
  // as a literal name and falls back to default.
  if (GENERIC_FAMILIES.has(trimmed)) return trimmed;
  return `"${trimmed}"`;
}

/**
 * Return the width in CSS pixels of `text` rendered at `fontSize` in
 * `fontFamily`. Falls back to a geometric approximation when no DOM is
 * available.
 *
 * @param {string} text
 * @param {number} fontSize - in pixels
 * @param {string} fontFamily - any CSS font-family value (will be passed
 *   through; commas/quotes are fine)
 * @returns {number} width in pixels
 */
export function measureTextWidth(text, fontSize, fontFamily) {
  if (!text) return 0;
  const ctx = getMeasureCtx();
  if (!ctx) {
    // Fallback approximation; close enough to keep layout stable until
    // hydration completes and the real measurement takes over.
    return fontSize * Math.max(1, text.length) * 0.55;
  }
  // Match Konva's default style so our measurement aligns with what Konva
  // actually renders. Konva defaults to non-bold non-italic.
  // Family name is quoted via quoteFontFamily — ctx.font requires
  // quotes around multi-word names or it silently falls back to the
  // default system font, returning wrong metrics.
  ctx.font = `${fontSize}px ${quoteFontFamily(fontFamily)}`;
  return ctx.measureText(text).width;
}

/**
 * Return width, ascent, and descent for `text` rendered at `fontSize` in
 * `fontFamily`. Same canvas-measurement basis as measureTextWidth, but
 * also returns the actual rendered glyph ascent/descent from the
 * TextMetrics object — i.e. how far the glyphs reach above and below the
 * baseline for THIS specific string.
 *
 * Why this exists separately from measureTextWidth
 * ──────────────────────────────────────────────
 * Snap-to-center math in DesignCanvas needs the visible-ink bbox so the
 * snap centers what the user actually sees, not the abstract Konva.Text
 * line-height bbox (which has empty space above the baseline). For that
 * we need ascent/descent specifically. fontSize × 0.7 and fontSize × 0.2
 * are reasonable estimates, but they're wrong for fonts with unusual
 * proportions — script faces with tall capitals, condensed display
 * faces, or text containing tall punctuation. The TextMetrics API gives
 * us the real values per-text, per-font.
 *
 * Browser support: actualBoundingBoxAscent/Descent are available in
 * Chrome 77+, Firefox 74+, Safari 11.1+. Fallback to fontSize ratios
 * preserves correctness on older engines (none of which we actually
 * target, but defensive defaults are cheap).
 *
 * @param {string} text
 * @param {number} fontSize - in pixels
 * @param {string} fontFamily
 * @returns {{ width: number, ascent: number, descent: number }}
 */
export function measureTextMetrics(text, fontSize, fontFamily) {
  // Fallbacks used when DOM is unavailable or when the TextMetrics API
  // doesn't return the bounding-box fields (very old browsers).
  const fallbackAscent = fontSize * 0.7;
  const fallbackDescent = fontSize * 0.2;
  // fontBoundingBox* are the font's declared max ascent/descent across
  // ALL its glyphs — useful as a floor for cursive faces where
  // actualBoundingBoxDescent under-reports the rendered tails/swirls.
  const fallbackFontAscent = fontSize * 0.85;
  const fallbackFontDescent = fontSize * 0.25;

  if (!text) {
    return {
      width: 0,
      ascent: fallbackAscent,
      descent: fallbackDescent,
      fontAscent: fallbackFontAscent,
      fontDescent: fallbackFontDescent,
    };
  }
  const ctx = getMeasureCtx();
  if (!ctx) {
    return {
      width: fontSize * Math.max(1, text.length) * 0.55,
      ascent: fallbackAscent,
      descent: fallbackDescent,
      fontAscent: fallbackFontAscent,
      fontDescent: fallbackFontDescent,
    };
  }
  ctx.font = `${fontSize}px ${quoteFontFamily(fontFamily)}`;
  const m = ctx.measureText(text);
  return {
    width: m.width,
    ascent: m.actualBoundingBoxAscent ?? fallbackAscent,
    descent: m.actualBoundingBoxDescent ?? fallbackDescent,
    fontAscent: m.fontBoundingBoxAscent ?? fallbackFontAscent,
    fontDescent: m.fontBoundingBoxDescent ?? fallbackFontDescent,
  };
}
