// Oberleiste (Top-Bar) — MVP nach docs/design/topbar-footer.md.
//
// Gruppen (links → rechts): Marke · Ansichts-Umschalter (Grundriss/Perspektive)
// · Detailgrad (grob/mittel/fein) · Massstab „1:N" + Zoom (Einpassen/Auswahl/
// 100 %) · Render-Modus · Referenzlinien-Toggle · Layout-Menü · Ressourcen.
//
// Die Leiste ist rein gesteuert (controlled): alle Zustände kommen als Props
// aus App, Aktionen melden sich über Callbacks zurück. Bezeichner englisch,
// UI-Text/Kommentare deutsch (CONVENTIONS.md).

import type { Project } from "../model/types";
import { t } from "../i18n";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Dropdown } from "./Dropdown";
import type { DropdownItem, DropdownOption } from "./Dropdown";
import { PromptDialog } from "./PromptDialog";
import { WindowControls } from "./WindowControls";
import { RibbonTabs } from "./ribbon/RibbonBar";
import type { RibbonTabId } from "./ribbon/ribbonItems";
import { getCommand } from "../commands/registry";
import { CommandIcon } from "./ribbon/CommandIcon";
import {
  applyMark,
  applyPreset,
  commonMarkValue,
  DEFAULT_LINE_HEIGHT,
  DEFAULT_PRESETS,
  docLength,
  isMarkActive,
  toggleMark,
} from "../text/richText";
import type {
  Align,
  BoolMark,
  RichTextDoc,
  TextRange,
} from "../text/richText";

/** Ansichtstyp eines Geschosses (wie in App). */
export type ViewType = "grundriss" | "perspektive";

/**
 * Detailgrad der Darstellung (≙ DOSSIER Einfach/Standard/Detail). Wirkt im
 * Grundriss auf Tür-/Fenstersymbole + Schraffuren/Linienfeinheit (siehe
 * generatePlan.DetailLevel); in der Perspektive noch ohne Wirkung (deferred).
 */
export type DetailLevel = "grob" | "mittel" | "fein";

/**
 * Render-/Anzeigemodus der 3D-Ansicht: schattiert, einheitlich weiss (Clay-Look),
 * texturiert (echte PBR-Materialien je Bauteil), Drahtgitter, Verdeckte Kanten
 * (Hidden-Line). Wirkt nur in der Perspektive. Der Slot „section" (Schnitt-
 * schraffur, Welle B) ist bewusst freigelassen.
 */
export type RenderMode =
  | "shaded"
  | "white"
  | "textured"
  | "wireframe"
  | "hidden";

/**
 * Kanonischer Blickwinkel der 3D-Ansicht (Vectorworks/DOSSIER-Stil). „front"/
 * „back"/„side"/„left" springen auf die vier Kardinalrichtungen (Vorne/Hinten/
 * Rechts/Links — KEINE Himmelsrichtungs-Labels, da es noch keinen Nordwinkel
 * gibt); „top" auf die Draufsicht. „iso"/„isoFrontLeft"/„isoBackRight"/
 * „isoBackLeft" sind die vier oberen Iso-Oktanten (untere vier bleiben im
 * Hochbau ungenutzt) — „iso" (vorne-oben-rechts) ist der bestehende Default.
 * „perspective" ist der freie 3/4-Blick. Wirkt nur in der Perspektive.
 */
export type View3d =
  | "front"
  | "back"
  | "side"
  | "left"
  | "top"
  | "iso"
  | "isoFrontLeft"
  | "isoBackRight"
  | "isoBackLeft"
  | "perspective";

/**
 * Farb-Modus des Grundrisses: „color" zeigt Kategorie-/Element-Farben (heutiges
 * Verhalten), „mono" erzeugt einen reinen Schwarz-Weiss-Plan. Wirkt nur im
 * Grundriss (orthogonal zum Linien-Modus Display/Print).
 */
export type PlanColorMode = "color" | "mono";

/**
 * Massstab-Presets „1:N" (Nenner). `null` steht für „frei" (benutzerdefiniert,
 * per Eingabe). Quelle für die Anzeige in Top-Bar und Status-Leiste.
 */
export const SCALE_PRESETS: number[] = [1, 5, 10, 20, 50, 100, 200, 500, 1000];

/**
 * Gemeinsame Breite (px) von Massstab-Dropdown und Zoom-Segmentpille im
 * Zoom-Cluster — beide sollen exakt gleich breit sein (Nutzer-Wunsch).
 */
const ZOOM_CLUSTER_WIDTH = 140;

export interface TopBarProps {
  project: Project;

  // Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) sitzen in DIESER Zeile — der Ribbon-
  // Inhalt darunter (RibbonBar). Der aktive Tab liegt in App (geteilter State).
  ribbonTab: RibbonTabId;
  onRibbonTab: (id: RibbonTabId) => void;

  // Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab
  // gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale
  // Leiste: Marke/Ressourcen · Tabs · Text-Formatierung · Datei/Export/Einstell.

  /** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */
  zoomEnabled: boolean;
  /** Öffnet den PDF-Export-Dialog (nur im Grundriss sinnvoll). */
  onExportPdf: () => void;
  /** Öffnet den DXF-Export-Dialog (nur im Grundriss sinnvoll). */
  onExportDxf: () => void;
  /** Lädt die Bauteilliste des Projekts als CSV herunter. */
  onExportSchedule: () => void;
  /** Lädt das Projekt als IFC4-STEP-Datei herunter. */
  onExportIfc: () => void;
  onExportObj: () => void;
  onExportStl: () => void;

  // Kamera-Settings (FOV), ZUSÄTZLICH zur bestehenden im „Ansichten"-Ribbon-
  // Tab (ViewRibbonTab) und im 3D-Ribbon-Tab (Ribbon3dTab) — hier direkt neben
  // dem CSV-Export in der schmalen Chrome-Zeile, damit sie immer erreichbar
  // ist (Nutzer-Wunsch). Dieselben Felder/Handler wie dort.
  viewType: ViewType;
  fov: number;
  onFov: (n: number) => void;
  /** Presets nur sinnvoll, solange ein Geschoss aktiv ist (wie `viewToggleEnabled`
   *  in {@link ViewRibbonTabProps}). */
  cameraEnabled: boolean;

  // Ressourcen-Fenster.
  resourcesOpen: boolean;
  onToggleResources: () => void;

  /** Öffnet das Einstellungs-Fenster (Darstellungsfarben, Projekt-Felder). */
  onOpenSettings: () => void;

  /** Layout-Menü (von App geliefert, damit dessen Logik dort bleibt). */

  // ── Datei-Menü (Burger, ganz rechts) ───────────────────────────────────────
  /** Öffnet den DXF/DWG-Import-Datei-Dialog. */
  onImport: () => void;
  /** Projekt als Datei speichern (JSON-Export des gesamten Projekts). */
  onSaveProject: () => void;
  /** Projekt aus einer Datei laden. */
  onOpenProject: () => void;

  // Die Text-/Font-Formatierung (TextGroup) liegt jetzt im „Text"-Ribbon-Tab
  // (App reicht sie via RibbonBar.tabContent), nicht mehr in dieser Leiste.
}

/**
 * Beschreibt das aktuell formatierbare Rich-Text-Ziel der Oberleiste. `apply`
 * schreibt das neue Dokument zurück in den App-State (z. B. via setRoomStampDoc).
 */
export interface TextTarget {
  doc: RichTextDoc;
  range: TextRange | null;
  apply: (doc: RichTextDoc) => void;
}

/**
 * Verdrahtung der beiden Kombinations-Dropdowns (Vectorworks-Stil). App liefert
 * die Speicher-/Lade-/Lösch-Logik; die Komponente bleibt rein gesteuert.
 */
export interface ComboMenuHandlers {
  // Ebenen-Kombinationen.
  listLayerCombos: () => string[];
  saveLayerCombo: (name: string) => void;
  loadLayerCombo: (name: string) => void;
  deleteLayerCombo: (name: string) => void;
  // Zeichnungs-Kombinationen.
  listDrawingCombos: () => string[];
  saveDrawingCombo: (name: string) => void;
  loadDrawingCombo: (name: string) => void;
  deleteDrawingCombo: (name: string) => void;
}

/** Kleiner vertikaler Trenner zwischen Gruppen. */
function Sep() {
  return <span className="tb-sep" aria-hidden="true" />;
}

/**
 * Material-Symbols-Icon für die Oberleiste (gleiche Icon-Font wie ContextMenu/
 * MenuIcon). Rein dekorativ — der sprechende Text steckt im `title`/aria-label
 * des umgebenden Buttons.
 */
function TbIcon({ name }: { name: string }) {
  return (
    <span className="material-symbols-outlined tb-ico" aria-hidden="true">
      {name}
    </span>
  );
}

/**
 * Kleines Kombinations-Menü (Vectorworks-Stil) — gespiegelt von `LayoutMenu`:
 * aktuelle Sichtbarkeit unter einem Namen sichern, eine gespeicherte Kombination
 * wählen (laden) oder löschen. Die Namen werden frisch gelesen, sobald das Menü
 * geöffnet wird (onFocus/onMouseDown). Rein gesteuert: alle Aktionen kommen über
 * die Callbacks zurück. Wiederverwendbar für Ebenen- UND Zeichnungskombinationen.
 */
function ComboMenu({
  title,
  savePrompt,
  list,
  onSave,
  onLoad,
  onDelete,
}: {
  /** Tooltip/aria-label des Dropdowns. */
  title: string;
  /** Prompt-Text beim Speichern. */
  savePrompt: string;
  list: () => string[];
  onSave: (name: string) => void;
  onLoad: (name: string) => void;
  onDelete: (name: string) => void;
}) {
  // Namen werden frisch gelesen, sobald das Menü gebaut wird (beim Render des
  // Triggers reicht nicht — wir lesen bei jedem Öffnen über `refresh`). Da das
  // Dropdown gesteuert ist, halten wir die aktuelle Liste im State und
  // aktualisieren sie beim Trigger-Klick (siehe `items`-Aufbau unten).
  const [names, setNames] = useState<string[]>([]);
  // Name-Erfassung über PromptDialog statt window.prompt (im Tauri-WKWebView
  // deaktiviert — liefert sofort null, nichts würde je gespeichert).
  const [saving, setSaving] = useState(false);

  const refresh = () => setNames(list());

  const save = () => setSaving(true);
  const commitSave = (name: string) => {
    onSave(name.trim());
    refresh();
    setSaving(false);
  };

  // Aktions-Menü (analog ContextMenu-Items): „Aktuelle speichern" oben, dann je
  // eine Gruppe „Laden" und „Löschen" (danach Trenner). Das Häkchen entfällt
  // hier (kein „Wert"); Löschen ist als danger-Item markiert.
  const items: DropdownItem[] = [];
  items.push({ label: t("combo.saveCurrent"), onSelect: save });
  if (names.length > 0) {
    items.push({ divider: true });
    for (const n of names) {
      items.push({ label: n, onSelect: () => onLoad(n) });
    }
    items.push({ divider: true });
    for (const n of names) {
      items.push({
        label: `✕ ${n}`,
        danger: true,
        onSelect: () => {
          onDelete(n);
          refresh();
        },
      });
    }
  }

  return (
    <span onPointerDownCapture={refresh}>
      <Dropdown
        items={items}
        placeholder={t("combo.placeholder")}
        title={title}
      />
      {saving && (
        <PromptDialog
          title={title}
          label={savePrompt}
          onConfirm={commitSave}
          onClose={() => setSaving(false)}
        />
      )}
    </span>
  );
}

/**
 * Kamera-Einstellungen als kleines Popover (Vectorworks-Stil): ein FOV-Regler
 * für den Sichtwinkel der Perspektivkamera. Öffnet/schliesst per Button; ein
 * Klick ausserhalb schliesst es. Rein gesteuert über `fov`/`onFov`.
 */
function CameraMenu({
  fov,
  onFov,
  disabled,
}: {
  fov: number;
  onFov: (n: number) => void;
  disabled: boolean;
}) {
  const [open, setOpen] = useState(false);
  // Popover-Position (fixed), aus dem Button-Rect berechnet — die Oberleiste
  // hat overflow:hidden und würde ein absolut positioniertes Popover abschneiden.
  const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const btnRef = useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e: MouseEvent) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  const toggle = () => {
    const next = !open;
    if (next && btnRef.current) {
      const r = btnRef.current.getBoundingClientRect();
      setPos({ left: r.left, top: r.bottom + 6 });
    }
    setOpen(next);
  };

  return (
    <div className="tb-popwrap" ref={wrapRef}>
      <button
        ref={btnRef}
        className={`view-icon${open ? " active" : ""}`}
        disabled={disabled}
        onClick={toggle}
        title={`${t("view3d.camera")} — ${t("view3d.camera.hint")}`}
        aria-label={t("view3d.camera")}
        aria-haspopup="true"
        aria-expanded={open}
      >
        <ViewIcon name="camera" />
      </button>
      {open && !disabled && (
        <div
          className="tb-popover"
          role="dialog"
          aria-label={t("view3d.camera")}
          style={{ left: pos.left, top: pos.top }}
        >
          <label className="tb-poprow">
            <span className="tb-label">
              {t("view3d.fov")}: {Math.round(fov)}°
            </span>
            <input
              type="range"
              min={20}
              max={90}
              step={1}
              value={fov}
              onChange={(e) => onFov(Number(e.target.value))}
              title={t("view3d.fov.hint")}
            />
          </label>
        </div>
      )}
    </div>
  );
}

/** Die vier oberen Iso-Oktanten (untere vier bleiben im Hochbau ungenutzt). */
const ISO_VARIANTS: View3d[] = ["iso", "isoFrontLeft", "isoBackRight", "isoBackLeft"];

/** Häkchen vor der aktiven Iso-Zeile (gleiche Glyphe wie `Dropdown`s Check). */
function IsoCheck() {
  return (
    <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
      <path
        d="M2.5 6.2 L4.7 8.6 L9.5 3"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

/**
 * Iso-Oktanten als Split-Button (Vectorworks-Stil): Klick auf das Würfel-Icon
 * springt direkt auf die zuletzt gewählte Iso-Ansicht (Default „iso" =
 * vorne-oben-rechts, s. View3d); der schmale Pfeil daneben öffnet ein Popover
 * mit allen vier oberen Iso-Oktanten (untere vier bleiben im Hochbau ungenutzt).
 * Popover-Markup/Klassen wie `Dropdown`s Listenmodus (`ctx-menu`/`tb-dd-item`),
 * Positionierung wie `CameraMenu` (fixed, kein Portal — entkommt dem
 * `overflow:hidden` der Oberleiste).
 */
function IsoMenu({
  view3d,
  onView3d,
  viewType,
  onViewType,
  disabled,
}: {
  view3d: View3d;
  onView3d: (v: View3d) => void;
  viewType: ViewType;
  onViewType: (v: ViewType) => void;
  disabled: boolean;
}) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const caretRef = useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e: MouseEvent) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  const current: View3d = ISO_VARIANTS.includes(view3d) ? view3d : "iso";
  const active = viewType === "perspektive" && ISO_VARIANTS.includes(view3d);

  const select = (v: View3d) => {
    onView3d(v);
    if (viewType !== "perspektive") onViewType("perspektive");
    setOpen(false);
  };

  const toggle = () => {
    const next = !open;
    if (next && caretRef.current) {
      const r = caretRef.current.getBoundingClientRect();
      setPos({ left: r.left, top: r.bottom + 6 });
    }
    setOpen(next);
  };

  return (
    <div className="tb-popwrap view-icon-split" ref={wrapRef}>
      <button
        className={`view-icon view-icon-split-main${active ? " active" : ""}`}
        disabled={disabled}
        onClick={() => select(current)}
        title={disabled ? t("topbar.viewGroup.disabled") : `${t(`view3d.${current}`)} — ${t(`view3d.${current}.hint`)}`}
        aria-label={t("view3d.iso")}
      >
        <ViewIcon name="iso" />
      </button>
      <button
        ref={caretRef}
        className={`view-icon-caret${open ? " active" : ""}`}
        disabled={disabled}
        onClick={toggle}
        title={disabled ? t("topbar.viewGroup.disabled") : `${t("view3d.iso.menu")} — ${t("view3d.iso.menu.hint")}`}
        aria-label={t("view3d.iso.menu")}
        aria-haspopup="true"
        aria-expanded={open}
      >
        <svg width="9" height="6" viewBox="0 0 10 6" aria-hidden="true">
          <path d="M0 0l5 6 5-6z" fill="currentColor" />
        </svg>
      </button>
      {open && !disabled && (
        <div
          className="ctx-menu tb-dd-menu"
          role="menu"
          aria-label={t("view3d.iso.menu")}
          style={{ left: pos.left, top: pos.top }}
        >
          {ISO_VARIANTS.map((v) => (
            <button
              key={v}
              type="button"
              role="menuitemradio"
              aria-checked={view3d === v}
              className={`ctx-item tb-dd-item${view3d === v ? " is-active" : ""}`}
              onClick={() => select(v)}
              title={t(`view3d.${v}.hint`)}
            >
              <span className="tb-dd-check-slot">{view3d === v && <IsoCheck />}</span>
              <span className="ctx-item-label">{t(`view3d.${v}`)}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/**
 * Parst eine Massstab-Eingabe „1:35" ODER „35" zum Nenner N (gerundet, > 0).
 * Liefert null bei ungültiger/leerer Eingabe.
 */
function parseScaleInput(input: string | null): number | null {
  if (!input) return null;
  // Teil nach einem ':' nehmen, falls vorhanden („1:35" → „35").
  const raw = input.includes(":") ? input.split(":").pop() ?? "" : input;
  const n = Math.round(Number(raw.trim()));
  return Number.isFinite(n) && n > 0 ? n : null;
}

/**
 * Kleines Vektor-Icon je 3D-Ansicht (16×16, currentColor). Front/Oben/Seite als
 * Quadrat mit Mittellinie (keine/horizontal/vertikal), Perspektive/Isometrie als
 * Würfel, Kamera als Kamera-Glyph. Tooltip nennt die Bedeutung im Klartext.
 */
function ViewIcon({ name }: { name: View3d | "camera" | "grundriss" }) {
  const s = {
    width: 16,
    height: 16,
    viewBox: "0 0 16 16",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.5,
    strokeLinejoin: "round" as const,
    strokeLinecap: "round" as const,
    "aria-hidden": true,
  };
  switch (name) {
    case "grundriss":
      // Grundriss (2D-Plan): Rechteck mit zwei inneren Wandstummeln + Türlücke.
      return (
        <svg {...s}>
          <rect x="2.5" y="2.5" width="11" height="11" />
          <path d="M2.5 9.5 L6 9.5 M9 9.5 L13.5 9.5 M9 9.5 L9 13.5" />
        </svg>
      );
    case "front":
      // Kardinal-Quartett (front/back/side/left): Quadrat + kleiner Steg an der
      // Aussenkante zeigt, von welcher Seite die Kamera blickt (Vorne = unten).
      return (
        <svg {...s}>
          <rect x="3.5" y="3.5" width="9" height="9" />
          <line x1="8" y1="12.5" x2="8" y2="14.3" />
        </svg>
      );
    case "back":
      return (
        <svg {...s}>
          <rect x="3.5" y="3.5" width="9" height="9" />
          <line x1="8" y1="1.7" x2="8" y2="3.5" />
        </svg>
      );
    case "top":
      return (
        <svg {...s}>
          <rect x="3.5" y="3.5" width="9" height="9" />
          <line x1="3.5" y1="8" x2="12.5" y2="8" />
        </svg>
      );
    case "side":
      return (
        <svg {...s}>
          <rect x="3.5" y="3.5" width="9" height="9" />
          <line x1="12.5" y1="8" x2="14.3" y2="8" />
        </svg>
      );
    case "left":
      return (
        <svg {...s}>
          <rect x="3.5" y="3.5" width="9" height="9" />
          <line x1="1.7" y1="8" x2="3.5" y2="8" />
        </svg>
      );
    case "perspective":
      return (
        <svg {...s}>
          <rect x="2.5" y="6" width="7" height="7" />
          <path d="M9.5 6 L13.5 3 M9.5 13 L13.5 10 M2.5 6 L6.5 3 L13.5 3 L13.5 10" />
        </svg>
      );
    case "iso":
      return (
        <svg {...s}>
          <path d="M8 2 L14 5.5 L14 10.5 L8 14 L2 10.5 L2 5.5 Z M8 8 L8 14 M8 8 L14 5.5 M8 8 L2 5.5" />
        </svg>
      );
    case "camera":
      return (
        <svg {...s}>
          <rect x="2.5" y="5.5" width="11" height="7.5" rx="1" />
          <path d="M6 5.5 L7 3.5 L9 3.5 L10 5.5" />
          <circle cx="8" cy="9.2" r="2.1" />
        </svg>
      );
  }
}

/** Eine Zelle einer Segmentpille. */
interface SegmentCell {
  /** Stabiler Schlüssel. */
  key: string;
  /** Material-Symbols-Icon-Name. */
  icon: string;
  /** Tooltip/aria-label (Klartext). */
  title: string;
  /** Aktiver Zustand (Akzentfüllung). */
  active?: boolean;
  onClick: () => void;
  disabled?: boolean;
}

/**
 * Segmentpille (DOSSIER-Look): ein Aussencontainer (radius 999, overflow hidden)
 * mit gleich hohen Zellen; interne Trenner über `border-left`. Aktive Zelle in
 * Akzentfarbe. Genutzt für Zoom-Aktionen, B/I/U und L/C/R. `accent` setzt einen
 * Akzent-Rand am Container (Auswahl-Bewusstsein der Text-Gruppe).
 */
function Segment({
  cells,
  ariaLabel,
  accent,
  style,
}: {
  cells: SegmentCell[];
  ariaLabel: string;
  accent?: boolean;
  style?: React.CSSProperties;
}) {
  return (
    <div
      className={`tb-seg${accent ? " tb-seg-accent" : ""}`}
      role="group"
      aria-label={ariaLabel}
      style={style}
    >
      {cells.map((c) => (
        <button
          key={c.key}
          type="button"
          className={`tb-seg-cell${c.active ? " active" : ""}`}
          onClick={c.onClick}
          disabled={c.disabled}
          aria-pressed={c.active}
          title={c.title}
          aria-label={c.title}
        >
          <span className="material-symbols-outlined tb-ico" aria-hidden="true">
            {c.icon}
          </span>
        </button>
      ))}
    </div>
  );
}

/**
 * Schriftauswahl der Text-Gruppe: 15 kuratierte, lokal gebuendelte
 * Open-Source-Schriften (SIL Open Font License, siehe `src/text/fonts.css`
 * und `public/fonts/<slug>/OFL.txt`). Bewusst KEINE Systemfonts mehr — die
 * Darstellung soll unabhaengig vom Betriebssystem und der PDF-Export
 * deterministisch sein. Ein spaeterer Einstellungen-Dialog kann weitere
 * Schriften nachladen; das hier ist die Standardliste.
 */
const TEXT_FONTS: string[] = [
  "Inter",
  "Work Sans",
  "IBM Plex Sans",
  "IBM Plex Mono",
  "Source Sans 3",
  "Space Grotesk",
  "Manrope",
  "Outfit",
  "DM Sans",
  "Public Sans",
  "Karla",
  "Rubik",
  "Jost",
  "Archivo",
  "Source Serif 4",
];

/** Grössen-Presets (pt) der Text-Gruppe. */
const TEXT_SIZES: number[] = [8, 9, 10, 11, 12, 14, 18, 24, 36, 48];

/**
 * Text-Gruppe der Oberleiste (immer sichtbar, 3×2-Raster). Setzt Defaults für
 * neuen Text (lokaler State) UND formatiert das aktuelle `textTarget` live. Ist
 * `textTarget` gesetzt, tragen die Zeile-2-Pillen einen Akzent-Rand und alle
 * Aktionen wirken auf `textTarget.doc` via `textTarget.apply`.
 */
export function TextGroup({
  textTarget,
}: {
  textTarget: TextTarget | null;
}) {
  // Defaults für neuen Text, solange nichts Text-artiges selektiert ist. Bei
  // aktivem Ziel spiegeln die Controls die gemeinsamen Werte des Ziels wider.
  const [defFont, setDefFont] = useState<string>("Inter");
  const [defSize, setDefSize] = useState<number>(12);
  const [defAlign, setDefAlign] = useState<Align>("left");
  const [defLineHeight, setDefLineHeight] = useState<number>(DEFAULT_LINE_HEIGHT);
  const [defBold, setDefBold] = useState(false);
  const [defItalic, setDefItalic] = useState(false);
  const [defUnderline, setDefUnderline] = useState(false);
  // Freie Grösse über PromptDialog statt window.prompt (Tauri-WKWebView).
  const [sizePromptOpen, setSizePromptOpen] = useState(false);

  // Effektiver Bereich am Ziel: expliziter Bereich oder — ohne Bereich — das
  // ganze Dokument (mind. 1 Zeichen, damit isMarkActive/applyMark greifen).
  const targetRange = (doc: RichTextDoc, range: TextRange | null): TextRange =>
    range && range.start !== range.end
      ? range
      : { start: 0, end: Math.max(1, docLength(doc)) };

  const active = textTarget !== null;

  // Aktueller Anzeige-Zustand: vom Ziel abgeleitet, sonst aus den Defaults.
  const rng = textTarget ? targetRange(textTarget.doc, textTarget.range) : null;
  const isBold = textTarget && rng ? isMarkActive(textTarget.doc, rng, "bold") : defBold;
  const isItalic = textTarget && rng ? isMarkActive(textTarget.doc, rng, "italic") : defItalic;
  const isUnderline =
    textTarget && rng ? isMarkActive(textTarget.doc, rng, "underline") : defUnderline;
  const curFont =
    textTarget && rng ? commonMarkValue(textTarget.doc, rng, "font") ?? "" : defFont;
  const curSize =
    textTarget && rng ? commonMarkValue(textTarget.doc, rng, "sizePt") ?? 0 : defSize;
  // Ausrichtung: gemeinsamer Wert der berührten Absätze, sonst Default.
  const curAlign: Align = textTarget
    ? commonAlign(textTarget.doc, rng) ?? "left"
    : defAlign;
  // Zeilenhöhe: gemeinsamer Wert der berührten Absätze, sonst Default.
  const curLineHeight: number = textTarget
    ? commonLineHeight(textTarget.doc, rng) ?? DEFAULT_LINE_HEIGHT
    : defLineHeight;

  // ── Aktions-Helfer (wirken auf das Ziel oder setzen Defaults) ──────────────
  const toggle = (mark: BoolMark, setDefault: (v: boolean) => void, cur: boolean) => {
    if (textTarget) {
      const r = targetRange(textTarget.doc, textTarget.range);
      textTarget.apply(toggleMark(textTarget.doc, r, mark));
    } else {
      setDefault(!cur);
    }
  };

  const setFontValue = (font: string) => {
    if (textTarget) {
      const r = targetRange(textTarget.doc, textTarget.range);
      textTarget.apply(applyMark(textTarget.doc, r, "font", font));
    } else {
      setDefFont(font);
    }
  };

  const setSizeValue = (size: number) => {
    if (!Number.isFinite(size) || size <= 0) return;
    if (textTarget) {
      const r = targetRange(textTarget.doc, textTarget.range);
      textTarget.apply(applyMark(textTarget.doc, r, "sizePt", size));
    } else {
      setDefSize(size);
    }
  };

  const setAlignValue = (align: Align) => {
    if (textTarget) {
      textTarget.apply(applyAlign(textTarget.doc, textTarget.range, align));
    } else {
      setDefAlign(align);
    }
  };

  const setLineHeightValue = (lh: number) => {
    const clamped = Math.round(Math.min(3, Math.max(0.8, lh)) * 10) / 10;
    if (textTarget) {
      textTarget.apply(applyLineHeight(textTarget.doc, textTarget.range, clamped));
    } else {
      setDefLineHeight(clamped);
    }
  };

  const applyPresetValue = (id: string) => {
    const preset = DEFAULT_PRESETS.find((p) => p.id === id);
    if (!preset) return;
    if (textTarget) {
      const r = textTarget.range ?? { start: 0, end: 0 };
      textTarget.apply(applyPreset(textTarget.doc, r, preset));
    } else {
      // Ohne Ziel: Preset-Defaults übernehmen (Font bleibt, Marks spiegeln).
      if (preset.marks.sizePt != null) setDefSize(preset.marks.sizePt);
      setDefBold(!!preset.marks.bold);
      setDefItalic(!!preset.marks.italic);
    }
  };

  // ── Options-Listen ─────────────────────────────────────────────────────────
  const styleOptions: DropdownOption[] = [
    { value: "__none__", label: t("text.style.none") },
    ...DEFAULT_PRESETS.map((p) => ({ value: p.id, label: p.label })),
  ];
  const fontOptions: DropdownOption[] = TEXT_FONTS.map((f) => ({
    value: f,
    label: f,
  }));
  const sizeInPresets = TEXT_SIZES.includes(curSize);
  const sizeOptions: DropdownOption[] = [
    ...(sizeInPresets || curSize <= 0
      ? []
      : [{ value: "__current__", label: `${curSize} pt` }]),
    ...TEXT_SIZES.map((n) => ({ value: String(n), label: `${n} pt` })),
    { value: "__custom__", label: t("text.size.custom") },
  ];

  const onSizeChange = (value: string) => {
    if (value === "__custom__") {
      setSizePromptOpen(true);
      return;
    }
    if (value === "__current__") return;
    setSizeValue(Number(value));
  };
  const onSizePromptConfirm = (input: string) => {
    const n = Math.round(Number(input.trim()));
    if (Number.isFinite(n) && n > 0) setSizeValue(n);
    setSizePromptOpen(false);
  };

  return (
    <div className="tb-group tb-textgroup" role="group" aria-label={t("text.group")}>
      {/* Zeile 1: Stil-Preset · Schrift · Grösse. */}
      <div className="tb-textgroup-row">
        <Dropdown
          value="__none__"
          onChange={applyPresetValue}
          title={t("text.style")}
          width={110}
          options={styleOptions}
        />
        <Dropdown
          value={curFont || "__none__"}
          onChange={setFontValue}
          title={t("text.font")}
          width={130}
          options={
            curFont && !TEXT_FONTS.includes(curFont)
              ? [{ value: curFont, label: curFont }, ...fontOptions]
              : fontOptions
          }
        />
        <Dropdown
          value={
            curSize <= 0
              ? "__custom__"
              : sizeInPresets
                ? String(curSize)
                : "__current__"
          }
          onChange={onSizeChange}
          title={t("text.size")}
          width={80}
          triggerClassName="tb-dd-mono"
          options={sizeOptions}
        />
      </div>

      {/* Zeile 2: B/I/U · L/C/R · „+ Text". */}
      <div className="tb-textgroup-row">
        <Segment
          ariaLabel={t("text.group")}
          accent={active}
          style={{ width: 110 }}
          cells={[
            {
              key: "bold",
              icon: "format_bold",
              title: t("text.bold"),
              active: !!isBold,
              onClick: () => toggle("bold", setDefBold, defBold),
            },
            {
              key: "italic",
              icon: "format_italic",
              title: t("text.italic"),
              active: !!isItalic,
              onClick: () => toggle("italic", setDefItalic, defItalic),
            },
            {
              key: "underline",
              icon: "format_underlined",
              title: t("text.underline"),
              active: !!isUnderline,
              onClick: () => toggle("underline", setDefUnderline, defUnderline),
            },
          ]}
        />
        <Segment
          ariaLabel={t("text.group")}
          accent={active}
          style={{ width: 130 }}
          cells={[
            {
              key: "left",
              icon: "format_align_left",
              title: t("text.align.left"),
              active: curAlign === "left",
              onClick: () => setAlignValue("left"),
            },
            {
              key: "center",
              icon: "format_align_center",
              title: t("text.align.center"),
              active: curAlign === "center",
              onClick: () => setAlignValue("center"),
            },
            {
              key: "right",
              icon: "format_align_right",
              title: t("text.align.right"),
              active: curAlign === "right",
              onClick: () => setAlignValue("right"),
            },
          ]}
        />
        <div
          className="tb-lineheight"
          role="group"
          aria-label={t("text.lineHeight")}
          title={t("text.lineHeight.hint")}
        >
          <button
            type="button"
            className="tb-lineheight-btn"
            onClick={() => setLineHeightValue(curLineHeight - 0.1)}
            title={t("text.lineHeight.decrease")}
            aria-label={t("text.lineHeight.decrease")}
          >
            <span className="material-symbols-outlined tb-ico" aria-hidden="true">
              remove
            </span>
          </button>
          <span className="tb-lineheight-value">{curLineHeight.toFixed(1)}×</span>
          <button
            type="button"
            className="tb-lineheight-btn"
            onClick={() => setLineHeightValue(curLineHeight + 0.1)}
            title={t("text.lineHeight.increase")}
            aria-label={t("text.lineHeight.increase")}
          >
            <span className="material-symbols-outlined tb-ico" aria-hidden="true">
              add
            </span>
          </button>
        </div>
      </div>
      {sizePromptOpen && (
        <PromptDialog
          title={t("text.size.custom")}
          label={t("text.size.customPrompt")}
          defaultValue={String(curSize > 0 ? curSize : 12)}
          type="number"
          onConfirm={onSizePromptConfirm}
          onClose={() => setSizePromptOpen(false)}
        />
      )}
    </div>
  );
}

/**
 * Gemeinsame Absatz-Ausrichtung der vom Bereich berührten Absätze; `undefined`
 * bei uneinheitlicher/leerer Auswahl. Ohne Bereich → Ausrichtung des ersten
 * Absatzes (das ganze Doc als Ziel).
 */
function commonAlign(doc: RichTextDoc, range: TextRange | null): Align | undefined {
  const paras = touchedParagraphs(doc, range);
  if (paras.length === 0) return doc.paragraphs[0]?.align ?? "left";
  let value: Align | undefined;
  let first = true;
  for (const idx of paras) {
    const a = doc.paragraphs[idx]?.align ?? "left";
    if (first) {
      value = a;
      first = false;
    } else if (a !== value) {
      return undefined;
    }
  }
  return value;
}

/**
 * Setzt die Absatz-Ausrichtung auf alle vom Bereich berührten Absätze. Ohne
 * Bereich (null) auf ALLE Absätze des Dokuments.
 */
function applyAlign(doc: RichTextDoc, range: TextRange | null, align: Align): RichTextDoc {
  const touched = new Set(touchedParagraphs(doc, range));
  const all = range == null;
  return {
    version: 1,
    paragraphs: doc.paragraphs.map((p, i) =>
      all || touched.has(i) ? { ...p, align } : p,
    ),
  };
}

/**
 * Gemeinsame Zeilenhöhe der vom Bereich berührten Absätze (Vielfaches der
 * Basisgrösse); `undefined` bei uneinheitlicher Auswahl.
 */
function commonLineHeight(doc: RichTextDoc, range: TextRange | null): number | undefined {
  const paras = touchedParagraphs(doc, range);
  if (paras.length === 0) return doc.paragraphs[0]?.lineHeight ?? DEFAULT_LINE_HEIGHT;
  let value: number | undefined;
  let first = true;
  for (const idx of paras) {
    const lh = doc.paragraphs[idx]?.lineHeight ?? DEFAULT_LINE_HEIGHT;
    if (first) {
      value = lh;
      first = false;
    } else if (lh !== value) {
      return undefined;
    }
  }
  return value;
}

/**
 * Setzt die Zeilenhöhe auf alle vom Bereich berührten Absätze. Ohne Bereich
 * (null) auf ALLE Absätze des Dokuments.
 */
function applyLineHeight(doc: RichTextDoc, range: TextRange | null, lineHeight: number): RichTextDoc {
  const touched = new Set(touchedParagraphs(doc, range));
  const all = range == null;
  return {
    version: 1,
    paragraphs: doc.paragraphs.map((p, i) =>
      all || touched.has(i) ? { ...p, lineHeight } : p,
    ),
  };
}

/** Indizes der Absätze, die der Bereich [start,end) berührt. */
function touchedParagraphs(doc: RichTextDoc, range: TextRange | null): number[] {
  if (range == null || range.start === range.end) {
    return doc.paragraphs.map((_, i) => i);
  }
  const start = Math.min(range.start, range.end);
  const end = Math.max(range.start, range.end);
  const out: number[] = [];
  let cursor = 0;
  doc.paragraphs.forEach((p, i) => {
    const len = p.runs.reduce((n, r) => n + r.text.length, 0);
    const pStart = cursor;
    const pEnd = cursor + len;
    if (!(pEnd < start || pStart > end)) out.push(i);
    cursor += len + 1;
  });
  return out;
}

/**
 * Datei-Menü der Oberleiste (Burger, ganz rechts): Speichern/Öffnen des
 * Projekts sowie Import (DXF/DWG) und Export (PDF/DXF) — vorher eigene
 * Icon-Pillen im Zoom-Bereich, jetzt gebündelt hinter einem Menü, damit die
 * Leiste ruhiger bleibt (GTK-Burger-Stil).
 */
/** Ein kleiner Icon-Knopf der Quick-Access-Leiste (OCS-Stil). */
function QaButton({
  icon,
  label,
  onClick,
  disabled,
  active,
}: {
  icon: string;
  label: string;
  onClick: () => void;
  disabled?: boolean;
  active?: boolean;
}) {
  return (
    <button
      type="button"
      className={`tb-qa-btn${active ? " active" : ""}`}
      onClick={onClick}
      disabled={disabled}
      aria-pressed={active}
      aria-label={label}
      title={label}
    >
      <TbIcon name={icon} />
    </button>
  );
}

/** Kleiner Trenner in der Quick-Access-Leiste. */
function QaSep() {
  return <span className="tb-qa-sep" aria-hidden="true" />;
}

/**
 * Quick-Access-Leiste (OCS-Stil) — alle früheren Datei-Burger-Aktionen plus
 * Ressourcen/Einstellungen als kleine Icon-Knöpfe direkt in der TopBar-Zeile.
 * Ersetzt das Burger-Menü (`AppMenu`). Rein gesteuert über Callbacks (i18n via t).
 */
/**
 * Kompakte Kamera-Gruppe der Quick-Access-Leiste: NUR das Kamera-Settings-
 * Popover (FOV), damit es immer erreichbar ist, ohne die Ansichts-Presets
 * (die bleiben unverändert im „Ansichten"-Tab und im 3D-Ribbon-Tab).
 */
function QaCameraGroup({
  viewType,
  fov,
  onFov,
  cameraEnabled,
}: {
  viewType: ViewType;
  fov: number;
  onFov: (n: number) => void;
  cameraEnabled: boolean;
}) {
  return (
    <div className="tb-qa-camera" role="toolbar" aria-label={t("topbar.viewGroup")}>
      <CameraMenu
        fov={fov}
        onFov={onFov}
        disabled={!cameraEnabled || viewType !== "perspektive"}
      />
    </div>
  );
}

/** App-Version (spiegelt src-tauri/tauri.conf.json). */
const APP_VERSION = "0.1.0";

/**
 * Export-Sammelmenü: EIN „Export"-Knopf in der Quick-Access-Leiste öffnet ein
 * Dropdown (wie ein Kontextmenü) mit allen Export-Formaten — ersetzt die früher
 * einzeln aufgereihten Export-Icons. Popover fixed positioniert (die Oberleiste
 * hat overflow:hidden), Muster wie {@link CameraMenu}.
 */
function ExportMenu({
  onExportPdf,
  onExportDxf,
  onExportSchedule,
  onExportIfc,
  onExportObj,
  onExportStl,
  exportEnabled,
}: {
  onExportPdf: () => void;
  onExportDxf: () => void;
  onExportSchedule: () => void;
  onExportIfc: () => void;
  onExportObj: () => void;
  onExportStl: () => void;
  exportEnabled: boolean;
}) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const btnRef = useRef<HTMLButtonElement | null>(null);
  // Das Popover hängt per Portal an body (NICHT in wrapRef) — der Outside-Handler
  // muss es daher separat als „innen" behandeln, sonst schliesst der mousedown
  // das Menü vor dem Klick.
  const popRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e: MouseEvent) => {
      const target = e.target as Node;
      if (wrapRef.current?.contains(target)) return;
      if (popRef.current?.contains(target)) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  const toggle = () => {
    const next = !open;
    if (next && btnRef.current) {
      const r = btnRef.current.getBoundingClientRect();
      setPos({ left: r.left, top: r.bottom + 6 });
    }
    setOpen(next);
  };
  const pick = (fn: () => void) => {
    setOpen(false);
    fn();
  };

  const items: { icon: string; label: string; onClick: () => void; disabled?: boolean }[] = [
    { icon: "picture_as_pdf", label: t("file.exportPdf"), onClick: onExportPdf, disabled: !exportEnabled },
    { icon: "output", label: t("file.exportDxf"), onClick: onExportDxf, disabled: !exportEnabled },
    { icon: "table_view", label: t("file.exportSchedule"), onClick: onExportSchedule },
    { icon: "deployed_code", label: t("file.exportIfc"), onClick: onExportIfc },
    { icon: "view_in_ar", label: t("file.exportObj"), onClick: onExportObj },
    { icon: "landscape", label: t("file.exportStl"), onClick: onExportStl },
  ];

  return (
    <div className="tb-popwrap" ref={wrapRef}>
      <button
        ref={btnRef}
        type="button"
        className={`tb-qa-btn${open ? " active" : ""}`}
        onClick={toggle}
        title={t("file.export")}
        aria-label={t("file.export")}
        aria-haspopup="true"
        aria-expanded={open}
      >
        <TbIcon name="ios_share" />
      </button>
      {open &&
        createPortal(
          <div
            ref={popRef}
            className="tb-popover tb-menu"
            role="menu"
            aria-label={t("file.export")}
            style={{ left: pos.left, top: pos.top }}
          >
            {items.map((it) => (
              <button
                key={it.label}
                type="button"
                className="ctx-item"
                role="menuitem"
                disabled={it.disabled}
                onClick={() => pick(it.onClick)}
              >
                <span className="ctx-item-icon">
                  <TbIcon name={it.icon} />
                </span>
                <span className="ctx-item-label">{it.label}</span>
              </button>
            ))}
          </div>,
          document.body,
        )}
    </div>
  );
}

/**
 * „Über"-Dialog (In-App, dunkel) — App-Marke, Version, Kurzbeschreibung und die
 * Lizenzen der wichtigsten Open-Source-Bausteine. Öffnet per Klick auf die
 * Wortmarke. Klick ausserhalb / Esc schliesst.
 */
function AboutDialog({ onClose }: { onClose: () => void }) {
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div className="about-overlay" onMouseDown={onClose}>
      <div
        className="about-dialog"
        role="dialog"
        aria-modal="true"
        aria-label={t("about.title")}
        onMouseDown={(e) => e.stopPropagation()}
      >
        <div className="about-brand">
          dossier<span className="brand-dot">.</span>
        </div>
        <div className="about-version">
          {t("about.version")} {APP_VERSION}
        </div>
        <p className="about-desc">{t("about.desc")}</p>
        <div className="about-meta">
          <div>{t("about.copyright")}</div>
          <div>{t("about.license")}</div>
          <div>{t("about.suite")}</div>
        </div>
        <div className="about-sec-label">{t("about.licenses")}</div>
        <ul className="about-licenses">
          <li>React · Vite — MIT</li>
          <li>Tauri — MIT / Apache-2.0</li>
          <li>wgpu (render3d) — MIT / Apache-2.0</li>
          <li>truck · csgrs — Apache-2.0</li>
          <li>OpenCascade.js — LGPL-2.1</li>
          <li>LibreDWG — GPL-3.0</li>
          <li>acadrust — MPL-2.0</li>
        </ul>
        <button type="button" className="about-close" onClick={onClose}>
          {t("about.close")}
        </button>
      </div>
    </div>
  );
}

function QuickAccess({
  onOpenProject,
  onSaveProject,
  onImport,
  onExportPdf,
  onExportDxf,
  onExportSchedule,
  onExportIfc,
  onExportObj,
  onExportStl,
  onToggleResources,
  onOpenSettings,
  resourcesOpen,
  exportEnabled,
  viewType,
  fov,
  onFov,
  cameraEnabled,
}: {
  onOpenProject: () => void;
  onSaveProject: () => void;
  onImport: () => void;
  onExportPdf: () => void;
  onExportDxf: () => void;
  onExportSchedule: () => void;
  onExportIfc: () => void;
  onExportObj: () => void;
  onExportStl: () => void;
  onToggleResources: () => void;
  onOpenSettings: () => void;
  resourcesOpen: boolean;
  exportEnabled: boolean;
  viewType: ViewType;
  fov: number;
  onFov: (n: number) => void;
  cameraEnabled: boolean;
}) {
  return (
    <div className="tb-qa" role="toolbar" aria-label={t("file.menu")}>
      <QaButton icon="folder_open" label={t("file.open")} onClick={onOpenProject} />
      <QaButton icon="save" label={t("file.save")} onClick={onSaveProject} />
      <QaSep />
      <QaButton icon="download" label={t("file.import")} onClick={onImport} />
      <ExportMenu
        onExportPdf={onExportPdf}
        onExportDxf={onExportDxf}
        onExportSchedule={onExportSchedule}
        onExportIfc={onExportIfc}
        onExportObj={onExportObj}
        onExportStl={onExportStl}
        exportEnabled={exportEnabled}
      />
      <QaSep />
      <QaCameraGroup
        viewType={viewType}
        fov={fov}
        onFov={onFov}
        cameraEnabled={cameraEnabled}
      />
      <QaSep />
      <QaButton
        icon="widgets"
        label={t("topbar.resources")}
        onClick={onToggleResources}
        active={resourcesOpen}
      />
      <QaButton icon="settings" label={t("topbar.settings")} onClick={onOpenSettings} />
    </div>
  );
}

/**
 * Props des „Ansichten"-Tabs — die Ansichts-/Zoom-/Darstellungs-Steuerung, die
 * früher in der Mitte der TopBar sass. Eine Teilmenge von {@link TopBarProps}.
 */
export interface ViewRibbonTabProps {
  viewToggleEnabled: boolean;
  viewType: ViewType;
  onViewType: (v: ViewType) => void;
  view3d: View3d;
  onView3d: (v: View3d) => void;
  detail: DetailLevel;
  onDetail: (d: DetailLevel) => void;
  detailEnabled: boolean;
  scaleDenominator: number;
  onApplyScale: (n: number) => void;
  liveScale: number | null;
  zoomPercent: number;
  zoomEnabled: boolean;
  onFit: () => void;
  onFitSelection: () => void;
  onZoom100: () => void;
  renderMode: RenderMode;
  onRenderMode: (m: RenderMode) => void;
  renderModeEnabled: boolean;
  planColorMode: PlanColorMode;
  onPlanColorMode: (m: PlanColorMode) => void;
  combos: ComboMenuHandlers;
  /** Text-/Font-Formatierung auf DERSELBEN Leiste (Raumstempel-Ziel; `null` =
   *  nur Defaults). Liegt bewusst im „Ansichten"-Tab (Nutzer-Wunsch). */
  textTarget: TextTarget | null;
}

/**
 * „Ansichten"-Tab des Ribbons — trägt die aus der TopBar gemergten Mittelcluster:
 * 2D/3D-Ansichts-Raster (+ Kamera-FOV), Sichtbarkeits-Kombinationen, Detailgrad,
 * Massstab/Zoom-Cluster und Darstellungsart. EIN Ribbon für alles; Handler/Setter
 * kommen unverändert von App (gleiche Props wie zuvor an die TopBar). Wird von
 * `RibbonBar` als Inhalt des „Ansichten"-Tabs gerendert.
 */
export function ViewRibbonTab({
  viewToggleEnabled,
  viewType,
  onViewType,
  view3d,
  onView3d,
  detail,
  onDetail,
  detailEnabled,
  scaleDenominator,
  onApplyScale,
  liveScale,
  zoomPercent,
  zoomEnabled,
  onFit,
  onFitSelection,
  onZoom100,
  renderMode,
  onRenderMode,
  renderModeEnabled,
  planColorMode,
  onPlanColorMode,
  combos,
  textTarget,
}: ViewRibbonTabProps) {
  // „frei" wird über einen In-App-Dialog erfasst (window.prompt ist im Tauri-
  // WKWebView deaktiviert); akzeptiert „1:35" ODER „35".
  const [scalePromptOpen, setScalePromptOpen] = useState(false);
  const onScaleChange = (value: string) => {
    if (value === "__custom__") {
      setScalePromptOpen(true);
      return;
    }
    const n = Number(value);
    if (Number.isFinite(n) && n > 0) onApplyScale(n);
  };
  const onScalePromptConfirm = (input: string) => {
    const n = parseScaleInput(input);
    if (n != null) onApplyScale(n);
    setScalePromptOpen(false);
  };
  const inPresets = SCALE_PRESETS.includes(scaleDenominator);
  const scaleOptions: DropdownOption[] = [
    ...(inPresets
      ? []
      : [{ value: "__current__", label: `1:${scaleDenominator}` }]),
    ...SCALE_PRESETS.map((n) => ({ value: String(n), label: `1:${n}` })),
    { value: "__custom__", label: t("topbar.scale.custom") },
  ];

  return (
    <div className="ribbon-views">
      {/* Ansichten als EIN kompaktes Icon-Grid (2D-Grundriss + 3D-Blicke). */}
      <div className="tb-group" role="group" aria-label={t("topbar.viewGroup")}>
        <div className="view-grid view-grid-4">
          <button
            className={`view-icon${
              viewToggleEnabled && viewType === "grundriss" ? " active" : ""
            }`}
            onClick={() => onViewType("grundriss")}
            disabled={!viewToggleEnabled}
            title={
              viewToggleEnabled
                ? t("topbar.grundriss")
                : t("topbar.viewGroup.disabled")
            }
            aria-label={t("topbar.grundriss")}
          >
            <ViewIcon name="grundriss" />
          </button>
          {(
            [
              ["top", t("view3d.top")],
              ["iso", t("view3d.iso")],
              ["perspective", t("view3d.perspective")],
              ["front", t("view3d.front")],
              ["side", t("view3d.side")],
              ["back", t("view3d.back")],
              ["left", t("view3d.left")],
            ] as [View3d, string][]
          ).map(([key, label]) =>
            key === "iso" ? (
              <IsoMenu
                key={key}
                view3d={view3d}
                onView3d={onView3d}
                viewType={viewType}
                onViewType={onViewType}
                disabled={!viewToggleEnabled}
              />
            ) : (
              <button
                key={key}
                className={`view-icon${
                  viewToggleEnabled && viewType === "perspektive" && view3d === key
                    ? " active"
                    : ""
                }`}
                onClick={() => {
                  onView3d(key);
                  if (viewType !== "perspektive") onViewType("perspektive");
                }}
                disabled={!viewToggleEnabled}
                title={
                  viewToggleEnabled
                    ? `${label} — ${t(`view3d.${key}.hint`)}`
                    : t("topbar.viewGroup.disabled")
                }
                aria-label={label}
              >
                <ViewIcon name={key} />
              </button>
            ),
          )}
        </div>
      </div>
      <Sep />

      {/* Sichtbarkeits-Kombinationen (Ebenen-/Zeichnungskombinationen). */}
      <div className="tb-group tb-stack" role="group" aria-label={t("combo.layers.title")}>
        <label className="tb-field tb-field-row">
          <span className="tb-label">{t("combo.layers")}</span>
          <ComboMenu
            title={t("combo.layers.title")}
            savePrompt={t("combo.layers.savePrompt")}
            list={combos.listLayerCombos}
            onSave={combos.saveLayerCombo}
            onLoad={combos.loadLayerCombo}
            onDelete={combos.deleteLayerCombo}
          />
        </label>
        <label className="tb-field tb-field-row">
          <span className="tb-label">{t("combo.drawings")}</span>
          <ComboMenu
            title={t("combo.drawings.title")}
            savePrompt={t("combo.drawings.savePrompt")}
            list={combos.listDrawingCombos}
            onSave={combos.saveDrawingCombo}
            onLoad={combos.loadDrawingCombo}
            onDelete={combos.deleteDrawingCombo}
          />
        </label>
      </div>
      <Sep />

      {/* Detailgrad (grob/mittel/fein) + Darstellungsart übereinander gestapelt
          (Nutzer-Wunsch) — gleiches Stapel-Muster wie die Kombinationen. */}
      <div className="tb-group tb-stack" role="group" aria-label={t("topbar.detailGroup")}>
        <label className="tb-field tb-field-row">
          <span className="tb-label">{t("topbar.detail")}</span>
          <Dropdown
            value={detail}
            disabled={!detailEnabled}
            onChange={(v) => onDetail(v as DetailLevel)}
            title={
              detailEnabled
                ? t("topbar.detail.hint")
                : t("topbar.detail.hintDisabled")
            }
            options={[
              { value: "grob", label: t("topbar.detail.grob") },
              { value: "mittel", label: t("topbar.detail.mittel") },
              { value: "fein", label: t("topbar.detail.fein") },
            ]}
          />
        </label>
        <label className="tb-field tb-field-row">
          <span className="tb-label">{t("display.style")}</span>
          {viewType === "perspektive" ? (
            <Dropdown
              value={renderMode}
              disabled={!renderModeEnabled}
              onChange={(v) => onRenderMode(v as RenderMode)}
              title={t("display.style.hint")}
              options={[
                { value: "shaded", label: t("display.style.shaded") },
                { value: "white", label: t("display.style.white") },
                { value: "textured", label: t("display.style.textured") },
                { value: "wireframe", label: t("display.style.wireframe") },
                { value: "hidden", label: t("display.style.hidden") },
              ]}
            />
          ) : (
            <Dropdown
              value={planColorMode}
              onChange={(v) => onPlanColorMode(v as PlanColorMode)}
              title={t("display.style.hint")}
              options={[
                { value: "color", label: t("display.style.color") },
                { value: "mono", label: t("display.style.mono") },
              ]}
            />
          )}
        </label>
      </div>
      <Sep />

      {/* Massstab/Zoom-Cluster (2×2). */}
      <div className="tb-group tb-zoomcluster" role="group" aria-label={t("topbar.scaleGroup")}>
        <div
          className={`tb-stat${
            zoomEnabled && liveScale != null && Math.round(liveScale) === scaleDenominator
              ? " tb-stat-atscale"
              : ""
          }`}
          title={`${t("topbar.scale.live")} · ${t("topbar.zoom.current")}`}
        >
          <span className="tb-stat-scale">
            {zoomEnabled && liveScale != null ? `1:${Math.round(liveScale)}` : "—"}
          </span>
          <span className="tb-stat-zoom">
            {zoomEnabled ? `${Math.round(zoomPercent)}%` : "—"}
          </span>
        </div>
        <div className="tb-zoomcluster-right">
          <Dropdown
            value={inPresets ? String(scaleDenominator) : "__current__"}
            disabled={!zoomEnabled}
            onChange={onScaleChange}
            title={t("topbar.scale.apply")}
            triggerClassName="tb-dd-mono"
            width={ZOOM_CLUSTER_WIDTH}
            options={scaleOptions}
          />
          <Segment
            ariaLabel={t("topbar.scaleGroup")}
            style={{ width: ZOOM_CLUSTER_WIDTH }}
            cells={[
              {
                key: "zoom100",
                icon: "straighten",
                title: t("topbar.zoom100.hint", { n: scaleDenominator }),
                onClick: onZoom100,
                disabled: !zoomEnabled,
              },
              {
                key: "fit",
                icon: "fit_screen",
                title: t("topbar.fit.hint"),
                onClick: onFit,
                disabled: !zoomEnabled,
              },
              {
                key: "fitSelection",
                icon: "center_focus_strong",
                title: t("topbar.fitSelection.hint"),
                onClick: onFitSelection,
                disabled: !zoomEnabled,
              },
            ]}
          />
        </div>
      </div>
      <Sep />


      {/* Text-/Font-Formatierung auf DERSELBEN Leiste (Nutzer-Wunsch: Text und
          Ansichten zusammen) — Stil/Schrift/Grösse + B/I/U · L/C/R + Zeilenhöhe,
          formatiert das aktuelle Text-Ziel (Raumstempel) live. */}
      <TextGroup textTarget={textTarget} />
      {scalePromptOpen && (
        <PromptDialog
          title={t("topbar.scale.custom")}
          label={t("topbar.scale.prompt")}
          defaultValue={`1:${scaleDenominator}`}
          onConfirm={onScalePromptConfirm}
          onClose={() => setScalePromptOpen(false)}
        />
      )}
    </div>
  );
}

export interface Ribbon3dTabProps {
  viewToggleEnabled: boolean;
  viewType: ViewType;
  onViewType: (v: ViewType) => void;
  view3d: View3d;
  onView3d: (v: View3d) => void;
  renderMode: RenderMode;
  onRenderMode: (m: RenderMode) => void;
  renderModeEnabled: boolean;
  onRunCommand: (name: string) => void;
  activeCommand: string | null;
}

/** 3D-Tab des Ribbons — Kamera-Presets + Darstellungsart. Kein Duplikat des
 *  Ansichten-Tabs: hier nur 3D-relevante Steuerung (Blickwinkel, Rendermodus). */
export function Ribbon3dTab({
  viewToggleEnabled,
  viewType,
  onViewType,
  view3d,
  onView3d,
  renderMode,
  onRenderMode,
  renderModeEnabled,
  onRunCommand,
  activeCommand,
}: Ribbon3dTabProps) {
  return (
    <div className="ribbon-views">
      <div className="tb-group" role="group" aria-label={t("topbar.viewGroup")}>
        <div className="view-grid view-grid-4">
          <button
            className={`view-icon${viewToggleEnabled && viewType === "grundriss" ? " active" : ""}`}
            onClick={() => onViewType("grundriss")}
            disabled={!viewToggleEnabled}
            title={viewToggleEnabled ? t("topbar.grundriss") : t("topbar.viewGroup.disabled")}
            aria-label={t("topbar.grundriss")}
          >
            <ViewIcon name="grundriss" />
          </button>
          {(
            [
              ["top", t("view3d.top")],
              ["iso", t("view3d.iso")],
              ["perspective", t("view3d.perspective")],
              ["front", t("view3d.front")],
              ["side", t("view3d.side")],
              ["back", t("view3d.back")],
              ["left", t("view3d.left")],
            ] as [View3d, string][]
          ).map(([key, label]) =>
            key === "iso" ? (
              <IsoMenu
                key={key}
                view3d={view3d}
                onView3d={onView3d}
                viewType={viewType}
                onViewType={onViewType}
                disabled={!viewToggleEnabled}
              />
            ) : (
              <button
                key={key}
                className={`view-icon${viewToggleEnabled && viewType === "perspektive" && view3d === key ? " active" : ""}`}
                onClick={() => {
                  onView3d(key);
                  if (viewType !== "perspektive") onViewType("perspektive");
                }}
                disabled={!viewToggleEnabled}
                title={viewToggleEnabled ? `${label} — ${t(`view3d.${key}.hint`)}` : t("topbar.viewGroup.disabled")}
                aria-label={label}
              >
                <ViewIcon name={key} />
              </button>
            ),
          )}
        </div>
      </div>
      <Sep />
      <div className="tb-group" role="group" aria-label={t("display.style.group")}>
        <label className="tb-field">
          <span className="tb-label">{t("display.style")}</span>
          <Dropdown
            value={renderMode}
            disabled={!renderModeEnabled}
            onChange={(v) => onRenderMode(v as RenderMode)}
            title={t("display.style.hint")}
            options={[
              { value: "shaded", label: t("display.style.shaded") },
              { value: "white", label: t("display.style.white") },
              { value: "textured", label: t("display.style.textured") },
              { value: "wireframe", label: t("display.style.wireframe") },
              { value: "hidden", label: t("display.style.hidden") },
            ]}
          />
        </label>
      </div>
      <Sep />
      <div className="tb-group" role="group" aria-label={t("cmd.extrude.label")}>
        {(() => {
          const cmd = getCommand("extrude");
          const label = cmd ? t(cmd.labelKey) : "extrude";
          const active = activeCommand === "extrude";
          return (
            <button
              type="button"
              className={`ribbon-btn${active ? " active" : ""}`}
              disabled={!viewToggleEnabled}
              title={viewToggleEnabled ? label : t("tool.floorOnlyDisabled")}
              onClick={() => onRunCommand("extrude")}
            >
              <CommandIcon name="extrude" />
              <span>{label}</span>
            </button>
          );
        })()}
      </div>
    </div>
  );
}

export function TopBar({
  project,
  ribbonTab,
  onRibbonTab,
  zoomEnabled,
  onExportPdf,
  onExportDxf,
  onExportSchedule,
  onExportIfc,
  onExportObj,
  onExportStl,
  viewType,
  fov,
  onFov,
  cameraEnabled,
  resourcesOpen,
  onToggleResources,
  onOpenSettings,
  onImport,
  onSaveProject,
  onOpenProject,
}: TopBarProps) {
  // Dezente Scrollleiste: die Oberleiste scrollt horizontal (overflow-x). Statt
  // der prägnanten Standard-Leiste blenden wir eine eigene, sehr ruhige Leiste
  // NUR während des Scrollens ein (Klasse `is-scrolling`, ~900 ms nachlaufend).
  const headerRef = useRef<HTMLElement | null>(null);
  useEffect(() => {
    const el = headerRef.current;
    if (!el) return;
    let timer: ReturnType<typeof setTimeout> | undefined;
    const onScroll = () => {
      el.classList.add("is-scrolling");
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => el.classList.remove("is-scrolling"), 900);
    };
    el.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      el.removeEventListener("scroll", onScroll);
      if (timer) clearTimeout(timer);
    };
  }, []);

  // Unter Tauri (native macOS-Ampeln oben links via titleBarStyle "Overlay")
  // braucht die Zeile links Platz, damit `dossier.` nicht darunter liegt.
  const [aboutOpen, setAboutOpen] = useState(false);

  const tauriNativeTitlebar =
    typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;

  return (
    // `data-tauri-drag-region`: die Titelleiste ist selbst der Fenster-Ziehbereich
    // (macOS titleBarStyle "Overlay"). Kind-Buttons/Dropdowns bleiben klickbar;
    // nur die leeren Zonen ziehen das Fenster. Unter Tauri bekommt die Zeile links
    // einen Inset, damit `dossier.` nicht unter den nativen macOS-Ampeln liegt.
    <header
      className={`topbar${tauriNativeTitlebar ? " topbar-native-inset" : ""}`}
      ref={headerRef}
      data-tauri-drag-region
    >
      {/* Fensterknöpfe NUR unter Electron (randloses frame:false-Fenster) — unter
          Tauri liefern die NATIVEN macOS-Ampeln Schliessen/Minimieren/Maximieren,
          daher rendert WindowControls dort null. */}
      <WindowControls />

      {/* Kleine Wortmarke „dossier" (OCS-Stil, klein/kleingeschrieben) + Quick-
          Access-Leiste: alle Datei-/Export-Aktionen + Ressourcen/Einstellungen
          als kleine Icons direkt in der Zeile (ersetzt das Burger-Menü). */}
      <div className="tb-brandgroup">
        <button
          type="button"
          className="brand-word"
          onClick={() => setAboutOpen(true)}
          title={t("about.title")}
          aria-label={t("about.title")}
        >
          dossier<span className="brand-dot">.</span>
        </button>
      </div>
      {aboutOpen && <AboutDialog onClose={() => setAboutOpen(false)} />}
      <QuickAccess
        onOpenProject={onOpenProject}
        onSaveProject={onSaveProject}
        onImport={onImport}
        onExportPdf={onExportPdf}
        onExportDxf={onExportDxf}
        onExportSchedule={onExportSchedule}
        onExportIfc={onExportIfc}
        onExportObj={onExportObj}
        onExportStl={onExportStl}
        onToggleResources={onToggleResources}
        onOpenSettings={onOpenSettings}
        resourcesOpen={resourcesOpen}
        exportEnabled={zoomEnabled}
        viewType={viewType}
        fov={fov}
        onFov={onFov}
        cameraEnabled={cameraEnabled}
      />

      <Sep />

      {/* Ribbon-Tab-Reiter (2D/3D/BIM/Text/Ansichten) — sitzen in dieser Zeile,
          der Ribbon-Inhalt folgt darunter (RibbonBar). So gibt es nur ZWEI
          Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */}
      <RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />

      {/* Rechte Gruppe: Projektname. Die Fensterknöpfe sitzen ganz links (macOS-
          Titelleisten-Konvention); Datei-/Export-Aktionen in der Quick-Access-
          Leiste; die Arbeitsumgebung (Layout) liegt jetzt in den Einstellungen. */}
      <div className="topbar-right">
        <span className="project-name">{project.name}</span>
      </div>
    </header>
  );
}

// Hinweis: Die früheren Helfer SNAP_TOGGLES + SnapMenu (Fang-Optionen) und die
// Zeichenwerkzeug-Buttons wurden aus der Oberleiste entfernt; sie leben jetzt
// in der Werkzeug-Palette (src/panels/ToolsPanel.tsx).
