// AdBlock-Maxxer — Twitch live-latency reader (MAIN world, twitch.tv).
//
// Twitch computes the broadcaster→viewer delay internally; the only real source is the
// player instance's getLiveLatency(). That lives behind React internals, so we run in the
// MAIN world, find the player by walking the fiber tree, and postMessage the number out to
// the isolated UI script. Fully defensive: if Twitch changes internals we just report null
// (the UI shows "—") and never touch or break the page.

(function () {
  "use strict";
  if (window.__abmTwitchLatency) return;
  window.__abmTwitchLatency = true;

  let player = null;
  let forceQ = false;

  // The isolated extras script relays the "force source quality" toggle here (this is the
  // world that holds the player instance).
  window.addEventListener("message", (e) => {
    if (e.source === window && e.data && e.data.__abmTwitch === "forceQuality") forceQ = !!e.data.on;
  });

  function fiberOf(el) {
    const k = Object.keys(el).find(
      (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
    );
    return k ? el[k] : null;
  }
  function hasLatency(o) {
    return o && typeof o.getLiveLatency === "function";
  }

  // DFS down the player container's fiber tree for the media player instance.
  function search() {
    const roots = document.querySelectorAll(
      '.persistent-player, [data-a-target="video-player"], .video-player'
    );
    for (const root of roots) {
      const start = fiberOf(root);
      if (!start) continue;
      const stack = [start];
      let n = 0;
      while (stack.length && n < 5000) {
        const f = stack.pop();
        n++;
        if (!f) continue;
        const sn = f.stateNode;
        if (hasLatency(sn)) return sn;
        if (sn && hasLatency(sn.player)) return sn.player;
        if (sn && hasLatency(sn.mediaPlayerInstance)) return sn.mediaPlayerInstance;
        const mp = f.memoizedProps;
        if (mp) {
          if (hasLatency(mp.mediaPlayerInstance)) return mp.mediaPlayerInstance;
          if (hasLatency(mp.player)) return mp.player;
        }
        if (f.child) stack.push(f.child);
        if (f.sibling) stack.push(f.sibling);
      }
    }
    return null;
  }

  // The quality API may live on a DIFFERENT instance than getLiveLatency, so search for one
  // that actually exposes setQuality. Cache it separately.
  let qPlayer = null;
  function hasQuality(o) {
    return o && typeof o.getQualities === "function" && typeof o.setQuality === "function";
  }
  function searchQuality() {
    const roots = document.querySelectorAll('.persistent-player, [data-a-target="video-player"], .video-player');
    for (const root of roots) {
      const start = fiberOf(root);
      if (!start) continue;
      const stack = [start];
      let n = 0;
      while (stack.length && n < 6000) {
        const f = stack.pop();
        n++;
        if (!f) continue;
        const sn = f.stateNode;
        if (hasQuality(sn)) return sn;
        if (sn && hasQuality(sn.player)) return sn.player;
        if (sn && hasQuality(sn.mediaPlayerInstance)) return sn.mediaPlayerInstance;
        const mp = f.memoizedProps;
        if (mp) {
          if (hasQuality(mp.mediaPlayerInstance)) return mp.mediaPlayerInstance;
          if (hasQuality(mp.player)) return mp.player;
        }
        if (f.child) stack.push(f.child);
        if (f.sibling) stack.push(f.sibling);
      }
    }
    return null;
  }

  // Keep the player pinned to its source/highest quality so Twitch's auto-quality (ABR)
  // can't quietly downgrade you on a bandwidth dip. Note: this does NOT raise the lower-res
  // ad-free backup stream shown briefly during an ad-swap — that's inherent to the blocker.
  // All in try/catch so an internals change can never break playback; it just no-ops.
  function forceSource() {
    try {
      if (!hasQuality(qPlayer)) qPlayer = searchQuality();
      if (!hasQuality(qPlayer)) return;
      const qs = qPlayer.getQualities();
      if (!qs || !qs.length) return;
      let best = qs.find((q) => q && q.group === "chunked"); // source group, if labelled that way
      if (!best) best = qs.slice().sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]; // else highest bitrate
      const cur = typeof qPlayer.getQuality === "function" ? qPlayer.getQuality() : null;
      const curGroup = cur && typeof cur === "object" ? cur.group : cur;
      if (best && best.group && curGroup !== best.group) qPlayer.setQuality(best.group);
    } catch (e) {}
  }

  function report() {
    let value = null,
      buffer = null;
    try {
      if (!hasLatency(player)) player = search();
      if (hasLatency(player)) {
        const lat = player.getLiveLatency();
        if (typeof lat === "number" && isFinite(lat)) value = lat;
        if (typeof player.getBufferDuration === "function") {
          const b = player.getBufferDuration();
          if (typeof b === "number" && isFinite(b)) buffer = b;
        }
        if (forceQ) forceSource();
      }
    } catch (e) {
      player = null;
    }
    try { window.postMessage({ __abmTwitch: "latency", value: value, buffer: buffer }, "*"); } catch (e) {}
  }

  // Light cadence — once every 2s is plenty for a latency readout and costs almost nothing.
  setInterval(report, 2000);
})();
