// Canvas-2D call-text renderer, ported faithfully from the user's two
// reference generators ("glossy bulle" / XV9Q and "western 3D" / HS0ZLE).
// SVG filters can only approximate these looks; the exact technique is
// canvas — inflated glyphs (fill + stroke to dilate), contour bands via
// boolean compositing, a candy gloss that hugs the outline, an orange inner
// shadow, and organic multi-layer grunge.
//
// renderTextFx() runs the recipe offscreen, trims the transparent padding,
// and returns a PNG data URL + its size in logical (card) pixels. CardPreview
// embeds that as an <image> so the same output drives the editor preview,
// thumbnails and the final e-mail rasterization.

export type TextFxKind = 'glossy' | 'western';

export interface TextFxParams {
  kind: TextFxKind;
  text: string;
  weight: string; // canvas font weight, e.g. "800" or "400"
  family: string; // canvas font family, e.g. '"Baloo 2", sans-serif'
  size: number; // font size in card px
  space: number; // letter spacing in px
  cTop: string;
  cMid: string;
  cBot: string;
  cDark: string; // dark edge / outline
  cOuter?: string; // silver rim (glossy only)
  // glossy
  plump?: number; // 0..0.06 — inflation as a fraction of size
  edge?: number; // 0..1 — dark edge weight
  outerw?: number; // 0..1 — silver rim weight
  gloss?: number; // 0..1 — gloss intensity
  glossH?: number; // 0..1 — gloss height
  glossI?: number; // 0..0.14 — gloss inset
  innerB?: number; // 0..1 — bottom inner shadow
  // western
  depth?: number; // 3D extrusion depth in px
  angle?: number; // extrusion direction in radians
  slant?: number; // shear, -0.4..0.4
  grunge?: number; // 0..1 — distress amount
  bevel?: number; // 0..1 — top-edge light
  seed?: number;
}

export interface TextFxResult {
  url: string; // PNG data URL of the trimmed text
  w: number; // logical (card-px) width
  h: number; // logical (card-px) height
}

const SS = 2; // supersample for crisp downscaling

function supportsFilter(c: CanvasRenderingContext2D) {
  return typeof c.filter === 'string';
}
function shade(hex: string, pct: number) {
  const n = parseInt(hex.slice(1), 16);
  const f = (v: number) => Math.max(0, Math.min(255, Math.round(v + (255 * pct) / 100)));
  return 'rgb(' + f(n >> 16) + ',' + f((n >> 8) & 255) + ',' + f(n & 255) + ')';
}
function deepen(hex: string) {
  const n = parseInt(hex.slice(1), 16);
  return 'rgb(' + Math.round((n >> 16) * 0.96) + ',' + Math.round(((n >> 8) & 255) * 0.62) + ',' + Math.round((n & 255) * 0.25) + ')';
}
function rustOf(hex: string) {
  const n = parseInt(hex.slice(1), 16);
  const r = Math.min(255, Math.round((n >> 16) * 0.72));
  const g = Math.round(((n >> 8) & 255) * 0.49);
  const b = Math.min(255, Math.round((n & 255) * 0.3) + 10);
  return 'rgb(' + r + ',' + g + ',' + b + ')';
}
function mulberry32(seed: number) {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
function hashStr(s: string) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

// ── glossy bubble (XV9Q) ────────────────────────────────────────────────
function renderGlossy(p: Required<Pick<TextFxParams, 'text' | 'weight' | 'family' | 'size' | 'space' | 'plump' | 'edge' | 'outerw' | 'gloss' | 'glossH' | 'glossI' | 'innerB' | 'cTop' | 'cMid' | 'cBot' | 'cDark' | 'cOuter'>>, scale: number): HTMLCanvasElement {
  const probe = document.createElement('canvas').getContext('2d')!;
  const font = p.weight + ' ' + p.size + 'px ' + p.family;
  probe.letterSpacing = p.space + 'px';
  probe.font = font;
  const m = probe.measureText(p.text);
  const asc = m.actualBoundingBoxAscent, desc = m.actualBoundingBoxDescent;
  const capH = asc + desc;
  const plumpPx = p.size * p.plump;
  const edgeW = p.size * 0.1 * p.edge;
  const outerW = edgeW + p.size * 0.22 * p.outerw + 2 * plumpPx;
  const pad = Math.ceil(outerW / 2 + p.size * 0.12);
  const w = Math.ceil(m.width + pad * 2);
  const h = Math.ceil(capH + pad * 2);

  const canvas = document.createElement('canvas');
  canvas.width = w * scale;
  canvas.height = h * scale;
  const c = canvas.getContext('2d')!;

  const x = pad, y = pad + asc;
  const top = y - asc - plumpPx, bot = y + desc + plumpPx;

  const mk = () => {
    const k = document.createElement('canvas');
    k.width = canvas.width;
    k.height = canvas.height;
    return k;
  };
  const setT = (cx: CanvasRenderingContext2D) => {
    cx.setTransform(scale, 0, 0, scale, 0, 0);
    cx.letterSpacing = p.space + 'px';
    cx.font = font;
    cx.textBaseline = 'alphabetic';
    cx.lineJoin = 'round';
    cx.lineCap = 'round';
  };
  const glyph = (delta: number, dx?: number, dy?: number) => {
    const k = mk();
    const g = k.getContext('2d')!;
    setT(g);
    g.fillStyle = '#fff';
    g.strokeStyle = '#fff';
    g.fillText(p.text, x + (dx || 0), y + (dy || 0));
    if (delta > 0.1) {
      g.lineWidth = delta * 2;
      g.strokeText(p.text, x + (dx || 0), y + (dy || 0));
    } else if (delta < -0.1) {
      g.globalCompositeOperation = 'destination-out';
      g.lineWidth = -delta * 2;
      g.strokeText(p.text, x + (dx || 0), y + (dy || 0));
    }
    return k;
  };
  const clipTo = (k: HTMLCanvasElement, mask: HTMLCanvasElement) => {
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'destination-in';
    g.drawImage(mask, 0, 0);
    g.restore();
    return k;
  };
  const contourBand = (shiftX: number, shiftY: number, insetPx: number) => {
    const k = glyph(plumpPx, 0, 0);
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'destination-out';
    g.drawImage(glyph(plumpPx, shiftX, shiftY), 0, 0);
    g.restore();
    return clipTo(k, glyph(plumpPx - insetPx, 0, 0));
  };
  const vRamp = (k: HTMLCanvasElement, f0: number, f1: number, keepTop: boolean) => {
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(scale, 0, 0, scale, 0, 0);
    g.globalCompositeOperation = 'destination-in';
    const gr = g.createLinearGradient(0, top, 0, bot);
    const a1 = keepTop ? 1 : 0, a2 = keepTop ? 0 : 1;
    gr.addColorStop(0, 'rgba(255,255,255,' + a1 + ')');
    gr.addColorStop(Math.max(0.001, Math.min(0.998, f0)), 'rgba(255,255,255,' + a1 + ')');
    gr.addColorStop(Math.max(0.002, Math.min(0.999, f1)), 'rgba(255,255,255,' + a2 + ')');
    gr.addColorStop(1, 'rgba(255,255,255,' + a2 + ')');
    g.fillStyle = gr;
    g.fillRect(0, 0, k.width / scale, k.height / scale);
    g.restore();
    return k;
  };
  const blurred = (src: HTMLCanvasElement, px: number) => {
    const k = mk();
    const g = k.getContext('2d')!;
    if (supportsFilter(g) && px > 0.3) {
      g.filter = 'blur(' + (px * scale).toFixed(2) + 'px)';
      g.drawImage(src, 0, 0);
      g.filter = 'none';
    } else {
      g.drawImage(src, 0, 0);
    }
    return k;
  };
  const tint = (k: HTMLCanvasElement, color: string, alpha: number) => {
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'source-in';
    g.fillStyle = color;
    g.fillRect(0, 0, k.width, k.height);
    g.restore();
    if (alpha < 1) {
      const k2 = mk();
      const g2 = k2.getContext('2d')!;
      g2.globalAlpha = alpha;
      g2.drawImage(k, 0, 0);
      return k2;
    }
    return k;
  };
  const paintText = (cx: CanvasRenderingContext2D, paint: string | CanvasGradient) => {
    cx.fillStyle = paint;
    cx.fillText(p.text, x, y);
    if (plumpPx > 0.1) {
      cx.strokeStyle = paint;
      cx.lineWidth = plumpPx * 2;
      cx.strokeText(p.text, x, y);
    }
  };

  c.setTransform(1, 0, 0, 1, 0, 0);
  c.clearRect(0, 0, canvas.width, canvas.height);
  setT(c);

  // drop shadow
  c.save();
  c.shadowColor = 'rgba(10,15,25,0.40)';
  c.shadowBlur = p.size * 0.045 * scale;
  c.shadowOffsetY = p.size * 0.03 * scale;
  c.lineWidth = Math.max(outerW, edgeW + 2 * plumpPx, 1);
  c.strokeStyle = 'rgba(10,15,25,0.40)';
  c.strokeText(p.text, x, y);
  c.restore();

  const face = mk();
  const f = face.getContext('2d')!;
  setT(f);

  if (p.outerw > 0) {
    const gOuter = f.createLinearGradient(0, top - outerW / 2, 0, bot + outerW / 2);
    gOuter.addColorStop(0, '#ffffff');
    gOuter.addColorStop(0.45, p.cOuter);
    gOuter.addColorStop(1, shade(p.cOuter, -22));
    f.lineWidth = outerW;
    f.strokeStyle = gOuter;
    f.strokeText(p.text, x, y);
  }
  if (p.edge > 0) {
    f.lineWidth = edgeW + 2 * plumpPx;
    f.strokeStyle = p.cDark;
    f.strokeText(p.text, x, y);
  }
  const gBody = f.createLinearGradient(0, top, 0, bot);
  gBody.addColorStop(0, p.cTop);
  gBody.addColorStop(0.55, p.cMid);
  gBody.addColorStop(1, p.cBot);
  paintText(f, gBody);

  if (p.innerB > 0) {
    let band = contourBand(0, -0.22 * capH, p.size * 0.03);
    band = blurred(band, p.size * 0.04);
    band = clipTo(band, glyph(plumpPx, 0, 0));
    band = vRamp(band, 0.45, 0.8, false);
    band = tint(band, deepen(p.cBot), 0.7);
    f.save();
    f.setTransform(1, 0, 0, 1, 0, 0);
    f.globalCompositeOperation = 'source-atop';
    f.globalAlpha = Math.min(1, p.innerB);
    f.drawImage(band, 0, 0);
    f.restore();
  }

  if (p.gloss > 0) {
    const shiftY = p.glossH * capH;
    const shiftX = 0.12 * shiftY;
    const inset = p.size * p.glossI;
    const clip0 = p.glossH * 0.6, clip1 = Math.min(0.95, p.glossH * 1.4);
    let band = contourBand(shiftX, shiftY, inset);
    band = vRamp(band, clip0, clip1, true);
    let halo = blurred(band, p.size * 0.014);
    halo = clipTo(halo, glyph(plumpPx - inset * 0.5, 0, 0));
    let core = mk();
    core.getContext('2d')!.drawImage(band, 0, 0);
    core = clipTo(core, glyph(plumpPx - inset * 1.35, 0, 0));
    core = blurred(core, p.size * 0.007);
    f.save();
    f.setTransform(1, 0, 0, 1, 0, 0);
    f.globalCompositeOperation = 'source-atop';
    f.globalAlpha = Math.min(1, 0.85 * p.gloss);
    f.drawImage(halo, 0, 0);
    f.globalAlpha = Math.min(1, 1.15 * p.gloss);
    f.drawImage(core, 0, 0);
    f.restore();
  }

  c.setTransform(1, 0, 0, 1, 0, 0);
  c.drawImage(face, 0, 0);
  return canvas;
}

// ── western 3D (HS0ZLE) ─────────────────────────────────────────────────
function noiseMask(W: number, H: number, featurePx: number, thresh: number, seed: number, softenPx: number, stretchY = 1): HTMLCanvasElement {
  const sw = Math.max(2, Math.round(W / featurePx));
  const sh = Math.max(2, Math.round(H / (featurePx * stretchY)));
  const small = document.createElement('canvas');
  small.width = sw;
  small.height = sh;
  const sg = small.getContext('2d')!;
  const id = sg.createImageData(sw, sh);
  const rnd = mulberry32(seed);
  for (let i = 0; i < sw * sh; i++) {
    const v = (rnd() * 255) | 0;
    id.data[i * 4] = v;
    id.data[i * 4 + 1] = v;
    id.data[i * 4 + 2] = v;
    id.data[i * 4 + 3] = 255;
  }
  sg.putImageData(id, 0, 0);
  const big = document.createElement('canvas');
  big.width = W;
  big.height = H;
  const bg = big.getContext('2d')!;
  bg.imageSmoothingEnabled = true;
  bg.imageSmoothingQuality = 'high';
  if (supportsFilter(bg)) bg.filter = 'blur(' + (featurePx * 0.35).toFixed(1) + 'px)';
  bg.drawImage(small, 0, 0, W, H);
  if (supportsFilter(bg)) bg.filter = 'none';
  const src = bg.getImageData(0, 0, W, H);
  const out = bg.createImageData(W, H);
  const k = 1 / Math.max(1e-6, (1 - thresh) * 0.35);
  for (let i = 0; i < W * H; i++) {
    const v = src.data[i * 4] / 255;
    let a = (v - thresh) * k;
    a = a < 0 ? 0 : a > 1 ? 1 : a;
    out.data[i * 4] = 255;
    out.data[i * 4 + 1] = 255;
    out.data[i * 4 + 2] = 255;
    out.data[i * 4 + 3] = (a * 255) | 0;
  }
  const mask = document.createElement('canvas');
  mask.width = W;
  mask.height = H;
  const mg = mask.getContext('2d')!;
  if (supportsFilter(mg) && softenPx > 0.3) mg.filter = 'blur(' + softenPx.toFixed(1) + 'px)';
  const tmp = document.createElement('canvas');
  tmp.width = W;
  tmp.height = H;
  tmp.getContext('2d')!.putImageData(out, 0, 0);
  mg.drawImage(tmp, 0, 0);
  if (supportsFilter(mg)) mg.filter = 'none';
  return mask;
}

function renderWestern(p: Required<Pick<TextFxParams, 'text' | 'weight' | 'family' | 'size' | 'space' | 'depth' | 'angle' | 'slant' | 'grunge' | 'bevel' | 'seed' | 'cTop' | 'cMid' | 'cBot' | 'cDark'>>, scale: number): HTMLCanvasElement {
  const probe = document.createElement('canvas').getContext('2d')!;
  const font = p.weight + ' ' + p.size + 'px ' + p.family;
  probe.letterSpacing = p.space + 'px';
  probe.font = font;
  const m = probe.measureText(p.text);
  const asc = m.actualBoundingBoxAscent, desc = m.actualBoundingBoxDescent;
  const capH = asc + desc;
  const edgeW = p.size * 0.07;
  const dx = Math.cos(p.angle) * p.depth, dy = Math.sin(p.angle) * p.depth;
  const slantPad = Math.abs(p.slant) * capH;
  const pad = Math.ceil(edgeW + p.size * 0.1 + p.depth + slantPad);
  const w = Math.ceil(m.width + pad * 2);
  const h = Math.ceil(capH + pad * 2);

  const canvas = document.createElement('canvas');
  canvas.width = w * scale;
  canvas.height = h * scale;
  const c = canvas.getContext('2d')!;

  const x = pad - Math.min(0, dx), y = pad + asc - Math.min(0, dy);
  const top = y - asc, bot = y + desc;

  const mk = () => {
    const k = document.createElement('canvas');
    k.width = canvas.width;
    k.height = canvas.height;
    return k;
  };
  const setT = (cx: CanvasRenderingContext2D) => {
    cx.setTransform(scale, 0, 0, scale, 0, 0);
    cx.transform(1, 0, -p.slant, 1, p.slant * y, 0);
    cx.letterSpacing = p.space + 'px';
    cx.font = font;
    cx.textBaseline = 'alphabetic';
    cx.lineJoin = 'round';
    cx.lineCap = 'butt'; // their source set 'miter' here, which browsers ignore on lineCap
  };
  const glyph = (dxx?: number, dyy?: number) => {
    const k = mk();
    const g = k.getContext('2d')!;
    setT(g);
    g.fillStyle = '#fff';
    g.fillText(p.text, x + (dxx || 0), y + (dyy || 0));
    return k;
  };
  const clipTo = (k: HTMLCanvasElement, mask: HTMLCanvasElement) => {
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'destination-in';
    g.drawImage(mask, 0, 0);
    g.restore();
    return k;
  };
  const blurred = (src: HTMLCanvasElement, px: number) => {
    const k = mk();
    const g = k.getContext('2d')!;
    if (supportsFilter(g) && px > 0.3) {
      g.filter = 'blur(' + (px * scale).toFixed(2) + 'px)';
      g.drawImage(src, 0, 0);
      g.filter = 'none';
    } else {
      g.drawImage(src, 0, 0);
    }
    return k;
  };
  const tintAlpha = (k: HTMLCanvasElement, color: string, alpha: number) => {
    const g = k.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'source-in';
    g.fillStyle = color;
    g.fillRect(0, 0, k.width, k.height);
    g.restore();
    const k2 = mk();
    const g2 = k2.getContext('2d')!;
    g2.globalAlpha = alpha;
    g2.drawImage(k, 0, 0);
    return k2;
  };

  c.setTransform(1, 0, 0, 1, 0, 0);
  c.clearRect(0, 0, canvas.width, canvas.height);
  setT(c);

  // drop shadow
  c.save();
  c.shadowColor = 'rgba(15,8,4,0.50)';
  c.shadowBlur = p.size * 0.035 * scale;
  c.shadowOffsetX = p.size * 0.02 * scale;
  c.shadowOffsetY = p.size * 0.035 * scale;
  c.lineWidth = edgeW;
  c.strokeStyle = 'rgba(15,8,4,0.50)';
  c.fillStyle = 'rgba(15,8,4,0.50)';
  c.strokeText(p.text, x, y);
  c.fillText(p.text, x, y);
  c.restore();

  // 3D extrusion
  if (p.depth > 0) {
    const deepCol = shade(p.cDark, -4);
    c.lineWidth = edgeW;
    const steps = Math.ceil(p.depth);
    for (let i = steps; i >= 1; i--) {
      const t = i / steps;
      c.strokeStyle = deepCol;
      c.fillStyle = deepCol;
      c.strokeText(p.text, x + dx * t, y + dy * t);
      c.fillText(p.text, x + dx * t, y + dy * t);
    }
  }

  const face = mk();
  const f = face.getContext('2d')!;
  setT(f);

  f.lineWidth = edgeW;
  f.strokeStyle = p.cDark;
  f.strokeText(p.text, x, y);

  const gBody = f.createLinearGradient(0, top, 0, bot);
  gBody.addColorStop(0, p.cTop);
  gBody.addColorStop(0.5, p.cMid);
  gBody.addColorStop(1, p.cBot);
  f.fillStyle = gBody;
  f.fillText(p.text, x, y);

  const M = glyph(0, 0);

  if (p.bevel > 0) {
    let band = glyph(0, 0);
    const g = band.getContext('2d')!;
    g.save();
    g.setTransform(1, 0, 0, 1, 0, 0);
    g.globalCompositeOperation = 'destination-out';
    g.drawImage(glyph(0.02 * p.size, 0.06 * capH), 0, 0);
    g.restore();
    band = blurred(band, p.size * 0.008);
    band = clipTo(band, M);
    band = tintAlpha(band, '#ffeba8', p.bevel);
    f.save();
    f.setTransform(1, 0, 0, 1, 0, 0);
    f.globalCompositeOperation = 'source-atop';
    f.drawImage(band, 0, 0);
    f.restore();
  }

  if (p.grunge > 0) {
    const W = canvas.width, H = canvas.height;
    const S = p.size * scale;
    const seed = (hashStr(p.text + '|' + p.family) ^ (p.seed * 2654435761)) >>> 0;
    const rust = rustOf(p.cBot);
    const layers = [
      { mask: noiseMask(W, H, 0.022 * S, 0.72, seed, 0.6 * scale), color: rust, a: 0.8 },
      { mask: noiseMask(W, H, 0.011 * S, 0.7, seed + 11, 0.5 * scale, 4.0), color: rust, a: 0.6 },
      { mask: noiseMask(W, H, 0.0066 * S, 0.74, seed + 7, 0.3 * scale), color: rust, a: 0.75 },
      { mask: noiseMask(W, H, 0.0154 * S, 0.86, seed + 3, 0.4 * scale), color: p.cDark, a: 0.9 },
    ];
    f.save();
    f.setTransform(1, 0, 0, 1, 0, 0);
    f.globalCompositeOperation = 'source-atop';
    for (const L of layers) {
      let k = clipTo(L.mask, M);
      k = tintAlpha(k, L.color, L.a * p.grunge);
      f.drawImage(k, 0, 0);
    }
    f.restore();
  }

  c.setTransform(1, 0, 0, 1, 0, 0);
  c.drawImage(face, 0, 0);
  return canvas;
}

// Trim transparent padding so the element's x/y maps to the visible glyphs.
function trim(canvas: HTMLCanvasElement): { canvas: HTMLCanvasElement; w: number; h: number } {
  const ctx = canvas.getContext('2d')!;
  const W = canvas.width, H = canvas.height;
  const data = ctx.getImageData(0, 0, W, H).data;
  let minX = W, minY = H, maxX = -1, maxY = -1;
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      if (data[(y * W + x) * 4 + 3] > 8) {
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
      }
    }
  }
  if (maxX < minX) return { canvas, w: W, h: H };
  const cw = maxX - minX + 1, ch = maxY - minY + 1;
  const out = document.createElement('canvas');
  out.width = cw;
  out.height = ch;
  out.getContext('2d')!.drawImage(canvas, minX, minY, cw, ch, 0, 0, cw, ch);
  return { canvas: out, w: cw, h: ch };
}

export function renderTextFx(p: TextFxParams): TextFxResult {
  let raw: HTMLCanvasElement;
  if (p.kind === 'western') {
    raw = renderWestern(
      {
        text: p.text, weight: p.weight, family: p.family, size: p.size, space: p.space,
        depth: p.depth ?? p.size * 0.06, angle: p.angle ?? (112 * Math.PI) / 180,
        slant: p.slant ?? 0.08, grunge: p.grunge ?? 0.8, bevel: p.bevel ?? 0.45, seed: p.seed ?? 7,
        cTop: p.cTop, cMid: p.cMid, cBot: p.cBot, cDark: p.cDark,
      },
      SS,
    );
  } else {
    raw = renderGlossy(
      {
        text: p.text, weight: p.weight, family: p.family, size: p.size, space: p.space,
        plump: p.plump ?? 0.03, edge: p.edge ?? 0.5, outerw: p.outerw ?? 0.45,
        gloss: p.gloss ?? 0.85, glossH: p.glossH ?? 0.45, glossI: p.glossI ?? 0.08, innerB: p.innerB ?? 0.7,
        cTop: p.cTop, cMid: p.cMid, cBot: p.cBot, cDark: p.cDark, cOuter: p.cOuter ?? '#ced3db',
      },
      SS,
    );
  }
  const t = trim(raw);
  return { url: t.canvas.toDataURL('image/png'), w: t.w / SS, h: t.h / SS };
}
