"""
FreeFileSync .ffs_gui Generator  v3
=====================================
• Quell- und Zielpfade eintragen oder per Paste einfügen
• Bestehende .ffs_gui laden und bearbeiten
• Automatische Umbenennung bei Namenskonflikt: datei(1).ffs_gui, datei(2).ffs_gui …
• Speicherort-Vorgabe: D:\\ddownloads
"""

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import xml.etree.ElementTree as ET
from xml.dom import minidom
import os
import re

# ── Rechtsklick-Kontextmenü für Entry und Text ────────────────────────────────
class ContextMenu:
    """Fügt jedem Entry- und Text-Widget ein Rechtsklick-Menü hinzu."""

    def __init__(self, widget):
        self.widget = widget
        self.menu = tk.Menu(widget, tearoff=0)
        self.menu.add_command(label="Ausschneiden",  command=self._cut)
        self.menu.add_command(label="Kopieren",      command=self._copy)
        self.menu.add_command(label="Einfügen",      command=self._paste)
        self.menu.add_separator()
        self.menu.add_command(label="Alles markieren", command=self._select_all)
        widget.bind("<Button-3>", self._show)

    def _show(self, event):
        # Menüeinträge je nach Zustand aktivieren/deaktivieren
        has_sel = self._has_selection()
        has_clip = bool(self._clipboard())
        state_sel  = "normal" if has_sel  else "disabled"
        state_clip = "normal" if has_clip else "disabled"
        self.menu.entryconfig("Ausschneiden",    state=state_sel)
        self.menu.entryconfig("Kopieren",        state=state_sel)
        self.menu.entryconfig("Einfügen",        state=state_clip)
        try:
            self.menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.menu.grab_release()

    def _clipboard(self):
        try:
            return self.widget.clipboard_get()
        except Exception:
            return ""

    def _has_selection(self):
        w = self.widget
        if isinstance(w, tk.Text):
            return w.tag_ranges("sel") != ()
        else:  # Entry / ttk.Entry
            try:
                return w.selection_present()
            except Exception:
                return False

    def _cut(self):
        self._copy()
        w = self.widget
        if isinstance(w, tk.Text):
            if w.tag_ranges("sel"):
                w.delete("sel.first", "sel.last")
        else:
            if w.selection_present():
                w.delete("sel.first", "sel.last")

    def _copy(self):
        w = self.widget
        try:
            if isinstance(w, tk.Text):
                text = w.get("sel.first", "sel.last")
            else:
                text = w.selection_get()
            w.clipboard_clear()
            w.clipboard_append(text)
        except Exception:
            pass

    def _paste(self):
        text = self._clipboard()
        if not text:
            return
        w = self.widget
        if isinstance(w, tk.Text):
            if w.tag_ranges("sel"):
                w.delete("sel.first", "sel.last")
            w.insert("insert", text)
        else:
            try:
                if w.selection_present():
                    w.delete("sel.first", "sel.last")
            except Exception:
                pass
            w.insert("insert", text)

    def _select_all(self):
        w = self.widget
        if isinstance(w, tk.Text):
            w.tag_add("sel", "1.0", "end-1c")
        else:
            w.select_range(0, "end")
            w.icursor("end")


# ── Vorausgefüllte Wärme-Pfade ────────────────────────────────────────────────
DEFAULT_NAME   = "Vergleich.ffs_gui"
DEFAULT_SOURCE = r"D:\dbackup\excel\files"
DEFAULT_DIR    = r"D:\ddownloads"          # Vorgabe-Speicherort
DEFAULT_TARGETS = """\
d:\\dbackup\\teil2
C:\\Users\\heb\\OneDrive\\Dokumente\\Strom\\
C:\\Users\\heb\\OneDrive\\excel\\WaermeHeisswasserKaltwasser\\
C:\\Users\\heb\\OneDrive\\Microsoft Dateien\\
C:\\Users\\heb\\OneDrive\\sync googledrive\\Wichtiges\\you\\excel\\
C:\\Users\\heb\\OneDrive\\sync googledrive\\Wichtiges\\you\\qnap\\
C:\\Users\\heb\\OneDrive\\sync onedrive\\Documents\\
C:\\Users\\heb\\OneDrive\\sync onedrive\\Documents\\Arbeit Lohn Stunden\\
V:\\My Drive\\Wichtiges\\you\\Rechnungen\\toll you privat\\
W:\\
W:\\000docs\\docs\\
W:\\backup excel word\\
W:\\Exceldokumente\\
W:\\Exceldokumente\\excel acer\\
W:\\Finanzen\\
W:\\Rechnungen\\Strom Heizung\\
X:\\
Y:\\Arbeit Lohn Stunden\\
Y:\\Arbeit Lohn Stunden\\Arbeit Lohn Stunden\\
Y:\\Garage\\toll you privat\\
Y:\\googledriveoffline\\My Drive\\Wichtiges\\Wichtiges\\you\\excel\\
Y:\\googledriveoffline\\My Drive\\Wichtiges\\Wichtiges\\you\\qnap\\
Y:\\googledriveoffline\\My Drive\\Wichtiges\\Wichtiges\\you\\sync\\documents\\
Y:\\sync onedrive\\Verbrauch\\
\\\\NAS4C36D7\\Dokumente\\
\\\\NAS4C36D7\\Dokumente\\000docs\\docs\\
\\\\NAS4C36D7\\Dokumente\\backup excel word\\
\\\\NAS4C36D7\\Dokumente\\Exceldokumente\\
\\\\NAS4C36D7\\Dokumente\\Exceldokumente\\excel acer\\
\\\\NAS4C36D7\\Dokumente\\Finanzen\\
\\\\NAS4C36D7\\Dokumente\\Rechnungen\\Strom Heizung\\
\\\\NAS4C36D7\\Download\\
\\\\NAS4C36D7\\Google Drive (you@gmail.com)\\My Drive\\Wichtiges\\you\\Rechnungen\\toll you privat\\"""


# ── Hilfsfunktionen: Dateipfad ────────────────────────────────────────────────
def unique_path(folder: str, name: str) -> str:
    """
    Gibt einen Pfad zurück, der noch nicht existiert.
    datei.ffs_gui  →  datei(1).ffs_gui  →  datei(2).ffs_gui …
    Bereits vorhandene (N)-Suffixe im Namen werden erkannt und
    beim Zählen übersprungen, sodass nie eine Lücke entsteht.
    """
    if name.lower().endswith(".ffs_gui"):
        stem = name[:-8]
        ext  = ".ffs_gui"
    else:
        stem, ext = os.path.splitext(name)

    # Vorhandenen (N)-Suffix entfernen → immer vom echten Stamm aus zählen
    m = re.match(r"^(.*)\((\d+)\)$", stem)
    if m:
        stem = m.group(1).rstrip()

    candidate = os.path.join(folder, stem + ext)
    if not os.path.exists(candidate):
        return candidate

    counter = 1
    while True:
        candidate = os.path.join(folder, f"{stem}({counter}){ext}")
        if not os.path.exists(candidate):
            return candidate
        counter += 1


# ── XML-Erzeugung ──────────────────────────────────────────────────────────────
def make_sync_element() -> ET.Element:
    sync = ET.Element("Synchronize")
    ch   = ET.SubElement(sync, "Changes")
    l    = ET.SubElement(ch, "Left");  l.set("Create","right"); l.set("Update","right"); l.set("Delete","none")
    r    = ET.SubElement(ch, "Right"); r.set("Create","none");  r.set("Update","none");  r.set("Delete","none")
    ET.SubElement(sync, "DeletionPolicy").text = "RecycleBin"
    ET.SubElement(sync, "VersioningFolder").set("Style", "Replace")
    return sync


def build_ffs_xml(source: str, targets: list) -> str:
    root = ET.Element("FreeFileSync")
    root.set("XmlType", "GUI")
    root.set("XmlFormat", "23")

    ET.SubElement(root, "Notes")

    cmp = ET.SubElement(root, "Compare")
    ET.SubElement(cmp, "Variant").text  = "TimeAndSize"
    ET.SubElement(cmp, "Symlinks").text = "Exclude"
    ET.SubElement(cmp, "IgnoreTimeShift")

    root.append(make_sync_element())   # globaler Sync-Block

    flt = ET.SubElement(root, "Filter")
    inc = ET.SubElement(flt, "Include"); ET.SubElement(inc, "Item").text = "*"
    exc = ET.SubElement(flt, "Exclude")
    for e in [r"\System Volume Information\ ", r"\$Recycle.Bin\ ",
              r"\RECYCLE?\ ", r"\Recovery\ ", r"*\thumbs.db"]:
        ET.SubElement(exc, "Item").text = e.strip()
    for tag, attr, val in [("SizeMin","Unit","None"), ("SizeMax","Unit","None"), ("TimeSpan","Type","None")]:
        el = ET.SubElement(flt, tag); el.set(attr, val); el.text = "0"

    pairs = ET.SubElement(root, "FolderPairs")
    for t in targets:
        pair = ET.SubElement(pairs, "Pair")
        lft  = ET.SubElement(pair, "Left"); lft.set("Threads", "4"); lft.text = source
        ET.SubElement(pair, "Right").text = t
        pair.append(make_sync_element())

    err = ET.SubElement(root, "Errors")
    err.set("Ignore","false"); err.set("Retry","0"); err.set("Delay","5")
    ET.SubElement(root, "PostSyncCommand").set("Condition", "Completion")
    ET.SubElement(root, "LogFolder")
    ET.SubElement(root, "EmailNotification").set("Condition", "Always")
    ET.SubElement(root, "GridViewType").text = "Action"

    raw    = ET.tostring(root, encoding="unicode")
    dom    = minidom.parseString(raw)
    pretty = dom.toprettyxml(indent="    ", encoding="utf-8").decode("utf-8")
    lines  = pretty.splitlines()
    lines[0] = '<?xml version="1.0" encoding="utf-8"?>'
    return "\n".join(lines)


def parse_ffs_gui(path: str) -> tuple:
    """
    Liest eine bestehende .ffs_gui.
    Rückgabe: (source_str, [target_str, …])
    source = Left-Pfad des ersten Paares (alle Paare müssen denselben Left haben).
    """
    tree = ET.parse(path)
    root = tree.getroot()
    pairs   = root.find("FolderPairs")
    source  = ""
    targets = []
    if pairs is not None:
        for i, pair in enumerate(pairs.findall("Pair")):
            left  = pair.find("Left")
            right = pair.find("Right")
            if i == 0 and left is not None:
                source = (left.text or "").strip()
            if right is not None:
                t = (right.text or "").strip()
                if t:
                    targets.append(t)
    return source, targets


# ── GUI ────────────────────────────────────────────────────────────────────────
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("FreeFileSync .ffs_gui Generator  v3")
        self.resizable(True, True)
        self.configure(padx=12, pady=12)
        self._build()
        self.minsize(660, 560)

    # ── Aufbau ────────────────────────────────────────────────────────────────
    def _build(self):

        # ── Datei-Zeile (Name + Laden + Ordner) ──────────────────────────────
        frm_top = ttk.LabelFrame(self, text="Datei")
        frm_top.pack(fill="x", pady=(0, 8))

        ttk.Label(frm_top, text="Dateiname:").grid(row=0, column=0, sticky="w", padx=6, pady=4)
        self.var_name = tk.StringVar(value=DEFAULT_NAME)
        ent_name = ttk.Entry(frm_top, textvariable=self.var_name, width=36)
        ent_name.grid(row=0, column=1, sticky="ew", padx=6, pady=4)
        ContextMenu(ent_name)
        ttk.Button(frm_top, text="📂  Laden …", command=self._load_ffs).grid(row=0, column=2, padx=4, pady=4)
        ttk.Button(frm_top, text="Ordner …",   command=self._pick_dir).grid(row=0, column=3, padx=4, pady=4)
        frm_top.columnconfigure(1, weight=1)

        ttk.Label(frm_top, text="Ordner:").grid(row=1, column=0, sticky="w", padx=6, pady=2)
        fallback = DEFAULT_DIR if os.path.isdir(DEFAULT_DIR) else os.path.expanduser("~\\Desktop")
        self.var_dir = tk.StringVar(value=fallback)
        ttk.Label(frm_top, textvariable=self.var_dir, foreground="#555").grid(
            row=1, column=1, columnspan=3, sticky="w", padx=6, pady=2)

        # ── Quellpfad ─────────────────────────────────────────────────────────
        frm_src = ttk.LabelFrame(self, text="Quellpfad (Left) – ein einzelner Pfad")
        frm_src.pack(fill="x", pady=(0, 8))
        self.ent_source = ttk.Entry(frm_src, width=80)
        self.ent_source.insert(0, DEFAULT_SOURCE)
        self.ent_source.pack(fill="x", padx=6, pady=6)
        ContextMenu(self.ent_source)

        # ── Zielpfade ─────────────────────────────────────────────────────────
        frm_tgt = ttk.LabelFrame(self, text="Zielpfade (Right) – einen pro Zeile, per Paste einfügen")
        frm_tgt.pack(fill="both", expand=True, pady=(0, 8))

        self.txt_targets = tk.Text(frm_tgt, wrap="none", undo=True, height=18,
                                   font=("Consolas", 9))
        self.txt_targets.insert("1.0", DEFAULT_TARGETS)

        vsb = ttk.Scrollbar(frm_tgt, orient="vertical",   command=self.txt_targets.yview)
        hsb = ttk.Scrollbar(frm_tgt, orient="horizontal", command=self.txt_targets.xview)
        self.txt_targets.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
        ContextMenu(self.txt_targets)
        self.txt_targets.grid(row=0, column=0, sticky="nsew", padx=(6, 0), pady=6)
        vsb.grid(row=0, column=1, sticky="ns",  pady=6)
        hsb.grid(row=1, column=0, sticky="ew",  padx=(6, 0))
        frm_tgt.rowconfigure(0, weight=1)
        frm_tgt.columnconfigure(0, weight=1)

        self.lbl_count = ttk.Label(frm_tgt, text="", foreground="#777")
        self.lbl_count.grid(row=2, column=0, columnspan=2, sticky="w", padx=6, pady=(0, 4))
        self.txt_targets.bind("<KeyRelease>", self._update_count)
        self._update_count()

        # ── Buttons ───────────────────────────────────────────────────────────
        frm_btn = ttk.Frame(self)
        frm_btn.pack(fill="x")
        ttk.Button(frm_btn, text="🗑  Leeren",             command=self._clear_targets).pack(side="left", padx=4)
        ttk.Button(frm_btn, text="⎘  Duplikate entfernen", command=self._dedup).pack(side="left", padx=4)
        ttk.Button(frm_btn, text="✔  .ffs_gui erstellen",  command=self._generate).pack(side="right", padx=4)

        # Statuszeile
        self.lbl_status = ttk.Label(self, text="", foreground="green")
        self.lbl_status.pack(anchor="w", pady=(6, 0))

    # ── Aktionen ──────────────────────────────────────────────────────────────
    def _pick_dir(self):
        d = filedialog.askdirectory(title="Speicherort wählen",
                                    initialdir=self.var_dir.get())
        if d:
            self.var_dir.set(d)

    def _load_ffs(self):
        """Bestehende .ffs_gui einlesen und alle Felder befüllen."""
        path = filedialog.askopenfilename(
            title="Bestehende .ffs_gui laden",
            initialdir=self.var_dir.get(),
            filetypes=[("FreeFileSync GUI", "*.ffs_gui"), ("Alle Dateien", "*.*")]
        )
        if not path:
            return
        try:
            source, targets = parse_ffs_gui(path)
        except Exception as e:
            messagebox.showerror("Fehler beim Laden", str(e))
            return

        self.var_name.set(os.path.basename(path))
        self.var_dir.set(os.path.dirname(path))

        self.ent_source.delete(0, "end")
        self.ent_source.insert(0, source)

        self.txt_targets.delete("1.0", "end")
        self.txt_targets.insert("1.0", "\n".join(targets))
        self._update_count()
        self.lbl_status.config(
            text=f"📂  Geladen: {os.path.basename(path)}  ({len(targets)} Paare)",
            foreground="#1a6fc4")

    def _get_targets(self) -> list:
        raw = self.txt_targets.get("1.0", "end")
        return [ln.strip() for ln in raw.splitlines() if ln.strip()]

    def _update_count(self, _event=None):
        n = len(self._get_targets())
        self.lbl_count.config(text=f"{n} Zielpfad{'e' if n != 1 else ''}")

    def _clear_targets(self):
        self.txt_targets.delete("1.0", "end")
        self._update_count()

    def _dedup(self):
        seen, result = set(), []
        for ln in self._get_targets():
            key = ln.lower().rstrip("\\/")
            if key not in seen:
                seen.add(key)
                result.append(ln)
        self.txt_targets.delete("1.0", "end")
        self.txt_targets.insert("1.0", "\n".join(result))
        self._update_count()
        self.lbl_status.config(text="Duplikate entfernt.", foreground="#888")

    def _generate(self):
        source  = self.ent_source.get().strip()
        targets = self._get_targets()
        name    = self.var_name.get().strip()
        outdir  = self.var_dir.get().strip()

        if not source:
            messagebox.showerror("Fehler", "Bitte einen Quellpfad eingeben.")
            return
        if not targets:
            messagebox.showerror("Fehler", "Bitte mindestens einen Zielpfad eingeben.")
            return
        if not name.lower().endswith(".ffs_gui"):
            name += ".ffs_gui"

        os.makedirs(outdir, exist_ok=True)

        # ── Automatische Umbenennung bei Namenskonflikt ───────────────────────
        out_path    = unique_path(outdir, name)
        actual_name = os.path.basename(out_path)

        try:
            with open(out_path, "w", encoding="utf-8") as f:
                f.write(build_ffs_xml(source, targets))

            renamed = actual_name != name
            self.lbl_status.config(
                text=(f"✔  Gespeichert: {out_path}  ({len(targets)} Paare)"
                      + (f"   →  umbenannt zu '{actual_name}'" if renamed else "")),
                foreground="#b07000" if renamed else "green")

            # Dateinamenfeld auf tatsächlich gespeicherten Namen aktualisieren
            self.var_name.set(actual_name)

        except Exception as e:
            messagebox.showerror("Fehler beim Speichern", str(e))


if __name__ == "__main__":
    app = App()
    app.mainloop()
