import argparse
import json
from pathlib import Path


from error_collector import ErrorCollector, write_json_file
from utils import (
    check_environment_var, check_file_in_work, dxf_is_binary, get_dxf_file,
    extract_insert_attributes_with_doc as extract_block_attributes,
    draw_symbol_frames
)


"""
Dieses Programm liest eine dxf Datei und holt sich vom Layer RENAME die Angaben
für die Bereiche in der Anlage, welche Motoren, Sensoren und Aktoren welchem
Unterverteiler zugeordnet werden müssen. (Attribut KENNZEICHNUNG)
Alle Symbol Templates werden entsprechend der Richtungsangaben in DIRECTION nummeriert
DIRECTION kann TOP_BOTTOM, BOTTOM_TOP, LEFT_RIGHT oder RIGHT_LEFT sein

Ein Renamer Symbol enthält z.B. die folgenden Angaben: 
für POT-RA, POT-MA und POT-UC:

NAME1=POT-RA@@
NAME2=POT-MA@@
NAME3=POT-UC@@
KENNZEICHNUNG=A01+UC…
LAYER_NAME1=ILS_POT-RA
LAYER_NAME2=ILS_POT-MA
LAYER_NAME3=ILS_POT-UC
DIRECTION = LEFT_RIGHT

für BG, MB:

NAME1=BG-1@@@
NAME2=MB-1@@@
NAME3=
KENNZEICHNUNG=
LAYER_NAME1=ILS_Eingang
LAYER_NAME2=ILS_Ausgang
LAYER_NAME3=
DIRECTION = LEFT_RIGHT

oder nur zur Nummerierung der MA Symbole
NAME=MA-1@@
KENNZEICHNUNG=A01+UH00
LAYER_NAME=ILS_MOTOR
DIRECTION = TOP_BOTTOM/LEFT_RIGHT

Diese Symbole auf dem RENAMER Layer sind immer in einem Block mit einer Polylinie oder einen REchteck. 
Alle Sensoren, Motoren, Aktoren etc. innerhalb der zugehörigen Polylinie nehmen an der Nummerierung teil, falls diese das @ Zeichen enthalten, also noch ein Template sind. 

"""


def read_test_scenes_from_json(json_path: Path) -> list:
    """
    Liest die create_tests.json und gibt alle konfigurierten Szenen-Namen zurück.

    Args:
        json_path: Pfad zur create_tests.json

    Returns:
        Liste der Szenen-Namen (z.B. ['Nummerierung1', 'Nummerierung2', 'BGMG', ...])
    """
    if not json_path.exists():
        return []

    try:
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        # Hole alle Keys aus test_scenes
        if 'test_scenes' in data:
            return list(data['test_scenes'].keys())
        else:
            return []
    except Exception as e:
        print(f"Fehler beim Lesen der JSON-Datei {json_path}: {e}")
        return []


def read_config_layers(config_path: Path) -> list:
    """
    Liest die enumerate.cfg und gibt die Layer zurück, auf denen nach Renamer-Blöcken gesucht werden soll.
    Die Konfiguration hat eine einfache Liste unter [Layers]:
    [Layers]
    RENAMER
    OTHER_LAYER
    """
    layers = []

    with open(config_path, 'r', encoding='utf-8') as f:
        in_layers_section = False
        for line in f:
            line = line.strip()

            # Überspringe leere Zeilen und Kommentare
            if not line or line.startswith('#') or line.startswith(';'):
                continue

            # Prüfe ob wir in der [Layers] Sektion sind
            if line.startswith('['):
                in_layers_section = line.lower() == '[layers]'
                continue

            # Wenn in [Layers], füge Layer hinzu
            if in_layers_section:
                layers.append(line)

    return layers


def read_config_parameters(config_path: Path) -> dict:
    """
    Liest die enumerate.cfg und gibt die Parameter zurück.
    Die Konfiguration hat Key-Value-Paare unter [Parameters]:
    [Parameters]
    polyline_max_distance = 500.0

    Returns:
        Dictionary mit Parametern und ihren Werten
    """
    parameters = {
        'polyline_max_distance': 500.0  # Default-Wert
    }

    with open(config_path, 'r', encoding='utf-8') as f:
        in_parameters_section = False
        for line in f:
            line = line.strip()

            # Überspringe leere Zeilen und Kommentare
            if not line or line.startswith('#') or line.startswith(';'):
                continue

            # Prüfe ob wir in der [Parameters] Sektion sind
            if line.startswith('['):
                in_parameters_section = line.lower() == '[parameters]'
                continue

            # Wenn in [Parameters], parse Key-Value-Paare
            if in_parameters_section and '=' in line:
                key, value = line.split('=', 1)
                key = key.strip()
                value = value.strip()

                # Versuche den Wert als Float zu parsen
                try:
                    parameters[key] = float(value)
                except ValueError:
                    # Wenn nicht als Float parsbar, speichere als String
                    parameters[key] = value

    return parameters


def is_rectangle_polyline(points):
    """
    Prüft, ob eine Liste von Punkten ein Rechteck darstellt.
    Ein Rechteck hat genau 4 Eckpunkte (oder 5 wenn geschlossen mit identischem Start/End).

    Args:
        points: Liste von Punkten (Tupel mit mindestens x, y Koordinaten)

    Returns:
        True wenn es ein Rechteck ist, False sonst
    """
    if len(points) < 4:
        return False

    # Prüfe ob erster und letzter Punkt identisch sind (geschlossene Polylinie)
    first = (points[0][0], points[0][1])
    last = (points[-1][0], points[-1][1])

    if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001:
        # Geschlossene Polylinie - sollte 5 Punkte haben (4 Ecken + 1 Schließung)
        if len(points) == 5:
            return True
        # Wenn mehr als 5 Punkte, ist es kein einfaches Rechteck
        return False
    else:
        # Offene Polylinie - sollte genau 4 Punkte haben
        return len(points) == 4


def get_boundary_geometry(doc, insert):
    """
    Sucht im Block nach einem Rechteck oder einer geschlossenen Polylinie.
    Unterstützt zweistufige Blockstruktur (äußerer Block mit LWPOLYLINE + INSERT zu Attribut-Block).
    Gibt die Eckpunkte zurück.

    Returns:
        Tupel (transformed_points, is_rectangle) oder (None, None) wenn nichts gefunden
    """
    block_layout = doc.blocks.get(insert.dxf.name)

    # Suche nach LWPOLYLINE, POLYLINE oder geschlossenen Linien
    for entity in block_layout:
        if entity.dxftype() == 'LWPOLYLINE':
            # Get points and transform them
            points = list(entity.get_points())

            # Prüfe ob geschlossen (entweder Flag gesetzt oder erster == letzter Punkt)
            is_closed = entity.closed or (entity.dxf.flags & 1)
            if not is_closed and len(points) > 1:
                # Prüfe ob erster und letzter Punkt gleich sind
                first = points[0][:2]
                last = points[-1][:2]
                if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001:
                    is_closed = True

            if is_closed or len(points) >= 4:
                # Transform points relative to insert position
                insert_point = insert.dxf.insert
                transformed_points = []
                for p in points:
                    # Einfache Translation (ohne Rotation/Skalierung für ersten Ansatz)
                    transformed_points.append((
                        insert_point[0] + p[0],
                        insert_point[1] + p[1]
                    ))

                # Prüfe ob es ein Rechteck ist
                is_rect = is_rectangle_polyline(points)
                return transformed_points, is_rect

        elif entity.dxftype() == 'POLYLINE':
            if entity.is_closed:
                points = [(v.dxf.location.x, v.dxf.location.y) for v in entity.vertices]
                insert_point = insert.dxf.insert
                transformed_points = []
                for p in points:
                    transformed_points.append((
                        insert_point[0] + p[0],
                        insert_point[1] + p[1]
                    ))

                # Prüfe ob es ein Rechteck ist
                is_rect = is_rectangle_polyline(points)
                return transformed_points, is_rect

    return None, None


def point_in_polygon(point, polygon):
    """
    Prüft, ob ein Punkt innerhalb eines Polygons liegt (Ray-casting Algorithmus).
    """
    x, y = point
    n = len(polygon)
    inside = False

    p1x, p1y = polygon[0]
    for i in range(1, n + 1):
        p2x, p2y = polygon[i % n]
        if y > min(p1y, p2y):
            if y <= max(p1y, p2y):
                if x <= max(p1x, p2x):
                    if p1y != p2y:
                        xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                    if p1x == p2x or x <= xinters:
                        inside = not inside
        p1x, p1y = p2x, p2y

    return inside


def point_near_polyline(point, polyline_points, max_distance=500.0):
    """
    Prüft ob ein Punkt in der Nähe einer Polylinie liegt.

    Args:
        point: (x, y) Punkt
        polyline_points: Liste von (x, y) Punkten der Polylinie
        max_distance: Maximale Distanz zum nächsten Punkt auf der Polylinie

    Returns:
        True wenn Punkt innerhalb von max_distance zur Polylinie liegt
    """
    if not polyline_points or len(polyline_points) < 2:
        return False

    min_dist_sq = float('inf')

    # Prüfe Distanz zu jedem Segment der Polylinie
    for i in range(len(polyline_points) - 1):
        p1 = polyline_points[i]
        p2 = polyline_points[i + 1]

        # Berechne die Projektion des Punktes auf das Segment
        dx = p2[0] - p1[0]
        dy = p2[1] - p1[1]
        segment_length_sq = dx * dx + dy * dy

        if segment_length_sq == 0:
            # Segment ist ein Punkt
            dist_sq = (point[0] - p1[0])**2 + (point[1] - p1[1])**2
            min_dist_sq = min(min_dist_sq, dist_sq)
            continue

        # Parameter t für die Projektion (0 = p1, 1 = p2)
        t = ((point[0] - p1[0]) * dx + (point[1] - p1[1]) * dy) / segment_length_sq
        t = max(0.0, min(1.0, t))  # Clampe auf [0, 1]

        # Nächster Punkt auf dem Segment
        proj_x = p1[0] + t * dx
        proj_y = p1[1] + t * dy

        # Distanz vom Punkt zur Projektion
        dist_sq = (point[0] - proj_x)**2 + (point[1] - proj_y)**2
        min_dist_sq = min(min_dist_sq, dist_sq)

    return min_dist_sq <= (max_distance * max_distance)


def find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes, error_collector=None, polyline_max_distance=500.0):
    """
    Findet alle Symbole (INSERT-Blöcke) innerhalb des angegebenen Bereichs auf den Ziel-Layern.

    Für POLYLINE_PATH: Symbole in der Nähe der Polylinie
    Für Rechtecke: Symbole innerhalb des Polygons

    Args:
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)
    """
    symbols = []

    # Bestimme welche Layer durchsucht werden sollen
    search_layers = []
    for i in range(1, 4):  # NAME1, NAME2, NAME3
        layer_key = f"LAYER_NAME{i}"
        if layer_key in attributes and attributes[layer_key]:
            search_layers.append(attributes[layer_key])

    # Falls nur NAME/LAYER_NAME vorhanden
    if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]:
        search_layers.append(attributes["LAYER_NAME"])

    # Prüfe ob es POLYLINE_PATH ist
    direction = attributes.get("DIRECTION", "")
    is_polyline_path = (direction == "POLYLINE_PATH")

    # Durchsuche alle INSERT-Blöcke
    for entity in msp.query('INSERT'):
        if entity.dxf.layer not in search_layers:
            continue

        # Prüfe ob der Block Attribute hat (direkt oder in zweistufiger Struktur)
        # Verwende extract_block_attributes um beides zu unterstützen
        symbol_attribs = extract_block_attributes(doc, entity, error_collector)

        if not symbol_attribs:
            continue

        # Hole Position des Symbols
        pos = entity.dxf.insert
        point = (pos[0], pos[1])

        # Prüfe ob innerhalb des Bereichs
        is_in_boundary = False
        if is_polyline_path:
            # Für POLYLINE_PATH: Prüfe Nähe zur Polylinie
            is_in_boundary = point_near_polyline(point, boundary, max_distance=polyline_max_distance)
        else:
            # Für Rechtecke: Prüfe ob innerhalb des Polygons
            is_in_boundary = point_in_polygon(point, boundary)

        if is_in_boundary:
            # Prüfe ob es ein Template ist (enthält @)
            has_template = False
            for value in symbol_attribs.values():
                if '@' in str(value):
                    has_template = True
                    break

            if has_template:
                symbols.append({
                    'entity': entity,
                    'position': point,
                    'attributes': symbol_attribs,
                    'layer': entity.dxf.layer
                })

    return symbols


def calculate_position_along_polyline(symbol_pos, polyline_points):
    """
    Berechnet die Position eines Symbols entlang einer Polylinie.

    Die Position wird als kumulative Distanz vom Start der Polylinie bis zum
    nächstgelegenen Punkt auf der Polylinie zum Symbol berechnet.

    Args:
        symbol_pos: (x, y) Position des Symbols
        polyline_points: Liste von (x, y) Punkten der Polylinie

    Returns:
        Float: Kumulative Distanz entlang der Polylinie
    """
    if not polyline_points or len(polyline_points) < 2:
        return 0.0

    min_dist = float('inf')
    best_segment_idx = 0
    best_proj_dist = 0.0

    # Finde das nächste Segment und die Projektion auf diesem Segment
    for i in range(len(polyline_points) - 1):
        p1 = polyline_points[i]
        p2 = polyline_points[i + 1]

        # Berechne die Projektion des Symbols auf das Segment
        # Vektor von p1 zu p2
        dx = p2[0] - p1[0]
        dy = p2[1] - p1[1]
        segment_length_sq = dx * dx + dy * dy

        if segment_length_sq == 0:
            # Segment ist ein Punkt
            dist_sq = (symbol_pos[0] - p1[0])**2 + (symbol_pos[1] - p1[1])**2
            if dist_sq < min_dist:
                min_dist = dist_sq
                best_segment_idx = i
                best_proj_dist = 0.0
            continue

        # Parameter t für die Projektion (0 = p1, 1 = p2)
        t = ((symbol_pos[0] - p1[0]) * dx + (symbol_pos[1] - p1[1]) * dy) / segment_length_sq
        t = max(0.0, min(1.0, t))  # Clampe auf [0, 1]

        # Nächster Punkt auf dem Segment
        proj_x = p1[0] + t * dx
        proj_y = p1[1] + t * dy

        # Distanz vom Symbol zur Projektion
        dist_sq = (symbol_pos[0] - proj_x)**2 + (symbol_pos[1] - proj_y)**2

        if dist_sq < min_dist:
            min_dist = dist_sq
            best_segment_idx = i
            best_proj_dist = t * (segment_length_sq ** 0.5)

    # Berechne kumulative Distanz bis zum besten Segment + Projektion auf dem Segment
    cumulative_dist = 0.0
    for i in range(best_segment_idx):
        p1 = polyline_points[i]
        p2 = polyline_points[i + 1]
        segment_length = ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2) ** 0.5
        cumulative_dist += segment_length

    cumulative_dist += best_proj_dist

    return cumulative_dist


def sort_symbols_by_direction(symbols, direction, polyline_points=None):
    """
    Sortiert die Symbole nach der angegebenen Richtung.

    Unterstützte Richtungen (erste Angabe = Hauptsortierung):
        TOP_BOTTOM/LEFT_RIGHT: Zeilenweise von oben nach unten, dann links nach rechts (primär Y, sekundär X)
        TOP_BOTTOM/RIGHT_LEFT: Zeilenweise von oben nach unten, dann rechts nach links (primär Y, sekundär -X)
        BOTTOM_TOP/LEFT_RIGHT: Zeilenweise von unten nach oben, dann links nach rechts (primär Y, sekundär X)
        BOTTOM_TOP/RIGHT_LEFT: Zeilenweise von unten nach oben, dann rechts nach links (primär Y, sekundär -X)

        LEFT_RIGHT/TOP_BOTTOM: Spaltenweise von links nach rechts, dann oben nach unten (primär X, sekundär -Y)
        LEFT_RIGHT/BOTTOM_TOP: Spaltenweise von links nach rechts, dann unten nach oben (primär X, sekundär Y)
        RIGHT_LEFT/TOP_BOTTOM: Spaltenweise von rechts nach links, dann oben nach unten (primär -X, sekundär -Y)
        RIGHT_LEFT/BOTTOM_TOP: Spaltenweise von rechts nach links, dann unten nach oben (primär -X, sekundär Y)

    Einfache Richtungen:
        TOP_BOTTOM → TOP_BOTTOM/LEFT_RIGHT (zeilenweise von oben nach unten)
        BOTTOM_TOP → BOTTOM_TOP/LEFT_RIGHT (zeilenweise von unten nach oben)
        LEFT_RIGHT: Spaltenweise von links nach rechts (sortiert nach X, dann Y)
        RIGHT_LEFT: Spaltenweise von rechts nach links (sortiert nach -X, dann Y)

    Spezielle Richtungen:
        POLYLINE_PATH: Nummerierung entlang einer Polylinie in Reihenfolge der Polylinien-Punkte

    Args:
        symbols: Liste von Symbol-Dictionaries mit 'position' (x, y)
        direction: DIRECTION-Attribut (z.B. "LEFT_RIGHT", "TOP_BOTTOM/LEFT_RIGHT", "POLYLINE_PATH")
        polyline_points: Optional - Liste von (x, y) Punkten für POLYLINE_PATH Richtung

    Returns:
        Sortierte Liste von Symbolen
    """
    # POLYLINE_PATH: Sortiere entlang der Polylinie
    if direction == "POLYLINE_PATH":
        if not polyline_points or len(polyline_points) < 2:
            print("    WARNUNG: POLYLINE_PATH ohne gültige Polylinie - verwende Fallback-Sortierung")
            return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0]))

        # Berechne Position entlang der Polylinie für jedes Symbol
        symbols_with_dist = []
        for symbol in symbols:
            dist = calculate_position_along_polyline(symbol['position'], polyline_points)
            symbols_with_dist.append((dist, symbol))

        # Sortiere nach Distanz entlang der Polylinie
        symbols_with_dist.sort(key=lambda x: x[0])
        return [symbol for dist, symbol in symbols_with_dist]

    # Kombinierte Richtungen (vollständig spezifiziert) - Y/X Reihenfolge
    if direction == "TOP_BOTTOM/LEFT_RIGHT"  or direction == "TOP_BOTTOM":
        # Von oben nach unten (-Y), in jeder Zeile von links nach rechts (X)
        return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0]))
    elif direction == "LEFT_RIGHT/TOP_BOTTOM" :
        # Von links nach rechts (X), in jeder Spalte von oben nach unten (Y)
        return sorted(symbols, key=lambda s: (s['position'][0], -s['position'][1]))
    elif direction == "LEFT_RIGHT/BOTTOM_TOP":
        # Von links nach rechts (X), in jeder Spalte von unten nach oben (Y)
        return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1]))
    elif direction == "TOP_BOTTOM/RIGHT_LEFT":
        # Von oben nach unten (-Y), in jeder Zeile von rechts nach links (-X)
        return sorted(symbols, key=lambda s: (-s['position'][1], -s['position'][0]))
    elif direction == "BOTTOM_TOP/LEFT_RIGHT" or direction == "BOTTOM_TOP":
        # Von unten nach oben (Y), in jeder Zeile von links nach rechts (X)
        return sorted(symbols, key=lambda s: (s['position'][1], s['position'][0]))
    elif direction == "BOTTOM_TOP/RIGHT_LEFT":
        # Von unten nach oben (Y), in jeder Zeile von rechts nach links (-X)
        return sorted(symbols, key=lambda s: (s['position'][1], -s['position'][0]))
    elif direction == "RIGHT_LEFT/BOTTOM_TOP":
        # Von unten nach oben (Y), in jeder Zeile von rechts nach links (-X)
        return sorted(symbols, key=lambda s: (-s['position'][0], s['position'][1]))
    elif direction == "RIGHT_LEFT/TOP_BOTTOM":
        # Von rechts nach links (-X), in jeder Spalte von oben nach unten (Y)
        return sorted(symbols, key=lambda s: (-s['position'][0], -s['position'][1]))

    # Einfache Richtungen
    elif direction == "LEFT_RIGHT":
        # Spaltenweise von links nach rechts: Sortiere nach X, dann Y
        return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1]))
    elif direction == "RIGHT_LEFT":
        # Spaltenweise von rechts nach links: Sortiere nach -X, dann Y
        return sorted(symbols, key=lambda s: (-s['position'][0], s['position'][1]))
    else:
        # Fallback: TOP_BOTTOM/LEFT_RIGHT
        return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0]))


def enumerate_symbols(symbols, attributes):
    """
    Nummeriert die Symbole durch und ersetzt die @@ Platzhalter.
    """
    counter = 1
    renamed = []

    for symbol in symbols:
        layer = symbol['layer']

        # Finde das passende NAME-Template für diesen Layer
        name_template = None
        for i in range(1, 4):
            layer_key = f"LAYER_NAME{i}"
            name_key = f"NAME{i}"
            if layer_key in attributes and attributes[layer_key] == layer:
                if name_key in attributes:
                    name_template = attributes[name_key]
                    break

        # Falls nur NAME/LAYER_NAME vorhanden
        if not name_template and "LAYER_NAME" in attributes and attributes["LAYER_NAME"] == layer:
            if "NAME" in attributes:
                name_template = attributes["NAME"]

        if not name_template:
            continue

        # Ersetze @-Zeichen durch Zahlen
        # Zähle wie viele @ im Template sind
        at_count = name_template.count('@')
        number_str = oct(counter)[2:].zfill(at_count)

        # Aktualisiere Attribute im Symbol
        for attrib in symbol['entity'].attribs:
            old_value = attrib.dxf.text
            if '@' in old_value:
                # Ersetze @ durch die Nummer
                new_value = old_value.replace('@' * at_count, number_str)
                attrib.dxf.text = new_value

                renamed.append({
                    'position': symbol['position'],
                    'layer': layer,
                    'old_value': old_value,
                    'new_value': new_value
                })

        counter += 1

    return renamed


def get_layer_names_from_attributes(attributes):
    """
    Extrahiert alle LAYER_NAME Attribute aus einem Renamer-Block.

    Args:
        attributes: Dictionary mit Block-Attributen

    Returns:
        Liste der Layer-Namen (z.B. ['ILS_Eingang', 'ILS_Ausgang'])
    """
    layer_names = []
    if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]:
        layer_names.append(attributes["LAYER_NAME"])
    for i in range(1, 4):
        layer_key = f"LAYER_NAME{i}"
        if layer_key in attributes and attributes[layer_key]:
            layer_names.append(attributes[layer_key])
    return layer_names


def get_renamer_config_key(attributes):
    """
    Erstellt einen eindeutigen Schlüssel für die Renamer-Block-Konfiguration.
    Blöcke mit demselben Schlüssel werden gruppiert und gemeinsam nummeriert.

    Args:
        attributes: Dictionary mit Block-Attributen

    Returns:
        Tuple mit (NAME-Patterns, LAYER_NAMEs, DIRECTION, KENNZEICHNUNG)
    """
    # Sammle NAME-Patterns (normalisiert: NAME oder NAME1-3)
    name_patterns = []
    if "NAME" in attributes and attributes["NAME"]:
        name_patterns.append(("NAME", attributes["NAME"]))
    for i in range(1, 4):
        name_key = f"NAME{i}"
        if name_key in attributes and attributes[name_key]:
            name_patterns.append((name_key, attributes[name_key]))

    # Sammle LAYER_NAMEs (normalisiert: LAYER_NAME oder LAYER_NAME1-3)
    layer_names = []
    if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]:
        layer_names.append(("LAYER_NAME", attributes["LAYER_NAME"]))
    for i in range(1, 4):
        layer_key = f"LAYER_NAME{i}"
        if layer_key in attributes and attributes[layer_key]:
            layer_names.append((layer_key, attributes[layer_key]))

    # DIRECTION und KENNZEICHNUNG
    direction = attributes.get("DIRECTION", "")
    kennzeichnung = attributes.get("KENNZEICHNUNG", "")

    # Erstelle einen eindeutigen Schlüssel
    # Sortiere die Listen für konsistente Keys
    name_patterns.sort()
    layer_names.sort()

    return (tuple(name_patterns), tuple(layer_names), direction, kennzeichnung)


def validate_direction_with_geometry(direction, is_rectangle):
    """
    Validiert die DIRECTION-Angabe gegen die tatsächliche Geometrie.

    Args:
        direction: DIRECTION-Attribut (z.B. "LEFT_RIGHT", "POLYLINE_PATH")
        is_rectangle: True wenn die Polylinie ein Rechteck ist, False sonst

    Returns:
        Tupel (is_valid, error_message):
        - is_valid: True wenn Kombination gültig ist
        - error_message: Fehlermeldung wenn ungültig, sonst None
    """
    # POLYLINE_PATH-Richtungen erwarten komplexe Polylinien (keine Rechtecke)
    polyline_path_directions = ["POLYLINE_PATH"]

    # Rechteck/Standard-Richtungen erwarten einfache Rechtecke
    rectangle_directions = [
        "TOP_BOTTOM", "BOTTOM_TOP", "LEFT_RIGHT", "RIGHT_LEFT",
        "TOP_BOTTOM/LEFT_RIGHT", "TOP_BOTTOM/RIGHT_LEFT",
        "BOTTOM_TOP/RIGHT_LEFT", "BOTTOM_TOP/LEFT_RIGHT",
        "LEFT_RIGHT/TOP_BOTTOM", "RIGHT_LEFT/TOP_BOTTOM",
        "LEFT_RIGHT/BOTTOM_TOP", "RIGHT_LEFT/BOTTOM_TOP"
    ]

    if direction in polyline_path_directions:
        if is_rectangle:
            return False, f"DIRECTION '{direction}' erfordert eine komplexe Polylinie, aber ein Rechteck wurde gefunden"
        return True, None

    elif direction in rectangle_directions:
        if not is_rectangle:
            return False, f"DIRECTION '{direction}' erfordert ein Rechteck, aber eine komplexe Polylinie wurde gefunden"
        return True, None

    else:
        # Unbekannte DIRECTION - gebe Warnung, aber breche nicht ab
        return True, f"Unbekannte DIRECTION '{direction}' - Verarbeitung wird fortgesetzt"


def collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector):
    """
    Erster Pass: Sammelt und gruppiert alle Renamer-Blöcke nach Konfiguration.
    
    Returns:
        Dictionary mit {config_key: [(insert, boundary, attributes, is_rectangle), ...]}
    """
    renamer_groups = {}  # {config_key: [(insert, boundary, attributes, is_rectangle), ...]}

    for layer in renamer_layers:
        print(f"Durchsuche Layer: {layer}")

        # Finde alle INSERT-Blöcke auf diesem Layer
        for insert in msp.query(f'INSERT[layer=="{layer}"]'):
            # Extrahiere Attribute (unterstützt zweistufige Blockstruktur)
            attributes = extract_block_attributes(doc, insert, error_collector)

            if not attributes:
                print(f"  Block ohne Attribute gefunden an Position {insert.dxf.insert}")
                continue

            # Prüfe ob es ein Renamer-Block ist
            has_name = "NAME" in attributes or "NAME1" in attributes
            has_direction = "DIRECTION" in attributes

            if not (has_name and has_direction):
                continue

            block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))
            print(f"  Renamer-Block gefunden: {block_name}")
            print(f"    Direction: {attributes.get('DIRECTION', 'UNKNOWN')}")
            print(f"    Kennzeichnung: {attributes.get('KENNZEICHNUNG', 'N/A')}")

            # Finde Boundary (Rechteck oder Polylinie)
            boundary, is_rectangle = get_boundary_geometry(doc, insert)
            if boundary is None:
                error_msg = f"Keine Polylinie/Rechteck im Renamer-Block '{block_name}' an Position {insert.dxf.insert} gefunden"
                print(f"    FEHLER: {error_msg}")
                error_collector.add_errors({"missing_boundary": error_msg})
                continue

            print(f"    Boundary gefunden mit {len(boundary)} Punkten (Rechteck: {is_rectangle})")
            print(f"    Eckpunkte: {', '.join([f'({p[0]:.2f}, {p[1]:.2f})' for p in boundary])}")

            # Validiere DIRECTION gegen Geometrie
            direction = attributes.get("DIRECTION", "LEFT_RIGHT")
            is_valid, validation_error = validate_direction_with_geometry(direction, is_rectangle)

            if not is_valid:
                error_msg = f"Renamer-Block '{block_name}' an Position {insert.dxf.insert}: {validation_error}"
                print(f"    FEHLER: {error_msg}")
                error_collector.add_errors({"direction_geometry_mismatch": error_msg})
                continue
            elif validation_error:
                # Warnung bei unbekannter DIRECTION, aber fortfahren
                print(f"    WARNUNG: {validation_error}")
                error_collector.add_warnings({"unknown_direction": validation_error})

            # Erstelle Konfigurationsschlüssel und gruppiere
            config_key = get_renamer_config_key(attributes)
            if config_key not in renamer_groups:
                renamer_groups[config_key] = []
            renamer_groups[config_key].append((insert, boundary, attributes, is_rectangle))

    return renamer_groups


def find_template_symbols_in_geometry(doc, msp, group_blocks, error_collector):
    """
    Findet alle Symbole mit '@' die geometrisch in den Renamer-Bereichen liegen,
    unabhängig vom Layer.

    Args:
        doc: DXF-Dokument
        msp: Modelspace
        group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln
        error_collector: ErrorCollector für Fehlerbehandlung

    Returns:
        Liste von Dictionaries mit 'layer', 'position', 'io' für jedes gefundene Symbol
    """
    symbols_in_geometry = []
    for insert, boundary, attributes, is_rectangle in group_blocks:
        for entity in msp.query('INSERT'):
            symbol_attribs = extract_block_attributes(doc, entity, error_collector)
            if not symbol_attribs:
                continue

            # Prüfe ob irgendein Attribut "@" enthält
            has_template = any('@' in str(value) for value in symbol_attribs.values())

            if has_template:
                pos = entity.dxf.insert
                point = (pos[0], pos[1])
                if point_in_polygon(point, boundary):
                    symbols_in_geometry.append({
                        'layer': entity.dxf.layer,
                        'position': point,
                        'io': symbol_attribs.get('IO', 'N/A')
                    })
    return symbols_in_geometry


def create_no_symbols_found_error_message(block_name, direction, layer_names, group_blocks,
                                          doc, msp, error_collector):
    """
    Erstellt eine detaillierte Fehlermeldung wenn keine Symbole in einem Renamer-Bereich gefunden wurden.

    Args:
        block_name: Name des Renamer-Blocks
        direction: DIRECTION-Attribut
        layer_names: Liste der erwarteten Layer-Namen
        group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln
        doc: DXF-Dokument
        msp: Modelspace
        error_collector: ErrorCollector für Fehlerbehandlung

    Returns:
        Fehlermeldungs-String
    """
    renamer_positions = [f"({insert.dxf.insert[0]:.1f}, {insert.dxf.insert[1]:.1f})"
                        for insert, _, _, _ in group_blocks]

    # Prüfe ob es Symbole mit @ gibt, die geometrisch im Bereich liegen (egal auf welchem Layer)
    symbols_in_geometry = find_template_symbols_in_geometry(doc, msp, group_blocks, error_collector)

    # Erstelle Fehlermeldung basierend auf gefundenen Symbolen
    if symbols_in_geometry and not layer_names:
        # Es gibt Symbole im Bereich, aber keine LAYER_NAME Attribute definiert
        return (
            f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) "
            f"hat keine passenden Symbole gefunden. "
            f"Es wurden {len(symbols_in_geometry)} Symbol(e) mit '@' geometrisch im Bereich gefunden, "
            f"aber der Renamer-Block hat KEINE LAYER_NAME Attribute (LAYER_NAME1, LAYER_NAME2, LAYER_NAME3) definiert. "
            f"Ohne diese Attribute können Symbole nicht zugeordnet werden. "
            f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}"
        )
    elif symbols_in_geometry and layer_names:
        # Es gibt Symbole im Bereich, aber sie sind auf falschen Layern
        found_layers = set(s['layer'] for s in symbols_in_geometry)
        return (
            f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) "
            f"hat keine passenden Symbole gefunden. "
            f"Es wurden {len(symbols_in_geometry)} Symbol(e) mit '@' geometrisch im Bereich gefunden, "
            f"aber diese sind auf Layer: {', '.join(found_layers)}. "
            f"Erwartet werden Layer: {', '.join(layer_names)}. "
            f"Prüfen Sie die LAYER_NAME Attribute im Renamer-Block. "
            f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}"
        )
    else:
        # Keine Symbole im Bereich gefunden (weder geometrisch noch auf richtigen Layern)
        return (
            f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) "
            f"hat keine passenden Symbole gefunden. "
            f"Gesucht auf Layer: {', '.join(layer_names) if layer_names else 'N/A'}. "
            f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}"
        )


def collect_symbols_from_group(doc, msp, renamer_layers, group_blocks, block_name, error_collector, polyline_max_distance=500.0):
    """
    Sammelt alle Symbole aus allen Boundaries einer Renamer-Gruppe.
    Prüft auf Duplikate (Symbole in mehreren Bereichen).

    Args:
        doc: DXF-Dokument
        msp: Modelspace
        renamer_layers: Layer auf denen nach Renamer-Blöcken gesucht wird
        group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln
        block_name: Name des Renamer-Blocks (für Fehlermeldungen)
        error_collector: ErrorCollector für Fehlerbehandlung
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)

    Returns:
        Liste aller eindeutigen Symbole
    """
    all_symbols = []
    seen_entities = {}  # {entity_id: (symbol, [list of renamer block positions])}

    for insert, boundary, attributes, is_rectangle in group_blocks:
        # Finde Symbole innerhalb dieses Bereichs
        symbols = find_symbols_in_boundary(doc, msp, boundary, renamer_layers, attributes, error_collector, polyline_max_distance)

        # Füge Symbole hinzu, aber nur wenn das Entity noch nicht gesehen wurde
        for symbol in symbols:
            entity_id = id(symbol['entity'])  # Eindeutige ID des Entity-Objekts
            renamer_pos = insert.dxf.insert

            if entity_id not in seen_entities:
                seen_entities[entity_id] = (symbol, [renamer_pos])
                all_symbols.append(symbol)
            else:
                # Symbol wurde bereits in einem anderen Bereich gefunden - Fehler
                existing_symbol, existing_positions = seen_entities[entity_id]
                existing_positions.append(renamer_pos)
                symbol_pos = symbol['position']
                io_value = symbol['attributes'].get('IO', 'N/A')
                error_msg = (
                    f"Symbol an Position ({symbol_pos[0]:.1f}, {symbol_pos[1]:.1f}) "
                    f"mit IO='{io_value}' liegt in mehreren Renamer-Bereichen der Gruppe '{block_name}'. "
                    f"Gefunden in Renamer-Blöcken an Positionen: "
                    f"{', '.join([f'({p[0]:.1f}, {p[1]:.1f})' for p in existing_positions])}"
                )
                print(f"    FEHLER: {error_msg}")
                error_collector.add_errors({"symbol_in_multiple_boundaries": error_msg})

    return all_symbols


def process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_collector, polyline_max_distance=500.0):
    """
    Zweiter Pass: Verarbeitet jede Renamer-Block-Gruppe.
    Sammelt Symbole, sortiert sie und nummeriert sie.

    Args:
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)

    Returns:
        Liste aller umbenannten Symbole
    """
    all_renamed = []

    for config_key, group_blocks in renamer_groups.items():
        if not group_blocks:
            continue

        # Verwende die Attribute des ersten Blocks als Referenz (alle in der Gruppe haben identische Attribute)
        reference_attributes = group_blocks[0][2]
        direction = reference_attributes.get("DIRECTION", "LEFT_RIGHT")
        block_name = reference_attributes.get('NAME', reference_attributes.get('NAME1', 'UNKNOWN'))

        print(f"\n  Verarbeite Gruppe: {block_name} ({len(group_blocks)} Renamer-Blöcke)")

        # Sammle Symbole aus ALLEN Boundaries dieser Gruppe
        all_symbols = collect_symbols_from_group(doc, msp, renamer_layers, group_blocks, block_name, error_collector, polyline_max_distance)

        print(f"    Gesamt {len(all_symbols)} eindeutige Template-Symbole aus {len(group_blocks)} Blöcken")

        if not all_symbols:
            # Fehlermeldung: Keine Symbole gefunden
            layer_names = get_layer_names_from_attributes(reference_attributes)
            error_msg = create_no_symbols_found_error_message(
                block_name, direction, layer_names, group_blocks, doc, msp, error_collector
            )
            print(f"    FEHLER: {error_msg}")
            error_collector.add_errors({"no_symbols_found": error_msg})
            continue

        # Für POLYLINE_PATH: Hole die Polylinie vom ersten Block
        polyline_points = None
        if direction == "POLYLINE_PATH":
            if group_blocks:
                insert, boundary, attributes, is_rectangle = group_blocks[0]
                if boundary and not is_rectangle:
                    polyline_points = boundary
                    print(f"    POLYLINE_PATH: Verwende Polylinie mit {len(polyline_points)} Punkten")
                else:
                    print(f"    WARNUNG: POLYLINE_PATH angegeben, aber keine Polylinie gefunden (is_rectangle={is_rectangle})")

        # Sortiere alle Symbole zusammen nach Richtung
        sorted_symbols = sort_symbols_by_direction(all_symbols, direction, polyline_points)

        # Nummeriere alle Symbole als eine Sequenz
        renamed = enumerate_symbols(sorted_symbols, reference_attributes)
        all_renamed.extend(renamed)

        print(f"    {len(renamed)} Symbole nummeriert")

    return all_renamed


def find_symbols_with_at_in_io(doc, msp, error_collector):
    """
    Findet alle Symbole mit "@" im IO-Attribut.

    Args:
        doc: DXF-Dokument
        msp: Modelspace
        error_collector: ErrorCollector für Fehlerbehandlung

    Returns:
        Liste von Dictionaries mit Symbol-Informationen
    """
    symbols_with_at = []
    for entity in msp.query('INSERT'):
        symbol_attribs = extract_block_attributes(doc, entity, error_collector)
        if not symbol_attribs:
            continue

        # Prüfe ob IO-Attribut "@" enthält
        io_value = symbol_attribs.get('IO', '')
        if '@' in str(io_value):
            pos = entity.dxf.insert
            point = (pos[0], pos[1])
            symbols_with_at.append({
                'entity': entity,
                'position': point,
                'attributes': symbol_attribs,
                'layer': entity.dxf.layer,
                'io': io_value
            })
    return symbols_with_at


def check_symbol_in_boundary(symbol, boundary, attributes, renamer_pos, polyline_max_distance=500.0):
    """
    Prüft ob ein Symbol geometrisch und vom Layer her in einem Renamer-Bereich liegt.

    Args:
        symbol: Dictionary mit Symbol-Informationen
        boundary: Polygon-Punkte des Renamer-Bereichs
        attributes: Attribute des Renamer-Blocks
        renamer_pos: Position des Renamer-Blocks
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)

    Returns:
        Tuple (in_boundary, wrong_layer_info):
        - in_boundary: True wenn Symbol geometrisch und vom Layer her im Bereich liegt
        - wrong_layer_info: Dictionary mit Fehlerinformationen wenn Layer falsch ist, sonst None
    """
    # Prüfe ob Symbol geometrisch innerhalb des Bereichs liegt
    direction = attributes.get("DIRECTION", "")
    is_polyline_path = (direction == "POLYLINE_PATH")

    if is_polyline_path:
        # Für POLYLINE_PATH: Prüfe Nähe zur Polylinie
        if not point_near_polyline(symbol['position'], boundary, max_distance=polyline_max_distance):
            return False, None
    else:
        # Für Rechtecke: Prüfe ob innerhalb des Polygons
        if not point_in_polygon(symbol['position'], boundary):
            return False, None

    # Symbol liegt geometrisch im Bereich - prüfe Layer
    search_layers = get_layer_names_from_attributes(attributes)

    if symbol['layer'] in search_layers:
        # Layer passt auch - alles OK
        return True, None
    else:
        # Symbol liegt geometrisch im Bereich, aber Layer passt nicht
        block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))
        wrong_layer_info = {
            'block_name': block_name,
            'renamer_pos': renamer_pos,
            'expected_layers': search_layers,
            'attributes': attributes
        }
        return False, wrong_layer_info


def create_wrong_layer_warning(symbol, wrong_layer_info):
    """
    Erstellt eine Warnung wenn ein Symbol im Bereich liegt, aber der Layer nicht passt.

    Args:
        symbol: Dictionary mit Symbol-Informationen
        wrong_layer_info: Dictionary mit Fehlerinformationen

    Returns:
        Warnmeldungs-String
    """
    if wrong_layer_info['expected_layers']:
        return (
            f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) "
            f"mit IO='{symbol['io']}' liegt geometrisch im Renamer-Bereich '{wrong_layer_info['block_name']}' "
            f"(Renamer-Position: ({wrong_layer_info['renamer_pos'][0]:.1f}, {wrong_layer_info['renamer_pos'][1]:.1f})), "
            f"aber das Symbol ist auf Layer '{symbol['layer']}', erwartet wird: {', '.join(wrong_layer_info['expected_layers'])}. "
            f"Prüfen Sie die LAYER_NAME Attribute im Renamer-Block."
        )
    else:
        return (
            f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) "
            f"mit IO='{symbol['io']}' liegt geometrisch im Renamer-Bereich '{wrong_layer_info['block_name']}' "
            f"(Renamer-Position: ({wrong_layer_info['renamer_pos'][0]:.1f}, {wrong_layer_info['renamer_pos'][1]:.1f})), "
            f"aber der Renamer-Block hat KEINE LAYER_NAME Attribute definiert. "
            f"Symbole können nur gefunden werden, wenn LAYER_NAME1/LAYER_NAME2/LAYER_NAME3 Attribute gesetzt sind."
        )


def check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector, polyline_max_distance=500.0):
    """
    Dritter Pass: Prüft ob alle Symbole mit "@" im IO in einem Renamer-Bereich liegen.

    Args:
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)
    """
    print("\n" + "="*60)
    print("Prüfe ob alle Symbole mit '@' im IO in einem Renamer-Bereich liegen")
    print("="*60)

    # Sammle alle Renamer-Boundaries
    all_boundaries = []
    for group_blocks in renamer_groups.values():
        for insert, boundary, attributes, is_rectangle in group_blocks:
            all_boundaries.append((boundary, attributes, insert.dxf.insert))

    # Finde alle Symbole mit "@" im IO-Attribut
    symbols_with_at = find_symbols_with_at_in_io(doc, msp, error_collector)

    # Prüfe für jedes Symbol, ob es in mindestens einem Renamer-Bereich liegt
    symbols_not_in_any_boundary = []
    for symbol in symbols_with_at:
        in_any_boundary = False
        wrong_layer_info = None

        for boundary, attributes, renamer_pos in all_boundaries:
            is_in_boundary, layer_info = check_symbol_in_boundary(symbol, boundary, attributes, renamer_pos, polyline_max_distance)

            if is_in_boundary:
                # Layer passt auch - alles OK
                in_any_boundary = True
                break
            elif layer_info is not None:
                # Symbol liegt geometrisch im Bereich, aber Layer passt nicht
                wrong_layer_info = layer_info

        if not in_any_boundary:
            if wrong_layer_info:
                # Warnung: Symbol liegt geometrisch im Bereich, aber Layer stimmt nicht
                warning_msg = create_wrong_layer_warning(symbol, wrong_layer_info)
                print(f"  WARNUNG: {warning_msg}")
                error_collector.add_warnings({"symbol_wrong_layer": warning_msg})
                # NICHT als Fehler hinzufügen - Warnung reicht aus
            else:
                # Fehler: Symbol liegt weder geometrisch noch vom Layer her in einem Renamer-Bereich
                symbols_not_in_any_boundary.append(symbol)

    # Melde Fehler für Symbole, die in keinem Bereich liegen
    if symbols_not_in_any_boundary:
        for symbol in symbols_not_in_any_boundary:
            error_msg = (
                f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) "
                f"mit IO='{symbol['io']}' auf Layer '{symbol['layer']}' "
                f"enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."
            )
            print(f"  FEHLER: {error_msg}")
            error_collector.add_errors({"symbol_with_at_not_in_boundary": error_msg})
    else:
        print(f"  Alle {len(symbols_with_at)} Symbole mit '@' im IO liegen in einem Renamer-Bereich.")


def rectangles_overlap(boundary1, boundary2):
    """
    Prüft ob sich zwei Rechteck-Boundaries überlappen.

    Args:
        boundary1: Liste von (x, y) Punkten (mindestens 4 Eckpunkte)
        boundary2: Liste von (x, y) Punkten (mindestens 4 Eckpunkte)

    Returns:
        True wenn sich die Rechtecke überlappen
    """
    # Extrahiere Min/Max für beide Rechtecke
    b1_xs = [p[0] for p in boundary1]
    b1_ys = [p[1] for p in boundary1]
    b1_left, b1_right = min(b1_xs), max(b1_xs)
    b1_bottom, b1_top = min(b1_ys), max(b1_ys)

    b2_xs = [p[0] for p in boundary2]
    b2_ys = [p[1] for p in boundary2]
    b2_left, b2_right = min(b2_xs), max(b2_xs)
    b2_bottom, b2_top = min(b2_ys), max(b2_ys)

    # Prüfe ob keine Überlappung
    no_overlap = (b1_right <= b2_left or   # B1 ist links von B2
                  b2_right <= b1_left or   # B2 ist links von B1
                  b1_top <= b2_bottom or   # B1 ist unter B2
                  b2_top <= b1_bottom)     # B2 ist unter B1

    return not no_overlap


def check_overlapping_renamer_blocks(renamer_groups, error_collector):
    """
    Prüft ob sich Renamer-Rechtecke überlappen.
    Polyline-Pfade (DIRECTION=POLYLINE_PATH) werden nicht geprüft.

    Args:
        renamer_groups: Dictionary mit gruppierten Renamer-Blöcken
        error_collector: ErrorCollector für Fehlerbehandlung
    """
    print("\n" + "="*60)
    print("Prüfe auf überlappende Renamer-Bereiche")
    print("="*60)

    # Sammle alle Rechteck-Renamer (ohne POLYLINE_PATH)
    rectangle_renamers = []
    for config_key, group_blocks in renamer_groups.items():
        for insert, boundary, attributes, is_rectangle in group_blocks:
            direction = attributes.get("DIRECTION", "")
            block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))

            # Überspringe POLYLINE_PATH
            if direction == 'POLYLINE_PATH':
                continue

            # Nur Rechtecke prüfen
            if is_rectangle:
                rectangle_renamers.append({
                    'name': block_name,
                    'position': insert.dxf.insert,
                    'boundary': boundary,
                    'direction': direction,
                    'is_rectangle': is_rectangle
                })

    # Prüfe alle Paare auf Überlappung
    overlaps_found = []
    for i in range(len(rectangle_renamers)):
        for j in range(i + 1, len(rectangle_renamers)):
            r1 = rectangle_renamers[i]
            r2 = rectangle_renamers[j]

            if rectangles_overlap(r1['boundary'], r2['boundary']):
                overlaps_found.append((r1, r2))

    # Melde Fehler
    if overlaps_found:
        print(f"\nFEHLER: {len(overlaps_found)} Überlappung(en) zwischen Renamer-Bereichen gefunden:")
        for r1, r2 in overlaps_found:
            error_msg = (
                f"Renamer-Bereich '{r1['name']}' an Position "
                f"({r1['position'][0]:.1f}, {r1['position'][1]:.1f}) "
                f"überlappt mit Renamer-Bereich '{r2['name']}' an Position "
                f"({r2['position'][0]:.1f}, {r2['position'][1]:.1f}). "
                f"Symbole könnten mehrfach nummeriert werden."
            )
            print(f"  {error_msg}")
            error_collector.add_errors({"overlapping_renamer_blocks": error_msg})
    else:
        print(f"\nOK: Keine Überlappungen zwischen {len(rectangle_renamers)} Renamer-Rechtecken gefunden")


def process_renamer_blocks(doc, msp, renamer_layers, error_collector, polyline_max_distance=500.0):
    """
    Verarbeitet alle Renamer-Blöcke auf den angegebenen Layern.
    Gruppiert Blöcke mit identischer Konfiguration und nummeriert alle Symbole
    aus allen Blöcken einer Gruppe gemeinsam.

    Args:
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)

    Returns:
        Tuple (all_renamed, renamer_groups):
        - all_renamed: Liste der umbenannten Symbole
        - renamer_groups: Dictionary der gruppierten Renamer-Blöcke
    """
    # Erster Pass: Sammle und gruppiere alle Renamer-Blöcke
    renamer_groups = collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector)

    # Prüfe auf überlappende Renamer-Bereiche
    check_overlapping_renamer_blocks(renamer_groups, error_collector)

    # Zweiter Pass: Verarbeite jede Gruppe
    all_renamed = process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_collector, polyline_max_distance)

    # Dritter Pass: Prüfe ob alle Symbole mit "@" im IO in einem Renamer-Bereich liegen
    check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector, polyline_max_distance)

    return all_renamed, renamer_groups


def collect_all_symbols_from_groups(doc, msp, renamer_groups, renamer_layers, error_collector, polyline_max_distance=500.0):
    """
    Sammelt alle Symbole aus allen Renamer-Gruppen für Visualisierungszwecke.

    Args:
        doc: DXF-Dokument
        msp: Modelspace
        renamer_groups: Dictionary mit gruppierten Renamer-Blöcken
        renamer_layers: Liste der Layer mit Renamer-Blöcken
        error_collector: ErrorCollector für Fehlerbehandlung
        polyline_max_distance: Maximale Distanz zur Polylinie (aus Config)

    Returns:
        Liste aller gefundenen Symbole mit 'entity', 'position', 'attributes', 'layer'
    """
    all_symbols = []
    seen_entities = set()  # Verhindere Duplikate

    for config_key, group_blocks in renamer_groups.items():
        if not group_blocks:
            continue

        reference_attributes = group_blocks[0][2]

        for insert, boundary, attributes, is_rectangle in group_blocks:
            # Finde Symbole innerhalb dieses Bereichs
            symbols = find_symbols_in_boundary(doc, msp, boundary, renamer_layers, attributes, error_collector, polyline_max_distance)

            # Füge Symbole hinzu, aber nur wenn das Entity noch nicht gesehen wurde
            for symbol in symbols:
                entity_id = id(symbol['entity'])
                if entity_id not in seen_entities:
                    seen_entities.add(entity_id)
                    all_symbols.append(symbol)

    return all_symbols


def process_single_file(dxf_path, work_dir, renamer_layers, polyline_max_distance, args):
    """
    Verarbeitet eine einzelne DXF-Datei.

    Args:
        dxf_path: Pfad zur DXF-Datei
        work_dir: Work-Verzeichnis
        renamer_layers: Liste der konfigurierten Renamer-Layer
        polyline_max_distance: Maximale Distanz zur Polylinie
        args: Argparse-Argumente

    Returns:
        Tuple (success, error_collector):
        - success: True wenn erfolgreich verarbeitet
        - error_collector: ErrorCollector mit gesammelten Fehlern/Warnungen
    """
    # Initialisiere ErrorCollector für diese Datei
    error_collector = ErrorCollector()

    print(f"\n{'='*80}")
    print(f"Verarbeite Datei: {dxf_path.name}")
    print(f"{'='*80}\n")

    # Lese DXF-Datei
    if dxf_is_binary(dxf_path):
        print("DXF-Datei ist binär. Lese komplette Datei. Achten Sie auf RAM-Nutzung.")
        doc = get_dxf_file(dxf_path)
        msp = doc.modelspace()
    else:
        print("DXF-Datei ist ASCII.")
        doc = get_dxf_file(dxf_path)
        msp = doc.modelspace()

    # Verarbeite Renamer-Blöcke
    print("\n" + "="*60)
    print("Starte Verarbeitung der Renamer-Blöcke")
    print("="*60 + "\n")

    renamed_symbols, renamer_groups = process_renamer_blocks(doc, msp, renamer_layers, error_collector, polyline_max_distance)

    print("\n" + "="*60)
    print(f"Verarbeitung abgeschlossen: {len(renamed_symbols)} Symbole nummeriert")
    print("="*60 + "\n")

    # Ausgabe der Ergebnisse
    if renamed_symbols:
        print("Nummerierte Symbole:")
        for item in renamed_symbols:
            print(f"  {item['old_value']} -> {item['new_value']} (Layer: {item['layer']}, Pos: {item['position']})")

    # Zeichne Symbol-Rahmen wenn gewünscht
    if args.show_symbol_frames:
        all_symbols = collect_all_symbols_from_groups(doc, msp, renamer_groups, renamer_layers, error_collector, polyline_max_distance)
        if all_symbols:
            draw_symbol_frames(doc, msp, all_symbols)
        else:
            print("\nKeine Symbole für Rahmen-Zeichnung gefunden")

    # Speichere DXF-Datei wenn nicht dry-run
    if not args.dryrun:
        if args.overwrite:
            # Überschreibe die Originaldatei
            output_path = dxf_path
            doc.saveas(output_path)
            print(f"\nOriginaldatei überschrieben: {output_path}")
        else:
            # Erstelle neue Datei mit _numbered Suffix
            output_path = dxf_path.parent / f"{dxf_path.stem}_numbered{dxf_path.suffix}"
            doc.saveas(output_path)
            print(f"\nNummerierte DXF-Datei gespeichert: {output_path}")
    else:
        print("\nDry-run Modus: DXF-Datei wurde nicht überschrieben")

    # Schreibe JSON-Ausgabe wenn gewünscht
    if args.info:
        # Formatiere die Ausgabe für bessere Lesbarkeit in JSON
        formatted_symbols = []
        for symbol in renamed_symbols:
            formatted_symbols.append({
                'old_value': symbol['old_value'],
                'new_value': symbol['new_value'],
                'layer': symbol['layer'],
                'position': {
                    'x': symbol['position'][0],
                    'y': symbol['position'][1]
                }
            })

        output_data = {
            'renamed_symbols': formatted_symbols,
            'total_count': len(formatted_symbols)
        }

        info_path = Path(args.info)

        # Wenn nur ein Dateiname ohne Pfad angegeben wurde, speichere im work-Ordner
        if info_path.parent == Path('.'):
            output_dir = dxf_path.parent
            output_filename = info_path.name
        else:
            # Absoluter oder relativer Pfad mit Verzeichnis angegeben
            output_dir = info_path.parent
            output_filename = info_path.name

        write_json_file(output_data, output_dir, output_filename)
        print(f"Ergebnisse in JSON gespeichert: {output_dir / output_filename}")

    # Schreibe Fehler-Datei nur wenn Fehler oder Warnungen vorhanden sind
    if error_collector.has_errors_or_warnings():
        if args.errorfile:
            error_data = error_collector.get_all_issues()
            error_path = Path(args.errorfile)

            # Wenn nur ein Dateiname ohne Pfad angegeben wurde, speichere im work-Ordner
            if error_path.parent == Path('.'):
                output_dir = dxf_path.parent
                output_filename = error_path.name
            else:
                # Absoluter oder relativer Pfad mit Verzeichnis angegeben
                output_dir = error_path.parent
                output_filename = error_path.name

            write_json_file(error_data, output_dir, output_filename)
            print(f"Fehler/Warnungen gespeichert: {output_dir / output_filename}")
        else:
            # Keine explizite Fehlerdatei angegeben, generiere eine basierend auf Input-Datei
            error_filename = f"{dxf_path.stem}_errors.json"
            error_data = error_collector.get_all_issues()
            write_json_file(error_data, dxf_path.parent, error_filename)
            print(f"Fehler/Warnungen gespeichert: {dxf_path.parent / error_filename}")

    # Status-Meldung
    if error_collector.has_errors():
        print("\n!!! Es sind Fehler aufgetreten !!!")
        return False, error_collector
    elif error_collector.has_errors_or_warnings():
        print("\n(Warnungen vorhanden, aber keine kritischen Fehler)")
        return True, error_collector
    else:
        return True, error_collector


if __name__ == '__main__':
    # Lese Umgebungsvariablen vor argparse, um Config-Info in Hilfe anzuzeigen
    out_dir = check_environment_var('PROJECT_DATA')
    work_dir = check_environment_var('PROJECT_WORK')
    config_dir = check_environment_var("PROJECT_CFG")

    # Versuche Config zu lesen für --help Ausgabe
    config_path = Path(config_dir) / "enumerate.cfg"
    available_layers = []
    if config_path.exists():
        try:
            available_layers = read_config_layers(config_path)
        except Exception:
            pass  # Ignoriere Fehler beim Lesen für --help

    # Erstelle Epilog mit verfügbaren Layern
    epilog_text = "\nKonfigurierte Renamer-Layer:\n"
    if available_layers:
        epilog_text += "  " + ", ".join(available_layers) + "\n"
        epilog_text += f"\n(aus Konfigurationsdatei: {config_path})"
    else:
        epilog_text += "  (keine Layer konfiguriert oder Config-Datei nicht gefunden)\n"
        epilog_text += f"  Standard-Pfad: {config_path}"

    parser = argparse.ArgumentParser(
        description='Nummeriert Symbole in DXF-Dateien basierend auf Renamer-Blöcken',
        prog='create_numbers',
        epilog=epilog_text,
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument('-f', '--filename', action='store', required=False, help='DXF-Datei die verarbeitet werden soll', metavar='myfile.dxf')
    parser.add_argument('-e', '--errorfile', action='store', required=False, help='JSON-Datei für Fehler und Warnungen', metavar='errors.json')
    parser.add_argument('-i', '--info', action='store', help='Schreibe Ergebnisse der Nummerierung in eine JSON-Datei')
    parser.add_argument('-d', '--dryrun', action='store_true', help='Symbole nicht in der DXF-Datei überschreiben, nur Ausgabe auf Konsole')
    parser.add_argument('--overwrite', action='store_true', help='Überschreibt die Originaldatei anstatt eine neue Datei mit Suffix _numbered zu erstellen')
    parser.add_argument('--show_symbol_frames', action='store_true', help='Zeichne Rahmen um jedes Symbol, um die Grenze des Symbols zu visualisieren')
    parser.add_argument('--all-examples', action='store_true', help='Verarbeitet alle in create_tests.json konfigurierten DXF-Dateien aus dem work-Ordner')

    args = parser.parse_args()

    # Validierung: --filename oder --all-examples muss angegeben werden
    if not args.filename and not args.all_examples:
        parser.error("Entweder --filename oder --all-examples muss angegeben werden")

    # Validierung: --filename und --all-examples können nicht zusammen verwendet werden
    if args.filename and args.all_examples:
        parser.error("--filename und --all-examples können nicht zusammen verwendet werden")

    # Validierung: --overwrite und --dryrun können nicht zusammen verwendet werden
    if args.overwrite and args.dryrun:
        parser.error("--overwrite und --dryrun können nicht zusammen verwendet werden")

    # Validierung: Bei --all-examples können --errorfile und --info nicht verwendet werden
    if args.all_examples and (args.errorfile or args.info):
        parser.error("Bei --all-examples können --errorfile und --info nicht verwendet werden (Dateien werden automatisch generiert)")

    # Config-Datei existiert bereits (wurde für --help geprüft)
    if not config_path.exists():
        print(f"Konfigurationsdatei nicht gefunden: {config_path}")
        exit(1)

    renamer_layers = read_config_layers(config_path)
    print(f"Konfigurierte Layer: {renamer_layers}")

    if not renamer_layers:
        print("Keine Layer in der Konfiguration gefunden")
        exit(1)

    # Lese Parameter aus Config
    config_parameters = read_config_parameters(config_path)
    polyline_max_distance = config_parameters.get('polyline_max_distance', 500.0)
    print(f"Parameter: polyline_max_distance = {polyline_max_distance}")

    # ====================================================================================
    # Verarbeite entweder eine einzelne Datei oder alle Beispiel-Dateien
    # ====================================================================================

    if args.all_examples:
        # Verarbeite alle in create_tests.json konfigurierten Dateien
        print("\n" + "="*80)
        print("MODUS: Verarbeite alle Beispiel-Dateien aus create_tests.json")
        print("="*80)

        # Lese Test-Szenen aus create_tests.json
        create_tests_path = Path(config_dir) / "create_tests.json"
        test_scenes = read_test_scenes_from_json(create_tests_path)

        if not test_scenes:
            print(f"\nFEHLER: Keine Test-Szenen in {create_tests_path} gefunden")
            exit(1)

        print(f"\nGefundene Test-Szenen: {', '.join(test_scenes)}")
        print(f"Anzahl: {len(test_scenes)}\n")

        # Prüfe welche DXF-Dateien im work-Ordner existieren
        existing_files = []
        missing_files = []

        for scene_name in test_scenes:
            dxf_filename = f"{scene_name}.dxf"
            dxf_path = Path(work_dir) / dxf_filename

            if dxf_path.exists():
                existing_files.append((scene_name, dxf_path))
            else:
                missing_files.append(dxf_filename)

        # Melde fehlende Dateien
        if missing_files:
            print("WARNUNG: Folgende DXF-Dateien wurden im work-Ordner nicht gefunden:")
            for filename in missing_files:
                print(f"  - {filename}")
            print()

        if not existing_files:
            print("FEHLER: Keine DXF-Dateien im work-Ordner gefunden")
            exit(1)

        print(f"Verarbeite {len(existing_files)} Datei(en):\n")

        # Verarbeite alle gefundenen Dateien
        results = []
        for scene_name, dxf_path in existing_files:
            success, error_collector = process_single_file(dxf_path, work_dir, renamer_layers, polyline_max_distance, args)
            results.append({
                'scene_name': scene_name,
                'filename': dxf_path.name,
                'success': success,
                'has_errors': error_collector.has_errors(),
                'has_warnings': error_collector.has_errors_or_warnings() and not error_collector.has_errors()
            })

        # Zusammenfassung
        print("\n" + "="*80)
        print("ZUSAMMENFASSUNG")
        print("="*80)

        successful = sum(1 for r in results if r['success'])
        with_errors = sum(1 for r in results if r['has_errors'])
        with_warnings = sum(1 for r in results if r['has_warnings'])

        print(f"\nVerarbeitete Dateien: {len(results)}")
        print(f"Erfolgreich: {successful}")
        print(f"Mit Fehlern: {with_errors}")
        print(f"Mit Warnungen (ohne Fehler): {with_warnings}")

        if with_errors > 0:
            print("\nDateien mit Fehlern:")
            for r in results:
                if r['has_errors']:
                    print(f"  - {r['filename']}")

        if with_warnings > 0:
            print("\nDateien mit Warnungen:")
            for r in results:
                if r['has_warnings']:
                    print(f"  - {r['filename']}")

        # Exit-Code: Fehler wenn mindestens eine Datei Fehler hat
        if with_errors > 0:
            print("\n!!! Mindestens eine Datei hat Fehler !!!")
            exit(1)
        else:
            print("\nAlle Dateien erfolgreich verarbeitet!")
            exit(0)

    else:
        # Verarbeite einzelne Datei
        filename = Path(args.filename)
        if not filename.suffix == ".dxf":
            print("Nur für .dxf Dateien verfügbar")
            exit(1)

        (dxf_path, dexists) = check_file_in_work(work_dir, filename)
        if dexists == False:
            print(f"Datei nicht gefunden: {filename}")
            parser.print_help()
            exit(1)

        success, error_collector = process_single_file(dxf_path, work_dir, renamer_layers, polyline_max_distance, args)

        # Exit-Code basierend auf Fehlern
        if error_collector.has_errors():
            exit(1)
        else:
            exit(0)
