# -*- coding: utf-8 -*-
"""비상구 실좌표 시드 생성기 — V65 (hall_exit 실측 대체, DOOR 레이어 기반).

방식: DOOR 계열 레이어 지오메트리를 홀 로컬로 변환, 홀 외곽 ±2m 밴드 내 포인트를
1m 셀 클러스터 → 문 규모(<=12m) 클러스터 중심을 가장 가까운 변에 투영 = 비상구 실좌표.
가정(is_assumed) 비상구는 실측 확보 홀에 한해 대체. K1=밴드, K2=회전 프레임.

사용:
  python tools/cad/extract_exit_seed.py <k1_trench.dxf> <k2_slim.dxf> <out.sql>
"""
import io
import math
import sys
from collections import deque

import ezdxf

MM = 1000.0
CELL = 1.0
EDGE_BAND = 2.0
MAX_DOOR_SPAN = 12.0
DOOR_KEYS = ("DOOR", "출입구")

K1 = {"x0": 293.5, "w": 171.0, "y0": 80.1, "h": 63.0, "halls": [1, 2, 3, 4, 5]}
K2_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),
}
# A/B 분리 셔터(가동벽) midline y — 셔터 트랙 끝점이 비상구로 오인되지 않게 ±2m 제외
SHUTTER_Y = {"H7": 63.0, "H8": 63.0, "H9": 66.0, "H10": 66.0}


def is_door(layer):
    u = layer.upper()
    return any(k.upper() in u for k in DOOR_KEYS)


def rot(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 sample_points(e):
    t = e.dxftype()
    try:
        if t == "LINE":
            a, b = e.dxf.start, e.dxf.end
            ln = math.hypot(b[0] - a[0], b[1] - a[1]) / MM
            n = max(2, int(ln / 0.5))
            return [((a[0] + (b[0] - a[0]) * i / n) / MM, (a[1] + (b[1] - a[1]) * i / n) / MM) for i in range(n + 1)]
        if t == "LWPOLYLINE":
            pts = [(p[0] / MM, p[1] / MM) for p in e.get_points()]
            out = []
            for a, b in zip(pts, pts[1:]):
                ln = math.hypot(b[0] - a[0], b[1] - a[1])
                n = max(1, int(ln / 0.5))
                out += [(a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n) for i in range(n + 1)]
            return out
        if t in ("ARC", "CIRCLE"):
            return [(p[0] / MM, p[1] / MM) for p in e.flattening(200)]
    except Exception:
        pass
    return []


def edge_exits(points, w, h):
    """홀 로컬 문 포인트 → 외곽 밴드 클러스터 → 변 투영 비상구 좌표 목록."""
    cells = set()
    for lx, ly in points:
        near = lx <= EDGE_BAND or lx >= w - EDGE_BAND or ly <= EDGE_BAND or ly >= h - EDGE_BAND
        if near and 0 <= lx < w and 0 <= ly < h:
            cells.add((int(lx / CELL), int(ly / CELL)))
    seen = set()
    exits = []
    for cell in sorted(cells):
        if cell in seen:
            continue
        q = deque([cell])
        seen.add(cell)
        comp = []
        while q:
            ci, cj = q.popleft()
            comp.append((ci, cj))
            for ni, nj in ((ci + 1, cj), (ci - 1, cj), (ci, cj + 1), (ci, cj - 1),
                           (ci + 1, cj + 1), (ci - 1, cj - 1), (ci + 1, cj - 1), (ci - 1, cj + 1)):
                if (ni, nj) in cells and (ni, nj) not in seen:
                    seen.add((ni, nj))
                    q.append((ni, nj))
        xs = [c[0] for c in comp]
        ys = [c[1] for c in comp]
        span = max(max(xs) - min(xs), max(ys) - min(ys)) + 1
        if span > MAX_DOOR_SPAN:
            continue  # 분리 셔터 트랙 등 장스팬 제외
        cx = (min(xs) + max(xs) + 1) / 2 * CELL
        cy = (min(ys) + max(ys) + 1) / 2 * CELL
        # 가장 가까운 변으로 투영
        d = [(cx, "W"), (w - cx, "E"), (cy, "N"), (h - cy, "S")]
        dist, side = min(d)
        if side == "W":
            cx = 0.0
        elif side == "E":
            cx = w
        elif side == "N":
            cy = 0.0
        else:
            cy = h
        exits.append((round(cx, 1), round(cy, 1)))
    # 1.5m 이내 중복 병합
    merged = []
    for x, y in sorted(exits):
        if not any(abs(x - mx) < 1.5 and abs(y - my) < 1.5 for mx, my in merged):
            merged.append((x, y))
    return merged


def main():
    k1_src, k2_src, out_sql = sys.argv[1], sys.argv[2], sys.argv[3]
    per_hall = {}

    doc1 = ezdxf.readfile(k1_src)
    k1_pts = {n: [] for n in K1["halls"]}
    for e in doc1.modelspace():
        if not is_door(e.dxf.layer):
            continue
        for x, y in sample_points(e):
            lx = x - K1["x0"]
            if not (0 <= lx < K1["w"]):
                continue
            for n in K1["halls"]:
                y0 = K1["y0"] + (n - 1) * K1["h"]
                if y0 <= y < y0 + K1["h"]:
                    k1_pts[n].append((lx, K1["h"] - (y - y0)))
    for n in K1["halls"]:
        per_hall[f"H{n}"] = (edge_exits(k1_pts[n], K1["w"], K1["h"]), K1["w"], K1["h"])

    doc2 = ezdxf.readfile(k2_src)
    k2_pts = {h: [] for h in K2_FRAMES}
    for e in doc2.modelspace():
        if not is_door(e.dxf.layer):
            continue
        for x, y in sample_points(e):
            for hall, (deg, c, u0, u1, v0, v1) in K2_FRAMES.items():
                u, v = rot(x, y, deg, c)
                if u0 <= u < u1 and v0 <= v < v1:
                    k2_pts[hall].append((u - u0, v1 - v))
    for hall, (deg, c, u0, u1, v0, v1) in K2_FRAMES.items():
        exits = edge_exits(k2_pts[hall], u1 - u0, v1 - v0)
        sy = SHUTTER_Y.get(hall)
        if sy is not None:
            exits = [(x, y) for x, y in exits if abs(y - sy) > 2.0]
        per_hall[hall] = (exits, u1 - u0, v1 - v0)

    sql = io.StringIO()
    sql.write(
        "-- V65: 비상구 실좌표 시드 — CAD DOOR 레이어 경계 문 클러스터(±2m 밴드) 변 투영.\n"
        "-- 생성기: tools/cad/extract_exit_seed.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md.\n"
        "-- 실측 확보 홀만 가정(is_assumed) 비상구를 대체. 분리 셔터 등 12m 초과 장스팬 제외.\n\n"
    )
    report = []
    for hall, (exits, w, h) in per_hall.items():
        report.append(f"{hall}: {len(exits)}개 {exits[:8]}")
        if len(exits) < 2:
            sql.write(f"-- {hall}: 경계 문 {len(exits)}개 — 신뢰 부족, 가정 비상구 유지\n")
            continue
        sql.write(f"-- {hall}: 실측 비상구 {len(exits)}개\nDELETE FROM hall_exit WHERE hall_id = '{hall}' AND is_assumed;\n")
        vals = ",\n    ".join(
            f"('{hall}-EXIT-R{i}', '{hall}', ST_SetSRID(ST_MakePoint({x}, {y}), 0), 3.0, false)"
            for i, (x, y) in enumerate(exits, 1)
        )
        sql.write(
            "INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)\nVALUES\n    "
            + vals + "\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}")


if __name__ == "__main__":
    main()
