/**
 * Encodes the recorded screencast frames into a silent WebM video. WebCodecs
 * VideoEncoder is not exposed to service workers, so this runs in the
 * offscreen document (hosted by src/offscreen/main.ts). With cursor crop
 * options the output is a square that follows the cursor (like the GIF
 * path); without them the whole frame is downscaled to 800px wide. Returns
 * null when WebCodecs is unavailable (Firefox) so the caller can fall back
 * to the GIF path.
 */
import { Muxer, ArrayBufferTarget } from "webm-muxer";
import { cursorCropRect, type CursorCropOptions } from "./gif";
import type { VideoFrame } from "./gif";

/** VP8 needs even dimensions. */
function even(value: number): number {
  return Math.max(2, Math.floor(value / 2) * 2);
}

export async function buildVideoWebm(frames: VideoFrame[], crop?: CursorCropOptions): Promise<Blob | null> {
  if (!frames.length || typeof VideoEncoder === "undefined") return null;

  // fixed output size for the whole file; the cursor crop outputs the source
  // square at (nearly) native resolution — capped at the requested square size
  let width: number;
  let height: number;
  if (crop && crop.viewport.w > 0 && crop.viewport.h > 0) {
    const scale = frames[0].w / crop.viewport.w;
    const cropDevice = Math.min(crop.size, crop.viewport.w, crop.viewport.h) * scale;
    width = even(Math.min(cropDevice, crop.size));
    height = width;
  } else {
    // fallback: full frame capped at 800px wide
    const first = frames[0];
    const scale = Math.min(1, 800 / first.w);
    width = even(first.w * scale);
    height = even(first.h * scale);
  }

  const muxer = new Muxer({
    target: new ArrayBufferTarget(),
    video: { codec: "V_VP8", width, height, frameRate: 15 },
    firstTimestampBehavior: "offset",
  });

  // the encoder reports async failures through its error callback
  const failure: { error: Error | null } = { error: null };
  const encoder = new VideoEncoder({
    output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
    error: (error) => {
      failure.error = error instanceof Error ? error : new Error(String(error));
    },
  });
  encoder.configure({ codec: "vp8", width, height, bitrate: 1_200_000 });

  const canvas = new OffscreenCanvas(width, height);
  const context = canvas.getContext("2d");
  if (!context) throw new Error("no 2d context");

  try {
    // media timestamps start at the first frame: the muxer requires a
    // zero-based timeline (see firstTimestampBehavior above)
    const baseAt = frames[0].at;
    for (let position = 0; position < frames.length; position++) {
      const frame = frames[position];
      const nextAt = position + 1 < frames.length ? frames[position + 1].at : null;
      const timestampUs = Math.max(frame.at - baseAt, 0) * 1000;
      const durationUs = nextAt != null ? Math.max(nextAt - frame.at, 1) * 1000 : 33_000;

      const image = await createImageBitmap(new Blob([frame.jpeg.buffer as ArrayBuffer], { type: "image/jpeg" }));
      try {
        if (crop && crop.viewport.w > 0 && crop.viewport.h > 0) {
          const { sx, sy, s } = cursorCropRect(frame, crop);
          // letterbox fill guards against rounding gaps on the edge px
          context.fillStyle = "#10121c";
          context.fillRect(0, 0, width, height);
          context.drawImage(image, sx, sy, s, s, 0, 0, width, height);
        } else {
          context.drawImage(image, 0, 0, width, height);
        }
      } finally {
        image.close();
      }
      const videoFrame = new VideoFrame(canvas, { timestamp: timestampUs, duration: durationUs });
      encoder.encode(videoFrame, { keyFrame: position % 30 === 0 });
      videoFrame.close();
      if (failure.error) throw failure.error;
    }
    if (encoder.state === "configured") await encoder.flush();
  } finally {
    encoder.close();
  }

  if (failure.error) throw failure.error;
  muxer.finalize();
  const { buffer } = muxer.target as ArrayBufferTarget;
  return new Blob([buffer], { type: "video/webm" });
}