#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PointCab Project Renamer v4.2
A GUI tool for renaming scans in PointCab projects with full scan names.
Now with Batch Renamer and Project Merger support!

LSDX-Struktur:
- <LSDX version="4.0">
  - <Project>
  - <Elements>
    - <Element type="registration" uuid="{...}" parents="">  # Wurzelelement
    - <Element type="cluster" uuid="{...}" parents="{registration_uuid}" name="ClusterName">
    - <Element type="scan" uuid="{...}" parents="{cluster_uuid}" name="1">
      - <References>
        - <FilePath type="lsd">1.lsd</FilePath>
        - <FilePath type="preview">Previews/1.png</FilePath>

Author: DeepAgent
Date: 2026-01-14
"""

import os
import re
import shutil
import logging
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from datetime import datetime
import xml.etree.ElementTree as ET
from pathlib import Path
import uuid
import copy


class PointCabRenamer:
    """Main application class for PointCab project renaming (Single Project Mode)."""
    
    def __init__(self, root, return_callback=None):
        self.root = root
        self.return_callback = return_callback
        self.root.title("PointCab Projekt Umbenenner - Einzelprojekt")
        self.root.geometry("850x750")
        self.root.minsize(700, 600)
        
        # Variables
        self.root_dir = tk.StringVar()
        self.pointcloud_dir = None
        self.lsdx_file = None
        self.preview_dir = None
        self.changes = []
        self.logger = None
        self.cleanup_strings = []
        
        # Load cleanup configuration
        self.load_cleanup_config()
        
        self.setup_ui()
        
    def load_cleanup_config(self):
        """Load cleanup strings from cluster_cleanup.txt."""
        config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cluster_cleanup.txt")
        self.cleanup_strings = []
        
        if os.path.exists(config_path):
            try:
                # Use utf-8-sig to handle BOM from Windows editors
                with open(config_path, 'r', encoding='utf-8-sig') as f:
                    for line in f:
                        # Strip whitespace including BOM characters
                        line = line.strip()
                        # Skip empty lines and comments
                        if not line:
                            continue
                        if line.startswith('#'):
                            continue
                        # Remove any remaining BOM or special chars
                        line = line.lstrip('\ufeff')
                        if line:
                            self.cleanup_strings.append(line)
                print(f"Cleanup-Konfiguration geladen: {len(self.cleanup_strings)} Einträge")
            except Exception as e:
                print(f"Fehler beim Laden der Cleanup-Konfiguration: {e}")
                
    def clean_cluster_name(self, name):
        """Remove cleanup strings from the cluster name."""
        cleaned_name = name
        removed_strings = []
        
        for cleanup_str in self.cleanup_strings:
            if cleanup_str in cleaned_name:
                removed_strings.append(cleanup_str)
                cleaned_name = cleaned_name.replace(cleanup_str, '')
                
        return cleaned_name, removed_strings
        
    def setup_ui(self):
        """Setup the user interface."""
        # Main frame with padding
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Back button if we have a return callback
        if self.return_callback:
            back_frame = ttk.Frame(main_frame)
            back_frame.pack(fill=tk.X, pady=(0, 10))
            ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
        
        # Directory selection frame
        dir_frame = ttk.LabelFrame(main_frame, text="Projektverzeichnis", padding="5")
        dir_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Entry(dir_frame, textvariable=self.root_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        ttk.Button(dir_frame, text="Durchsuchen...", command=self.select_directory).pack(side=tk.LEFT)
        ttk.Button(dir_frame, text="Analysieren", command=self.analyze_project).pack(side=tk.LEFT, padx=(5, 0))
        
        # Info frame
        self.info_frame = ttk.LabelFrame(main_frame, text="Projektinformationen", padding="5")
        self.info_frame.pack(fill=tk.X, pady=(0, 10))
        
        self.info_label = ttk.Label(self.info_frame, text="Bitte wählen Sie ein Projektverzeichnis aus.", wraplength=800)
        self.info_label.pack(fill=tk.X)
        
        # Cleanup config info frame
        cleanup_frame = ttk.LabelFrame(main_frame, text="Clustername-Bereinigung", padding="5")
        cleanup_frame.pack(fill=tk.X, pady=(0, 10))
        
        cleanup_info = f"Strings die aus dem Clusternamen entfernt werden: {', '.join(self.cleanup_strings) if self.cleanup_strings else '(keine)'}\n"
        cleanup_info += f"📁 Konfigurationsdatei: cluster_cleanup.txt (im Programmverzeichnis)"
        self.cleanup_label = ttk.Label(cleanup_frame, text=cleanup_info, wraplength=800, foreground="gray")
        self.cleanup_label.pack(fill=tk.X)
        
        ttk.Button(cleanup_frame, text="Konfiguration neu laden", command=self.reload_cleanup_config).pack(side=tk.LEFT, pady=(5, 0))
        
        # Changes preview frame
        preview_frame = ttk.LabelFrame(main_frame, text="Vorschau der Änderungen", padding="5")
        preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        self.preview_text = scrolledtext.ScrolledText(preview_frame, wrap=tk.WORD, height=20, font=('Consolas', 9))
        self.preview_text.pack(fill=tk.BOTH, expand=True)
        self.preview_text.config(state=tk.DISABLED)
        
        # Status frame
        status_frame = ttk.Frame(main_frame)
        status_frame.pack(fill=tk.X, pady=(0, 10))
        
        self.status_label = ttk.Label(status_frame, text="Bereit", foreground="gray")
        self.status_label.pack(side=tk.LEFT)
        
        self.progress = ttk.Progressbar(status_frame, mode='indeterminate', length=200)
        self.progress.pack(side=tk.RIGHT)
        
        # Buttons frame
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X)
        
        self.execute_btn = ttk.Button(button_frame, text="Änderungen ausführen", command=self.execute_changes, state=tk.DISABLED)
        self.execute_btn.pack(side=tk.RIGHT)
        
        ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
        
    def go_back(self):
        """Return to main menu."""
        if self.return_callback:
            self.return_callback()
        
    def reload_cleanup_config(self):
        """Reload cleanup configuration and update display."""
        self.load_cleanup_config()
        cleanup_info = f"Strings die aus dem Clusternamen entfernt werden: {', '.join(self.cleanup_strings) if self.cleanup_strings else '(keine)'}\n"
        cleanup_info += f"📁 Konfigurationsdatei: cluster_cleanup.txt (im Programmverzeichnis)"
        self.cleanup_label.config(text=cleanup_info)
        messagebox.showinfo("Info", f"Konfiguration neu geladen.\n{len(self.cleanup_strings)} Cleanup-Strings gefunden.")
        
    def select_directory(self):
        """Open directory selection dialog."""
        directory = filedialog.askdirectory(title="Stammverzeichnis auswählen")
        if directory:
            self.root_dir.set(directory)
            self.analyze_project()
            
    def setup_logging(self, log_dir):
        """Setup logging to file."""
        log_file = os.path.join(log_dir, "renamer.log")
        
        # Create logger
        self.logger = logging.getLogger('PointCabRenamer')
        self.logger.setLevel(logging.INFO)
        
        # Clear existing handlers
        self.logger.handlers.clear()
        
        # File handler
        fh = logging.FileHandler(log_file, encoding='utf-8')
        fh.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        fh.setFormatter(formatter)
        self.logger.addHandler(fh)
        
        return log_file
        
    def find_pointcloud_folder(self, root_dir):
        """Find the PointCloud folder in the root directory."""
        root_name = os.path.basename(root_dir)
        expected_name = f"{root_name}_PointCloud"
        
        # Check for expected folder name
        expected_path = os.path.join(root_dir, expected_name)
        if os.path.isdir(expected_path):
            return expected_path
            
        # Search for any folder ending with _PointCloud
        for item in os.listdir(root_dir):
            item_path = os.path.join(root_dir, item)
            if os.path.isdir(item_path) and item.endswith("_PointCloud"):
                return item_path
                
        return None
        
    def find_lsdx_file(self, pointcloud_dir):
        """Find the LSDX file in the PointCloud directory."""
        for item in os.listdir(pointcloud_dir):
            if item.endswith(".lsdx"):
                return os.path.join(pointcloud_dir, item)
        return None
        
    def get_file_mapping(self, file_list, extension, scan_prefix):
        """Create mapping for files with full scan names."""
        # Extract numbers from filenames
        files_with_nums = []
        for f in file_list:
            match = re.match(r'^(\d+)\.' + extension + '$', f)
            if match:
                num = int(match.group(1))
                files_with_nums.append((num, f))
        
        if not files_with_nums:
            return {}, 2
            
        files_with_nums.sort(key=lambda x: x[0])
        max_num = max(num for num, _ in files_with_nums)
        
        # Determine padding width
        if max_num >= 100:
            width = 3
        else:
            width = 2
            
        # Create mapping with full scan name
        mapping = {}
        for num, old_name in files_with_nums:
            new_num_str = str(num).zfill(width)
            new_name = f"{scan_prefix}_{new_num_str}.{extension}"
            if old_name != new_name:
                mapping[old_name] = new_name
                
        return mapping, width
        
    def get_scan_name_mappings(self, lsdx_file, width, scan_prefix):
        """Get scan name mappings from LSDX file."""
        mappings = []
        try:
            tree = ET.parse(lsdx_file)
            root = tree.getroot()
            
            # Find all scan elements with name attribute
            for element in root.findall(".//Element[@type='scan']"):
                old_name = element.get('name')
                if old_name:
                    # Try to extract number from name
                    match = re.match(r'^(\d+)$', old_name)
                    if match:
                        num = int(match.group(1))
                        new_num_str = str(num).zfill(width)
                        new_name = f"{scan_prefix}_{new_num_str}"
                        if old_name != new_name:
                            mappings.append((old_name, new_name))
        except Exception as e:
            print(f"Fehler beim Lesen der Scan-Namen: {e}")
            
        return mappings
        
    def analyze_project(self):
        """Analyze the selected project directory."""
        root_dir = self.root_dir.get()
        
        if not root_dir or not os.path.isdir(root_dir):
            messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Verzeichnis aus.")
            return
            
        self.update_status("Analysiere Projekt...")
        self.progress.start()
        self.changes = []
        
        try:
            # Setup logging
            log_file = self.setup_logging(root_dir)
            self.logger.info(f"Analyse gestartet für: {root_dir}")
            
            # Find PointCloud folder
            self.pointcloud_dir = self.find_pointcloud_folder(root_dir)
            if not self.pointcloud_dir:
                messagebox.showerror("Fehler", "PointCloud-Ordner nicht gefunden!")
                self.progress.stop()
                self.update_status("Fehler: PointCloud-Ordner nicht gefunden")
                return
                
            # Find LSDX file
            self.lsdx_file = self.find_lsdx_file(self.pointcloud_dir)
            if not self.lsdx_file:
                messagebox.showerror("Fehler", "LSDX-Datei nicht gefunden!")
                self.progress.stop()
                self.update_status("Fehler: LSDX-Datei nicht gefunden")
                return
                
            # Find Previews folder
            self.preview_dir = os.path.join(self.pointcloud_dir, "Previews")
            if not os.path.isdir(self.preview_dir):
                self.preview_dir = None
                
            # Get root name and clean cluster name
            root_name = os.path.basename(root_dir)
            cluster_name, removed_strings = self.clean_cluster_name(root_name)
            scan_prefix = root_name
            
            # Update info
            info_text = f"Stammverzeichnis: {root_name}\n"
            info_text += f"PointCloud-Ordner: {os.path.basename(self.pointcloud_dir)}\n"
            info_text += f"LSDX-Datei: {os.path.basename(self.lsdx_file)}\n"
            info_text += f"Preview-Ordner: {'Gefunden' if self.preview_dir else 'Nicht gefunden'}\n"
            info_text += f"Clustername: {root_name} → {cluster_name}"
            if removed_strings:
                info_text += f" (entfernt: {', '.join(removed_strings)})"
            info_text += f"\nScan-Namen-Präfix: {scan_prefix} (unverändert)"
            self.info_label.config(text=info_text)
            
            # Collect LSD files
            lsd_files = [f for f in os.listdir(self.pointcloud_dir) if f.endswith('.lsd')]
            
            # Get mapping for LSD files
            lsd_mapping, lsd_width = self.get_file_mapping(lsd_files, 'lsd', scan_prefix)
                
            # Collect PNG files if preview folder exists
            png_mapping = {}
            png_width = lsd_width
            if self.preview_dir:
                png_files = [f for f in os.listdir(self.preview_dir) if f.endswith('.png')]
                png_mapping, png_width = self.get_file_mapping(png_files, 'png', scan_prefix)
                    
            # Get scan name mappings
            scan_name_mappings = self.get_scan_name_mappings(self.lsdx_file, lsd_width, scan_prefix)
            
            # Build changes list
            preview_text = "=== GEPLANTE ÄNDERUNGEN ===\n\n"
            
            # LSDX Backup
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_name = f"{os.path.basename(self.lsdx_file)}.backup_{timestamp}"
            preview_text += f"📁 BACKUP der LSDX-Datei:\n"
            preview_text += f"   → {backup_name}\n\n"
            self.changes.append(('backup', self.lsdx_file, os.path.join(self.pointcloud_dir, backup_name)))
            
            # LSD file renames
            if lsd_mapping:
                preview_text += f"📄 LSD-Dateien umbenennen ({len(lsd_mapping)} Dateien):\n"
                for old, new in sorted(lsd_mapping.items(), key=lambda x: x[0]):
                    preview_text += f"   {old} → {new}\n"
                    self.changes.append(('rename_lsd', 
                                        os.path.join(self.pointcloud_dir, old), 
                                        os.path.join(self.pointcloud_dir, new)))
                preview_text += "\n"
            else:
                preview_text += "📄 LSD-Dateien: Keine Umbenennung erforderlich\n\n"
                
            # PNG file renames
            if self.preview_dir and png_mapping:
                preview_text += f"🖼️ PNG-Dateien umbenennen ({len(png_mapping)} Dateien):\n"
                for old, new in sorted(png_mapping.items(), key=lambda x: x[0]):
                    preview_text += f"   {old} → {new}\n"
                    self.changes.append(('rename_png', 
                                        os.path.join(self.preview_dir, old), 
                                        os.path.join(self.preview_dir, new)))
                preview_text += "\n"
            elif self.preview_dir:
                preview_text += "🖼️ PNG-Dateien: Keine Umbenennung erforderlich\n\n"
            else:
                preview_text += "🖼️ PNG-Dateien: Preview-Ordner nicht vorhanden\n\n"
            
            # Scan name changes
            if scan_name_mappings:
                preview_text += f"🏷️ Scan-Namen in LSDX aktualisieren ({len(scan_name_mappings)} Scans):\n"
                for old_name, new_name in scan_name_mappings[:10]:
                    preview_text += f"   name=\"{old_name}\" → name=\"{new_name}\"\n"
                if len(scan_name_mappings) > 10:
                    preview_text += f"   ... und {len(scan_name_mappings) - 10} weitere\n"
                preview_text += "\n"
            else:
                preview_text += "🏷️ Scan-Namen: Keine Änderungen erforderlich\n\n"
                
            # LSDX updates
            preview_text += "📝 LSDX-Datei aktualisieren:\n"
            preview_text += f"   - Clustername: '{root_name}' → '{cluster_name}'"
            if removed_strings:
                preview_text += f" (entfernt: {', '.join(removed_strings)})"
            preview_text += "\n"
            preview_text += f"   - Dateinamen in FilePath-Elementen mit vollständigem Scan-Namen\n"
            preview_text += f"   - Beispiel: 1.lsd → {scan_prefix}_01.lsd\n"
            preview_text += f"   - Scan-Namen mit Präfix '{scan_prefix}_' versehen\n"
            
            self.changes.append(('update_lsdx', self.lsdx_file, {
                'cluster_name': cluster_name,
                'scan_prefix': scan_prefix,
                'original_name': root_name,
                'removed_strings': removed_strings,
                'lsd_width': lsd_width,
                'png_width': png_width if self.preview_dir else lsd_width
            }))
            
            preview_text += f"\n=== ZUSAMMENFASSUNG ===\n"
            preview_text += f"Gesamtänderungen: {len(self.changes)}\n"
            if removed_strings:
                preview_text += f"Bereinigte Strings: {', '.join(removed_strings)}\n"
            preview_text += f"Log-Datei: {log_file}\n"
            
            # Update preview
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete(1.0, tk.END)
            self.preview_text.insert(tk.END, preview_text)
            self.preview_text.config(state=tk.DISABLED)
            
            # Enable execute button if there are changes
            if self.changes:
                self.execute_btn.config(state=tk.NORMAL)
                
            self.logger.info(f"Analyse abgeschlossen: {len(self.changes)} Änderungen geplant")
            self.logger.info(f"Clustername: {root_name} → {cluster_name}")
            self.logger.info(f"Scan-Namen-Präfix: {scan_prefix}")
            if removed_strings:
                self.logger.info(f"Entfernte Strings: {', '.join(removed_strings)}")
            self.update_status(f"Analyse abgeschlossen: {len(self.changes)} Änderungen geplant")
            
        except Exception as e:
            messagebox.showerror("Fehler", f"Fehler bei der Analyse:\n{str(e)}")
            if self.logger:
                self.logger.error(f"Analysefehler: {str(e)}")
            self.update_status("Fehler bei der Analyse")
            
        finally:
            self.progress.stop()
            
    def execute_changes(self):
        """Execute all planned changes."""
        if not self.changes:
            messagebox.showinfo("Info", "Keine Änderungen zum Ausführen.")
            return
            
        # Confirmation dialog
        result = messagebox.askyesno(
            "Bestätigung",
            f"Möchten Sie wirklich {len(self.changes)} Änderungen ausführen?\n\n"
            "Ein Backup der LSDX-Datei wird automatisch erstellt."
        )
        
        if not result:
            return
            
        self.update_status("Führe Änderungen aus...")
        self.progress.start()
        self.execute_btn.config(state=tk.DISABLED)
        
        try:
            self.logger.info("Starte Ausführung der Änderungen")
            
            # First pass: backup and collect rename operations
            renames_lsd = []
            renames_png = []
            lsdx_update = None
            
            for change in self.changes:
                if change[0] == 'backup':
                    # Create backup
                    _, src, dst = change
                    shutil.copy2(src, dst)
                    self.logger.info(f"Backup erstellt: {dst}")
                elif change[0] == 'rename_lsd':
                    renames_lsd.append((change[1], change[2]))
                elif change[0] == 'rename_png':
                    renames_png.append((change[1], change[2]))
                elif change[0] == 'update_lsdx':
                    lsdx_update = change
                    
            # Execute renames with temporary names to avoid conflicts
            def safe_rename(rename_list, file_type):
                """Rename files using temporary names to avoid conflicts."""
                temp_suffix = f"_temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
                
                # First: rename to temporary names
                temp_mappings = []
                for src, dst in rename_list:
                    if os.path.exists(src):
                        temp_name = src + temp_suffix
                        os.rename(src, temp_name)
                        temp_mappings.append((temp_name, dst))
                        self.logger.info(f"{file_type}: {os.path.basename(src)} → temp")
                        
                # Second: rename from temporary to final names
                for temp_name, dst in temp_mappings:
                    os.rename(temp_name, dst)
                    self.logger.info(f"{file_type}: temp → {os.path.basename(dst)}")
                    
            safe_rename(renames_lsd, "LSD")
            safe_rename(renames_png, "PNG")
            
            # Update LSDX file
            if lsdx_update:
                self.update_lsdx_file(lsdx_update[1], lsdx_update[2])
                
            self.logger.info("Alle Änderungen erfolgreich ausgeführt")
            self.update_status("Alle Änderungen erfolgreich ausgeführt!")
            messagebox.showinfo("Erfolg", "Alle Änderungen wurden erfolgreich ausgeführt!")
            
            # Clear changes
            self.changes = []
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete(1.0, tk.END)
            self.preview_text.insert(tk.END, "Alle Änderungen wurden erfolgreich ausgeführt.\n\nSie können nun ein neues Projekt auswählen.")
            self.preview_text.config(state=tk.DISABLED)
            
        except Exception as e:
            messagebox.showerror("Fehler", f"Fehler bei der Ausführung:\n{str(e)}")
            self.logger.error(f"Ausführungsfehler: {str(e)}")
            self.update_status("Fehler bei der Ausführung")
            self.execute_btn.config(state=tk.NORMAL)
            
        finally:
            self.progress.stop()
            
    def update_lsdx_file(self, lsdx_path, params):
        """Update the LSDX file with new filenames, cluster name, and scan names."""
        cluster_name = params['cluster_name']
        scan_prefix = params.get('scan_prefix', params.get('original_name', cluster_name))
        original_name = params.get('original_name', cluster_name)
        removed_strings = params.get('removed_strings', [])
        lsd_width = params['lsd_width']
        png_width = params['png_width']
        
        # Parse XML
        tree = ET.parse(lsdx_path)
        root = tree.getroot()
        
        # Update cluster name
        for element in root.findall(".//Element[@type='cluster']"):
            old_cluster = element.get('name')
            element.set('name', cluster_name)
            self.logger.info(f"Clustername: '{old_cluster}' → '{cluster_name}'")
            if removed_strings:
                self.logger.info(f"  Entfernte Strings: {', '.join(removed_strings)}")
        
        # Update scan names with prefix
        for element in root.findall(".//Element[@type='scan']"):
            old_name = element.get('name')
            if old_name:
                match = re.match(r'^(\d+)$', old_name)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(lsd_width)
                    new_name = f"{scan_prefix}_{new_num_str}"
                    element.set('name', new_name)
                    self.logger.info(f"Scan-Name: '{old_name}' → '{new_name}'")
            
        # Update FilePath elements
        for filepath in root.findall(".//FilePath"):
            file_type = filepath.get('type')
            text = filepath.text
            
            if file_type == 'lsd' and text:
                match = re.match(r'^(\d+)\.lsd$', text)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(lsd_width)
                    new_name = f"{scan_prefix}_{new_num_str}.lsd"
                    filepath.text = new_name
                    self.logger.info(f"LSDX FilePath: {text} → {new_name}")
                    
            elif file_type == 'preview' and text:
                match = re.match(r'^Previews/(\d+)\.png$', text)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(png_width)
                    new_name = f"Previews/{scan_prefix}_{new_num_str}.png"
                    filepath.text = new_name
                    self.logger.info(f"LSDX FilePath: {text} → {new_name}")
                    
        # Write back
        tree.write(lsdx_path, encoding='utf-8', xml_declaration=True)
        
        # Add DOCTYPE back
        with open(lsdx_path, 'r', encoding='utf-8') as f:
            content = f.read()
            
        if '<!DOCTYPE LSDX>' not in content:
            content = content.replace(
                "<?xml version='1.0' encoding='utf-8'?>",
                "<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
            )
            with open(lsdx_path, 'w', encoding='utf-8') as f:
                f.write(content)
                
        self.logger.info("LSDX-Datei erfolgreich aktualisiert")
        
    def update_status(self, message):
        """Update the status label."""
        self.status_label.config(text=message)
        self.root.update_idletasks()


class BatchRenamer:
    """Batch Renamer for processing multiple PointCab projects."""
    
    def __init__(self, root, return_callback=None):
        self.root = root
        self.return_callback = return_callback
        self.root.title("PointCab Batch Renamer")
        self.root.geometry("900x700")
        self.root.minsize(800, 600)
        
        # Variables
        self.main_dir = tk.StringVar()
        self.projects = []
        self.cleanup_strings = []
        self.batch_logger = None
        self.processing = False
        
        # Load cleanup configuration
        self.load_cleanup_config()
        
        self.setup_ui()
        
    def load_cleanup_config(self):
        """Load cleanup strings from cluster_cleanup.txt."""
        config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cluster_cleanup.txt")
        self.cleanup_strings = []
        
        if os.path.exists(config_path):
            try:
                # Use utf-8-sig to handle BOM from Windows editors
                with open(config_path, 'r', encoding='utf-8-sig') as f:
                    for line in f:
                        # Strip whitespace including BOM characters
                        line = line.strip()
                        # Skip empty lines and comments
                        if not line:
                            continue
                        if line.startswith('#'):
                            continue
                        # Remove any remaining BOM or special chars
                        line = line.lstrip('\ufeff')
                        if line:
                            self.cleanup_strings.append(line)
                print(f"Cleanup-Konfiguration geladen: {len(self.cleanup_strings)} Einträge")
            except Exception as e:
                print(f"Fehler beim Laden der Cleanup-Konfiguration: {e}")
                
    def clean_cluster_name(self, name):
        """Remove cleanup strings from the cluster name."""
        cleaned_name = name
        removed_strings = []
        
        for cleanup_str in self.cleanup_strings:
            if cleanup_str in cleaned_name:
                removed_strings.append(cleanup_str)
                cleaned_name = cleaned_name.replace(cleanup_str, '')
                
        return cleaned_name, removed_strings
        
    def setup_ui(self):
        """Setup the batch renamer UI."""
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Back button
        if self.return_callback:
            back_frame = ttk.Frame(main_frame)
            back_frame.pack(fill=tk.X, pady=(0, 10))
            ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
        
        # Directory selection
        dir_frame = ttk.LabelFrame(main_frame, text="Hauptverzeichnis auswählen", padding="5")
        dir_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Entry(dir_frame, textvariable=self.main_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        ttk.Button(dir_frame, text="Durchsuchen...", command=self.select_directory).pack(side=tk.LEFT)
        ttk.Button(dir_frame, text="Projekte suchen", command=self.scan_for_projects).pack(side=tk.LEFT, padx=(5, 0))
        
        # Projects list
        projects_frame = ttk.LabelFrame(main_frame, text="Gefundene PointCab-Projekte", padding="5")
        projects_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        # Treeview for projects
        columns = ('status', 'project', 'cluster', 'scans')
        self.projects_tree = ttk.Treeview(projects_frame, columns=columns, show='headings', height=10)
        self.projects_tree.heading('status', text='Status')
        self.projects_tree.heading('project', text='Projekt')
        self.projects_tree.heading('cluster', text='Clustername')
        self.projects_tree.heading('scans', text='Scans')
        self.projects_tree.column('status', width=100)
        self.projects_tree.column('project', width=250)
        self.projects_tree.column('cluster', width=150)
        self.projects_tree.column('scans', width=80)
        
        scrollbar = ttk.Scrollbar(projects_frame, orient=tk.VERTICAL, command=self.projects_tree.yview)
        self.projects_tree.configure(yscrollcommand=scrollbar.set)
        
        self.projects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Info label
        self.info_label = ttk.Label(main_frame, text="Wählen Sie ein Hauptverzeichnis und klicken Sie auf 'Projekte suchen'.")
        self.info_label.pack(fill=tk.X, pady=(0, 10))
        
        # Progress section
        progress_frame = ttk.LabelFrame(main_frame, text="Fortschritt", padding="5")
        progress_frame.pack(fill=tk.X, pady=(0, 10))
        
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=(0, 5))
        
        self.progress_label = ttk.Label(progress_frame, text="")
        self.progress_label.pack(fill=tk.X)
        
        # Log area
        log_frame = ttk.LabelFrame(main_frame, text="Verarbeitungslog", padding="5")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=10, font=('Consolas', 9))
        self.log_text.pack(fill=tk.BOTH, expand=True)
        self.log_text.config(state=tk.DISABLED)
        
        # Buttons
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X)
        
        self.process_btn = ttk.Button(button_frame, text="Alle verarbeiten", command=self.process_all, state=tk.DISABLED)
        self.process_btn.pack(side=tk.RIGHT)
        
        ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
        
    def go_back(self):
        """Return to main menu."""
        if self.processing:
            messagebox.showwarning("Warnung", "Bitte warten Sie, bis die Verarbeitung abgeschlossen ist.")
            return
        if self.return_callback:
            self.return_callback()
            
    def select_directory(self):
        """Open directory selection dialog."""
        directory = filedialog.askdirectory(title="Hauptverzeichnis auswählen")
        if directory:
            self.main_dir.set(directory)
            
    def scan_for_projects(self):
        """Scan the main directory for PointCab projects."""
        main_dir = self.main_dir.get()
        
        if not main_dir or not os.path.isdir(main_dir):
            messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Hauptverzeichnis aus.")
            return
            
        self.projects = []
        self.projects_tree.delete(*self.projects_tree.get_children())
        
        # Scan for subdirectories with _PointCloud folder
        for item in os.listdir(main_dir):
            item_path = os.path.join(main_dir, item)
            if os.path.isdir(item_path):
                pointcloud_folder = self.find_pointcloud_folder(item_path)
                if pointcloud_folder:
                    lsdx_file = self.find_lsdx_file(pointcloud_folder)
                    if lsdx_file:
                        scan_count = self.count_lsd_files(pointcloud_folder)
                        cluster_name, _ = self.clean_cluster_name(item)
                        
                        self.projects.append({
                            'path': item_path,
                            'name': item,
                            'pointcloud': pointcloud_folder,
                            'lsdx': lsdx_file,
                            'cluster_name': cluster_name,
                            'scan_count': scan_count,
                            'status': 'Ausstehend'
                        })
                        
                        self.projects_tree.insert('', tk.END, values=(
                            'Ausstehend', item, cluster_name, scan_count
                        ))
                        
        if self.projects:
            self.info_label.config(text=f"{len(self.projects)} PointCab-Projekte gefunden.")
            self.process_btn.config(state=tk.NORMAL)
        else:
            self.info_label.config(text="Keine PointCab-Projekte gefunden.")
            self.process_btn.config(state=tk.DISABLED)
            
    def find_pointcloud_folder(self, root_dir):
        """Find the PointCloud folder in the root directory."""
        root_name = os.path.basename(root_dir)
        expected_name = f"{root_name}_PointCloud"
        
        expected_path = os.path.join(root_dir, expected_name)
        if os.path.isdir(expected_path):
            return expected_path
            
        for item in os.listdir(root_dir):
            item_path = os.path.join(root_dir, item)
            if os.path.isdir(item_path) and item.endswith("_PointCloud"):
                return item_path
                
        return None
        
    def find_lsdx_file(self, pointcloud_dir):
        """Find the LSDX file in the PointCloud directory."""
        for item in os.listdir(pointcloud_dir):
            if item.endswith(".lsdx"):
                return os.path.join(pointcloud_dir, item)
        return None
        
    def count_lsd_files(self, pointcloud_dir):
        """Count LSD files in directory."""
        return len([f for f in os.listdir(pointcloud_dir) if f.endswith('.lsd')])
        
    def setup_batch_logging(self, main_dir):
        """Setup batch summary logging."""
        log_file = os.path.join(main_dir, "batch_summary.log")
        
        self.batch_logger = logging.getLogger('BatchRenamer')
        self.batch_logger.setLevel(logging.INFO)
        self.batch_logger.handlers.clear()
        
        fh = logging.FileHandler(log_file, encoding='utf-8')
        fh.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        fh.setFormatter(formatter)
        self.batch_logger.addHandler(fh)
        
        return log_file
        
    def log_message(self, message, level='info'):
        """Add message to log display and batch log."""
        self.log_text.config(state=tk.NORMAL)
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)
        
        if self.batch_logger:
            if level == 'error':
                self.batch_logger.error(message)
            else:
                self.batch_logger.info(message)
                
        self.root.update_idletasks()
        
    def update_project_status(self, index, status):
        """Update project status in treeview."""
        item_id = self.projects_tree.get_children()[index]
        values = list(self.projects_tree.item(item_id)['values'])
        values[0] = status
        self.projects_tree.item(item_id, values=values)
        self.root.update_idletasks()
        
    def process_all(self):
        """Process all found projects."""
        if not self.projects:
            messagebox.showinfo("Info", "Keine Projekte zum Verarbeiten.")
            return
            
        result = messagebox.askyesno(
            "Bestätigung",
            f"Möchten Sie wirklich {len(self.projects)} Projekte verarbeiten?\n\n"
            "Ein Backup der LSDX-Dateien wird automatisch erstellt."
        )
        
        if not result:
            return
            
        self.processing = True
        self.process_btn.config(state=tk.DISABLED)
        
        # Clear log
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state=tk.DISABLED)
        
        # Setup batch logging
        batch_log = self.setup_batch_logging(self.main_dir.get())
        
        self.log_message(f"=== Batch-Verarbeitung gestartet ===")
        self.log_message(f"Hauptverzeichnis: {self.main_dir.get()}")
        self.log_message(f"Anzahl Projekte: {len(self.projects)}")
        self.log_message(f"Batch-Log: {batch_log}")
        self.log_message("")
        
        success_count = 0
        error_count = 0
        
        for i, project in enumerate(self.projects):
            self.progress_var.set((i / len(self.projects)) * 100)
            self.progress_label.config(text=f"Verarbeite {i+1}/{len(self.projects)}: {project['name']}")
            self.update_project_status(i, 'Verarbeite...')
            
            self.log_message(f"--- Projekt: {project['name']} ---")
            
            try:
                self.process_single_project(project)
                self.update_project_status(i, '✓ Erfolgreich')
                self.log_message(f"✓ {project['name']} erfolgreich verarbeitet")
                success_count += 1
            except Exception as e:
                self.update_project_status(i, '✗ Fehler')
                self.log_message(f"✗ Fehler bei {project['name']}: {str(e)}", 'error')
                error_count += 1
                
            self.log_message("")
            
        self.progress_var.set(100)
        self.progress_label.config(text="Fertig")
        
        # Summary
        self.log_message("=== ZUSAMMENFASSUNG ===")
        self.log_message(f"Gesamt: {len(self.projects)}")
        self.log_message(f"Erfolgreich: {success_count}")
        self.log_message(f"Fehlgeschlagen: {error_count}")
        
        self.processing = False
        
        messagebox.showinfo(
            "Batch-Verarbeitung abgeschlossen",
            f"Verarbeitung abgeschlossen!\n\n"
            f"Erfolgreich: {success_count}\n"
            f"Fehlgeschlagen: {error_count}\n\n"
            f"Details siehe: {batch_log}"
        )
        
    def process_single_project(self, project):
        """Process a single project without GUI interaction."""
        root_dir = project['path']
        pointcloud_dir = project['pointcloud']
        lsdx_file = project['lsdx']
        
        # Setup project-specific logging
        project_logger = logging.getLogger(f'Project_{project["name"]}')
        project_logger.setLevel(logging.INFO)
        project_logger.handlers.clear()
        
        log_file = os.path.join(root_dir, "renamer.log")
        fh = logging.FileHandler(log_file, encoding='utf-8')
        fh.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        fh.setFormatter(formatter)
        project_logger.addHandler(fh)
        
        project_logger.info(f"Batch-Verarbeitung gestartet für: {root_dir}")
        
        # Get names
        root_name = os.path.basename(root_dir)
        cluster_name, removed_strings = self.clean_cluster_name(root_name)
        scan_prefix = root_name
        
        project_logger.info(f"Clustername: {root_name} → {cluster_name}")
        project_logger.info(f"Scan-Namen-Präfix: {scan_prefix}")
        if removed_strings:
            project_logger.info(f"Entfernte Strings: {', '.join(removed_strings)}")
            
        # Find preview dir
        preview_dir = os.path.join(pointcloud_dir, "Previews")
        if not os.path.isdir(preview_dir):
            preview_dir = None
            
        # Get LSD files and mapping
        lsd_files = [f for f in os.listdir(pointcloud_dir) if f.endswith('.lsd')]
        lsd_mapping, lsd_width = self.get_file_mapping(lsd_files, 'lsd', scan_prefix)
        
        # Get PNG files and mapping
        png_mapping = {}
        png_width = lsd_width
        if preview_dir:
            png_files = [f for f in os.listdir(preview_dir) if f.endswith('.png')]
            png_mapping, png_width = self.get_file_mapping(png_files, 'png', scan_prefix)
                
        # Create backup
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_name = f"{os.path.basename(lsdx_file)}.backup_{timestamp}"
        backup_path = os.path.join(pointcloud_dir, backup_name)
        shutil.copy2(lsdx_file, backup_path)
        project_logger.info(f"Backup erstellt: {backup_name}")
        
        # Rename LSD files
        if lsd_mapping:
            self.safe_rename(pointcloud_dir, lsd_mapping, project_logger, "LSD")
            
        # Rename PNG files
        if preview_dir and png_mapping:
            self.safe_rename(preview_dir, png_mapping, project_logger, "PNG")
            
        # Update LSDX file
        self.update_lsdx_file(lsdx_file, {
            'cluster_name': cluster_name,
            'scan_prefix': scan_prefix,
            'original_name': root_name,
            'removed_strings': removed_strings,
            'lsd_width': lsd_width,
            'png_width': png_width
        }, project_logger)
        
        project_logger.info("Verarbeitung abgeschlossen")
        
    def get_file_mapping(self, file_list, extension, scan_prefix):
        """Create mapping for files with full scan names."""
        files_with_nums = []
        for f in file_list:
            match = re.match(r'^(\d+)\.' + extension + '$', f)
            if match:
                num = int(match.group(1))
                files_with_nums.append((num, f))
        
        if not files_with_nums:
            return {}, 2
            
        files_with_nums.sort(key=lambda x: x[0])
        max_num = max(num for num, _ in files_with_nums)
        
        width = 3 if max_num >= 100 else 2
            
        mapping = {}
        for num, old_name in files_with_nums:
            new_num_str = str(num).zfill(width)
            new_name = f"{scan_prefix}_{new_num_str}.{extension}"
            if old_name != new_name:
                mapping[old_name] = new_name
                
        return mapping, width
        
    def safe_rename(self, directory, mapping, logger, file_type):
        """Rename files using temporary names to avoid conflicts."""
        temp_suffix = f"_temp_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        
        temp_mappings = []
        for old_name, new_name in mapping.items():
            src = os.path.join(directory, old_name)
            if os.path.exists(src):
                temp_name = src + temp_suffix
                os.rename(src, temp_name)
                temp_mappings.append((temp_name, os.path.join(directory, new_name)))
                logger.info(f"{file_type}: {old_name} → temp")
                
        for temp_name, dst in temp_mappings:
            os.rename(temp_name, dst)
            logger.info(f"{file_type}: temp → {os.path.basename(dst)}")
            
    def update_lsdx_file(self, lsdx_path, params, logger):
        """Update the LSDX file with new filenames, cluster name, and scan names."""
        cluster_name = params['cluster_name']
        scan_prefix = params.get('scan_prefix', params.get('original_name', cluster_name))
        removed_strings = params.get('removed_strings', [])
        lsd_width = params['lsd_width']
        png_width = params['png_width']
        
        tree = ET.parse(lsdx_path)
        root = tree.getroot()
        
        # Update cluster name
        for element in root.findall(".//Element[@type='cluster']"):
            old_cluster = element.get('name')
            element.set('name', cluster_name)
            logger.info(f"Clustername: '{old_cluster}' → '{cluster_name}'")
            if removed_strings:
                logger.info(f"  Entfernte Strings: {', '.join(removed_strings)}")
        
        # Update scan names with prefix
        for element in root.findall(".//Element[@type='scan']"):
            old_name = element.get('name')
            if old_name:
                match = re.match(r'^(\d+)$', old_name)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(lsd_width)
                    new_name = f"{scan_prefix}_{new_num_str}"
                    element.set('name', new_name)
                    logger.info(f"Scan-Name: '{old_name}' → '{new_name}'")
            
        # Update FilePath elements
        for filepath in root.findall(".//FilePath"):
            file_type = filepath.get('type')
            text = filepath.text
            
            if file_type == 'lsd' and text:
                match = re.match(r'^(\d+)\.lsd$', text)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(lsd_width)
                    new_name = f"{scan_prefix}_{new_num_str}.lsd"
                    filepath.text = new_name
                    logger.info(f"LSDX FilePath: {text} → {new_name}")
                    
            elif file_type == 'preview' and text:
                match = re.match(r'^Previews/(\d+)\.png$', text)
                if match:
                    num = int(match.group(1))
                    new_num_str = str(num).zfill(png_width)
                    new_name = f"Previews/{scan_prefix}_{new_num_str}.png"
                    filepath.text = new_name
                    logger.info(f"LSDX FilePath: {text} → {new_name}")
                    
        tree.write(lsdx_path, encoding='utf-8', xml_declaration=True)
        
        # Add DOCTYPE back
        with open(lsdx_path, 'r', encoding='utf-8') as f:
            content = f.read()
            
        if '<!DOCTYPE LSDX>' not in content:
            content = content.replace(
                "<?xml version='1.0' encoding='utf-8'?>",
                "<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
            )
            with open(lsdx_path, 'w', encoding='utf-8') as f:
                f.write(content)
                
        logger.info("LSDX-Datei erfolgreich aktualisiert")


class ProjectMerger:
    """Project Merger for combining multiple PointCab projects into one."""
    
    def __init__(self, root, return_callback=None):
        self.root = root
        self.return_callback = return_callback
        self.root.title("PointCab Projektmerger")
        self.root.geometry("1000x800")
        self.root.minsize(900, 700)
        
        # Variables
        self.target_dir = tk.StringVar()
        self.source_dir = tk.StringVar()
        self.merge_mode = tk.StringVar(value="single")
        self.target_project = None
        self.source_projects = []
        self.merge_logger = None
        self.processing = False
        
        self.setup_ui()
        
    def setup_ui(self):
        """Setup the project merger UI."""
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Back button
        if self.return_callback:
            back_frame = ttk.Frame(main_frame)
            back_frame.pack(fill=tk.X, pady=(0, 10))
            ttk.Button(back_frame, text="← Zurück zum Hauptmenü", command=self.go_back).pack(side=tk.LEFT)
        
        # Target project selection
        target_frame = ttk.LabelFrame(main_frame, text="🎯 Stammprojekt (Ziel)", padding="5")
        target_frame.pack(fill=tk.X, pady=(0, 10))
        
        target_entry_frame = ttk.Frame(target_frame)
        target_entry_frame.pack(fill=tk.X)
        ttk.Entry(target_entry_frame, textvariable=self.target_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        ttk.Button(target_entry_frame, text="Durchsuchen...", command=self.select_target).pack(side=tk.LEFT)
        ttk.Button(target_entry_frame, text="Analysieren", command=self.analyze_target).pack(side=tk.LEFT, padx=(5, 0))
        
        self.target_info = ttk.Label(target_frame, text="Bitte Stammprojekt auswählen", foreground="gray")
        self.target_info.pack(fill=tk.X, pady=(5, 0))
        
        # Mode selection
        mode_frame = ttk.LabelFrame(main_frame, text="Merge-Modus", padding="5")
        mode_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Radiobutton(mode_frame, text="📄 Einzelprojekt hinzufügen", variable=self.merge_mode, 
                        value="single", command=self.on_mode_change).pack(side=tk.LEFT, padx=(0, 20))
        ttk.Radiobutton(mode_frame, text="📂 Batch-Merge (mehrere Projekte)", variable=self.merge_mode, 
                        value="batch", command=self.on_mode_change).pack(side=tk.LEFT)
        
        # Source project selection
        source_frame = ttk.LabelFrame(main_frame, text="📁 Quellprojekt(e)", padding="5")
        source_frame.pack(fill=tk.X, pady=(0, 10))
        
        source_entry_frame = ttk.Frame(source_frame)
        source_entry_frame.pack(fill=tk.X)
        ttk.Entry(source_entry_frame, textvariable=self.source_dir, width=60).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        ttk.Button(source_entry_frame, text="Durchsuchen...", command=self.select_source).pack(side=tk.LEFT)
        ttk.Button(source_entry_frame, text="Projekte suchen", command=self.scan_source).pack(side=tk.LEFT, padx=(5, 0))
        
        self.source_mode_label = ttk.Label(source_frame, text="Wählen Sie ein Quellprojekt aus", foreground="gray")
        self.source_mode_label.pack(fill=tk.X, pady=(5, 0))
        
        # Source projects list
        projects_frame = ttk.LabelFrame(main_frame, text="Zu mergende Projekte", padding="5")
        projects_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        columns = ('status', 'project', 'scans', 'lsd_files', 'png_files')
        self.projects_tree = ttk.Treeview(projects_frame, columns=columns, show='headings', height=8)
        self.projects_tree.heading('status', text='Status')
        self.projects_tree.heading('project', text='Projekt')
        self.projects_tree.heading('scans', text='Scans')
        self.projects_tree.heading('lsd_files', text='LSD-Dateien')
        self.projects_tree.heading('png_files', text='PNG-Dateien')
        self.projects_tree.column('status', width=100)
        self.projects_tree.column('project', width=250)
        self.projects_tree.column('scans', width=80)
        self.projects_tree.column('lsd_files', width=100)
        self.projects_tree.column('png_files', width=100)
        
        scrollbar = ttk.Scrollbar(projects_frame, orient=tk.VERTICAL, command=self.projects_tree.yview)
        self.projects_tree.configure(yscrollcommand=scrollbar.set)
        
        self.projects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Preview frame
        preview_frame = ttk.LabelFrame(main_frame, text="Vorschau der Merge-Operationen", padding="5")
        preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        self.preview_text = scrolledtext.ScrolledText(preview_frame, wrap=tk.WORD, height=10, font=('Consolas', 9))
        self.preview_text.pack(fill=tk.BOTH, expand=True)
        self.preview_text.config(state=tk.DISABLED)
        
        # Progress section
        progress_frame = ttk.LabelFrame(main_frame, text="Fortschritt", padding="5")
        progress_frame.pack(fill=tk.X, pady=(0, 10))
        
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=(0, 5))
        
        self.progress_label = ttk.Label(progress_frame, text="")
        self.progress_label.pack(fill=tk.X)
        
        # Buttons
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X)
        
        self.preview_btn = ttk.Button(button_frame, text="Vorschau", command=self.show_preview, state=tk.DISABLED)
        self.preview_btn.pack(side=tk.RIGHT, padx=(5, 0))
        
        self.merge_btn = ttk.Button(button_frame, text="🔀 Merge durchführen", command=self.execute_merge, state=tk.DISABLED)
        self.merge_btn.pack(side=tk.RIGHT)
        
        ttk.Button(button_frame, text="Beenden", command=self.root.quit).pack(side=tk.LEFT)
        
    def go_back(self):
        """Return to main menu."""
        if self.processing:
            messagebox.showwarning("Warnung", "Bitte warten Sie, bis der Merge abgeschlossen ist.")
            return
        if self.return_callback:
            self.return_callback()
            
    def on_mode_change(self):
        """Handle mode change."""
        if self.merge_mode.get() == "single":
            self.source_mode_label.config(text="Wählen Sie ein einzelnes Quellprojekt aus")
        else:
            self.source_mode_label.config(text="Wählen Sie ein Hauptverzeichnis mit mehreren Quellprojekten aus")
        self.source_projects = []
        self.projects_tree.delete(*self.projects_tree.get_children())
        self.update_buttons()
        
    def select_target(self):
        """Select target project directory."""
        directory = filedialog.askdirectory(title="Stammprojekt auswählen")
        if directory:
            self.target_dir.set(directory)
            self.analyze_target()
            
    def select_source(self):
        """Select source project/directory."""
        directory = filedialog.askdirectory(title="Quellprojekt(e) auswählen")
        if directory:
            self.source_dir.set(directory)
            
    def find_pointcloud_folder(self, root_dir):
        """Find the PointCloud folder in the root directory."""
        root_name = os.path.basename(root_dir)
        expected_name = f"{root_name}_PointCloud"
        
        expected_path = os.path.join(root_dir, expected_name)
        if os.path.isdir(expected_path):
            return expected_path
            
        for item in os.listdir(root_dir):
            item_path = os.path.join(root_dir, item)
            if os.path.isdir(item_path) and item.endswith("_PointCloud"):
                return item_path
                
        return None
        
    def find_lsdx_file(self, pointcloud_dir):
        """Find the LSDX file in the PointCloud directory."""
        for item in os.listdir(pointcloud_dir):
            if item.endswith(".lsdx"):
                return os.path.join(pointcloud_dir, item)
        return None
        
    def count_files(self, directory, extension):
        """Count files with given extension."""
        if not directory or not os.path.isdir(directory):
            return 0
        return len([f for f in os.listdir(directory) if f.endswith(extension)])
        
    def get_files(self, directory, extension):
        """Get list of files with given extension."""
        if not directory or not os.path.isdir(directory):
            return []
        return [f for f in os.listdir(directory) if f.endswith(extension)]
        
    def analyze_target(self):
        """Analyze the target project."""
        target_dir = self.target_dir.get()
        
        if not target_dir or not os.path.isdir(target_dir):
            messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Stammprojekt aus.")
            return
            
        pointcloud_dir = self.find_pointcloud_folder(target_dir)
        if not pointcloud_dir:
            messagebox.showerror("Fehler", "Kein PointCloud-Ordner im Stammprojekt gefunden!")
            self.target_project = None
            self.update_buttons()
            return
            
        lsdx_file = self.find_lsdx_file(pointcloud_dir)
        if not lsdx_file:
            messagebox.showerror("Fehler", "Keine LSDX-Datei im Stammprojekt gefunden!")
            self.target_project = None
            self.update_buttons()
            return
            
        preview_dir = os.path.join(pointcloud_dir, "Previews")
        if not os.path.isdir(preview_dir):
            preview_dir = None
            
        lsd_count = self.count_files(pointcloud_dir, '.lsd')
        png_count = self.count_files(preview_dir, '.png') if preview_dir else 0
        
        # Count scans in LSDX
        scan_count = 0
        try:
            tree = ET.parse(lsdx_file)
            root = tree.getroot()
            scan_count = len(root.findall(".//Element[@type='scan']"))
        except:
            pass
            
        self.target_project = {
            'path': target_dir,
            'name': os.path.basename(target_dir),
            'pointcloud': pointcloud_dir,
            'preview': preview_dir,
            'lsdx': lsdx_file,
            'scan_count': scan_count,
            'lsd_count': lsd_count,
            'png_count': png_count
        }
        
        info = f"✓ {self.target_project['name']} | Scans: {scan_count} | LSD: {lsd_count} | PNG: {png_count}"
        self.target_info.config(text=info, foreground="green")
        self.update_buttons()
        
    def scan_source(self):
        """Scan source directory for projects."""
        source_dir = self.source_dir.get()
        
        if not source_dir or not os.path.isdir(source_dir):
            messagebox.showerror("Fehler", "Bitte wählen Sie ein gültiges Quellverzeichnis aus.")
            return
            
        self.source_projects = []
        self.projects_tree.delete(*self.projects_tree.get_children())
        
        if self.merge_mode.get() == "single":
            # Single project mode
            project = self.analyze_source_project(source_dir)
            if project:
                self.source_projects.append(project)
                self.projects_tree.insert('', tk.END, values=(
                    'Bereit', project['name'], project['scan_count'], 
                    project['lsd_count'], project['png_count']
                ))
            else:
                messagebox.showerror("Fehler", "Kein gültiges PointCab-Projekt gefunden!")
        else:
            # Batch mode - scan subdirectories
            for item in sorted(os.listdir(source_dir)):
                item_path = os.path.join(source_dir, item)
                if os.path.isdir(item_path):
                    # Skip if this is the target project
                    if self.target_project and os.path.samefile(item_path, self.target_project['path']):
                        continue
                    project = self.analyze_source_project(item_path)
                    if project:
                        self.source_projects.append(project)
                        self.projects_tree.insert('', tk.END, values=(
                            'Bereit', project['name'], project['scan_count'],
                            project['lsd_count'], project['png_count']
                        ))
                        
        if self.source_projects:
            self.source_mode_label.config(text=f"{len(self.source_projects)} Quellprojekt(e) gefunden", foreground="green")
        else:
            self.source_mode_label.config(text="Keine gültigen Quellprojekte gefunden", foreground="red")
            
        self.update_buttons()
        
    def analyze_source_project(self, project_dir):
        """Analyze a source project and return its info."""
        pointcloud_dir = self.find_pointcloud_folder(project_dir)
        if not pointcloud_dir:
            return None
            
        lsdx_file = self.find_lsdx_file(pointcloud_dir)
        if not lsdx_file:
            return None
            
        preview_dir = os.path.join(pointcloud_dir, "Previews")
        if not os.path.isdir(preview_dir):
            preview_dir = None
            
        lsd_count = self.count_files(pointcloud_dir, '.lsd')
        png_count = self.count_files(preview_dir, '.png') if preview_dir else 0
        
        # Count scans in LSDX
        scan_count = 0
        try:
            tree = ET.parse(lsdx_file)
            root = tree.getroot()
            scan_count = len(root.findall(".//Element[@type='scan']"))
        except:
            pass
            
        return {
            'path': project_dir,
            'name': os.path.basename(project_dir),
            'pointcloud': pointcloud_dir,
            'preview': preview_dir,
            'lsdx': lsdx_file,
            'scan_count': scan_count,
            'lsd_count': lsd_count,
            'png_count': png_count,
            'lsd_files': self.get_files(pointcloud_dir, '.lsd'),
            'png_files': self.get_files(preview_dir, '.png') if preview_dir else []
        }
        
    def update_buttons(self):
        """Update button states based on current selection."""
        if self.target_project and self.source_projects:
            self.preview_btn.config(state=tk.NORMAL)
            self.merge_btn.config(state=tk.NORMAL)
        else:
            self.preview_btn.config(state=tk.DISABLED)
            self.merge_btn.config(state=tk.DISABLED)
            
    def get_conflict_resolved_name(self, filename, existing_files, suffix_counter):
        """Get a conflict-resolved filename."""
        base, ext = os.path.splitext(filename)
        new_name = filename
        
        while new_name in existing_files:
            suffix_counter[0] += 1
            new_name = f"{base}_merged_{suffix_counter[0]}{ext}"
            
        return new_name
        
    def show_preview(self):
        """Show preview of merge operations."""
        if not self.target_project or not self.source_projects:
            return
            
        preview_text = "=== MERGE-VORSCHAU ===\n\n"
        preview_text += f"🎯 STAMMPROJEKT: {self.target_project['name']}\n"
        preview_text += f"   Aktuelle Scans: {self.target_project['scan_count']}\n"
        preview_text += f"   LSD-Dateien: {self.target_project['lsd_count']}\n"
        preview_text += f"   PNG-Dateien: {self.target_project['png_count']}\n\n"
        
        # Get existing files in target
        existing_lsd = set(self.get_files(self.target_project['pointcloud'], '.lsd'))
        existing_png = set(self.get_files(self.target_project['preview'], '.png')) if self.target_project['preview'] else set()
        
        total_new_scans = 0
        total_new_lsd = 0
        total_new_png = 0
        
        for project in self.source_projects:
            preview_text += f"📁 QUELLPROJEKT: {project['name']}\n"
            preview_text += f"   Scans: {project['scan_count']}\n"
            
            # Check for LSD conflicts
            lsd_conflicts = 0
            for lsd in project.get('lsd_files', []):
                if lsd in existing_lsd:
                    lsd_conflicts += 1
                    
            preview_text += f"   LSD-Dateien: {project['lsd_count']}"
            if lsd_conflicts > 0:
                preview_text += f" ({lsd_conflicts} Namenskonflikte → werden umbenannt)"
            preview_text += "\n"
            
            # Check for PNG conflicts
            png_conflicts = 0
            for png in project.get('png_files', []):
                if png in existing_png:
                    png_conflicts += 1
                    
            preview_text += f"   PNG-Dateien: {project['png_count']}"
            if png_conflicts > 0:
                preview_text += f" ({png_conflicts} Namenskonflikte → werden umbenannt)"
            preview_text += "\n\n"
            
            total_new_scans += project['scan_count']
            total_new_lsd += project['lsd_count']
            total_new_png += project['png_count']
            
            # Add to existing for next iteration
            for lsd in project.get('lsd_files', []):
                existing_lsd.add(lsd)
            for png in project.get('png_files', []):
                existing_png.add(png)
                
        preview_text += "=== ZUSAMMENFASSUNG ===\n"
        preview_text += f"Neue Scans: {total_new_scans}\n"
        preview_text += f"Neue LSD-Dateien: {total_new_lsd}\n"
        preview_text += f"Neue PNG-Dateien: {total_new_png}\n"
        preview_text += f"\nNach Merge:\n"
        preview_text += f"  Scans gesamt: {self.target_project['scan_count'] + total_new_scans}\n"
        preview_text += f"  LSD-Dateien: {self.target_project['lsd_count'] + total_new_lsd}\n"
        preview_text += f"  PNG-Dateien: {self.target_project['png_count'] + total_new_png}\n"
        
        preview_text += "\n⚠️ HINWEIS:\n"
        preview_text += "  - Ein Backup der Stammprojekt-LSDX wird erstellt\n"
        preview_text += "  - Bei Namenskonflikten werden Dateien mit Suffix '_merged_N' umbenannt\n"
        preview_text += "  - Alle Referenzen in der LSDX werden entsprechend aktualisiert\n"
        
        self.preview_text.config(state=tk.NORMAL)
        self.preview_text.delete(1.0, tk.END)
        self.preview_text.insert(tk.END, preview_text)
        self.preview_text.config(state=tk.DISABLED)
        
    def setup_merge_logging(self, target_dir):
        """Setup merge logging."""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        log_file = os.path.join(target_dir, f"merge_{timestamp}.log")
        
        self.merge_logger = logging.getLogger(f'ProjectMerger_{timestamp}')
        self.merge_logger.setLevel(logging.INFO)
        self.merge_logger.handlers.clear()
        
        fh = logging.FileHandler(log_file, encoding='utf-8')
        fh.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        fh.setFormatter(formatter)
        self.merge_logger.addHandler(fh)
        
        return log_file
        
    def log_message(self, message, level='info'):
        """Log message."""
        if self.merge_logger:
            if level == 'error':
                self.merge_logger.error(message)
            else:
                self.merge_logger.info(message)
                
    def update_project_status(self, index, status):
        """Update project status in treeview."""
        if index < len(self.projects_tree.get_children()):
            item_id = self.projects_tree.get_children()[index]
            values = list(self.projects_tree.item(item_id)['values'])
            values[0] = status
            self.projects_tree.item(item_id, values=values)
            self.root.update_idletasks()
            
    def execute_merge(self):
        """Execute the merge operation."""
        if not self.target_project or not self.source_projects:
            return
            
        result = messagebox.askyesno(
            "Bestätigung",
            f"Möchten Sie wirklich {len(self.source_projects)} Projekt(e) in das Stammprojekt mergen?\n\n"
            "Ein Backup der LSDX-Datei wird automatisch erstellt."
        )
        
        if not result:
            return
            
        self.processing = True
        self.merge_btn.config(state=tk.DISABLED)
        self.preview_btn.config(state=tk.DISABLED)
        
        # Setup logging
        log_file = self.setup_merge_logging(self.target_project['path'])
        
        self.log_message("=== MERGE GESTARTET ===")
        self.log_message(f"Stammprojekt: {self.target_project['name']}")
        self.log_message(f"Quellprojekte: {len(self.source_projects)}")
        self.log_message(f"Log-Datei: {log_file}")
        
        # Update preview with log
        self.preview_text.config(state=tk.NORMAL)
        self.preview_text.delete(1.0, tk.END)
        self.preview_text.insert(tk.END, f"=== MERGE LÄUFT ===\n\nLog: {log_file}\n\n")
        
        try:
            # Create backup of target LSDX
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_path = f"{self.target_project['lsdx']}.backup_{timestamp}"
            shutil.copy2(self.target_project['lsdx'], backup_path)
            self.log_message(f"Backup erstellt: {backup_path}")
            self.preview_text.insert(tk.END, f"✓ Backup erstellt\n")
            
            # Parse target LSDX
            target_tree = ET.parse(self.target_project['lsdx'])
            target_root = target_tree.getroot()
            target_elements = target_root.find('Elements')
            
            # Get existing files
            existing_lsd = set(self.get_files(self.target_project['pointcloud'], '.lsd'))
            existing_png = set(self.get_files(self.target_project['preview'], '.png')) if self.target_project['preview'] else set()
            
            # Track UUID mappings for parent references
            suffix_counter = [0]
            
            success_count = 0
            error_count = 0
            
            for i, project in enumerate(self.source_projects):
                self.progress_var.set((i / len(self.source_projects)) * 100)
                self.progress_label.config(text=f"Merging {i+1}/{len(self.source_projects)}: {project['name']}")
                self.update_project_status(i, 'Verarbeite...')
                self.root.update_idletasks()
                
                try:
                    self.merge_single_project(
                        project, 
                        target_elements, 
                        existing_lsd, 
                        existing_png, 
                        suffix_counter
                    )
                    self.update_project_status(i, '✓ Erfolgreich')
                    self.preview_text.insert(tk.END, f"✓ {project['name']} gemerged\n")
                    success_count += 1
                except Exception as e:
                    self.update_project_status(i, '✗ Fehler')
                    self.preview_text.insert(tk.END, f"✗ Fehler bei {project['name']}: {e}\n")
                    self.log_message(f"Fehler bei {project['name']}: {e}", 'error')
                    error_count += 1
                    
                self.preview_text.see(tk.END)
                self.root.update_idletasks()
                
            # Zähle finale Elemente vor dem Speichern
            final_clusters = len(target_elements.findall("Element[@type='cluster']"))
            final_scans = len(target_elements.findall("Element[@type='scan']"))
            self.log_message(f"")
            self.log_message(f"{'='*60}")
            self.log_message(f"FINALE LSDX STATISTIK")
            self.log_message(f"{'='*60}")
            self.log_message(f"  Cluster gesamt: {final_clusters}")
            self.log_message(f"  Scans gesamt: {final_scans}")
            
            # Save merged LSDX
            target_tree.write(self.target_project['lsdx'], encoding='utf-8', xml_declaration=True)
            
            # Add DOCTYPE back
            with open(self.target_project['lsdx'], 'r', encoding='utf-8') as f:
                content = f.read()
            if '<!DOCTYPE LSDX>' not in content:
                content = content.replace(
                    "<?xml version='1.0' encoding='utf-8'?>",
                    "<?xml version='1.0' encoding='utf-8'?>\n<!DOCTYPE LSDX>"
                )
                with open(self.target_project['lsdx'], 'w', encoding='utf-8') as f:
                    f.write(content)
                    
            self.log_message("LSDX-Datei gespeichert")
            self.log_message(f"Pfad: {self.target_project['lsdx']}")
            
            self.progress_var.set(100)
            self.progress_label.config(text="Fertig")
            
            # Summary
            self.log_message("=== MERGE ABGESCHLOSSEN ===")
            self.log_message(f"Erfolgreich: {success_count}")
            self.log_message(f"Fehlgeschlagen: {error_count}")
            
            self.preview_text.insert(tk.END, f"\n=== ZUSAMMENFASSUNG ===\n")
            self.preview_text.insert(tk.END, f"Erfolgreich: {success_count}\n")
            self.preview_text.insert(tk.END, f"Fehlgeschlagen: {error_count}\n")
            self.preview_text.insert(tk.END, f"Log: {log_file}\n")
            self.preview_text.config(state=tk.DISABLED)
            
            messagebox.showinfo(
                "Merge abgeschlossen",
                f"Merge abgeschlossen!\n\n"
                f"Erfolgreich: {success_count}\n"
                f"Fehlgeschlagen: {error_count}\n\n"
                f"Details siehe: {log_file}"
            )
            
        except Exception as e:
            self.log_message(f"Kritischer Fehler: {e}", 'error')
            messagebox.showerror("Fehler", f"Fehler beim Merge:\n{e}")
            
        finally:
            self.processing = False
            self.merge_btn.config(state=tk.NORMAL)
            self.preview_btn.config(state=tk.NORMAL)
            
    def merge_single_project(self, source_project, target_elements, existing_lsd, existing_png, suffix_counter):
        """
        Merge a single source project into target.
        
        LSDX Struktur:
        - Registration (Wurzel, parents="")
        - Cluster (parents=registration_uuid, name="ClusterName")  
        - Scan (parents=cluster_uuid, name="1", enthält FilePath-Referenzen)
        
        Merge-Logik:
        1. Finde Target-Registration UUID
        2. Für jeden Cluster im Quellprojekt:
           - Prüfe ob Cluster mit gleichem Namen im Ziel existiert
           - Wenn ja: Verwende existierende Cluster-UUID für Scans
           - Wenn nein: Füge neuen Cluster hinzu mit neuer UUID
        3. Für jeden Scan: Füge hinzu mit korrekter Cluster-UUID als Parent
        """
        self.log_message(f"")
        self.log_message(f"{'='*60}")
        self.log_message(f"MERGE START: {source_project['name']}")
        self.log_message(f"{'='*60}")
        
        # Parse source LSDX
        source_tree = ET.parse(source_project['lsdx'])
        source_root = source_tree.getroot()
        source_elements = source_root.find('Elements')
        
        if source_elements is None:
            self.log_message("FEHLER: Keine Elements in Quell-LSDX gefunden!")
            raise ValueError("Keine Elements in Quell-LSDX")
        
        # === SCHRITT 1: Finde Target-Registration UUID ===
        target_registration_uuid = None
        for elem in target_elements.findall("Element[@type='registration']"):
            target_registration_uuid = elem.get('uuid')
            self.log_message(f"[TARGET] Registration UUID: {target_registration_uuid}")
            break
            
        if not target_registration_uuid:
            self.log_message("FEHLER: Keine Registration im Ziel gefunden!")
            raise ValueError("Keine Registration im Ziel-LSDX")
        
        # === SCHRITT 2: Sammle existierende Cluster im Target ===
        target_clusters = {}  # name -> uuid
        for elem in target_elements.findall("Element[@type='cluster']"):
            cluster_name = elem.get('name', '')
            cluster_uuid = elem.get('uuid')
            target_clusters[cluster_name] = cluster_uuid
            self.log_message(f"[TARGET] Existierender Cluster: '{cluster_name}' UUID={cluster_uuid}")
        
        self.log_message(f"[TARGET] Cluster gesamt: {len(target_clusters)}")
        
        # === SCHRITT 3: Analysiere Source-Struktur ===
        source_registration_uuid = None
        source_clusters = {}  # source_uuid -> {name, scans: []}
        source_scans = []
        
        for elem in source_elements.findall("Element"):
            elem_type = elem.get('type')
            elem_uuid = elem.get('uuid')
            elem_name = elem.get('name', '')
            elem_parents = elem.get('parents', '')
            
            if elem_type == 'registration':
                source_registration_uuid = elem_uuid
                self.log_message(f"[SOURCE] Registration UUID: {elem_uuid}")
            elif elem_type == 'cluster':
                source_clusters[elem_uuid] = {'name': elem_name, 'parent': elem_parents, 'scans': []}
                self.log_message(f"[SOURCE] Cluster: '{elem_name}' UUID={elem_uuid} parent={elem_parents}")
            elif elem_type == 'scan':
                source_scans.append({'elem': elem, 'uuid': elem_uuid, 'name': elem_name, 'parent': elem_parents})
                self.log_message(f"[SOURCE] Scan: '{elem_name}' UUID={elem_uuid} parent={elem_parents}")
        
        self.log_message(f"[SOURCE] Cluster: {len(source_clusters)}, Scans: {len(source_scans)}")
        
        # === SCHRITT 4: Ordne Scans den Clustern zu ===
        for scan in source_scans:
            scan_parent = scan['parent']
            if scan_parent in source_clusters:
                source_clusters[scan_parent]['scans'].append(scan)
            else:
                self.log_message(f"WARNUNG: Scan '{scan['name']}' hat unbekannten Parent: {scan_parent}")
        
        # === SCHRITT 5: Dateien kopieren ===
        lsd_renames = {}  # old_name -> new_name
        png_renames = {}  # old_name -> new_name
        
        self.log_message(f"")
        self.log_message(f"--- Kopiere LSD-Dateien ---")
        for lsd_file in source_project.get('lsd_files', []):
            src_path = os.path.join(source_project['pointcloud'], lsd_file)
            new_name = self.get_conflict_resolved_name(lsd_file, existing_lsd, suffix_counter)
            dst_path = os.path.join(self.target_project['pointcloud'], new_name)
            
            if os.path.exists(src_path):
                shutil.copy2(src_path, dst_path)
                existing_lsd.add(new_name)
                if lsd_file != new_name:
                    lsd_renames[lsd_file] = new_name
                    self.log_message(f"  LSD kopiert (umbenannt): {lsd_file} → {new_name}")
                else:
                    self.log_message(f"  LSD kopiert: {lsd_file}")
        
        self.log_message(f"--- Kopiere PNG-Dateien ---")
        if source_project['preview'] and self.target_project['preview']:
            if not os.path.exists(self.target_project['preview']):
                os.makedirs(self.target_project['preview'])
                
            for png_file in source_project.get('png_files', []):
                src_path = os.path.join(source_project['preview'], png_file)
                new_name = self.get_conflict_resolved_name(png_file, existing_png, suffix_counter)
                dst_path = os.path.join(self.target_project['preview'], new_name)
                
                if os.path.exists(src_path):
                    shutil.copy2(src_path, dst_path)
                    existing_png.add(new_name)
                    if png_file != new_name:
                        png_renames[png_file] = new_name
                        self.log_message(f"  PNG kopiert (umbenannt): {png_file} → {new_name}")
                    else:
                        self.log_message(f"  PNG kopiert: {png_file}")
        
        # === SCHRITT 6: Cluster und Scans hinzufügen ===
        self.log_message(f"")
        self.log_message(f"--- Füge Cluster und Scans hinzu ---")
        
        cluster_uuid_mapping = {}  # source_cluster_uuid -> target_cluster_uuid
        clusters_added = 0
        clusters_reused = 0
        scans_added = 0
        
        for source_cluster_uuid, cluster_info in source_clusters.items():
            cluster_name = cluster_info['name']
            
            # Prüfe ob Cluster mit gleichem Namen im Ziel existiert
            if cluster_name in target_clusters:
                # Cluster existiert bereits - verwende existierende UUID
                target_cluster_uuid = target_clusters[cluster_name]
                cluster_uuid_mapping[source_cluster_uuid] = target_cluster_uuid
                self.log_message(f"  CLUSTER EXISTIERT: '{cluster_name}' → verwende UUID {target_cluster_uuid}")
                clusters_reused += 1
            else:
                # Neuen Cluster hinzufügen
                new_cluster_uuid = "{" + str(uuid.uuid4()) + "}"
                cluster_uuid_mapping[source_cluster_uuid] = new_cluster_uuid
                
                # Finde das Cluster-Element im Source
                for elem in source_elements.findall("Element[@type='cluster']"):
                    if elem.get('uuid') == source_cluster_uuid:
                        new_cluster = copy.deepcopy(elem)
                        new_cluster.set('uuid', new_cluster_uuid)
                        new_cluster.set('parents', target_registration_uuid)  # Parent = Target-Registration
                        target_elements.append(new_cluster)
                        
                        # Merke für zukünftige Projekte
                        target_clusters[cluster_name] = new_cluster_uuid
                        
                        self.log_message(f"  CLUSTER HINZUGEFÜGT: '{cluster_name}' UUID={new_cluster_uuid} parent={target_registration_uuid}")
                        clusters_added += 1
                        break
            
            # Füge Scans dieses Clusters hinzu
            target_cluster_uuid = cluster_uuid_mapping[source_cluster_uuid]
            for scan_info in cluster_info['scans']:
                scan_elem = scan_info['elem']
                new_scan_uuid = "{" + str(uuid.uuid4()) + "}"
                
                new_scan = copy.deepcopy(scan_elem)
                new_scan.set('uuid', new_scan_uuid)
                new_scan.set('parents', target_cluster_uuid)  # Parent = Cluster-UUID
                
                # Update FilePath references
                for filepath in new_scan.findall(".//FilePath"):
                    file_type = filepath.get('type')
                    text = filepath.text or ''
                    
                    if file_type == 'lsd' and text:
                        if text in lsd_renames:
                            old_text = text
                            filepath.text = lsd_renames[text]
                            self.log_message(f"    FilePath lsd: {old_text} → {filepath.text}")
                            
                    elif file_type == 'preview' and text:
                        if text.startswith("Previews/"):
                            png_name = text[9:]
                            if png_name in png_renames:
                                old_text = text
                                filepath.text = f"Previews/{png_renames[png_name]}"
                                self.log_message(f"    FilePath preview: {old_text} → {filepath.text}")
                
                target_elements.append(new_scan)
                self.log_message(f"  SCAN HINZUGEFÜGT: '{scan_info['name']}' UUID={new_scan_uuid} parent={target_cluster_uuid}")
                scans_added += 1
        
        # === ZUSAMMENFASSUNG ===
        self.log_message(f"")
        self.log_message(f"--- MERGE ZUSAMMENFASSUNG ---")
        self.log_message(f"  Cluster hinzugefügt: {clusters_added}")
        self.log_message(f"  Cluster wiederverwendet: {clusters_reused}")
        self.log_message(f"  Scans hinzugefügt: {scans_added}")
        self.log_message(f"  LSD-Dateien kopiert: {len(source_project.get('lsd_files', []))}")
        self.log_message(f"  PNG-Dateien kopiert: {len(source_project.get('png_files', []))}")
        self.log_message(f"{'='*60}")


class MainMenu:
    """Main menu for selecting between Single Project, Batch mode, and Project Merger."""
    
    def __init__(self, root):
        self.root = root
        self.root.title("PointCab Projekt Umbenenner v4.1")
        self.root.geometry("500x450")
        self.root.resizable(False, False)
        
        self.current_frame = None
        self.setup_menu()
        
    def setup_menu(self):
        """Setup the main menu."""
        self.clear_window()
        self.root.geometry("500x450")
        self.root.resizable(False, False)
        
        main_frame = ttk.Frame(self.root, padding="30")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Title
        title_label = ttk.Label(main_frame, text="PointCab Projekt Umbenenner", font=('Helvetica', 18, 'bold'))
        title_label.pack(pady=(0, 5))
        
        version_label = ttk.Label(main_frame, text="Version 4.1", font=('Helvetica', 10), foreground='gray')
        version_label.pack(pady=(0, 30))
        
        # Description
        desc_label = ttk.Label(
            main_frame, 
            text="Wählen Sie einen Modus:",
            font=('Helvetica', 11)
        )
        desc_label.pack(pady=(0, 20))
        
        # Buttons frame
        buttons_frame = ttk.Frame(main_frame)
        buttons_frame.pack(fill=tk.X, pady=10)
        
        # Style for big buttons
        style = ttk.Style()
        style.configure('Big.TButton', font=('Helvetica', 12), padding=15)
        
        # Single project button
        single_btn = ttk.Button(
            buttons_frame, 
            text="📁 Einzelprojekt bearbeiten",
            style='Big.TButton',
            command=self.open_single_project
        )
        single_btn.pack(fill=tk.X, pady=5)
        
        single_desc = ttk.Label(
            buttons_frame,
            text="Ein einzelnes PointCab-Projekt auswählen und verarbeiten",
            foreground='gray'
        )
        single_desc.pack(pady=(0, 15))
        
        # Batch button
        batch_btn = ttk.Button(
            buttons_frame,
            text="📂 Batch Renamer",
            style='Big.TButton',
            command=self.open_batch_renamer
        )
        batch_btn.pack(fill=tk.X, pady=5)
        
        batch_desc = ttk.Label(
            buttons_frame,
            text="Mehrere Projekte in einem Hauptverzeichnis automatisch verarbeiten",
            foreground='gray'
        )
        batch_desc.pack(pady=(0, 15))
        
        # Merger button
        merger_btn = ttk.Button(
            buttons_frame,
            text="🔀 Projektmerger",
            style='Big.TButton',
            command=self.open_project_merger
        )
        merger_btn.pack(fill=tk.X, pady=5)
        
        merger_desc = ttk.Label(
            buttons_frame,
            text="Mehrere Projekte in ein Stammprojekt zusammenführen",
            foreground='gray'
        )
        merger_desc.pack(pady=(0, 15))
        
        # Exit button
        ttk.Button(main_frame, text="Beenden", command=self.root.quit).pack(pady=(20, 0))
        
    def clear_window(self):
        """Clear all widgets from window."""
        for widget in self.root.winfo_children():
            widget.destroy()
            
    def open_single_project(self):
        """Open single project mode."""
        self.clear_window()
        self.root.geometry("850x750")
        self.root.resizable(True, True)
        PointCabRenamer(self.root, return_callback=self.setup_menu)
        
    def open_batch_renamer(self):
        """Open batch renamer mode."""
        self.clear_window()
        self.root.geometry("900x700")
        self.root.resizable(True, True)
        BatchRenamer(self.root, return_callback=self.setup_menu)
        
    def open_project_merger(self):
        """Open project merger mode."""
        self.clear_window()
        self.root.geometry("1000x800")
        self.root.resizable(True, True)
        ProjectMerger(self.root, return_callback=self.setup_menu)


def main():
    """Main entry point."""
    root = tk.Tk()
    app = MainMenu(root)
    root.mainloop()


if __name__ == "__main__":
    main()
