import type {
  CaptchaChallengeGenerator,
  CaptchaGeneratorContext,
  CaptchaImageItem,
  GeneratedCaptchaChallenge,
} from "../types.ts";
import {
  createImage,
  drawLine,
  fillCircle,
  fillPolygon,
  fillRect,
  pngDataUri,
  setPixel,
} from "./png.ts";

const SHAPES = ["circle", "square", "triangle", "diamond", "star"] as const;
type Shape = (typeof SHAPES)[number];
const COLORS = [
  [37, 99, 235, 255],
  [22, 163, 74, 255],
  [220, 38, 38, 255],
  [147, 51, 234, 255],
  [234, 88, 12, 255],
] as const;

function starPoints(cx: number, cy: number, outer: number, inner: number): Array<[number, number]> {
  const points: Array<[number, number]> = [];
  for (let index = 0; index < 10; index++) {
    const radius = index % 2 === 0 ? outer : inner;
    const angle = -Math.PI / 2 + (index * Math.PI) / 5;
    points.push([cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius]);
  }
  return points;
}

function disturbanceRatio(context: CaptchaGeneratorContext): number {
  return Math.max(0, Math.min(1, (context.disturbance - 25) / 50));
}

function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
  const image = createImage(96, 72, [248, 250, 252, 255]);
  const color = COLORS[context.randomInt(0, COLORS.length - 1)]!;
  const cx = 48 + context.randomInt(-5, 5);
  const cy = 36 + context.randomInt(-4, 4);
  const size = context.randomInt(19, 25);
  if (shape === "circle") fillCircle(image, cx, cy, size, color);
  else if (shape === "square") fillRect(image, cx - size, cy - size, size * 2, size * 2, color);
  else if (shape === "triangle") {
    fillPolygon(
      image,
      [
        [cx, cy - size],
        [cx - size, cy + size],
        [cx + size, cy + size],
      ],
      color,
    );
  } else if (shape === "diamond") {
    fillPolygon(
      image,
      [
        [cx, cy - size],
        [cx - size, cy],
        [cx, cy + size],
        [cx + size, cy],
      ],
      color,
    );
  } else {
    fillPolygon(image, starPoints(cx, cy, size, size * 0.45), color);
  }

  const ratio = disturbanceRatio(context);
  const dots = Math.round(35 + ratio * 150);
  for (let index = 0; index < dots; index++) {
    setPixel(image, context.randomInt(0, 95), context.randomInt(0, 71), [
      context.randomInt(105, 225),
      context.randomInt(105, 225),
      context.randomInt(105, 225),
      Math.round(55 + ratio * 65),
    ]);
  }

  const lines = Math.round(1 + ratio * 4);
  for (let index = 0; index < lines; index++) {
    drawLine(
      image,
      context.randomInt(0, 95),
      context.randomInt(0, 71),
      context.randomInt(0, 95),
      context.randomInt(0, 71),
      [
        context.randomInt(100, 210),
        context.randomInt(100, 210),
        context.randomInt(100, 210),
        Math.round(45 + ratio * 55),
      ],
      ratio > 0.75 ? 2 : 1,
    );
  }
  return pngDataUri(image);
}

function shuffle<T>(items: T[], context: CaptchaGeneratorContext): T[] {
  for (let index = items.length - 1; index > 0; index--) {
    const other = context.randomInt(0, index);
    [items[index], items[other]] = [items[other]!, items[index]!];
  }
  return items;
}

export class ImageCaptchaGenerator implements CaptchaChallengeGenerator {
  readonly type = "image" as const;

  generate(context: CaptchaGeneratorContext): GeneratedCaptchaChallenge {
    const target = SHAPES[context.randomInt(0, SHAPES.length - 1)]!;
    const count = context.difficulty === "easy" ? 6 : 9;
    const matches = context.difficulty === "hard" ? 2 : 3;
    const entries: Array<{ id: string; shape: Shape; item: CaptchaImageItem }> = [];

    for (let index = 0; index < count; index++) {
      let shape: Shape;
      if (index < matches) shape = target;
      else {
        do shape = SHAPES[context.randomInt(0, SHAPES.length - 1)]!;
        while (shape === target);
      }
      const id = context.randomId(9);
      entries.push({
        id,
        shape,
        item: { id, image: shapeImage(shape, context), alt: `Challenge tile ${index + 1}` },
      });
    }

    shuffle(entries, context);
    const answer = entries
      .filter((entry) => entry.shape === target)
      .map((entry) => entry.id)
      .sort()
      .join(",");
    return {
      type: "image",
      presentation: "visual",
      prompt: `Select every ${target}`,
      answer,
      answerKind: "selections",
      items: entries.map((entry) => entry.item),
      minSelections: matches,
      maxSelections: matches,
      inputMode: "none",
      metadata: {
        target,
        count,
        matches,
        disturbance: context.disturbance,
        accessibleAlternative: "Request an audio challenge",
      },
    };
  }
}

export const imageCaptchaGenerator = new ImageCaptchaGenerator();
