/**
 * Layout-Apply & Shadow-Sync: setzt die Ziel-Pose aller Bars je Layout, schaltet Group-Sichtbarkeit
 * und pusht die Schatten-InstancedMesh-Matrix an die GPU.
 *
 * applyBarTargets  — pro Bar: _targetPos/_targetRotY/_targetRotZ anhand getBarLayout().
 * applyLayout      — setzt state.layout, Group-Visibility, Ruler-TargetOpacity, Camera und ruft
 *                    applyBarTargets (für Bar-Layouts).
 * syncShadowInstMesh — schreibt Matrix + instanceOpacity aus dem Bar-State zurück zur GPU
 *                      (pro Frame während barsMoving + einmalig nach Skip-Intro).
 */
export function createLayoutApply(ctx) {
  const {
    THREE, state, controls, markDirty,
    camCtl, invalidateFrameCache,
    bars, rulerEnabled, rulerGroup,
    barGroup, waveGroup, radialGroup, pieGroup, surfaceGroup, bubblePackGroup,
    seriesAxisXText, axisYLabelText,
    getBarLayout,
    getShadowInstMesh,
    barScale,
  } = ctx;

  function applyBarTargets(layout) {
    bars.forEach((bar, i) => {
      const wrapper = bar.wrapper;
      const h = wrapper.userData._h;
      const l = getBarLayout(layout, i, h);
      wrapper.userData._targetPos.set(l.x, l.y, l.z);
      wrapper.userData._targetRotY = l.rotationY;
      wrapper.userData._targetRotZ = l.rotationZ ?? 0;
    });
  }

  function applyLayout(layout) {
    state.layout = layout;
    invalidateFrameCache();
    state.stageAspectMin = null;
    state.lastStageMinHeight = null;
    camCtl.setZoomLimits();

    const __ext = (typeof window !== 'undefined' && window.__CE_PD_SCENE_EXT__) || null;

    /* Genau eine exklusive Layout-Gruppe ist sichtbar. Erst alle ausblenden, dann die
     * zustaendige einblenden — netto identisch zum frueheren per-Layout-Schalten. */
    barGroup.visible = false;
    waveGroup.visible = false;
    radialGroup.visible = false;
    pieGroup.visible = false;
    surfaceGroup.visible = false;
    bubblePackGroup.visible = false;

    let handled = false;
    if (layout === 'pie' || layout === 'pieStairs' || layout === 'pyramid') {
      /* Free-eigenes Pie/PieStairs/Pyramid → pieGroup, kein Ruler. */
      pieGroup.visible = true;
      rulerGroup.userData._rulerTargetOpacity = 0;
      handled = true;
    } else if (layout === 'wave') {
      waveGroup.visible = true;
      rulerGroup.userData._rulerTargetOpacity = rulerEnabled ? 1 : 0;
      handled = true;
    } else if (__ext && typeof __ext.layoutGroupVisibility === 'function') {
      /* Add-on-Plugins (Pro) schalten ihre Nicht-Bar-Layouts (wave/radial/surface/
       * bubblePack) auf die eigene Gruppe und liefern die Ziel-Ruler-Opacity zurueck. */
      const r = __ext.layoutGroupVisibility(layout, { barGroup, waveGroup, radialGroup, pieGroup, surfaceGroup, bubblePackGroup, rulerEnabled });
      if (r && r.matched === true) {
        rulerGroup.userData._rulerTargetOpacity = typeof r.rulerTargetOpacity === 'number' ? r.rulerTargetOpacity : 0;
        handled = true;
      }
    }
    if (!handled) {
      /* Bar-Layouts: Free's straight/stacked sowie Add-on-Bar-Layouts (arc/grouped/
       * horizontal) teilen sich die barGroup. */
      barGroup.visible = true;
      applyBarTargets(layout);
      rulerGroup.userData._rulerTargetOpacity = rulerEnabled ? 1 : 0;
      /* Bodenschatten-Blob: Add-ons koennen ihn pro Layout unterdruecken (z. B.
       * horizontal — Balken liegen flach am Boden), sonst sichtbar. */
      const disablesGroundShadow = __ext && typeof __ext.layoutDisablesGroundShadow === 'function' && __ext.layoutDisablesGroundShadow(layout) === true;
      const shadowTarget = disablesGroundShadow ? 0 : 1;
      bars.forEach(bar => { bar.wrapper.userData._shadowTargetOpacity = shadowTarget; });
    }
    /* Show-Flags werden jetzt statisch in createRuler pro Layout gesetzt; hier nur Text-leer-Faelle nachziehen. */
    if (rulerGroup.userData) {
      if (seriesAxisXText === '' && rulerGroup.userData._seriesAxisXLabel) rulerGroup.userData._seriesAxisXLabel.visible = false;
      if (axisYLabelText === '' && rulerGroup.userData._seriesAxisYLabel) rulerGroup.userData._seriesAxisYLabel.visible = false;
      if (seriesAxisXText === '' && rulerGroup.userData._horizontalAxisXLabel) rulerGroup.userData._horizontalAxisXLabel.visible = false;
    }
    if (!state.isOrthographic) {
      controls.minPolarAngle = 0.1;
      controls.maxPolarAngle = camCtl.maxPolarForLayout(layout);
    }
    if (state.isOrthographic) {
      /* Pseudo-Ortho: Layout-Wechsel → neue Frame-Center/Extent → neue frontale Pose bei
       * fov-low-gekoppeltem Radius. setCameraTargetFromLayout liefert die Pose-Werte,
       * applyCameraResult schreibt sie auf die PerspectiveCamera. */
      const cam = camCtl.setCameraTargetFromLayout(layout);
      camCtl.applyCameraResult(cam);
    } else {
      camCtl.refitPerspectiveCamera(true);
    }
    state.cameraCenterTarget.position = null;
    state.cameraCenterTarget.lookAt = null;
    markDirty();
  }

  /** Schreibt Matrix + instanceOpacity aller Schatten-Instances aus dem aktuellen Bar-State zurück
   *  zur GPU. Wird sowohl pro Frame (während `barsMoving`) als auch einmalig nach Skip-Intro genutzt. */
  function syncShadowInstMesh() {
    const shadowInstMesh = getShadowInstMesh();
    if (!shadowInstMesh) return;
    if (!shadowInstMesh._tmpM4) {
      shadowInstMesh._tmpM4 = new THREE.Matrix4();
      shadowInstMesh._tmpPos = new THREE.Vector3();
      shadowInstMesh._tmpQuat = new THREE.Quaternion();
      shadowInstMesh._tmpEuler = new THREE.Euler();
      shadowInstMesh._tmpScl = new THREE.Vector3(barScale, barScale, 1);
    }
    const isStackedShadow = state.layout === 'stacked';
    const opAttr = shadowInstMesh.geometry.getAttribute('instanceOpacity');
    bars.forEach((bar) => {
      const si = bar.shadowInstanceId;
      if (si == null || si < 0) return;
      const w = bar.wrapper;
      const easeY = w.scale.y || 0;
      const footY = isStackedShadow ? -0.004 : -bar.hh * easeY / 2 - 0.003;
      shadowInstMesh._tmpPos.set(w.position.x, w.position.y + footY, w.position.z);
      shadowInstMesh._tmpEuler.set(-Math.PI / 2, w.rotation.y, 0, 'YXZ');
      shadowInstMesh._tmpQuat.setFromEuler(shadowInstMesh._tmpEuler);
      shadowInstMesh._tmpM4.compose(shadowInstMesh._tmpPos, shadowInstMesh._tmpQuat, shadowInstMesh._tmpScl);
      shadowInstMesh.setMatrixAt(si, shadowInstMesh._tmpM4);
      if (opAttr) opAttr.array[si] = w.userData._shadowOpacityCurrent ?? 0;
    });
    shadowInstMesh.instanceMatrix.needsUpdate = true;
    if (opAttr) opAttr.needsUpdate = true;
  }

  return { applyBarTargets, applyLayout, syncShadowInstMesh };
}
