# -*- coding: utf-8 -*-
"""KINTEX CAD(DWG) 실측 추출 파이프라인 — PLANNING R4(트렌치·기둥·비상구 실측 대체).

흐름:
  DWG --(LibreDWG dwg2dxf)--> DXF --(ezdxf)--> 레이어 인벤토리 / 키워드 추출 / 그리드 앵커 탐지

사용:
  python tools/cad/dwg_extract.py inventory <dxf> [out.txt]
  python tools/cad/dwg_extract.py extract   <dxf> <레이어키워드> [out.txt]   # 예: 트렌치, COL
  python tools/cad/dwg_extract.py anchors   <dxf> [out.txt]                  # 대형 라벨·그리드축 탐지

준비물(로컬 1회):
  - pip install ezdxf
  - LibreDWG win64(dwg2dxf.exe): https://github.com/LibreDWG/libredwg/releases
    dwg2dxf.exe -o out.dxf in.dwg
  단위: KINTEX 도면은 INSUNITS=4(mm) — 본 스크립트 출력은 m 환산.

2026-07-14 1차 분석 결과 요약은 docs/analysis/cad-extraction.md 참조.
"""
import io
import sys
from collections import Counter, defaultdict

import ezdxf

MM = 1000.0


def _points_of(e):
    t = e.dxftype()
    if t == "LINE":
        return [(e.dxf.start[0], e.dxf.start[1]), (e.dxf.end[0], e.dxf.end[1])]
    if t == "LWPOLYLINE":
        return [(p[0], p[1]) for p in e.get_points()]
    if t in ("INSERT", "TEXT", "MTEXT"):
        p = e.dxf.insert
        return [(p[0], p[1])]
    if t == "CIRCLE":
        p = e.dxf.center
        return [(p[0], p[1])]
    return []


def inventory(msp, rep):
    by_layer = defaultdict(Counter)
    for e in msp:
        by_layer[e.dxf.layer][e.dxftype()] += 1
    rep.write("레이어별 엔티티(상위 40):\n")
    for layer, cnt in sorted(by_layer.items(), key=lambda kv: -sum(kv[1].values()))[:40]:
        top = ", ".join(f"{t}:{c}" for t, c in cnt.most_common(4))
        rep.write(f"{sum(cnt.values()):7d}  {layer}  [{top}]\n")


def extract(msp, keyword, rep):
    segs = []
    for e in msp:
        if keyword.upper() not in e.dxf.layer.upper():
            continue
        pts = _points_of(e)
        for a, b in zip(pts, pts[1:]):
            segs.append((e.dxf.layer, a, b))
    rep.write(f"'{keyword}' 레이어 세그먼트: {len(segs)}\n")
    if not segs:
        return
    allp = [p for _, a, b in segs for p in (a, b)]
    xs = [p[0] / MM for p in allp]
    ys = [p[1] / MM for p in allp]
    rep.write(f"bbox(m): x[{min(xs):.1f}..{max(xs):.1f}] y[{min(ys):.1f}..{max(ys):.1f}]\n")
    for layer, a, b in segs[:80]:
        rep.write(f"  {layer}: ({a[0]/MM:.2f},{a[1]/MM:.2f}) -> ({b[0]/MM:.2f},{b[1]/MM:.2f})\n")
    if len(segs) > 80:
        rep.write(f"  … 외 {len(segs)-80}건 생략\n")


def anchors(msp, rep):
    """대형 텍스트(도면 제목·홀 라벨)와 구조 그리드 축 라벨(X/Y·63m 피치) 탐지."""
    texts = []
    for e in msp:
        if e.dxftype() not in ("TEXT", "MTEXT"):
            continue
        try:
            txt = (e.dxf.text if e.dxftype() == "TEXT" else e.text).strip()
            h = e.dxf.height if e.dxftype() == "TEXT" else e.dxf.char_height
            p = e.dxf.insert
        except Exception:
            continue
        if txt:
            texts.append((h, txt, p[0] / MM, p[1] / MM))
    texts.sort(key=lambda t: -t[0])
    rep.write("대형 텍스트(상위 40):\n")
    for h, t, x, y in texts[:40]:
        rep.write(f"  {h:7.0f}mm ({x:7.1f},{y:7.1f})m  {t[:60]}\n")
    ys = sorted(y for h, t, x, y in texts if t in ("Y", "RY"))
    if len(ys) >= 3:
        diffs = [round(b - a, 1) for a, b in zip(ys, ys[1:])]
        rep.write(f"\nY 그리드 라벨 y좌표(m): {[round(v,1) for v in ys]}\n인접 간격: {diffs}\n")
        rep.write("(63m 간격이면 = 홀 깊이 베이 — 홀별 Y밴드 앵커)\n")


def main():
    if len(sys.argv) < 3:
        print(__doc__)
        sys.exit(1)
    cmd, src = sys.argv[1], sys.argv[2]
    try:
        doc = ezdxf.readfile(src)
    except ezdxf.DXFStructureError:
        # LibreDWG 산출 DXF는 구조 결함이 있을 수 있음 — 관용 파서로 복구 로드.
        from ezdxf import recover

        doc, auditor = recover.readfile(src)
        print(f"recover: errors={len(auditor.errors)} fixes={len(auditor.fixes)}", file=sys.stderr)
    msp = doc.modelspace()
    rep = io.StringIO()
    rep.write(f"FILE {src}\nINSUNITS={doc.header.get('$INSUNITS', '?')}\n\n")
    if cmd == "inventory":
        inventory(msp, rep)
        out_default = "cad_inventory.txt"
    elif cmd == "extract":
        extract(msp, sys.argv[3], rep)
        out_default = "cad_extract.txt"
    elif cmd == "anchors":
        anchors(msp, rep)
        out_default = "cad_anchors.txt"
    else:
        print(__doc__)
        sys.exit(1)
    out = sys.argv[-1] if sys.argv[-1].endswith(".txt") else out_default
    with open(out, "w", encoding="utf-8") as f:
        f.write(rep.getvalue())
    print(f"OK -> {out}")


if __name__ == "__main__":
    main()
