/*
 * sabr-proto.mjs — hand-rolled, dependency-free protobuf encoder for the
 * SABR VideoPlaybackAbrRequest. This is the reference implementation that
 * will be ported verbatim into includes/hls_worker_v2.js (no deps, no eval).
 *
 * Field numbers mirror LuanRT googlevideo's generated protos (verified, and
 * byte-diffed in sabr-encoder-test.mjs). Wire types:
 *   0 varint  | 2 length-delimited (bytes/string/nested/packed)
 *
 * Object shape (camelCase) matches googlevideo so the same object can feed
 * both encoders in the oracle test. int64/uint64 values are passed as
 * strings (or numbers); "0"/0/undefined are treated as default → omitted,
 * matching protoc-gen-ts_proto behaviour.
 */

class Writer {
  constructor() { this.b = []; }
  _varint(v) { v = BigInt(v); if (v < 0n) v += 1n << 64n; do { let byte = Number(v & 0x7fn); v >>= 7n; if (v > 0n) byte |= 0x80; this.b.push(byte); } while (v > 0n); }
  _tag(field, wire) { this._varint(field * 8 + wire); }
  int32(field, v) { if (v === undefined || v === 0) return; this._tag(field, 0); this._varint(v); }
  int64(field, v) { if (v === undefined || v === 0 || v === '0') return; this._tag(field, 0); this._varint(v); }
  bytes(field, u8) { if (!u8 || u8.length === 0) return; this._tag(field, 2); this._varint(u8.length); for (const x of u8) this.b.push(x); }
  string(field, s) { if (s === undefined || s === '') return; this.bytes(field, new TextEncoder().encode(s)); }
  message(field, sub) { const u8 = sub.finish(); this._tag(field, 2); this._varint(u8.length); for (const x of u8) this.b.push(x); } // always emit (matches .fork().join())
  packedEmpty(field) { this._tag(field, 2); this._varint(0); } // always-emitted empty packed repeated
  finish() { return Uint8Array.from(this.b); }
}

// FormatId { itag=1, lastModified=2 (uint64), xtags=3 }
function encFormatId(f) {
  const w = new Writer();
  w.int32(1, f.itag);
  w.int64(2, f.lastModified);
  w.string(3, f.xtags);
  return w;
}

// BufferedRange { formatId=1, startTimeMs=2 i64, durationMs=3 i64, startSegmentIndex=4, endSegmentIndex=5 }
function encBufferedRange(b) {
  const w = new Writer();
  if (b.formatId) w.message(1, encFormatId(b.formatId));
  w.int64(2, b.startTimeMs);
  w.int64(3, b.durationMs);
  w.int32(4, b.startSegmentIndex);
  w.int32(5, b.endSegmentIndex);
  return w;
}

// StreamerContext.ClientInfo — only the WEB-relevant string/int fields.
// deviceMake=12, deviceModel=13, clientName=16, clientVersion=17, osName=18, osVersion=19
function encClientInfo(c) {
  const w = new Writer();
  w.string(12, c.deviceMake);
  w.string(13, c.deviceModel);
  w.int32(16, c.clientName);
  w.string(17, c.clientVersion);
  w.string(18, c.osName);
  w.string(19, c.osVersion);
  return w;
}

// StreamerContext { clientInfo=1, poToken=2, ... , unsentSabrContexts=6 (always-emitted empty packed) }
function encStreamerContext(s) {
  const w = new Writer();
  if (s.clientInfo) w.message(1, encClientInfo(s.clientInfo));
  w.bytes(2, s.poToken);
  w.packedEmpty(6); // googlevideo always writes the empty packed unsentSabrContexts
  return w;
}

// ClientAbrState — minimal: clientViewportWidth=18, clientViewportHeight=19, playerTimeMs=28 (i64)
function encClientAbrState(c) {
  const w = new Writer();
  w.int32(18, c.clientViewportWidth);
  w.int32(19, c.clientViewportHeight);
  w.int64(28, c.playerTimeMs);
  return w;
}

// VideoPlaybackAbrRequest:
//  clientAbrState=1, selectedFormatIds=2, bufferedRanges=3, playerTimeMs=4 (i64),
//  videoPlaybackUstreamerConfig=5 (bytes), preferredAudioFormatIds=16,
//  preferredVideoFormatIds=17, streamerContext=19
export function encodeVideoPlaybackAbrRequest(req) {
  const w = new Writer();
  if (req.clientAbrState) w.message(1, encClientAbrState(req.clientAbrState));
  for (const f of req.selectedFormatIds || []) w.message(2, encFormatId(f));
  for (const r of req.bufferedRanges || []) w.message(3, encBufferedRange(r));
  w.int64(4, req.playerTimeMs);
  w.bytes(5, req.videoPlaybackUstreamerConfig);
  for (const f of req.preferredAudioFormatIds || []) w.message(16, encFormatId(f));
  for (const f of req.preferredVideoFormatIds || []) w.message(17, encFormatId(f));
  if (req.streamerContext) w.message(19, encStreamerContext(req.streamerContext));
  return w.finish();
}

export { Writer, encFormatId, encBufferedRange, encStreamerContext, encClientInfo, encClientAbrState };
