/*
 * Bar-Top-Label-Fabriken fuer die Standard-Layouts straight & stacked.
 *
 * Add-on-Plugins koennen weitere Label-Fabriken (z. B. fuer eigene Layouts)
 * unter `window.__CE_PD_SCENE_EXT__.labels` registrieren; init.js und der
 * Sync-Loop rufen sie ueber diesen Namespace ab.
 *
 * Damit Add-ons Plugin-Grenzen ueberbruecken koennen (kein relativer Import
 * moeglich), exponiert dieses Modul beim Laden die internen Helfer + Konstanten
 * ueber das globale Objekt `window.__CE_PD_LABEL_HELPERS__` als Utility-API.
 */
import {
  formatBarValueLineForRow,
  STRAIGHT_XLABEL_PLANE_H,
  clampDataLabelFontScale,
  getBarValueLabelParts,
  layoutRulerTickGroup,
  getRulerTickGroupWidth,
  rulerTickSuffixFontUrl,
  TICK_PREFIX_UNIT_GAP_FRAC,
  TICK_VALUE_UNIT_GAP_FRAC,
} from './ruler.js';
import { isEmptyBar } from '../shared/config.js';
import { getFallbackChartNumberFormatter } from '../shared/numberFormat.js';
import { createTroikaLabel, createOverlayLineMaterial, updateTroikaLabel, getLabelBounds } from './troikaLabel.js';

/** Lueftungslueck zwischen Balken-Top und Label-Mittelpunkt (World-Units). */
const LABEL_ABOVE_GAP = 0.045;

/**
 * Glyph-Höhe (em) als Anteil der "Plane-Höhe" der alten Canvas-Renderung.
 * BAR_CATEGORY_HEADLINE_FONT_SIZE (31.5) / HEADLINE_CANVAS_LOGICAL_H (42) = 0.75.
 * Übernommen, damit die optische Größe der Beschriftungen unverändert bleibt.
 */
const HEADLINE_FONT_RATIO = 31.5 / 42;

function resolveFormatChartNumber(ctx) {
  return typeof ctx?.formatChartNumber === 'function'
    ? ctx.formatChartNumber
    : getFallbackChartNumberFormatter().formatTick;
}

function dataLabelFs(ctx) {
  return clampDataLabelFontScale(ctx?.dataLabelFontScale);
}

function hexForBarLabel(barLabelColor, rulerFallback) {
  const base = (typeof barLabelColor === 'string' && barLabelColor.trim() !== '')
    ? barLabelColor.trim()
    : (typeof rulerFallback === 'string' && rulerFallback.trim() !== '' ? rulerFallback.trim() : '#858585');
  return base.startsWith('#') ? base : '#' + base;
}

/** Summe + Zeileneinheit der Spalte (für Wert-Label über gestapelten Balken). */
function stackedTopLabelDataRow(indices, DATA) {
  if (!indices || !indices.length || !DATA) return null;
  let sum = 0;
  let any = false;
  for (const i of indices) {
    const row = DATA[i];
    if (!row || isEmptyBar(row.value)) continue;
    any = true;
    sum += Number(row.value) || 0;
  }
  if (!any) return null;
  const ref = DATA[indices[0]];
  return { value: sum, unit: ref?.unit };
}

function rawTextFor(item, mode, axisYPrefix, axisYSuffix, formatChartNumber) {
  return mode === 'value'
    ? formatBarValueLineForRow(item, axisYPrefix, axisYSuffix, formatChartNumber)
    : String(item.label || '').trim();
}

/**
 * Maximale Zeichenanzahl wie früher; verhindert, dass extrem lange Beschriftungen den Layout-Frame sprengen.
 */
function clampTextChars(raw, maxChars = 36) {
  const s = String(raw == null ? '' : raw);
  if (s.length <= maxChars) return s;
  return s.slice(0, Math.max(1, maxChars - 1)) + '\u2026';
}

/**
 * Gemeinsamer Troika-Label-Factory. Liefert ein Mesh + initial geschätzte Plane-Maße.
 *
 * @param {*} THREE
 * @param {string} colorHex Hex-Farbe (mit oder ohne '#').
 * @param {string} rawText Anzeige-Text (wird auf 36 Zeichen geclampt).
 * @param {number} fs Data-Label-Font-Scale (User-Slider).
 * @param {object} [opts]
 * @param {number} [opts.renderOrder=1000]
 * @param {boolean} [opts.depthTest=true]
 * @param {'left'|'center'|'right'} [opts.anchorX='center']
 * @returns {{ mesh: any, planeW: number, planeH: number }}
 */
function buildLabelMesh(THREE, colorHex, rawText, fs, { renderOrder = 1000, depthTest = true, anchorX = 'center' } = {}) {
  const fontSize = STRAIGHT_XLABEL_PLANE_H * HEADLINE_FONT_RATIO * fs;
  const text = clampTextChars(rawText, 36);
  const mesh = createTroikaLabel({
    THREE,
    text,
    color: colorHex,
    fontSize,
    anchorX,
    anchorY: 'middle',
    weight: 'bold',
    renderOrder,
    depthTest,
    onSync: (m) => {
      const b = getLabelBounds(m);
      m.userData._planeW = b.width;
      m.userData._planeH = b.height;
      const cb = m.userData._afterSized;
      if (typeof cb === 'function') cb(m);
    },
  });
  /* Initial-Schätzung bis Troika-Sync abgeschlossen ist (1–2 Frames). */
  const est = getLabelBounds(mesh);
  const planeW = est.width;
  const planeH = est.height;
  /* Materials starten bei opacity:0; Bar-/Wave-Sync setzt =1 pro Frame (siehe barLabelSync.js).
   * Radial-Labels haben keinen per-Frame-Sync und werden direkt sichtbar geschaltet. */
  if (mesh.material && mesh.material.opacity !== undefined) {
    mesh.material.opacity = 0;
  }
  mesh.userData._planeW = planeW;
  mesh.userData._planeH = planeH;
  return { mesh, planeW, planeH };
}

/** Segment-Sync: gleiche Logik wie Lineal-Ticks, ohne rulerGroup._tickLabelW. */
function wireBarValueSegmentSync(group) {
  const p = group.userData._tickPair;
  if (!p) return;
  const meshList = [p.preMesh, p.bodyMesh, p.sufMesh].filter(Boolean);
  const bump = () => {
    for (let i = 0; i < meshList.length; i += 1) {
      if (!meshList[i].textRenderInfo) return;
    }
    layoutRulerTickGroup(group);
    const totalW = getRulerTickGroupWidth(group);
    let maxH = 0;
    for (let j = 0; j < meshList.length; j += 1) {
      const hh = getLabelBounds(meshList[j]).height;
      if (hh > maxH) maxH = hh;
    }
    group.userData._planeW = totalW;
    group.userData._planeH = maxH;
    const cb = group.userData._afterSized;
    if (typeof cb === 'function') cb(group);
  };
  meshList.forEach((m) => {
    const prev = m.userData._onSized;
    m.userData._onSized = (mm) => {
      try {
        if (typeof prev === 'function') prev(mm);
      } catch (_) { /* ignore */ }
      bump();
    };
  });
}

function sealBarLabelSubtreeOpacity(root, opacity) {
  root.traverse((obj) => {
    if (!obj.isMesh) return;
    const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
    mats.forEach((mat) => {
      if (mat && mat.opacity !== undefined) mat.opacity = opacity;
    });
  });
}

/**
 * Wert+Einheit oberhalb Balken: Präfix|Zahl|Suffix mit festen Lücken (wie Lineal).
 * @param {'center'|'right'|'left'} anchorMode — center = über Balken; right = radial; left = wave.
 */
function buildBarValueSegmentGroup(THREE, colorHex, parts, fs, { renderOrder = 1000, depthTest = true, anchorMode = 'center' }) {
  const fontSize = STRAIGHT_XLABEL_PLANE_H * HEADLINE_FONT_RATIO * fs;
  const anchorX = anchorMode === 'right' ? 'right' : anchorMode === 'left' ? 'left' : 'center';
  const gapPrefixWorld = parts.prefix ? fontSize * TICK_PREFIX_UNIT_GAP_FRAC : 0;
  const gapSuffixWorld = parts.suffix ? fontSize * TICK_VALUE_UNIT_GAP_FRAC : 0;
  const group = new THREE.Group();
  group.layers.set(1);
  group.renderOrder = renderOrder;
  group.userData._tickPair = {
    preMesh: null,
    bodyMesh: null,
    sufMesh: null,
    gapPrefixWorld,
    gapSuffixWorld,
    anchorMode,
  };
  const onSegSync = (m) => {
    const b = getLabelBounds(m);
    m.userData._planeW = b.width;
    m.userData._planeH = b.height;
    const cb = m.userData._afterSized;
    if (typeof cb === 'function') cb(m);
  };
  if (parts.prefix) {
    const preMesh = createTroikaLabel({
      THREE,
      text: clampTextChars(parts.prefix, 36),
      color: colorHex,
      fontSize,
      anchorX,
      anchorY: 'middle',
      weight: 'bold',
      renderOrder,
      depthTest,
      onSync: onSegSync,
    });
    sealBarLabelSubtreeOpacity(preMesh, 0);
    group.userData._tickPair.preMesh = preMesh;
    group.add(preMesh);
  }
  const bodyMesh = createTroikaLabel({
    THREE,
    text: clampTextChars(parts.numberStr, 36),
    color: colorHex,
    fontSize,
    anchorX,
    anchorY: 'middle',
    weight: 'bold',
    renderOrder,
    depthTest,
    onSync: onSegSync,
  });
  sealBarLabelSubtreeOpacity(bodyMesh, 0);
  group.userData._tickPair.bodyMesh = bodyMesh;
  group.add(bodyMesh);
  if (parts.suffix) {
    const sufUrl = rulerTickSuffixFontUrl(parts.suffix, 'bold');
    const sufMesh = createTroikaLabel({
      THREE,
      text: clampTextChars(parts.suffix, 36),
      color: colorHex,
      fontSize,
      anchorX,
      anchorY: 'middle',
      weight: 'bold',
      renderOrder,
      depthTest,
      onSync: onSegSync,
      ...(sufUrl ? { fontUrl: sufUrl } : {}),
    });
    sealBarLabelSubtreeOpacity(sufMesh, 0);
    group.userData._tickPair.sufMesh = sufMesh;
    group.add(sufMesh);
  }
  wireBarValueSegmentSync(group);
  const joined = [parts.prefix, parts.numberStr, parts.suffix].filter(Boolean).join('');
  const estW = Math.max(0.05, joined.length * fontSize * 0.45);
  const estH = fontSize * 1.1;
  group.userData._planeW = estW;
  group.userData._planeH = estH;
  return { mesh: group, planeW: estW, planeH: estH };
}

/**
 * Balken-Top-Label (Headline-Zeichenkette oder Wert als Segment-Gruppe).
 * @param {'center'|'right'|'left'} [opts.anchorMode='center']
 */
function buildBarTopLabel(THREE, colorHex, item, mode, axisYPrefix, axisYSuffix, ft, fs, opts = {}) {
  const { renderOrder = 1000, depthTest = true, anchorMode = 'center' } = opts;
  const anchorX = anchorMode === 'right' ? 'right' : anchorMode === 'left' ? 'left' : 'center';
  if (mode !== 'value') {
    const raw = rawTextFor(item, mode, axisYPrefix, axisYSuffix, ft);
    if (!raw) return null;
    return buildLabelMesh(THREE, colorHex, raw, fs, { renderOrder, depthTest, anchorX });
  }
  const parts = getBarValueLabelParts(item, axisYPrefix, axisYSuffix, ft);
  if (!parts || !parts.numberStr) return null;
  const hasPre = !!parts.prefix;
  const hasSuf = !!parts.suffix;
  if (!hasPre && !hasSuf) {
    return buildLabelMesh(THREE, colorHex, parts.numberStr, fs, { renderOrder, depthTest, anchorX });
  }
  return buildBarValueSegmentGroup(THREE, colorHex, parts, fs, { renderOrder, depthTest, anchorMode });
}

function updateBarValueSegmentGroup(mesh, parts, rulerColorHex, fontSize) {
  const p = mesh.userData._tickPair;
  if (!p || !p.bodyMesh || !parts) return;
  p.gapPrefixWorld = parts.prefix ? fontSize * TICK_PREFIX_UNIT_GAP_FRAC : 0;
  p.gapSuffixWorld = parts.suffix ? fontSize * TICK_VALUE_UNIT_GAP_FRAC : 0;
  if (p.preMesh) {
    updateTroikaLabel(p.preMesh, {
      text: clampTextChars(parts.prefix, 36),
      color: rulerColorHex,
      fontSize,
    });
  }
  updateTroikaLabel(p.bodyMesh, {
    text: clampTextChars(parts.numberStr, 36),
    color: rulerColorHex,
    fontSize,
  });
  if (p.sufMesh) {
    const sufUrl = rulerTickSuffixFontUrl(parts.suffix, 'bold');
    const patch = {
      text: clampTextChars(parts.suffix, 36),
      color: rulerColorHex,
      fontSize,
    };
    if (sufUrl) patch.fontUrl = sufUrl;
    updateTroikaLabel(p.sufMesh, patch);
  }
}

function makeLabelGroup(THREE) {
  const group = new THREE.Group();
  group.layers.set(1);
  group.renderOrder = 1000;
  return group;
}

/** Setzt die „aufrecht über dem Balken"-Pose für straight/grouped (Text hochkant, lookAt +Z). */
function poseVerticalAboveBar(mesh, x, yCenter, z) {
  mesh.position.set(x, yCenter, z);
  mesh.lookAt(mesh.position.x, mesh.position.y, 10);
  mesh.rotateZ(Math.PI / 2);
}

/** World pose + billboard so labels move with bar intro scale/position (grouped columns). Z = bar column center (same as wrapper). */
export function applyGroupedBarHeadlineLabelPose(mesh, wrapper, THREE) {
  const pw = mesh.userData._planeW;
  if (pw == null) return;
  const h = wrapper.userData._h;
  const sy = wrapper.scale.y;
  const barTopY = wrapper.userData._stackedBottomPivot
    ? wrapper.position.y + h * sy
    : wrapper.position.y + (h * sy) / 2;
  const yCenter = barTopY + LABEL_ABOVE_GAP + pw / 2;
  mesh.position.set(wrapper.position.x, yCenter, wrapper.position.z);
  mesh.lookAt(mesh.position.x, mesh.position.y, 10);
  mesh.rotateZ(Math.PI / 2);
}

/** Stacked: one vertical label per column — sum of segment values at the stack top. */
export function createStackedBarSumLabels(ctx) {
  const {
    THREE, DATA, valueToNorm, stackedLayout,
    barLabelColor, rulerColor, straightBarZ,
    axisYPrefix = '', axisYSuffix = '',
  } = ctx;
  const ft = resolveFormatChartNumber(ctx);
  const fs = dataLabelFs(ctx);
  const rulerColorHex = hexForBarLabel(barLabelColor, rulerColor);
  const group = makeLabelGroup(THREE);
  const meshes = [];
  const { groupRowIndices, barX, stackBottomY } = stackedLayout;

  groupRowIndices.forEach((indices) => {
    if (!indices || !indices.length) return;
    const sumRow = stackedTopLabelDataRow(indices, DATA);
    if (!sumRow) return;
    const built = buildBarTopLabel(THREE, rulerColorHex, sumRow, 'value', axisYPrefix, axisYSuffix, ft, fs, {
      anchorMode: 'center',
    });
    if (!built) return;
    const topIdx = indices[indices.length - 1];
    const h = valueToNorm(DATA[topIdx].value) * 4;
    const stackBottom = stackBottomY[topIdx] != null ? stackBottomY[topIdx] : 0;
    const barTopAbs = stackBottom + h;
    const x = barX[topIdx];
    const { mesh, planeW, planeH } = built;
    const yCenter = barTopAbs + LABEL_ABOVE_GAP + planeW / 2;
    poseVerticalAboveBar(mesh, x, yCenter, straightBarZ);
    mesh.userData._frustumHalfW = planeH * 0.55;
    mesh.userData._frustumHalfH = planeW * 0.55 + LABEL_ABOVE_GAP;
    mesh.userData._dataIndex = topIdx;
    mesh.userData._barTopY = barTopAbs;
    mesh.userData._stackedSumMemberIndices = indices.slice();
    mesh.userData._groupedTopLabelMode = 'stackedSum';
    mesh.userData._afterSized = (m) => {
      m.userData._frustumHalfW = m.userData._planeH * 0.55;
      m.userData._frustumHalfH = m.userData._planeW * 0.55 + LABEL_ABOVE_GAP;
      const yC = m.userData._barTopY + LABEL_ABOVE_GAP + m.userData._planeW / 2;
      poseVerticalAboveBar(m, m.position.x, yC, m.position.z);
    };
    meshes.push(mesh);
    group.add(mesh);
  });

  return { group, meshes };
}

/**
 * Straight layout: vertikale Beschriftung oberhalb jedes Balkens (Bezeichnung oder Wert+Einheit).
 */
export function createStraightBarTopLabels(ctx) {
  const {
    THREE, DATA, valueToNorm, getBarLayout,
    barLabelColor, rulerColor, straightBarZ,
    mode, axisYPrefix, axisYSuffix,
  } = ctx;
  const ft = resolveFormatChartNumber(ctx);
  const fs = dataLabelFs(ctx);
  const rulerColorHex = hexForBarLabel(barLabelColor, rulerColor);
  const group = makeLabelGroup(THREE);
  const meshes = [];

  DATA.forEach((item, i) => {
    if (isEmptyBar(item.value)) return;
    const built = buildBarTopLabel(THREE, rulerColorHex, item, mode, axisYPrefix, axisYSuffix, ft, fs, {
      anchorMode: 'center',
    });
    if (!built) return;
    const h = valueToNorm(item.value) * 4;
    const l = getBarLayout('straight', i, h);
    const { mesh, planeW, planeH } = built;
    const yCenter = h + LABEL_ABOVE_GAP + planeW / 2;
    poseVerticalAboveBar(mesh, l.x, yCenter, straightBarZ);
    mesh.userData._frustumHalfW = planeH * 0.55;
    mesh.userData._frustumHalfH = planeW * 0.55 + LABEL_ABOVE_GAP;
    mesh.userData._dataIndex = i;
    mesh.userData._barTopY = h;
    mesh.userData._straightTopLabelMode = mode;
    mesh.userData._afterSized = (m) => {
      m.userData._frustumHalfW = m.userData._planeH * 0.55;
      m.userData._frustumHalfH = m.userData._planeW * 0.55 + LABEL_ABOVE_GAP;
      const yC = m.userData._barTopY + LABEL_ABOVE_GAP + m.userData._planeW / 2;
      poseVerticalAboveBar(m, m.position.x, yC, m.position.z);
    };
    meshes.push(mesh);
    group.add(mesh);
  });

  return { group, meshes };
}

/**
 * Generischer Update-Pfad fuer Top-Labels (straight/stacked-sum).
 * @param {string} modeKey      userData property holding 'headline'|'value'
 * @param {boolean} updateYPos  recalculate position.y from _barTopY
 */
function _updateTopLabelColorsCore(meshes, barLabelColor, THREE, DATA, axisYPrefix, axisYSuffix, dataLabelFontScale, modeKey, updateYPos, waveNode, formatChartNumber) {
  if (!meshes?.length) return;
  const ft = typeof formatChartNumber === 'function' ? formatChartNumber : getFallbackChartNumberFormatter().formatTick;
  const fs = clampDataLabelFontScale(dataLabelFontScale);
  const fontSize = STRAIGHT_XLABEL_PLANE_H * HEADLINE_FONT_RATIO * fs;
  const rulerColorHex = hexForBarLabel(barLabelColor, null);
  meshes.forEach((mesh) => {
    const i = mesh.userData._dataIndex;
    const mode = mesh.userData[modeKey];
    if (i == null || DATA[i] == null || (mode !== 'headline' && mode !== 'value')) return;
    if (mode === 'value' && mesh.isGroup && mesh.userData._tickPair) {
      const parts = getBarValueLabelParts(DATA[i], axisYPrefix, axisYSuffix, ft);
      if (!parts || !parts.numberStr) return;
      updateBarValueSegmentGroup(mesh, parts, rulerColorHex, fontSize);
      return;
    }
    const rawText = rawTextFor(DATA[i], mode, axisYPrefix, axisYSuffix, ft);
    if (!rawText) return;
    updateTroikaLabel(mesh, {
      text: clampTextChars(rawText, 36),
      color: rulerColorHex,
      fontSize,
    });
    /* Frustum-Bounds + Pose werden im _afterSized-Callback aktualisiert (siehe createXxx). */
    /* Für Bar-Top-Labels mit updateYPos=true muss Y nach Größenänderung nachgezogen werden;
     * die per-Frame-Sync (barLabelSync) übernimmt das automatisch über _planeW. */
    void updateYPos;
    void waveNode;
  });
}

export function updateStraightBarTopLabelColors(meshes, barLabelColor, THREE, DATA, axisYPrefix, axisYSuffix, dataLabelFontScale = 1, formatChartNumber) {
  _updateTopLabelColorsCore(meshes, barLabelColor, THREE, DATA, axisYPrefix, axisYSuffix, dataLabelFontScale, '_straightTopLabelMode', true, false, formatChartNumber);
}

export function updateStackedBarSumLabelColors(meshes, barLabelColor, THREE, DATA, axisYPrefix, axisYSuffix, dataLabelFontScale = 1, formatChartNumber) {
  if (!meshes || !meshes.length) return;
  const ft = typeof formatChartNumber === 'function' ? formatChartNumber : getFallbackChartNumberFormatter().formatTick;
  const fs = clampDataLabelFontScale(dataLabelFontScale);
  const fontSize = STRAIGHT_XLABEL_PLANE_H * HEADLINE_FONT_RATIO * fs;
  const rulerColorHex = hexForBarLabel(barLabelColor, null);
  meshes.forEach((mesh) => {
    if (mesh.userData._groupedTopLabelMode !== 'stackedSum') return;
    const indices = mesh.userData._stackedSumMemberIndices;
    if (!indices || !indices.length || !DATA) return;
    const sumRow = stackedTopLabelDataRow(indices, DATA);
    if (!sumRow) return;
    if (mesh.isGroup && mesh.userData._tickPair) {
      const parts = getBarValueLabelParts(sumRow, axisYPrefix, axisYSuffix, ft);
      if (!parts || !parts.numberStr) return;
      updateBarValueSegmentGroup(mesh, parts, rulerColorHex, fontSize);
      return;
    }
    const text = formatBarValueLineForRow(sumRow, axisYPrefix, axisYSuffix, ft);
    if (!text) return;
    updateTroikaLabel(mesh, {
      text: clampTextChars(text, 36),
      color: rulerColorHex,
      fontSize,
    });
  });
}

/* --------------------------------------------------------------------------
 * Utility-Namespace fuer Add-on-Plugins.
 *
 * Add-on-Plugins koennen die internen Helfer + Konstanten via
 * `window.__CE_PD_LABEL_HELPERS__` lesen (kein cross-plugin Import moeglich).
 * ------------------------------------------------------------------------ */
if (typeof window !== 'undefined' && !window.__CE_PD_LABEL_HELPERS__) {
  window.__CE_PD_LABEL_HELPERS__ = {
    /* Konstanten */
    LABEL_ABOVE_GAP,
    HEADLINE_FONT_RATIO,
    STRAIGHT_XLABEL_PLANE_H,
    TICK_PREFIX_UNIT_GAP_FRAC,
    TICK_VALUE_UNIT_GAP_FRAC,
    /* Funktionen */
    resolveFormatChartNumber,
    dataLabelFs,
    hexForBarLabel,
    rawTextFor,
    clampTextChars,
    buildLabelMesh,
    buildBarValueSegmentGroup,
    buildBarTopLabel,
    updateBarValueSegmentGroup,
    makeLabelGroup,
    poseVerticalAboveBar,
    sealBarLabelSubtreeOpacity,
    isEmptyBar,
    formatBarValueLineForRow,
    getBarValueLabelParts,
    layoutRulerTickGroup,
    getRulerTickGroupWidth,
    rulerTickSuffixFontUrl,
    getFallbackChartNumberFormatter,
    createTroikaLabel,
    createOverlayLineMaterial,
    updateTroikaLabel,
    getLabelBounds,
  };
}
