/**
 * Zentrale Timer-Helfer: weniger parallele `setTimeout`-Handles bei vielen Diagramm-Instanzen.
 * Keine Diagrammtyp-spezifische Logik — nur Scheduling-Mechanik.
 */

import {
  addScrollUnfreezeCallback,
  removeScrollUnfreezeCallback,
} from './rendererPool.js';

/** @param {{ wakeTimer: unknown, wakeScrollUnfreezeCb: (() => void) | null }} state */
export function clearDiagramWakeSchedule(state) {
  if (state.wakeTimer != null) {
    try {
      const w = typeof window !== 'undefined' ? window : null;
      if (w && typeof w.clearTimeout === 'function') w.clearTimeout(state.wakeTimer);
    } catch (_) { /* ignore */ }
    state.wakeTimer = null;
  }
  if (state.wakeScrollUnfreezeCb != null) {
    try { removeScrollUnfreezeCallback(state.wakeScrollUnfreezeCb); } catch (_) { /* ignore */ }
    state.wakeScrollUnfreezeCb = null;
  }
}

/** @returns {boolean} true wenn gerade ein Wake (Timer oder Scroll-Warte) aktiv ist */
export function isDiagramWakeScheduled(state) {
  return state.wakeTimer != null || state.wakeScrollUnfreezeCb != null;
}

/* ── Globaler Stuck-Recovery-Master (ein Timeout statt N × rekursiv pro Chart) ── */

const STUCK_RECOVERY_MS = 4000;
const STUCK_RECOVERY_MS_MAX = 60000;

/** @type {{ nextDue: number, pulse: () => void, token: object }[]} */
let _stuckJobs = [];
/** @type {ReturnType<typeof setTimeout> | null} */
let _stuckMasterTimer = null;

function _stuckRemove(token) {
  for (let i = _stuckJobs.length - 1; i >= 0; i -= 1) {
    if (_stuckJobs[i].token === token) _stuckJobs.splice(i, 1);
  }
  _stuckArmMaster();
}

function _stuckNextFireTime() {
  let t = Infinity;
  for (let i = 0; i < _stuckJobs.length; i += 1) {
    const n = _stuckJobs[i].nextDue;
    if (n < t) t = n;
  }
  return t;
}

function _stuckArmMaster() {
  if (typeof window === 'undefined' || typeof setTimeout !== 'function') return;
  try {
    if (_stuckMasterTimer != null) window.clearTimeout(_stuckMasterTimer);
  } catch (_) { /* ignore */ }
  _stuckMasterTimer = null;
  if (!_stuckJobs.length) return;
  const now = Date.now();
  const next = _stuckNextFireTime();
  const delay = Math.max(0, Math.min(next - now, STUCK_RECOVERY_MS_MAX + 5000));
  _stuckMasterTimer = window.setTimeout(_stuckPulse, delay);
}

function _stuckPulse() {
  _stuckMasterTimer = null;
  if (typeof window === 'undefined') return;
  const now = Date.now();
  for (let i = 0; i < _stuckJobs.length; i += 1) {
    const j = _stuckJobs[i];
    if (now >= j.nextDue) {
      try { j.pulse(); } catch (_) { /* ignore */ }
    }
  }
  _stuckArmMaster();
}

/**
 * Registriert die Stuck-Recovery-Schleife (vormals rekursives setTimeout pro Instanz).
 * @param {(scheduleNext: (delayMs: number) => void) => void} runWhenDue  — nächster Schritt via
 *   `scheduleNext(ms)` anmelden (wie früher rekursives setTimeout mit delay `ms`).
 * @param {Window | null | undefined} ownerWin
 * @returns {{ armInitial: () => void, unbind: () => void }}
 */
export function registerStuckRecoveryLoop(runWhenDue, ownerWin) {
  const win = ownerWin && typeof ownerWin.setTimeout === 'function' ? ownerWin : (typeof window !== 'undefined' ? window : null);
  const token = {};

  /** @param {number} ms */
  function scheduleNext(ms) {
    const job = _stuckJobs.find((j) => j.token === token);
    if (!job) return;
    job.nextDue = Date.now() + ms;
    _stuckArmMaster();
  }

  function pulse() {
    runWhenDue(scheduleNext);
  }

  function armInitial() {
    if (!win) return;
    if (_stuckJobs.some((j) => j.token === token)) return;
    _stuckJobs.push({
      token,
      nextDue: Date.now() + STUCK_RECOVERY_MS,
      pulse,
    });
    _stuckArmMaster();
  }

  function unbind() {
    _stuckRemove(token);
  }

  return { armInitial, unbind };
}

export { STUCK_RECOVERY_MS, STUCK_RECOVERY_MS_MAX };

/**
 * Einfaches Debounce (max. ein Timer): mehrere schnelle Anstöße → ein späterer `fn`-Lauf.
 * @param {number} ms
 * @returns {(fn: () => void) => void}
 */
export function createDebouncedInvoker(ms) {
  /** @type {ReturnType<typeof setTimeout> | null} */
  let id = null;
  return function schedule(fn) {
    if (id !== null) return;
    const w = typeof window !== 'undefined' ? window : null;
    if (!w || typeof w.setTimeout !== 'function') {
      try { fn(); } catch (_) { /* ignore */ }
      return;
    }
    id = w.setTimeout(() => {
      id = null;
      try { fn(); } catch (_) { /* ignore */ }
    }, ms);
  };
}
