# -*- coding: utf-8 -*-
"""create_dxf.py — erzeugt eine DXF-Uebersicht aller TROs mit ihren Sensoren.

Quelle sind die vom Skeleton-Generator erzeugten results/skeleton_UH0x.json
(Sensor-Positionen aus der Positions-JSON, Einheiten mit IO-Zuordnung).

Je TRO-Einheit wird ein Block "TRO_BOX" (Rechteck + eingerahmter Name)
am Schwerpunkt seiner Sensoren eingefuegt; die TRO-Namen entsprechen der
Nummerierung des SCL-Grundgeruests (create_skel.py). Jeder Sensor wird als
Block "SENSOR" (Kreis + Name) an seiner x/y-Position eingefuegt und per
Linie mit seinem TRO verbunden.

Block-Attribute (fuer Identifikation im CAD, teils unsichtbar):
  TRO_BOX: NAME, FBTYPE (sichtbar) — UNIT, PLC, AREAS, SENSORS, IO (unsichtbar)
  SENSOR:  NAME (sichtbar) — ROLE, TRO, PLC (unsichtbar)

Pfade ueber Umgebungsvariablen aus bin\\setenv.bat:
  PV_RESULTS   Eingabe (skeleton_UH0x.json) und Ausgabe (DXF)

Mit --colors werden die TRO-Kaesten zusaetzlich nach FB-Typ eingefaerbt
(SOLID-Fuellung auf Layer TRO_FUELL, gleiche Palette wie im Topologie-
Graphen doc/TRO_Graph_UH01-UH05.dot); ohne den Schalter bleiben die
Kaesten ungefuellt.

Aufruf:  bin\\create_dxf.bat  [--uh 1..5]  [--out DATEI.dxf]  [--colors]
"""

import argparse
import json
import os
import sys
from pathlib import Path

try:
    import ezdxf
    from ezdxf.enums import TextEntityAlignment
except ImportError:
    sys.exit("ezdxf fehlt — bitte bin\\install_py.bat ausfuehren.")

# ============================================================================
# GLOBALE DEFINITIONEN (Geometrie in mm, Layer, Farben)
# ============================================================================

OUT_NAME = "ST500592_TROs.dxf"

BOX_W = 2500            # TRO-Kasten Breite
BOX_H = 1200            # TRO-Kasten Hoehe
BOX_TEXT_H = 380        # Texthoehe TRO-Name
BOX_SUB_H = 220         # Texthoehe FB-Typ
SEN_RADIUS = 120        # Sensor-Kreisradius
SEN_TEXT_H = 150        # Texthoehe Sensorname
ATTR_MAXLEN = 250       # DXF-Attributwerte kuerzen

LAYERS = {
    "TRO": 7,           # Kaesten (Farbe je PLC am Insert)
    "TRO_FUELL": 7,     # Kasten-Fuellung (Truecolor je FB-Typ)
    "SENSOR": 8,        # Sensoren mit TRO-Zuordnung
    "SENSOR_FREI": 9,   # Sensoren ohne Einheit (nur Position)
    "VERBINDUNG": 253,  # Linien Sensor -> TRO
}

#: ACI-Farbe je Steuerung
PLC_COLOR = {1: 1, 2: 2, 3: 3, 4: 4, 5: 6}   # rot, gelb, gruen, cyan, magenta

#: Fuellfarbe je FB-Typ (Truecolor) — gleiche Palette wie im
#: Topologie-Graphen doc/TRO_Graph_UH01-UH05.dot
TYPE_COLOR = {
    "FB_ILS_MTRO_1Sep": 0xD4E8FF,               # hellblau
    "FB_ILS_MTRO_1Sep_SSCC": 0xFFD4D4,          # rot
    "FB_ILS_MTRO_1Sep1Swi": 0xFFE4A0,           # gelb (Weiche)
    "FB_ILS_MTRO_1Sep2Swi": 0xFFC060,           # orange (Weiche)
    "FB_ILS_MTRO_2Sep1Swi": 0xFFE4A0,           # gelb (Weiche)
    "FB_ILS_MTRO_Vario": 0xE8D4F0,              # violett
    "FB_ILS_MTRO_Vario_workStation": 0xC0F0F0,  # tuerkis
    "FB_ILS_MTRO_PinStore_Auto": 0xD4F0D4,      # gruen (Lager)
    "FB_EmptyCarrBuffer": 0xD4F0D4,             # gruen (Puffer)
    "FB_Conveyor": 0xF0F0F0,                    # grau
}
TYPE_COLOR_DEFAULT = 0xF0F0F0                   # grau (Sonstige/UNBEKANNT)


def type_color(fbtype):
    for key in sorted(TYPE_COLOR, key=len, reverse=True):
        if fbtype.startswith(key):
            return TYPE_COLOR[key]
    return TYPE_COLOR_DEFAULT

# ============================================================================


def project_paths():
    root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
    results = Path(os.environ.get("PV_RESULTS", root / "results"))
    return results


def build_blocks(doc):
    """Blockdefinitionen TRO_BOX und SENSOR anlegen."""
    tro = doc.blocks.new(name="TRO_BOX")
    w2, h2 = BOX_W / 2, BOX_H / 2
    tro.add_lwpolyline([(-w2, -h2), (w2, -h2), (w2, h2), (-w2, h2)],
                       close=True)
    att = tro.add_attdef("NAME", height=BOX_TEXT_H)
    att.set_placement((0, BOX_H * 0.12), align=TextEntityAlignment.MIDDLE_CENTER)
    att = tro.add_attdef("FBTYPE", height=BOX_SUB_H)
    att.set_placement((0, -BOX_H * 0.28), align=TextEntityAlignment.MIDDLE_CENTER)
    for tag in ("UNIT", "PLC", "AREAS", "SENSORS", "IO"):
        att = tro.add_attdef(tag, height=BOX_SUB_H)
        att.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
        att.dxf.invisible = 1

    sen = doc.blocks.new(name="SENSOR")
    sen.add_circle((0, 0), SEN_RADIUS)
    att = sen.add_attdef("NAME", height=SEN_TEXT_H)
    att.set_placement((SEN_RADIUS * 1.4, SEN_RADIUS * 1.2),
                      align=TextEntityAlignment.BOTTOM_LEFT)
    for tag in ("ROLE", "TRO", "PLC"):
        att = sen.add_attdef(tag, height=SEN_TEXT_H)
        att.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
        att.dxf.invisible = 1


def instance_names(skel, uh_digit):
    """TRO-Instanznamen je Einheit — identische Nummerierung wie
    render_scl() in create_skel.py (Conveyor verbraucht eine lfd. Nummer)."""
    names = {}
    seq = 0
    for u in skel["units"]:
        if u["fbType"] == "UNBEKANNT":
            names[u["unit"]] = u["unit"]          # kein TRO-Name vergeben
            continue
        seq += 1
        if u["fbType"] == "FB_Conveyor":
            names[u["unit"]] = f"fbConveyor{u['unit'][1:]}"
        else:
            names[u["unit"]] = f"TRO{uh_digit}{seq:02d}"
    return names


def cut(s):
    return s if len(s) <= ATTR_MAXLEN else s[:ATTR_MAXLEN - 3] + "..."


def draw_plc(msp, skel, n, colors=False):
    """Alle Einheiten + Sensoren einer Steuerung zeichnen."""
    uh = f"UH0{n}"
    color = PLC_COLOR[n]
    names = instance_names(skel, str(n))

    # Sensoren je Einheit sammeln (nur mit Position)
    unit_sensors = {}
    free_sensors = []
    for s in skel["sensors"]:
        if not s.get("pos"):
            continue
        if s.get("unit"):
            unit_sensors.setdefault(s["unit"], []).append(s)
        else:
            free_sensors.append(s)

    boxes = lines = 0
    for u in skel["units"]:
        sens = unit_sensors.get(u["unit"], [])
        if sens:                                   # Schwerpunkt der Sensoren
            cx = sum(s["pos"][0] for s in sens) / len(sens)
            cy = sum(s["pos"][1] for s in sens) / len(sens)
        elif u.get("pos"):
            cx, cy = u["pos"][0], u["pos"][1]
        else:
            print(f"  {uh} {u['unit']}: keine Position — uebersprungen")
            continue

        io_txt = ";".join(f"{role}={','.join(v)}"
                          for role, v in sorted(u["io"].items()))
        if colors:
            # Fuellung in FB-Typ-Farbe (vor dem Insert -> liegt dahinter)
            w2, h2 = BOX_W / 2, BOX_H / 2
            msp.add_solid(
                [(cx - w2, cy - h2), (cx + w2, cy - h2),
                 (cx - w2, cy + h2), (cx + w2, cy + h2)],
                dxfattribs={"layer": "TRO_FUELL",
                            "true_color": type_color(u["fbType"])})
        blk = msp.add_blockref("TRO_BOX", (cx, cy),
                               dxfattribs={"layer": "TRO", "color": color})
        blk.add_auto_attribs({
            "NAME": names[u["unit"]],
            "FBTYPE": u["fbType"].replace("FB_ILS_MTRO_", "").replace("FB_", ""),
            "UNIT": u["unit"],
            "PLC": uh,
            "AREAS": cut(",".join(a["id"] for a in u["areas"])),
            "SENSORS": cut(",".join(s["id"] for s in sens)),
            "IO": cut(io_txt),
        })
        boxes += 1

        for s in sens:
            sx, sy = s["pos"][0], s["pos"][1]
            sb = msp.add_blockref("SENSOR", (sx, sy),
                                  dxfattribs={"layer": "SENSOR",
                                              "color": color})
            sb.add_auto_attribs({
                "NAME": s["id"],
                "ROLE": s.get("role") or "",
                "TRO": names[u["unit"]],
                "PLC": uh,
            })
            msp.add_line((sx, sy), (cx, cy),
                         dxfattribs={"layer": "VERBINDUNG"})
            lines += 1

    for s in free_sensors:
        sb = msp.add_blockref("SENSOR", (s["pos"][0], s["pos"][1]),
                              dxfattribs={"layer": "SENSOR_FREI"})
        sb.add_auto_attribs({
            "NAME": s["id"],
            "ROLE": s.get("role") or "",
            "TRO": "",
            "PLC": uh,
        })

    print(f"  {uh}: {boxes} TRO-Kaesten, {lines} Sensor-Verbindungen, "
          f"{len(free_sensors)} freie Sensoren")


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--uh", type=int, choices=range(1, 6),
                    help="nur diese Steuerung (1..5), sonst alle")
    ap.add_argument("--out", default=OUT_NAME,
                    help=f"Ausgabedatei (Default: {OUT_NAME})")
    ap.add_argument("--colors", action="store_true",
                    help="TRO-Kaesten nach FB-Typ einfaerben "
                         "(Palette wie doc/TRO_Graph_UH01-UH05.dot)")
    args = ap.parse_args()

    results = project_paths()
    doc = ezdxf.new("R2010", setup=True)
    doc.header["$INSUNITS"] = 4                    # Millimeter
    for name, aci in LAYERS.items():
        doc.layers.add(name, color=aci)
    build_blocks(doc)
    msp = doc.modelspace()

    print("Erzeuge DXF aus Skeleton-Dateien:")
    for n in ([args.uh] if args.uh else range(1, 6)):
        skel_file = results / f"skeleton_UH0{n}.json"
        if not skel_file.exists():
            sys.exit(f"FEHLER: {skel_file} fehlt — zuerst bin\\create_skel.bat "
                     f"ausfuehren.")
        skel = json.loads(skel_file.read_text(encoding="utf-8"))
        draw_plc(msp, skel, n, colors=args.colors)

    out = results / args.out
    doc.saveas(out)
    print(f"DXF geschrieben: {out}")


if __name__ == "__main__":
    main()
