/**
 * Sprite loading + chroma keying for Hearthroot Hollow.
 *
 * CHROMA KEY: gameplay sprites are generated by gpt-image-2 on SOLID MAGENTA
 * (#FF00FF) because the model has no reliable alpha channel. We key the
 * magenta out at load time (with a despill pass for fringe pixels) and
 * auto-trim transparent margins. Sheets (crops = 3 cells, tools = 2x2 cells)
 * are sliced into cells BEFORE keying/trim.
 *
 * FALLBACK: if a sprite file fails to load (404 / gen script never ran), we
 * draw a procedural stand-in and record it in `fallbacks`.
 */
import { CROPS, type CropType } from './world.ts';

export interface Sprite {
  canvas: HTMLCanvasElement;
  generated: boolean; // false => procgen fallback
  /** trimmed size as a fraction of the source cell — preserves relative scale
   *  between growth stages painted in the same sheet */
  relW: number;
  relH: number;
}

export interface Assets {
  farmerDown: Sprite;
  farmerUp: Sprite;
  farmerSide: Sprite;
  tree: Sprite;
  background: Sprite; // full-bleed title plate, NOT keyed
  crops: Record<CropType, Sprite[]>; // 3 growth stages each
  tools: Sprite[]; // hoe, can, seeds, scythe
  house: Sprite;
  scarecrow: Sprite;
  chicken: Sprite[]; // [standing, pecking]
  duck: Sprite[]; // [gliding, dabbling]
  props: Sprite[]; // rock, mushroom, log, lantern (2x2 sheet)
  fallbacks: string[];
}

type FallbackFn = (ctx: CanvasRenderingContext2D, w: number, h: number) => void;

export async function loadAssets(): Promise<Assets> {
  const fallbacks: string[] = [];

  async function one(file: string, chroma: boolean, fb: FallbackFn, fbW = 256, fbH = 256): Promise<Sprite> {
    const img = await loadImage(spriteUrl(file));
    if (!img) {
      fallbacks.push(file);
      const c = makeCanvas(fbW, fbH);
      fb(c.getContext('2d')!, fbW, fbH);
      return { canvas: c, generated: false, relW: 1, relH: 1 };
    }
    return chroma ? keyAndTrim(img, 0, 0, img.naturalWidth, img.naturalHeight) : plain(img);
  }

  async function cells(file: string, cols: number, rows: number, fbs: FallbackFn[]): Promise<Sprite[]> {
    const img = await loadImage(spriteUrl(file));
    if (!img) {
      fallbacks.push(file);
      return fbs.map((fb) => {
        const c = makeCanvas(256, 256);
        fb(c.getContext('2d')!, 256, 256);
        return { canvas: c, generated: false, relW: 1, relH: 1 };
      });
    }
    const cw = Math.floor(img.naturalWidth / cols);
    const ch = Math.floor(img.naturalHeight / rows);
    const out: Sprite[] = [];
    for (let r = 0; r < rows; r++) {
      for (let cIdx = 0; cIdx < cols; cIdx++) {
        // inset cells slightly so neighbours bleeding over the gutter get cut
        const inset = Math.floor(cw * 0.02);
        out.push(keyAndTrim(img, cIdx * cw + inset, r * ch, cw - inset * 2, ch));
      }
    }
    return out;
  }

  const [farmerDown, farmerUp, farmerSide, tree, background, tools, house, scarecrow, chicken, duck, props, ...cropSheets] = await Promise.all([
    one('farmer-down.png', true, fbFarmer),
    one('farmer-up.png', true, fbFarmer),
    one('farmer-side.png', true, fbFarmer),
    one('tree.png', true, fbTree),
    one('background.png', false, fbBackground, 1536, 1024),
    cells('tools.png', 2, 2, [fbTool('#b08850'), fbTool('#7ba8c9'), fbTool('#c9a36a'), fbTool('#9aa5ad')]),
    one('farmhouse.png', true, fbHouse),
    one('scarecrow.png', true, fbScarecrow),
    cells('chicken.png', 2, 1, [fbChicken, fbChicken]),
    cells('duck.png', 2, 1, [fbDuck, fbDuck]),
    cells('props.png', 2, 2, [fbRock, fbMushroom, fbLog, fbLantern]),
    ...CROPS.map((c) =>
      cells(`crop-${c.id}.png`, 3, 1, [fbCrop(c.color, 0), fbCrop(c.color, 1), fbCrop(c.color, 2)]),
    ),
  ]);

  const crops = {} as Record<CropType, Sprite[]>;
  CROPS.forEach((c, i) => (crops[c.id] = cropSheets[i]));

  if (fallbacks.length) {
    console.warn(
      `[assets] PROCGEN FALLBACK in use for: ${fallbacks.join(', ')} — run \`npm run gen:sprites\` for real art.`,
    );
  }
  return { farmerDown, farmerUp, farmerSide, tree, background, crops, tools, house, scarecrow, chicken, duck, props, fallbacks };
}

function spriteUrl(file: string): string {
  return `${import.meta.env.BASE_URL}sprites/${file}`;
}

function loadImage(url: string): Promise<HTMLImageElement | null> {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => resolve(null);
    img.src = url;
  });
}

function makeCanvas(w: number, h: number): HTMLCanvasElement {
  const c = document.createElement('canvas');
  c.width = w;
  c.height = h;
  return c;
}

function plain(img: HTMLImageElement): Sprite {
  const c = makeCanvas(img.naturalWidth, img.naturalHeight);
  c.getContext('2d')!.drawImage(img, 0, 0);
  return { canvas: c, generated: true, relW: 1, relH: 1 };
}

/** Slice a region, remove the magenta key, despill fringes, trim margins. */
function keyAndTrim(img: HTMLImageElement, sx: number, sy: number, sw: number, sh: number): Sprite {
  const c = makeCanvas(sw, sh);
  const ctx = c.getContext('2d', { willReadFrequently: true })!;
  ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
  const data = ctx.getImageData(0, 0, sw, sh);
  const px = data.data;

  for (let i = 0; i < px.length; i += 4) {
    const r = px[i];
    const g = px[i + 1];
    const b = px[i + 2];
    const magenta = Math.min(r, b) - g; // how strongly "magenta" the pixel is
    if (magenta > 70 && r > 90 && b > 90) {
      px[i + 3] = 0; // hard key
    } else if (magenta > 20) {
      // despill: pull pink fringe back toward neutral and feather alpha
      const spill = magenta - 20;
      px[i] = r - spill * 0.7;
      px[i + 2] = b - spill * 0.7;
      px[i + 3] = Math.max(0, px[i + 3] - spill * 2);
    }
  }
  // connected-component cleanup: sheet cells can catch fragments of the
  // NEIGHBOURING stage bleeding over the cell boundary. Those fragments
  // always touch the cell's left/right edge — erase every side-touching or
  // speck-sized component except the largest one.
  const solid = (i: number) => px[i * 4 + 3] > 12;
  const labels = new Int32Array(sw * sh).fill(-1);
  interface Comp { size: number; touchesSide: boolean; id: number }
  const comps: Comp[] = [];
  const stack: number[] = [];
  for (let start = 0; start < sw * sh; start++) {
    if (!solid(start) || labels[start] !== -1) continue;
    const id = comps.length;
    const comp: Comp = { size: 0, touchesSide: false, id };
    comps.push(comp);
    stack.length = 0;
    stack.push(start);
    labels[start] = id;
    while (stack.length) {
      const i = stack.pop()!;
      comp.size++;
      const x = i % sw, y = (i / sw) | 0;
      if (x === 0 || x === sw - 1) comp.touchesSide = true;
      for (const j of [x > 0 ? i - 1 : -1, x < sw - 1 ? i + 1 : -1, y > 0 ? i - sw : -1, y < sh - 1 ? i + sw : -1]) {
        if (j >= 0 && labels[j] === -1 && solid(j)) {
          labels[j] = id;
          stack.push(j);
        }
      }
    }
  }
  if (comps.length > 1) {
    const largest = comps.reduce((a, b) => (b.size > a.size ? b : a));
    const minKeep = sw * sh * 0.004;
    const erase = new Set<number>();
    for (const c of comps) {
      if (c.id !== largest.id && (c.touchesSide || c.size < minKeep)) erase.add(c.id);
    }
    if (erase.size) {
      for (let i = 0; i < sw * sh; i++) {
        if (labels[i] !== -1 && erase.has(labels[i])) px[i * 4 + 3] = 0;
      }
    }
  }
  ctx.putImageData(data, 0, 0);

  // auto-trim transparent margins
  let minX = sw, minY = sh, maxX = 0, maxY = 0;
  for (let y = 0; y < sh; y++) {
    for (let x = 0; x < sw; x++) {
      if (px[(y * sw + x) * 4 + 3] > 12) {
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
      }
    }
  }
  if (maxX <= minX || maxY <= minY) return { canvas: c, generated: true, relW: 1, relH: 1 };
  const tw = maxX - minX + 1;
  const th = maxY - minY + 1;
  const trimmed = makeCanvas(tw, th);
  trimmed.getContext('2d')!.drawImage(c, minX, minY, tw, th, 0, 0, tw, th);
  return { canvas: trimmed, generated: true, relW: tw / sw, relH: th / sh };
}

// ---------------------------------------------------------------------------
// Procedural fallbacks (only used when generated art is missing)
// ---------------------------------------------------------------------------

function fbFarmer(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  // little overalls person
  ctx.fillStyle = '#8a5a33';
  ctx.beginPath();
  ctx.roundRect(w * 0.36, h * 0.42, w * 0.28, h * 0.4, 18);
  ctx.fill();
  ctx.fillStyle = '#f5d6a8';
  ctx.beginPath();
  ctx.arc(w * 0.5, h * 0.32, w * 0.15, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#e8b04a';
  ctx.beginPath();
  ctx.ellipse(w * 0.5, h * 0.24, w * 0.22, h * 0.06, 0, 0, Math.PI * 2);
  ctx.fill();
}

function fbTree(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#7a5230';
  ctx.fillRect(w * 0.44, h * 0.6, w * 0.12, h * 0.3);
  ctx.fillStyle = '#3f7d3a';
  ctx.beginPath();
  ctx.arc(w * 0.5, h * 0.4, w * 0.32, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#549b4c';
  ctx.beginPath();
  ctx.arc(w * 0.4, h * 0.32, w * 0.18, 0, Math.PI * 2);
  ctx.fill();
}

function fbCrop(color: string, stage: number): FallbackFn {
  return (ctx, w, h) => {
    ctx.strokeStyle = '#3e7a35';
    ctx.lineWidth = w * 0.05;
    ctx.beginPath();
    ctx.moveTo(w / 2, h * 0.9);
    ctx.lineTo(w / 2, h * (0.85 - stage * 0.25));
    ctx.stroke();
    ctx.fillStyle = stage === 2 ? color : '#57a04b';
    const r = w * (0.08 + stage * 0.1);
    ctx.beginPath();
    ctx.arc(w / 2, h * (0.8 - stage * 0.25), r, 0, Math.PI * 2);
    ctx.fill();
  };
}

function fbTool(color: string): FallbackFn {
  return (ctx, w, h) => {
    ctx.strokeStyle = '#7a5230';
    ctx.lineWidth = w * 0.08;
    ctx.beginPath();
    ctx.moveTo(w * 0.3, h * 0.8);
    ctx.lineTo(w * 0.7, h * 0.3);
    ctx.stroke();
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(w * 0.7, h * 0.3, w * 0.16, 0, Math.PI * 2);
    ctx.fill();
  };
}

function fbHouse(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#e8d9b8'; // walls
  ctx.fillRect(w * 0.18, h * 0.42, w * 0.64, h * 0.46);
  ctx.fillStyle = '#b5453a'; // roof
  ctx.beginPath();
  ctx.moveTo(w * 0.1, h * 0.45);
  ctx.lineTo(w * 0.5, h * 0.12);
  ctx.lineTo(w * 0.9, h * 0.45);
  ctx.fill();
  ctx.fillStyle = '#7a5230'; // door
  ctx.beginPath();
  ctx.roundRect(w * 0.43, h * 0.62, w * 0.14, h * 0.26, 10);
  ctx.fill();
  ctx.fillStyle = '#ffd98a'; // windows
  ctx.fillRect(w * 0.24, h * 0.52, w * 0.12, h * 0.12);
  ctx.fillRect(w * 0.64, h * 0.52, w * 0.12, h * 0.12);
  ctx.fillStyle = '#8a8d94'; // chimney
  ctx.fillRect(w * 0.66, h * 0.14, w * 0.09, h * 0.2);
}

function fbScarecrow(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.strokeStyle = '#8a6238';
  ctx.lineWidth = w * 0.05;
  ctx.beginPath();
  ctx.moveTo(w * 0.5, h * 0.3);
  ctx.lineTo(w * 0.5, h * 0.9);
  ctx.moveTo(w * 0.25, h * 0.45);
  ctx.lineTo(w * 0.75, h * 0.45);
  ctx.stroke();
  ctx.fillStyle = '#d9b56a'; // head
  ctx.beginPath();
  ctx.arc(w * 0.5, h * 0.3, w * 0.13, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#c98a2e'; // hat
  ctx.beginPath();
  ctx.ellipse(w * 0.5, h * 0.2, w * 0.2, h * 0.05, 0, 0, Math.PI * 2);
  ctx.fill();
}

function fbChicken(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#f5f0e3';
  ctx.beginPath();
  ctx.ellipse(w * 0.5, h * 0.55, w * 0.25, h * 0.2, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(w * 0.68, h * 0.38, w * 0.1, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#e85542'; // comb
  ctx.beginPath();
  ctx.arc(w * 0.68, h * 0.27, w * 0.045, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#f2a93e'; // beak
  ctx.beginPath();
  ctx.moveTo(w * 0.77, h * 0.38);
  ctx.lineTo(w * 0.86, h * 0.41);
  ctx.lineTo(w * 0.77, h * 0.44);
  ctx.fill();
}

function fbDuck(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#8a7a52';
  ctx.beginPath();
  ctx.ellipse(w * 0.5, h * 0.6, w * 0.26, h * 0.16, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#3e7a4a'; // head
  ctx.beginPath();
  ctx.arc(w * 0.7, h * 0.42, w * 0.09, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#f2c93e'; // bill
  ctx.beginPath();
  ctx.ellipse(w * 0.81, h * 0.44, w * 0.06, h * 0.03, 0, 0, Math.PI * 2);
  ctx.fill();
}

function fbRock(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#9a9da3';
  for (const [bx, by, r] of [[0.38, 0.62, 0.2], [0.62, 0.65, 0.16], [0.52, 0.5, 0.13]] as const) {
    ctx.beginPath();
    ctx.arc(w * bx, h * by, w * r, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.fillStyle = 'rgba(255,255,255,.25)';
  ctx.beginPath();
  ctx.arc(w * 0.34, h * 0.56, w * 0.07, 0, Math.PI * 2);
  ctx.fill();
}

function fbMushroom(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  for (const [mx, s] of [[0.38, 1], [0.6, 0.7]] as const) {
    ctx.fillStyle = '#f2e8d5';
    ctx.fillRect(w * (mx - 0.04 * s), h * 0.55, w * 0.08 * s, h * 0.25 * s);
    ctx.fillStyle = '#d9483e';
    ctx.beginPath();
    ctx.ellipse(w * mx, h * 0.55, w * 0.14 * s, h * 0.1 * s, 0, Math.PI, 0);
    ctx.fill();
  }
}

function fbLog(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#8a6238';
  ctx.beginPath();
  ctx.roundRect(w * 0.2, h * 0.5, w * 0.6, h * 0.22, 14);
  ctx.fill();
  ctx.fillStyle = '#c9a36a';
  ctx.beginPath();
  ctx.ellipse(w * 0.2, h * 0.61, w * 0.05, h * 0.11, 0, 0, Math.PI * 2);
  ctx.fill();
}

function fbLantern(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.strokeStyle = '#6b4a28';
  ctx.lineWidth = w * 0.06;
  ctx.beginPath();
  ctx.moveTo(w * 0.45, h * 0.9);
  ctx.lineTo(w * 0.45, h * 0.2);
  ctx.lineTo(w * 0.62, h * 0.25);
  ctx.stroke();
  ctx.fillStyle = '#ffd98a';
  ctx.beginPath();
  ctx.roundRect(w * 0.56, h * 0.27, w * 0.13, h * 0.16, 6);
  ctx.fill();
}

function fbBackground(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  const sky = ctx.createLinearGradient(0, 0, 0, h);
  sky.addColorStop(0, '#ffd9a0');
  sky.addColorStop(0.45, '#ffb877');
  sky.addColorStop(0.55, '#7fae54');
  sky.addColorStop(1, '#4a7a3a');
  ctx.fillStyle = sky;
  ctx.fillRect(0, 0, w, h);
}
