import { drawStamp } from "./brushStamp";
import type { BrushStamp, CoverRect } from "./types";

/**
 * Owns the actual canvas pixels for the reveal effect: the visible main
 * canvas plus two detached ("offscreen") accumulator canvases used to build
 * a soft-edged stamp mask before punching it into the main canvas. Kept
 * separate from pointer physics / the animation loop so each piece can be
 * reasoned about (and swapped) independently.
 */
export interface RevealCanvas {
  canvas: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
  /** Accumulates all currently-active stamps (plain source-over — overlapping stamps merge into one denser shape, not separate dabs). */
  maskCanvas: HTMLCanvasElement;
  maskCtx: CanvasRenderingContext2D;
  /** The mask above, blurred — kept as a *separate* draw step from the final destination-out punch (some browsers render `filter` + non-default `globalCompositeOperation` combined in one call incorrectly). */
  blurredMaskCanvas: HTMLCanvasElement;
  blurredMaskCtx: CanvasRenderingContext2D;
  cssWidth: number;
  cssHeight: number;
  coverRect: CoverRect;
}

/** Uses plain detached `<canvas>` elements as the "offscreen" surfaces — never attached to the DOM, so they're offscreen in every practical sense, while staying compatible everywhere (no OffscreenCanvas/worker feature-detection needed for two small accumulator buffers). */
export function createRevealCanvas(canvas: HTMLCanvasElement): RevealCanvas | null {
  const ctx = canvas.getContext("2d");
  const maskCanvas = document.createElement("canvas");
  const maskCtx = maskCanvas.getContext("2d");
  const blurredMaskCanvas = document.createElement("canvas");
  const blurredMaskCtx = blurredMaskCanvas.getContext("2d");

  if (!ctx || !maskCtx || !blurredMaskCtx) return null;

  return {
    canvas,
    ctx,
    maskCanvas,
    maskCtx,
    blurredMaskCanvas,
    blurredMaskCtx,
    cssWidth: 0,
    cssHeight: 0,
    coverRect: { sx: 0, sy: 0, sw: 0, sh: 0 },
  };
}

function computeCoverRect(img: HTMLImageElement, cssWidth: number, cssHeight: number): CoverRect {
  if (!img.naturalWidth || !img.naturalHeight || cssHeight === 0) {
    return { sx: 0, sy: 0, sw: 0, sh: 0 };
  }
  const containerRatio = cssWidth / cssHeight;
  const imageRatio = img.naturalWidth / img.naturalHeight;
  let sw: number;
  let sh: number;
  if (imageRatio > containerRatio) {
    sh = img.naturalHeight;
    sw = sh * containerRatio;
  } else {
    sw = img.naturalWidth;
    sh = sw / containerRatio;
  }
  return { sx: (img.naturalWidth - sw) / 2, sy: (img.naturalHeight - sh) / 2, sw, sh };
}

/** Resizes all three canvases to match the container (accounting for devicePixelRatio) and recomputes the object-fit: cover source rect. */
export function resizeRevealCanvas(
  reveal: RevealCanvas,
  containerRect: { width: number; height: number },
  topImage: HTMLImageElement,
  dprCap: number,
): void {
  reveal.cssWidth = containerRect.width;
  reveal.cssHeight = containerRect.height;
  const dpr = Math.min(window.devicePixelRatio || 1, dprCap);

  for (const c of [reveal.canvas, reveal.maskCanvas, reveal.blurredMaskCanvas]) {
    c.width = Math.max(1, Math.round(reveal.cssWidth * dpr));
    c.height = Math.max(1, Math.round(reveal.cssHeight * dpr));
  }
  reveal.canvas.style.width = `${reveal.cssWidth}px`;
  reveal.canvas.style.height = `${reveal.cssHeight}px`;

  for (const c of [reveal.ctx, reveal.maskCtx, reveal.blurredMaskCtx]) {
    c.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  reveal.coverRect = computeCoverRect(topImage, reveal.cssWidth, reveal.cssHeight);
}

/**
 * One frame of the reveal: redraws the "after" (treated) image as the base
 * layer, accumulates all active stamps onto the mask, blurs that mask (soft
 * alpha edge — never a hard-edge cutout), then punches it out of the main
 * canvas in a single composite step (cost independent of stamp count).
 */
export function paintRevealFrame(
  reveal: RevealCanvas,
  topImage: HTMLImageElement,
  pool: readonly BrushStamp[],
  now: number,
  featherBlurPx: number,
): void {
  const { ctx, maskCtx, blurredMaskCanvas, blurredMaskCtx, maskCanvas, coverRect, cssWidth, cssHeight } = reveal;

  ctx.clearRect(0, 0, cssWidth, cssHeight);
  ctx.drawImage(topImage, coverRect.sx, coverRect.sy, coverRect.sw, coverRect.sh, 0, 0, cssWidth, cssHeight);

  maskCtx.clearRect(0, 0, cssWidth, cssHeight);
  let anyActive = false;
  for (const stamp of pool) {
    if (!stamp.active) continue;
    anyActive = true;
    if (!drawStamp(maskCtx, stamp, now)) stamp.active = false;
  }

  if (!anyActive) return;

  blurredMaskCtx.clearRect(0, 0, cssWidth, cssHeight);
  blurredMaskCtx.filter = `blur(${featherBlurPx}px)`;
  blurredMaskCtx.drawImage(maskCanvas, 0, 0, maskCanvas.width, maskCanvas.height, 0, 0, cssWidth, cssHeight);
  blurredMaskCtx.filter = "none";

  ctx.save();
  ctx.globalCompositeOperation = "destination-out";
  ctx.drawImage(blurredMaskCanvas, 0, 0, cssWidth, cssHeight);
  ctx.restore();
}
