import { createPreviewLoop } from "../utils.ts";

// disc dimensions
const OUTER_RADIUS_RATIO = 0.42;
const HUB_OUTER_RATIO = 0.28;
const HUB_INNER_RATIO = 0.19;
const HOLE_RADIUS_RATIO = 0.09;
const STREAK_SIZE_RATIO = 1.5;
const OUTER_EDGE_THICKNESS_RATIO = 0.012;

export const category = "media";

export const params = [
  {
    key: "labelImg",
    type: "img",
    default: "",
    label: "label img",
  },
  {
    key: "labelFit",
    type: "select",
    default: "fill",
    values: ["fill", "fit", "stretch"],
    label: "fit",
  },
  {
    key: "bgImg",
    type: "img",
    default: "",
    label: "bg img",
  },
  {
    key: "speed",
    type: "number",
    default: 0.2,
    min: 0.1,
    max: 52,
    step: 0.1,
    label: "rps",
  },
];

let angle = 0;

// text sprite cache — regenerated when canvas dimensions change
let cachedTextW = 0;
let cachedTextH = 0;
let hubLabelSprites: HTMLCanvasElement[] | null = null;
let outerTextSprite: HTMLCanvasElement | null = null;

function filledRing(
  ctx: CanvasRenderingContext2D,
  outerR: number,
  innerR: number,
  fillStyle: string,
) {
  ctx.beginPath();
  ctx.arc(0, 0, outerR, 0, Math.PI * 2);
  ctx.arc(0, 0, innerR, 0, Math.PI * 2, true);
  ctx.fillStyle = fillStyle;
  ctx.fill();
}

function buildTextSprites(
  outerRadius: number,
  hubOuterRadius: number,
  _hubInnerRadius: number,
) {
  const hubFontSize = Math.round(hubOuterRadius * 0.4);
  const hubCharSpacing = 0.23 * hubOuterRadius;
  const labels = ["CD-RW", "700MB"];
  hubLabelSprites = [];
  for (const label of labels) {
    const w = label.length * hubCharSpacing + hubFontSize;
    const h = hubFontSize * 1.2;
    const sprite = document.createElement("canvas");
    sprite.width = w;
    sprite.height = h;
    const sCtx = sprite.getContext("2d")!;
    sCtx.fillStyle = "rgba(15,15,15,0.90)";
    sCtx.font = `${hubFontSize}px 'Maple Mono'`;
    sCtx.textAlign = "left";
    sCtx.textBaseline = "middle";
    for (let i = 0; i < label.length; i++) {
      sCtx.fillText(label[i], i * hubCharSpacing, h / 2);
    }
    hubLabelSprites.push(sprite);
  }

  // outer ring
  const label = "DEFTOKU | CD-RW | ".repeat(12);
  const kerning = 0.0291;
  const fontSize = Math.round(hubOuterRadius * 0.13);
  const labelRadius = outerRadius * 0.95;
  const spriteSize = Math.ceil((labelRadius + fontSize) * 2);

  outerTextSprite = document.createElement("canvas");
  outerTextSprite.width = spriteSize;
  outerTextSprite.height = spriteSize;
  const otsCtx = outerTextSprite.getContext("2d")!;
  otsCtx.translate(spriteSize / 2, spriteSize / 2);
  otsCtx.fillStyle = "rgba(15,15,15,0.90)";
  otsCtx.font = `${fontSize}px 'Maple Mono'`;
  otsCtx.textAlign = "center";
  otsCtx.textBaseline = "middle";

  for (let i = 0; i < label.length; i++) {
    const a = -label.length / 2 * kerning + i * kerning;
    otsCtx.save();
    otsCtx.rotate(a);
    otsCtx.fillText(label[i], 0, -labelRadius);
    otsCtx.restore();
  }
}

export function draw(
  canvas: HTMLCanvasElement,
  _freq: Uint8Array,
  options: Record<string, string | number | HTMLImageElement>,
  dt = 1,
) {
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  // background
  const bgImg = options["bgImg"] as HTMLImageElement | undefined;
  if (bgImg && bgImg.complete && bgImg.naturalWidth > 0) {
    const scale = Math.max(
      canvas.width / bgImg.naturalWidth,
      canvas.height / bgImg.naturalHeight,
    );
    const dw = bgImg.naturalWidth * scale;
    const dh = bgImg.naturalHeight * scale;
    const dx = (canvas.width - dw) / 2;
    const dy = (canvas.height - dh) / 2;
    ctx.drawImage(bgImg, dx, dy, dw, dh);
  } else {
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  const cx = canvas.width / 2;
  const cy = canvas.height / 2;
  const outerRadius = Math.min(canvas.width, canvas.height) *
    OUTER_RADIUS_RATIO;
  const hubOuterRadius = outerRadius * HUB_OUTER_RATIO;
  const hubInnerRadius = outerRadius * HUB_INNER_RATIO;
  const holeRadius = outerRadius * HOLE_RADIUS_RATIO;
  const streakSize = outerRadius * STREAK_SIZE_RATIO;
  const rotationSpeed = Number(options.speed) * dt;

  // rebuild outer text sprite if canvas size changed
  if (canvas.width !== cachedTextW || canvas.height !== cachedTextH) {
    cachedTextW = canvas.width;
    cachedTextH = canvas.height;
    buildTextSprites(outerRadius, hubOuterRadius, hubInnerRadius);
  }

  // rotation
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(angle);

  // disc surface (clipped)
  ctx.save();
  filledRing(ctx, outerRadius, hubInnerRadius, "");
  ctx.clip();

  // printed disc surface
  const baseGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, outerRadius);
  baseGrad.addColorStop(0.0, "#f7c8e0");
  baseGrad.addColorStop(0.5, "#f196c8");
  baseGrad.addColorStop(1.0, "#d870a8");
  ctx.fillStyle = baseGrad;
  ctx.fillRect(-outerRadius, -outerRadius, outerRadius * 2, outerRadius * 2);

  // subtle iridescent streaks (printed ink sheen)
  function drawStreak(angleDegrees: number, colorStops: [number, string][]) {
    ctx!.save();
    ctx!.rotate(angleDegrees * Math.PI / 180);
    const grad = ctx!.createLinearGradient(
      -streakSize,
      -streakSize,
      streakSize,
      streakSize,
    );
    colorStops.forEach((stop) => grad.addColorStop(stop[0], stop[1]));
    ctx!.fillStyle = grad;
    ctx!.fillRect(-streakSize, -streakSize, streakSize * 2, streakSize * 2);
    ctx!.restore();
  }

  drawStreak(-30, [
    [0.0, "rgba(255,255,255,0)"],
    [0.3, "rgba(255,220,240,0.08)"],
    [0.5, "rgba(220,180,255,0.05)"],
    [1.0, "rgba(255,255,255,0)"],
  ]);

  drawStreak(45, [
    [0.0, "rgba(255,255,255,0)"],
    [0.4, "rgba(200,220,255,0.06)"],
    [0.6, "rgba(255,200,230,0.04)"],
    [1.0, "rgba(255,255,255,0)"],
  ]);

  ctx.restore(); // end disc shape

  // hub ring - lead in
  filledRing(ctx, hubOuterRadius, hubInnerRadius, "rgba(200,200,255,0.8)");

  // hub ring - outer edge
  const edgeHalf = 1.5;
  filledRing(
    ctx,
    hubOuterRadius + edgeHalf,
    hubOuterRadius - edgeHalf,
    "rgba(80,100,160,0.7)",
  );

  // hub labels from sprites
  if (hubLabelSprites) {
    const labelPositions = [outerRadius * -0.70, outerRadius * 0.45];
    for (let i = 0; i < hubLabelSprites.length; i++) {
      const s = hubLabelSprites[i];
      ctx.drawImage(s, labelPositions[i], -s.height / 2);
    }
  }

  // label image drawn over text
  const labelImg = options["labelImg"] as HTMLImageElement | undefined;
  if (labelImg && labelImg.complete && labelImg.naturalWidth > 0) {
    ctx.save();
    filledRing(ctx, outerRadius, hubInnerRadius, "");
    ctx.clip();
    const discSize = outerRadius * 2;
    const labelFit = (options["labelFit"] as string) || "fill";
    let iw: number, ih: number;
    if (labelFit === "fit") {
      const scale = Math.min(
        discSize / labelImg.naturalWidth,
        discSize / labelImg.naturalHeight,
      );
      iw = labelImg.naturalWidth * scale;
      ih = labelImg.naturalHeight * scale;
    } else if (labelFit === "stretch") {
      iw = discSize;
      ih = discSize;
    } else {
      // fill (cover)
      const scale = Math.max(
        discSize / labelImg.naturalWidth,
        discSize / labelImg.naturalHeight,
      );
      iw = labelImg.naturalWidth * scale;
      ih = labelImg.naturalHeight * scale;
    }
    ctx.drawImage(labelImg, -iw / 2, -ih / 2, iw, ih);
    ctx.restore();
  }

  // outer ring text from sprite
  if (outerTextSprite) {
    const s = outerTextSprite.width;
    ctx.drawImage(outerTextSprite, -s / 2, -s / 2);
  }

  // outer edge ring (no data)
  filledRing(
    ctx,
    outerRadius,
    outerRadius * (1 - OUTER_EDGE_THICKNESS_RATIO),
    "rgba(255,255,255,0.35)",
  );

  // outer edge ring stroke
  const rimHalf = 0.5;
  filledRing(
    ctx,
    outerRadius + rimHalf,
    outerRadius - rimHalf,
    "rgba(200,200,255,0.1)",
  );

  // inner ring (clear plastic look)
  filledRing(ctx, hubInnerRadius, holeRadius, "rgba(255,255,255,0.15)");

  ctx.restore(); // end rotation

  // specular highlight
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(Math.PI / 4); // rotation
  ctx.beginPath();
  ctx.arc(0, 0, outerRadius, 0, Math.PI * 2);
  ctx.clip();
  const specGrad = ctx.createLinearGradient(
    -outerRadius * 0.8,
    0,
    -outerRadius * 0.1,
    0,
  );
  specGrad.addColorStop(0.0, "rgba(255,255,255,0)");
  specGrad.addColorStop(0.2, "rgba(255,255,255,0.05)");
  specGrad.addColorStop(0.5, "rgba(255,255,255,0.1)");
  specGrad.addColorStop(0.8, "rgba(200, 230, 255, 0.05)");
  specGrad.addColorStop(1.0, "rgba(255,255,255,0)");
  ctx.fillStyle = specGrad;
  ctx.fillRect(
    -outerRadius,
    -outerRadius,
    outerRadius,
    outerRadius * 2,
  );
  ctx.restore();

  angle += rotationSpeed;
}

export function preview(
  canvas: HTMLCanvasElement,
  freq: Uint8Array,
  getOptions: () => Record<string, string | number | HTMLImageElement>,
  fps: number,
) {
  angle = 0;

  return createPreviewLoop(canvas, fps, (offscreen, dt) => {
    draw(offscreen, freq, getOptions(), dt);
  });
}
