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

// disc dimensions (shared between both faces)
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: "spinSpeed",
    type: "number",
    default: 0.2,
    min: 0.1,
    max: 52,
    step: 0.1,
    label: "spin rps",
  },
  {
    key: "flipSpeed",
    type: "number",
    default: 0.1,
    min: 0.05,
    max: 2,
    step: 0.05,
    label: "flip rps",
  },
];

let spinAngle = 0;
let flipAngle = 0;

// sprite caches — rebuilt when canvas dimensions change
let cachedW = 0;
let cachedH = 0;

// bottom face sprites
let bottomHubTextSprite: HTMLCanvasElement | null = null;

// top face sprites
let topHubLabelSprites: HTMLCanvasElement[] | null = null;
let topOuterTextSprite: 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();
}

// --- bottom face: arc-text on the hub ---
function buildBottomHubSprite(
  hubOuterRadius: number,
  hubInnerRadius: number,
) {
  const fontSize = Math.round(hubOuterRadius * 0.13);
  const labelRadius = hubInnerRadius +
    (hubOuterRadius - hubInnerRadius) * 0.5;
  const kerning = 0.09;
  const spriteSize = Math.ceil((labelRadius + fontSize) * 2);

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

  function drawArcText(angle: number, label: string) {
    ctx.save();
    ctx.rotate(angle);
    for (let i = 0; i < label.length; i++) {
      const a = -label.length / 2 * kerning + i * kerning;
      ctx.save();
      ctx.rotate(a);
      ctx.fillText(label[i], 0, -labelRadius);
      ctx.restore();
    }
    ctx.restore();
  }

  drawArcText(0, "DEFTOKU");
  drawArcText(2.15, "CD-RW");
  drawArcText(4.35, "700MB");
}

function buildTopSprites(
  outerRadius: number,
  hubOuterRadius: number,
) {
  const hubFontSize = Math.round(hubOuterRadius * 0.4);
  const hubCharSpacing = 0.23 * hubOuterRadius;
  const labels = ["CD-RW", "700MB"];
  topHubLabelSprites = [];
  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);
    }
    topHubLabelSprites.push(sprite);
  }

  // outer ring arc text
  const outerLabel = "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);

  topOuterTextSprite = document.createElement("canvas");
  topOuterTextSprite.width = spriteSize;
  topOuterTextSprite.height = spriteSize;
  const otsCtx = topOuterTextSprite.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 < outerLabel.length; i++) {
    const a = -outerLabel.length / 2 * kerning + i * kerning;
    otsCtx.save();
    otsCtx.rotate(a);
    otsCtx.fillText(outerLabel[i], 0, -labelRadius);
    otsCtx.restore();
  }
}

function buildAllSprites(
  outerRadius: number,
  hubOuterRadius: number,
  hubInnerRadius: number,
) {
  buildBottomHubSprite(hubOuterRadius, hubInnerRadius);
  buildTopSprites(outerRadius, hubOuterRadius);
}

// helper: iridescent streak using a rotated linear gradient
function drawStreak(
  ctx: CanvasRenderingContext2D,
  streakSize: number,
  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();
}

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 spinSpeed = Number(options.spinSpeed) * dt;
  const flipSpeed = Number(options.flipSpeed) * 2 * Math.PI * dt;

  if (canvas.width !== cachedW || canvas.height !== cachedH) {
    cachedW = canvas.width;
    cachedH = canvas.height;
    buildAllSprites(outerRadius, hubOuterRadius, hubInnerRadius);
  }

  const cosFlip = Math.cos(flipAngle);
  const showingTop = cosFlip > 0;

  // fake depth
  ctx.save();
  ctx.translate(cx, cy);
  ctx.scale(cosFlip, 1);

  if (showingTop) {
    // all disc content rotates with the spin
    ctx.save();
    ctx.rotate(spinAngle);

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

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

    // subtle iridescent streaks
    drawStreak(ctx, streakSize, -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(ctx, streakSize, 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 clip

    // hub ring
    filledRing(ctx, hubOuterRadius, hubInnerRadius, "rgba(200,200,255,0.8)");
    const edgeHalf = 1.5;
    filledRing(
      ctx,
      hubOuterRadius + edgeHalf,
      hubOuterRadius - edgeHalf,
      "rgba(80,100,160,0.7)",
    );

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

    // label image drawn over the disc surface
    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 s = Math.min(
          discSize / labelImg.naturalWidth,
          discSize / labelImg.naturalHeight,
        );
        iw = labelImg.naturalWidth * s;
        ih = labelImg.naturalHeight * s;
      } else if (labelFit === "stretch") {
        iw = discSize;
        ih = discSize;
      } else {
        // fill (cover)
        const s = Math.max(
          discSize / labelImg.naturalWidth,
          discSize / labelImg.naturalHeight,
        );
        iw = labelImg.naturalWidth * s;
        ih = labelImg.naturalHeight * s;
      }
      ctx.drawImage(labelImg, -iw / 2, -ih / 2, iw, ih);
      ctx.restore();
    }

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

    // outer edge rings
    filledRing(
      ctx,
      outerRadius,
      outerRadius * (1 - OUTER_EDGE_THICKNESS_RATIO),
      "rgba(255,255,255,0.35)",
    );
    const rimHalf = 0.5;
    filledRing(
      ctx,
      outerRadius + rimHalf,
      outerRadius - rimHalf,
      "rgba(200,200,255,0.1)",
    );

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

    ctx.restore(); // end spin rotation

    // specular highlight (slightly offset from spin)
    ctx.save();
    ctx.rotate(Math.PI / 4);
    ctx.beginPath();
    ctx.arc(0, 0, outerRadius, 0, Math.PI * 2);
    ctx.clip();
    const topSpec = ctx.createLinearGradient(
      -outerRadius * 0.8,
      0,
      -outerRadius * 0.1,
      0,
    );
    topSpec.addColorStop(0.0, "rgba(255,255,255,0)");
    topSpec.addColorStop(0.2, "rgba(255,255,255,0.05)");
    topSpec.addColorStop(0.5, "rgba(255,255,255,0.1)");
    topSpec.addColorStop(0.8, "rgba(200, 230, 255, 0.05)");
    topSpec.addColorStop(1.0, "rgba(255,255,255,0)");
    ctx.fillStyle = topSpec;
    ctx.fillRect(-outerRadius, -outerRadius, outerRadius, outerRadius * 2);
    ctx.restore();
  } else { // bottom face
    ctx.scale(-1, 1);

    // stationary lighting — streaks are a fixed "light reflection"
    // that doesn't spin with the disc
    ctx.save();
    filledRing(ctx, outerRadius, hubInnerRadius, "");
    ctx.clip();

    const bottomGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, outerRadius);
    bottomGrad.addColorStop(0.0, "#ffffff");
    bottomGrad.addColorStop(0.1, "#8ab4f8");
    bottomGrad.addColorStop(0.4, "#5a8fe0");
    bottomGrad.addColorStop(0.7, "#3a5bb0");
    bottomGrad.addColorStop(1.0, "#1a1a40");
    ctx.fillStyle = bottomGrad;
    ctx.fillRect(-outerRadius, -outerRadius, outerRadius * 2, outerRadius * 2);

    drawStreak(ctx, streakSize, -30, [
      [0.0, "rgba(255,255,255,0)"],
      [0.2, "rgba(255, 200, 100, 0.6)"],
      [0.4, "rgba(255, 100, 150, 0.3)"],
      [0.6, "rgba(100, 200, 255, 0.5)"],
      [1.0, "rgba(255,255,255,0)"],
    ]);
    drawStreak(ctx, streakSize, 45, [
      [0.0, "rgba(100, 200, 255, 0.4)"],
      [0.5, "rgba(200, 100, 255, 0.6)"],
      [0.7, "rgba(255, 150, 200, 0.3)"],
      [1.0, "rgba(255,255,255,0)"],
    ]);

    ctx.restore(); // end stationary lighting

    // rotating elements
    ctx.save();
    ctx.rotate(spinAngle);

    // hub ring
    filledRing(ctx, hubOuterRadius, hubInnerRadius, "rgba(200,200,255,0.3)");
    const edgeHalf = 1.5;
    filledRing(
      ctx,
      hubOuterRadius + edgeHalf,
      hubOuterRadius - edgeHalf,
      "rgba(80,100,160,0.3)",
    );

    // hub arc text
    if (bottomHubTextSprite) {
      const s = bottomHubTextSprite.width;
      ctx.drawImage(bottomHubTextSprite, -s / 2, -s / 2);
    }

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

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

    ctx.restore(); // end spin rotation

    // specular highlight
    ctx.save();
    ctx.rotate(Math.PI / 6);
    ctx.beginPath();
    ctx.arc(0, 0, outerRadius, 0, Math.PI * 2);
    ctx.clip();
    const bottomSpec = ctx.createLinearGradient(
      -outerRadius * 0.8,
      0,
      -outerRadius * 0.1,
      0,
    );
    bottomSpec.addColorStop(0.0, "rgba(255,255,255,0)");
    bottomSpec.addColorStop(0.3, "rgba(255,255,255,0.2)");
    bottomSpec.addColorStop(0.5, "rgba(255,255,255,0.3)");
    bottomSpec.addColorStop(0.7, "rgba(200, 230, 255, 0.3)");
    bottomSpec.addColorStop(1.0, "rgba(255,255,255,0)");
    ctx.fillStyle = bottomSpec;
    ctx.fillRect(-outerRadius, -outerRadius, outerRadius, outerRadius * 2);
    ctx.restore();
  }

  ctx.restore(); // end 3D transform

  // fake disc thickness
  const absSin = Math.abs(Math.sin(flipAngle));
  const edgeAlpha = Math.pow(absSin, 1500);
  if (edgeAlpha > 0.001) {
    const discThickness = outerRadius * 0.015;
    const edgeWidth = absSin * discThickness;
    const halfEdge = edgeWidth / 2;

    ctx.save();
    ctx.globalAlpha = edgeAlpha;

    // disc body — metallic cylindrical gradient
    const edgeGrad = ctx.createLinearGradient(
      cx - halfEdge,
      0,
      cx + halfEdge,
      0,
    );
    edgeGrad.addColorStop(0.0, "#5a5a7a");
    edgeGrad.addColorStop(0.25, "#a8a8c4");
    edgeGrad.addColorStop(0.5, "#d0d0f0");
    edgeGrad.addColorStop(0.75, "#a8a8c4");
    edgeGrad.addColorStop(1.0, "#5a5a7a");
    ctx.fillStyle = edgeGrad;
    ctx.fillRect(
      cx - halfEdge,
      cy - outerRadius,
      edgeWidth,
      outerRadius * 2,
    );

    // outer rim highlights (top and bottom of the edge strip)
    ctx.fillStyle = "rgba(255,255,255,0.4)";
    ctx.fillRect(cx - halfEdge, cy - outerRadius, edgeWidth, 1.5);
    ctx.fillRect(cx - halfEdge, cy + outerRadius - 1.5, edgeWidth, 1.5);

    ctx.restore();
  }

  // advance angles
  spinAngle += spinSpeed;
  flipAngle += flipSpeed;
}

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

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