import { isEmptyBar, PIE_STAIRS_SHADOW_SOFTNESS } from '../shared/config.js';

export function createLightsAndShadows(ctx) {
  const { THREE, scene, bars, valueToNorm, barScale, layout, isMobile, pieGroup } = ctx;

  /* HemisphereLight ersetzt die frühere RoomEnvironment/PMREM: weicher Sky→Ground-Gradient
   * liefert die Grundbeleuchtung, DirectionalLights setzen die Highlights. */
  scene.add(new THREE.HemisphereLight(0xffffff, 0xdde5ef, 0.9));
  const keyLight = new THREE.DirectionalLight(0xffffff, 1.4);
  /* pyramid: keyLight weiter rechts (+X), Schatten etwas stärker nach links. */
  if (layout === 'pyramid') keyLight.position.set(11, 14, 17);
  else keyLight.position.set(6, 14, 18);
  keyLight.target.position.set(0, 2, 0);
  scene.add(keyLight);
  scene.add(keyLight.target);
  const fillLight = new THREE.DirectionalLight(0xffffff, 0.45);
  fillLight.position.set(-6, 6, 8);
  scene.add(fillLight);
  const rimLight = new THREE.DirectionalLight(0xffffff, 1.2);
  /* testweise pyramid: Rim höher für stärkeren Kanten-Top-Highlight. */
  if (layout === 'pyramid') rimLight.position.set(0, 28, 6);
  else rimLight.position.set(0, 20, 6);
  scene.add(rimLight);

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

  /* pieStairs: Free-eigenes Layout mit echtem Schlagschatten. Der keyLight wirft die
   * gestufte Silhouette der liegenden Scheibe weich auf den Boden (Empfänger-Plane +
   * Kontakt-Saum entstehen in layouts/pie.js). PCF + shadow.radius = weiche Ränder;
   * die Dezenz steuert die ShadowMaterial-Opacity im Pie-Layout.
   *
   * Add-on-Plugins koennen weitere Layouts via `layoutEnablesCastShadow(layout) === true`
   * fuer den Shadow-Map-Cast aktivieren und ueber `layoutShadowCameraExtent(layout)` das
   * Shadow-Frustum vergroessern (z. B. Wave: langer Schatten nach hinten). */
  /* `bars.length > 0` aktiviert echte Cast-Shadows generisch fuer alle Box-Balken-Layouts
   * (straight/stacked sowie Add-on horizontal/grouped/arc), ohne Pro-Layout-Namen. Wave fuellt
   * `bars` nicht und laeuft weiter ueber `layoutEnablesCastShadow`. */
  const castsShadow = layout === 'pieStairs'
    || layout === 'pyramid'
    || layout === 'pie'
    || layout === 'wave'
    || bars.length > 0
    || (__ext && typeof __ext.layoutEnablesCastShadow === 'function' && __ext.layoutEnablesCastShadow(layout) === true);
  if (castsShadow) {
    const mapSz = isMobile ? 1024 : 2048;
    keyLight.castShadow = true;
    keyLight.shadow.mapSize.set(mapSz, mapSz);
    keyLight.shadow.radius = PIE_STAIRS_SHADOW_SOFTNESS;
    keyLight.shadow.bias = -0.0004;
    keyLight.shadow.normalBias = layout === 'pyramid' ? 0.012 : 0.03;
    const extFromExt = (__ext && typeof __ext.layoutShadowCameraExtent === 'function') ? __ext.layoutShadowCameraExtent(layout) : undefined;
    /* Free-Default 8 (kompaktes, flachliegendes pieStairs); fuer Balken grosszuegiger (12),
     * damit lange Schatten aufrechter Balken nicht am Frustum-Rand abreissen; flaches Pie/Donut
     * schwebt aufrecht wie radial und wirft einen langen Schatten nach hinten → 14.
     * Pro-Override (z. B. Wave/Radial 14) hat weiterhin Vorrang. */
    let baseExt = 8;
    if (bars.length > 0) baseExt = 12;
    else if (layout === 'pie' || layout === 'wave') baseExt = 14;
    else if (layout === 'pyramid') {
      const fromPyramid = pieGroup?.userData?._pyramidShadowExtent;
      baseExt = typeof fromPyramid === 'number' ? fromPyramid : 14;
    }
    const ext = typeof extFromExt === 'number' ? extFromExt : baseExt;
    const sc = keyLight.shadow.camera;
    sc.near = 1;
    sc.far = 60;
    sc.left = -ext;
    sc.right = ext;
    sc.top = ext;
    sc.bottom = -ext;
    sc.updateProjectionMatrix();
  }

  if (!bars.length) {
    return { shadowInstMesh: null };
  }
  /* Add-on-Plugins koennen via `__CE_PD_SCENE_EXT__.layoutDisablesGroundShadow(layout) === true`
   * NUR den weichen Blob-Gradient-Bodenschatten ausschliessen (z. B. Horizontal — Balken liegen
   * flach am Boden, ein Blob ergibt dort keinen Sinn). Der ECHTE Schlagschatten-Empfaenger
   * (weiter unten) ist davon unabhaengig und wird trotzdem fuer alle Balken-Layouts gebaut. */
  const blobDisabled = __ext && typeof __ext.layoutDisablesGroundShadow === 'function'
    && __ext.layoutDisablesGroundShadow(layout) === true;

  const visibleIndices = [];
  bars.forEach((bar, gi) => {
    const { wrapper, item } = bar;
    if (isEmptyBar(item.value)) return;
    if (wrapper.userData._stackedShadowBottomOnly && !wrapper.userData._isStackBottomSegment) return;
    if (wrapper.userData._isEmpty) return;
    visibleIndices.push(gi);
  });

  let shadowInstMesh = null;
  if (!blobDisabled && visibleIndices.length > 0) {
    const softShadowCanvas = document.createElement('canvas');
    softShadowCanvas.width = softShadowCanvas.height = 128;
    const c = softShadowCanvas.getContext('2d');
    const grd = c.createRadialGradient(64, 64, 0, 64, 64, 64);
    grd.addColorStop(0, 'rgba(0,0,0,0.28)');
    grd.addColorStop(0.4, 'rgba(0,0,0,0.12)');
    grd.addColorStop(1, 'rgba(0,0,0,0)');
    c.fillStyle = grd;
    c.fillRect(0, 0, 128, 128);
    const softShadowTex = new THREE.CanvasTexture(softShadowCanvas);
    softShadowTex.needsUpdate = true;

    const shadowPlaneGeo = new THREE.PlaneGeometry(1.2, 1.2);

    const shadowMat = new THREE.MeshBasicMaterial({ map: softShadowTex, transparent: true, depthWrite: false, opacity: 1 });
    shadowMat.onBeforeCompile = (shader) => {
      shader.vertexShader = shader.vertexShader
        .replace('#include <common>', '#include <common>\nattribute float instanceOpacity;\nvarying float vInstanceOpacity;\n')
        .replace('#include <begin_vertex>', '#include <begin_vertex>\nvInstanceOpacity = instanceOpacity;\n');
      shader.fragmentShader = shader.fragmentShader
        .replace('#include <common>', '#include <common>\nvarying float vInstanceOpacity;\n')
        .replace('#include <dithering_fragment>', 'gl_FragColor.a *= vInstanceOpacity;\n#include <dithering_fragment>\n');
    };

    shadowInstMesh = new THREE.InstancedMesh(shadowPlaneGeo, shadowMat, visibleIndices.length);
    shadowInstMesh.frustumCulled = false;
    /* renderOrder muss > floorOverlay.renderOrder (0 im Grid-Mode) sein, damit die
     * Soft-Shadows garantiert NACH dem Boden gezeichnet werden. Sonst werden sie bei
     * Grid-BG + backgroundTransparent=false beim 2D-Toggle von der halb-transparenten
     * floorOverlay (opacity≈0.99, weißer Fill) in der Transparent-Queue überdeckt. */
    shadowInstMesh.renderOrder = 1;
    shadowInstMesh.castShadow = false;
    shadowInstMesh.receiveShadow = false;

    const opacityArray = new Float32Array(visibleIndices.length);
    shadowInstMesh.geometry.setAttribute('instanceOpacity', new THREE.InstancedBufferAttribute(opacityArray, 1));

    const _m4 = new THREE.Matrix4();
    const _pos = new THREE.Vector3();
    const _quat = new THREE.Quaternion();
    const _euler = new THREE.Euler();
    const _scl = new THREE.Vector3(barScale, barScale, 1);

    visibleIndices.forEach((gi, si) => {
      const bar = bars[gi];
      bar.shadowInstanceId = si;
      bar.wrapper.userData._shadowOpacityCurrent = 0;
      bar.wrapper.userData._shadowTargetOpacity = 1;

      const w = bar.wrapper;
      const h = valueToNorm(bar.item.value) * 4;
      const footY = w.userData._stackedBottomPivot === true ? -0.004 : -h / 2 - 0.003;
      _pos.set(w.position.x, w.position.y + footY, w.position.z);
      _euler.set(-Math.PI / 2, 0, 0);
      _quat.setFromEuler(_euler);
      _m4.compose(_pos, _quat, _scl);
      shadowInstMesh.setMatrixAt(si, _m4);
      opacityArray[si] = 0;
    });

    shadowInstMesh.instanceMatrix.needsUpdate = true;
  }

  /* Echter Schlagschatten der Balken braucht einen Boden-Empfaenger (Blob bleibt zusaetzlich,
   * User-Entscheidung). ShadowMaterial ist ueberall transparent und zeigt NUR die projizierte
   * Schatten-Maske; die grosse Plane ist daher visuell unkritisch. Der Balken-Fit ist analytisch
   * (layoutFrame expandBar) und traversiert keine Szenen-Meshes → kein Exclude-Flag noetig. */
  if (castsShadow && visibleIndices.length > 0) {
    let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity, baseY = Infinity, maxTopY = -Infinity;
    visibleIndices.forEach((gi) => {
      const w = bars[gi].wrapper;
      const hh = bars[gi].hh;
      const stacked = w.userData._stackedBottomPivot === true;
      const foot = stacked ? w.position.y : w.position.y - hh / 2;
      const top = stacked ? w.position.y + hh : w.position.y + hh / 2;
      if (w.position.x < minX) minX = w.position.x;
      if (w.position.x > maxX) maxX = w.position.x;
      if (w.position.z < minZ) minZ = w.position.z;
      if (w.position.z > maxZ) maxZ = w.position.z;
      if (foot < baseY) baseY = foot;
      if (top > maxTopY) maxTopY = top;
    });
    const maxH = Math.max(0.001, maxTopY - baseY);

    /* keyLight steht vorn-oben (Offset 6,12,18 ggue. Target) → Schatten faellt nach hinten-links.
     * Pro Hoeheneinheit verschiebt sich die Schattenspitze um ~1.5 in -Z und ~0.5 in -X. Die Plane
     * muss so weit reichen, sonst wird der Schatten an der Kante abgeschnitten. */
    const dropZ = 1.5 * maxH, dropX = 0.5 * maxH;
    const frontZ = maxZ + 1.0;
    const backZ = minZ - dropZ - 1.5;
    const leftX = minX - dropX - 1.5;
    const rightX = maxX + 1.5;
    const cx = (leftX + rightX) / 2;
    const cz = (frontZ + backZ) / 2;
    const planeW = rightX - leftX;
    const planeD = frontZ - backZ;

    /* Back-Fade wie wave.js: lokales position.y der um -90deg gedrehten Plane laeuft entlang der
     * Tiefe (worldZ = cz - localY). Schatten voll im Balkenbereich (vorne), blendet zur Spitze aus. */
    const fadeFullY = (cz - (maxZ + 0.5)).toFixed(4);
    const fadeEndY = (cz - (backZ + 0.5)).toFixed(4);
    const groundMat = new THREE.ShadowMaterial({ opacity: 0.24, transparent: true, depthWrite: false });
    groundMat.onBeforeCompile = (shader) => {
      shader.vertexShader = shader.vertexShader
        .replace('#include <common>', '#include <common>\nvarying float vShadowFadeY;')
        .replace('#include <begin_vertex>', '#include <begin_vertex>\nvShadowFadeY = position.y;');
      shader.fragmentShader = shader.fragmentShader
        .replace('#include <common>', '#include <common>\nvarying float vShadowFadeY;')
        .replace(
          'gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );',
          'gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\tgl_FragColor.a *= 1.0 - smoothstep(' + fadeFullY + ', ' + fadeEndY + ', vShadowFadeY);',
        );
    };
    const ground = new THREE.Mesh(new THREE.PlaneGeometry(planeW, planeD), groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.position.set(cx, baseY - 0.003, cz);
    ground.receiveShadow = true;
    ground.castShadow = false;
    ground.renderOrder = 1;
    ground.raycast = () => {};
    scene.add(ground);
  }

  return { shadowInstMesh };
}
