/**
 * Text-element geometry helpers.
 *
 * These keep edits to a text element's `text` / `fontFamily` / `fontSize`
 * feeling stable to the user:
 *
 *   • `withTextCenterPreservation` recomputes (x, y) on each bbox-changing
 *     edit so the visual center of the text stays put. Without this, typing
 *     into a textarea would make on-canvas text drift to the right (since x
 *     is the top-left anchor and the bbox grows rightward as text gets
 *     longer). Same for switching to a wider font, or bumping the size.
 *
 *   • `fontSizeToPreserveWidth` returns a fontSize that, when paired with a
 *     new font family, keeps the bbox width approximately equal to the
 *     current bbox width. Bidirectional: a swap to a wider font shrinks
 *     the size, a swap to a narrower font grows it. Clamped to
 *     [MIN_FONT_SIZE, MAX_FONT_SIZE] so a pathological measurement
 *     can't produce 4px or 600px text.
 *
 * Both helpers are pure: they take old-state + a partial attrs patch and
 * return a (possibly augmented) attrs patch ready to feed to `updateElement`.
 */

import { measureTextWidth, measureTextMetrics } from './textMetrics';
import {
  DEFAULT_DISPLAY_FONT,
  DEFAULT_FONT_SIZE,
  LINE_HEIGHT_RATIO,
  MIN_FONT_SIZE,
  MAX_FONT_SIZE,
} from '../constants/textMetrics';

// Aliased here so the existing local-name references below keep working.
// Centralizing the actual values in constants/textMetrics.js prevents
// drift between this module, elementBounds.js, and TextElement.jsx.
const DEFAULT_FONT_FAMILY = DEFAULT_DISPLAY_FONT;

// Konva.Text places the baseline at this fraction of the line-height
// box's height. Used below by computeTextVisibleBbox; mirrored in
// TextElement.jsx as a local `baselineY = approxH * 0.85`. Pulling
// it out here as a named constant makes the cross-file pairing
// explicit.
const BASELINE_Y_RATIO = 0.85;

// Arc bulge magnitude as a fraction of approxH × |arc|. Mirrors
// TextElement.jsx's `bulgeMag = (arcAbs / 100) * approxH * 1.4`.
const ARC_BULGE_PER_UNIT = 1.4;

/**
 * If the patch changes any bbox-affecting attribute (text, fontFamily,
 * fontSize), augment it with x/y values that keep the visual center of the
 * text element constant.
 *
 * Pass-through cases:
 *   • non-text element              → returned unchanged
 *   • caller already set x or y     → returned unchanged (caller wins)
 *   • patch doesn't change the bbox → returned unchanged
 */
export function withTextCenterPreservation(element, attrs) {
  if (!element || element.type !== 'text') return attrs;
  if (attrs.x !== undefined || attrs.y !== undefined) return attrs;

  const textChanged = attrs.text !== undefined;
  const fontChanged = attrs.fontFamily !== undefined;
  const sizeChanged = attrs.fontSize !== undefined;
  if (!textChanged && !fontChanged && !sizeChanged) return attrs;

  const oldText = element.text ?? '';
  const oldFamily = element.fontFamily ?? DEFAULT_FONT_FAMILY;
  const oldSize = element.fontSize ?? DEFAULT_FONT_SIZE;
  const newText = textChanged ? attrs.text : oldText;
  const newFamily = fontChanged ? attrs.fontFamily : oldFamily;
  const newSize = sizeChanged ? attrs.fontSize : oldSize;

  const oldW = measureTextWidth(oldText, oldSize, oldFamily);
  const newW = measureTextWidth(newText, newSize, newFamily);
  const oldH = oldSize * LINE_HEIGHT_RATIO;
  const newH = newSize * LINE_HEIGHT_RATIO;

  const oldX = element.x ?? 0;
  const oldY = element.y ?? 0;
  const centerX = oldX + oldW / 2;
  const centerY = oldY + oldH / 2;

  return {
    ...attrs,
    x: centerX - newW / 2,
    y: centerY - newH / 2,
  };
}

/**
 * Pick a fontSize that keeps the rendered text width unchanged when
 * switching fonts — *bidirectional*. A swap to a wider font shrinks the
 * size; a swap to a narrower font grows it. Both directions are clamped
 * to [MIN_FONT_SIZE, MAX_FONT_SIZE].
 *
 * Earlier versions of this helper only shrank — the user's chosen size
 * was treated as a ceiling and narrower fonts were allowed to render
 * at a smaller visual area. That produced the exact "why did my text
 * just get tiny?" complaint the rename addressed: pick wide font at
 * size 48, see a comfortable bbox; pick a condensed font, see the
 * same 48 but rendered at half the visual width. Users expect the
 * area to feel constant across font swaps; this function delivers
 * that symmetrically.
 *
 * Returns `oldFontSize` unchanged if `text` is empty or the measurement
 * subsystem returns 0 for the new font (defensive — avoids division
 * by zero and avoids resetting to MAX_FONT_SIZE on a measurement glitch).
 */
export function fontSizeToPreserveWidth(text, oldFontFamily, oldFontSize, newFontFamily) {
  if (!text) return oldFontSize;
  const currentW = measureTextWidth(text, oldFontSize, oldFontFamily);
  const newWAtSameSize = measureTextWidth(text, oldFontSize, newFontFamily);
  if (currentW <= 0 || newWAtSameSize <= 0) return oldFontSize;
  // Same ratio formula both ways: target = oldSize * (currentW / newWAtSameSize).
  //   newWAtSameSize > currentW  → new font is WIDER → ratio < 1 → shrink
  //   newWAtSameSize < currentW  → new font is NARROWER → ratio > 1 → grow
  //   newWAtSameSize === currentW → returns oldSize unchanged (the ratio is 1)
  const targetSize = oldFontSize * (currentW / newWAtSameSize);
  return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, Math.round(targetSize)));
}

/**
 * Compute initial placement for a newly-created text element so that it
 * lands centered in the working area. Auto-shrinks fontSize when the text
 * at the requested size would be wider than the canvas (with a small
 * margin), so the new element is fully visible from the moment it appears.
 *
 * @param {Object} draft           - { text, fontFamily, fontSize }
 * @param {number} canvasSize      - design canvas size in design units
 * @param {number} marginRatio     - max content width as fraction of canvas
 *                                   (default 0.9 — 5% margin on each side)
 * @returns {{ x, y, fontSize }}   - top-left position + (possibly clamped) size
 */
export function placeTextCentered(draft, canvasSize, marginRatio = 0.9) {
  const text = draft.text ?? '';
  const fontFamily = draft.fontFamily ?? DEFAULT_FONT_FAMILY;
  let fontSize = draft.fontSize ?? DEFAULT_FONT_SIZE;

  let measuredW = measureTextWidth(text, fontSize, fontFamily);
  const maxW = canvasSize * marginRatio;

  if (measuredW > maxW && measuredW > 0) {
    fontSize = Math.max(MIN_FONT_SIZE, Math.floor(fontSize * (maxW / measuredW)));
    measuredW = measureTextWidth(text, fontSize, fontFamily);
  }
  const measuredH = fontSize * LINE_HEIGHT_RATIO;

  const x = Math.max(0, Math.round((canvasSize - measuredW) / 2));
  const y = Math.max(0, Math.round((canvasSize - measuredH) / 2));

  return { x, y, fontSize };
}

/**
 * Compute the VISIBLE-INK bounding box of a text element in node-local
 * coordinates. This is the rectangle that hugs the actual rendered
 * glyphs — NOT the line-height box (which contains substantial empty
 * space above the baseline and below the descent).
 *
 * Single source of truth for two consumers:
 *
 *   1. TextElement.jsx: drives the in-canvas yellow OOB indicator rect,
 *      the Konva node's getSelfRect override (which controls Transformer
 *      handle positions), and the _pawSnapBbox custom attrs that
 *      DesignCanvas's snap-to-center math reads.
 *   2. elementBounds.js: drives the print-area out-of-bounds check that
 *      flags elements as OOB and disables Add-to-Cart.
 *
 * Both consumers need the SAME envelope or the user sees inconsistency:
 * "the yellow rect looks fine but the warning still fires," or vice-
 * versa. Sharing this function eliminates the drift risk.
 *
 * Returns null for empty or zero-width text.
 *
 * Per-character iteration for arc'd text
 * ────────────────────────────────────────
 * Earlier versions of this calculation used the WHOLE-STRING ascent
 * (max ascent of any character) at the arc's peak, plus the whole-string
 * descent at the trough. That over-estimated the bbox for the common
 * case of mixed-case text:
 *
 *   For "Queen" arc-up at fontSize 50 with arc=80:
 *     baselineY = 51, bulgeMag/2 = 33.6, peakY = 17.4
 *     whole-string ascent (the "Q") = ~35
 *     peakY - wholeAscent = -17.6   ← OLD top of bbox
 *     But the letter AT the peak is an "e" with ascent ~25:
 *     peakY - eAscent = -7.6        ← ACTUAL top of visible glyphs
 *
 *   A 10-unit overestimate — enough to false-flag arc-up text
 *   positioned within the print area but visually safe.
 *
 * The fix: iterate per character. For each character, compute its baseline
 * y-position along the quadratic Bezier path, then add its OWN ascent/
 * descent (measured for that specific character). Take min/max over all
 * characters. This naturally handles:
 *
 *   • Tall capitals at the endpoints (no overestimate at the bulge tip)
 *   • Tall capitals at the peak (correctly extends the bbox)
 *   • Short lowercase at the peak with tall caps at endpoints (the
 *     screenshotted "Queen" case)
 *   • Spaces and punctuation contributing zero ink at their positions
 *
 * Cost: O(N) measureText calls per evaluation where N is text length.
 * For typical strings (5-20 chars), this is sub-millisecond. The flat-
 * text path (no arc) short-circuits to a single measureText for the
 * whole string.
 *
 * Curve geometry and arc-length positioning (Change 25)
 * ──────────────────────────────────────────────────
 * The path is a quadratic Bezier from (0, baselineY) through
 * (W/2, controlY) to (W, baselineY). The baseline at parameter t is:
 *
 *   y(t) = (1-t)² × baselineY + 2(1-t)t × controlY + t² × baselineY
 *
 * CRITICAL: Konva.TextPath with align="center" distributes characters
 * along the path by ARC LENGTH, not by uniform parameter t. For a
 * quadratic Bezier the velocity |B'(t)| = sqrt(W² + (2(1-2t)(controlY -
 * baselineY))²) is HIGH at endpoints (t=0, t=1) and LOW at the middle
 * (t=0.5). So arc length covered per unit t is largest at the ends,
 * smallest at the middle.
 *
 * An earlier version of this code approximated u_i ≈ (i + 0.5) / N
 * (uniform-t spacing). That pushed endpoint characters TOO FAR toward
 * the middle of the path: for the "Bun-teria" arc=-50 case, char 0 (B)
 * actually sits near t ≈ 0.11, but the approximation placed it at
 * t = 0.056. The 0.05 difference at the endpoints translated to ~3.6
 * design units of charBaselineY error, which made the per-char top
 * appear ~3.6 units higher than where the glyph actually renders — and
 * the same error on the opposite endpoint. Visually: the Transformer
 * rect's top sat above the actual B cap (empty space above), and the
 * rect's bottom sat above the actual peak descender (letters extended
 * below).
 *
 * The fix: numerically build an arc-length-vs-t table by sampling the
 * Bezier at 100 t values, then for each character compute its desired
 * arc length along the path (text start offset + cumulative char widths
 * + char's own width / 2) and binary-search the table for the
 * corresponding t. The result matches Konva's internal char placement
 * to within sub-pixel precision for typical arcs/text lengths.
 *
 * Cost: O(SAMPLES + N log SAMPLES) per evaluation. With SAMPLES = 100
 * and N ≤ ~30 in practice, this is still well under a millisecond.
 *
 * @param {Object} params
 * @param {string} params.text
 * @param {number} params.fontSize
 * @param {string} params.fontFamily
 * @param {number} [params.arc] - in [-100, 100]
 * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number } | null}
 */
export function computeTextVisibleBbox({ text, fontSize, fontFamily, arc = 0 }) {
  const t = text ?? '';
  const fSize = fontSize ?? DEFAULT_FONT_SIZE;
  const fFamily = fontFamily ?? DEFAULT_FONT_FAMILY;

  const wholeMetrics = measureTextMetrics(t, fSize, fFamily);
  const approxW = Math.max(fSize, wholeMetrics.width);
  const approxH = fSize * LINE_HEIGHT_RATIO;

  if (approxW <= 0 || approxH <= 0) return null;

  const baselineY = approxH * BASELINE_Y_RATIO;
  const arcAbs = Math.min(100, Math.abs(arc ?? 0));
  const isArced = arcAbs > 0.5;
  const bulgeMag = (arcAbs / 100) * approxH * ARC_BULGE_PER_UNIT;
  const controlY = (arc ?? 0) > 0 ? baselineY - bulgeMag : baselineY + bulgeMag;

  let minY, maxY;

  if (!isArced || t.length === 0) {
    // Flat text (or empty arc'd text): single baseline at baselineY,
    // whole-string ascent/descent envelopes the bbox.
    minY = baselineY - wholeMetrics.ascent;
    maxY = baselineY + wholeMetrics.descent;
  } else {
    // Arc'd text: per-character envelope with arc-length-based
    // positioning to match Konva.TextPath's internal placement.
    //
    // Path geometry mirrors TextElement.jsx: same `pathW = approxW * 1.02`
    // overshoot, same quadratic Bezier from (0, baselineY) through
    // (pathW/2, controlY) to (pathW, baselineY). align="center" centers
    // the rendered text along the path.
    const pathW = approxW * 1.02;

    // Step 1: Build an arc-length-vs-t lookup table by sampling the
    // Bezier curve. 100 samples gives sub-pixel accuracy for typical
    // arcs. Storage: ~1.6 KiB transient. Build time: ~0.1 ms.
    const SAMPLES = 100;
    const arcTable = new Array(SAMPLES + 1);
    arcTable[0] = { t: 0, s: 0 };
    let cumLen = 0;
    let prevX = 0;
    let prevY = baselineY;
    for (let k = 1; k <= SAMPLES; k++) {
      const tk = k / SAMPLES;
      const oneMinusTk = 1 - tk;
      // Bezier(tk) for x: P0.x=0, P1.x=pathW/2, P2.x=pathW
      //   x(tk) = 2*tk*(1-tk) * (pathW/2) + tk² * pathW
      //         = tk*(1-tk) * pathW + tk² * pathW
      //         = tk * pathW * ((1-tk) + tk)
      //         = tk * pathW
      //   (For symmetric x endpoints with control at midpoint, the
      //    x-coordinate is exactly linear in t — the curvature is
      //    entirely in y.)
      const xk = tk * pathW;
      const yk = oneMinusTk * oneMinusTk * baselineY +
                 2 * tk * oneMinusTk * controlY +
                 tk * tk * baselineY;
      const dx = xk - prevX;
      const dy = yk - prevY;
      cumLen += Math.sqrt(dx * dx + dy * dy);
      arcTable[k] = { t: tk, s: cumLen };
      prevX = xk;
      prevY = yk;
    }
    const totalArcLen = cumLen;

    // Step 2: Compute the text's start offset along the path. With
    // align="center", the rendered text occupies the middle of the
    // path, with equal slack on either side. The slack is the
    // difference between the path's arc length and the text's total
    // rendered width.
    const textTotalWidth = wholeMetrics.width;
    const textStartS = (totalArcLen - textTotalWidth) / 2;

    // Step 3: Iterate characters. For each char, compute its center's
    // arc length position along the path, then binary-search the
    // table to find the corresponding t. Use that t to evaluate the
    // Bezier y, giving the char's baseline-along-the-path.
    //
    // Whole-string descent floor (Change 27)
    // ----------------------------------------
    // Per-character `measureTextMetrics(singleChar)` for cursive fonts
    // like Pacifico under-reports the actual rendered descent: when a
    // character is rendered in isolation the browser draws just the
    // base glyph, but inside a word/path the cursive flourishes and
    // bottom swirls add several units of depth that the standalone
    // measurement misses. Even the whole-string actualBoundingBoxDescent
    // can under-report for cursive faces because the browser measures
    // the visible ink envelope, and stroke joins / hairlines along the
    // baseline don't always count.
    //
    // Use fontBoundingBoxDescent as a more conservative floor. This is
    // the font's DECLARED max descent across all glyphs in the font
    // — for cursive faces that includes the deepest possible flourish,
    // so it captures the visual extent that actualBoundingBoxDescent
    // misses. For non-cursive fonts the gap between actual and font
    // descent is smaller (closer to the descender of g/p/q/y), so this
    // adds a small safety margin but doesn't over-extend dramatically.
    //
    // (We do NOT apply a parallel ascent floor: for mixed-case strings
    // like "Queen" we WANT lowercase peak letters to use their own
    // smaller ascent so the bbox top doesn't extend up to cap-height
    // when only an 'e' is at the peak. Cursive flourishes happen
    // overwhelmingly at the bottom, not the top.)
    const wholeDescentFloor = Math.max(
      wholeMetrics.descent,
      wholeMetrics.fontDescent,
    );
    minY = Infinity;
    maxY = -Infinity;
    let cumCharS = 0;
    const N = t.length;
    for (let i = 0; i < N; i++) {
      const ch = t[i];
      const cm = measureTextMetrics(ch, fSize, fFamily);
      const charCenterS = textStartS + cumCharS + cm.width / 2;
      cumCharS += cm.width;

      // Binary search arcTable for the entry pair bracketing charCenterS.
      let lo = 0;
      let hi = arcTable.length - 1;
      while (lo < hi - 1) {
        const mid = (lo + hi) >> 1;
        if (arcTable[mid].s <= charCenterS) lo = mid;
        else hi = mid;
      }
      const lo_s = arcTable[lo].s;
      const hi_s = arcTable[hi].s;
      const lo_t = arcTable[lo].t;
      const hi_t = arcTable[hi].t;
      let u_i;
      if (hi_s === lo_s) {
        u_i = lo_t;
      } else {
        u_i = lo_t + (hi_t - lo_t) * (charCenterS - lo_s) / (hi_s - lo_s);
      }
      // Clamp to [0, 1] in case the text overflows the path (shouldn't
      // happen with the 2% overshoot, but defensive).
      if (u_i < 0) u_i = 0;
      else if (u_i > 1) u_i = 1;

      const charBaselineY = baselineY - 2 * u_i * (1 - u_i) * (baselineY - controlY);
      const effectiveDescent = Math.max(cm.descent, wholeDescentFloor);
      const top = charBaselineY - cm.ascent;
      const bot = charBaselineY + effectiveDescent;
      if (top < minY) minY = top;
      if (bot > maxY) maxY = bot;
    }
  }

  return {
    minX: 0,
    minY,
    maxX: approxW,
    maxY,
    width: approxW,
    height: maxY - minY,
  };
}
