#!/usr/bin/env python3
# SPS-Editor.py
# Anpassungen:
# - Beim Öffnen wird eine einzelne .bak-Datei im selben Ordner erstellt (überschreibt alte .bak)
# - Beim Speichern wird atomar über eine temp-Datei geschrieben (os.replace)
# - Spaltenkopf ist in der Breite veränderbar (Interactive)
#
# Benötigt: PySide6, defusedxml
# pip install PySide6 defusedxml

import sys
import os
import time
import traceback
import shutil
import tempfile
from PySide6.QtWidgets import (
    QApplication, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QListWidget,
    QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem, QLabel, QLineEdit,
    QHeaderView, QSplitter
)
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtCore import Qt
from defusedxml import ElementTree as ET

LOGFILE = os.path.expanduser("~/.local/share/sps-editor-start.log")

def log(msg):
    try:
        with open(LOGFILE, "a", encoding="utf-8") as f:
            f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {msg}\n")
    except Exception:
        pass

# Column mapping: (elementInformation name -> column header)
COLUMN_DEFS = [
    ("label", "BMK"),
    ("unity", "Adresse"),
    ("comment", "Funktion"),
    ("description", "AKS"),
    ("designation", "Anschluss1"),
    ("manufacturer", "Anschluss2"),
    ("manufacturer_reference", "Anschluss3")
]

# --- Backup helpers ---
def create_single_backup(path):
    """
    Erstellt eine einzelne .bak-Datei im selben Verzeichnis: path + ".bak".
    Überschreibt vorhandene .bak mit demselben Namen.
    """
    try:
        bak = path + ".bak"
        shutil.copy2(path, bak)
        return bak
    except Exception as e:
        raise

def safe_write_tree(tree, dest_path):
    """
    Schreibt tree in eine temporäre Datei im selben Verzeichnis und ersetzt atomar dest_path.
    Falls Fehler, wirft Exception und dest_path bleibt unverändert.
    """
    dirn = os.path.dirname(dest_path) or "."
    fd, tmpname = tempfile.mkstemp(prefix=".tmp_sps_", suffix=".qet", dir=dirn)
    os.close(fd)
    try:
        tree.write(tmpname, encoding='utf-8', xml_declaration=True)
        # atomar ersetzen
        os.replace(tmpname, dest_path)
    except Exception:
        # versuche temporäre Datei zu entfernen
        try:
            os.remove(tmpname)
        except Exception:
            pass
        raise

# ---------- XML / folio helpers ----------
def collect_all_folios(root):
    folios = []
    for el in root.iter():
        f = el.get('folio')
        if f is not None:
            folios.append((f, el))
    return folios

def find_pages_between(root, start_val, end_val):
    folio_items = collect_all_folios(root)
    if not folio_items:
        return []

    folio_items = [(f.strip(), el) for (f, el) in folio_items]

    try:
        si = int(str(start_val).strip())
        ei = int(str(end_val).strip())
        if si > ei:
            si, ei = ei, si
        out = []
        for fstr, el in folio_items:
            try:
                fi = int(fstr)
            except Exception:
                continue
            if si <= fi <= ei:
                out.append(el)
        return out
    except Exception:
        folio_list = [f for (f, _) in folio_items]
        if start_val.strip() in folio_list and end_val.strip() in folio_list:
            i0 = folio_list.index(start_val.strip())
            i1 = folio_list.index(end_val.strip())
            if i0 <= i1:
                sliced = folio_items[i0:i1+1]
            else:
                sliced = folio_items[i1:i0+1]
            return [el for (_, el) in sliced]
        else:
            out = []
            for target in (start_val.strip(), end_val.strip()):
                for fstr, el in folio_items:
                    if fstr == target:
                        out.append(el)
            seen = set()
            ordered = []
            for fstr, el in folio_items:
                if el in out and id(el) not in seen:
                    ordered.append(el)
                    seen.add(id(el))
            return ordered

def get_page_title(page_elem):
    title = page_elem.get('title') or page_elem.get('name') or page_elem.get('folio')
    if title is None:
        title = "<unnamed>"
    return title

def collect_elements_on_page(page_elem):
    out = []
    for el in page_elem.findall('.//element'):
        x = el.get('x'); y = el.get('y')
        try:
            xf = float(x) if x is not None else 0.0
            yf = float(y) if y is not None else 0.0
        except ValueError:
            xf, yf = 0.0, 0.0
        info = {}
        ei_parent = el.find('elementInformations')
        if ei_parent is not None:
            for info_node in ei_parent.findall('elementInformation'):
                name = info_node.get('name')
                val = (info_node.text or "").strip()
                if name:
                    info[name] = val
        # Filter: if all relevant fields empty => skip
        has_any = any((info.get(fname, "").strip() != "") for fname, _ in COLUMN_DEFS)
        if not has_any:
            continue
        out.append({'elem': el, 'x': xf, 'y': yf, 'fields': info})
    return out

def column_major_sort(elements, x_tolerance=10.0):
    for e in elements:
        e['colx'] = round(e['x'] / x_tolerance) * x_tolerance
    elems_sorted = sorted(elements, key=lambda e: (e['colx'], e['y']))
    return elems_sorted

# ---------- GUI ----------
class SPS_Editor(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("SPS-Editor")
        self.resize(1550, 920)

        self.qet_path = None
        self.tree = None
        self.root = None
        self.pages = []
        self.page_elements = {}
        self.current_folio = None
        self.current_page_elem = None

        layout = QVBoxLayout(self)
        top_row = QHBoxLayout()
        self.file_label = QLabel("Keine Datei geladen")
        top_row.addWidget(self.file_label)
        open_btn = QPushButton("Datei öffnen")
        open_btn.clicked.connect(self.open_file)
        top_row.addWidget(open_btn)
        layout.addLayout(top_row)

        range_row = QHBoxLayout()
        range_row.setSpacing(6)
        range_row.setContentsMargins(0, 0, 0, 0)

        label_pages = QLabel("Seiten auswählen:")
        label_pages.setFixedWidth(130)
        range_row.addWidget(label_pages)

        self.folio_from = QLineEdit()
        self.folio_from.setFixedWidth(80)
        range_row.addWidget(self.folio_from)

        label_bis = QLabel("bis")
        label_bis.setFixedWidth(20)
        range_row.addWidget(label_bis)

        self.folio_to = QLineEdit()
        self.folio_to.setFixedWidth(80)
        range_row.addWidget(self.folio_to)

        # Wenn der Benutzer Enter in einem der Felder drückt -> Laden starten
        self.folio_from.returnPressed.connect(self.load_pages_range)
        self.folio_to.returnPressed.connect(self.load_pages_range)

        # Flexibler Abstand nach rechts, Button bleibt am Rand
        range_row.addStretch(1)

        load_btn = QPushButton("Laden")
        load_btn.clicked.connect(self.load_pages_range)   # <-- HIER die Verbindung!
        range_row.addWidget(load_btn)

        layout.addLayout(range_row)



        splitter = QSplitter(Qt.Horizontal)
        left_widget = QWidget(); left_layout = QVBoxLayout(left_widget)
        left_layout.addWidget(QLabel("Seiten (Doppelklick zum Öffnen)"))
        self.pages_list = QListWidget()
        self.pages_list.itemDoubleClicked.connect(self.on_page_double_clicked)
        left_layout.addWidget(self.pages_list)
        splitter.addWidget(left_widget)

        right_widget = QWidget(); right_layout = QVBoxLayout(right_widget)
        self.table_label = QLabel("Seite: -"); right_layout.addWidget(self.table_label)
        self.table = QTableWidget()
        self.table.setColumnCount(len(COLUMN_DEFS))
        headers = [h for (_, h) in COLUMN_DEFS]
        self.table.setHorizontalHeaderLabels(headers)
        # WICHTIG: Interactive -> Spaltenköpfe anfassbar und in der Breite veränderbar
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
        # normale (nicht bewegliche) Spaltenköpfe: nur Größe ändern
        self.table.setColumnWidth(0, 80)   # BMK
        self.table.setColumnWidth(1, 80)   # Adresse
        self.table.setColumnWidth(2, 360)  # Funktion
        self.table.setColumnWidth(3, 360)  # AKS
        self.table.setColumnWidth(4, 100)  # Anschluss 1
        self.table.setColumnWidth(5, 100)  # Anschluss 2
        self.table.setColumnWidth(6, 100)  # Anschluss 3

        self.table.horizontalHeader().setSectionsMovable(False)
        self.table.horizontalHeader().setStretchLastSection(False)
        right_layout.addWidget(self.table, 1)

        btn_row = QHBoxLayout()
        save_btn = QPushButton("Übernehmen (in .qet schreiben)")
        save_btn.clicked.connect(self.apply_changes_to_qet)
        btn_row.addWidget(save_btn)

        right_layout.addLayout(btn_row)


        splitter.addWidget(right_widget)
        layout.addWidget(splitter)

        # set initial sizes: left small, right large
        splitter.setSizes([240, 980])
        self.pages_list.setMaximumWidth(340)

        # Global shortcut Ctrl+V -> paste (works when window focused)
        try:
            QShortcut(QKeySequence("Ctrl+V"), self, activated=self.paste_from_clipboard)
        except Exception:
            pass

    def open_file(self):
        path, _ = QFileDialog.getOpenFileName(self, "QET Datei öffnen", "", "QElectroTech (*.qet);;Alle Dateien (*)")
        if not path:
            return
        try:
            # parse first to ensure file ok
            tree = ET.parse(path)
            root = tree.getroot()
        except Exception as e:
            QMessageBox.critical(self, "Fehler", f"Datei konnte nicht geladen werden:\n{e}")
            return

        # create a single backup in same folder: file.qet.bak (overwrites existing .bak)
        try:
            bak_path = create_single_backup(path)
            log(f"Backup erstellt: {bak_path}")
        except Exception as e:
            # non-fatal: inform user but continue
            QMessageBox.information(self, "Backup-Fehler", f"Backup konnte nicht erstellt werden:\n{e}\nVorgang wird trotzdem fortgesetzt.")

        # accept parsed tree
        self.qet_path = path; self.tree = tree; self.root = root
        self.file_label.setText(os.path.basename(path) + " (Backup erstellt)")

        self.pages_list.clear(); self.page_elements.clear(); self.pages = []

    def load_pages_range(self):
        if not self.root:
            QMessageBox.warning(self, "Keine Datei", "Bitte zuerst eine .qet Datei öffnen.")
            return
        from_val = self.folio_from.text().strip(); to_val = self.folio_to.text().strip()
        if not from_val or not to_val:
            QMessageBox.warning(self, "Fehler", "Bitte beide Felder ausfüllen: 'von' und 'bis'.")
            return

        found_elements = find_pages_between(self.root, from_val, to_val)
        self.pages_list.clear(); self.pages = []; self.page_elements.clear()

        if not found_elements:
            folios = [f for (f, _) in collect_all_folios(self.root)]
            folios_unique = sorted(list(dict.fromkeys([f.strip() for f in folios])))
            sample = ', '.join(folios_unique[:80])
            QMessageBox.information(self, "Keine Seiten", f"Keine Seiten mit den angegebenen folio-Werten gefunden.\nGefundene folio-Werte im Dokument (Auszug):\n{sample}")
            return

        loaded_count = 0
        for el in found_elements:
            folio = el.get('folio', '').strip()
            title = get_page_title(el)
            display = f"{title} (folio={folio})"
            elems = collect_elements_on_page(el)
            elems_sorted = column_major_sort(elems, x_tolerance=10.0)
            if not elems_sorted:
                continue
            self.pages.append((el, display, folio))
            self.pages_list.addItem(display)
            self.page_elements[folio] = elems_sorted
            loaded_count += 1

        if loaded_count == 0:
            QMessageBox.information(self, "Keine Seiten", "Zwischen den angegebenen Werten wurden Seiten gefunden, jedoch hatten alle keine relevanten (nicht-leeren) Felder.")
            return
        QMessageBox.information(self, "Geladen", f"{loaded_count} Seite(n) geladen.")

    def on_page_double_clicked(self, item):
        text = item.text(); folio = None; page_elem = None
        for p, disp, f in self.pages:
            if disp == text:
                page_elem = p; folio = f; break
        if folio is None: return
        self.current_folio = folio; self.current_page_elem = page_elem
        self.populate_table_for_folio(folio)

    def populate_table_for_folio(self, folio):
        elems = self.page_elements.get(folio, [])
        self.table.setRowCount(len(elems))
        self.table_label.setText(f"Seite: folio={folio}  |  Elemente: {len(elems)}")
        for r, e in enumerate(elems):
            fields = e.get('fields', {})
            for c, (field_name, header) in enumerate(COLUMN_DEFS):
                val = fields.get(field_name, "")
                item = QTableWidgetItem(val)
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self.table.setItem(r, c, item)

    def paste_from_clipboard(self):
        cb = QApplication.clipboard().text()
        if not cb:
            QMessageBox.information(self, "Clipboard leer", "Kein Text in der Zwischenablage.")
            return
        rows = [row.split('\t') for row in cb.splitlines()]
        rows = [[cell.strip() for cell in row] for row in rows]
        rows = [r for r in rows if any(cell != "" for cell in r)]
        if not rows:
            QMessageBox.information(self, "Keine Daten", "Zwischenablage enthält keine tabellarischen Daten (oder nur leere Zeilen).")
            return

        sel_ranges = self.table.selectedRanges()
        if sel_ranges:
            sel = sel_ranges[0]
            start_row = sel.topRow()
            start_col = sel.leftColumn()
        else:
            cur_row = self.table.currentRow()
            cur_col = self.table.currentColumn()
            start_row = cur_row if cur_row >= 0 else 0
            start_col = cur_col if cur_col >= 0 else None

        single_col_clip = all(len(r) == 1 for r in rows)
        if single_col_clip:
            if start_col is None or start_col < 0:
                header_lower = [hdr.lower() for hdr in [h for (_, h) in COLUMN_DEFS]]
                try:
                    start_col = header_lower.index("funktion")
                except ValueError:
                    start_col = 0
        else:
            if start_col is None or start_col < 0:
                start_col = 0

        for i, rdata in enumerate(rows):
            target_row = start_row + i
            if target_row >= self.table.rowCount():
                self.table.insertRow(target_row)
            for j, val in enumerate(rdata):
                target_col = start_col + j
                if target_col >= self.table.columnCount():
                    break
                self.table.setItem(target_row, target_col, QTableWidgetItem(val))

        QMessageBox.information(self, "Eingefügt", f"{len(rows)} Zeile(n) aus der Zwischenablage eingefügt (Start: row {start_row}, col {start_col}).")

    def apply_changes_to_qet(self):
        if not self.qet_path or not self.tree:
            QMessageBox.warning(self, "Keine Datei", "Bitte zuerst eine .qet Datei öffnen.")
            return
        if not self.current_folio:
            QMessageBox.warning(self, "Keine Seite", "Bitte eine Seite auswählen (Doppelklick).")
            return
        elems = self.page_elements.get(self.current_folio, [])
        changed = 0
        for r in range(self.table.rowCount()):
            if r >= len(elems): break
            e = elems[r]; elem_node = e['elem']
            ei_parent = elem_node.find('elementInformations')
            if ei_parent is None:
                ei_parent = ET.SubElement(elem_node, 'elementInformations')
            name_map = {n.get('name'): n for n in ei_parent.findall('elementInformation') if n.get('name')}
            for c, (field_name, header) in enumerate(COLUMN_DEFS):
                item = self.table.item(r, c)
                newval = (item.text() if item else "").strip()
                oldnode = name_map.get(field_name)
                oldval = (oldnode.text or "").strip() if oldnode is not None else ""
                if newval != oldval:
                    changed += 1
                    if oldnode is not None:
                        oldnode.text = newval
                    else:
                        newnode = ET.SubElement(ei_parent, 'elementInformation')
                        newnode.set('name', field_name)
                        newnode.set('show', '1')
                        newnode.text = newval
                    e['fields'][field_name] = newval

        if changed == 0:
            QMessageBox.information(self, "Keine Änderungen", "Es wurden keine Änderungen festgestellt.")
            return

        # safe write to temp then replace original; backup was created at open
        try:
            safe_write_tree(self.tree, self.qet_path)
        except Exception as e:
            QMessageBox.critical(self, "Schreibfehler", f"Fehler beim Schreiben der Datei:\n{e}\nBackup bleibt: {self.qet_path}.bak")
            return

        QMessageBox.information(self, "Fertig", f"{changed} Feld(ern) geschrieben. Backup: {os.path.basename(self.qet_path)}.bak")

# ---------- main with logging and exception handling ----------
def main():
    log("Start SPS-Editor")
    try:
        app = QApplication(sys.argv)
        w = SPS_Editor()
        w.show()
        rc = app.exec()
        log(f"Exit with code {rc}")
        sys.exit(rc)
    except Exception as e:
        tb = traceback.format_exc()
        log("Uncaught exception:\n" + tb)
        try:
            app = QApplication.instance() or QApplication(sys.argv)
            QMessageBox.critical(None, "Absturz", f"Unbehandelter Fehler beim Start:\n{e}\n\nDetails wurden in {LOGFILE} geloggt.")
        except Exception:
            pass
        print(tb, file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()
