import { toInternalData, sortBars, sortBarsGrouped, MAX_BARS, i18nStr, resolveStraightBarLabels, resolveStackedBarSumAbove, resolvePieCalloutLineLength, resolvePieCalloutFontScale, resolvePieCalloutColor, resolvePieAngleBasis, resolvePieThickness, resolveStairStepHeight, resolvePyramidSliceLabelMode, resolvePyramidSegmentGap, resolvePyramidInverted, resolvePyramidLabelAutoContrast, resolvePyramidAnnotation, resolvePyramidAnnotationLineLength, resolvePyramidAnnotationFontScale, paletteIndexForDataItem, resolveDataItemColor, contrastTextColor, escapeHtml } from './shared/config.js';
import { createChartNumberFormatter, getFallbackChartNumberFormatter } from './shared/numberFormat.js';
import { normalizeCameraPreset } from './shared/cameraPresetIds.js';
import { enqueueSerializedDiagramInit } from './scene/rendererPool.js';
import { createDebouncedInvoker } from './scene/cePdTimerHub.js';
import { loadTroika, createTroikaLabel, getLabelBounds } from './scene/troikaLabel.js';
import { normalizeRulerStyle } from './scene/ruler.js';
import { patchEmissiveForInstancing, setInstancePosScale } from './scene/instanced.js';
import { enableProjectionFlatMorph } from './scene/projectionFlatMaterial.js';
import { createWave } from './scene/layouts/wave.js';

/** '#rgb' | '#rrggbb' → 0xRRGGBB int, oder null bei ungültigem Wert (für die Theme-Palette). */
function hexToInt(hex) {
  if (typeof hex !== 'string') return null;
  let s = hex.replace(/^#/, '').trim();
  if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
  return /^[0-9a-fA-F]{6}$/.test(s) ? parseInt(s, 16) : null;
}

/* Utility-Namespace fuer Add-on-Plugins: die intern verwendeten
 * Hilfsfunktionen werden ueber `window.__CE_PD_VIEW_HELPERS__` exponiert,
 * damit Add-on-Plugins sie ohne relative Imports ueber die Plugin-Grenzen
 * hinweg verwenden koennen. */
if (typeof window !== 'undefined' && !window.__CE_PD_VIEW_HELPERS__) {
  window.__CE_PD_VIEW_HELPERS__ = {
    paletteIndexForDataItem,
    resolveDataItemColor,
    contrastTextColor,
    escapeHtml,
    getFallbackChartNumberFormatter,
    createTroikaLabel,
    getLabelBounds,
    patchEmissiveForInstancing,
    setInstancePosScale,
    enableProjectionFlatMorph,
  };
}

(function () {
  'use strict';

  const _winScene = typeof window !== 'undefined' ? window : null;
  if (_winScene) {
    if (!_winScene.__CE_PD_SCENE_EXT__) {
      const _cePdSceneExtHandlers = [];
      const _cePdOptsExtenders = [];
      _winScene.__CE_PD_SCENE_EXT__ = {
        /** @param {(ctx: object) => void} fn */
        register(fn) {
          if (typeof fn === 'function') _cePdSceneExtHandlers.push(fn);
        },
        runAfterBars(ctx) {
          for (let hi = 0; hi < _cePdSceneExtHandlers.length; hi++) {
            try {
              _cePdSceneExtHandlers[hi](ctx);
            } catch (_) { /* ignore broken extensions */ }
          }
        },
        /** @param {(opts: object, config: object, layout: string) => void} fn */
        registerOptsExtender(fn) {
          if (typeof fn === 'function') _cePdOptsExtenders.push(fn);
        },
        applyOpts(opts, config, layout) {
          for (let i = 0; i < _cePdOptsExtenders.length; i++) {
            try {
              _cePdOptsExtenders[i](opts, config, layout);
            } catch (_) { /* ignore */ }
          }
        },
      };
    }
    if (!_winScene.__CE_PD_LAYOUTS__) _winScene.__CE_PD_LAYOUTS__ = {};
    _winScene.__CE_PD_LAYOUTS__.wave = { create: createWave };
    if (!_winScene.__CE_PD_PALETTES__) _winScene.__CE_PD_PALETTES__ = {};
  }

  /** Viewer: staggered init queue — only charts currently in the viewport are kept;
   *  scrolling past a chart removes it so visible charts load without unnecessary delay. */
  const CE_PD_INIT_STAGGER_MS = 500;
  const _cePdInitQueue = [];
  let _cePdInitProcessing = false;

  function _cePdProcessNext() {
    if (!_cePdInitQueue.length) { _cePdInitProcessing = false; return; }
    _cePdInitProcessing = true;
    const entry = _cePdInitQueue.shift();
    entry.run();
    const win = typeof window !== 'undefined' ? window : null;
    if (win) { win.setTimeout(_cePdProcessNext, CE_PD_INIT_STAGGER_MS); }
    else { _cePdProcessNext(); }
  }

  function _cePdEnqueue(entry) {
    _cePdInitQueue.push(entry);
    if (!_cePdInitProcessing) _cePdProcessNext();
  }

  function _cePdDequeue(entry) {
    const i = _cePdInitQueue.indexOf(entry);
    if (i >= 0) _cePdInitQueue.splice(i, 1);
  }

  let threeLoadPromise = null;

  /**
   * WICHTIG: Three.js wird AUSSCHLIESSLICH lokal aus dem Plugin-Ordner
   * vendor/three/ geladen. Es darf NIEMALS ein CDN (esm.sh, unpkg, jsdelivr,
   * …) verwendet werden – weder als Primär-Quelle noch als Fallback.
   * Die absoluten URLs werden von PHP (importmap.php) per
   * window.__CE_PD_THREE_IMPORTS__ injiziert; iOS braucht keine Bare-Specifier.
   */
  function resolveThreeImportUrls() {
    const w = typeof window !== 'undefined' ? window : null;
    const inj = w && w.__CE_PD_THREE_IMPORTS__;
    if (inj && inj.three && inj.orbit && inj.roundedBox && inj.reflector) return inj;
    throw new Error('CE Diagram: window.__CE_PD_THREE_IMPORTS__ fehlt. vendor/three/ muss lokal ausgeliefert werden – CDN ist nicht erlaubt.');
  }

  function loadThreeModules() {
    if (threeLoadPromise) return threeLoadPromise;
    const abs = resolveThreeImportUrls();
    threeLoadPromise = Promise.all([
      import(abs.three),
      import(abs.orbit),
      import(abs.roundedBox),
      import(abs.reflector),
    ]).then(([threeMod, { OrbitControls }, { RoundedBoxGeometry }, { Reflector }]) => {
      const THREE = threeMod.default ?? threeMod;
      return { THREE, OrbitControls, RoundedBoxGeometry, Reflector };
    });
    return threeLoadPromise;
  }

  function initBlock(wrapper) {
    if (wrapper._ceDiagramConfigObserver) {
      wrapper._ceDiagramConfigObserver.disconnect();
      wrapper._ceDiagramConfigObserver = null;
    }
    if (typeof wrapper._ceDiagramCleanup === 'function') {
      try { wrapper._ceDiagramCleanup(); } catch (_) { /* ignore */ }
    }
    wrapper._cePdInitGen = (wrapper._cePdInitGen || 0) + 1;
    const initGen = wrapper._cePdInitGen;

    const container = wrapper.querySelector('.pop3d-charts');
    const tooltip = wrapper.querySelector('.ce-diagram-tooltip');
    const iconEl = wrapper.querySelector('.ce-diagram-icon-wrapper');

    let config;
    try {
      const raw = wrapper.getAttribute('data-config');
      config = raw ? JSON.parse(raw) : {};
    } catch (_) {
      config = {};
    }
    if (!config || typeof config !== 'object' || Array.isArray(config)) {
      config = {};
    }
    const layout = config.defaultLayout || 'straight';
    const sortOrderEff = config.sortOrder || 'custom';
    const maxDataRows = MAX_BARS;
    /* "Override individual bar colors": die per-Balken-Farben aus den Diagramm-Daten
     * werden ignoriert, ohne sie zu loeschen. Einmal hier an der Quelle entfernt,
     * greift es fuer Szene, Legende, Datentabelle und Viewer-Sortierung gleich. */
    const ignoreBarColors = config.ignoreBarColors === true;
    const barsTagged = (config.bars || []).slice(0, maxDataRows).map((b, i) => {
      const tagged = { ...b, _ceBarIndex: i };
      if (ignoreBarColors) tagged.color = undefined;
      return tagged;
    });
    const barsSorted = layout === 'stacked' ? sortBarsGrouped(barsTagged, sortOrderEff) : sortBars(barsTagged, sortOrderEff);
    const DATA = toInternalData(barsSorted, layout);
    const viewerSortEligible = config.viewerSortEnabled === true;
    if (DATA.length === 0) return;
    const projection = config.defaultProjection || '3d';
    const straightBarSlots = resolveStraightBarLabels(config);

    const opts = {
      gridEnabled: config.gridEnabled !== false,
      gridLineColor: config.gridLineColor || '#dcdcdc',
      backgroundType: config.backgroundType || 'grid',
      backgroundTransparent: config.backgroundTransparent === true,
      backgroundColor: config.backgroundColor || '#f8f9fa',
      gradientFloorColor: config.gradientFloorColor || '#c0c1c2',
      defaultLayout: layout,
      defaultProjection: projection,
      cameraPreset: normalizeCameraPreset(config.cameraPreset),
      startZoom: (() => {
        const n = Number(config.startZoom);
        if (!Number.isFinite(n) || n <= 0) return 1;
        return Math.max(0.5, Math.min(2, n));
      })(),
      alwaysColorful: config.alwaysColorful === true,
      colorScheme: typeof config.colorScheme === 'string' ? config.colorScheme : 'vivid',
      themePalette: (Array.isArray(config.themePalette) ? config.themePalette : [])
        .map(hexToInt)
        .filter((n) => n != null),
      defaultDataColor: typeof config.defaultDataColor === 'string' ? config.defaultDataColor.trim() : '',
      rulerEnabled: config.rulerEnabled !== false,
      rulerColor: config.rulerColor || '#858585',
      rulerColorAttr: (typeof config.rulerColorAttr === 'string' && config.rulerColorAttr.trim() !== '')
        ? config.rulerColorAttr.trim()
        : '',
      rulerStyle: normalizeRulerStyle(config.rulerStyle),
      barLabelColor: (typeof config.barLabelColor === 'string' && config.barLabelColor.trim() !== '') ? config.barLabelColor.trim() : (config.rulerColor || '#858585'),
      groupLabelColor: (typeof config.groupLabelColor === 'string' && config.groupLabelColor.trim() !== '') ? config.groupLabelColor.trim() : (config.rulerColor || '#858585'),

      interactionEnabled: config.interactionEnabled !== false,
      axisY: config.axisY || { mode: 'numeric', min: 0, max: 100, unit: '%' },
      axisXLabel: typeof config.axisXLabel === 'string' ? config.axisXLabel.trim().slice(0, 48) : '',
      axisYLabel: typeof config.axisYLabel === 'string' ? config.axisYLabel.trim().slice(0, 48) : '',
      diagramMaxHeightByBreakpoint: config.diagramMaxHeightByBreakpoint && typeof config.diagramMaxHeightByBreakpoint === 'object' ? config.diagramMaxHeightByBreakpoint : null,
      diagramMaxHeightLegacy: typeof config.diagramMaxHeightLegacy === 'string' ? config.diagramMaxHeightLegacy.trim() : '',
      barThicknessScale: (() => {
        const n = Number(config.barThicknessScale);
        return Number.isFinite(n) ? n : 1;
      })(),
      barOpacity: (() => {
        const n = Number(config.barOpacity);
        return Number.isFinite(n) ? n : 1;
      })(),
      dataLabelFontScale: (() => {
        const n = Number(config.dataLabelFontScale);
        return Number.isFinite(n) ? n : 1;
      })(),
      pieSliceLabel: typeof config.pieSliceLabel === 'string' ? config.pieSliceLabel : '',
      pieCalloutLineLength: resolvePieCalloutLineLength(config),
      pieCalloutFontScale: resolvePieCalloutFontScale(config),
      pieCalloutColor: resolvePieCalloutColor(config),
      pieInnerRadius: typeof config.pieInnerRadius === 'number' ? config.pieInnerRadius : 0.5,
      pieThickness: resolvePieThickness(config),
      pieAngleBasis: resolvePieAngleBasis(config),
      stairStepHeight: resolveStairStepHeight(config),
      circleLabelStyle: typeof config.circleLabelStyle === 'string' ? config.circleLabelStyle : 'simple',
      pieLabelAutoContrast: config.pieLabelAutoContrast !== false,
      labelsUnderBars: config.labelsUnderBars === true,
      straightBarLabelAbove: straightBarSlots.above,
      straightBarLabelBelow: straightBarSlots.below,
      stackedBarSumAbove: resolveStackedBarSumAbove(config),
      i18nSource: config.i18nSource || 'Source:',
      i18nLoadError: config.i18nLoadError || 'Diagram could not be loaded. Please check your connection or try again later.',
      i18nContextLost: config.i18nContextLost || 'WebGL context was lost. The diagram will recover automatically.',
      i18nContextRestoring: i18nStr(config.i18nContextRestoring, 'Restoring graphics…'),
      i18nHibernateHint: i18nStr(config.i18nHibernateHint, 'Hover over the chart to load the interactive view.'),
      i18nTapToInteract: i18nStr(config.i18nTapToInteract, 'Tap to interact'),
      i18nFullscreenEnter: i18nStr(config.i18nFullscreenEnter, 'Enter fullscreen'),
      i18nFullscreenExit: i18nStr(config.i18nFullscreenExit, 'Exit fullscreen'),
      i18nDataTableShow: i18nStr(config.i18nDataTableShow, 'Show data table'),
      i18nDataTableClose: i18nStr(config.i18nDataTableClose, 'Close'),
      i18nDataTableCaption: i18nStr(config.i18nDataTableCaption, 'Diagram data'),
      i18nDataTableColValue: i18nStr(config.i18nDataTableColValue, 'Value'),
      i18nDataTableColTitle: i18nStr(config.i18nDataTableColTitle, 'Title'),
      i18nDataTableColDescription: i18nStr(config.i18nDataTableColDescription, 'Description'),
      i18nDataTableColSmallPrint: i18nStr(config.i18nDataTableColSmallPrint, 'Note'),
      i18nDataTableColColor: i18nStr(config.i18nDataTableColColor, 'Color'),
      i18nDataTableColGroup: i18nStr(config.i18nDataTableColGroup, 'X group'),
      i18nDataTableSearch: i18nStr(config.i18nDataTableSearch, 'Search\u2026'),
      legendEnabled: config.legendEnabled !== false,
      legendVisibleDefault: config.legendVisibleDefault !== false,
      legendPosition: (config.legendPosition === 'bottom' || config.legendPosition === 'hidden') ? config.legendPosition : 'left',
      legendOutsideChart: config.legendOutsideChart === true,
      legendTextColor: typeof config.legendTextColor === 'string' ? config.legendTextColor.trim() : '',
      i18nLegendShow: i18nStr(config.i18nLegendShow, 'Show legend'),
      i18nLegendHide: i18nStr(config.i18nLegendHide, 'Hide legend'),
      viewerSortEnabled: viewerSortEligible,
      sortOrder: sortOrderEff,
      barsSourceTagged: barsTagged,
      viewerSortGlyphUrl: typeof config.viewerSortGlyphUrl === 'string' ? config.viewerSortGlyphUrl.trim() : '',
      i18nViewerSortLabel: i18nStr(config.i18nViewerSortLabel, 'Sort order'),
      i18nViewerSortAria: i18nStr(config.i18nViewerSortAria, ''),
      i18nSortCustom: i18nStr(config.i18nSortCustom, ''),
      i18nSortValueAsc: i18nStr(config.i18nSortValueAsc, ''),
      i18nSortValueDesc: i18nStr(config.i18nSortValueDesc, ''),
      i18nSortHeadlineAsc: i18nStr(config.i18nSortHeadlineAsc, ''),
      viewerPngDownloadEnabled: config.viewerPngDownloadEnabled === true,
      i18nPngDownload: i18nStr(config.i18nPngDownload, 'PNG Download'),
      barCornerRadius: (() => { const n = Number(config.barCornerRadius); return Number.isFinite(n) ? n : 1; })(),
      barGradientEnabled: config.barGradientEnabled === true,
      sceneYScale: (() => { const n = Number(config.sceneYScale); return Number.isFinite(n) ? n : 1; })(),
      headerTextColor: typeof config.headerTextColor === 'string' ? config.headerTextColor.trim() : '',
      seriesAxisXLabelTransform: config.seriesAxisXLabelTransform ?? null,
      waveStyle: typeof config.waveStyle === 'string' ? config.waveStyle : 'ribbon',
      waveRibbonDepth: (() => { const n = Number(config.waveRibbonDepth); return Number.isFinite(n) ? n : 0.08; })(),
      waveRibbonOpacity: (() => { const n = Number(config.waveRibbonOpacity); return Number.isFinite(n) ? n : 1; })(),
      waveAreaFill: config.waveAreaFill === true,
      waveAreaFillOpacity: (() => { const n = Number(config.waveAreaFillOpacity); return Number.isFinite(n) ? n : 0.35; })(),
      waveBaselineValue: (() => { const n = Number(config.waveBaselineValue); return Number.isFinite(n) ? n : 0; })(),
      waveGradientDirection: typeof config.waveGradientDirection === 'string' ? config.waveGradientDirection : 'vertical',
      waveLineThickness: (() => { const n = Number(config.waveLineThickness); return Number.isFinite(n) ? n : 1; })(),
      waveSmoothness: (() => { const n = Number(config.waveSmoothness); return Number.isFinite(n) ? n : 0.5; })(),
      waveShowPoints: config.waveShowPoints !== false,
      wavePointSize: (() => { const n = Number(config.wavePointSize); return Number.isFinite(n) ? n : 1; })(),
      waveMultiSeries: config.waveMultiSeries !== false,
      waveDropShadow: config.waveDropShadow === true,
      waveAnnotationLineLength: (() => { const n = Number(config.waveAnnotationLineLength); return Number.isFinite(n) ? n : 0.25; })(),
      waveAnnotationFontScale: (() => { const n = Number(config.waveAnnotationFontScale); return Number.isFinite(n) ? n : 1; })(),
      waveLabelAtNodes: typeof config.waveLabelAtNodes === 'string' ? config.waveLabelAtNodes : 'off',
      waveLabelAlongFloor: typeof config.waveLabelAlongFloor === 'string' ? config.waveLabelAlongFloor : 'off',
      pyramidSegmentGap: resolvePyramidSegmentGap(config),
      pyramidSegmentOpacity: 1,
      pyramidInverted: resolvePyramidInverted(config),
      pyramidSliceLabel: resolvePyramidSliceLabelMode(config),
      pyramidLabelAutoContrast: resolvePyramidLabelAutoContrast(config),
      pyramidAnnotation: resolvePyramidAnnotation(config),
      pyramidAnnotationLineLength: resolvePyramidAnnotationLineLength(config),
      pyramidAnnotationFontScale: resolvePyramidAnnotationFontScale(config),
    };

    /* Add-on-Erweiterung: Add-ons erweitern `opts` via `__CE_PD_SCENE_EXT__.applyOpts`.
     * WICHTIG: Der Aufruf erfolgt NICHT hier, sondern erst weiter unten in `doInit`
     * nach dem dynamischen Import von `init.js`. Grund: Add-on-Module (eigene
     * `<script type="module">`-Tags) laufen in Document-Order *nach* `view.js`,
     * registrieren ihre Extender also erst, nachdem `view.js` komplett executed
     * wurde. Wuerden wir `applyOpts` an dieser Stelle synchron aufrufen, sind
     * die Add-on-Extender noch nicht registriert und `opts` bekaeme keine
     * Add-on-Felder. Die Verschiebung in `doInit` macht den Aufruf asynchron
     * (Promise.all wartet auf Network-Fetches), sodass alle Top-Level-Module
     * garantiert executed sind, bevor `applyOpts` laeuft. */

    {
      const winLang = (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en-US';
      const nl = (typeof config.numberLocale === 'string' && config.numberLocale.trim() !== '')
        ? config.numberLocale.trim()
        : winLang;
      const nf = createChartNumberFormatter(nl);
      opts.numberLocale = nl;
      opts.formatChartNumber = nf.formatTick;
    }

    function doInit() {
      const initVer = (typeof window !== 'undefined' && window.__CE_PD_ASSET_VER__) ? String(window.__CE_PD_ASSET_VER__) : '';
      /* Label-Texturen werden per Canvas 2D gebacken. Canvas nutzt nur Fonts, die der
       * Browser bereits geladen hat. document.fonts.ready alleine reicht nicht: der
       * Font wird nur als "in use" betrachtet, wenn ihn ein DOM-Element rendert.
       * Daher explizit per document.fonts.load() triggern. Die genutzte Family kommt
       * aus der CSS-Variable --ce-diagram-font-body (fallback: Barlow Semi Condensed). */
      const family = (() => {
        try {
          const v = getComputedStyle(wrapper).getPropertyValue('--ce-diagram-font-body').trim();
          return v || '"Barlow Semi Condensed", sans-serif';
        } catch (_) { return '"Barlow Semi Condensed", sans-serif'; }
      })();
      const fontsReady = (typeof document !== 'undefined' && document.fonts && document.fonts.load)
        ? Promise.all([
            document.fonts.load('700 16px ' + family),
            document.fonts.load('400 16px ' + family),
          ]).catch(() => null)
        : Promise.resolve();
      Promise.all([
        loadThreeModules(),
        import('./scene/init.js?ver=' + encodeURIComponent(initVer)),
        fontsReady,
        loadTroika(),
      ]).then(([{ THREE, OrbitControls, RoundedBoxGeometry, Reflector }, { initScene }]) => {
        if (wrapper._cePdInitGen !== initGen) return;
        /* Erst hier `applyOpts` rufen: zu diesem Zeitpunkt sind alle anderen
         * Top-Level-Module (insbesondere Add-on-View-Module) in Document-Order
         * laengst executed und haben ihre Extender via `registerOptsExtender`
         * angemeldet. Vor diesem Punkt (im synchron laufenden initBlock) wuerden
         * Add-on-Felder im `opts`-Snapshot fehlen. */
        if (_winScene && _winScene.__CE_PD_SCENE_EXT__ && typeof _winScene.__CE_PD_SCENE_EXT__.applyOpts === 'function') {
          try { _winScene.__CE_PD_SCENE_EXT__.applyOpts(opts, config, layout); } catch (_) { /* ignore broken extension */ }
        }
        enqueueSerializedDiagramInit(() => {
          initScene({ THREE, OrbitControls, RoundedBoxGeometry, Reflector, wrapper, container, tooltip, iconEl, DATA, opts });
        });
      }).catch((err) => {
        if (wrapper._cePdInitGen !== initGen) return;
        console.error('CE Diagram: Three.js failed to load', err);
        const loader = wrapper.querySelector('.ce-diagram-loader');
        if (loader) {
          loader.classList.add('ce-diagram-loader-error');
          loader.setAttribute('aria-busy', 'false');
          const bar = loader.querySelector('.ce-diagram-loader-bar');
          if (bar) bar.style.display = 'none';
          const errEl = (wrapper.ownerDocument || document).createElement('p');
          errEl.className = 'ce-diagram-loader-error-msg';
          errEl.textContent = opts.i18nLoadError;
          loader.appendChild(errEl);
        }
      });
    }

    const IOCtor = (wrapper.ownerDocument || document).defaultView?.IntersectionObserver || IntersectionObserver;
    const queueEntry = { run: function () { io.disconnect(); doInit(); } };
    let queued = false;
    /** 100 px Vorlauf + `threshold: 0`: Init startet, sobald der Wrapper noch 100 px
     *  außerhalb des Viewports ist (statt erst bei 15 % Sichtbarkeit). */
    const io = new IOCtor((entries) => {
      if (entries[0].isIntersecting) {
        if (!queued) { queued = true; _cePdEnqueue(queueEntry); }
      } else {
        if (queued) { queued = false; _cePdDequeue(queueEntry); }
      }
    }, { rootMargin: '100px 0px', threshold: 0 });
    io.observe(wrapper);
  }

  function runCleanupForRemoved(node) {
    if (node.nodeType !== 1) return;
    if (node.classList?.contains('ce-diagram-wrapper')) {
      if (node._ceDiagramConfigObserver) {
        node._ceDiagramConfigObserver.disconnect();
        node._ceDiagramConfigObserver = null;
      }
      if (node._ceDiagramCleanup) node._ceDiagramCleanup();
    }
    const wrappers = node.querySelectorAll?.('.ce-diagram-wrapper');
    if (wrappers) {
      wrappers.forEach((w) => {
        if (w._ceDiagramConfigObserver) {
          w._ceDiagramConfigObserver.disconnect();
          w._ceDiagramConfigObserver = null;
        }
        if (w._ceDiagramCleanup) w._ceDiagramCleanup();
      });
    }
  }

  function initWrappersIn(doc) {
    doc.querySelectorAll('.ce-diagram-wrapper:not([data-ce-init]):not([data-ce-defer-init]):not([data-ce-pd-await-tap])').forEach((w) => {
      w.setAttribute('data-ce-init', '1');
      initBlock(w);
    });
  }

  /**
   * Editor: React/Gutenberg aktualisiert oft nur data-config auf dem gleichen Knoten — MutationObserver
   * feuert dabei nicht zuverlässig (iframe, Batch-Updates). edit.js ruft das nach erfolgter DOM-Aktualisierung auf.
   */
  function refreshEditorPreviewWrapper(wrapper) {
    if (!wrapper || !wrapper.classList?.contains('ce-diagram-wrapper') || !wrapper.classList.contains('ce-diagram-editor-preview')) return;
    if (typeof wrapper._ceDiagramCleanup === 'function') {
      try { wrapper._ceDiagramCleanup(); } catch (_) { /* ignore */ }
    }
    wrapper.removeAttribute('data-ce-init');
    wrapper.removeAttribute('data-ce-pd-await-tap');
    wrapper.classList.remove('loaded');
    if (wrapper._ceDiagramConfigObserver) {
      wrapper._ceDiagramConfigObserver.disconnect();
      wrapper._ceDiagramConfigObserver = null;
    }
    initWrappersIn(wrapper.ownerDocument || document);
  }

  /** Einmal pro Document: Tap auf Snapshot/Container bei `data-ce-pd-await-tap` → volles initBlock (nach Deep-Dispose). */
  function ensureDeepDisposeTapListener(doc) {
    if (!doc || doc.__cePdDeepTapBound) return;
    doc.__cePdDeepTapBound = true;
    doc.addEventListener('click', function (e) {
      const el = e.target;
      if (!el || typeof el.closest !== 'function') return;
      const wrap = el.closest('.ce-diagram-wrapper[data-ce-pd-await-tap]');
      if (!wrap) return;
      if (el.closest && el.closest('.ce-diagram-top-actions')) return;
      const inner = wrap.querySelector('.ce-diagram-inner');
      if (!inner || !inner.contains(el)) return;
      e.preventDefault();
      const wnd = doc.defaultView || window;
      if (typeof wnd.__cePdReInit === 'function') wnd.__cePdReInit(wrap);
    }, true);
  }

  if (typeof window !== 'undefined') {
    window.__cePdRefreshEditorPreview = refreshEditorPreviewWrapper;
    window.__cePdEnsureDeepDisposeTapListener = ensureDeepDisposeTapListener;
    /** Nach Deep-Dispose: nur noch per Tap (nicht beim Scroll-in); Resume-State wird in initScene konsumiert. */
    window.__cePdReInit = function (wrapperEl) {
      if (!wrapperEl || !wrapperEl.classList?.contains('ce-diagram-wrapper')) return;
      wrapperEl.removeAttribute('data-ce-pd-await-tap');
      wrapperEl.setAttribute('data-ce-init', '1');
      initBlock(wrapperEl);
    };
    /** Hosts: nach DOM-Remove oder manuellem Dispose (ohne Entfernen aus dem Baum). */
    window.__cePdTeardownDiagram = function (wrapperEl) {
      if (!wrapperEl || !wrapperEl.classList?.contains('ce-diagram-wrapper')) return;
      runCleanupForRemoved(wrapperEl);
    };
    /** Entfernt `data-ce-defer-init` und triggert normales Init (Intersection + Queue). */
    window.__cePdAllowDeferredInit = function (wrapperEl) {
      if (!wrapperEl || !wrapperEl.classList?.contains('ce-diagram-wrapper')) return;
      wrapperEl.removeAttribute('data-ce-defer-init');
      initWrappersIn(wrapperEl.ownerDocument || document);
    };
    /** Neu sichtbare Wrapper einbinden (z. B. nach dynamischem DOM-Update). */
    window.__cePdScanAndInit = scanAndInit;
  }

  function scanAndInit() {
    initWrappersIn(document);
    try {
      const iframe = document.querySelector('iframe[name="editor-canvas"]');
      if (iframe && iframe.contentDocument) initWrappersIn(iframe.contentDocument);
    } catch (_) {}
  }

  const SCAN_DEBOUNCE_MS = 120;
  const scheduleDebouncedScan = createDebouncedInvoker(SCAN_DEBOUNCE_MS);
  function runDebouncedScan() {
    scanAndInit();
    observeEditorIframe();
  }

  function observeEditorIframe() {
    try {
      const iframe = document.querySelector('iframe[name="editor-canvas"]');
      if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body) return;
      if (iframe.contentDocument._cePdObserved) return;
      iframe.contentDocument._cePdObserved = true;
      ensureDeepDisposeTapListener(iframe.contentDocument);
      scanAndInit();
      function scheduleIframeScan() {
        scheduleDebouncedScan(runDebouncedScan);
      }
      const iframeMo = new MutationObserver((mutations) => {
        let dirty = false;
        for (let i = 0; i < mutations.length; i++) {
          const m = mutations[i];
          if (m.removedNodes && m.removedNodes.length) {
            dirty = true;
            m.removedNodes.forEach(runCleanupForRemoved);
          }
          if (m.addedNodes && m.addedNodes.length) dirty = true;
        }
        if (dirty) scheduleIframeScan();
      });
      iframeMo.observe(iframe.contentDocument.body, { childList: true, subtree: true });
    } catch (_) {}
  }

  ensureDeepDisposeTapListener(document);
  scanAndInit();
  observeEditorIframe();
  /** MutationObserver auf document.body feuert bei jeder DOM-Änderung der ganzen Seite
   *  (WordPress-Themes, Ads, Scroll-Animationen, Sticky-Header-Updates, Cookie-Banner, …).
   *  Ein unkonditioniertes `scanAndInit()` pro Mutation ist ein signifikanter CPU-Kostenpunkt
   *  auf Content-Seiten. Wir debouncen deshalb den Scan (feuert max. alle SCAN_DEBOUNCE_MS);
   *  Cleanup für entfernte Knoten läuft weiterhin synchron, damit Diagramme beim Block-Remove
   *  ihre Ressourcen sofort freigeben. */
  const mo = new MutationObserver((mutations) => {
    let hasRemovals = false;
    let hasAdditions = false;
    for (let i = 0; i < mutations.length; i++) {
      const m = mutations[i];
      if (m.removedNodes && m.removedNodes.length) {
        hasRemovals = true;
        m.removedNodes.forEach(runCleanupForRemoved);
      }
      if (m.addedNodes && m.addedNodes.length) hasAdditions = true;
    }
    if (hasAdditions || hasRemovals) scheduleDebouncedScan(runDebouncedScan);
  });
  mo.observe(document.body, { childList: true, subtree: true });
})();
