/*
 * Stage-Height-Controller: responsive Höhe des Wrapper-Elements.
 *
 * Berechnet die minimale Höhe für den Chart-Bereich aus:
 *   - festen Breakpoint-Werten (px/em/%)
 *   - legacy CSS-Höhenwert
 *   - dynamischem Aspect-Ratio der Szene (fallback)
 * und addiert Header/Footer/Legend-Höhen.
 *
 * Der Controller hält seinen internen State (skipRecalcUntil, headerFooterH)
 * und exponiert eine minimale API für init.js.
 */
import { PIE_STAIRS_DEFAULT_POLAR_RAD } from './cameraController.js';

export function createStageHeight({
  wrapper,
  container,
  ownerDoc,
  state,
  getExternalLegendEl,
  diagramMaxByBp,
  diagramMaxHeightLegacy,
  isDiagramFullscreen,
  getLayoutFrame,
}) {
  let skipHeightRecalcUntil = 0;

  function diagramViewport() {
    return wrapper.ownerDocument?.defaultView || window;
  }

  /** Desktop / Tablet / Mobile: immer Viewport (Fenster/Iframe), nicht schmale Nachbar-Container. */
  function breakpointKeyForStageWidth(stageW) {
    const w = stageW > 0 ? stageW : diagramViewport().innerWidth;
    if (w < 782) return 'mobile';
    if (w < 1024) return 'tablet';
    return 'desktop';
  }

  function measureHeightPxLegacy(raw) {
    const s = String(raw || '').trim();
    if (!s || s.length > 64) return null;
    if (/[<>";{}@!$`\\]|url\s*\(|expression\s*\(/i.test(s)) return null;
    const probe = ownerDoc.createElement('div');
    probe.style.cssText = 'position:absolute;left:-9999px;top:0;width:1px;visibility:hidden;pointer-events:none;box-sizing:border-box;height:' + s;
    ownerDoc.body.appendChild(probe);
    const h = probe.offsetHeight;
    ownerDoc.body.removeChild(probe);
    return h > 0 && Number.isFinite(h) ? h : null;
  }

  function resolveResponsiveHeightPx() {
    const key = breakpointKeyForStageWidth(diagramViewport().innerWidth || 0);
    const vh = diagramViewport().innerHeight;
    if (diagramMaxByBp && diagramMaxByBp[key]) {
      const entry = diagramMaxByBp[key];
      const valueStr = String(entry.value || '').trim();
      const unit = entry.unit === 'em' || entry.unit === '%' ? entry.unit : 'px';
      if (valueStr) {
        const n = parseFloat(valueStr);
        if (Number.isFinite(n) && n >= 0 && n <= 9999) {
          if (unit === 'px') return Math.round(n);
          if (unit === '%') return Math.round(vh * (n / 100));
          if (unit === 'em') {
            const probe = ownerDoc.createElement('div');
            probe.style.cssText = 'position:absolute;left:-9999px;width:1px;height:0;margin:0;padding:0;border:0;visibility:hidden;font:inherit;box-sizing:border-box';
            wrapper.appendChild(probe);
            probe.style.height = n + 'em';
            const h = probe.offsetHeight;
            wrapper.removeChild(probe);
            if (h > 0) return h;
          }
        }
      }
    }
    if (diagramMaxHeightLegacy) return measureHeightPxLegacy(diagramMaxHeightLegacy);
    return null;
  }

  function measureHeaderFooter() {
    const externalLegendEl = getExternalLegendEl();
    const header = wrapper.querySelector('.ce-diagram-header');
    const footer = wrapper.querySelector('.ce-diagram-footnote');
    const hh = header ? header.offsetHeight : 0;
    const fh = footer ? footer.offsetHeight : 0;
    const lh = (externalLegendEl && !externalLegendEl.hasAttribute('hidden')) ? externalLegendEl.offsetHeight : 0;
    wrapper.style.setProperty('--ce-header-h', hh + 'px');
    wrapper.style.setProperty('--ce-footer-h', fh + 'px');
    wrapper.style.setProperty('--ce-legend-h', lh + 'px');
    return hh + fh + lh;
  }

  let headerFooterH = measureHeaderFooter();

  function calcStageHeight() {
    if (performance.now() < skipHeightRecalcUntil) return;
    const w = container.clientWidth;
    if (!(w > 1)) return;
    if (isDiagramFullscreen()) {
      wrapper.style.minHeight = '';
      /* Sonst bleibt lastStageMinHeight vom Embed-Zustand; nach Exit trifft „gleiches target" das Kurzschluss-Return,
         ohne minHeight erneut zu setzen (Inline war im Vollbild geleert → nur noch CSS-Mindesthöhe ~200px). */
      state.lastStageMinHeight = null;
      return;
    }
    if (state.stageAspectMin == null) {
      const f = getLayoutFrame(state.layout);
      let a;
      if (state.layout === 'pieStairs') {
        /* Liegendes Chart, von schräg oben betrachtet (Default-Polar ⇒ Elevation). Die naive
         * extentH/extentV-Aspect (flache Bbox) ergäbe einen sehr breiten, niedrigen Wrapper.
         * Stattdessen die projizierte Schirm-Höhe schätzen: Tiefe (Z) projiziert mit
         * cos(Elevation), Höhe der Stufen (Y) mit sin(Elevation) — so wird der Wrapper
         * ähnlich ausgewogen wie beim stehenden Pie und passt sich an die Stufenhöhe an. */
        const elev = Math.PI / 2 - PIE_STAIRS_DEFAULT_POLAR_RAD;
        const screenV = f.size.z * Math.cos(elev) + f.size.y * Math.sin(elev);
        const screenH = Math.max(f.size.x, f.size.z);
        a = screenH / Math.max(1e-6, screenV);
      } else {
        a = f.extentH / Math.max(1e-6, f.extentV);
      }
      state.stageAspectMin = Number.isFinite(a) && a > 0 ? a : 1;
      wrapper._cePdComputedAspect = state.stageAspectMin;
      try { wrapper.dispatchEvent(new CustomEvent('ce-pd-aspect-computed', { bubbles: true, detail: { aspect: state.stageAspectMin } })); } catch (_) {}
    }
    const cs = window.getComputedStyle(wrapper);
    const padV = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
    const fixedPx = resolveResponsiveHeightPx();
    headerFooterH = measureHeaderFooter();
    let target = fixedPx != null ? Math.round(fixedPx) : Math.max(0, Math.round(w / state.stageAspectMin + padV));
    target += headerFooterH;
    if (state.lastStageMinHeight != null && Math.abs(target - state.lastStageMinHeight) <= 2) return;
    const hasAspectHint = wrapper.hasAttribute('data-aspect-hint');
    state.lastStageMinHeight = target;
    wrapper.style.minHeight = target + 'px';
    if (hasAspectHint) {
      wrapper.style.aspectRatio = '';
      wrapper.removeAttribute('data-aspect-hint');
    }
  }

  return {
    calcStageHeight,
    measureHeaderFooter,
    getHeaderFooterH: () => headerFooterH,
    refreshHeaderFooterHeight: () => { headerFooterH = measureHeaderFooter(); calcStageHeight(); },
    markSkipHeightRecalc: (ms) => { skipHeightRecalcUntil = performance.now() + ms; },
  };
}
