/**
 * Loads a pre-rendered frame sequence out of /public.
 *
 * The JPGs are produced by scripts/prepare_frames.py, which normalises an
 * uploaded frame folder's numbering and patches its burnt-in watermark. Re-run
 * it to change a sequence, and keep the `count` below in sync with the number
 * it prints — a mismatch either 404s past the end or silently drops the tail
 * of the animation.
 */

export type FrameSet = {
  /** Public path of the folder, no trailing slash. */
  dir: string;
  count: number;
};

/** Sun → rooftop → interior → house at dusk. Drives the landing hero. */
export const HERO_FRAMES: FrameSet = { dir: "/frames", count: 300 };

/** Studio render of a panel separating into its layers. Services showcase. */
export const SERVICES_FRAMES: FrameSet = { dir: "/frames-services", count: 192 };

/** Browsers cap connections per origin anyway; a pool keeps progress smooth. */
const CONCURRENCY = 8;

export type LoadHandle = { cancelled: boolean };

export function frameSrc(set: FrameSet, index: number): string {
  return `${set.dir}/frame_${String(index + 1).padStart(4, "0")}.jpg`;
}

function loadOne(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.decoding = "async";
    img.onload = () => {
      // Warm the decode so the first drawImage of a frame does not block the
      // scroll handler mid-scrub — but never gate the pool on it. decode()
      // can stall indefinitely while the tab is backgrounded, which would
      // otherwise hang every worker and leave the section permanently loading.
      if (typeof img.decode === "function") img.decode().catch(() => {});
      resolve(img);
    };
    img.onerror = () => reject(new Error(`Failed to load ${src}`));
    img.src = src;
  });
}

/**
 * Preloads every frame in a set, reporting real progress as each one settles.
 *
 * A frame that fails to load leaves a hole rather than rejecting the whole
 * sequence — one missing JPG should not cost the visitor the animation. The
 * draw guard (`img.complete && img.naturalWidth`) skips the holes.
 */
export async function loadFrameSequence(
  set: FrameSet,
  onProgress: (progress: number) => void,
  handle: LoadHandle,
): Promise<HTMLImageElement[]> {
  const images = new Array<HTMLImageElement>(set.count);
  let settled = 0;
  let next = 0;

  async function worker() {
    while (next < set.count) {
      if (handle.cancelled) return;
      const index = next++;
      try {
        images[index] = await loadOne(frameSrc(set, index));
      } catch {
        // leave the hole; drawing skips it
      }
      settled += 1;
      if (!handle.cancelled) onProgress(settled / set.count);
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(CONCURRENCY, set.count) }, worker),
  );

  if (handle.cancelled) return [];

  if (images.filter((img) => !img).length === set.count) {
    throw new Error(`No frames could be loaded from ${set.dir}`);
  }

  return images;
}
