import type { CaptchaConcreteImageStyle, CaptchaGeneratorContext } from "../types.ts";
import {
  createImage,
  drawGlyph,
  drawLine,
  fillCircle,
  fillRect,
  pngDataUri,
  setPixel,
  type Rgba,
  type RgbaImage,
} from "./png.ts";

const LIGHT_BACKGROUND: Rgba = [248, 250, 252, 255];
const ALT_BACKGROUND: Rgba = [241, 245, 249, 255];
const INK: Rgba = [15, 23, 42, 255];
const ACCENTS: readonly Rgba[] = [
  [30, 64, 175, 235],
  [126, 34, 206, 235],
  [190, 24, 93, 230],
  [15, 118, 110, 230],
  [194, 65, 12, 230],
];

interface TextLayout {
  scale: number;
  spacing: number;
  startX: number;
  startY: number;
  textWidth: number;
  textHeight: number;
}

function clamp(value: number, minimum: number, maximum: number): number {
  return Math.max(minimum, Math.min(maximum, value));
}

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

function randomColor(
  context: CaptchaGeneratorContext,
  minimum = 70,
  maximum = 205,
  alpha = 100,
): Rgba {
  return [
    context.randomInt(minimum, maximum),
    context.randomInt(minimum, maximum),
    context.randomInt(minimum, maximum),
    alpha,
  ];
}

function rawPixel(
  image: RgbaImage,
  x: number,
  y: number,
): readonly [number, number, number, number] {
  const px = clamp(Math.round(x), 0, image.width - 1);
  const py = clamp(Math.round(y), 0, image.height - 1);
  const index = (py * image.width + px) * 4;
  return [
    image.data[index]!,
    image.data[index + 1]!,
    image.data[index + 2]!,
    image.data[index + 3]!,
  ];
}

function putRawPixel(
  image: RgbaImage,
  x: number,
  y: number,
  color: readonly [number, number, number, number],
): void {
  const px = Math.round(x);
  const py = Math.round(y);
  if (px < 0 || py < 0 || px >= image.width || py >= image.height) return;
  const index = (py * image.width + px) * 4;
  image.data[index] = color[0];
  image.data[index + 1] = color[1];
  image.data[index + 2] = color[2];
  image.data[index + 3] = color[3];
}

function cloneImage(image: RgbaImage): RgbaImage {
  return { width: image.width, height: image.height, data: image.data.slice() };
}

function replaceImage(target: RgbaImage, source: RgbaImage): void {
  target.data.set(source.data);
}

function layoutFor(text: string, style: CaptchaConcreteImageStyle): TextLayout {
  const width = 300;
  const height = 104;
  const scale = text.length > 9 ? 3 : text.length > 7 ? 4 : 5;
  const glyphWidth = scale * 5;
  const normalSpacing = scale + 3;
  const spacing =
    style === "collision"
      ? Math.max(-Math.floor(scale * 0.45), -2)
      : style === "cross-shadow"
        ? scale
        : normalSpacing;
  const textWidth = text.length * glyphWidth + Math.max(0, text.length - 1) * spacing;
  const textHeight = scale * 7;
  return {
    scale,
    spacing,
    startX: Math.max(12, Math.floor((width - textWidth) / 2)),
    startY: Math.floor((height - textHeight) / 2),
    textWidth,
    textHeight,
  };
}

function drawCircleOutline(
  image: RgbaImage,
  centerX: number,
  centerY: number,
  radius: number,
  color: Rgba,
  thickness = 1,
): void {
  const steps = Math.max(24, Math.round(radius * 8));
  for (let index = 0; index < steps; index++) {
    const angle = (index / steps) * Math.PI * 2;
    const x = centerX + Math.cos(angle) * radius;
    const y = centerY + Math.sin(angle) * radius;
    fillCircle(image, x, y, Math.max(0.75, thickness / 2), color);
  }
}

function drawDashedLine(
  image: RgbaImage,
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  color: Rgba,
  dash = 5,
  gap = 4,
  thickness = 1,
): void {
  const distance = Math.hypot(x1 - x0, y1 - y0);
  if (distance <= 0) return;
  const dx = (x1 - x0) / distance;
  const dy = (y1 - y0) / distance;
  for (let offset = 0; offset < distance; offset += dash + gap) {
    const end = Math.min(distance, offset + dash);
    drawLine(
      image,
      x0 + dx * offset,
      y0 + dy * offset,
      x0 + dx * end,
      y0 + dy * end,
      color,
      thickness,
    );
  }
}

function addDots(
  image: RgbaImage,
  context: CaptchaGeneratorContext,
  count: number,
  alpha: number,
  minimum = 75,
  maximum = 220,
  radius = 0,
): void {
  for (let index = 0; index < count; index++) {
    const x = context.randomInt(0, image.width - 1);
    const y = context.randomInt(0, image.height - 1);
    const color = randomColor(context, minimum, maximum, alpha);
    if (radius > 0) fillCircle(image, x, y, context.randomInt(1, radius), color);
    else setPixel(image, x, y, color);
  }
}

function addCrossingLines(
  image: RgbaImage,
  context: CaptchaGeneratorContext,
  count: number,
  alpha: number,
  thickness = 1,
): void {
  for (let index = 0; index < count; index++) {
    drawLine(
      image,
      context.randomInt(0, image.width - 1),
      context.randomInt(0, image.height - 1),
      context.randomInt(0, image.width - 1),
      context.randomInt(0, image.height - 1),
      randomColor(context, 65, 195, alpha),
      thickness,
    );
  }
}

function drawCharacters(
  image: RgbaImage,
  text: string,
  context: CaptchaGeneratorContext,
  layout: TextLayout,
  style: CaptchaConcreteImageStyle,
): void {
  const ratio = disturbanceRatio(context);
  const jitterX = Math.round(1 + ratio * 3);
  const jitterY = Math.round(2 + ratio * 4);
  const shear = 0.12 + ratio * 0.42;
  let cursor = layout.startX;

  for (let index = 0; index < text.length; index++) {
    const character = text[index]!;
    const x = cursor + context.randomInt(-jitterX, jitterX);
    const y = layout.startY + context.randomInt(-jitterY, jitterY);
    const glyphShear = (context.randomFloat() - 0.5) * shear;
    const accent = ACCENTS[context.randomInt(0, ACCENTS.length - 1)]!;
    const color =
      style === "collision" || style === "cross-shadow" || style === "pixel" ? accent : INK;

    if (style === "cross-shadow") {
      drawGlyph(image, character, x - 3, y + 3, layout.scale, [37, 99, 235, 95], glyphShear);
      drawGlyph(image, character, x + 3, y - 2, layout.scale, [220, 38, 38, 85], glyphShear);
    } else if (style === "collision") {
      drawGlyph(image, character, x - 2, y + 2, layout.scale, [15, 23, 42, 75], glyphShear);
      if (index > 0 && context.randomFloat() < 0.65) {
        drawGlyph(
          image,
          character,
          x + context.randomInt(-4, 1),
          y,
          layout.scale,
          [2, 6, 23, 60],
          -glyphShear,
        );
      }
    } else if (context.randomFloat() < 0.28 + ratio * 0.35) {
      drawGlyph(image, character, x + 1, y + 1, layout.scale, [15, 23, 42, 70], glyphShear);
    }

    drawGlyph(image, character, x, y, layout.scale, color, glyphShear);
    if (style === "pixel" && context.randomFloat() < 0.45) {
      drawGlyph(image, character, x + 1, y, layout.scale, [15, 23, 42, 90], glyphShear);
    }

    cursor += layout.scale * 5 + layout.spacing;
  }
}

function shiftRows(
  image: RgbaImage,
  shiftForY: (y: number) => number,
  background: Rgba = LIGHT_BACKGROUND,
): void {
  const source = cloneImage(image);
  const output = createImage(image.width, image.height, background);
  for (let y = 0; y < image.height; y++) {
    const shift = Math.round(shiftForY(y));
    for (let x = 0; x < image.width; x++) {
      const sourceX = x - shift;
      if (sourceX >= 0 && sourceX < image.width) {
        putRawPixel(output, x, y, rawPixel(source, sourceX, y));
      }
    }
  }
  replaceImage(image, output);
}

function shiftColumns(
  image: RgbaImage,
  shiftForX: (x: number) => number,
  background: Rgba = LIGHT_BACKGROUND,
): void {
  const source = cloneImage(image);
  const output = createImage(image.width, image.height, background);
  for (let x = 0; x < image.width; x++) {
    const shift = Math.round(shiftForX(x));
    for (let y = 0; y < image.height; y++) {
      const sourceY = y - shift;
      if (sourceY >= 0 && sourceY < image.height) {
        putRawPixel(output, x, y, rawPixel(source, x, sourceY));
      }
    }
  }
  replaceImage(image, output);
}

function applyClassic(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  addCrossingLines(
    image,
    context,
    Math.round(3 + ratio * 5),
    Math.round(55 + ratio * 55),
    ratio > 0.7 ? 2 : 1,
  );
  addDots(image, context, Math.round(140 + ratio * 470), Math.round(45 + ratio * 50));
}

function applyCollision(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  addCrossingLines(
    image,
    context,
    Math.round(5 + ratio * 7),
    Math.round(60 + ratio * 60),
    ratio > 0.55 ? 2 : 1,
  );
  addDots(image, context, Math.round(120 + ratio * 320), 70);
  const bars = Math.round(2 + ratio * 5);
  for (let index = 0; index < bars; index++) {
    fillRect(
      image,
      context.randomInt(0, image.width - 35),
      context.randomInt(18, image.height - 20),
      context.randomInt(18, 50),
      context.randomInt(1, ratio > 0.65 ? 3 : 2),
      randomColor(context, 35, 160, 75),
    );
  }
}

function applySnow(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  addDots(
    image,
    context,
    Math.round(420 + ratio * 950),
    Math.round(85 + ratio * 75),
    120,
    245,
    ratio > 0.5 ? 2 : 1,
  );
  for (let index = 0; index < Math.round(35 + ratio * 90); index++) {
    const x = context.randomInt(0, image.width - 1);
    const y = context.randomInt(0, image.height - 1);
    drawLine(image, x - 2, y, x + 2, y, [255, 255, 255, 185], 1);
    drawLine(image, x, y - 2, x, y + 2, [255, 255, 255, 185], 1);
  }
  addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
}

function applyCorrosion(
  image: RgbaImage,
  context: CaptchaGeneratorContext,
  layout: TextLayout,
): void {
  const ratio = disturbanceRatio(context);
  const holes = Math.round(70 + ratio * 230);
  for (let index = 0; index < holes; index++) {
    const x = context.randomInt(layout.startX - 5, layout.startX + layout.textWidth + 5);
    const y = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
    const size = context.randomInt(1, ratio > 0.65 ? 4 : 3);
    fillRect(
      image,
      x,
      y,
      size,
      size,
      context.randomFloat() < 0.6 ? LIGHT_BACKGROUND : [203, 213, 225, 210],
    );
  }
  addDots(image, context, Math.round(170 + ratio * 430), 85, 90, 190, 2);
  addCrossingLines(image, context, Math.round(2 + ratio * 4), 60, 1);
}

function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const nodes = Array.from({ length: Math.round(8 + ratio * 12) }, () => ({
    x: context.randomInt(0, image.width - 1),
    y: context.randomInt(0, image.height - 1),
  }));
  for (let index = 0; index < nodes.length; index++) {
    const current = nodes[index]!;
    const next =
      nodes[(index + context.randomInt(1, Math.max(1, nodes.length - 1))) % nodes.length]!;
    drawLine(
      image,
      current.x,
      current.y,
      next.x,
      next.y,
      [71, 85, 105, Math.round(65 + ratio * 65)],
      1,
    );
    if (index % 2 === 0)
      drawCircleOutline(
        image,
        current.x,
        current.y,
        context.randomInt(2, 5),
        [100, 116, 139, 70],
        1,
      );
  }
  const anchorX = context.randomInt(Math.floor(image.width * 0.25), Math.floor(image.width * 0.75));
  const anchorY = context.randomInt(
    Math.floor(image.height * 0.25),
    Math.floor(image.height * 0.75),
  );
  for (let index = 0; index < Math.round(7 + ratio * 7); index++) {
    const angle = (index / Math.round(7 + ratio * 7)) * Math.PI * 2;
    drawLine(
      image,
      anchorX,
      anchorY,
      anchorX + Math.cos(angle) * image.width,
      anchorY + Math.sin(angle) * image.height,
      [71, 85, 105, 65],
      1,
    );
  }
  addDots(image, context, Math.round(80 + ratio * 200), 60);
}

function applyCrossShadow(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const centerY = Math.floor(image.height / 2);
  drawLine(
    image,
    0,
    centerY - 6,
    image.width - 1,
    centerY + 6,
    [37, 99, 235, 75],
    ratio > 0.55 ? 2 : 1,
  );
  drawLine(
    image,
    0,
    centerY + 7,
    image.width - 1,
    centerY - 8,
    [220, 38, 38, 65],
    ratio > 0.55 ? 2 : 1,
  );
  addCrossingLines(image, context, Math.round(2 + ratio * 5), 55, 1);
  addDots(image, context, Math.round(120 + ratio * 300), 60);
}

function applySplit(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const bandHeight = context.randomInt(7, 13);
  const amplitude = Math.round(4 + ratio * 13);
  shiftRows(image, (y) => {
    const band = Math.floor(y / bandHeight);
    return band % 2 === 0 ? amplitude : -amplitude;
  });
  for (let y = bandHeight; y < image.height; y += bandHeight) {
    drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 75], 1);
  }
  addDots(image, context, Math.round(90 + ratio * 250), 65);
}

function applySplit2(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const stripWidth = context.randomInt(10, 18);
  const amplitude = Math.round(3 + ratio * 10);
  shiftColumns(image, (x) => {
    const strip = Math.floor(x / stripWidth);
    return strip % 2 === 0 ? amplitude : -amplitude;
  });
  for (let x = stripWidth; x < image.width; x += stripWidth) {
    drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 65], 1);
  }
  addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
}

function applyCut(image: RgbaImage, context: CaptchaGeneratorContext, layout: TextLayout): void {
  const ratio = disturbanceRatio(context);
  const cuts = Math.round(4 + ratio * 8);
  for (let index = 0; index < cuts; index++) {
    const x0 = context.randomInt(layout.startX - 10, layout.startX + layout.textWidth);
    const y0 = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
    const x1 = x0 + context.randomInt(24, 72);
    const y1 = y0 + context.randomInt(-18, 18);
    drawLine(image, x0, y0, x1, y1, [248, 250, 252, 245], ratio > 0.6 ? 3 : 2);
    drawLine(image, x0, y0 + 2, x1, y1 + 2, randomColor(context, 75, 170, 80), 1);
  }
  addDots(image, context, Math.round(90 + ratio * 260), 70);
}

function applyDarts(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const targets = Math.round(2 + ratio * 3);
  for (let index = 0; index < targets; index++) {
    const x = context.randomInt(25, image.width - 25);
    const y = context.randomInt(16, image.height - 16);
    const maxRadius = context.randomInt(9, Math.round(14 + ratio * 11));
    for (let radius = maxRadius; radius >= 4; radius -= 5) {
      drawCircleOutline(image, x, y, radius, randomColor(context, 55, 175, 70), 1);
    }
    for (let ray = 0; ray < Math.round(3 + ratio * 4); ray++) {
      const angle = context.randomFloat() * Math.PI * 2;
      drawLine(
        image,
        x,
        y,
        x + Math.cos(angle) * context.randomInt(25, 70),
        y + Math.sin(angle) * context.randomInt(18, 55),
        randomColor(context, 55, 175, 70),
        1,
      );
    }
  }
  addDots(image, context, Math.round(100 + ratio * 240), 60);
}

function applyDistortion(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const amplitudeX = 3 + ratio * 9;
  const amplitudeY = 2 + ratio * 5;
  const phase = context.randomFloat() * Math.PI * 2;
  shiftRows(image, (y) => Math.sin(y / (7 + ratio * 4) + phase) * amplitudeX);
  shiftColumns(image, (x) => Math.sin(x / (20 - ratio * 6) + phase) * amplitudeY);
  addCrossingLines(image, context, Math.round(3 + ratio * 5), 60, ratio > 0.7 ? 2 : 1);
  addDots(image, context, Math.round(100 + ratio * 280), 60);
}

function applyStitch(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const rows = Math.round(4 + ratio * 5);
  for (let index = 0; index < rows; index++) {
    const y = Math.round(((index + 1) / (rows + 1)) * image.height);
    drawDashedLine(
      image,
      0,
      y,
      image.width - 1,
      y + context.randomInt(-3, 3),
      [71, 85, 105, 75],
      4,
      4,
      1,
    );
  }
  const columns = Math.round(2 + ratio * 4);
  for (let index = 0; index < columns; index++) {
    const x = context.randomInt(10, image.width - 10);
    drawDashedLine(
      image,
      x,
      0,
      x + context.randomInt(-5, 5),
      image.height - 1,
      [100, 116, 139, 65],
      3,
      5,
      1,
    );
  }
  for (let index = 0; index < Math.round(18 + ratio * 35); index++) {
    const x = context.randomInt(0, image.width - 1);
    const y = context.randomInt(0, image.height - 1);
    drawLine(image, x - 2, y - 2, x + 2, y + 2, [100, 116, 139, 65], 1);
    drawLine(image, x + 2, y - 2, x - 2, y + 2, [100, 116, 139, 65], 1);
  }
}

function applyStriped(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const spacing = Math.round(10 - ratio * 4);
  const offset = context.randomInt(-image.height, image.width);
  for (let x = offset; x < image.width + image.height; x += spacing) {
    drawLine(
      image,
      x,
      0,
      x - image.height,
      image.height - 1,
      [71, 85, 105, Math.round(45 + ratio * 45)],
      ratio > 0.7 ? 2 : 1,
    );
  }
  for (let y = context.randomInt(3, 9); y < image.height; y += context.randomInt(8, 14)) {
    drawLine(image, 0, y, image.width - 1, y, [148, 163, 184, 45], 1);
  }
  addDots(image, context, Math.round(70 + ratio * 180), 55);
}

function applyWave(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const phase = context.randomFloat() * Math.PI * 2;
  const amplitude = 5 + ratio * 13;
  shiftRows(image, (y) => Math.sin(y / (5.5 + ratio * 3) + phase) * amplitude);
  shiftColumns(image, (x) => Math.cos(x / (24 - ratio * 7) + phase) * (2 + ratio * 6));
  for (let index = 0; index < Math.round(2 + ratio * 3); index++) {
    const baseY = context.randomInt(10, image.height - 10);
    let previousX = 0;
    let previousY = baseY;
    for (let x = 8; x < image.width; x += 8) {
      const y = baseY + Math.sin(x / 17 + phase + index) * (5 + ratio * 5);
      drawLine(image, previousX, previousY, x, y, randomColor(context, 75, 180, 55), 1);
      previousX = x;
      previousY = y;
    }
  }
}

function applyGridNoise(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const cell = Math.round(16 - ratio * 6);
  for (let x = 0; x < image.width; x += cell) {
    drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 55], 1);
  }
  for (let y = 0; y < image.height; y += cell) {
    drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 55], 1);
  }
  for (let index = 0; index < Math.round(10 + ratio * 26); index++) {
    const x = context.randomInt(0, Math.floor(image.width / cell)) * cell;
    const y = context.randomInt(0, Math.floor(image.height / cell)) * cell;
    fillRect(image, x, y, cell, cell, randomColor(context, 100, 220, Math.round(25 + ratio * 35)));
  }
}

function applyScribble(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const scribbles = Math.round(3 + ratio * 6);
  for (let index = 0; index < scribbles; index++) {
    let x = context.randomInt(0, image.width - 1);
    let y = context.randomInt(0, image.height - 1);
    const segments = context.randomInt(7, Math.round(12 + ratio * 13));
    const color = randomColor(context, 45, 180, Math.round(55 + ratio * 45));
    for (let segment = 0; segment < segments; segment++) {
      const nextX = clamp(x + context.randomInt(-28, 28), 0, image.width - 1);
      const nextY = clamp(y + context.randomInt(-18, 18), 0, image.height - 1);
      drawLine(image, x, y, nextX, nextY, color, ratio > 0.7 && segment % 3 === 0 ? 2 : 1);
      x = nextX;
      y = nextY;
    }
  }
  addDots(image, context, Math.round(80 + ratio * 220), 55);
}

function applyPixel(image: RgbaImage, context: CaptchaGeneratorContext): void {
  const ratio = disturbanceRatio(context);
  const blocks = Math.round(28 + ratio * 75);
  for (let index = 0; index < blocks; index++) {
    const size = context.randomInt(2, Math.round(4 + ratio * 5));
    fillRect(
      image,
      context.randomInt(0, image.width - size),
      context.randomInt(0, image.height - size),
      size,
      size,
      randomColor(context, 65, 220, Math.round(45 + ratio * 45)),
    );
  }
  const source = cloneImage(image);
  for (let index = 0; index < Math.round(4 + ratio * 8); index++) {
    const blockWidth = context.randomInt(12, 28);
    const blockHeight = context.randomInt(6, 16);
    const x = context.randomInt(0, image.width - blockWidth);
    const y = context.randomInt(0, image.height - blockHeight);
    const shift = context.randomInt(-8, 8);
    for (let py = 0; py < blockHeight; py++) {
      for (let px = 0; px < blockWidth; px++) {
        const sourceX = clamp(x + px, 0, image.width - 1);
        const targetX = x + px + shift;
        putRawPixel(image, targetX, y + py, rawPixel(source, sourceX, y + py));
      }
    }
  }
}

function applyBrokenLines(
  image: RgbaImage,
  context: CaptchaGeneratorContext,
  layout: TextLayout,
): void {
  const ratio = disturbanceRatio(context);
  const gaps = Math.round(12 + ratio * 35);
  for (let index = 0; index < gaps; index++) {
    const horizontal = context.randomFloat() < 0.7;
    const width = horizontal ? context.randomInt(6, 24) : context.randomInt(1, 4);
    const height = horizontal ? context.randomInt(1, 3) : context.randomInt(7, 18);
    fillRect(
      image,
      context.randomInt(layout.startX - 3, layout.startX + layout.textWidth),
      context.randomInt(layout.startY - 3, layout.startY + layout.textHeight),
      width,
      height,
      LIGHT_BACKGROUND,
    );
  }
  for (let index = 0; index < Math.round(12 + ratio * 28); index++) {
    const x = context.randomInt(0, image.width - 12);
    const y = context.randomInt(0, image.height - 1);
    drawLine(
      image,
      x,
      y,
      x + context.randomInt(4, 18),
      y + context.randomInt(-2, 2),
      randomColor(context, 45, 175, 75),
      1,
    );
  }
  addDots(image, context, Math.round(70 + ratio * 180), 60);
}

function applyStyle(image: RgbaImage, context: CaptchaGeneratorContext, layout: TextLayout): void {
  switch (context.imageStyle) {
    case "collision":
      applyCollision(image, context);
      break;
    case "snow":
      applySnow(image, context);
      break;
    case "corrosion":
      applyCorrosion(image, context, layout);
      break;
    case "spiderweb":
      applySpiderweb(image, context);
      break;
    case "cross-shadow":
      applyCrossShadow(image, context);
      break;
    case "split":
      applySplit(image, context);
      break;
    case "split2":
      applySplit2(image, context);
      break;
    case "cut":
      applyCut(image, context, layout);
      break;
    case "darts":
      applyDarts(image, context);
      break;
    case "distortion":
      applyDistortion(image, context);
      break;
    case "stitch":
      applyStitch(image, context);
      break;
    case "striped":
      applyStriped(image, context);
      break;
    case "wave":
      applyWave(image, context);
      break;
    case "grid-noise":
      applyGridNoise(image, context);
      break;
    case "scribble":
      applyScribble(image, context);
      break;
    case "pixel":
      applyPixel(image, context);
      break;
    case "broken-lines":
      applyBrokenLines(image, context, layout);
      break;
    default:
      applyClassic(image, context);
  }
}

function backgroundFor(style: CaptchaConcreteImageStyle): Rgba {
  if (style === "snow" || style === "grid-noise" || style === "stitch") return ALT_BACKGROUND;
  return LIGHT_BACKGROUND;
}

export function renderTextChallenge(text: string, context: CaptchaGeneratorContext): string {
  const layout = layoutFor(text, context.imageStyle);
  const image = createImage(300, 104, backgroundFor(context.imageStyle));

  if (context.imageStyle === "grid-noise") applyGridNoise(image, context);
  else if (context.imageStyle === "snow") addDots(image, context, 130, 45, 160, 235, 1);
  else addDots(image, context, Math.round(35 + disturbanceRatio(context) * 80), 30);

  drawCharacters(image, text, context, layout, context.imageStyle);
  applyStyle(image, context, layout);

  if (context.imageStyle !== "snow" && context.imageStyle !== "grid-noise") {
    const ratio = disturbanceRatio(context);
    addDots(image, context, Math.round(25 + ratio * 100), 35, 100, 220);
  }

  return pngDataUri(image);
}
