import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import xml.etree.ElementTree as ET
import fitz  # PyMuPDF
import math
import os
import statistics

# ==========================================
# 1. FEINTUNING (Der "kleine Versatz" für das Label)
NUDGE_X = 3.0
NUDGE_Y = 3.0

# 2. ZOOM FEINTUNING (Zentrierung beim Klick)
ZOOM_FACTOR = 2.5       # 1.5 = 150% Zoom (0.0 = Zoom des Users beibehalten)
ZOOM_OFFSET_X = 0.0    # Wie weit links vom Bauteil soll der Bildschirmrand sein?
ZOOM_OFFSET_Y = 0.0    # Wie weit über dem Bauteil soll der Bildschirmrand sein?

# 3. INFO-BOXEN FEINTUNING (Zentriert sich ab jetzt exakt auf die Bauteilmitte)
INFO_OFFSET_X = -10.0    # Verschiebt die Info-Box von der exakten Bauteilmitte nach rechts/links
INFO_OFFSET_Y = -10.0    # Verschiebt die Info-Box von der exakten Bauteilmitte nach oben/unten

# 4. XREF: SLAVE EINSTELLUNGEN
XREF_SLAVE_OFFSET_BOTTOM_Y = 22.0
XREF_SLAVE_OFFSET_RIGHT_X = 26.0
XREF_SLAVE_OFFSET_LEFT_X = -28.0
XREF_SLAVE_OFFSET_TOP_Y = -21.0

# 5. XREF: MASTER EINSTELLUNGEN KONTAKTKAMM UNTER LABEL (snapto="label")
XREF_MASTER_OFFSET_X = 28.0
XREF_MASTER_NO_NC_STEP_Y = 10.0
XREF_MASTER_NO_NC_OFFSET_Y_START = 15.0

XREF_MASTER_SW_OFFSET_Y_EXTRA = 19.0
XREF_MASTER_SW_STEP_Y = 20.0

# "OTHER" / ANDERS Kontakte (Die Trennung von den Commutatoren!)
XREF_MASTER_OTHER_OFFSET_X = 28.0
XREF_MASTER_OTHER_OFFSET_Y_EXTRA = 10.0
XREF_MASTER_OTHER_STEP_Y = 10.0

# Commutatoren / Motorschutzschalter
XREF_COMMUTATOR_OFFSET_X = 28.0
XREF_COMMUTATOR_OFFSET_Y_START = 10.0
XREF_COMMUTATOR_STEP_Y = 10.0

# 6. XREF: CROSS EINSTELLUNGEN (Kreuzdarstellung displayhas="cross") UNTER DEM LABEL
XREF_CROSS_NC_OFFSET_X = 15.0  # Öffner (unter Label)
XREF_CROSS_NO_OFFSET_X = -15.0 # Schließer (unter Label)
XREF_CROSS_OFFSET_Y_START = 21.0  # Start-Versatz für die gesamte Tabelle UNTER DEM LABEL
XREF_CROSS_STEP_Y = 11.0        # Zeilenabstand innerhalb der Tabellenspalten

# ------------------------------------------
# 7. NEU: MASTER EINSTELLUNGEN FÜR CROSS TABELLEN AM FOLIENENDE (snapto="bottom")
# ------------------------------------------
# Dynamischer Startpunkt je nach Tabellengröße
XREF_BOTTOM_CROSS_OFFSET_Y_START_SMALL = -12.0  # <-- Start-Versatz bei 1 oder 2 Elementen
XREF_BOTTOM_CROSS_OFFSET_Y_START_LARGE = -23.0  # <-- Start-Versatz bei 3 oder mehr Elementen

XREF_BOTTOM_CROSS_NC_OFFSET_X = 11.0  # Öffner (am Folienende)
XREF_BOTTOM_CROSS_NO_OFFSET_X = -19.0 # Schließer (am Folienende)
XREF_BOTTOM_CROSS_STEP_Y = 11.0       # Zeilenabstand (am Folienende)
XREF_BOTTOM_CROSS_MAX_ROWS_DOWN = 3   # Ab wie vielen Elementen wächst die Tabelle nach OBEN? (Der Fahrstuhl-Effekt)

# ------------------------------------------
# 8. XREF: MASTER EINSTELLUNGEN FÜR SYMBOLE AM FOLIENENDE (snapto="bottom")
# ------------------------------------------
XREF_BOTTOM_Y_NUDGE = 0.0
XREF_BOTTOM_X_NUDGE = 0.0
XREF_BOTTOM_OFFSET_TWEAK = 1.0
XREF_BOTTOM_OFFSET_SCALE = 1.44

# *** LOGIK-SCHALTER ***
XREF_BOTTOM_REVERSE_LIST = True
XREF_BOTTOM_STEP_DIR = -1
# ****************************************************************

XREF_BOTTOM_NO_NC_OFFSET_X = 25.0
XREF_BOTTOM_NO_NC_OFFSET_Y_EXTRA = -6.0
XREF_BOTTOM_NO_NC_STEP_Y = 10.0

XREF_BOTTOM_SW_OFFSET_X = 25.0
XREF_BOTTOM_SW_OFFSET_Y_EXTRA = 3.0
XREF_BOTTOM_SW_STEP_Y = 20.0

XREF_BOTTOM_OTHER_OFFSET_X = 25.0
XREF_BOTTOM_OTHER_OFFSET_Y_EXTRA = 8.0
XREF_BOTTOM_OTHER_STEP_Y = 10.0

# ==========================================
# SPRACH-ÜBERSETZUNGEN (GUI & INFO-BOXEN)
# ==========================================
GUI_TEXTS = {
    "Deutsch": {
        "qet_label": "1. QET Projektdatei:",
        "pdf_label": "2. PDF Datei:",
        "btn_browse": "Suche",
        "frame_opts": "Optionen",
        "chk_debug": "Rote Rahmen zur Kontrolle einzeichnen",
        "chk_info": "Bauteilinformationen (Info-Boxen) eintragen",
        "chk_terminals": "Klemmen von Info-Boxen ausschließen",
        "chk_icons": "Info-Symbole sichtbar machen (für Debug)",
        "btn_generate": "Links & Infos generieren",
        "msg_start_calib": "Starte optische Kalibrierung...",
        "msg_done": "FERTIG!\n- {} dynamische Links erstellt.",
        "msg_done_info": "\n- {} Bauteil-Info-Boxen gesetzt.",
        "msg_success_title": "Erfolg",
        "msg_success_text": "Datei gespeichert:\n{}\n\n{}",
        "msg_error_title": "Fehler",
        "msg_err_calib": "FEHLER: Konnte keine Kalibrierungspunkte finden."
    },
    "English": {
        "qet_label": "1. QET Project file:",
        "pdf_label": "2. PDF File:",
        "btn_browse": "Browse",
        "frame_opts": "Options",
        "chk_debug": "Draw red frames for debugging",
        "chk_info": "Insert component info (Info-Boxes)",
        "chk_terminals": "Exclude terminals from Info-Boxes",
        "chk_icons": "Make info symbols visible (for debug)",
        "btn_generate": "Generate Links & Infos",
        "msg_start_calib": "Starting optical calibration...",
        "msg_done": "DONE!\n- {} dynamic links created.",
        "msg_done_info": "\n- {} component Info-Boxes inserted.",
        "msg_success_title": "Success",
        "msg_success_text": "File saved:\n{}\n\n{}",
        "msg_error_title": "Error",
        "msg_err_calib": "ERROR: Could not find any calibration points."
    }
}

INFO_TRANSLATIONS = {
    "Deutsch": {
        'label': 'BMK', 'plant': 'Anlage', 'location': 'Ort', 'comment': 'Kommentar',
        'function': 'Funktion', 'description': 'Artikelbeschreibung', 'designation': 'Artikelnummer',
        'manufacturer': 'Hersteller', 'manufacturer_reference': 'Bestellnummer',
        'machine_manufacturer_reference': 'Interne Nummer', 'supplier': 'Lieferant',
        'quantity': 'Menge', 'unity': 'Einheit',
        'position': 'Position', 'title': 'Titel', 'folio': 'Foliennummer', 'diagram_position': 'Seite'
    },
    "English": {
        'label': 'Label', 'plant': 'Plant', 'location': 'Location', 'comment': 'Comment',
        'function': 'Function', 'description': 'Description', 'designation': 'Part number',
        'manufacturer': 'Manufacturer', 'manufacturer_reference': 'Order number',
        'machine_manufacturer_reference': 'Internal number', 'supplier': 'Supplier',
        'quantity': 'Quantity', 'unity': 'Unit',
        'position': 'Position', 'title': 'Title', 'folio': 'Folio', 'diagram_position': 'Page'
    }
}

for i in range(1, 5):
    INFO_TRANSLATIONS["Deutsch"].update({
        f'auxiliary{i}': f'Zusatzinfo Zusatzartikel {i}',
        f'description_auxiliary{i}': f'Artikelbeschreibung Zusatzartikel {i}',
        f'designation_auxiliary{i}': f'Artikelnummer Zusatzartikel {i}',
        f'manufacturer_auxiliary{i}': f'Hersteller Zusatzartikel {i}',
        f'manufacturer_reference_auxiliary{i}': f'Bestellnummer Zusatzartikel {i}',
        f'machine_manufacturer_reference_auxiliary{i}': f'Interne Nummer Zusatzartikel {i}',
        f'supplier_auxiliary{i}': f'Lieferant Zusatzartikel {i}',
        f'quantity_auxiliary{i}': f'Menge Zusatzartikel {i}',
        f'unity_auxiliary{i}': f'Einheit Zusatzartikel {i}'
    })
    INFO_TRANSLATIONS["English"].update({
        f'auxiliary{i}': f'Auxiliary info {i}',
        f'description_auxiliary{i}': f'Description {i}',
        f'designation_auxiliary{i}': f'Part number {i}',
        f'manufacturer_auxiliary{i}': f'Manufacturer {i}',
        f'manufacturer_reference_auxiliary{i}': f'Order number {i}',
        f'machine_manufacturer_reference_auxiliary{i}': f'Internal number {i}',
        f'supplier_auxiliary{i}': f'Supplier {i}',
        f'quantity_auxiliary{i}': f'Quantity {i}',
        f'unity_auxiliary{i}': f'Unit {i}'
    })

def try_float(v, default=0.0):
    if v is None: return default
    try: return float(str(v).replace(',', '.'))
    except: return default

def norm_uuid(u):
    if not u: return None
    return u.strip('{} ').lower()

class QETSmartLinker:
    def __init__(self, root):
        self.root = root
        self.root.title("QET Xref-Linker v0.51 (Decoupled Cross Columns)")
        self.root.geometry("700x650")

        top_frm = ttk.Frame(root)
        top_frm.pack(fill=tk.X, padx=20, pady=(10, 0))
        self.lang_var = tk.StringVar(value="Deutsch")
        self.lang_combo = ttk.Combobox(top_frm, textvariable=self.lang_var, values=["Deutsch", "English"], state="readonly", width=10)
        self.lang_combo.pack(side=tk.RIGHT)
        self.lang_combo.bind("<<ComboboxSelected>>", self.update_language)

        frm = ttk.Frame(root, padding="20")
        frm.pack(fill=tk.BOTH, expand=True)

        self.lbl_qet = ttk.Label(frm, text="")
        self.lbl_qet.pack(anchor="w")
        self.ent_qet = ttk.Entry(frm, width=50)
        self.ent_qet.pack(fill="x", pady=5)
        self.btn_qet_browse = ttk.Button(frm, text="", command=self.browse_qet)
        self.btn_qet_browse.pack(anchor="e")

        self.lbl_pdf = ttk.Label(frm, text="")
        self.lbl_pdf.pack(anchor="w", pady=(10,0))
        self.ent_pdf = ttk.Entry(frm, width=50)
        self.ent_pdf.pack(fill="x", pady=5)
        self.btn_pdf_browse = ttk.Button(frm, text="", command=self.browse_pdf)
        self.btn_pdf_browse.pack(anchor="e")

        self.frame_opts = ttk.LabelFrame(frm, text="", padding="10")
        self.frame_opts.pack(fill="x", pady=15)

        self.var_debug = tk.BooleanVar(value=True)
        self.chk_debug_btn = ttk.Checkbutton(self.frame_opts, text="", variable=self.var_debug)
        self.chk_debug_btn.pack(anchor="w")

        self.var_info = tk.BooleanVar(value=False)
        self.chk_info = ttk.Checkbutton(self.frame_opts, text="", variable=self.var_info, command=self.toggle_info_options)
        self.chk_info.pack(anchor="w", pady=(10, 0))

        self.var_klemmen = tk.BooleanVar(value=False)
        self.chk_klemmen = ttk.Checkbutton(self.frame_opts, text="", variable=self.var_klemmen, state=tk.DISABLED)
        self.chk_klemmen.pack(anchor="w", padx=20)

        self.var_info_visible = tk.BooleanVar(value=True)
        self.chk_info_visible = ttk.Checkbutton(self.frame_opts, text="", variable=self.var_info_visible, state=tk.DISABLED)
        self.chk_info_visible.pack(anchor="w", padx=20)

        self.btn_generate = ttk.Button(frm, text="", command=self.process)
        self.btn_generate.pack(fill="x", ipady=5, pady=10)

        self.status = tk.Text(frm, height=12, font=("Consolas", 8))
        self.status.pack(fill=tk.BOTH, expand=True)

        self.update_language()

    def update_language(self, event=None):
        t = GUI_TEXTS[self.lang_var.get()]
        self.lbl_qet.config(text=t["qet_label"])
        self.btn_qet_browse.config(text=t["btn_browse"])
        self.lbl_pdf.config(text=t["pdf_label"])
        self.btn_pdf_browse.config(text=t["btn_browse"])
        self.frame_opts.config(text=t["frame_opts"])
        self.chk_debug_btn.config(text=t["chk_debug"])
        self.chk_info.config(text=t["chk_info"])
        self.chk_klemmen.config(text=t["chk_terminals"])
        self.chk_info_visible.config(text=t["chk_icons"])
        self.btn_generate.config(text=t["btn_generate"])

    def toggle_info_options(self):
        if self.var_info.get():
            self.chk_klemmen.config(state=tk.NORMAL)
            self.chk_info_visible.config(state=tk.NORMAL)
        else:
            self.chk_klemmen.config(state=tk.DISABLED)
            self.chk_info_visible.config(state=tk.DISABLED)
            self.var_klemmen.set(False)

    def log(self, msg):
        self.status.insert(tk.END, msg + "\n")
        self.status.see(tk.END)
        self.root.update()

    def browse_qet(self):
        f = filedialog.askopenfilename(filetypes=[("QET", "*.qet")])
        if f: self.ent_qet.delete(0, tk.END); self.ent_qet.insert(0, f)

    def browse_pdf(self):
        f = filedialog.askopenfilename(filetypes=[("PDF", "*.pdf")])
        if f: self.ent_pdf.delete(0, tk.END); self.ent_pdf.insert(0, f)

    def calculate_calibration(self, anchors_x, anchors_y):
        if len(anchors_x) < 2: return None, None, None, None
        scales_x = [(anchors_x[i][1]-anchors_x[j][1])/(anchors_x[i][0]-anchors_x[j][0]) for i in range(len(anchors_x)) for j in range(i+1, len(anchors_x)) if abs(anchors_x[i][0]-anchors_x[j][0]) > 50]
        scales_y = [(anchors_y[i][1]-anchors_y[j][1])/(anchors_y[i][0]-anchors_y[j][0]) for i in range(len(anchors_y)) for j in range(i+1, len(anchors_y)) if abs(anchors_y[i][0]-anchors_y[j][0]) > 50]
        if not scales_x or not scales_y: return None, None, None, None

        sc_x = statistics.median(scales_x)
        sc_y = statistics.median(scales_y)
        off_x = statistics.median([p - (q * sc_x) for q, p in anchors_x])
        off_y = statistics.median([p - (q * sc_y) for q, p in anchors_y])
        return sc_x, sc_y, off_x, off_y

    def get_calibrations(self, doc, diagrams):
        self.log(GUI_TEXTS[self.lang_var.get()]["msg_start_calib"])
        global_anchors_x, global_anchors_y = [], []
        page_anchors = {}

        for d_idx, diag in enumerate(diagrams):
            try: page_num = int(diag.get('order', d_idx + 1)) - 1
            except: page_num = d_idx
            if page_num >= len(doc) or page_num < 0: continue
            page = doc[page_num]

            p_anchors_x, p_anchors_y = [], []
            for el in diag.findall('.//element'):
                orient_step = 0.0
                ori_val = str(el.get('orientation', '0')).lower()
                if ori_val.isdigit(): orient_step = float(ori_val)
                elif ori_val == 'e': orient_step = 1.0
                elif ori_val == 's': orient_step = 2.0
                elif ori_val == 'w': orient_step = 3.0
                angle_deg = orient_step * 90.0

                for dt in el.findall('.//dynamic_text') + el.findall('.//dynamic_elmt_text'):
                    txt_node = dt.find('text')
                    if txt_node is not None and txt_node.text and len(txt_node.text.strip()) > 1:
                        val = txt_node.text.strip()
                        text_rot = try_float(dt.get('rotation'), 0.0)
                        total_rot = (angle_deg + text_rot) % 360

                        if total_rot != 0.0 or '\n' in val or '\r' in val:
                            continue

                        tx = try_float(dt.get('x'))
                        ty = try_float(dt.get('y'))
                        if angle_deg != 0:
                            rad = math.radians(angle_deg)
                            rx = tx * math.cos(rad) - ty * math.sin(rad)
                            ry = tx * math.sin(rad) + ty * math.cos(rad)
                            tx, ty = rx, ry

                        qx = try_float(el.get('x')) + tx
                        qy = try_float(el.get('y')) + ty

                        rects = page.search_for(val)
                        if len(rects) == 1:
                            p_anchors_x.append((qx, rects[0].x0))
                            p_anchors_y.append((qy, rects[0].y0))
                            global_anchors_x.append((qx, rects[0].x0))
                            global_anchors_y.append((qy, rects[0].y0))

            page_anchors[page_num] = {'x': p_anchors_x, 'y': p_anchors_y}

        g_sc_x, g_sc_y, g_off_x, g_off_y = self.calculate_calibration(global_anchors_x, global_anchors_y)
        if g_sc_x is None:
            return None, (None, None, None, None)

        page_calibrations = {}
        for p_num, anchors in page_anchors.items():
            sc_x, sc_y, off_x, off_y = self.calculate_calibration(anchors['x'], anchors['y'])
            if sc_x is not None:
                page_calibrations[p_num] = (sc_x, sc_y, off_x, off_y)
            else:
                page_calibrations[p_num] = (g_sc_x, g_sc_y, g_off_x, g_off_y)

        return page_calibrations, (g_sc_x, g_sc_y, g_off_x, g_off_y)

    def get_best_label(self, element):
        best_dt = None
        for tag in ['.//dynamic_text', './/dynamic_elmt_text']:
            for dt in element.findall(tag):
                info = dt.find('info_name')
                if info is not None and info.text == 'label':
                    tf = dt.get('text_from')
                    if tf == 'ElementInfo': return dt
                    elif tf == 'UserText' and (best_dt is None or best_dt.get('text_from') != 'UserText'): best_dt = dt
                    elif best_dt is None: best_dt = dt
        return best_dt

    def parse_xrefs_config(self, root_xml):
        xref_config = {}
        for xref in root_xml.findall('.//xrefs/xref'):
            t = xref.get('type')
            if t:
                xref_config[t] = {
                    'pos': xref.get('xrefpos', 'AlignBottom'),
                    'offset': try_float(xref.get('offset'), 40.0),
                    'snapto': xref.get('snapto', 'label'),
                    'displayhas': xref.get('displayhas', 'contacts')
                }
        return xref_config

    def build_link_type_map(self, root_xml):
        l_types = {}
        def traverse(node, current_path):
            for child in node:
                if child.tag == 'category':
                    traverse(child, current_path + child.get('name', '') + '/')
                elif child.tag == 'element':
                    name = child.get('name', '')
                    full_path = current_path + name
                    df = child.find('definition')
                    if df is not None:
                        l_type = df.get('link_type', 'simple')
                        k_type = 'simple'
                        c_count = 1
                        c_state = 'NO'

                        width = try_float(df.get('width'), 0.0)
                        height = try_float(df.get('height'), 0.0)
                        hotspot_x = try_float(df.get('hotspot_x'), 0.0)
                        hotspot_y = try_float(df.get('hotspot_y'), 0.0)

                        for ki in df.findall('.//kindInformation'):
                            n = ki.get('name')
                            if n == 'type' and ki.text: k_type = ki.text.strip().lower()
                            if n == 'number' and ki.text:
                                try: c_count = int(ki.text.strip())
                                except: pass
                            if n == 'state' and ki.text: c_state = ki.text.strip().upper()

                        info = {
                            'link_type': l_type, 'kind_type': k_type, 'contact_count': c_count, 'state': c_state,
                            'width': width, 'height': height, 'hotspot_x': hotspot_x, 'hotspot_y': hotspot_y
                        }

                        l_types[full_path] = info
                        if name not in l_types:
                            l_types[name] = info

        collection = root_xml.find('.//collection')
        if collection is not None:
            traverse(collection, "")

        return l_types

    def scan_local_table_layout(self, page, anchor_x, anchor_y, def_start_pdf, def_step_pdf, step_dir, label_text):
        words = page.get_text("words")
        y_centers = []

        for w in words:
            cx = (w[0] + w[2]) / 2
            cy = (w[1] + w[3]) / 2

            if abs(cx - anchor_x) < 60:
                if step_dir == 1:
                    if anchor_y + 5 < cy < anchor_y + 300:
                        y_centers.append(cy)
                else:
                    if anchor_y - 300 < cy < anchor_y - 5:
                        y_centers.append(cy)

        if not y_centers:
            return def_start_pdf, def_step_pdf

        y_centers.sort()
        if step_dir == -1:
            y_centers.reverse()

        rows = []
        current_cluster = [y_centers[0]]
        for y in y_centers[1:]:
            if abs(y - current_cluster[0]) < 4.0:
                current_cluster.append(y)
            else:
                rows.append(statistics.median(current_cluster))
                current_cluster = [y]
        if current_cluster:
            rows.append(statistics.median(current_cluster))

        dyn_start_pdf = abs(rows[0] - anchor_y)

        if abs(dyn_start_pdf - def_start_pdf) > 25.0:
            dyn_start_pdf = def_start_pdf

        dyn_step_pdf = def_step_pdf
        if len(rows) >= 2:
            steps = [abs(rows[i] - rows[i-1]) for i in range(1, len(rows))]
            measured_step = statistics.median(steps)

            if 4.0 < measured_step < 25.0:
                dyn_step_pdf = measured_step

                if abs(dyn_step_pdf - def_step_pdf) > 0.2:
                    self.log(f"  --> Scanner (Tabelle {label_text}): Quetschung! Step von {def_step_pdf:.1f} auf {dyn_step_pdf:.1f} korrigiert.")

        return dyn_start_pdf, dyn_step_pdf


    def process(self):
        if not self.ent_qet.get() or not self.ent_pdf.get(): return

        try:
            self.status.delete(1.0, tk.END)
            tree = ET.parse(self.ent_qet.get())
            root_xml = tree.getroot()
            doc = fitz.open(self.ent_pdf.get())

            xref_config = self.parse_xrefs_config(root_xml)
            link_type_map = self.build_link_type_map(root_xml)
            element_map = {norm_uuid(el.get('uuid') or el.get('id')): el for el in root_xml.findall('.//element')}
            diagrams = root_xml.findall('.//diagram')

            page_calibrations, global_calib = self.get_calibrations(doc, diagrams)
            if global_calib[0] is None:
                err_title = GUI_TEXTS.get(self.lang_var.get(), GUI_TEXTS["Deutsch"])["msg_error_title"]
                messagebox.showerror(err_title, GUI_TEXTS[self.lang_var.get()]["msg_err_calib"])
                return

            links_count = 0
            info_count = 0

            current_lang = self.lang_var.get()
            gui_trans = GUI_TEXTS[current_lang]
            info_trans = INFO_TRANSLATIONS[current_lang]

            for d_idx, diag in enumerate(diagrams):
                try: page_num = int(diag.get('order', d_idx + 1)) - 1
                except: page_num = d_idx
                if page_num >= len(doc) or page_num < 0: continue
                page = doc[page_num]

                sc_x, sc_y, off_x, off_y = page_calibrations.get(page_num, global_calib)

                rows = try_float(diag.get('rows'), 0.0)
                rowsize = try_float(diag.get('rowsize'), 0.0)

                if rows == 0.0 or rowsize == 0.0:
                    border = root_xml.find('.//newdiagrams/border')
                    if border is not None:
                        rows = try_float(border.get('rows'), 6.0)
                        rowsize = try_float(border.get('rowsize'), 120.0)
                    else:
                        rows, rowsize = 6.0, 120.0

                qet_bottom_y_raw = rows * rowsize

                for el in diag.findall('.//element'):
                    el_type_raw = el.get('type', '')
                    el_path = el_type_raw.replace('embed://', '').replace('qet://', '')
                    el_name = el_type_raw.split('/')[-1]

                    if el_path in link_type_map:
                        type_info = link_type_map[el_path]
                    else:
                        type_info = link_type_map.get(el_name, {'link_type': 'simple', 'kind_type': 'coil'})

                    l_type = type_info['link_type']
                    k_type = type_info['kind_type']

                    best_dt = self.get_best_label(el)
                    tx, ty = 0.0, 0.0
                    label_text = None

                    if best_dt is not None:
                        tx, ty = try_float(best_dt.get('x')), try_float(best_dt.get('y'))
                        t_node = best_dt.find('text')
                        if t_node is not None and t_node.text:
                            label_text = t_node.text.strip()

                    orient_step = try_float(el.get('orientation'), 0.0)
                    angle_deg = orient_step * 90.0

                    if angle_deg != 0:
                        rad = math.radians(angle_deg)
                        rx = tx * math.cos(rad) - ty * math.sin(rad)
                        ry = tx * math.sin(rad) + ty * math.cos(rad)
                        tx, ty = rx, ry

                    m_width = try_float(type_info.get('width', 0.0))
                    m_height = try_float(type_info.get('height', 0.0))
                    m_hx = try_float(type_info.get('hotspot_x', 0.0))
                    m_hy = try_float(type_info.get('hotspot_y', 0.0))

                    cx_unrot = (m_width / 2.0) - m_hx
                    cy_unrot = (m_height / 2.0) - m_hy

                    m_rad = math.radians(angle_deg)
                    center_offset_x = cx_unrot * math.cos(m_rad) - cy_unrot * math.sin(m_rad)
                    center_offset_y = cx_unrot * math.sin(m_rad) + cy_unrot * math.cos(m_rad)

                    base_pdf_x_raw = off_x + (try_float(el.get('x')) + center_offset_x) * sc_x

                    pdf_x = off_x + (try_float(el.get('x')) + tx) * sc_x + NUDGE_X
                    pdf_y = off_y + (try_float(el.get('y')) + ty) * sc_y + NUDGE_Y

                    if label_text:
                        found_rects = page.search_for(label_text)
                        if found_rects:
                            best_rect = None
                            min_dist = float('inf')
                            for r in found_rects:
                                cx, cy = (r.x0 + r.x1)/2, (r.y0 + r.y1)/2
                                dist = math.hypot(cx - pdf_x, cy - pdf_y)
                                if dist < 80 and dist < min_dist:
                                    min_dist = dist
                                    best_rect = r
                            if best_rect:
                                pdf_x = (best_rect.x0 + best_rect.x1) / 2
                                pdf_y = (best_rect.y0 + best_rect.y1) / 2

                    if self.var_info.get():
                        is_arrow = l_type in ['next_report', 'previous_report']
                        is_slave = (l_type == 'slave')
                        is_terminal = (l_type == 'terminal')
                        skip_terminal = is_terminal and self.var_klemmen.get()

                        if not (is_arrow or is_slave or skip_terminal):
                            info_data = {}
                            for e_info in el.findall('.//elementInformations/elementInformation'):
                                name = e_info.get('name')
                                val = e_info.text
                                if name == 'formula': continue
                                if name and val and str(e_info.get('show', '1')) == '1':
                                    info_data[name] = val.strip()

                            if info_data:
                                info_lines = []
                                for key in ['label', 'manufacturer', 'designation', 'description']:
                                    if key in info_data:
                                        info_lines.append(f"{info_trans.get(key, key)}: {info_data.pop(key)}")
                                for key, val in info_data.items():
                                    info_lines.append(f"{info_trans.get(key, key)}: {val}")

                                info_text = "\n".join(info_lines)
                                target_x = try_float(el.get('x')) + center_offset_x
                                target_y = try_float(el.get('y')) + center_offset_y
                                base_pdf_x_info = off_x + (target_x * sc_x) + INFO_OFFSET_X
                                base_pdf_y_info = off_y + (target_y * sc_y) + INFO_OFFSET_Y

                                annot_pt = fitz.Point(base_pdf_x_info, base_pdf_y_info)
                                annot = page.add_text_annot(annot_pt, info_text)
                                annot.set_info(title=str(info_data.get('label', 'Bauteil-Info')))
                                if not self.var_info_visible.get(): annot.set_opacity(0.0)
                                annot.update()
                                info_count += 1

                    text_rot = try_float(best_dt.get('rotation'), 0.0) if best_dt is not None else 0.0
                    total_rot = (angle_deg + text_rot) % 180
                    is_rotated = (total_rot == 90.0)
                    w_off = 3.0 if is_rotated else 6.0
                    h_off = 6.0 if is_rotated else 3.0

                    tuids = [norm_uuid(l.get('uuid')) for l in el.findall('.//link_uuid') if l.get('uuid')]
                    if not tuids: continue

                    anchor_x = pdf_x
                    anchor_y = pdf_y
                    step_dir = 1
                    current_y_offset = 0.0
                    display_has = 'contacts'

                    cross_y_nc = 0.0
                    cross_y_no = 0.0
                    c_step_y = 0.0

                    if l_type == 'master':
                        m_config = xref_config.get(k_type, {'offset': 40.0, 'snapto': 'label', 'displayhas': 'contacts'})
                        snap_to = m_config.get('snapto', 'label')
                        display_has = m_config.get('displayhas', 'contacts')
                        element_offset = m_config.get('offset', 40.0)

                        el_xref = el.find('.//xrefs/xref')
                        if el_xref is not None:
                            snap_to = el_xref.get('snapto', snap_to)
                            display_has = el_xref.get('displayhas', display_has)
                            element_offset = try_float(el_xref.get('offset'), element_offset)

                        if snap_to == 'bottom':
                            anchor_x = base_pdf_x_raw + XREF_BOTTOM_X_NUDGE
                            base_anchor_y = off_y + (qet_bottom_y_raw * sc_y) + XREF_BOTTOM_Y_NUDGE
                            delta_offset = element_offset - 40.0
                            jump_pdf = delta_offset * XREF_BOTTOM_OFFSET_SCALE * sc_y
                            anchor_y = base_anchor_y - jump_pdf
                            step_dir = XREF_BOTTOM_STEP_DIR
                            current_y_offset = 0.0
                            if XREF_BOTTOM_REVERSE_LIST: tuids = tuids[::-1]
                        else:
                            anchor_x = pdf_x
                            anchor_y = pdf_y
                            step_dir = 1
                            current_y_offset = 0.0

                        if display_has == 'cross':
                            if snap_to == 'bottom':
                                step_dir = 1
                                if XREF_BOTTOM_REVERSE_LIST:
                                    tuids = tuids[::-1]
                                c_step_y = XREF_BOTTOM_CROSS_STEP_Y

                                total_elements = len(tuids)

                                if total_elements <= 2:
                                    current_y_offset += XREF_BOTTOM_CROSS_OFFSET_Y_START_SMALL
                                else:
                                    current_y_offset += XREF_BOTTOM_CROSS_OFFSET_Y_START_LARGE

                                if total_elements > XREF_BOTTOM_CROSS_MAX_ROWS_DOWN:
                                    shift_up_px = (total_elements - XREF_BOTTOM_CROSS_MAX_ROWS_DOWN) * c_step_y * sc_y
                                    anchor_y -= shift_up_px

                                    if self.var_debug.get():
                                        lname = label_text if label_text else "Unbekannt"
                                        self.log(f"  --> Tabelle am Boden ({lname}): {total_elements} Elemente! Startpunkt um {(total_elements - XREF_BOTTOM_CROSS_MAX_ROWS_DOWN)} Stufen nach oben korrigiert.")

                            else:
                                def_start_qet = XREF_CROSS_OFFSET_Y_START
                                def_step_qet  = XREF_CROSS_STEP_Y

                                def_start_pdf = def_start_qet * sc_y
                                def_step_pdf = def_step_qet * sc_y
                                dyn_start_pdf, dyn_step_pdf = self.scan_local_table_layout(
                                    page, anchor_x, anchor_y, def_start_pdf, def_step_pdf, step_dir, label_text
                                )
                                def_start_qet = dyn_start_pdf / sc_y
                                c_step_y = dyn_step_pdf / sc_y

                                current_y_offset += def_start_qet

                            cross_y_nc = current_y_offset
                            cross_y_no = current_y_offset

                    for t_uid in tuids:
                        target_el = element_map.get(t_uid)
                        if target_el is None: continue

                        tp_num = -1
                        t_pdf_x, t_pdf_y = 0.0, 0.0
                        t_sc_x, t_sc_y = sc_x, sc_y

                        for td_idx, tdiag in enumerate(diagrams):
                            if target_el in tdiag.findall('.//element'):
                                try: tp_num = int(tdiag.get('order', td_idx + 1)) - 1
                                except: tp_num = td_idx
                                t_sc_x, t_sc_y, t_off_x, t_off_y = page_calibrations.get(tp_num, global_calib)
                                t_pdf_x = t_off_x + try_float(target_el.get('x')) * t_sc_x
                                t_pdf_y = t_off_y + try_float(target_el.get('y')) * t_sc_y
                                break

                        if tp_num == -1: continue

                        if ZOOM_OFFSET_X == 0.0 and ZOOM_OFFSET_Y == 0.0 and ZOOM_FACTOR > 0:
                            smart_offset_x = 800.0 / ZOOM_FACTOR
                            smart_offset_y = 450.0 / ZOOM_FACTOR
                            target_view_x = max(0, t_pdf_x - smart_offset_x)
                            target_view_y = max(0, t_pdf_y - smart_offset_y)
                        else:
                            target_view_x = max(0, t_pdf_x - (ZOOM_OFFSET_X * t_sc_x))
                            target_view_y = max(0, t_pdf_y - (ZOOM_OFFSET_Y * t_sc_y))

                        link_dest = {
                            'kind': fitz.LINK_GOTO,
                            'page': tp_num,
                            'to': fitz.Point(target_view_x, target_view_y),
                            'zoom': ZOOM_FACTOR
                        }

                        t_el_type_raw = target_el.get('type', '')
                        t_el_path_child = t_el_type_raw.replace('embed://', '').replace('qet://', '')
                        t_el_name = t_el_type_raw.split('/')[-1]

                        if t_el_path_child in link_type_map:
                            t_type_info = link_type_map[t_el_path_child]
                        else:
                            t_type_info = link_type_map.get(t_el_name, {'contact_count': 1, 'state': 'NO', 'kind_type': 'coil'})

                        c_count = t_type_info['contact_count']
                        t_state = t_type_info['state']
                        target_k_type = t_type_info['kind_type']

                        if l_type == 'master' and display_has == 'cross':
                            c_count = 1

                        if l_type in ['next_report', 'previous_report']:
                            rect = fitz.Rect(pdf_x - w_off, pdf_y - h_off, pdf_x + w_off, pdf_y + h_off)
                            link_dest['from'] = rect
                            page.insert_link(link_dest)
                            if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                            links_count += 1

                        elif l_type == 'slave':
                            target_config = xref_config.get(target_k_type, {'pos': 'AlignBottom', 'offset': 40.0})
                            if target_config['pos'] == 'AlignRight':
                                rx = pdf_x + (XREF_SLAVE_OFFSET_RIGHT_X * sc_x)
                                ry = pdf_y
                            elif target_config['pos'] == 'AlignLeft':
                                rx = pdf_x + (XREF_SLAVE_OFFSET_LEFT_X * sc_x)
                                ry = pdf_y
                            elif target_config['pos'] == 'AlignTop':
                                rx = pdf_x
                                ry = pdf_y + (XREF_SLAVE_OFFSET_TOP_Y * sc_y)
                            else:
                                rx = pdf_x
                                ry = pdf_y + (XREF_SLAVE_OFFSET_BOTTOM_Y * sc_y)
                            rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                            link_dest['from'] = rect
                            page.insert_link(link_dest)
                            if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                            links_count += 1

                        elif l_type == 'master':
                            if display_has == 'cross':
                                if snap_to == 'bottom':
                                    c_nc_off_x = XREF_BOTTOM_CROSS_NC_OFFSET_X
                                    c_no_off_x = XREF_BOTTOM_CROSS_NO_OFFSET_X
                                else:
                                    c_nc_off_x = XREF_CROSS_NC_OFFSET_X
                                    c_no_off_x = XREF_CROSS_NO_OFFSET_X

                                if t_state == 'NC':
                                    for _ in range(c_count):
                                        rx = anchor_x + (c_nc_off_x * sc_x)
                                        ry = anchor_y + (cross_y_nc * sc_y * step_dir)
                                        rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                        link_dest['from'] = rect
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect, color=(0,0,1), width=0.5)
                                        links_count += 1
                                        cross_y_nc += c_step_y
                                elif t_state == 'NO':
                                    for _ in range(c_count):
                                        rx = anchor_x + (c_no_off_x * sc_x)
                                        ry = anchor_y + (cross_y_no * sc_y * step_dir)
                                        rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                        link_dest['from'] = rect
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect, color=(0,0,1), width=0.5)
                                        links_count += 1
                                        cross_y_no += c_step_y
                                elif t_state == 'SW':
                                    for _ in range(c_count):
                                        # ========================================================
                                        # BUGFIX V0.51: Entkoppelte Spalten für Wechsler (SW)
                                        # ========================================================

                                        # 1. Öffner (NC) in das nächste freie Öffner-Fach
                                        rx_nc = anchor_x + (c_nc_off_x * sc_x)
                                        ry_nc = anchor_y + (cross_y_nc * sc_y * step_dir)
                                        rect_nc = fitz.Rect(rx_nc - 6, ry_nc - 3, rx_nc + 6, ry_nc + 3)
                                        link_dest['from'] = rect_nc
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect_nc, color=(0,0,1), width=0.5)
                                        links_count += 1

                                        # 2. Schließer (NO) in das nächste freie Schließer-Fach
                                        rx_no = anchor_x + (c_no_off_x * sc_x)
                                        ry_no = anchor_y + (cross_y_no * sc_y * step_dir)
                                        rect_no = fitz.Rect(rx_no - 6, ry_no - 3, rx_no + 6, ry_no + 3)
                                        link_dest_no = link_dest.copy()
                                        link_dest_no['from'] = rect_no
                                        page.insert_link(link_dest_no)
                                        if self.var_debug.get(): page.draw_rect(rect_no, color=(0,0,1), width=0.5)
                                        links_count += 1

                                        # Beide Spalten unabhängig voneinander hochzählen!
                                        cross_y_nc += c_step_y
                                        cross_y_no += c_step_y
                                        # ========================================================
                            else:
                                if k_type == 'commutator':
                                    x_off   = XREF_BOTTOM_OTHER_OFFSET_X if snap_to == 'bottom' else XREF_COMMUTATOR_OFFSET_X
                                    step_y  = XREF_BOTTOM_OTHER_STEP_Y if snap_to == 'bottom' else XREF_COMMUTATOR_STEP_Y
                                    extra_y = XREF_BOTTOM_OTHER_OFFSET_Y_EXTRA if snap_to == 'bottom' else XREF_COMMUTATOR_OFFSET_Y_START
                                    for _ in range(c_count):
                                        rx = anchor_x + (x_off * sc_x)
                                        ry = anchor_y + ((current_y_offset + extra_y) * sc_y * step_dir)
                                        rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                        link_dest['from'] = rect
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                                        links_count += 1
                                        current_y_offset += step_y
                                elif t_state == 'OTHER':
                                    x_off   = XREF_BOTTOM_OTHER_OFFSET_X if snap_to == 'bottom' else XREF_MASTER_OTHER_OFFSET_X
                                    step_y  = XREF_BOTTOM_OTHER_STEP_Y if snap_to == 'bottom' else XREF_MASTER_OTHER_STEP_Y
                                    extra_y = XREF_BOTTOM_OTHER_OFFSET_Y_EXTRA if snap_to == 'bottom' else XREF_MASTER_OTHER_OFFSET_Y_EXTRA
                                    for _ in range(c_count):
                                        rx = anchor_x + (x_off * sc_x)
                                        ry = anchor_y + ((current_y_offset + extra_y) * sc_y * step_dir)
                                        rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                        link_dest['from'] = rect
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                                        links_count += 1
                                        current_y_offset += step_y
                                elif t_state == 'SW':
                                    x_off   = XREF_BOTTOM_SW_OFFSET_X if snap_to == 'bottom' else XREF_MASTER_OFFSET_X
                                    step_y  = XREF_BOTTOM_SW_STEP_Y if snap_to == 'bottom' else XREF_MASTER_SW_STEP_Y
                                    extra_y = XREF_BOTTOM_SW_OFFSET_Y_EXTRA if snap_to == 'bottom' else XREF_MASTER_SW_OFFSET_Y_EXTRA
                                    rx = anchor_x + (x_off * sc_x)
                                    ry = anchor_y + ((current_y_offset + extra_y) * sc_y * step_dir)
                                    rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                    link_dest['from'] = rect
                                    page.insert_link(link_dest)
                                    if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                                    links_count += 1
                                    current_y_offset += step_y
                                else:
                                    x_off  = XREF_BOTTOM_NO_NC_OFFSET_X if snap_to == 'bottom' else XREF_MASTER_OFFSET_X
                                    step_y = XREF_BOTTOM_NO_NC_STEP_Y if snap_to == 'bottom' else XREF_MASTER_NO_NC_STEP_Y
                                    extra_y = XREF_BOTTOM_NO_NC_OFFSET_Y_EXTRA if snap_to == 'bottom' else XREF_MASTER_NO_NC_OFFSET_Y_START
                                    for _ in range(c_count):
                                        rx = anchor_x + (x_off * sc_x)
                                        ry = anchor_y + ((current_y_offset + extra_y) * sc_y * step_dir)
                                        rect = fitz.Rect(rx - 6, ry - 3, rx + 6, ry + 3)
                                        link_dest['from'] = rect
                                        page.insert_link(link_dest)
                                        if self.var_debug.get(): page.draw_rect(rect, color=(1,0,0), width=0.5)
                                        links_count += 1
                                        current_y_offset += step_y

            out_path = self.ent_pdf.get().replace(".pdf", "_linked.pdf")
            doc.save(out_path); doc.close()
            self.log("\n==============================")
            msg_done_base = gui_trans["msg_done"].format(links_count)
            if self.var_info.get(): msg_done_base += gui_trans["msg_done_info"].format(info_count)
            self.log(msg_done_base)
            messagebox.showinfo(gui_trans["msg_success_title"], gui_trans["msg_success_text"].format(os.path.basename(out_path), msg_done_base))

        except Exception as e:
            self.log(f"FEHLER: {e}")
            err_title = GUI_TEXTS.get(getattr(self, 'lang_var', tk.StringVar(value="Deutsch")).get(), GUI_TEXTS["Deutsch"])["msg_error_title"]
            messagebox.showerror(err_title, str(e))

if __name__ == "__main__":
    root = tk.Tk(); app = QETSmartLinker(root); root.mainloop()
