"""s12 — 블록 세트장 실험 (사용자 지시 2026-07-05):

LLM 이 그룹의 **전 샷 원문+멤버 정보를 취합**해 "상대적 크기·배치·구조적으로
중요한 정보"만 구조화 JSON 으로 추출하고, **그리는 것은 결정론 코드**(PIL) —
블록 형태 세트장 도면(구조 파악 전용, AI 이미지 아님).
  - 네모/동그라미 블록, 구조적으로 중요한 창/문, 원형 설비(물탱크류),
    계단(발판 기호), 평상류(낮은 사각), 장비(실외기류), 건물 입구 주변 도로.
  - 이웃 건물은 민네모로 간략화. 이야기 소품/차량/사람 배제(evidence 필수).
시나리오 중립: 코드에는 기하 프리미티브/일반 kind 만. 구체 명칭은 LLM 이
데이터에서 추출(name_en 자유 텍스트).
사용: .venv/bin/python s12_blockset.py [--only plan|render|html]
산출: plans/blockset_layout.json, out/blockset/blockset.png, blockset.html
"""
import argparse
import html as _html
import json
import math
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402

OUTB = F.OUT / "blockset"
PAGE = F.EXP / "blockset.html"

KINDS = ["building_mass", "upper_shell", "terrace", "yard", "road", "alley",
         "stair", "gate", "door", "window", "fixture_round", "fixture_rect",
         "equipment", "neighbor_mass", "wall"]

LAYOUT_SYSTEM = """You are a set-layout analyst for film pre-production. You get one
real-world place's member descriptions and the FULL content of every shot there.
Build ONE top-down BLOCK LAYOUT of the whole property as structured data for a
code renderer — plain blocks for structural understanding (relative sizes and
positions), not art.

Canvas: 100 wide x 100 tall, x rightward, y downward. Use most of the canvas.
Choose relative sizes and positions so every shot's action space exists and the
spatial relations the data implies (what is beside/under/above what, what
connects what, which side an entrance or stair is on) stay consistent. Where the
data implies a layout fact, honour it; otherwise choose one plausible consistent
arrangement.

Element rules:
- kind must be one of: building_mass (main building footprint), upper_shell
  (an enclosed upper-level unit — outline only, never interior), terrace (open roof
  deck), yard, road, alley, stair, gate, door, window, fixture_round (round
  free-standing fixture), fixture_rect (low rectangular fixture), equipment
  (small mounted/free machine unit), neighbor_mass, wall.
- shape "rect": x,y = top-left corner, w,h = sizes, rot_deg rotates around the
  rect centre. shape "circle": x,y = centre, w = h = diameter, rot_deg = 0.
- level: 0 = ground, 1 = main roof / terrace level, 2 = top of the upper shell.
  A stair connecting levels lists both in connects_levels (otherwise []).
- doors/windows/gates sit ON their block's edge line, sized to that wall.
  Mark ONLY structurally important openings (the ones shots depend on).
- Include the road/alley approach around the building's street entrance.
- Simplify ALL neighbouring buildings to plain neighbor_mass rects (no detail,
  no openings, no fixtures).
- Include the permanent fixtures shots rely on (round free-standing units ->
  fixture_round, low rectangular units -> fixture_rect, machine units ->
  equipment). NO story props,
  no vehicles, no people, nothing temporary.
- 8 to 20 elements total. name_en: 1-3 generic English words, no proper names.
- EVERY element needs evidence: source = a shot key or member id, quote = the
  exact grounding phrase copied from the data (original language). Do not
  invent elements without evidence."""

LAYOUT_SCHEMA = {
    "type": "object",
    "properties": {
        "canvas": {"type": "object", "properties": {
            "width": {"type": "number"}, "height": {"type": "number"},
            "orientation_note": {"type": "string"}},
            "required": ["width", "height", "orientation_note"],
            "additionalProperties": False},
        "elements": {"type": "array", "items": {"type": "object", "properties": {
            "id": {"type": "string"},
            "name_en": {"type": "string"},
            "kind": {"type": "string", "enum": KINDS},
            "level": {"type": "integer"},
            "shape": {"type": "string", "enum": ["rect", "circle"]},
            "x": {"type": "number"}, "y": {"type": "number"},
            "w": {"type": "number"}, "h": {"type": "number"},
            "rot_deg": {"type": "number"},
            "importance": {"type": "string", "enum": ["major", "minor"]},
            "connects_levels": {"type": "array", "items": {"type": "integer"}},
            "evidence": {"type": "array", "items": {"type": "object",
                "properties": {"source": {"type": "string"},
                               "quote": {"type": "string"}},
                "required": ["source", "quote"],
                "additionalProperties": False}},
            "notes": {"type": "string"}},
            "required": ["id", "name_en", "kind", "level", "shape", "x", "y",
                         "w", "h", "rot_deg", "importance", "connects_levels",
                         "evidence", "notes"],
            "additionalProperties": False}},
        "layout_notes": {"type": "string"},
    },
    "required": ["canvas", "elements", "layout_notes"],
    "additionalProperties": False,
}


def gen_layout(recon):
    members = F.members_block(recon["members"])
    shot_blocks = "\n\n".join(
        F.shot_block(k, s) for k, s in sorted(recon["shots"].items()))
    user = ("PLACE MEMBERS:\n" + members
            + "\n\nALL SHOTS AT THIS PLACE (full content):\n\n" + shot_blocks
            + "\n\nBuild the block layout now.")
    layout = F.llm("forest_blockset_layout", LAYOUT_SYSTEM, user, LAYOUT_SCHEMA,
                   model="gpt")
    F.save_plan("blockset_layout", layout)
    print(f"blockset: {len(layout['elements'])} elements")
    for e in layout["elements"]:
        print(f"  [{e['id']}] {e['name_en']} ({e['kind']}, lv{e['level']},"
              f" {e['importance']})")
    return layout


# ── 결정론 렌더러 (PIL — AI 이미지 아님) ──

STYLE = {  # kind -> (fill, outline, outline_w)
    "road":          ("#cfcfcf", "#7a7a7a", 2),
    "alley":         ("#d8d4c8", "#7a7a7a", 2),
    "yard":          ("#e7e2d3", "#5a5a5a", 2),
    "neighbor_mass": ("#ececec", "#9a9a9a", 2),
    "building_mass": ("#d7d2c6", "#3f3f3f", 5),
    "terrace":       ("#e3ded0", "#4a4a4a", 3),
    "upper_shell":   ("#cbc3ae", "#33302a", 5),
    "wall":          ("#b9b2a1", "#6a655c", 2),
    "stair":         ("#f5f3ec", "#3f3f3f", 3),
    "gate":          ("#6b4f3a", "#3a2c20", 2),
    "door":          ("#7a3b2e", "#3a1c14", 2),
    "window":        ("#5b87a8", "#2e5670", 2),
    "fixture_round": ("#8fa9bd", "#43596b", 3),
    "fixture_rect":  ("#c9a86a", "#6e5732", 3),
    "equipment":     ("#a8a8a8", "#565656", 2),
}
OPENINGS = {"door", "window", "gate"}
LV_TINT = {0: 0, 1: -12, 2: -24}  # 레벨별 미묘한 명도 차 (구조 판독 보조)


def _hex(c):
    return tuple(int(c[i:i + 2], 16) for i in (1, 3, 5))


def _tint(c, d):
    return tuple(max(0, min(255, v + d)) for v in _hex(c))


def _rot(cx, cy, deg, pts):
    a = math.radians(deg)
    out = []
    for x, y in pts:
        dx, dy = x - cx, y - cy
        out.append((cx + dx * math.cos(a) - dy * math.sin(a),
                    cy + dx * math.sin(a) + dy * math.cos(a)))
    return out


def render(layout, out_path):
    from PIL import Image, ImageDraw, ImageFont
    cw = float(layout["canvas"]["width"]) or 100.0
    ch = float(layout["canvas"]["height"]) or 100.0
    S = 11  # canvas unit -> px
    M = 50  # margin
    LEG = 420  # 우측 범례 폭
    W, H = int(cw * S + M * 2 + LEG), int(ch * S + M * 2 + 40)
    img = Image.new("RGB", (W, H), "#f7f5f0")
    d = ImageDraw.Draw(img)

    def font(sz, bold=False):
        for p in ("/System/Library/Fonts/Helvetica.ttc",
                  "/System/Library/Fonts/Supplemental/Arial.ttf"):
            try:
                return ImageFont.truetype(p, sz, index=1 if bold else 0)
            except Exception:
                continue
        return ImageFont.load_default()

    f_title, f_lab, f_min, f_leg = font(26, True), font(15, True), font(12), font(14)

    def px(x, y):
        return M + x * S, M + 30 + y * S

    # 캔버스 테두리
    d.rectangle([px(0, 0), px(cw, ch)], outline="#8a857a", width=2)
    d.text((M, 12), "BLOCK SET LAYOUT — code-rendered, relative units"
           " (structure only)", font=f_title, fill="#333")

    els = list(layout["elements"])
    base = sorted([e for e in els if e["kind"] not in OPENINGS],
                  key=lambda e: (e["level"], -(e["w"] * e["h"])))
    tops = [e for e in els if e["kind"] in OPENINGS]

    def draw_el(e):
        fill, oc, ow = STYLE.get(e["kind"], ("#ddd", "#555", 2))
        fill = _tint(fill, LV_TINT.get(int(e["level"]), 0))
        x, y, w, h = e["x"], e["y"], e["w"], e["h"]
        if e["shape"] == "circle":
            cxp, cyp = px(x, y)
            r = w * S / 2
            d.ellipse([cxp - r, cyp - r, cxp + r, cyp + r], fill=fill,
                      outline=oc, width=ow)
            return
        cx, cy = x + w / 2, y + h / 2
        corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
        pts = [px(*p) for p in _rot(cx, cy, e.get("rot_deg", 0) or 0, corners)]
        d.polygon(pts, fill=fill, outline=oc)
        if ow > 1:  # PIL polygon outline 두께 보강
            d.line(pts + [pts[0]], fill=oc, width=ow, joint="curve")
        if e["kind"] == "stair":  # 발판(rung) 기호 — 장축 따라 등간격
            n = max(4, int(max(w, h)))
            along_x = w >= h
            for i in range(1, n):
                t = i / n
                if along_x:
                    p1, p2 = (x + w * t, y), (x + w * t, y + h)
                else:
                    p1, p2 = (x, y + h * t), (x + w, y + h * t)
                q1, q2 = _rot(cx, cy, e.get("rot_deg", 0) or 0, [p1, p2])
                d.line([px(*q1), px(*q2)], fill=oc, width=2)
            lv = e.get("connects_levels") or []
            if len(lv) >= 2:
                lo, hi = min(lv), max(lv)
                a = px(x + w / 2, y + h) if h >= w else px(x, y + h / 2)
                d.text((a[0] + 4, a[1] + 2), f"L{lo}→L{hi}", font=f_min,
                       fill="#3f3f3f")

    for e in base:
        draw_el(e)
    for e in tops:
        draw_el(e)

    # 라벨 (major 진하게, minor 작게) — 겹침 최소화: 중심에, 작은 요소는 우측에
    for e in els:
        if e["kind"] == "neighbor_mass" and e["importance"] == "minor":
            lab, f, col = e["name_en"].upper(), f_min, "#9a9a9a"
        elif e["importance"] == "major":
            lab, f, col = f"{e['id']} {e['name_en'].upper()}", f_lab, "#222"
        else:
            lab, f, col = f"{e['id']} {e['name_en']}", f_min, "#555"
        if e["shape"] == "circle":
            cxp, cyp = px(e["x"], e["y"])
            anchor_xy, anc = (cxp, cyp + e["w"] * S / 2 + 3), "ma"
        else:
            cx, cy = e["x"] + e["w"] / 2, e["y"] + e["h"] / 2
            small = (e["w"] * S < 60) or (e["h"] * S < 26)
            if small:
                p = px(e["x"] + e["w"], cy)
                anchor_xy, anc = (p[0] + 4, p[1]), "lm"
            else:
                anchor_xy, anc = px(cx, cy), "mm"
        bb = d.textbbox(anchor_xy, lab, font=f, anchor=anc)
        d.rectangle([bb[0] - 2, bb[1] - 1, bb[2] + 2, bb[3] + 1],
                    fill=(247, 245, 240, 200))
        d.text(anchor_xy, lab, font=f, fill=col, anchor=anc)

    # 우측 범례 — 사용된 kind 만
    lx = int(cw * S + M + 30)
    ly = M + 40
    d.text((lx, ly - 26), "LEGEND (kind)", font=f_lab, fill="#333")
    used = []
    for e in els:
        if e["kind"] not in used:
            used.append(e["kind"])
    for k in used:
        fill, oc, ow = STYLE[k]
        d.rectangle([lx, ly, lx + 26, ly + 18], fill=fill, outline=oc, width=2)
        d.text((lx + 34, ly + 2), k, font=f_leg, fill="#333")
        ly += 28
    ly += 10
    d.text((lx, ly), "L0 ground / L1 roof / L2 shell-top", font=f_min,
           fill="#666")
    d.text((lx, ly + 18), "labels: ID NAME (major=bold)", font=f_min,
           fill="#666")
    note = layout["canvas"].get("orientation_note") or ""
    if note:
        d.text((lx, ly + 44), ("orientation: " + note)[:60], font=f_min,
               fill="#666")

    out_path.parent.mkdir(parents=True, exist_ok=True)
    img.save(out_path)
    F.runlog({"kind": "code_render", "tag": "blockset", "out": str(out_path),
              "elements": len(els)})
    print("render ->", out_path)


def build_html(layout):
    png = OUTB / "blockset.png"
    rel = png.relative_to(F.EXP)
    rows = ""
    for e in layout["elements"]:
        ev = "<br>".join(
            f"<span class='src'>{_html.escape(v['source'])}</span> "
            f"{_html.escape(v['quote'])}" for v in e["evidence"])
        rows += (f"<tr><td>{e['id']}</td><td>{_html.escape(e['name_en'])}</td>"
                 f"<td>{e['kind']}</td><td>L{e['level']}"
                 f"{('→' + str(max(e['connects_levels']))) if len(e['connects_levels']) >= 2 else ''}</td>"
                 f"<td>{e['importance']}</td>"
                 f"<td>({e['x']:g},{e['y']:g}) {e['w']:g}×{e['h']:g}"
                 f"{(' rot' + str(e['rot_deg'])) if e.get('rot_deg') else ''}</td>"
                 f"<td class='ev'>{ev}</td></tr>")
    doc = f"""<meta charset='utf-8'><title>블록 세트장 — 코드 렌더 실험</title><style>
body{{font-family:sans-serif;background:#171717;color:#eee;margin:24px;max-width:1860px}}
h1{{font-size:22px}} img{{width:100%;max-width:1500px;border-radius:8px;background:#fff}}
.guide{{background:#1e2430;padding:12px 16px;border-radius:8px;font-size:13px;line-height:1.7;margin-bottom:14px}}
table{{border-collapse:collapse;font-size:12px;margin-top:16px;width:100%}}
td,th{{border:1px solid #333;padding:5px 8px;vertical-align:top}}
th{{background:#222;color:#fd9}} .src{{color:#8ac}} .ev{{color:#9c9;max-width:520px}}
pre{{white-space:pre-wrap;font-size:11px;color:#9c9;background:#1b1b1b;padding:8px;max-height:300px;overflow:auto}}
</style>
<h1>블록 세트장 실험 — LLM 구조화 추출 → 코드 렌더 (AI 이미지 아님)</h1>
<div class='guide'>LLM 이 <b>전 샷 원문+장소 데이터를 취합</b>해 요소·상대 크기·배치를
구조화 JSON 으로만 추출(evidence 필수) → <b>결정론 코드(PIL)</b>가 블록 도면을 그림.
네모/동그라미 블록, 구조적으로 중요한 문/창, 원형 설비, 계단(발판 기호+레벨 연결),
낮은 평상형 설비, 장비 유닛, 입구 주변 도로. 이웃 건물=민네모 간략화. 목적=구조 파악
(LLM 의 공간 모델이 데이터만으로 정합적인지 검증).</div>
<a href='{rel}' target='_blank'><img src='{rel}'></a>
<h3>요소 테이블 (LLM 산출 그대로)</h3>
<table><tr><th>ID</th><th>이름</th><th>kind</th><th>레벨</th><th>중요도</th>
<th>좌표/크기 (100×100 상대)</th><th>evidence (근거 인용)</th></tr>{rows}</table>
<details style='margin-top:12px'><summary>LLM layout notes</summary>
<pre>{_html.escape(layout.get('layout_notes', ''))}</pre></details>
<details><summary>canvas orientation</summary>
<pre>{_html.escape(json.dumps(layout.get('canvas', {}), ensure_ascii=False, indent=1))}</pre></details>
"""
    PAGE.write_text(doc)
    print("page ->", PAGE)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", default="all",
                    choices=["all", "plan", "render", "html"])
    args = ap.parse_args()
    recon = F.load_recon()
    if args.only in ("all", "plan"):
        layout = gen_layout(recon)
    else:
        layout = F.load_plan("blockset_layout")
    if args.only in ("all", "render"):
        render(layout, OUTB / "blockset.png")
    if args.only in ("all", "html"):
        build_html(layout)
    F.runlog({"kind": "stage", "stage": "s12_blockset", "done": args.only})


if __name__ == "__main__":
    main()
