import type { Raster, Rgb } from "./Raster.js";

/**
 * Synthetic reference figures for testing the analyzer.
 *
 * Deterministic and with known ground truth, so a landmark assertion means
 * something precise. Testing only against a real generated image would prove
 * the analyzer works on *that* image; these prove it works against stated
 * proportions, and let us build adversarial cases — a backpack that merges with
 * the collar, legs that never separate — on demand.
 */

export interface FigureSpec {
  width: number;
  height: number;
  backdrop: Rgb;
  /** All values are fractions of image height, measured from the crown. */
  headTop: number;
  neckY: number;
  shoulderY: number;
  crotchY: number;
  ankleY: number;
  soleY: number;
  headWidth: number;
  neckWidth: number;
  torsoWidth: number;
  legWidth: number;
  legGap: number;
  footWidth: number;
  armWidth: number;
  /** Arms separate from the torso below this height. */
  armSeparationY: number;
  armReach: number;
  /** Ankle width as a fraction of thigh width. */
  ankleTaper: number;
  colors: {
    hair: Rgb;
    skin: Rgb;
    torso: Rgb;
    arm: Rgb;
    leg: Rgb;
    foot: Rgb;
  };
  /** Optional pack that projects sideways beside the neck, merging with it. */
  backpack?: { fromY: number; toY: number; width: number; color: Rgb };
}

export const DEFAULT_FIGURE: FigureSpec = {
  width: 512,
  height: 768,
  backdrop: { r: 128, g: 128, b: 128 },
  headTop: 0.02,
  neckY: 0.14,
  shoulderY: 0.2,
  crotchY: 0.55,
  ankleY: 0.93,
  soleY: 0.98,
  headWidth: 0.1,
  neckWidth: 0.05,
  torsoWidth: 0.26,
  legWidth: 0.09,
  legGap: 0.035,
  footWidth: 0.11,
  armWidth: 0.075,
  armSeparationY: 0.3,
  armReach: 0.38,
  ankleTaper: 0.62,
  colors: {
    hair: { r: 40, g: 34, b: 30 },
    skin: { r: 198, g: 156, b: 130 },
    torso: { r: 86, g: 92, b: 60 },
    arm: { r: 74, g: 80, b: 52 },
    leg: { r: 48, g: 50, b: 58 },
    foot: { r: 92, g: 66, b: 44 },
  },
};

export function renderFigure(spec: FigureSpec = DEFAULT_FIGURE): Raster {
  const { width, height } = spec;
  const data = new Uint8Array(width * height * 4);

  const put = (x: number, y: number, color: Rgb) => {
    if (x < 0 || y < 0 || x >= width || y >= height) return;
    const i = (y * width + x) * 4;
    data[i] = color.r;
    data[i + 1] = color.g;
    data[i + 2] = color.b;
    data[i + 3] = 255;
  };

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) put(x, y, spec.backdrop);
  }

  const midX = Math.round(width / 2);
  const py = (t: number) => Math.round(t * height);
  const px = (t: number) => Math.round(t * height); // widths are height-relative

  // Half-widths must be integers: a fractional loop bound produces fractional
  // array indices, which typed arrays silently discard, and the shape simply
  // never gets drawn.
  const bar = (fromY: number, toY: number, halfWidth: number, color: Rgb, centre = midX) => {
    const half = Math.round(halfWidth);
    for (let y = py(fromY); y < py(toY); y++) {
      for (let x = centre - half; x <= centre + half; x++) put(x, y, color);
    }
  };

  // Head: a hair cap over a face, so the palette sampler has two bands to find.
  const headHalf = Math.round(px(spec.headWidth) / 2);
  const hairEnd = spec.headTop + (spec.neckY - spec.headTop) * 0.34;
  bar(spec.headTop, hairEnd, headHalf, spec.colors.hair);
  bar(hairEnd, spec.neckY, headHalf, spec.colors.skin);

  bar(spec.neckY, spec.shoulderY, Math.round(px(spec.neckWidth) / 2), spec.colors.skin);

  if (spec.backpack) {
    bar(
      spec.backpack.fromY,
      spec.backpack.toY,
      Math.round(px(spec.backpack.width) / 2),
      spec.backpack.color,
    );
  }

  bar(spec.shoulderY, spec.crotchY, Math.round(px(spec.torsoWidth) / 2), spec.colors.torso);

  // Arms: angled outward, leaving the torso at `armSeparationY`.
  const armHalf = Math.round(px(spec.armWidth) / 2);
  const torsoHalf = Math.round(px(spec.torsoWidth) / 2);
  const reachPx = px(spec.armReach);
  const armStartY = py(spec.shoulderY);
  const armEndY = py(spec.crotchY);

  for (let y = armStartY; y < armEndY; y++) {
    const t = (y - armStartY) / Math.max(1, armEndY - armStartY);
    const offset = Math.round(torsoHalf * 0.6 + (reachPx - armHalf - torsoHalf * 0.6) * t);
    for (const side of [-1, 1]) {
      const centre = midX + side * offset;
      for (let x = centre - armHalf; x <= centre + armHalf; x++) put(x, y, spec.colors.arm);
    }
  }

  // Legs and boots.
  const legHalf = Math.round(px(spec.legWidth) / 2);
  const gapHalf = Math.round(px(spec.legGap) / 2);
  const footHalf = Math.round(px(spec.footWidth) / 2);
  const ankleHalf = Math.round(legHalf * spec.ankleTaper);

  for (const side of [-1, 1]) {
    const centre = midX + side * (gapHalf + legHalf);

    // Legs taper from thigh to ankle, so the ankle is a genuine local minimum
    // in the width profile rather than an arbitrary row of a constant column.
    const fromY = py(spec.crotchY);
    const toY = py(spec.ankleY);
    for (let y = fromY; y < toY; y++) {
      const t = (y - fromY) / Math.max(1, toY - fromY);
      const half = Math.round(legHalf + (ankleHalf - legHalf) * t);
      for (let x = centre - half; x <= centre + half; x++) put(x, y, spec.colors.leg);
    }
    bar(spec.ankleY, spec.soleY, footHalf, spec.colors.foot, centre);
  }

  return { width, height, data };
}

/** The adversarial case: a high pack that merges with the collar in silhouette. */
export const PACKED_FIGURE: FigureSpec = {
  ...DEFAULT_FIGURE,
  backpack: {
    fromY: 0.155,
    toY: 0.45,
    width: 0.3,
    color: { r: 70, g: 64, b: 50 },
  },
};
