"""
Setzt den Einfuegepunkt ($INSBASE) von Omniflo DXF-Dateien.

Kriterien je Typ:

  --boegen            Schnittpunkt der Tangenten am Anfangs- und Endpunkt
                      der Bogenkurve (bei 180-Grad: Mittelpunkt).

  --weichen45         Schnittpunkt der laengsten vertikalen Linie mit einer
  --weichen90         Horizontalen durch den Endpunkt der Bogenkurve.

  --weichenkoerper    Schnittpunkt der laengsten Diagonale mit der laengsten
                      vertikalen Linie (Konvergenzpunkt aller Linien).

  --weichen-parallel  Wie --weichenkoerper: Schnittpunkt der laengsten
                      Diagonale mit der laengsten vertikalen Linie.

  --delta             Mittelpunkt der laengsten horizontalen Linie.
  --dreifachweiche    Mittelpunkt der laengsten horizontalen Linie.
  --sternweiche       Zentrum: Mittelpunkt der laengsten horizontalen Linie.

  --number SIVASNR    Nur diese eine 9-stellige Sivasnr verarbeiten.
  --show-omniflo      Uebersichts-DXF mit allen Elementen erzeugen.
"""

import argparse
import json
import math
import os
import sys

import ezdxf

from utils import (
    ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH,
    SWITCH_FILTERS,
    build_row_layout, import_element_as_block, draw_cross,
)


# ---------------------------------------------------------------------------
# Geometrie-Hilfsfunktionen
# ---------------------------------------------------------------------------

def find_arc(doc):
    """Findet die erste ARC-Entity im Modelspace."""
    msp = doc.modelspace()
    for entity in msp:
        if entity.dxftype() == "ARC":
            return entity
    return None


def tangent_intersection(arc):
    """Berechnet den Schnittpunkt der Tangenten am Start- und Endpunkt des Bogens.
    Bei 180-Grad-Boegen wird der Mittelpunkt zwischen Start und Ende verwendet.
    """
    cx, cy, _ = arc.dxf.center
    r = arc.dxf.radius
    a_start = math.radians(arc.dxf.start_angle)
    a_end = math.radians(arc.dxf.end_angle)

    p1x = cx + r * math.cos(a_start)
    p1y = cy + r * math.sin(a_start)
    p2x = cx + r * math.cos(a_end)
    p2y = cy + r * math.sin(a_end)

    d1x = -math.sin(a_start)
    d1y = math.cos(a_start)
    d2x = -math.sin(a_end)
    d2y = math.cos(a_end)

    det = d1x * (-d2y) - (-d2x) * d1y

    if abs(det) < 1e-9:
        return (p1x + p2x) / 2, (p1y + p2y) / 2

    dx = p2x - p1x
    dy = p2y - p1y
    t = (-dx * d2y + dy * d2x) / det

    ix = p1x + t * d1x
    iy = p1y + t * d1y
    return ix, iy


def find_longest_vertical_line(doc):
    """Findet die laengste vertikale Linie im Modelspace."""
    msp = doc.modelspace()
    longest = None
    longest_len = 0
    for entity in msp:
        if entity.dxftype() != "LINE":
            continue
        start = entity.dxf.start
        end = entity.dxf.end
        dx = abs(end[0] - start[0])
        if dx > 0.01:
            continue
        length = abs(end[1] - start[1])
        if length > longest_len:
            longest_len = length
            longest = entity
    return longest


def find_longest_horizontal_line(doc):
    """Findet die laengste horizontale Linie im Modelspace."""
    msp = doc.modelspace()
    longest = None
    longest_len = 0
    for entity in msp:
        if entity.dxftype() != "LINE":
            continue
        start = entity.dxf.start
        end = entity.dxf.end
        dy = abs(end[1] - start[1])
        if dy > 0.01:
            continue
        length = abs(end[0] - start[0])
        if length > longest_len:
            longest_len = length
            longest = entity
    return longest


def arc_endpoint(arc):
    """Gibt den Endpunkt (end_angle) des Bogens zurueck."""
    cx, cy, _ = arc.dxf.center
    r = arc.dxf.radius
    a_end = math.radians(arc.dxf.end_angle)
    return cx + r * math.cos(a_end), cy + r * math.sin(a_end)


# ---------------------------------------------------------------------------
# Einfuegepunkt-Berechnungen (je Typ)
# ---------------------------------------------------------------------------

def boegen_insertion_point(doc):
    """Tangentenschnittpunkt der Bogenkurve."""
    arc = find_arc(doc)
    if arc is None:
        return None
    return tangent_intersection(arc)


def weichen_insertion_point(doc):
    """Schnittpunkt der laengsten vertikalen Linie mit der Tangente am oberen Arc-Endpunkt."""
    vline = find_longest_vertical_line(doc)
    if vline is None:
        return None
    vx = vline.dxf.start[0]

    # Alle Arcs sammeln, oberen Punkt (max Y aus Start/Ende) waehlen
    best_angle = None
    best_y = -float('inf')
    best_arc = None
    for entity in doc.modelspace():
        if entity.dxftype() != "ARC":
            continue
        cx, cy, _ = entity.dxf.center
        r = entity.dxf.radius
        for angle_deg in (entity.dxf.start_angle, entity.dxf.end_angle):
            a = math.radians(angle_deg)
            py = cy + r * math.sin(a)
            if py > best_y:
                best_y = py
                best_angle = a
                best_arc = entity

    if best_arc is None:
        return None

    cx, cy, _ = best_arc.dxf.center
    r = best_arc.dxf.radius

    ex = cx + r * math.cos(best_angle)
    ey = cy + r * math.sin(best_angle)
    tdx = -math.sin(best_angle)
    tdy = math.cos(best_angle)

    if abs(tdx) < 1e-9:
        return vx, ey
    t = (vx - ex) / tdx
    return vx, ey + t * tdy


def weichenkoerper_insertion_point(doc):
    """Schnittpunkt der laengsten Diagonale mit der vertikalen Linie."""
    vline = find_longest_vertical_line(doc)
    if vline is None:
        return None
    vx = vline.dxf.start[0]

    best_point = None
    best_len = 0
    for entity in doc.modelspace():
        if entity.dxftype() != "LINE":
            continue
        s = entity.dxf.start
        e = entity.dxf.end
        dx = abs(e[0] - s[0])
        dy = abs(e[1] - s[1])
        if dx < 0.01 or dy < 0.01:
            continue
        length = (dx ** 2 + dy ** 2) ** 0.5
        if abs(s[0] - vx) < 0.01 and length > best_len:
            best_point = (s[0], s[1])
            best_len = length
        if abs(e[0] - vx) < 0.01 and length > best_len:
            best_point = (e[0], e[1])
            best_len = length
    return best_point


def horizontal_midpoint_insertion(doc):
    """Mittelpunkt der laengsten horizontalen Linie."""
    hline = find_longest_horizontal_line(doc)
    if hline is None:
        return None
    sx = hline.dxf.start[0]
    ex = hline.dxf.end[0]
    y = hline.dxf.start[1]
    return (sx + ex) / 2, y


# ---------------------------------------------------------------------------
# Schalter-Definitionen: (Label, JSON-Datei, Filter, Einfuegepunkt-Funktion)
# Filter kommen aus SWITCH_FILTERS in utils.py.
# ---------------------------------------------------------------------------

_INSERTION_FUNCS = {
    "boegen": boegen_insertion_point,
    "weichen45": weichen_insertion_point,
    "weichen90": weichen_insertion_point,
    "weichenkoerper": weichenkoerper_insertion_point,
    "weichen_parallel": weichenkoerper_insertion_point,
    "sternweiche": horizontal_midpoint_insertion,
    "delta": horizontal_midpoint_insertion,
    "dreifachweiche": horizontal_midpoint_insertion,
}

SWITCH_DEFS = {
    key: (*SWITCH_FILTERS[key], _INSERTION_FUNCS[key])
    for key in SWITCH_FILTERS
}


def process_dxf(data_dir, label, json_file, filter_func, insertion_func,
                results_dir, number=None):
    """Generische Verarbeitung: filtert IDs aus JSON, berechnet Einfuegepunkt, speichert."""
    json_path = os.path.join(data_dir, "json", json_file)
    if not os.path.exists(json_path):
        print(f"FEHLER: {json_path} nicht gefunden.")
        sys.exit(1)

    with open(json_path, "r", encoding="utf-8") as f:
        items = json.load(f)

    omniflo_dir = os.path.join(data_dir, "omniflo")
    filtered = [item for item in items if filter_func(item)]
    if number:
        filtered = [item for item in filtered if str(item["Sivasnr"]) == str(number)]

    if not filtered:
        print(f"Keine Elemente fuer '{label}' gefunden.")
        return

    print(f"=== {label} ===")
    for item in filtered:
        sivasnr = str(item["Sivasnr"])
        dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf")

        if not os.path.exists(dxf_path):
            print(f"WARNUNG: {dxf_path} nicht gefunden, ueberspringe.")
            continue

        doc = ezdxf.readfile(dxf_path)
        result = insertion_func(doc)

        if result is None:
            print(f"WARNUNG: Kein Einfuegepunkt in {sivasnr}.dxf bestimmbar, ueberspringe.")
            continue

        ix, iy = result
        old_base = doc.header.get("$INSBASE", (0, 0, 0))
        doc.header["$INSBASE"] = (ix, iy, 0)

        out_path = os.path.join(results_dir, f"{sivasnr}.dxf")
        doc.saveas(out_path)

        print(
            f"{sivasnr}: INSBASE ({old_base[0]:.2f}, {old_base[1]:.2f}) "
            f"-> ({ix:.2f}, {iy:.2f})  [{item['ProfilTyp']}]"
        )

    print(f"\nErgebnisse in: {results_dir}")


# ---------------------------------------------------------------------------
# show-omniflo
# ---------------------------------------------------------------------------

def show_omniflo(data_dir, results_dir):
    """Erzeugt eine Uebersichts-DXF mit allen Omniflo-Elementen in Reihen."""
    sources, rows = build_row_layout(data_dir, results_dir)

    target = ezdxf.new(dxfversion='R2010')
    target_msp = target.modelspace()
    target.layers.add('ANNOTATION', color=7)
    target.layers.add('INSPOINT', color=1)
    target.layers.add('ROW_LABEL', color=3)

    block_counter = 0

    for row in rows:
        label_y = row['cursor_y'] + row['max_height'] / 2
        target_msp.add_mtext(
            row['label'],
            dxfattribs={
                'layer': 'ROW_LABEL',
                'char_height': TEXT_HEIGHT * 1.2,
            }
        ).set_location(insert=(-ROW_LABEL_WIDTH, label_y))

        for elem in row['elements']:
            block_name = f"BLK_{block_counter}"
            block_counter += 1

            import_element_as_block(elem['source'], target, block_name)
            target_msp.add_blockref(block_name,
                                    insert=(elem['offset_x'], elem['offset_y']))

            insbase = elem['source'].header.get("$INSBASE", (0, 0, 0))
            ipx = insbase[0] + elem['offset_x']
            ipy = insbase[1] + elem['offset_y']
            draw_cross(target_msp, ipx, ipy, CROSS_SIZE, 1, 'INSPOINT')

            text_x = elem['offset_x'] + elem['extmin'][0]
            text_y = row['cursor_y'] + elem['height'] + TEXT_MARGIN
            target_msp.add_mtext(
                elem['sivasnr'],
                dxfattribs={
                    'layer': 'ANNOTATION',
                    'char_height': TEXT_HEIGHT,
                }
            ).set_location(insert=(text_x, text_y))

    out_path = os.path.join(results_dir, "omniflo_uebersicht.dxf")
    target.saveas(out_path)
    print(f"Uebersicht gespeichert: {out_path}")
    print(f"  {block_counter} Elemente in {len(rows)} Reihen")


# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Setzt Einfuegepunkte fuer Omniflo DXF-Dateien"
    )
    for key, (label, _, _, _) in SWITCH_DEFS.items():
        arg_name = f"--{key.replace('_', '-')}"
        parser.add_argument(arg_name, action="store_true", help=f"Einfuegepunkt fuer {label} setzen")
    parser.add_argument(
        "--number", type=int,
        help="Nur diese 9-stellige Sivasnr verarbeiten",
    )
    parser.add_argument(
        "--show-omniflo", action="store_true",
        help="Uebersichts-DXF mit allen Omniflo-Elementen erzeugen",
    )
    args = parser.parse_args()

    if args.number and len(str(args.number)) != 9:
        print("FEHLER: --number muss eine 9-stellige Ganzzahl sein.")
        sys.exit(1)

    active = [key for key in SWITCH_DEFS if getattr(args, key)]
    if not active and not args.show_omniflo:
        parser.print_help()
        sys.exit(1)

    data_dir = os.environ.get("DXFM_DATA")
    results_dir = os.environ.get("DXFM_RESULTS")

    if not data_dir or not results_dir:
        print("FEHLER: Umgebungsvariablen DXFM_DATA und DXFM_RESULTS muessen gesetzt sein.")
        print("Bitte zuerst setenv.bat ausfuehren.")
        sys.exit(1)

    os.makedirs(results_dir, exist_ok=True)

    for key in active:
        label, json_file, filter_func, insertion_func = SWITCH_DEFS[key]
        process_dxf(data_dir, label, json_file, filter_func, insertion_func,
                    results_dir, args.number)

    if args.show_omniflo:
        print("=== Omniflo Uebersicht ===")
        show_omniflo(data_dir, results_dir)


if __name__ == "__main__":
    main()
