/**
 * Globaler WebGL-Renderer-Pool und verwandte Runtime-Primitiven.
 *
 * Enthält modul-weit geteilten Zustand, den alle Chart-Instanzen gemeinsam nutzen:
 *   - Plattform-Erkennung (Mobile / iOS)                 → Budget-Grenzen
 *   - Renderer-Zähler + Eviction-Logik                   → WebGL-Context-Budget
 *   - Scroll-Freeze-State                                → Render-Loop-Pause beim Scrollen
 *   - Wake-Stagger                                       → zeitliches Entzerren von Wake-Calls
 *
 * Wichtig: Diese Datei wurde 1:1 aus scene/init.js extrahiert. Verhalten ist unverändert.
 */

/* ── Plattform-Erkennung ──
 * iOS Safari (iPhone/iPad ohne Chrome/Firefox/Edge) ist besonders aggressiv mit Memory-Kills
 * bei mehreren parallelen WebGL-Contexts. Mobile (iOS + Android) nutzt deshalb den
 * Shared-Renderer-Pfad (genau 1 persistenter Kontext + per-Chart-Blit auf ownCanvas).
 * Desktop mit WebGL2: per-Chart-Renderer, MAX=6 parallele Kontexte (Firefox Desktop: 3).
 * Desktop ohne WebGL2 (Win10/FF147 mit blocklisted GPU): ebenfalls Shared-Renderer-Pfad,
 * weil die WebGL1-Slot-Obergrenze sonst kollabiert (siehe useSharedRenderer weiter unten). */
export const IS_MOBILE_GLOBAL = typeof window !== 'undefined' && ('ontouchstart' in window || window.innerWidth < 768);
export const IS_IOS_GLOBAL = (() => {
  if (typeof navigator === 'undefined') return false;
  const ua = navigator.userAgent || '';
  const isAppleTouchDevice = /iPad|iPhone|iPod/.test(ua) || (ua.includes('Macintosh') && 'ontouchstart' in (typeof window !== 'undefined' ? window : {}));
  const isWebkitBrowser = !/CriOS|FxiOS|EdgiOS|OPiOS/.test(ua);
  return isAppleTouchDevice && isWebkitBrowser;
})();
/* ── WebGL2-Probe + Shared-Renderer-Trigger ──
 * Auf Systemen ohne verfügbares WebGL2 (z. B. Win10/FF147 mit per Treiber-
 * Blocklist deaktiviertem WebGL2) ist die WebGL1-Slot-Obergrenze deutlich
 * niedriger als auf modernen Stacks (~6–8 statt 16+). Bei MAX=6 reicht das
 * für typische Mehrfach-Diagramm-Seiten nicht und führt zu „context lost"-
 * Kaskaden beim Scrollen. Lösung: dieselbe Strategie wie iOS — genau ein
 * geteilter, persistenter Renderer + per-Chart-Blit auf das vorhandene
 * `ownCanvas`. Die Probe läuft 1× pro Page (Cache). */
let _webGL2AvailableCache = null;
export function isWebGL2Available() {
  if (_webGL2AvailableCache !== null) return _webGL2AvailableCache;
  if (typeof document === 'undefined') { _webGL2AvailableCache = false; return false; }

  /** Firefox loggt bei `getContext('webgl2')` auf Systemen mit per Treiber-
   *  Blocklist deaktiviertem WebGL2 die Browser-eigene Warnung
   *  „Failed to create WebGL context: AllowWebgl2:false". Damit Käufer/
   *  Entwickler in der DevTools-Konsole nicht denken, das sei ein Plugin-Bug,
   *  printen wir DAVOR eine eigene Erklär-Zeile. Andere Browser
   *  (Chrome/Edge/Safari) loggen bei fehlendem WebGL2 still – dort sparen
   *  wir die Meldung. */
  const isFirefox = typeof navigator !== 'undefined' && /Firefox/.test(navigator.userAgent || '');
  if (isFirefox) {
    try {
      console.info(
        '[Pop3D Charts] Probing WebGL2 capability (once per page). '
        + 'If a browser warning like "Failed to create WebGL context: AllowWebgl2:false" '
        + 'appears immediately after this line, it is the probe result on a system where '
        + "Firefox's GPU blocklist disables WebGL2 — not a plugin error. "
        + 'Charts will automatically fall back to a single shared WebGL1 renderer and continue to work normally.'
      );
    } catch (_) { /* ignore */ }
  }

  try {
    const probe = document.createElement('canvas');
    const gl = probe.getContext('webgl2');
    _webGL2AvailableCache = !!gl;
    /* Kein WEBGL_lose_context / loseContext(): Das triggert ein echtes
     * „context lost"-Ereignis; Firefox loggt das pro Seite sichtbar und die
     * Stack-Zeile landet hier — wirkt wie ein Plugin-Fehler, obwohl nur die
     * Probe endet. Ein Mini-Probe-Canvas ohne Zeichenbefehle wird mit dem
     * Scope freigegeben; der Kontext folgt dem GC (kurzlebig). */
  } catch (_) {
    _webGL2AvailableCache = false;
  }

  if (!_webGL2AvailableCache) {
    try {
      console.info(
        '[Pop3D Charts] WebGL2 unavailable on this system — using shared WebGL1 renderer mode. '
        + 'All charts on this page render correctly. Subsequent "WebGL 1 support was deprecated" and '
        + '"Texture has been resized" messages from three.module.js are informational and expected in this mode.'
      );
    } catch (_) { /* ignore */ }
  }

  return _webGL2AvailableCache;
}

/** True, wenn der einmalige Shared-Renderer-Pfad aktiv sein soll: immer auf
 *  Mobile/iOS (RAM/Slot-Leak) und auf Desktops ohne WebGL2 (Win10/FF147). */
let _useSharedRendererCache = null;
export function useSharedRenderer() {
  if (_useSharedRendererCache !== null) return _useSharedRendererCache;
  _useSharedRendererCache = IS_MOBILE_GLOBAL || !isWebGL2Available();
  return _useSharedRendererCache;
}

/** Gecko-Desktop (Win/macOS/Linux), nicht FxiOS/Android-Firefox — für WebGL-Kontext-Cap. */
function isFirefoxDesktopWebglPool() {
  if (typeof navigator === 'undefined' || IS_MOBILE_GLOBAL) return false;
  const ua = navigator.userAgent || '';
  if (!/Firefox/i.test(ua)) return false;
  if (/FxiOS/i.test(ua)) return false;
  if (/Android/i.test(ua)) return false;
  return true;
}

/** Shared-Pfad: 1 Slot. Per-Chart-Pfad: Firefox-Desktop 3, sonst 6 (Chrome/Edge/Safari …). */
function getMaxWebglContexts() {
  if (useSharedRenderer()) return 1;
  return isFirefoxDesktopWebglPool() ? 3 : 6;
}

/* ── Debug (keine Frame-Logs; max. alle 3 s pro Key) ──
 * Steuerung über PHP-Flag `window.__CE_PD_DEBUG_WEBGL__` (siehe importmap.php). */
export function isCePdDebugWebglEnabled() {
  return typeof window !== 'undefined' && window.__CE_PD_DEBUG_WEBGL__ === true;
}
const _cePdDbgLast = new Map();
const CE_PD_DBG_THROTTLE_MS = 3000;
const _cePdDbgOnce = new Set();

/**
 * @param {string} key
 * @param {string} message
 * @param {object} [payload]
 */
export function cePdDebugWebgl(key, message, payload) {
  if (!isCePdDebugWebglEnabled()) return;
  const t = typeof performance !== 'undefined' ? performance.now() : Date.now();
  const prev = _cePdDbgLast.get(key) || 0;
  if (t - prev < CE_PD_DBG_THROTTLE_MS) return;
  _cePdDbgLast.set(key, t);
  if (payload !== undefined) console.info('[CE_PD WebGL]', message, payload);
  else console.info('[CE_PD WebGL]', message);
}

export function cePdDebugWebglOnce(key, message, payload) {
  if (!isCePdDebugWebglEnabled()) return;
  if (_cePdDbgOnce.has(key)) return;
  _cePdDbgOnce.add(key);
  if (payload !== undefined) console.info('[CE_PD WebGL]', message, payload);
  else console.info('[CE_PD WebGL]', message);
}

/** Mobile (iOS + Android): kurzes, dynamisch mit Diagramm-Anzahl skaliertes Deep-Dispose, um
 *  RAM-Spitzen auf speicherengen Geräten (z.B. Pixel 4 unter Chrome-Tab-Limit) zu vermeiden.
 *  Beim Zurückscrollen erfolgt zwar ein voller Re-Init des WebGL-Renderers, das ist UX-unsichtbar:
 *  `ownCanvas` zeigt während des Re-Inits den letzten geblitteten Snapshot weiter, der visuell
 *  nicht vom Live-Chart zu unterscheiden ist.
 *  Desktop: 120 s — kein RAM/Kontext-Druck, also länger im Hibernate (schnelleres Wake statt Re-Init).
 *  `isIos` bleibt im Signatur-Slot für API-Stabilität; intern entscheidet jetzt nur noch IS_MOBILE_GLOBAL. */
export function getDeepDisposeDelayMs(_isIos, ownerDoc) {
  if (!IS_MOBILE_GLOBAL) return 120000;
  const doc = ownerDoc && typeof ownerDoc.querySelectorAll === 'function' ? ownerDoc : (typeof document !== 'undefined' ? document : null);
  let n = 0;
  try {
    if (doc) n = doc.querySelectorAll('.ce-diagram-wrapper[data-ce-init]').length;
  } catch (_) { /* ignore */ }
  const base = 3500;
  const perExtraChart = 350;
  return Math.max(2000, base - Math.max(0, n - 1) * perExtraChart);
}

/* ── „Szene noch im RAM, Hibernate-Snapshot“ — globales Budget (nicht gleich WebGL-Slots) ── */
const _pendingDeepDispose = new Map();

/** Cap auf „hibernated, Three.js-Szene noch im JS-Heap"-Charts. Mobile (iOS + Android) nutzt
 *  denselben strengen Wert, weil Android-Chrome-Tabs bei mehreren großen Three.js-Szenen
 *  ähnlich schnell ans OS-Memory-Limit stoßen wie iOS Safari (Symptom: „Aw, Snap!"-Tab-Kill). */
const MAX_PENDING_DEEP_DISPOSE = IS_MOBILE_GLOBAL ? 4 : 12;

export function registerPendingDeepDispose(wrapper, entry) {
  if (!wrapper || !entry || typeof entry.performDeepDispose !== 'function') return;
  _pendingDeepDispose.set(wrapper, {
    at: typeof performance !== 'undefined' ? performance.now() : Date.now(),
    getRatio: typeof entry.getRatio === 'function' ? entry.getRatio : () => 0,
    performDeepDispose: entry.performDeepDispose,
  });
  enforcePendingDeepDisposeBudget();
}

export function unregisterPendingDeepDispose(wrapper) {
  if (wrapper) _pendingDeepDispose.delete(wrapper);
}

/** Sync mit `VIEWPORT_ANIM_MIN_RATIO_GLOBAL` in observers.js — zirkulärer Import vermeiden. */
const _OFFSCREEN_RATIO_LT = 0.06;

function enforcePendingDeepDisposeBudget() {
  let guard = 0;
  while (_pendingDeepDispose.size > MAX_PENDING_DEEP_DISPOSE && guard < _pendingDeepDispose.size + 4) {
    guard++;
    const prevSize = _pendingDeepDispose.size;
    const rows = [];
    for (const [w, meta] of _pendingDeepDispose) {
      let r = 0;
      try { r = Number(meta.getRatio()); } catch (_) { r = 0; }
      if (!Number.isFinite(r)) r = 0;
      rows.push({ w, meta, r });
    }
    const off = rows.filter(x => x.r < _OFFSCREEN_RATIO_LT);
    const pool = off.length ? off : rows;
    pool.sort((a, b) => (a.r !== b.r ? a.r - b.r : a.meta.at - b.meta.at));
    const pick = pool[0];
    if (!pick) break;
    try { pick.meta.performDeepDispose(); } catch (_) { /* ignore */ }
    if (_pendingDeepDispose.size >= prevSize) break;
  }
}

/* ── Aktive-Renderer-Registry ──
 * Map<Element, { hibernate, isFullscreen, isInteracting, ratio }>. Wird von der Eviction-Logik
 * genutzt, um den "am wenigsten sichtbaren" Chart auszuwählen, wenn das Budget erschöpft ist. */
const _activeRenderers = new Map();

export function registerActiveRenderer(wrapper, entry) {
  _activeRenderers.set(wrapper, entry);
}

export function unregisterActiveRenderer(wrapper) {
  _activeRenderers.delete(wrapper);
}

export function getActiveRendererEntry(wrapper) {
  return _activeRenderers.get(wrapper);
}

/* ── Renderer-Pool ──
 * Shared-Pfad (Mobile/iOS + Desktops ohne WebGL2, siehe useSharedRenderer):
 *   GENAU EIN geteilter WebGLRenderer über die gesamte Page-Lifetime. Der
 *   WebGL-Canvas hängt nie im DOM (Display läuft per-Chart über `ownCanvas`
 *   + blitToOwnCanvas). Chart-Wechsel: `setSize()` + `render(newScene,
 *   newCamera)` auf derselben Instanz.
 *
 *   Warum: iOS Safari leakt WebGL-Kontext-Slots trotz `forceContextLoss` +
 *   `dispose()`. Nach ~7-8 create/dispose-Zyklen knallt Safari mit „too
 *   many active WebGL contexts", obwohl `_rendererCount` korrekt ≤1 ist.
 *   Ein einziger persistenter Renderer eliminiert diesen Leak komplett.
 *   Win10/FF147 ohne WebGL2 zeigt funktional dasselbe Symptom — die WebGL1-
 *   Slot-Obergrenze ist niedriger als das eigene MAX=6, beim Scrollen
 *   kommen `context lost`-Kaskaden. Shared-Pfad fixt beide Fälle identisch.
 *
 *   Three.js hält pro Scene intern WebGLProperties (WeakMap). Wird ein Chart
 *   deep-disposed, feuert `geometry.dispose()`/`material.dispose()` —
 *   Three.js räumt die GPU-Ressourcen via interner Event-Listener auf dem
 *   shared Renderer. GPU-Speicher bleibt damit konstant.
 *
 * Per-Chart-Pfad (moderner Desktop mit WebGL2): weiterhin pro Chart eigener
 *   Renderer, MAX=6. Mehr parallele Animationen, kein Slot-Druck. */
let _rendererCount = 0;
let _sharedMobileRenderer = null;

export function getRendererCount() { return _rendererCount; }

/** Escape-Hatch: wird vom Context-Loss-Recovery-Pfad genutzt, wenn der Renderer bereits
 *  inline disposed wurde (Context kaputt, kein forceContextLoss mehr möglich). */
export function decrementRendererCount() { _rendererCount--; }

/**
 * Disposed einen Renderer vollständig (WebGL-Context) und dekrementiert den Zähler.
 * Im Shared-Pfad: no-op für den shared Renderer — er bleibt über die gesamte Page-Lifetime bestehen.
 */
export function releaseRenderer(r) {
  if (useSharedRenderer() && r && r === _sharedMobileRenderer) {
    cePdDebugWebgl('release-shared-noop', 'releaseRenderer: shared renderer retained', {
      count: _rendererCount,
    });
    return;
  }
  try { r.dispose(); } catch (_) { /* ignore */ }
  /** `forceContextLoss()` = WEBGL_lose_context → Firefox loggt jedes Mal „WebGL context was lost“
   *  mit Stack in three.module.js (forceContextLoss), obwohl es nur Eviction beim Scrollen ist.
   *  Nach `dispose()` sind Three.js-GL-Objekte bereits abgemeldet; ohne forceContextLoss
   *  gibt der Treiber den Kontext später frei — für Gecko-Desktop akzeptabel, Konsole bleibt sauber.
   *  Chrome/Safari/Edge: weiter forceContextLoss für schnellere Slot-Freigabe (historisch iOS-Slots). */
  try {
    if (r && r.forceContextLoss && !isFirefoxDesktopWebglPool()) r.forceContextLoss();
  } catch (_) { /* ignore */ }
  try {
    const el = r && r.domElement;
    if (el) {
      try { el.width = 1; el.height = 1; } catch (_) { /* ignore */ }
      if (el.parentNode) el.parentNode.removeChild(el);
    }
  } catch (_) { /* ignore */ }
  _rendererCount--;
  cePdDebugWebgl('release', 'releaseRenderer: disposed + forceContextLoss + shrunk canvas', {
    count: _rendererCount,
    max: getMaxWebglContexts(),
  });
}

/**
 * Evict den "least visible" Chart aus der Registry, um ein WebGL-Context-Slot frei zu machen.
 * @param {boolean} [forAcquireBudget]  true: für acquireRenderer — auch „interacting“ Charts dürfen Opfer werden,
 *   sonst bleibt bei Touch oft niemand übrig und iOS sprengt das globale WebGL-Limit.
 *   Fullscreen bleibt geschützt (außer es gibt keinen anderen Kandidaten).
 */
function _evictLeastVisible(excludeWrapper, forAcquireBudget) {
  let victim = null;
  function pickVictim(allowInteract, allowFullscreen) {
    let v = null;
    let low = Infinity;
    for (const [w, entry] of _activeRenderers) {
      if (w === excludeWrapper) continue;
      if (!allowFullscreen && typeof entry.isFullscreen === 'function' && entry.isFullscreen()) continue;
      if (!allowInteract && typeof entry.isInteracting === 'function' && entry.isInteracting()) continue;
      const r = Number(entry.ratio);
      const eff = Number.isFinite(r) ? r : 0;
      if (eff < low) { low = eff; v = w; }
    }
    return v;
  }
  if (forAcquireBudget) {
    victim = pickVictim(true, false);
    if (!victim) victim = pickVictim(true, true);
  } else {
    victim = pickVictim(false, false);
  }
  if (victim) {
    const entry = _activeRenderers.get(victim);
    if (entry && typeof entry.hibernate === 'function') entry.hibernate(true);
    /** `hibernate` ruft `unregisterActiveRenderer(wrapper)` selbst auf; hier nur Safety-Net. */
    _activeRenderers.delete(victim);
  }
}

/**
 * Fordert einen Renderer an.
 * Shared-Pfad (siehe useSharedRenderer): gibt den einmalig erstellten shared Renderer
 *   zurück (nie disposed, kein Eviction nötig).
 * Per-Chart-Pfad: bisherige Logik mit Eviction.
 * @param {() => THREE.WebGLRenderer} createRendererFn  Factory (im Shared-Pfad nur 1× aufgerufen).
 * @param {Element} excludeWrapper  Der aufrufende Chart wird nie als Eviction-Opfer gewählt.
 * @returns {THREE.WebGLRenderer|null}  null, wenn Budget auch nach Eviction voll bleibt
 *   (nur im Per-Chart-Pfad möglich).
 */
/** True, wenn der shared Renderer existiert, aber sein WebGL-Kontext verloren ist
 *  (iOS Safari: Rotation/Background/Memory-Druck; Win10/FF147: Treiber-Reset). */
function _isSharedMobileRendererLost() {
  if (!_sharedMobileRenderer) return false;
  try {
    const gl = _sharedMobileRenderer.getContext();
    return !!(gl && typeof gl.isContextLost === 'function' && gl.isContextLost());
  } catch (_) {
    return true;
  }
}

/** Shared Renderer hart zurücksetzen — genutzt bei Context-Loss. Kein forceContextLoss,
 *  der Kontext IST ja bereits verloren. Ressourcen-Dispose best-effort, dann Null-Slot. */
function _resetSharedMobileRenderer() {
  if (!_sharedMobileRenderer) return;
  try { _sharedMobileRenderer.dispose(); } catch (_) { /* ignore */ }
  _sharedMobileRenderer = null;
  _rendererCount = 0;
}

/** Vom Chart-Context-Lost-Handler: shared Renderer als verloren markieren, damit der nächste
 *  `acquireRenderer`-Call einen frischen erstellt, ohne auf den 3.4s-Recover-Timer zu warten. */
export function noteSharedRendererMaybeLost() {
  if (!useSharedRenderer()) return;
  if (_isSharedMobileRendererLost()) {
    _resetSharedMobileRenderer();
  }
}

export function acquireRenderer(createRendererFn, excludeWrapper) {
  if (useSharedRenderer()) {
    /** Self-heal: wenn der geteilte Kontext (z.B. durch Rotation/Backgrounding/GPU-Reset)
     *  verloren ging, hier einfach wegwerfen und neu erstellen — sonst gäbe
     *  `acquireRenderer` einen toten Renderer zurück und jedes render() wirft
     *  INVALID_OPERATION (schwarzer Bildschirm). */
    if (_isSharedMobileRendererLost()) {
      _resetSharedMobileRenderer();
    }
    if (!_sharedMobileRenderer) {
      _sharedMobileRenderer = createRendererFn();
      _rendererCount = 1;
      cePdDebugWebgl('acquire-shared-new', 'acquireRenderer: shared renderer created', {
        ios: IS_IOS_GLOBAL,
        mobile: IS_MOBILE_GLOBAL,
        webgl2: isWebGL2Available(),
      });
    } else {
      cePdDebugWebgl('acquire-shared-reuse', 'acquireRenderer: shared renderer reused', {
        ios: IS_IOS_GLOBAL,
        mobile: IS_MOBILE_GLOBAL,
      });
    }
    return _sharedMobileRenderer;
  }
  let guard = 0;
  const maxGuard = Math.max(16, _activeRenderers.size + _rendererCount + 4);
  while (_rendererCount >= getMaxWebglContexts() && guard < maxGuard) {
    guard++;
    const beforeC = _rendererCount;
    const beforeM = _activeRenderers.size;
    _evictLeastVisible(excludeWrapper, true);
    if (_rendererCount === beforeC && _activeRenderers.size === beforeM) {
      cePdDebugWebgl('acquire-stall', 'acquireRenderer: eviction made no progress', {
        count: _rendererCount,
        max: getMaxWebglContexts(),
        registry: _activeRenderers.size,
      });
      break;
    }
  }
  if (_rendererCount >= getMaxWebglContexts()) {
    cePdDebugWebgl('acquire-null', 'acquireRenderer: budget still full', {
      count: _rendererCount,
      max: getMaxWebglContexts(),
      registry: _activeRenderers.size,
    });
    return null;
  }
  const r = createRendererFn();
  _rendererCount++;
  cePdDebugWebgl('acquire-new', 'acquireRenderer: new WebGL context', {
    count: _rendererCount,
    max: getMaxWebglContexts(),
  });
  return r;
}

/* ── Globaler Scroll-Freeze: stoppt alle Render-Loops während die Seite scrollt ── */
let _scrollFrozen = false;
let _scrollFreezeTimer = null;
const _scrollUnfreezeCallbacks = new Set();

if (typeof window !== 'undefined') {
  window.addEventListener('scroll', () => {
    _scrollFrozen = true;
    if (_scrollFreezeTimer !== null) clearTimeout(_scrollFreezeTimer);
    _scrollFreezeTimer = setTimeout(() => {
      _scrollFrozen = false;
      _scrollFreezeTimer = null;
      _scrollUnfreezeCallbacks.forEach(fn => fn());
    }, 150);
  }, { passive: true });
}

export function isScrollFrozen() { return _scrollFrozen; }

export function addScrollUnfreezeCallback(cb) {
  _scrollUnfreezeCallbacks.add(cb);
}

export function removeScrollUnfreezeCallback(cb) {
  _scrollUnfreezeCallbacks.delete(cb);
}

/* ── Eine gemeinsame Kette für alles, was acquireRenderer/WebGL anfasst ──
 * Init und Wake liefen vorher auf zwei separaten Promise-Ketten → konnten sich im selben
 * Tick abwechseln und mehrere neue WebGL-Kontexte erzeugen, obwohl MAX=1 (iOS).
 * Jetzt: strikt nacheinander (init, wake, init, …). */
let _gpuSerialChain = Promise.resolve();
let _diagramInitQueued = 0;
let _wakeDepth = 0;

/**
 * Per-Chart-Pfad only: Gibt WebGL-Slots synchron frei, bevor der nächste `acquireRenderer` läuft.
 * Im Shared-Pfad no-op — der shared Renderer wird nie disposed, Eviction ist sinnlos.
 */
function _preEvictUntilBudgetFree() {
  if (useSharedRenderer()) return;
  let guard = 0;
  const maxGuard = Math.max(16, _activeRenderers.size + _rendererCount + 4);
  while (_rendererCount >= getMaxWebglContexts() && guard < maxGuard) {
    guard++;
    const beforeC = _rendererCount;
    const beforeM = _activeRenderers.size;
    _evictLeastVisible(null, true);
    if (_rendererCount === beforeC && _activeRenderers.size === beforeM) break;
  }
}

/** Mit shared Renderer entfällt die ehemalige Release→Create-Pause komplett.
 *  Variable bleibt für Telemetrie-Logs (countBefore/countAfter) erhalten. */
const GPU_SLOT_YIELD_MS = 0;
function _yieldForGpuSlot() { return Promise.resolve(); }

/**
 * Führt einen synchronen Diagramm-Aufbau (`initScene`) erst aus, wenn der vorherige GPU-Job beendet ist.
 * @param {() => void} fn
 * @returns {Promise<void>}
 */
export function enqueueSerializedDiagramInit(fn) {
  _diagramInitQueued++;
  if (_diagramInitQueued > 1) {
    cePdDebugWebgl('init-queue', 'enqueueSerializedDiagramInit: further inits queued', {
      depth: _diagramInitQueued,
    });
  }
  const next = _gpuSerialChain.then(() => {
    /** Eviction VOR der Zeitpause: `forceContextLoss` läuft synchron, Safari räumt den
     *  GPU-Slot aber erst nach einigen ms; die Pause gibt dem Treiber die benötigte Zeit. */
    const before = _rendererCount;
    _preEvictUntilBudgetFree();
    cePdDebugWebgl('init-prepare', 'enqueueSerializedDiagramInit: pre-evict + yield', {
      countBefore: before,
      countAfter: _rendererCount,
      yieldMs: GPU_SLOT_YIELD_MS,
      registry: _activeRenderers.size,
    });
    return _yieldForGpuSlot();
  }).then(() => {
    try {
      fn();
    } finally {
      _diagramInitQueued = Math.max(0, _diagramInitQueued - 1);
    }
  });
  _gpuSerialChain = next.catch(() => {});
  return next;
}

/**
 * @param {() => void} fn  synchroner Wake-Body (typ. wake() aus diagramViewportLifecycle)
 * @returns {Promise<void>}
 */
export function enqueueSerializedWake(fn) {
  _wakeDepth++;
  if (_wakeDepth > 1) {
    cePdDebugWebgl('wake-bundle', 'enqueueSerializedWake: pending wake depth', { depth: _wakeDepth });
  }
  const next = _gpuSerialChain.then(() => {
    const before = _rendererCount;
    _preEvictUntilBudgetFree();
    cePdDebugWebgl('wake-prepare', 'enqueueSerializedWake: pre-evict + yield', {
      countBefore: before,
      countAfter: _rendererCount,
      yieldMs: GPU_SLOT_YIELD_MS,
      registry: _activeRenderers.size,
    });
    return _yieldForGpuSlot();
  }).then(() => {
    try {
      fn();
    } finally {
      _wakeDepth = Math.max(0, _wakeDepth - 1);
    }
  });
  _gpuSerialChain = next.catch(() => {});
  return next;
}

/* ── Globaler Wake-Stagger ──
 * Zusätzlich zum Stagger gibt es im visibilityObserver einen Stability-Gate: der Wake
 * läuft erst, wenn der Chart mind. `runAt - now` ms ununterbrochen sichtbar war.
 * Beim hin-und-her-Scrollen auf Mobile werden so „Flicker-Wakes" eliminiert. */
/** Mobile (iOS + Android): längerer Abstand zwischen global gestaffelten Wakes → weniger GL-
 *  Erstellung pro Minute. Verhindert auf RAM-/GPU-knappen Geräten (z.B. Pixel 4) Burst-Spikes,
 *  wenn mehrere Charts gleichzeitig in den Viewport scrollen. */
const WAKE_STAGGER_MS = IS_MOBILE_GLOBAL ? 1000 : 500;
let _nextWakeAt = 0;

/**
 * Reiht einen Wake-Call in den globalen Stagger ein. Liefert den Timer-Handle zurück,
 * damit der Aufrufer ihn ggf. canceln kann.
 * @param {() => void} wakeFn
 * @returns {ReturnType<typeof setTimeout>}
 */
export function scheduleWake(wakeFn) {
  const now = performance.now();
  const runAt = Math.max(now, _nextWakeAt);
  const delay = runAt - now;
  _nextWakeAt = runAt + WAKE_STAGGER_MS;
  cePdDebugWebgl('wake-stagger', 'scheduleWake: next stagger slot', { delayMs: Math.round(delay), staggerMs: WAKE_STAGGER_MS });
  return setTimeout(wakeFn, delay);
}
