/**
 * Troika-Text-Wrapper für alle Diagramm-Beschriftungen.
 *
 * Architektur:
 * - Troika wird einmalig per dynamischem `import(url)` geladen (URL aus `window.__CE_PD_TROIKA__.textModule`).
 * - Schriften: Barlow Bold (Default) + Barlow Light (für sekundären Text wie Annotation-Description).
 * - Pro Aufruf entsteht ein `Text`-Mesh mit gemeinsamer Default-Konfiguration (transparent, depthWrite=false,
 *   layers=1, side=DoubleSide). Der SDF-Atlas wird intern von Troika seitenweit geteilt.
 *
 * Verhalten:
 * - `text.sync()` läuft asynchron im Worker; das Mesh ist sofort in der Scene, die Geometrie wird nach
 *   Sync-Callback befüllt. Aufrufer kann `mesh.userData._onSized` setzen, um nach erstem Sync die Pose
 *   neu zu berechnen (Plane-Breite ist erst nach Sync exakt bekannt).
 * - Update von Text/Farbe/Größe via `updateTroikaLabel(...)` ist synchron für Properties, die keine neue
 *   Glyph-Layout brauchen (z.B. `color`); für Text-/Größenänderungen wird automatisch ein neuer Sync
 *   getriggert.
 *
 * Atlas-Disposal:
 * - Pro Mesh reicht `mesh.dispose()` (Troika-Methode) — gibt nur die meshlokalen Ressourcen frei.
 * - Der SDF-Atlas ist absichtlich seitenweit gecached und wird nicht pro Diagramm verworfen.
 */

let _troikaModulePromise = null;
let _troikaModule = null;
let _troikaConfig = null;
let _initialized = false;

/* Globaler Sync-Counter: jeder createTroikaLabel/updateTroikaLabel-Aufruf,
 * der `mesh.sync(cb)` triggert, erhoeht ihn; der cb dekrementiert wieder.
 * Wird vom Label-Idle-Register (onAllLabelsSynced) abgefragt, um Snapshots/Hibernate-Pfade
 * solange zu verzoegern, bis alle Troika-Worker-Tasks fertig sind. */
let _pendingSyncs = 0;
/** @type {{ fire:() => void, fired:boolean, deadline:number|null }[]} */
let _labelsIdleSubscribers = [];
/** Ein koaleszierter Safety-Timer fuer alle Subscriber (statt N × setTimeout). */
let _labelsIdleSafetyTimerId = null;

function _clearLabelsIdleSafetyTimer() {
  if (_labelsIdleSafetyTimerId != null && typeof clearTimeout === 'function') {
    try { clearTimeout(_labelsIdleSafetyTimerId); } catch (_) { /* ignore */ }
  }
  _labelsIdleSafetyTimerId = null;
}

function _armLabelsIdleSafetyTimer() {
  _clearLabelsIdleSafetyTimer();
  if (!_labelsIdleSubscribers.length) return;
  const now = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
  let nextDue = null;
  for (let i = 0; i < _labelsIdleSubscribers.length; i += 1) {
    const s = _labelsIdleSubscribers[i];
    if (s.fired || s.deadline == null) continue;
    if (nextDue == null || s.deadline < nextDue) nextDue = s.deadline;
  }
  if (nextDue == null) return;
  const delay = Math.max(0, nextDue - now);
  if (typeof setTimeout !== 'function') return;
  _labelsIdleSafetyTimerId = setTimeout(_onLabelsIdleSafetyTick, delay);
}

function _onLabelsIdleSafetyTick() {
  _labelsIdleSafetyTimerId = null;
  const now = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
  for (let i = _labelsIdleSubscribers.length - 1; i >= 0; i -= 1) {
    const s = _labelsIdleSubscribers[i];
    if (s.fired) {
      _labelsIdleSubscribers.splice(i, 1);
      continue;
    }
    if (s.deadline != null && now >= s.deadline) {
      try { s.fire(); } catch (_) { /* ignore */ }
      _labelsIdleSubscribers.splice(i, 1);
    }
  }
  _armLabelsIdleSafetyTimer();
}

function _flushLabelsIdleAfterSyncDrain() {
  _clearLabelsIdleSafetyTimer();
  if (!_labelsIdleSubscribers.length) return;
  const batch = _labelsIdleSubscribers;
  _labelsIdleSubscribers = [];
  for (let i = 0; i < batch.length; i += 1) {
    try { batch[i].fire(); } catch (_) { /* ignore */ }
  }
}

/* Debug-Tracking: alle aktiven Troika-Meshes, damit getTroikaDebugStats() ohne
 * Scene-Traversal die Anzahl ungesyncter Labels liefern kann. WeakSet reicht
 * nicht, weil wir iterieren wollen — Set + manuelles Entfernen via dispose. */
const _allLabels = new Set();

function _incPending() {
  _pendingSyncs += 1;
}

function _decPending() {
  _pendingSyncs = Math.max(0, _pendingSyncs - 1);
  if (_pendingSyncs === 0) {
    _flushLabelsIdleAfterSyncDrain();
  }
}

/**
 * Debug-Snapshot des Troika-Zustands. Nur fuer Diagnose gedacht — kein Hot Path.
 *
 * @returns {{ pendingSyncs:number, totalLabels:number, syncedLabels:number, unsyncedLabels:number }}
 */
export function getTroikaDebugStats() {
  let synced = 0;
  let total = 0;
  _allLabels.forEach((mesh) => {
    total += 1;
    if (mesh && mesh.textRenderInfo) synced += 1;
  });
  return {
    pendingSyncs: _pendingSyncs,
    totalLabels: total,
    syncedLabels: synced,
    unsyncedLabels: total - synced,
  };
}

/**
 * Registriert `cb` im gemeinsamen Label-Idle-Register: Aufruf, sobald alle pendenten
 * Troika-Sync-Tasks fertig sind. Keine Tasks: `cb` naechster Microtask.
 * `timeoutMs` > 0: Spaetestens nach dieser Zeit (seit Registrierung) wird `cb` trotzdem
 * einmal ausgefuehrt; `0` = nur Idle, kein Safety-Timeout.
 * Safety-Timeouts mehrerer Listener werden zu einem Timer verschachtelt (naechste faellige Deadline).
 *
 * @param {() => void} cb
 * @param {number} [timeoutMs=2000]
 */
export function onAllLabelsSynced(cb, timeoutMs = 2000) {
  if (typeof cb !== 'function') return;
  if (_pendingSyncs === 0) {
    Promise.resolve().then(cb);
    return;
  }
  const sub = {
    fired: false,
    deadline: timeoutMs > 0
      ? (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()) + timeoutMs
      : null,
    fire() {
      if (this.fired) return;
      this.fired = true;
      cb();
    },
  };
  _labelsIdleSubscribers.push(sub);
  _armLabelsIdleSafetyTimer();
}

/**
 * Lädt das Troika-Text-Modul einmalig dynamisch.
 * Idempotent: Mehrfachaufrufe geben dieselbe Promise zurück.
 *
 * @returns {Promise<{ Text:any, configureTextBuilder:any, preloadFont:any, fontBoldUrl:string, fontLightUrl:string, fontExtraBoldUrl:string }>}
 */
export function loadTroika() {
  if (_troikaModulePromise) return _troikaModulePromise;
  const w = typeof window !== 'undefined' ? window : null;
  const cfg = w && w.__CE_PD_TROIKA__;
  if (!cfg || !cfg.textModule || !cfg.fontBoldUrl) {
    _troikaModulePromise = Promise.reject(new Error('Pop3D Charts: window.__CE_PD_TROIKA__ fehlt (textModule/fontBoldUrl).'));
    return _troikaModulePromise;
  }
  _troikaConfig = {
    fontBoldUrl: cfg.fontBoldUrl,
    fontLightUrl: cfg.fontLightUrl || cfg.fontBoldUrl,
    fontExtraBoldUrl: cfg.fontExtraBoldUrl || cfg.fontBoldUrl,
  };
  _troikaModulePromise = import(cfg.textModule).then((mod) => {
    const { Text, configureTextBuilder, preloadFont } = mod;
    if (!_initialized) {
      _initialized = true;
      try {
        configureTextBuilder({ defaultFontURL: _troikaConfig.fontBoldUrl });
      } catch (_) { /* ignore */ }
      try {
        preloadFont({ font: _troikaConfig.fontBoldUrl }, () => {});
      } catch (_) { /* ignore */ }
      if (_troikaConfig.fontLightUrl !== _troikaConfig.fontBoldUrl) {
        try { preloadFont({ font: _troikaConfig.fontLightUrl }, () => {}); } catch (_) { /* ignore */ }
      }
      if (
        _troikaConfig.fontExtraBoldUrl
        && _troikaConfig.fontExtraBoldUrl !== _troikaConfig.fontBoldUrl
        && _troikaConfig.fontExtraBoldUrl !== _troikaConfig.fontLightUrl
      ) {
        try { preloadFont({ font: _troikaConfig.fontExtraBoldUrl }, () => {}); } catch (_) { /* ignore */ }
      }
    }
    _troikaModule = { Text, configureTextBuilder, preloadFont, ..._troikaConfig };
    return _troikaModule;
  });
  return _troikaModulePromise;
}

/**
 * Synchroner Zugriff auf das geladene Troika-Modul. Wirft, wenn `loadTroika()` noch nicht
 * aufgelöst ist — Aufrufer müssen vorher awaiten (typischerweise einmal in init.js).
 */
export function getTroika() {
  if (!_troikaModule) {
    throw new Error('Pop3D Charts: getTroika() vor loadTroika() aufgerufen.');
  }
  return _troikaModule;
}

/**
 * Sicherer Hex-Farbcheck: nimmt '#abc'/'abc'/'#aabbcc'/'aabbcc' und liefert immer ein '#'-Prefix.
 * @param {string} hex
 * @returns {string}
 */
function normalizeHex(hex) {
  const s = String(hex || '').trim();
  if (!s) return '#000000';
  return s.startsWith('#') ? s : '#' + s;
}

/**
 * Führungslinien (Pie/Pyramide/Wave): Tiefenpuffer respektieren — nicht durch Volumen „durchscheinen“.
 */
export function createOverlayLineMaterial(THREE, colorHex, opacity = 0) {
  return new THREE.MeshBasicMaterial({
    color: colorHex,
    transparent: true,
    opacity,
    depthTest: true,
    depthWrite: false,
  });
}

/**
 * Erzeugt ein neues Troika-Text-Mesh mit Plugin-Defaults.
 *
 * @param {object} args
 * @param {any}     args.THREE
 * @param {string}  args.text
 * @param {string}  args.color           Hex-Farbe (mit oder ohne '#').
 * @param {number}  args.fontSize        World-Units (z.B. 0.16).
 * @param {'left'|'center'|'right'|number|string} [args.anchorX='center']
 * @param {'top'|'middle'|'bottom'|'top-baseline'|'top-cap'|'top-ex'|'bottom-baseline'|number|string} [args.anchorY='middle']
 * @param {'bold'|'light'} [args.weight='bold'] Wählt die TTF-URL (Barlow Bold vs Light), wenn fontUrl fehlt.
 * @param {string} [args.fontUrl]          Explizite TTF-URL (schlaegt weight).
 * @param {number} [args.maxWidth]            Auto-Wrap (World-Units), Default: kein Wrap.
 * @param {'left'|'center'|'right'|'justify'} [args.textAlign='center']
 * @param {number} [args.renderOrder=1000]
 * @param {boolean}[args.depthTest=true]
 * @param {boolean}[args.depthWrite=false]
 * @param {boolean}[args.toneMapped=false]
 * @param {any}    [args.side]                THREE.DoubleSide etc.; Default DoubleSide.
 * @param {boolean}[args.transparent=true]
 * @param {number} [args.outlineWidth=0]
 * @param {string} [args.outlineColor]
 * @param {function(any):void} [args.onSync]  Callback nach Erst-Sync (Geometrie/Bounds verfügbar).
 * @returns {any} Troika `Text`-Instanz (extends THREE.Mesh).
 */
export function createTroikaLabel(args) {
  const {
    THREE,
    text,
    color = '#000000',
    fontSize,
    anchorX = 'center',
    anchorY = 'middle',
    weight = 'bold',
    maxWidth,
    textAlign = 'center',
    renderOrder = 1000,
    depthTest = true,
    depthWrite = false,
    toneMapped = false,
    side,
    transparent = true,
    outlineWidth = 0,
    outlineColor,
    outlineOpacity,
    onSync,
    fontUrl: fontUrlArg,
  } = args;
  const { Text, fontBoldUrl, fontLightUrl } = getTroika();
  const fontUrl = (typeof fontUrlArg === 'string' && fontUrlArg.trim() !== '')
    ? fontUrlArg.trim()
    : (weight === 'light' ? fontLightUrl : fontBoldUrl);

  const mesh = new Text();
  mesh.text = String(text == null ? '' : text);
  mesh.font = fontUrl;
  mesh.fontSize = Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 0.1;
  mesh.color = normalizeHex(color);
  mesh.anchorX = anchorX;
  mesh.anchorY = anchorY;
  mesh.textAlign = textAlign;
  if (Number.isFinite(maxWidth) && maxWidth > 0) mesh.maxWidth = maxWidth;
  if (outlineWidth) mesh.outlineWidth = outlineWidth;
  if (outlineColor != null) mesh.outlineColor = normalizeHex(outlineColor);
  if (Number.isFinite(outlineOpacity)) mesh.outlineOpacity = outlineOpacity;

  /* Materialeigenschaften: Troika erzeugt intern ein DerivedMaterial. Wir setzen das Basis-Material,
   * Troika übernimmt die relevanten Properties. side/transparent/depthWrite werden vom DerivedMaterial
   * propagiert. */
  const baseMat = new THREE.MeshBasicMaterial({
    transparent,
    depthWrite,
    depthTest,
    toneMapped,
    side: side || THREE.DoubleSide,
  });
  mesh.material = baseMat;

  mesh.renderOrder = renderOrder;
  mesh.layers.set(1);
  mesh.frustumCulled = true;

  if (typeof onSync === 'function') {
    mesh.userData._onSized = onSync;
  }
  _allLabels.add(mesh);
  _incPending();
  mesh.sync(() => {
    try {
      const cb = mesh.userData && mesh.userData._onSized;
      if (typeof cb === 'function') cb(mesh);
    } finally {
      _decPending();
    }
  });

  return mesh;
}

/**
 * Aktualisiert ein bestehendes Troika-Label.
 *
 * @param {any} mesh
 * @param {object} patch
 * @param {string} [patch.text]
 * @param {string} [patch.color]
 * @param {number} [patch.fontSize]
 * @param {string} [patch.fontUrl] Explizite TTF-URL (schlaegt patch.weight)
 * @param {'bold'|'light'} [patch.weight]
 * @param {number} [patch.maxWidth]
 * @param {'left'|'center'|'right'|number|string} [patch.anchorX]
 * @param {'top'|'middle'|'bottom'|number|string} [patch.anchorY]
 */
export function updateTroikaLabel(mesh, patch) {
  if (!mesh || !patch) return;
  let needsSync = false;
  if (patch.text != null && String(patch.text) !== mesh.text) {
    mesh.text = String(patch.text);
    needsSync = true;
  }
  if (patch.color != null) {
    mesh.color = normalizeHex(patch.color);
  }
  if (Number.isFinite(patch.fontSize) && patch.fontSize > 0 && patch.fontSize !== mesh.fontSize) {
    mesh.fontSize = patch.fontSize;
    needsSync = true;
  }
  if (patch.fontUrl != null && String(patch.fontUrl) !== String(mesh.font)) {
    mesh.font = String(patch.fontUrl);
    needsSync = true;
  } else if (patch.weight) {
    const cfg = _troikaConfig;
    if (cfg) {
      const url = patch.weight === 'light' ? cfg.fontLightUrl : cfg.fontBoldUrl;
      if (url !== mesh.font) {
        mesh.font = url;
        needsSync = true;
      }
    }
  }
  if (Number.isFinite(patch.maxWidth) && patch.maxWidth !== mesh.maxWidth) {
    mesh.maxWidth = patch.maxWidth;
    needsSync = true;
  }
  if (patch.anchorX != null && patch.anchorX !== mesh.anchorX) {
    mesh.anchorX = patch.anchorX;
    needsSync = true;
  }
  if (patch.anchorY != null && patch.anchorY !== mesh.anchorY) {
    mesh.anchorY = patch.anchorY;
    needsSync = true;
  }
  if (needsSync) {
    _incPending();
    mesh.sync(() => {
      try {
        const cb = mesh.userData && mesh.userData._onSized;
        if (typeof cb === 'function') cb(mesh);
      } finally {
        _decPending();
      }
    });
  }
}

/**
 * Liefert nach dem ersten Sync die Block-Bounds des Texts (World-Units, lokal zur Mesh-Origin).
 * Vor dem Sync: konservative Schätzung über Zeichenanzahl × fontSize × 0.55.
 *
 * @param {any} mesh
 * @returns {{ width:number, height:number }}
 */
export function getLabelBounds(mesh) {
  if (!mesh) return { width: 0, height: 0 };
  const info = mesh.textRenderInfo;
  if (info && Array.isArray(info.blockBounds)) {
    const [minX, minY, maxX, maxY] = info.blockBounds;
    return { width: maxX - minX, height: maxY - minY };
  }
  /* Pre-Sync-Schätzung: Barlow Semi Condensed ist schmal (≈ 0.5 × fontSize pro Glyph). */
  const fs = mesh.fontSize || 0.1;
  const len = String(mesh.text || '').length || 1;
  return { width: len * fs * 0.5, height: fs * 1.15 };
}

/**
 * Disposed ein Troika-Label-Mesh (Geometrie + Material). Atlas bleibt seitenweit gecached.
 *
 * @param {any} mesh
 */
export function disposeTroikaLabel(mesh) {
  if (!mesh) return;
  _allLabels.delete(mesh);
  if (typeof mesh.dispose === 'function') {
    try { mesh.dispose(); } catch (_) { /* ignore */ }
  }
}
