/**
 * Element bounds checking.
 *
 * The "print area" is the design canvas — a CANVAS_SIZE × CANVAS_SIZE square
 * (currently 300×300 in design units for shirts). Any element whose rotated
 * axis-aligned bounding box extends outside that square will be cropped at
 * print time. We surface a warning + block Add-to-Cart when that happens, so
 * a user never accidentally buys a shirt with their pet's ears guillotined.
 *
 * The math here mirrors the AABB calculation in DesignCanvas's
 * `constrainTransform` (the M4 hot-path bound function), but operates on
 * stored element state instead of mid-transform Konva data:
 *
 *   • Element x/y is the top-left corner in design coordinates.
 *   • For images: width/height come straight from element.
 *   • For text: we delegate to `computeTextVisibleBbox` in textGeometry.js,
 *     which returns the visible-ink envelope (the rectangle hugging the
 *     actual rendered glyphs, not the line-height box that contains
 *     substantial empty space above the baseline). For arc'd text the
 *     envelope is computed per-character along the curve, so a mixed-case
 *     string like "Queen" with lowercase letters at the arc peak gets a
 *     tight bbox there rather than the over-conservative whole-string
 *     cap-height ascent we used before (Change 23).
 *   • The same `computeTextVisibleBbox` feeds TextElement.jsx's in-canvas
 *     OOB indicator rect, snap-bbox custom attrs, and Transformer getSelfRect
 *     override — so the warning chip, the yellow tint rect, the
 *     selection handles, and the snap center are all driven by one
 *     calculation. No drift between visual and bounds.
 *   • Rotation is applied around (x, y) — the same convention Konva uses
 *     when no offset is set, which is how unflipped elements render.
 *   • Flipped elements have render-time offset adjustments, but element.x
 *     / element.y are stored as the un-flipped top-left, so this math is
 *     correct for them too.
 *
 * The tolerance (BOUNDS_EPSILON) absorbs sub-pixel rounding artifacts so
 * an element snapped exactly to the canvas edge isn't flagged due to a
 * 0.0001-unit floating-point residual.
 */

import { computeTextVisibleBbox } from './textGeometry';
import {
  DEFAULT_DISPLAY_FONT,
  DEFAULT_FONT_SIZE,
  MIN_FONT_SIZE,
} from '../constants/textMetrics';
import { MIN_ELEMENT_SIZE } from '../constants/elements';

const BOUNDS_EPSILON = 0.5;

/**
 * Compute the rotated AABB of an element, in design coordinates.
 * Returns null for element types we don't render (so they're treated as
 * "in bounds" by callers — no false warnings for unknown types).
 *
 * For text elements, the unrotated rect is the VISIBLE-INK envelope from
 * `computeTextVisibleBbox`. See that function's docblock for the
 * per-character arc geometry.
 */
function getElementAABB(element) {
  if (!element) return null;

  // Unrotated rect corners in node-local coords. For images this is
  // simply [0, w] × [0, h]. For text it's the visible-ink envelope.
  let minLocalX, maxLocalX, minLocalY, maxLocalY;

  if (element.type === 'image' || element.type === 'sticker') {
    const w = element.width || 0;
    const h = element.height || 0;
    if (w <= 0 || h <= 0) return null;
    minLocalX = 0;
    maxLocalX = w;
    minLocalY = 0;
    maxLocalY = h;
  } else if (element.type === 'text') {
    const bbox = computeTextVisibleBbox({
      text: element.text ?? '',
      fontSize: element.fontSize ?? DEFAULT_FONT_SIZE,
      fontFamily: element.fontFamily ?? DEFAULT_DISPLAY_FONT,
      arc: element.arc ?? 0,
    });
    if (!bbox) return null;
    minLocalX = bbox.minX;
    maxLocalX = bbox.maxX;
    minLocalY = bbox.minY;
    maxLocalY = bbox.maxY;
  } else {
    return null;
  }

  const x = element.x ?? 0;
  const y = element.y ?? 0;
  const rad = ((element.rotation ?? 0) * Math.PI) / 180;
  const cos = Math.cos(rad);
  const sin = Math.sin(rad);

  // Rotate the four corners of the unrotated rect around (x, y).
  // (dx, dy) → (x + dx*cos - dy*sin, y + dx*sin + dy*cos)
  // For non-text elements minLocalY=0 and maxLocalY=h, which reduces
  // to the original four-corner computation. For text the local Y
  // range may include negative values (arc-up rising above y=0 in
  // node coords) and the rotation math handles that fine.
  const tlX = x + minLocalX * cos - minLocalY * sin; const tlY = y + minLocalX * sin + minLocalY * cos;
  const trX = x + maxLocalX * cos - minLocalY * sin; const trY = y + maxLocalX * sin + minLocalY * cos;
  const blX = x + minLocalX * cos - maxLocalY * sin; const blY = y + minLocalX * sin + maxLocalY * cos;
  const brX = x + maxLocalX * cos - maxLocalY * sin; const brY = y + maxLocalX * sin + maxLocalY * cos;

  return {
    minX: Math.min(tlX, trX, blX, brX),
    maxX: Math.max(tlX, trX, blX, brX),
    minY: Math.min(tlY, trY, blY, brY),
    maxY: Math.max(tlY, trY, blY, brY),
  };
}

/**
 * True if the element's rotated AABB extends outside the [0, canvasSize]²
 * print area, beyond the small floating-point tolerance.
 */
export function isElementOutOfBounds(element, canvasSize) {
  const aabb = getElementAABB(element);
  if (!aabb) return false;
  return (
    aabb.minX < -BOUNDS_EPSILON
    || aabb.maxX > canvasSize + BOUNDS_EPSILON
    || aabb.minY < -BOUNDS_EPSILON
    || aabb.maxY > canvasSize + BOUNDS_EPSILON
  );
}

/**
 * Count of elements currently outside the print area. Returns 0 for an
 * empty array, so callers can treat 0 as "no warning, cart enabled".
 */
export function countOutOfBounds(elements, canvasSize) {
  if (!elements || elements.length === 0) return 0;
  let count = 0;
  for (const el of elements) {
    if (isElementOutOfBounds(el, canvasSize)) count++;
  }
  return count;
}

/**
 * Adjust an element so its rotated AABB fits inside [0, canvasSize]².
 *
 * Strategy
 * ────────
 *   1. If the AABB is already inside the canvas (within tolerance), the
 *      element is returned unchanged.
 *   2. If the AABB is wider or taller than the canvas, scale the element
 *      down uniformly so the AABB fits with a 5% margin. Scaling drives
 *      width/height for images and fontSize for text — both preserve
 *      aspect ratio implicitly.
 *   3. After any scale, translate (x, y) so any remaining out-of-bounds
 *      AABB shifts back inside.
 *
 * Used by template loading to keep authoring mistakes from breaking the
 * cart-disable check on first paint. A console.warn fires when fitting
 * actually changes anything so template authors notice during development.
 */
export function fitElementToBounds(element, canvasSize) {
  const aabb = getElementAABB(element);
  if (!aabb) return element;

  const inside =
    aabb.minX >= -BOUNDS_EPSILON
    && aabb.maxX <= canvasSize + BOUNDS_EPSILON
    && aabb.minY >= -BOUNDS_EPSILON
    && aabb.maxY <= canvasSize + BOUNDS_EPSILON;
  if (inside) return element;

  const updates = {};

  // Step 1: uniform scale-down if AABB is bigger than the canvas.
  const aabbW = aabb.maxX - aabb.minX;
  const aabbH = aabb.maxY - aabb.minY;
  const maxW = canvasSize * 0.95;
  const maxH = canvasSize * 0.95;
  let scale = 1;
  if (aabbW > maxW || aabbH > maxH) {
    scale = Math.min(maxW / aabbW, maxH / aabbH);
  }

  if (scale < 1) {
    if (element.type === 'image' || element.type === 'sticker') {
      updates.width = Math.max(MIN_ELEMENT_SIZE, Math.round((element.width || 100) * scale));
      updates.height = Math.max(MIN_ELEMENT_SIZE, Math.round((element.height || 100) * scale));
    } else if (element.type === 'text') {
      updates.fontSize = Math.max(MIN_FONT_SIZE, Math.round((element.fontSize || DEFAULT_FONT_SIZE) * scale));
    }
  }

  // Step 2: translate to bring the (possibly scaled) AABB inside the
  // canvas. We re-measure after scaling so the translate uses the new
  // dimensions, otherwise we'd over-shift.
  const scaled = Object.keys(updates).length > 0 ? { ...element, ...updates } : element;
  const newAabb = getElementAABB(scaled);
  if (newAabb) {
    let dx = 0;
    let dy = 0;
    if (newAabb.minX < 0) dx = -newAabb.minX;
    else if (newAabb.maxX > canvasSize) dx = canvasSize - newAabb.maxX;
    if (newAabb.minY < 0) dy = -newAabb.minY;
    else if (newAabb.maxY > canvasSize) dy = canvasSize - newAabb.maxY;
    if (dx !== 0 || dy !== 0) {
      updates.x = (element.x ?? 0) + dx;
      updates.y = (element.y ?? 0) + dy;
    }
  }

  return Object.keys(updates).length > 0 ? { ...element, ...updates } : element;
}

/**
 * Map `fitElementToBounds` over an array, returning a new array. If any
 * element actually changes, log a console.warn so template authors notice
 * the runtime correction — these are usually authoring bugs that should
 * be fixed at the template-data level rather than relied on as runtime
 * safety nets.
 */
export function fitElementsToBounds(elements, canvasSize) {
  if (!elements || elements.length === 0) return elements || [];
  let mutatedCount = 0;
  const out = elements.map((el) => {
    const fitted = fitElementToBounds(el, canvasSize);
    if (fitted !== el) mutatedCount++;
    return fitted;
  });
  if (mutatedCount > 0 && typeof console !== 'undefined') {
    console.warn(
      `[elementBounds] Auto-fit ${mutatedCount} element${mutatedCount === 1 ? '' : 's'} `
      + `to canvas bounds. The source data has elements outside [0, ${canvasSize}]² — `
      + 'this is usually a template authoring bug worth fixing at the data layer.'
    );
  }
  return out;
}
