/**
 * Tooltip-Controller: verwaltet DOM-Tooltip, Pick-Cache, Hover-Farb-Targets und Touch-Pinning.
 *
 * updateTooltip() — pro Frame aufgerufen (vom animate-Loop); raycastet nur bei Mausbewegung,
 *                   setzt Hover-Farben je Layout, rendert Tooltip-HTML und positioniert das DIV.
 * hideTooltip()   — faded Tooltip aus.
 * setPinned/clearPinned — Touch: Tooltip bleibt sichtbar bis zum nächsten Tap; Platzierung
 *                   wird beim Pin eingefroren (kein Mitlaufen beim Touch-Ziehen).
 * state.diagramHoldSuppressesTooltip — solange ein auf dem Diagramm gestarteter Pointer gedrückt ist: kein Hover.
 * setLastMouse    — von Pointer-Input: aktuelle Maus-Pixel-Koordinaten für Tooltip-Platzierung.
 * pickCache       — shared mutable object; OrbitControls `change` setzt `mouseX = NaN`, um
 *                   beim nächsten Frame einen frischen Raycast zu erzwingen.
 */
export function createTooltipController(ctx) {
  const {
    tooltip, container, state,
    mouse, lastMouse, pickCache,
    pickAtCurrentMouse, resetAllMeshColors, updateSurfaceHoverHighlight,
    layoutIsNonBar, applyHoverColors,
    bars,
    alwaysColorful,
    enableLinkNavigation = false,
    getCategoryLabel, formatDataPointDisplayValue, escapeHtml,
    scatterAxisXLabel, scatterAxisZLabel,
    i18nDataTableColValue, i18nSource,
  } = ctx;

  let cachedPick = null;
  let _lastTooltipUserData = null;
  let _lastTooltipMouseX = NaN;
  let _lastHoverPickObj = null;
  let _lastHoverPickInstIdx = -2;
  /** Touch: keep tooltip visible until another tap (bar or empty) or tap outside the diagram. */
  let tooltipTouchPinned = false;
  /** @type {{ obj: THREE.Object3D, hovered: THREE.Object3D | { userData: object }, hoveredSphereIndex: number } | null} */
  let pinnedTooltipPick = null;
  /** Touch-Pin: Viewport-Position beim Tap (Tooltip folgt nicht dem gezogenen Finger). */
  let pinnedScreenPos = null;

  function hideTooltip() {
    tooltip.style.opacity = '0';
    tooltip.style.transform = 'translateY(-8px)';
    if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer);
    tooltip._hideTimer = setTimeout(() => { tooltip.style.visibility = 'hidden'; tooltip._hideTimer = null; }, 200);
  }

  function updateTooltip() {
    if (!tooltip) return;
    if (state.diagramHoldSuppressesTooltip) {
      resetAllMeshColors();
      state.hovered = null;
      cachedPick = null;
      _lastTooltipUserData = null;
      _lastTooltipMouseX = NaN;
      _lastHoverPickObj = null;
      _lastHoverPickInstIdx = -2;
      pickCache.mouseX = NaN;
      pickCache.mouseY = NaN;
      if (container && container.style.cursor) container.style.cursor = '';
      hideTooltip();
      /* No-op ohne aktives Surface-Mesh (Add-on-Feature). */
      updateSurfaceHoverHighlight(null);
      return;
    }
    if (!state.mouseOverContainer && !tooltipTouchPinned) {
      resetAllMeshColors();
      state.hovered = null;
      cachedPick = null;
      _lastTooltipUserData = null;
      _lastTooltipMouseX = NaN;
      _lastHoverPickObj = null;
      _lastHoverPickInstIdx = -2;
      pickCache.mouseX = NaN;
      pickCache.mouseY = NaN;
      if (container && container.style.cursor) container.style.cursor = '';
      hideTooltip();
      return;
    }
    let pick = null;
    if (tooltipTouchPinned && pinnedTooltipPick) {
      pick = pinnedTooltipPick;
    } else if (mouse.x === pickCache.mouseX && mouse.y === pickCache.mouseY) {
      pick = cachedPick;
    } else {
      pickCache.mouseX = mouse.x;
      pickCache.mouseY = mouse.y;
      pick = pickAtCurrentMouse();
      cachedPick = pick;
    }
    if (pick) {
      const obj = pick.obj;
      const hoveredSphereIndex = pick.hoveredSphereIndex;
      state.hovered = pick.hovered;
      const pickChanged = obj !== _lastHoverPickObj || pick.hoveredInstanceIndex !== _lastHoverPickInstIdx;

      if (state.hovered) {
        const d = state.hovered.userData;
        if (pickChanged) {
          _lastHoverPickObj = obj;
          _lastHoverPickInstIdx = pick.hoveredInstanceIndex;
          const hbi = pick.hoveredInstanceIndex;
          const isNonBarLayout = layoutIsNonBar();
          bars.forEach((bar, bi) => {
            const ud = bar.meshUserData;
            if (!ud) return;
            if (alwaysColorful) ud._targetColor = ud._defaultColor;
            else if (isNonBarLayout) ud._targetColor = ud.color;
            else ud._targetColor = (bi === hbi) ? ud._defaultColor : ud.color;
          });
          /* Layout-eigene Mesh-Hover-Farben (pie/pieStairs in Free, wave/radial/
           * bubblePack via Add-on); Bars sind oben bereits behandelt. */
          applyHoverColors({ obj, hoveredSphereIndex, hoveredInstanceIndex: pick.hoveredInstanceIndex });
          /* Cursor-Feedback nur bei Pick-Wechsel (nicht pro Frame): Pointer-Cursor
           * über klickbaren Datenpunkten (Link gesetzt + Navigation aktiv). */
          if (container) container.style.cursor = (enableLinkNavigation && d.link) ? 'pointer' : '';
        }
        const barIndex = d._dataIndex ?? d.index ?? 0;
        const categoryLabel = getCategoryLabel(barIndex, d.label);
        const categoryHtml = categoryLabel ? '<div style="font-family:var(--ce-diagram-font-heading);font-weight:700;font-size:1.18em;line-height:1.1;color:#000;margin-top:0.85rem"><strong>' + escapeHtml(categoryLabel) + '</strong></div>' : '';
        /* Free-Default: nur die Wert-Headline. Add-ons koennen via `tooltipSections`
         * eine Wert-Label-Zeile (z. B. Surface) und Zusatzblöcke (Scatter-Grid-Koords)
         * beisteuern. */
        let valueHeadlineHtml = '<strong style="font-family:var(--ce-diagram-font-heading);font-weight:800;font-synthesis:none;font-size:2.39em;color:#222;display:block">' + escapeHtml(formatDataPointDisplayValue(d)) + '</strong>';
        let scatterCoordHtml = '';
        const __ext = (typeof window !== 'undefined' && window.__CE_PD_SCENE_EXT__) || null;
        if (__ext && typeof __ext.tooltipSections === 'function') {
          const sec = __ext.tooltipSections(state.layout, d, {
            escapeHtml,
            formatValue: formatDataPointDisplayValue,
            i18nValueLabel: i18nDataTableColValue,
            axisXLabel: scatterAxisXLabel,
            axisZLabel: scatterAxisZLabel,
          });
          if (sec) {
            if (typeof sec.valueHtml === 'string') valueHeadlineHtml = sec.valueHtml;
            if (typeof sec.extraHtml === 'string') scatterCoordHtml = sec.extraHtml;
          }
        }
        const descHtml = d.desc ? '<div style="font-family:var(--ce-diagram-font-body);font-weight:300;font-size:0.96em;color:#222;margin-top:0.65rem;line-height:1.4">' + escapeHtml(d.desc).replace(/\n/g, '<br>') + '</div>' : '';
        const sourceHtml = d.source ? '<div style="font-family:var(--ce-diagram-font-body);font-weight:300;font-size:0.73em;line-height:1.3;color:#222;margin-top:0.65rem">' + escapeHtml(i18nSource) + ' ' + escapeHtml(d.source) + '</div>' : '';
        const tooltipContentChanged = d !== _lastTooltipUserData;
        if (tooltipContentChanged) {
          _lastTooltipUserData = d;
          tooltip.innerHTML = valueHeadlineHtml + scatterCoordHtml + categoryHtml + descHtml + sourceHtml;
        }
        if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; }
        const posX = pinnedScreenPos ? pinnedScreenPos.x : lastMouse.x;
        const posY = pinnedScreenPos ? pinnedScreenPos.y : lastMouse.y;
        if (tooltipContentChanged || posX !== _lastTooltipMouseX) {
          _lastTooltipMouseX = posX;
          const tipOff = 16;
          const tipW = tooltip.offsetWidth || 320;
          const rect = container.getBoundingClientRect();
          const centerX = rect.left + rect.width / 2;
          const tipLeft = posX > centerX ? posX - tipW - tipOff : posX + tipOff;
          tooltip.style.left = Math.max(8, Math.min(tipLeft, window.innerWidth - tipW - 8)) + 'px';
          tooltip.style.top = (posY + tipOff) + 'px';
        }
        tooltip.style.opacity = '1'; tooltip.style.transform = 'translateY(0)'; tooltip.style.visibility = 'visible';
      }
    } else {
      resetAllMeshColors();
      state.hovered = null;
      _lastHoverPickObj = null;
      _lastHoverPickInstIdx = -2;
      if (container && container.style.cursor) container.style.cursor = '';
      hideTooltip();
    }
    /* No-op ohne aktives Surface-Mesh (Add-on-Feature). */
    updateSurfaceHoverHighlight(pick);
  }

  return {
    updateTooltip,
    hideTooltip,
    setPinned: (p) => {
      tooltipTouchPinned = true;
      pinnedTooltipPick = p;
      pinnedScreenPos = { x: lastMouse.x, y: lastMouse.y };
      _lastTooltipMouseX = NaN;
    },
    clearPinned: () => {
      tooltipTouchPinned = false;
      pinnedTooltipPick = null;
      pinnedScreenPos = null;
      _lastTooltipMouseX = NaN;
    },
    isPinned: () => tooltipTouchPinned,
  };
}
