/**
 * Sprite loading + chroma keying.
 *
 * 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 here at load time (with a despill pass for fringe pixels) and
 * auto-trim the transparent margins so sprites hug their hitboxes.
 *
 * FALLBACK: if any sprite file fails to load (404 / gen script never ran),
 * we draw a procedural stand-in and record it in `fallbacks` so the game can
 * shout about it loudly in the console.
 */

export interface Sprite {
  canvas: HTMLCanvasElement;
  generated: boolean; // false => procgen fallback
}

export interface Assets {
  playerIdle: Sprite;
  playerRun: Sprite;
  wisp: Sprite;
  scuttler: Sprite;
  boss: Sprite;
  pickup: Sprite;
  ground: Sprite;
  groundWall: Sprite;
  platform: Sprite;
  bgVestibule: Sprite; // full-bleed, NOT chroma keyed
  bgArchive: Sprite;
  bgShaft: Sprite;
  bgBoss: Sprite;
  fallbacks: string[];
}

interface SpriteDef {
  key: keyof Omit<Assets, 'fallbacks'>;
  file: string;
  chroma: boolean;
  /** optional pre-crop (fractions of source size) applied before keying/trim */
  crop?: { x: number; y: number; w: number; h: number };
  fallback: (ctx: CanvasRenderingContext2D, w: number, h: number) => void;
}

const DEFS: SpriteDef[] = [
  { key: 'playerIdle', file: 'player-idle.png', chroma: true, fallback: fbPlayer },
  { key: 'playerRun', file: 'player-run.png', chroma: true, fallback: fbPlayer },
  { key: 'wisp', file: 'enemy-wisp.png', chroma: true, fallback: fbWisp },
  { key: 'scuttler', file: 'enemy-scuttler.png', chroma: true, fallback: fbScuttler },
  { key: 'boss', file: 'boss-orrery.png', chroma: true, fallback: fbBoss },
  { key: 'pickup', file: 'pickup-stargrip.png', chroma: true, fallback: fbPickup },
  // the generated plate is a whole observatory facade (dome + telescope on
  // top, icicles below). Two crops of the same file: floors keep the snowy
  // walkable ledge; walls get the pure masonry body so stacking shows no
  // repeating ledge stripe.
  { key: 'ground', file: 'ground.png', chroma: true, crop: { x: 0.05, y: 0.37, w: 0.9, h: 0.55 }, fallback: fbGround },
  { key: 'groundWall', file: 'ground.png', chroma: true, crop: { x: 0.07, y: 0.52, w: 0.86, h: 0.37 }, fallback: fbGround },
  { key: 'platform', file: 'platform.png', chroma: true, crop: { x: 0, y: 0, w: 1, h: 0.72 }, fallback: fbPlatform },
  { key: 'bgVestibule', file: 'bg-vestibule.jpg', chroma: false, fallback: fbBg(16, 24, 40, 8, 12, 22) },
  { key: 'bgArchive', file: 'bg-archive.jpg', chroma: false, fallback: fbBg(18, 28, 44, 10, 14, 26) },
  { key: 'bgShaft', file: 'bg-shaft.jpg', chroma: false, fallback: fbBg(14, 22, 42, 6, 10, 20) },
  { key: 'bgBoss', file: 'bg-orrery.jpg', chroma: false, fallback: fbBg(28, 22, 30, 12, 8, 14) },
];

export async function loadAssets(onProgress?: (done: number, total: number) => void): Promise<Assets> {
  const out = { fallbacks: [] as string[] } as Assets;
  let done = 0;
  await Promise.all(
    DEFS.map(async (def) => {
      const url = `${import.meta.env.BASE_URL}sprites/${def.file}`;
      const img = await loadImage(url);
      let canvas: HTMLCanvasElement;
      let generated = true;
      if (img) {
        canvas = def.chroma ? keyAndTrim(img, def.crop) : toCanvas(img);
      } else {
        generated = false;
        out.fallbacks.push(def.file);
        const size = def.key.startsWith('bg') ? [1024, 1536] : [256, 256];
        canvas = makeCanvas(size[0], size[1]);
        def.fallback(canvas.getContext('2d')!, size[0], size[1]);
      }
      (out[def.key] as Sprite) = { canvas, generated };
      onProgress?.(++done, DEFS.length);
    }),
  );
  if (out.fallbacks.length) {
    console.warn(
      `[assets] PROCGEN FALLBACK in use for: ${out.fallbacks.join(', ')} — run \`npm run gen:sprites\` to generate real art.`,
    );
  }
  return out;
}

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 toCanvas(img: HTMLImageElement): HTMLCanvasElement {
  const c = makeCanvas(img.naturalWidth, img.naturalHeight);
  c.getContext('2d')!.drawImage(img, 0, 0);
  return c;
}

/** Remove the magenta key, despill fringes, trim transparent margins. */
function keyAndTrim(img: HTMLImageElement, crop?: SpriteDef['crop']): HTMLCanvasElement {
  const sx = crop ? Math.floor(img.naturalWidth * crop.x) : 0;
  const sy = crop ? Math.floor(img.naturalHeight * crop.y) : 0;
  const sw = crop ? Math.floor(img.naturalWidth * crop.w) : img.naturalWidth;
  const sh = crop ? Math.floor(img.naturalHeight * crop.h) : img.naturalHeight;

  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);
    }
  }
  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 c; // fully transparent? keep as-is
  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 trimmed;
}

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

function fbPlayer(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#3a5a8c';
  ctx.beginPath();
  ctx.roundRect(w * 0.3, h * 0.25, w * 0.4, h * 0.6, 24);
  ctx.fill();
  ctx.fillStyle = '#ffd98a';
  ctx.fillRect(w * 0.38, h * 0.34, w * 0.24, h * 0.1);
}

function fbWisp(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  const g = ctx.createRadialGradient(w / 2, h / 2, 8, w / 2, h / 2, w * 0.3);
  g.addColorStop(0, 'rgba(190,235,255,0.95)');
  g.addColorStop(1, 'rgba(110,170,220,0.15)');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(w / 2, h / 2, w * 0.3, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#0c2030';
  ctx.beginPath();
  ctx.arc(w * 0.46, h * 0.48, 12, 0, Math.PI * 2);
  ctx.fill();
}

function fbScuttler(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#6a5a3a';
  ctx.beginPath();
  ctx.ellipse(w / 2, h * 0.6, w * 0.3, h * 0.22, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#9adcff';
  ctx.beginPath();
  ctx.arc(w * 0.4, h * 0.52, 10, 0, Math.PI * 2);
  ctx.fill();
}

function fbBoss(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.strokeStyle = '#b89a5a';
  ctx.lineWidth = 14;
  ctx.beginPath();
  ctx.arc(w / 2, h / 2, w * 0.36, 0, Math.PI * 2);
  ctx.stroke();
  const g = ctx.createRadialGradient(w / 2, h / 2, 10, w / 2, h / 2, w * 0.24);
  g.addColorStop(0, '#eaf6ff');
  g.addColorStop(1, '#3a6a9c');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(w / 2, h / 2, w * 0.24, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#1a0c10';
  ctx.beginPath();
  ctx.arc(w / 2, h / 2, 16, 0, Math.PI * 2);
  ctx.fill();
}

function fbPickup(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#ffd166';
  ctx.beginPath();
  ctx.moveTo(w / 2, h * 0.2);
  ctx.lineTo(w * 0.7, h * 0.5);
  ctx.lineTo(w / 2, h * 0.8);
  ctx.lineTo(w * 0.3, h * 0.5);
  ctx.closePath();
  ctx.fill();
}

function fbPlatform(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#2e3a52';
  ctx.fillRect(0, h * 0.3, w, h * 0.4);
  ctx.fillStyle = '#7a8aa8';
  ctx.fillRect(0, h * 0.3, w, h * 0.08);
}

function fbGround(ctx: CanvasRenderingContext2D, w: number, h: number): void {
  ctx.fillStyle = '#1e2638';
  ctx.fillRect(0, 0, w, h);
  ctx.fillStyle = '#54688c';
  ctx.fillRect(0, 0, w, h * 0.1);
}

function fbBg(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number) {
  return (ctx: CanvasRenderingContext2D, w: number, h: number) => {
    const grad = ctx.createLinearGradient(0, 0, 0, h);
    grad.addColorStop(0, `rgb(${r1},${g1},${b1})`);
    grad.addColorStop(1, `rgb(${r2},${g2},${b2})`);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w, h);
    // a few procedural stars so the fallback still feels like Aphelion
    for (let i = 0; i < 90; i++) {
      const x = ((i * 137.5) % w);
      const y = ((i * 89.7) % h);
      ctx.fillStyle = `rgba(220,235,255,${0.15 + ((i * 7) % 10) * 0.05})`;
      ctx.fillRect(x, y, 2, 2);
    }
  };
}
