// Video decode pipeline using WebCodecs VideoDecoder.
//
// Wire format per codec (all from the host's hwcodec/vpx encoders):
//   - VP9 / VP8 / AV1: raw bitstream per frame, no container framing.
//   - H.264 / H.265:   Annex-B (start-code-prefixed NAL units) with inline
//                      SPS/PPS at every IDR. WebCodecs accepts this mode
//                      when `description` is omitted from the configure().
//
// Codec string for H.264 must encode the actual profile/level the stream
// uses (e.g. `avc1.640028` for high@4.0). Hardcoding `avc1.42E01E` (the
// baseline default) means the browser refuses streams encoded by nvenc
// or qsv at high profile. We parse the SPS on the first keyframe and
// build the right string from profile_idc/constraint_set_flags/level_idc.

import { hbb } from "../proto/generated.js";
import { deriveH264CodecString } from "./bitstream.js";

export type FrameSink = (frame: VideoFrame) => void;

export class VideoPipeline {
  private decoder: VideoDecoder | null = null;
  private currentCodec = "";
  private onFrame: FrameSink;
  // Diagnostic counters — log per-second so we can see whether a low FPS
  // is host-encode-bound (low frames-in) or browser-decode-bound (high
  // frames-in, low frames-out). Cleared each tick.
  private framesIn = 0;
  private framesOut = 0;
  private lastDiagAt = performance.now();

  constructor(onFrame: FrameSink) {
    this.onFrame = onFrame;
    if (typeof VideoDecoder === "undefined") {
      const insecure = !window.isSecureContext;
      const hint = insecure
        ? "Open this page via http://localhost or https:// — WebCodecs is gated to secure contexts and the LAN IP doesn't qualify."
        : "Update to Chrome 94+, Firefox 130+, or Safari 16.4+.";
      throw new Error(`WebCodecs VideoDecoder unavailable. ${hint}`);
    }
  }

  /** Snapshot of receive/decode rates (frames per second over the last
   *  second). Used by the HUD diag line. */
  diagStats(): { recvFps: number; decodeFps: number; codec: string } {
    const now = performance.now();
    const dt = (now - this.lastDiagAt) / 1000 || 1;
    const recvFps = Math.round(this.framesIn / dt);
    const decodeFps = Math.round(this.framesOut / dt);
    this.framesIn = 0;
    this.framesOut = 0;
    this.lastDiagAt = now;
    return { recvFps, decodeFps, codec: this.currentCodec };
  }

  /** Push one EncodedVideoFrames container (oneof from a VideoFrame Message). */
  pushVideoFrame(vf: hbb.IVideoFrame): void {
    const family = this.detectFamily(vf);
    if (!family) return;
    const frames =
      vf.vp9s?.frames || vf.vp8s?.frames || vf.av1s?.frames ||
      vf.h264s?.frames || vf.h265s?.frames;
    if (!frames || frames.length === 0) return;

    for (const f of frames) {
      this.handleFrame(family, f);
    }
  }

  private detectFamily(vf: hbb.IVideoFrame): "vp9" | "vp8" | "av1" | "h264" | "h265" | "" {
    if (vf.vp9s) return "vp9";
    if (vf.vp8s) return "vp8";
    if (vf.av1s) return "av1";
    if (vf.h264s) return "h264";
    if (vf.h265s) return "h265";
    return "";
  }

  /** Build the WebCodecs codec string for this frame's stream. For VP9
   *  we use level 5.0 (covers up to 4K@30) so the browser's hardware
   *  path engages — `vp09.00.10.08` (level 1.0) had been telling Chrome
   *  the stream was tiny, which can force the software fallback. */
  private codecStringFor(family: string, frameData?: Uint8Array): string | null {
    switch (family) {
      case "vp9":  return "vp09.00.50.08";
      case "vp8":  return "vp8";
      case "av1":  return "av01.0.04M.08";
      case "h264": return frameData ? deriveH264CodecString(frameData) : null;
      case "h265": return "hvc1.1.6.L93.B0";  // best-effort; SPS parsing for HEVC is more involved
      default:     return null;
    }
  }

  private handleFrame(
    family: "vp9" | "vp8" | "av1" | "h264" | "h265",
    f: hbb.IEncodedVideoFrame,
  ): void {
    if (!f.data || f.data.length === 0) return;
    const data = f.data as Uint8Array;
    const isKey = !!f.key;
    this.framesIn++;

    // For H.264 we MUST see an SPS before we can configure the decoder
    // (the codec string depends on its profile/level bytes). The host
    // sends SPS+PPS inline on every keyframe, so the first keyframe is
    // sufficient. Drop deltas that arrive before the first key.
    if (family === "h264" && !this.decoder) {
      if (!isKey) return;
      const codec = this.codecStringFor("h264", data);
      if (!codec) {
        console.warn("[rustdesk-web] H.264 keyframe missing SPS — dropping");
        return;
      }
      this.configureDecoder(codec);
    } else if (family !== "h264" && (!this.decoder || this.currentCodec !== this.codecStringFor(family))) {
      const codec = this.codecStringFor(family);
      if (!codec) return;
      this.configureDecoder(codec);
    }

    this.decode(data, isKey, f.pts);
  }

  private configureDecoder(codec: string): void {
    if (this.decoder) {
      try { this.decoder.close(); } catch { /* ignore */ }
    }
    this.currentCodec = codec;
    this.decoder = new VideoDecoder({
      output: (frame) => {
        this.framesOut++;
        this.onFrame(frame);
      },
      error: (e) => {
        console.error("[rustdesk-web] VideoDecoder error:", e);
      },
    });
    // optimizeForLatency hints the decoder to skip frame reordering
    // buffers — important for screen sharing where each frame matters
    // and we don't have B-frames to reorder anyway.
    this.decoder.configure({
      codec,
      optimizeForLatency: true,
    });
  }

  private decode(data: Uint8Array, isKey: boolean, ptsField: number | Long | null | undefined): void {
    if (!this.decoder) return;
    const pts = typeof ptsField === "number" ? ptsField : Number(ptsField || 0);
    try {
      const chunk = new EncodedVideoChunk({
        type: isKey ? "key" : "delta",
        timestamp: pts * 1000, // ms → microseconds
        data,
      });
      this.decoder.decode(chunk);
    } catch (e) {
      console.error("[rustdesk-web] decode chunk failed:", e);
    }
  }

  close(): void {
    if (this.decoder) {
      try { this.decoder.close(); } catch { /* ignore */ }
      this.decoder = null;
    }
  }
}

// protobufjs static-module emits `pts: number | Long | null` for int64
// fields. We don't import the Long shim, so use a structural type for the
// signature.
type Long = { low: number; high: number; unsigned: boolean; toNumber(): number };
