import { resolveDataItemColor, isEmptyBar } from '../../shared/config.js';
import { patchEmissiveForInstancing, setInstanceTransform } from '../instanced.js';
import { enableProjectionFlatMorph } from '../projectionFlatMaterial.js';

/** Wahrgenommene Helligkeit (Rec. 709) eines Farb-Ints fuer die Verlaufs-Sortierung. */
function _luminanceInt(c) {
  return 0.2126 * ((c >> 16) & 0xff) + 0.7152 * ((c >> 8) & 0xff) + 0.0722 * (c & 0xff);
}

/**
 * 3-Stopp-Farb-Triplett fuer Balken `n` aus der aktiven Palette (zyklisch), nach
 * Helligkeit sortiert (dunkelste zuerst) → dunkel unten/links, hell oben/rechts.
 * Identischer Algorithmus fuer Mesh-Rendering (bars.js) und Legenden-/Tabellen-Swatch (init.js).
 * @param {number[]} palette  Paletten-Farb-Ints.
 * @param {number} n  Balken-/Datenindex.
 * @returns {[number, number, number]}
 */
export function gradientStopsForIndex(palette, n) {
  const len = palette.length;
  return [
    palette[(3 * n) % len],
    palette[(3 * n + 1) % len],
    palette[(3 * n + 2) % len],
  ].sort((a, b) => _luminanceInt(a) - _luminanceInt(b));
}

/**
 * Gradient-Patch: faerbt jeden Balken mit einem 3-Stopp-Verlauf entlang der lokalen Y-Achse.
 * MUSS als LETZTER onBeforeCompile registriert werden (nach patchEmissiveForInstancing +
 * enableProjectionFlatMorph) und ruft `prev` zuerst auf — so existiert `emissive * vColor.rgb`
 * bereits und wird um `* ceGrad` ergaenzt.
 * @param {THREE.Material} material
 * @param {number} geoH  Geometriehoehe (1 = Box, 3 = RoundedBox).
 */
function patchGradientBars(material, geoH) {
  const GEO_H = geoH.toFixed(1);
  const prev = material.onBeforeCompile;
  material.onBeforeCompile = (shader, renderer) => {
    if (prev) prev(shader, renderer);
    shader.vertexShader = shader.vertexShader
      .replace(
        '#include <common>',
        '#include <common>\nattribute vec3 aGrad0;\nattribute vec3 aGrad1;\nattribute vec3 aGrad2;\nvarying vec3 vCeG0;\nvarying vec3 vCeG1;\nvarying vec3 vCeG2;\nvarying float vCeGradT;',
      )
      .replace(
        '#include <begin_vertex>',
        '#include <begin_vertex>\nvCeG0 = aGrad0;\nvCeG1 = aGrad1;\nvCeG2 = aGrad2;\nvCeGradT = clamp(position.y / ' + GEO_H + ' + 0.5, 0.0, 1.0);',
      );
    /* ceGrad wird an der emissive-Zeile deklariert (steht im MeshPhong-Shader VOR
     * <map_fragment>), damit es sowohl fuer emissive als auch fuer diffuseColor gilt. */
    shader.fragmentShader = shader.fragmentShader
      .replace(
        '#include <common>',
        '#include <common>\nvarying vec3 vCeG0;\nvarying vec3 vCeG1;\nvarying vec3 vCeG2;\nvarying float vCeGradT;',
      )
      .replace(
        'vec3 totalEmissiveRadiance = emissive * vColor.rgb;',
        'vec3 ceGrad = vCeGradT < 0.5 ? mix(vCeG0, vCeG1, vCeGradT * 2.0) : mix(vCeG1, vCeG2, (vCeGradT - 0.5) * 2.0);\n\tvec3 totalEmissiveRadiance = emissive * vColor.rgb * ceGrad;',
      )
      .replace(
        '#include <map_fragment>',
        '#include <map_fragment>\n\tdiffuseColor.rgb *= ceGrad;',
      );
  };
  material.needsUpdate = true;
}

export function createBars(ctx) {
  const { THREE, RoundedBoxGeometry, DATA, valueToNorm, getBarLayout, layout, barWidth, barDepth, maxBarHeight, HOVER_PALETTE, GRAY_PALETTE, uniformDataColor, barCornerRadius, barOpacity, barGradientEnabled, reverseBarDrawOrder, projectionFlatRegistry } = ctx;
  const gradientBars = barGradientEnabled === true;
  const bars = [];
  const barGroup = new THREE.Group();

  /* Graue Hell-Dunkel-Schattierungstextur nur fuer Normal-Balken. Im Gradient-Modus
   * wird sie nicht genutzt (map/emissiveMap = null), daher gar nicht erst erzeugen. */
  let gradientTex = null;
  if (!gradientBars) {
    const gradientCanvas = document.createElement('canvas');
    gradientCanvas.width = 4;
    gradientCanvas.height = 64;
    const gctx = gradientCanvas.getContext('2d');
    const grad = gctx.createLinearGradient(0, 64, 0, 0);
    grad.addColorStop(0, 'rgb(180,180,180)');
    grad.addColorStop(1, 'rgb(255,255,255)');
    gctx.fillStyle = grad;
    gctx.fillRect(0, 0, 4, 64);
    gradientTex = new THREE.CanvasTexture(gradientCanvas);
    gradientTex.wrapS = gradientTex.wrapT = THREE.ClampToEdgeWrapping;
  }

  const isStacked = layout === 'stacked';
  const cornerR = typeof barCornerRadius === 'number' && isFinite(barCornerRadius) ? Math.max(0, Math.min(1, barCornerRadius)) : 1;
  const barOpacityClamped = typeof barOpacity === 'number' && isFinite(barOpacity) ? Math.max(0.1, Math.min(1, barOpacity)) : 1;
  const useRounded = !isStacked && cornerR > 0;
  const ROUNDED_GEO_H = 3;
  const geoH = useRounded ? ROUNDED_GEO_H : 1;
  const sharedGeo = useRounded
    ? new RoundedBoxGeometry(barWidth, ROUNDED_GEO_H, barDepth, 2, Math.min(0.02, barWidth * 0.15) * cornerR)
    : new THREE.BoxGeometry(barWidth, 1, barDepth);

  const slotGroups = new Map();
  const _tmpCol = new THREE.Color();

  DATA.forEach((item, i) => {
    const h = isEmptyBar(item.value) ? 0 : valueToNorm(item.value) * maxBarHeight;
    const l = getBarLayout(layout, i, h);
    const hh = Math.max(h, 0.001);
    const defaultColor = resolveDataItemColor(item, i, HOVER_PALETTE, uniformDataColor);
    const stackSlot = isStacked && typeof item._paletteIndex === 'number' ? item._paletteIndex : 0;
    const isEmpty = isEmptyBar(item.value);

    const barWrapper = new THREE.Object3D();
    barWrapper.visible = !isEmpty;
    barWrapper.userData._stackedBottomPivot = isStacked;
    barWrapper.position.set(l.x, l.y, l.z);
    barWrapper.rotation.y = l.rotationY;
    barWrapper.rotation.z = l.rotationZ ?? 0;
    barWrapper.userData._h = h;
    barWrapper.userData._isEmpty = isEmpty;
    barWrapper.userData._targetPos = new THREE.Vector3(l.x, l.y, l.z);
    barWrapper.userData._targetRotY = l.rotationY;
    barWrapper.userData._targetRotZ = l.rotationZ ?? 0;

    const meshUserData = { ...item, index: i, _targetColor: defaultColor, _defaultColor: defaultColor };

    if (!slotGroups.has(stackSlot)) slotGroups.set(stackSlot, []);
    slotGroups.get(stackSlot).push(i);

    bars.push({ wrapper: barWrapper, item, instMesh: null, instanceId: -1, meshUserData, hh, stackSlot });
  });

  const barInstMeshes = [];
  for (const [slot, globalIndices] of slotGroups) {
    const stackPo = 0.6 + Math.min(slot, 14) * 0.34;
    const mat = new THREE.MeshPhongMaterial({
      color: 0xffffff,
      emissive: 0xffffff,
      emissiveIntensity: 0.4,
      shininess: 120,
      specular: 0x444444,
      map: gradientTex,
      emissiveMap: gradientTex,
      transparent: barOpacityClamped < 1,
      opacity: barOpacityClamped,
      ...(isStacked
        ? { polygonOffset: true, polygonOffsetFactor: stackPo, polygonOffsetUnits: stackPo }
        : {}),
    });
    patchEmissiveForInstancing(mat);
    if (projectionFlatRegistry) enableProjectionFlatMorph(mat, projectionFlatRegistry);
    /* Gradient-Modus: eigene Geometrie pro Slot, da die aGrad-Attribute instanzgebunden
     * sind und zwischen mehreren (Stacked-)Meshes sonst kollidieren wuerden. */
    const geoForMesh = gradientBars ? sharedGeo.clone() : sharedGeo;
    if (gradientBars) {
      /* map/emissiveMap sind hier bereits null (gradientTex wurde nicht erzeugt): die graue
       * Schattierung wuerde die ohnehin dunkelste Verlaufsfarbe am Boden doppelt abdunkeln.
       * Dezentes Self-Illumination-Leuchten (30%), ohne die Farben zu ueberstrahlen. */
      mat.emissiveIntensity = 0.3;
      patchGradientBars(mat, geoH);
    }
    const instMesh = new THREE.InstancedMesh(geoForMesh, mat, globalIndices.length);
    instMesh.castShadow = true;
    instMesh.receiveShadow = true;
    if (isStacked) instMesh.renderOrder = slot;
    instMesh.userData._barIndices = [];
    if (gradientBars) instMesh.userData._ceGradientBars = true;

    /* Transparente Balken: feste Zeichenreihenfolge hinten→vorne (wie wave.js renderOrder),
     * da der Tiefenpuffer (depthWrite=true) sonst weiter hinten liegende Balken verwirft.
     * Umkehren der Instanz-Reihenfolge spiegelt die korrekte Blickseite. Nur Layouts, deren
     * Tiefenstaffelung entgegengesetzt verläuft (per ctx-Flag aus init/Deskriptor), kehren um. */
    const reverseDrawOrder = barOpacityClamped < 1 && reverseBarDrawOrder === true;
    const count = globalIndices.length;
    const aGrad0 = gradientBars ? new Float32Array(count * 3) : null;
    const aGrad1 = gradientBars ? new Float32Array(count * 3) : null;
    const aGrad2 = gradientBars ? new Float32Array(count * 3) : null;
    globalIndices.forEach((gi, idx) => {
      const localId = reverseDrawOrder ? (globalIndices.length - 1 - idx) : idx;
      const bar = bars[gi];
      bar.instMesh = instMesh;
      bar.instanceId = localId;
      instMesh.userData._barIndices[localId] = gi;

      const w = bar.wrapper;
      const py = isStacked ? w.position.y + bar.hh / 2 : w.position.y;
      setInstanceTransform(THREE, instMesh, localId,
        w.position.x, py, w.position.z,
        w.rotation.y, w.rotation.z,
        1, bar.hh / geoH, 1);

      if (gradientBars) {
        /* instanceColor bleibt weiss; der Verlauf kommt allein aus den aGrad-Attributen. */
        _tmpCol.set(0xffffff);
        instMesh.setColorAt(localId, _tmpCol);
        const stops = gradientStopsForIndex(HOVER_PALETTE, gi);
        const o = localId * 3;
        _tmpCol.set(stops[0]); aGrad0[o] = _tmpCol.r; aGrad0[o + 1] = _tmpCol.g; aGrad0[o + 2] = _tmpCol.b;
        _tmpCol.set(stops[1]); aGrad1[o] = _tmpCol.r; aGrad1[o + 1] = _tmpCol.g; aGrad1[o + 2] = _tmpCol.b;
        _tmpCol.set(stops[2]); aGrad2[o] = _tmpCol.r; aGrad2[o + 1] = _tmpCol.g; aGrad2[o + 2] = _tmpCol.b;
      } else {
        _tmpCol.set(bar.meshUserData._defaultColor);
        instMesh.setColorAt(localId, _tmpCol);
      }
    });

    if (gradientBars) {
      geoForMesh.setAttribute('aGrad0', new THREE.InstancedBufferAttribute(aGrad0, 3));
      geoForMesh.setAttribute('aGrad1', new THREE.InstancedBufferAttribute(aGrad1, 3));
      geoForMesh.setAttribute('aGrad2', new THREE.InstancedBufferAttribute(aGrad2, 3));
    }

    instMesh.instanceMatrix.needsUpdate = true;
    if (instMesh.instanceColor) instMesh.instanceColor.needsUpdate = true;
    barGroup.add(instMesh);
    barInstMeshes.push(instMesh);
  }

  return { barGroup, bars, barInstMeshes, gradientTex, geoTemplateH: geoH };
}
