# -*- coding: utf-8 -*-
"""K2(제2전시장 H6~H10) 트렌치 실측 시드 생성기 — V66 (PLANNING R4 실측 대체, K1 V61의 K2판).

소스: 원본-1층 평면도.dwg → LibreDWG dwg2dxf 변환본(예: scratchpad k2_plan1f.dxf).
  홀 프레임(FRAMES)을 실측한 파일과 동일 도면이라 좌표계가 자동 정합된다.
  ※ "2전시장 E3-59~69 지상1층 전력간선 평면도(분할).dwg"는 교차검증용으로만 사용 —
    그 안의 '전시장 유틸라인' xref는 x=509.2m에서 잘린 크롭본 + 채널 위상이 ~3.5m 어긋나
    시드 소스로 부적합(docs/analysis/cad-extraction.md §K2 트렌치 참조).

방식: 111MB DXF를 ezdxf 없이 스트리밍 태그 파싱(4GB RAM 노트북 안전) —
  BLOCKS 내부까지 트렌치 레이어(LINE/LWPOLYLINE) 수집, INSERT 체인 변환으로 월드 좌표 복원,
  홀 프레임(±11~12° 회전) 로컬로 변환·클리핑 후 가로 채널 행을 병합(쌍선 스냅 1.5m),
  채널을 따라 6m 간격 공급 포인트 생성(K1과 동일 샘플링 규약).

사용:
  python tools/cad/extract_trench_seed_k2.py <k2_plan1f.dxf> <out.sql>

좌표 규약: CAD mm → 홀 로컬 m. 홀 로컬 y=0은 도면 상단(v=v1), y = v1 - v (상하 반전).
"""
import io
import math
import sys
from collections import defaultdict

MM = 1000.0
MIN_RUN_M = 30.0      # 채널 인정 최소 연장(짧은 스퍼·피더 배제 — K1 동일)
POINT_STEP_M = 6.0    # 공급 포인트 샘플링 간격(K1 동일 규약)
ROW_SNAP_M = 1.5      # 쌍선(채널 양변) 병합 허용(K1 동일)
HORIZ_TOL_M = 1.0     # 프레임 로컬 수평 판정 허용 dv

# 홀 프레임(fork 확정, docs/analysis/cad-extraction.md §K2): (회전deg, 중심, u0,u1, v0,v1)
FRAMES = {
    "H6": (-12, (530, 405), 482, 542, 350.5, 443.5),
    "H7": (0, (0, 0), 439, 529, 215.5, 341.5),
    "H8": (0, (0, 0), 439, 529, 89.5, 215.5),
    "H9": (-11, (245, 155), 196, 295, 95.5, 227.5),
    "H10": (11, (255, 330), 194.5, 293.5, 250, 382),
}

TRENCH_KEYS = [b"TRENCH", "트렌치".encode("cp949"), "트랜치".encode("cp949")]


def is_trench_layer(layer_bytes):
    u = layer_bytes.upper()
    return any(k in u for k in TRENCH_KEYS)


# ---------- 1) DXF 스트리밍 파싱 (BLOCKS + ENTITIES) ----------

def parse(src):
    segs_by_container = defaultdict(list)    # container(bytes) -> [(x1,y1,x2,y2)] mm
    inserts_by_container = defaultdict(list)  # container -> [(name,x,y,sx,sy,rot)]
    section = None
    container = None
    ent = None

    def flush(ent):
        if ent is None:
            return
        c = ent["container"]
        if c is None or ent.get("paper"):
            return
        t = ent["type"]
        if t == b"INSERT" and ent.get("name"):
            inserts_by_container[c].append(
                (ent["name"], ent.get("x", 0.0), ent.get("y", 0.0),
                 ent.get("sx", 1.0), ent.get("sy", 1.0), ent.get("rot", 0.0)))
            return
        if not is_trench_layer(ent.get("layer", b"")):
            return
        if t == b"LINE":
            if all(k in ent for k in ("x", "y", "x2", "y2")):
                segs_by_container[c].append((ent["x"], ent["y"], ent["x2"], ent["y2"]))
        elif t == b"LWPOLYLINE":
            pts = ent.get("pts", [])
            for a, b in zip(pts, pts[1:]):
                segs_by_container[c].append((a[0], a[1], b[0], b[1]))

    f = open(src, "rb", buffering=1024 * 1024 * 8)
    expect_section_name = False
    while True:
        code_raw = f.readline()
        if not code_raw:
            break
        val = f.readline()
        if not val:
            break
        try:
            code = int(code_raw)
        except ValueError:
            continue
        v = val.strip()
        if code == 0:
            flush(ent)
            ent = None
            if v == b"SECTION":
                expect_section_name = True
            elif v == b"ENDSEC":
                section = None
                container = None
            elif v == b"BLOCK" and section == b"BLOCKS":
                ent = {"type": b"BLOCK", "container": None}
            elif v == b"ENDBLK":
                container = None
            elif section in (b"BLOCKS", b"ENTITIES") and v in (b"LINE", b"LWPOLYLINE", b"INSERT"):
                ent = {"type": v, "container": container}
            continue
        if expect_section_name and code == 2:
            section = v
            expect_section_name = False
            if section == b"ENTITIES":
                container = b"*MS"
            continue
        if ent is None:
            continue
        if ent["type"] == b"BLOCK":
            if code == 2:
                container = v
                ent = None
            continue
        if code == 8:
            ent["layer"] = v
        elif code == 67:
            ent["paper"] = v == b"1"
        elif code == 2 and ent["type"] == b"INSERT":
            ent["name"] = v
        elif code == 10:
            x = float(v)
            if ent["type"] == b"LWPOLYLINE":
                ent.setdefault("pts", []).append([x, 0.0])
            else:
                ent["x"] = x
        elif code == 20:
            y = float(v)
            if ent["type"] == b"LWPOLYLINE":
                pts = ent.get("pts")
                if pts:
                    pts[-1][1] = y
            else:
                ent["y"] = y
        elif code == 11:
            ent["x2"] = float(v)
        elif code == 21:
            ent["y2"] = float(v)
        elif code == 41:
            ent["sx"] = float(v)
        elif code == 42:
            ent["sy"] = float(v)
        elif code == 50 and ent["type"] == b"INSERT":
            ent["rot"] = float(v)
    flush(ent)
    f.close()
    return segs_by_container, inserts_by_container


# ---------- 2) INSERT 체인 해석 → 월드 세그(m) ----------

def _transform(px, py, ins):
    x0, y0, sx, sy, rot = ins
    px, py = px * sx, py * sy
    if rot:
        t = math.radians(rot)
        px, py = px * math.cos(t) - py * math.sin(t), px * math.sin(t) + py * math.cos(t)
    return px + x0, py + y0


def world_segs(segs_by_container, inserts_by_container):
    cache = {}

    def placements(cname, depth=0):
        if cname == b"*MS":
            return [lambda p: p]
        if depth > 6:
            return []
        if cname in cache:
            return cache[cname]
        out = []
        for parent, inss in inserts_by_container.items():
            for ins in inss:
                if ins[0] != cname:
                    continue
                geo = ins[1:]
                for up in placements(parent, depth + 1):
                    out.append(lambda p, geo=geo, up=up: up(_transform(p[0], p[1], geo)))
        cache[cname] = out
        return out

    ws = []
    for c, ss in segs_by_container.items():
        for pl in placements(c):
            for (x1, y1, x2, y2) in ss:
                wx1, wy1 = pl((x1, y1))
                wx2, wy2 = pl((x2, y2))
                ws.append((wx1 / MM, wy1 / MM, wx2 / MM, wy2 / MM))
    return ws


# ---------- 3) 홀 프레임 변환·클리핑·채널 병합 ----------

def _rot_frame(x, y, deg, c):
    if deg == 0:
        return x, y
    t = math.radians(-deg)
    dx, dy = x - c[0], y - c[1]
    return c[0] + dx * math.cos(t) - dy * math.sin(t), c[1] + dx * math.sin(t) + dy * math.cos(t)


def _clip(ua, va, ub, vb, u0, u1, v0, v1):
    """Liang-Barsky 세그 사각 클리핑."""
    du, dv = ub - ua, vb - va
    t0, t1 = 0.0, 1.0
    for p, q in ((-du, ua - u0), (du, u1 - ua), (-dv, va - v0), (dv, v1 - va)):
        if p == 0:
            if q < 0:
                return None
            continue
        r = q / p
        if p < 0:
            if r > t1:
                return None
            t0 = max(t0, r)
        else:
            if r < t0:
                return None
            t1 = min(t1, r)
    if t0 > t1:
        return None
    return (ua + t0 * du, ua + t1 * du, (va + vb) / 2 + 0 * t0)  # (cu0, cu1, vm) — 수평 전제


def channels_of(ws):
    """홀별 채널 [(center_y, x0, x1)] — 로컬 y=0 도면 상단(상하 반전)."""
    out = {}
    for hall, (deg, c, u0, u1, v0, v1) in FRAMES.items():
        rows = defaultdict(list)
        for (x1, y1, x2, y2) in ws:
            ua, va = _rot_frame(x1, y1, deg, c)
            ub, vb = _rot_frame(x2, y2, deg, c)
            if abs(vb - va) > HORIZ_TOL_M:
                continue
            cl = _clip(ua, va, ub, vb, u0, u1, v0, v1)
            if cl is None:
                continue
            cu0, cu1, vm = cl
            lx0, lx1 = min(cu0, cu1) - u0, max(cu0, cu1) - u0
            if lx1 - lx0 < MIN_RUN_M:
                continue
            rows[round((v1 - vm) * 2) / 2].append((lx0, lx1))
        merged = []  # (center_y, x0, x1)
        for y, spans in sorted(rows.items()):
            x0 = min(s[0] for s in spans)
            x1 = max(s[1] for s in spans)
            if merged and y - merged[-1][0] <= ROW_SNAP_M:
                py, px0, px1 = merged[-1]
                merged[-1] = ((py + y) / 2, min(px0, x0), max(px1, x1))
            else:
                merged.append((y, x0, x1))
        out[hall] = merged
    return out


# ---------- 4) SQL 산출 ----------

def main():
    src, out_sql = sys.argv[1], sys.argv[2]
    segs_by_container, inserts_by_container = parse(src)
    ws = world_segs(segs_by_container, inserts_by_container)
    chans = channels_of(ws)

    sql = io.StringIO()
    sql.write(
        "-- V66: K2 홀6~10 트렌치 실측 시드 — CAD(원본-1층 평면도.dwg) TRENCH 레이어 추출(V61의 K2판).\n"
        "-- 생성기: tools/cad/extract_trench_seed_k2.py (재생성 가능·결정적). 근거: docs/analysis/cad-extraction.md §K2 트렌치.\n"
        "-- 실측: 홀 프레임(±11~12° 회전) 로컬 가로 채널(쌍선 병합 중심선)·연장 실좌표. 포인트 6m 간격은 샘플링 규약.\n"
        "-- 교차검증: 전력간선 평면도(E3-59~69)의 채널 구조(9m 피치·쌍선·홀 배치각)와 정합 —\n"
        "--   단, 그 도면의 유틸라인 xref는 크롭본(x<=509.2m)이라 시드 소스는 홀 프레임과 동일 원점인 원본 평면도 사용.\n"
        "-- 정책: 실측 확보 홀(H6~H10)의 가정(is_assumed) 포인트를 실측 포인트로 대체(K1 V61과 동일).\n"
        "-- 공급 플래그는 V5 정책 유지(공동구 — 전 서비스 true·gas는 홀 보유 여부), 위치만 실측 대체.\n\n"
    )
    report = []
    total = 0
    for hall, merged in chans.items():
        report.append(f"{hall}: 채널 {len(merged)}개 " + ", ".join(f"y={m[0]:.1f} x[{m[1]:.1f}..{m[2]:.1f}]" for m in merged))
        if not merged:
            continue
        total += len(merged)
        sql.write(f"-- {hall}: 실측 채널 {len(merged)}개\n")
        sql.write(f"DELETE FROM trench WHERE hall_id = '{hall}' AND is_assumed;\n")
        values = []
        for ri, (cy, x0, x1) in enumerate(merged, 1):
            k = 0
            x = x0
            while x <= x1 + 1e-6:
                k += 1
                values.append(
                    f"('{hall}-RT-{ri}-{k}', '{hall}', ST_SetSRID(ST_MakePoint({x:.2f}, {cy:.2f}), 0), "
                    f"true, true, true, true, (SELECT has_gas FROM hall WHERE id='{hall}'), false)"
                )
                x += POINT_STEP_M
        sql.write(
            "INSERT INTO trench (id, hall_id, geom, supply_power, supply_water, supply_air, supply_network, supply_gas, is_assumed)\nVALUES\n    "
            + ",\n    ".join(values)
            + "\nON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;\n\n"
        )

    with open(out_sql, "w", encoding="utf-8") as f:
        f.write(sql.getvalue())
    for line in report:
        print(line)
    print(f"OK -> {out_sql} (채널 {total}개)")


if __name__ == "__main__":
    main()
