import { BRUSH_CONFIG } from "./config";
import type { BrushStamp } from "./types";

/** Deterministic PRNG from an integer seed, so a stamp's shape is stable across the many frames it's redrawn. */
function mulberry32(seed: number) {
  let s = seed | 0;
  return function random() {
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export function easeOutCubic(t: number): number {
  return 1 - Math.pow(1 - t, 3);
}

export function easeInCubic(t: number): number {
  return t * t * t;
}

export function easeInOutSine(t: number): number {
  return -(Math.cos(Math.PI * t) - 1) / 2;
}

/**
 * Builds a closed, organic ink/watercolor-blob path around (0,0) at the
 * given radius. Two layered sine harmonics (coarse wave + finer ripple)
 * perturb the radius by angle — this reads as a flowing, hand-painted
 * silhouette rather than a jittery random polygon — with a small amount of
 * per-point noise on top so no two stamps are ever identical. Edges are
 * curved through jittered midpoints for softness (the actual feather comes
 * from a single blur pass on the accumulated mask, see useBrushReveal).
 */
function buildBlobPath(radius: number, seed: number): Path2D {
  const rng = mulberry32(seed);
  const { points, irregularity, noiseAmplitude1, noiseFrequency1, noiseAmplitude2, noiseFrequency2 } = BRUSH_CONFIG;
  const angleStep = (Math.PI * 2) / points;
  // Random phase offsets so the same harmonics never line up the same way twice.
  const phase1 = rng() * Math.PI * 2;
  const phase2 = rng() * Math.PI * 2;
  const radii: number[] = [];

  for (let i = 0; i < points; i++) {
    const angle = angleStep * i;
    const wave1 = Math.sin(angle * noiseFrequency1 + phase1) * noiseAmplitude1;
    const wave2 = Math.sin(angle * noiseFrequency2 + phase2) * noiseAmplitude2;
    const jitter = (rng() - 0.5) * 2 * irregularity;
    radii.push(radius * Math.max(0.35, 1 + wave1 + wave2 + jitter));
  }

  const path = new Path2D();
  for (let i = 0; i <= points; i++) {
    const idx = i % points;
    const angle = angleStep * i;
    const r = radii[idx];
    const x = Math.cos(angle) * r;
    const y = Math.sin(angle) * r;

    if (i === 0) {
      path.moveTo(x, y);
      continue;
    }

    // Curve through a jittered midpoint for a soft, rounded edge rather than sharp polygon corners.
    const prevAngle = angleStep * (i - 1);
    const prevR = radii[(i - 1) % points];
    const midAngle = (prevAngle + angle) / 2;
    const midR = ((prevR + r) / 2) * (1 + (rng() - 0.5) * 0.15);
    path.quadraticCurveTo(Math.cos(midAngle) * midR, Math.sin(midAngle) * midR, x, y);
  }
  path.closePath();
  return path;
}

/**
 * Mutates a pooled stamp in place (no allocation of the stamp object
 * itself) and builds its cached blob path.
 *
 * @param strokeAngle  Direction of travel (radians) at spawn time — the
 *   stamp's base rotation aligns to this (like a real brush tilting along
 *   the stroke) plus a small ±10° random jitter, rather than being fully
 *   random per stamp.
 */
export function spawnStamp(
  stamp: BrushStamp,
  x: number,
  y: number,
  radius: number,
  strokeAngle: number,
  seed: number,
  now: number,
): void {
  const rng = mulberry32(seed ^ 0x9e3779b9);
  const { scaleVariation, opacityJitterMin, rotationJitterRad, positionJitterPx, lifetimeVariation } = BRUSH_CONFIG;

  stamp.active = true;
  stamp.x = x + (rng() - 0.5) * 2 * positionJitterPx;
  stamp.y = y + (rng() - 0.5) * 2 * positionJitterPx;
  stamp.baseRadius = radius;
  stamp.baseScale = 1 + (rng() - 0.5) * 2 * scaleVariation;
  stamp.opacityMultiplier = opacityJitterMin + rng() * (1 - opacityJitterMin);
  stamp.lifetimeMultiplier = 1 + (rng() - 0.5) * 2 * lifetimeVariation;
  stamp.rotation = strokeAngle + (rng() - 0.5) * 2 * rotationJitterRad;
  stamp.seed = seed;
  stamp.spawnTime = now;
  stamp.path = buildBlobPath(radius, seed);
}

/**
 * Draws one stamp's current animated state onto ctx: organic grow-in
 * (overshoots slightly then settles, like ink expanding then relaxing),
 * a soft rotational wobble, a subtle scale "breathing" pulsation, and a
 * closing phase that *shrinks the mark back down* (mirroring the grow-in)
 * while it fades — so the reveal reads as the split healing itself shut
 * again, not just the same full-size mark turning transparent in place.
 * Returns false once the stamp has expired.
 */
export function drawStamp(ctx: CanvasRenderingContext2D, stamp: BrushStamp, now: number): boolean {
  const age = now - stamp.spawnTime;
  const { stampLifetimeMs, growInMs, fadeOutStart, wobbleAmount, wobbleSpeed, scaleWobbleAmount, scaleWobbleSpeed } =
    BRUSH_CONFIG;
  // Per-stamp randomized lifetime (see spawnStamp) — stamps along a stroke
  // then close at independently varied times instead of in strict spawn
  // order, so the recovery reads as random/organic rather than a visible
  // start-to-end sweep.
  const lifetime = stampLifetimeMs * stamp.lifetimeMultiplier;

  if (age >= lifetime || !stamp.path) return false;

  // Organic expansion: grows past 100% then eases back to rest, like ink spreading then settling.
  const growProgress = Math.min(1, age / growInMs);
  const eased = easeOutCubic(growProgress);
  const overshoot = Math.sin(growProgress * Math.PI) * 0.12 * (1 - growProgress);
  const growScale = 0.35 + eased * 0.65 + overshoot;

  const breathing = 1 + Math.sin(age * scaleWobbleSpeed + stamp.seed) * scaleWobbleAmount;

  const lifeFraction = age / lifetime;
  let opacity = 1;
  let closeScale = 1;
  if (lifeFraction > fadeOutStart) {
    const fadeProgress = Math.min(1, (lifeFraction - fadeOutStart) / (1 - fadeOutStart));
    const closeEase = easeInOutSine(fadeProgress);
    opacity = 1 - closeEase;
    // Shrinks back toward the same small starting size the mark grew in
    // from — a healing/reconnecting contraction, not a flat fade.
    closeScale = 1 - closeEase * 0.75;
  }

  const scale = growScale * closeScale * breathing * stamp.baseScale;

  const wobble = Math.sin(age * wobbleSpeed + stamp.seed) * wobbleAmount;

  ctx.save();
  ctx.translate(stamp.x, stamp.y);
  ctx.rotate(stamp.rotation + wobble);
  ctx.scale(scale, scale);
  ctx.globalAlpha = Math.max(0, Math.min(1, opacity * stamp.opacityMultiplier));
  ctx.fill(stamp.path);
  ctx.restore();

  return true;
}
