/**
 * Re-Synchronisierung von Label-Meshes auf ihre zugehörigen Bar-Wrapper /
 * Add-on-Layout-Node-Instanzen.
 *
 * Wird aufgerufen:
 *  - einmalig in setupRestState (Initial-Pose)
 *  - pro Frame im animate-Loop (nach Bar-Ziel-Pose-Update)
 *  - bei Live-Updates aus dem Gutenberg-Editor (RulerColorObserver)
 *
 * Generisch: Bar-Top-Label-Layouts teilen sich `syncBarLabelsGeneric`.
 * Listen, die von Add-on-Layouts gefuellt werden, sind ohne Add-on leer und
 * die jeweiligen Sync-Funktionen schleifen no-op.
 */
import { applyGroupedBarHeadlineLabelPose } from './groupedBarLabels.js';

function _extLabels() {
  return (typeof window !== 'undefined' && window.__CE_PD_SCENE_EXT__ && window.__CE_PD_SCENE_EXT__.labels) || null;
}

export function createBarLabelSync({
  THREE,
  state,
  rulerGroup,
  waveGroup,
  pieGroup,
  bars,
  waveSphereInstances,
}) {
  const _sphereProxy = { position: new THREE.Vector3(), userData: {} };

  function syncBarLabelsGeneric(userDataKey, applyPose) {
    /* Falls fuer das aktive Layout keine Label-Meshes existieren, ist die Liste leer
     * → schleifen-no-op. */
    const list = rulerGroup.userData[userDataKey];
    if (!list || !list.length) return;
    list.forEach((mesh) => {
      const idx = mesh.userData._dataIndex;
      if (idx == null || !bars[idx]) return;
      const { wrapper: barWrap } = bars[idx];
      mesh.traverse((obj) => {
        if (!obj.isMesh) return;
        const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
        mats.forEach((m) => { if (m && m.opacity !== undefined) m.opacity = 1; });
      });
      applyPose(mesh, barWrap, THREE);
    });
  }

  const syncGroupedBarHeadlineLabelsFromBars = () => syncBarLabelsGeneric('_groupedBarHeadlineMeshes', applyGroupedBarHeadlineLabelPose);
  const syncStackedBarSumLabelsFromBars = () => syncBarLabelsGeneric('_stackedBarSumMeshes', applyGroupedBarHeadlineLabelPose);
  const syncStraightBarTopLabelsFromBars = () => syncBarLabelsGeneric('_straightBarTopLabelMeshes', applyGroupedBarHeadlineLabelPose);
  const syncArcBarTopLabelsFromBars = () => syncBarLabelsGeneric('_arcBarTopLabelMeshes', (mesh, wrapper) => {
    const fn = _extLabels()?.applyArcBarHeadlineLabelPose;
    if (fn) fn(mesh, wrapper, THREE);
  });

  function syncWaveNodeLabelsFromSpheres() {
    const list = waveGroup.userData._waveNodeLabelMeshes;
    /* Wenn kein Add-on das Wave-Layout aktiviert hat, ist die Liste leer. */
    if (!list || !list.length) return;
    const applyPose = _extLabels()?.applyWaveNodeLabelPose;
    if (!applyPose) return;
    list.forEach((mesh) => {
      const idx = mesh.userData._dataIndex;
      if (idx == null) return;
      const instD = waveSphereInstances[idx];
      if (!instD) return;
      mesh.traverse((obj) => {
        if (!obj.isMesh) return;
        const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
        mats.forEach((m) => { if (m && m.opacity !== undefined) m.opacity = 1; });
      });
      _sphereProxy.position.copy(instD.position);
      _sphereProxy.userData._isSeriesEnd = instD.isSeriesEnd;
      applyPose(mesh, _sphereProxy);
    });
  }

  function syncWaveAnnotations() {
    const labels = waveGroup.userData._waveAnnotationMeshes;
    const lines = waveGroup.userData._waveAnnotationLines;
    /* Ohne aktiviertes Wave-Layout sind beide Listen leer. */
    if ((!labels || !labels.length) && (!lines || !lines.length)) return;
    if (labels) {
      labels.forEach((mesh) => {
        if (mesh.userData._dataIndex == null) return;
        const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
        mats.forEach((m) => { if (m && m.opacity !== undefined) m.opacity = 1; });
      });
    }
    if (lines) {
      lines.forEach((line) => {
        if (line.userData._dataIndex == null) return;
        if (line.material && line.material.opacity !== undefined) line.material.opacity = 1;
      });
    }
  }

  function syncPieCalloutsRemainBelowFullOpacity(labels, lines) {
    const OPACITY_FULL = 0.999;
    if (labels) {
      for (let i = 0; i < labels.length; i++) {
        const mesh = labels[i];
        if (mesh.userData._dataIndex == null) continue;
        const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
        for (let j = 0; j < mats.length; j++) {
          const m = mats[j];
          if (m && m.opacity !== undefined && m.opacity < OPACITY_FULL) return true;
        }
      }
    }
    if (lines) {
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.userData._dataIndex == null) continue;
        const mat = line.material;
        if (mat && mat.opacity !== undefined && mat.opacity < OPACITY_FULL) return true;
      }
    }
    return false;
  }

  function fadeOverlayMeshesToFull(labels, lines) {
    const OPACITY_FULL = 0.999;
    if (labels) {
      labels.forEach((mesh) => {
        if (mesh.userData._dataIndex == null) return;
        const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
        mats.forEach((m) => { if (m && m.opacity !== undefined && m.opacity < OPACITY_FULL) m.opacity = 1; });
      });
    }
    if (lines) {
      lines.forEach((line) => {
        if (line.userData._dataIndex == null) return;
        const mat = line.material;
        if (mat && mat.opacity !== undefined && mat.opacity < OPACITY_FULL) mat.opacity = 1;
      });
    }
  }

  function syncPieCallouts() {
    const labels = pieGroup?.userData._pieCalloutMeshes;
    const lines = pieGroup?.userData._pieCalloutLines;
    const annLabels = state.layout === 'pyramid' ? pieGroup?.userData._pyramidAnnotationMeshes : null;
    const annLines = state.layout === 'pyramid' ? pieGroup?.userData._pyramidAnnotationLines : null;
    const hasCallouts = (labels && labels.length) || (lines && lines.length);
    const hasAnn = (annLabels && annLabels.length) || (annLines && annLines.length);
    if (!hasCallouts && !hasAnn) return;
    if (state.layout !== 'pie' && state.layout !== 'pieStairs' && state.layout !== 'pyramid') return;
    /** Sobald alle Callouts vollständig opak sind, Schleifen vermeiden (invalidiert durch Troika-LayoutDirty). */
    if (pieGroup?.userData._pieCalloutOpacityClean === true) return;

    const remainCallouts = syncPieCalloutsRemainBelowFullOpacity(labels, lines);
    const remainAnn = syncPieCalloutsRemainBelowFullOpacity(annLabels, annLines);
    if (!remainCallouts && !remainAnn) {
      pieGroup.userData._pieCalloutOpacityClean = true;
      return;
    }

    /** Callouts/Annotationen starten bei opacity 0 — nur dann schreiben. */
    fadeOverlayMeshesToFull(labels, lines);
    fadeOverlayMeshesToFull(annLabels, annLines);

    pieGroup.userData._pieCalloutOpacityClean =
      !syncPieCalloutsRemainBelowFullOpacity(labels, lines)
      && !syncPieCalloutsRemainBelowFullOpacity(annLabels, annLines);
  }

  function syncAll() {
    syncGroupedBarHeadlineLabelsFromBars();
    syncStackedBarSumLabelsFromBars();
    syncStraightBarTopLabelsFromBars();
    syncArcBarTopLabelsFromBars();
    syncWaveNodeLabelsFromSpheres();
    syncWaveAnnotations();
    syncPieCallouts();
  }

  return {
    syncAll,
    syncGroupedBarHeadlineLabelsFromBars,
    syncStackedBarSumLabelsFromBars,
    syncStraightBarTopLabelsFromBars,
    syncArcBarTopLabelsFromBars,
    syncWaveNodeLabelsFromSpheres,
    syncWaveAnnotations,
    syncPieCallouts,
  };
}
