# 콜로세움 (로마, 서기 80년 완공) — 코드로만 만든 폐허 모델
# 평면은 장축 188m·단축 156m 타원. 바깥 타원의 호 길이를 따라 80개 베이를 고르게 나누고,
# (호 길이 s, 법선 방향 오프셋 w, 높이 z) 좌표계에서 만든 육면체 셀을 타원 위로 휘어 붙여
# 외벽·제2환·제3환 아케이드를 bmesh 로 직접 쌓았다. 외벽은 토스카나·이오니아·코린트 반원주가 붙은
# 3층 아치와 창이 난 4층 아틱으로 이루어지고, 북쪽 32칸만 온전히 남기고 남쪽은 무너뜨려
# 안쪽 고리가 드러나게 했다. 무너진 양 끝은 계단식 단면과 벽돌 버팀벽(스턴 버팀벽)으로 처리했다.
# 관람석(카베아)은 단면 프로필을 타원 둘레로 쓸어(sweep) 만들고, 위쪽은 방사형 벽돌 벽만 남은
# 폐허로, 경기장 바닥은 지하 통로(히포게움)가 드러난 채 동쪽 일부만 나무 바닥을 복원한 모습으로 표현했다.
# 기둥·아틱 기둥·까치발은 같은 메시를 공유하는 링크 복제로 배치했다. random 은 시드를 고정했다.
import bisect
import random

from atelier import *

random.seed(80)

SHOT = {"azimuth": 12, "elevation": 24, "lens": 45, "light": "day", "background": "#27303d"}

# ── 재질 ──────────────────────────────────────────────
M_TRAV = [mat('Travertine', '#cfb995', 0.85), mat('Travertine_Warm', '#c3ab88', 0.88),
          mat('Travertine_Weathered', '#ad9c80', 0.9)]
M_TUFA = mat('Tufa_InnerRing', '#a8977a', 0.9)
M_BRICK = mat('Roman_Brick', '#8c6250', 0.92)
M_BRICK_DK = mat('Brick_Rubble', '#6e5446', 0.95)
M_SEAT = mat('Marble_Seats', '#bdb19c', 0.8)
M_WOOD = mat('Arena_Floor_Wood', '#7a5a3c', 0.85)
M_EARTH = mat('Hypogeum_Earth', '#7c6c58', 0.95)
M_PAVE = mat('Paving_Stone', '#7d766a', 0.9)

# ── 타원과 호 길이 좌표계 ──────────────────────────────
A, B = 94.0, 78.0          # 바깥 외벽 면의 반장축·반단축
NB = 80                    # 아치 수
_N = 8000
_TS = [2 * math.pi * k / _N for k in range(_N + 1)]
_CUM = [0.0]
for k in range(_N):
    t0, t1 = _TS[k], _TS[k + 1]
    _CUM.append(_CUM[-1] + math.hypot(A * (math.cos(t1) - math.cos(t0)), B * (math.sin(t1) - math.sin(t0))))
PER = _CUM[-1]
L = PER / NB               # 베이 폭(바깥 면 기준, 약 6.8m)
_FR = {}


def frame(s):
    key = round(s % PER, 5)
    f = _FR.get(key)
    if f:
        return f
    j = min(max(bisect.bisect_right(_CUM, key) - 1, 0), _N - 1)
    t = _TS[j] + (key - _CUM[j]) / (_CUM[j + 1] - _CUM[j]) * (_TS[j + 1] - _TS[j])
    tx, ty = -A * math.sin(t), B * math.cos(t)
    ln = math.hypot(tx, ty)
    tx, ty = tx / ln, ty / ln
    f = (A * math.cos(t), B * math.sin(t), ty, -tx, tx, ty)   # 위치, 바깥 법선, 접선
    _FR[key] = f
    return f


def F(s, w, z):
    px, py, nx, ny, _, _ = frame(s)
    return (px + nx * w, py + ny * w, z)


class Mesher:
    """(s, z) 사각형 × w 범위 육면체 셀을 타원 위로 휘어 하나의 메시에 모은다."""

    def __init__(self, name, mats):
        self.name, self.mats, self.bm = name, mats, bmesh.new()

    def hexa(self, quad, w0, w1, m=0):
        bm = self.bm
        v = [bm.verts.new(F(s, w0, z)) for s, z in quad] + [bm.verts.new(F(s, w1, z)) for s, z in quad]
        for f in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)):
            bm.faces.new([v[i] for i in f]).material_index = m

    def box(self, s0, s1, z0, z1, w0, w1, m=0, n=1):
        for k in range(n):
            a, b = s0 + (s1 - s0) * k / n, s0 + (s1 - s0) * (k + 1) / n
            self.hexa([(a, z0), (b, z0), (b, z1), (a, z1)], w0, w1, m)

    def spandrel(self, sc, hw, zs, ztop, w0, w1, m=0, n=8):
        """반원 아치 위 벽면(아치 안쪽 면 포함)."""
        for j in range(n):
            a0, a1 = math.pi * (1 - j / n), math.pi * (1 - (j + 1) / n)
            p0 = (sc + hw * math.cos(a0), zs + hw * math.sin(a0))
            p1 = (sc + hw * math.cos(a1), zs + hw * math.sin(a1))
            self.hexa([p0, p1, (p1[0], ztop), (p0[0], ztop)], w0, w1, m)

    def finish(self):
        bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces)
        me = bpy.data.meshes.new(self.name)
        self.bm.to_mesh(me)
        self.bm.free()
        ob = bpy.data.objects.new(self.name, me)
        link(ob)
        for m in self.mats:
            me.materials.append(m)
        return ob


def sweep(name, prof, material, s0=0.0, s1=None, nseg=400):
    """(w, z) 단면 프로필을 타원 둘레로 쓸어 닫힌 고리 솔리드를 만든다."""
    closed = s1 is None
    s1 = PER if closed else s1
    bm = bmesh.new()
    rows = []
    for j in range(nseg if closed else nseg + 1):
        s = s0 + (s1 - s0) * j / nseg
        rows.append([bm.verts.new(F(s, w, z)) for w, z in prof])
    n = len(prof)
    for j in range(len(rows) - (0 if closed else 1)):
        r0, r1 = rows[j], rows[(j + 1) % len(rows)]
        for k in range(n):
            bm.faces.new((r0[k], r0[(k + 1) % n], r1[(k + 1) % n], r1[k]))
    if not closed:
        bm.faces.new(rows[0])
        bm.faces.new(rows[-1][::-1])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return assign(ob, material)


# ── 층 높이(절대 z, 기단 위 0.7m 부터) ─────────────────
Z0 = 0.7
LV = [Z0, 11.2, 23.1, 34.7, 49.2]
T_OUT = 2.4                       # 외벽 두께 (w: -2.4 ~ 0)
R2 = (-10.2, -8.0)                # 제2환
R3 = (-16.0, -14.0)               # 제3환
HW = 2.1                          # 외벽 아치 반폭

INTACT = range(5, 37)             # 4층이 온전히 남은 외벽 베이
STEP_W = {37: 3, 38: 2, 39: 1}    # 서쪽 끝 계단식 붕괴
NORTH_R2 = set(range(3, 41))      # 제2환 3층까지 온전한 구간


def outer_storeys(i):
    if i in INTACT:
        return 4
    return STEP_W.get(i, 0)


# ── 외벽 ─────────────────────────────────────────────
outer = Mesher('Outer_Wall_Arcade', M_TRAV)
pier_sets = [set(), set(), set(), set()]
for i in range(NB):
    ns = outer_storeys(i)
    sc = i * L
    s0, s1 = sc - L / 2, sc + L / 2
    W0 = -T_OUT
    for k in range(ns):
        m = random.choice((0, 0, 1, 2)) if k < 3 else random.choice((1, 2, 2))
        zb, zt = LV[k], LV[k + 1]
        pier_sets[k].update((round(s0, 4), round(s1, 4)))
        if k == 0:
            pb, ent, zs = zb, 9.9, 6.0
        elif k < 3:
            outer.box(s0, s1, zb, zb + 1.4, W0, 0.8, m, 3)          # 기둥 받침띠
            pb, ent, zs = zb + 1.4, (zt - 1.5 if k == 1 else zt - 1.4), (18.6 if k == 1 else 30.2)
        if k < 3:
            outer.box(s0, sc - HW, pb, ent, W0, 0, m)
            outer.box(sc + HW, s1, pb, ent, W0, 0, m)
            outer.spandrel(sc, HW, zs, ent, W0, 0, m)
            outer.box(s0, sc - HW, zs - 0.4, zs, 0.0, 0.2, m)          # 아치 임포스트
            outer.box(sc + HW, s1, zs - 0.4, zs, 0.0, 0.2, m)
            outer.box(s0, s1, ent, zt - 0.45, W0, 0.85, m, 3)       # 아키트레이브·프리즈
            outer.box(s0, s1, zt - 0.45, zt, W0, 1.2, m, 3)         # 코니스
        else:
            outer.box(s0, s1, zb, zb + 1.6, W0, 0.45, m, 3)
            top = 47.9
            if i % 2 == 0:                                          # 아틱의 작은 사각 창
                outer.box(s0, sc - 1.0, zb + 1.6, top, W0, 0, m, 2)
                outer.box(sc + 1.0, s1, zb + 1.6, top, W0, 0, m, 2)
                outer.box(sc - 1.0, sc + 1.0, zb + 1.6, 38.6, W0, 0, m)
                outer.box(sc - 1.0, sc + 1.0, 41.2, top, W0, 0, m)
            else:
                outer.box(s0, s1, zb + 1.6, top, W0, 0, m, 3)
            outer.box(s0, s1, top, 48.5, W0, 0.35, m, 3)
            outer.box(s0, s1, 48.5, 49.2, W0, 0.8, m, 3)
    if ns == 0 and not (2 <= i <= 4):
        # 남쪽: 외벽은 무너지고 기초선과 교각 그루터기만 남았다
        outer.box(s0, s1, Z0, Z0 + 0.45, W0 + 0.1, -0.1, 2, 3)
        if random.random() < 0.6:
            h = random.uniform(0.8, 4.5)
            outer.box(s1 - 1.25, s1 + 1.25, Z0, Z0 + h, W0, 0, random.choice((0, 1, 2)))
outer = outer.finish()

# 동쪽 끝: 스턴 버팀벽(벽돌 쐐기)
stern = Mesher('Buttresses_Brick', [M_BRICK])
sa, sb = 2.0 * L, 4.5 * L
NS = 9
zp = Z0
for k in range(NS):                 # 계단처럼 층층이 쌓아 올린 벽돌 덩어리
    a = sa + (sb - sa) * k / NS
    zt = Z0 + (49.2 - Z0) * ((k + 1) / NS) ** 1.2
    stern.box(a, sb, zp, zt, -3.0, 0.9 - 0.06 * k, 0, 2)
    zp = zt
# 서쪽 끝: 계단식 단면을 감싼 벽돌 보강(발라디에 버팀벽)
for k, (zb, zt) in enumerate(((LV[3], LV[4]), (LV[2], LV[3]), (LV[1], LV[2]), (Z0, LV[1]))):
    sb_ = (36.5 + k) * L
    stern.box(sb_, sb_ + 1.4, zb, zt, -2.7, 0.95, 0, 2)
stern = stern.finish()
stern.name = 'Buttresses_Brick'

# ── 반원주(토스카나·이오니아·코린트)와 아틱 기둥 ───────
def column_mesh(name, order, h):
    parts = []
    parts.append(cylinder(name + '_base', r=0.78, h=0.35, seg=20))
    shaft = lathe(name + '_shaft', [(0.0, 0.35), (0.62, 0.35), (0.6, h * 0.5), (0.54, h - 0.9), (0.0, h - 0.9)], seg=20)
    parts.append(shaft)
    if order == 'doric':
        parts.append(cylinder(name + '_echinus', r=0.56, r2=0.74, h=0.6, loc=(0, 0, h - 0.9), seg=20))
        parts.append(box(name + '_abacus', (1.6, 1.6, 0.3), loc=(0, 0, h - 0.3), base=True))
    elif order == 'ionic':
        parts.append(cylinder(name + '_neck', r=0.56, h=0.65, loc=(0, 0, h - 0.9), seg=20))
        for sy in (-1, 1):
            parts.append(cylinder(name + '_volute', r=0.27, h=0.9, loc=(0.0, sy * 0.62, h - 0.5),
                                  rot=(0, math.pi / 2, 0), base=False, seg=14))
        parts.append(box(name + '_abacus', (1.4, 1.6, 0.25), loc=(0, 0, h - 0.25), base=True))
    else:
        parts.append(cylinder(name + '_bell', r=0.55, r2=0.8, h=0.7, loc=(0, 0, h - 0.9), seg=20))
        parts.append(box(name + '_abacus', (1.6, 1.7, 0.2), loc=(0, 0, h - 0.2), base=True))
    ob = join(parts, name)
    return assign(ob, M_TRAV[0])


ORDERS = [('Column_Tuscan', 'doric', LV[0], 9.9),
          ('Column_Ionic', 'ionic', LV[1] + 1.4, LV[2] - 1.5),
          ('Column_Corinthian', 'corinthian', LV[2] + 1.4, LV[3] - 1.4)]
for k, (nm, order, zb, zt) in enumerate(ORDERS):
    proto = column_mesh(nm, order, zt - zb)
    first = True
    for s in sorted(pier_sets[k]):
        x, y, _ = F(s, 0, 0)
        nx, ny = frame(s)[2:4]
        ob = proto if first else duplicate(proto, name=nm)
        ob.location = (x, y, zb)
        ob.rotation_euler = (0, 0, math.atan2(ny, nx))
        first = False

# 아틱: 코린트식 벽기둥과 벨라리움 돛대를 받치던 까치발
pil = join([box('Attic_Pilaster', (0.5, 1.3, 11.6), loc=(0, 0, 0), base=True),
            box('Attic_Pilaster_cap', (0.7, 1.55, 0.5), loc=(0, 0, 11.1), base=True)], 'Attic_Pilaster')
assign(pil, M_TRAV[1])
corbel = box('Attic_Corbel', (0.8, 0.6, 0.7), loc=(0.35, 0, 0), base=True, material=M_TRAV[2])
first_p, first_c = True, True
for s in sorted(pier_sets[3]):
    x, y, _ = F(s, 0, 0)
    nx, ny = frame(s)[2:4]
    ob = pil if first_p else duplicate(pil, name='Attic_Pilaster')
    ob.location, ob.rotation_euler = (x, y, LV[3] + 1.6), (0, 0, math.atan2(ny, nx))
    first_p = False
for i in INTACT:
    for d in (-1.95, 0.0, 1.95):
        s = i * L + d
        x, y, _ = F(s, 0, 0)
        nx, ny = frame(s)[2:4]
        ob = corbel if first_c else duplicate(corbel, name='Attic_Corbel')
        ob.location, ob.rotation_euler = (x, y, 44.6), (0, 0, math.atan2(ny, nx))
        first_c = False

# ── 제2환·제3환 아케이드 ─────────────────────────────
ring2 = Mesher('Inner_Ring2_Arcade', [M_TUFA, M_TRAV[2]])
ring3 = Mesher('Inner_Ring3_Arcade', [M_TUFA, M_BRICK])
pier_h = {}
for i in range(NB):
    sc = i * L
    s0, s1 = sc - L / 2, sc + L / 2
    w0, w1 = R2
    hw = 2.0
    for k, zs in ((0, 6.0), (1, 18.0)):
        zb, zt = LV[k], LV[k + 1]
        ring2.box(s0, sc - hw, zb, zt - 0.6, w0, w1)
        ring2.box(sc + hw, s1, zb, zt - 0.6, w0, w1)
        ring2.spandrel(sc, hw, zs, zt - 0.6, w0, w1)
        ring2.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 1, 2)
    zb, zt = LV[2], LV[3]
    if i in NORTH_R2 or random.random() < 0.3:
        ring2.box(s0, sc - hw, zb, zt - 0.6, w0, w1)
        ring2.box(sc + hw, s1, zb, zt - 0.6, w0, w1)
        ring2.spandrel(sc, hw, 29.5, zt - 0.6, w0, w1)
        ring2.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 1, 2)
        if i in NORTH_R2:
            ring2.box(s0, s1, zt, 36.0, w0, w1, 0, 2)
        full3 = True
    else:
        for ps, a, b in ((round(s0, 3), s0, sc - hw), (round(s1, 3), sc + hw, s1)):
            h = pier_h.setdefault(ps, random.uniform(24.0, 31.0))
            ring2.box(a, b, zb, h, w0, w1)
        full3 = False
    # 제3환: 2층 아치 + 관람석 회랑 벽
    w0, w1 = R3
    for k, zs in ((0, 5.6), (1, 17.2)):
        zb, zt = LV[k], LV[k + 1]
        ring3.box(s0, sc - 1.8, zb, zt - 0.6, w0, w1)
        ring3.box(sc + 1.8, s1, zb, zt - 0.6, w0, w1)
        ring3.spandrel(sc, 1.8, zs, zt - 0.6, w0, w1)
        ring3.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 0, 2)
    ring3.box(s0, s1, LV[2], 30.5, w0, w1, 1, 2)
    # 회랑 천장(볼트) 슬래브
    ns = outer_storeys(i)
    for k in range(1, 4):
        if k <= ns and (k < 3 or i in NORTH_R2):
            ring2.box(s0, s1, LV[k] - 1.1, LV[k], -7.75, -T_OUT, 0, 2)
    for z in (LV[1], LV[2]):
        ring2.box(s0, s1, z - 1.1, z, -13.75, R2[0], 0, 2)
    if full3:
        ring2.box(s0, s1, 29.4, 30.5, -13.75, R2[0], 0, 2)
ring2 = ring2.finish()
ring3 = ring3.finish()

# ── 관람석(카베아) ────────────────────────────────────
prof = [(-51.0, Z0), (-51.0, 4.0), (-48.5, 4.0)]
z, w = 4.0, -48.5
for r in range(12):                 # 아래층 대리석 좌석 단
    z += 7.0 / 12
    prof.append((w, z))
    w += 8.5 / 12
    prof.append((w, z))
prof[-1] = (-40.0, prof[-1][1])
prof += [(-40.0, 8.5), (-28.0, 17.0), (-28.0, 18.5), (-18.0, 26.0), (-18.0, 30.5), (-16.0, 30.5), (-16.0, Z0)]
cavea = sweep('Cavea_Seating', prof, M_SEAT, nseg=480)
# 위층: 좌석이 사라지고 남은 방사형·동심원 벽돌 벽
sweep('Cavea_Balteus_Wall_1', [(-40.0, 8.2), (-38.8, 8.2), (-38.8, 12.5), (-40.0, 12.5)], M_BRICK, nseg=400)
sweep('Cavea_Balteus_Wall_2', [(-28.6, 16.4), (-27.6, 16.4), (-27.6, 22.5), (-28.6, 22.5)], M_BRICK, nseg=400)
radial = Mesher('Cavea_Radial_Walls', [M_BRICK, M_BRICK_DK])


def seat_z(w):
    if w <= -28.0:
        return 12.5 + (w + 40.0) / 12.0 * 8.5
    return 22.5 + (w + 28.0) / 10.0 * 7.5


def floor_z(w):
    if w <= -28.0:
        return 8.5 + (w + 40.0) / 12.0 * 8.5
    return 18.5 + (w + 28.0) / 10.0 * 7.5


bmr = radial.bm
for i in range(NB):
    s = (i + 0.5) * L
    px, py, nx, ny, tx, ty = frame(s)
    loss = random.choice((0, 0, 0, 1.5, 3.0))       # 일부 벽은 윗부분이 더 깎여 나갔다
    for wa, wb in ((-38.8, -28.6), (-27.6, -18.0)):
        pts = [(wa, floor_z(wa) - 0.3), (wb, floor_z(wb) - 0.3),
               (wb, max(seat_z(wb) - loss, floor_z(wb) + 0.5)), (wa, max(seat_z(wa) - loss, floor_z(wa) + 0.5))]
        vs = []
        for side in (-0.6, 0.6):
            for ww, zz in pts:
                vs.append(bmr.verts.new((px + nx * ww + tx * side, py + ny * ww + ty * side, zz)))
        for f in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)):
            bmr.faces.new([vs[k] for k in f]).material_index = 1 if loss else 0
radial.finish()

# ── 기단과 경기장 ─────────────────────────────────────
sweep('Site_Base', [(-51.0, -3.2), (12.0, -3.2), (9.0, 0.0), (3.0, 0.0), (3.0, 0.35), (1.5, 0.35),
                    (1.5, Z0), (-51.0, Z0)], M_PAVE, nseg=400)
arena_pts = [F(PER * k / 240, -51.0, 0)[:2] for k in range(240)]
prism('Arena_Pit_Floor', arena_pts, 0.4, loc=(0, 0, -2.8), material=M_EARTH)

# 히포게움: 장축 방향으로 달리는 지하 통로 벽과 둘레 통로
hyp = Mesher('Hypogeum_Walls', [M_BRICK_DK])
sweep('Hypogeum_Ring_Wall', [(-54.5, -2.4), (-53.6, -2.4), (-53.6, 0.4), (-54.5, 0.4)], M_BRICK_DK, nseg=300)
hb = hyp.bm
for y in (-1.6, 1.6, -5.0, 5.0, -8.4, 8.4, -11.8, 11.8, -15.2, 15.2, -18.6, 18.6):
    xe = 38.0 * math.sqrt(max(0.0, 1 - (y / 22.5) ** 2)) - 0.6
    x = -xe
    while x < xe - 1.5:
        x1 = min(x + random.uniform(4.5, 7.5), xe)
        bmesh.ops.create_cube(hb, size=1.0, matrix=Matrix.Translation(((x + x1) / 2, y, -1.0)) @
                              Matrix.Diagonal((x1 - x, 0.8, 2.8, 1.0)))
        x = x1 + 1.4
for x in range(-30, 31, 10):          # 가로 칸막이 벽
    ye = min(18.6, 22.5 * math.sqrt(1 - (x / 38.0) ** 2) - 1.0)
    bmesh.ops.create_cube(hb, size=1.0, matrix=Matrix.Translation((x, 0, -1.0)) @
                          Matrix.Diagonal((0.8, 2 * ye, 2.8, 1.0)))
hyp.finish()


def clip_x(poly, x0):
    out = []
    for k in range(len(poly)):
        p, q = poly[k], poly[(k + 1) % len(poly)]
        if p[0] >= x0:
            out.append(p)
        if (p[0] >= x0) != (q[0] >= x0):
            t = (x0 - p[0]) / (q[0] - p[0])
            out.append((x0, p[1] + t * (q[1] - p[1])))
    return out


prism('Arena_Floor_Reconstruction', clip_x(arena_pts, 14.0), 0.3, loc=(0, 0, 0.4), material=M_WOOD)
