/*
 * Viewport-basiertes Hibernate/Wake:
 * - IntersectionObserver entscheidet über `state.blockVisible` (min ratio).
 * - Mobile: bei `hibernated` weckt Sichtbarkeit (`wake` mit Scroll-Stability-Gate + Wake-Stagger); unsichtbar →
 *   verzögert `hibernate()`. Desktop: kein Auto-Wake nur durch Sichtbarkeit — nur `pointerenter` (und andere
 *   explizite Nutzerpfade), um Wake/Evict-Schleifen bei vielen gleichzeitig sichtbaren Charts zu vermeiden.
 * - Optional `tapToInteractMobile`: kein Auto-Wake beim Sichtbarwerden; Snapshot nach kurzer Render-Phase;
 *   beim Verlassen des Viewports sofort Hibernate (Scroll nicht vom Chart abfangen).
 * - Pointerenter auf Container weckt (Desktop, nicht Tap-Modus); Tap-Modus: Wake per Klick/Tap.
 * - 4 s Safety-Interval (Recovery-Wake nur auf Mobile, nicht iOS; Desktop: kein Timer-Wake).
 *
 * Troika: Hibernate niemals ausloesen solange noch Label-Syncs im Worker offen sind (`pendingSyncs`),
 * sonst greifen Snapshots ohne fertige Glyphen-SDF im Atlas (mobile Tap-to-interact).
 *
 * `createVisibilityLifecycle(deps)` bindet alles und gibt `unbind()` zurück.
 */

import {
  VIEWPORT_ANIM_MIN_RATIO_GLOBAL,
  observeVisibility as _ceObserveVisibility,
  unobserveVisibility as _ceUnobserveVisibility,
} from './observers.js';
import {
  addScrollUnfreezeCallback,
  getActiveRendererEntry,
  IS_IOS_GLOBAL,
  IS_MOBILE_GLOBAL,
  isScrollFrozen,
  removeScrollUnfreezeCallback,
  scheduleWake,
} from './rendererPool.js';
import {
  clearDiagramWakeSchedule,
  isDiagramWakeScheduled,
  registerStuckRecoveryLoop,
  STUCK_RECOVERY_MS,
  STUCK_RECOVERY_MS_MAX,
} from './cePdTimerHub.js';
import { getTroikaDebugStats, onAllLabelsSynced } from './troikaLabel.js';

const DEFER_LABEL_SYNC_MS = 5000;

/** Hibernate erst nach allen fertigen Troika-Syncs (oder Timeout); sonst leere Labels im Snapshot. */
function deferHibernateUntilLabels(done) {
  const s = getTroikaDebugStats();
  if (s.pendingSyncs === 0) {
    done();
    return;
  }
  onAllLabelsSynced(done, DEFER_LABEL_SYNC_MS);
}

/** Tap-Modus: WebGL weg, sobald weniger als dieser Anteil im Viewport liegt (IO meldet oft erst spät „unsichtbar“ bei ratio 0.06). */
const TAP_INTERACT_HIBERNATE_IF_RATIO_BELOW = 0.15;

export function createVisibilityLifecycle({
  wrapper,
  ownerDoc,
  container,
  state,
  wake,
  hibernate,
  markDirty,
  hibernateDelayMs,
  /** Mobile „Tap to interact“: kein Auto-Wake beim Sichtbarwerden; Snapshot nach kurzer Render-Phase;
   *  beim Verlassen des Viewports sofort Hibernate (Scroll nicht vom Chart abfangen). */
  tapToInteractMobile = false,
  isDiagramFullscreen = () => false,
  /** Tap-Modus: idempotent `updateTapHintVisibility` (nur Stacking), wenn IO sich ändert — nicht „Pill erst beim Reinscrollen“. */
  onTapIntersectionChange = null,
  /** Tap-Modus: Während aktiver Live-Session (nach Wake bzw. Deep-Dispose-Resume) keinen Auto-Snapshot
   *  triggern — sonst würde die Pill direkt nach dem User-Tap wieder erscheinen (→ 2-Tap-Bug). */
  isTapInteractLiveSession = null,
}) {
  const VIEWPORT_ANIM_MIN_RATIO = VIEWPORT_ANIM_MIN_RATIO_GLOBAL;

  /** Ein Frame reicht für einen gültigen Composite; zwei Frames verzögerten Pill+Snapshot spürbar. */
  function scheduleSnapshotAfterNextFrame() {
    deferHibernateUntilLabels(() => {
      requestAnimationFrame(() => {
        if (state.disposed) return;
        if (!state.blockVisible || state.hibernated || state.contextLost) return;
        if (isDiagramFullscreen()) return;
        try { hibernate(); } catch (_) { /* ignore */ }
      });
    });
  }

  function tapForceHibernateIfOffscreen(ratio) {
    if (!tapToInteractMobile) return;
    if (state.hibernated || state.contextLost || isDiagramFullscreen()) return;
    /** Vollständig weg: separater `!blockVisible`-Zweig; hier: nur noch „Streifen“ sichtbar (< 15 %). */
    if (ratio > 0 && ratio < TAP_INTERACT_HIBERNATE_IF_RATIO_BELOW) {
      deferHibernateUntilLabels(() => {
        try { hibernate(true); } catch (_) { /* ignore */ }
      });
    }
  }

  const onVisibilityEntry = (e) => {
    if (!e) return;
    state.intersectionIsIntersecting = e.isIntersecting === true;
    state.intersectionRatio = typeof e.intersectionRatio === 'number' ? e.intersectionRatio : 0;
    const regEntry = getActiveRendererEntry(wrapper);
    if (regEntry) regEntry.ratio = e.intersectionRatio;
    const ratio = state.intersectionRatio;
    const ir = e.intersectionRect;
    const hasVisibleRect = !!(ir && ir.width > 0 && ir.height > 0);
    /* ≥6 % wie bisher; darunter mit nicht-leerer intersectionRect trotzdem sichtbar (Loader/animate hängen sonst). */
    state.blockVisible = e.isIntersecting === true
      && (ratio >= VIEWPORT_ANIM_MIN_RATIO || (hasVisibleRect && ratio < VIEWPORT_ANIM_MIN_RATIO));
    tapForceHibernateIfOffscreen(state.intersectionRatio);
    if (state.blockVisible) {
      if (state.hibernateTimer != null) { clearTimeout(state.hibernateTimer); state.hibernateTimer = null; }
      if (tapToInteractMobile) {
        clearDiagramWakeSchedule(state);
        const inLiveSession = typeof isTapInteractLiveSession === 'function' && isTapInteractLiveSession() === true;
        if (!state.hibernated && !state.contextLost && !inLiveSession) {
          scheduleSnapshotAfterNextFrame();
        } else if (!state.hibernated && !ownerDoc.hidden && state.animId === null) {
          markDirty();
        }
      } else if (state.hibernated && !isDiagramWakeScheduled(state) && IS_MOBILE_GLOBAL) {
        /** Nur Mobile: Viewport-Sichtbarkeit weckt (Desktop: nur `pointerenter`, sonst WebGL-Slot-
         *  Eviction vs. viele gleichzeitig sichtbare Charts → Wake/Hibernate-Schleife).
         *  Stability-Gate: Scroll aktiv → Wake erst nach globalem Scroll-Unfreeze (ein Listener). */
        const runWakeIfStable = () => {
          state.wakeTimer = null;
          if (!state.blockVisible || !state.hibernated) return;
          if (isScrollFrozen()) {
            const onScrollDone = () => {
              try { removeScrollUnfreezeCallback(onScrollDone); } catch (_) { /* ignore */ }
              state.wakeScrollUnfreezeCb = null;
              runWakeIfStable();
            };
            state.wakeScrollUnfreezeCb = onScrollDone;
            addScrollUnfreezeCallback(onScrollDone);
            return;
          }
          wake();
        };
        state.wakeTimer = scheduleWake(runWakeIfStable);
      } else if (!state.hibernated && !ownerDoc.hidden && state.animId === null) { markDirty(); }
    } else {
      clearDiagramWakeSchedule(state);
      if (tapToInteractMobile) {
        if (state.hibernateTimer != null) { clearTimeout(state.hibernateTimer); state.hibernateTimer = null; }
        if (!state.hibernated && !state.contextLost && !isDiagramFullscreen()) {
          /** `true`: wie Budget-Eviction — auch bei laufendem Touch wegscrollen → garantiert Snapshot beim Wiedereinscrollen. */
          deferHibernateUntilLabels(() => {
            try { hibernate(true); } catch (_) { /* ignore */ }
          });
        }
      } else if (!state.hibernated && !state.contextLost && state.hibernateTimer == null) {
        state.hibernateTimer = setTimeout(() => {
          state.hibernateTimer = null;
          if (!state.blockVisible && !state.hibernated && !state.contextLost) hibernate();
        }, hibernateDelayMs);
      }
    }
    if (tapToInteractMobile && typeof onTapIntersectionChange === 'function') {
      try { onTapIntersectionChange(); } catch (_) { /* ignore */ }
    }
  };

  _ceObserveVisibility(ownerDoc.defaultView, wrapper, onVisibilityEntry);

  function onContainerPointerEnterWake() {
    if (tapToInteractMobile) return;
    if (state.hibernated && state.blockVisible) {
      clearDiagramWakeSchedule(state);
      try { wake(); } catch (_) { /* ignore */ }
    }
  }
  container.addEventListener('pointerenter', onContainerPointerEnterWake, { passive: true });

  let stuckRecoveryDelay = STUCK_RECOVERY_MS;
  const winStuck = ownerDoc.defaultView;
  const stuckRecovery = registerStuckRecoveryLoop((scheduleNext) => {
    if (!winStuck) return;
    if (!state.blockVisible || state.contextLost) {
      scheduleNext(stuckRecoveryDelay);
      return;
    }
    if (!state.hibernated) {
      stuckRecoveryDelay = STUCK_RECOVERY_MS;
      scheduleNext(stuckRecoveryDelay);
      return;
    }
    /** iOS: kein Wake aus diesem Timer — Tap-Modus: nie automatisch. Desktop: nur Hover weckt
     *  (kein Timer-Wake). Mobile (nicht iOS laut Legacy): Recovery-Wake bei hibernated+sichtbar. */
    if (!IS_IOS_GLOBAL && !tapToInteractMobile && IS_MOBILE_GLOBAL) {
      try { wake(); } catch (_) { /* ignore */ }
    }
    if (state.hibernated) {
      stuckRecoveryDelay = Math.min(stuckRecoveryDelay * 2, STUCK_RECOVERY_MS_MAX);
    } else {
      stuckRecoveryDelay = STUCK_RECOVERY_MS;
    }
    scheduleNext(stuckRecoveryDelay);
  }, winStuck);
  if (winStuck) stuckRecovery.armInitial();

  function unbind() {
    stuckRecovery.unbind();
    container.removeEventListener('pointerenter', onContainerPointerEnterWake);
    _ceUnobserveVisibility(ownerDoc.defaultView, wrapper);
  }

  return { unbind };
}
