# 제왕나비(Monarch butterfly, Danaus plexippus) — 날개를 펴고 앉은 수컷 한 마리, 실제 크기(날개폭 약 10cm).
# 날개는 "날개 기부에서 바깥 가장자리로 뻗는 방사형 좌표(t, r)"로 표현했다.
#   t = 가장자리 곡선(Catmull-Rom 스플라인)을 따라간 비율, r = 기부→가장자리 거리 비율.
#   검은 날개막(얇은 판) 위로 시맥(검은 줄) 사이의 주황 칸을 다각형 오프셋으로 깎아 내어 끼우고,
#   가장자리 검은 띠에는 흰 점 두 줄, 앞날개 끝에는 주황·흰 반점을 같은 좌표계로 배치했다.
# 몸통(머리·가슴·배)은 타원체와 회전체, 다리·더듬이·주둥이는 직접 만든 관(tube) 메시로 만들었다.
# 모든 형상은 밀리미터로 계산한 뒤 마지막에 미터 단위(×0.001)로 바꾼다.
from atelier import *

SHOT = {"azimuth": -72, "elevation": 64, "lens": 70, "light": "day", "background": "#24323a"}

MM = 0.001

# ── 재질 ────────────────────────────────────────────────
M_VEIN = mat('WingMembrane_BlackScales', '#1c1714', 0.62)
M_ORANGE = mat('WingScales_Orange', '#d06a08', 0.5)
M_ORANGE_UNDER = mat('WingScales_Orange_Underside', '#d67a2a', 0.6)
M_TAN_UNDER = mat('WingScales_Tan_Underside', '#d6aa62', 0.6)
M_WHITE = mat('Scales_WhiteSpots', '#ece4d2', 0.55)
M_APEX = mat('Scales_ApexSpots', '#efa24a', 0.55)
M_BODY = mat('Body_BlackCuticle', '#171311', 0.45)
M_EYE = mat('CompoundEye', '#2a2018', 0.25)
M_PROB = mat('Proboscis', '#3a2c20', 0.5)


# ── 2D 도우미 ───────────────────────────────────────────
def catmull(ctrl, per=16):
    pts = [ctrl[0]] + list(ctrl) + [ctrl[-1]]
    out = []
    for i in range(1, len(pts) - 2):
        p0, p1, p2, p3 = pts[i - 1], pts[i], pts[i + 1], pts[i + 2]
        for k in range(per):
            s = k / per
            s2, s3 = s * s, s * s * s
            out.append(tuple(0.5 * (2 * p1[j] + (-p0[j] + p2[j]) * s + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * s2
                                    + (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * s3) for j in (0, 1)))
    out.append(tuple(ctrl[-1]))
    return out


def resample(poly, n):
    d = [0.0]
    for a, b in zip(poly, poly[1:]):
        d.append(d[-1] + math.dist(a, b))
    total = d[-1]
    out, j = [], 0
    for i in range(n + 1):
        s = total * i / n
        while j < len(d) - 2 and d[j + 1] < s:
            j += 1
        f = (s - d[j]) / max(d[j + 1] - d[j], 1e-9)
        out.append((poly[j][0] + (poly[j + 1][0] - poly[j][0]) * f, poly[j][1] + (poly[j + 1][1] - poly[j][1]) * f))
    return out


def signed_area(p):
    return 0.5 * sum(p[i][0] * p[(i + 1) % len(p)][1] - p[(i + 1) % len(p)][0] * p[i][1] for i in range(len(p)))


def inset(poly, w):
    """반시계 다각형을 안쪽으로 w 만큼 오프셋(마이터)."""
    n = len(poly)
    out = []
    for i in range(n):
        a, b, c = poly[i - 1], poly[i], poly[(i + 1) % n]
        e1 = (b[0] - a[0], b[1] - a[1])
        e2 = (c[0] - b[0], c[1] - b[1])
        l1, l2 = math.hypot(*e1) or 1e-9, math.hypot(*e2) or 1e-9
        n1 = (-e1[1] / l1, e1[0] / l1)
        n2 = (-e2[1] / l2, e2[0] / l2)
        cc = max(n1[0] * n2[0] + n1[1] * n2[1], -0.6)
        k = w / (1 + cc)
        out.append((b[0] + (n1[0] + n2[0]) * k, b[1] + (n1[1] + n2[1]) * k))
    return out


def ellipse(c, ax, ay, ang, seg=12):
    ca, sa = math.cos(ang), math.sin(ang)
    return [(c[0] + ax * math.cos(2 * math.pi * i / seg) * ca - ay * math.sin(2 * math.pi * i / seg) * sa,
             c[1] + ax * math.cos(2 * math.pi * i / seg) * sa + ay * math.sin(2 * math.pi * i / seg) * ca) for i in range(seg)]


class WingShape:
    """기부(0,0)에서 본 날개 가장자리 곡선 M(t). u = 바깥쪽, v = 앞쪽(머리 방향)."""

    def __init__(self, ctrl, rot=0.0, n=480):
        a = math.radians(rot)  # 날개를 앞(+)·뒤(-)로 젖힌 각
        ctrl = [(x * math.cos(a) - y * math.sin(a), x * math.sin(a) + y * math.cos(a)) for x, y in ctrl]
        self.n = n
        self.m = resample(catmull(ctrl), n)

    def scallop(self, veins, depth=0.012, hw=0.022):
        """시맥 끝마다 가장자리가 살짝 오목하게 들어가는 물결 모양."""
        self.veins, self.depth, self.hw = veins, depth, hw

    def M(self, t):
        x = min(max(t, 0.0), 1.0) * self.n
        i = min(int(x), self.n - 1)
        f = x - i
        a, b = self.m[i], self.m[i + 1]
        k = 1.0
        for tv in getattr(self, 'veins', ()):
            k -= self.depth * bump(t, tv, self.hw)
        return ((a[0] + (b[0] - a[0]) * f) * k, (a[1] + (b[1] - a[1]) * f) * k)

    def P(self, t, r):
        m = self.M(t)
        return (m[0] * r, m[1] * r)

    def R(self, t):
        return math.hypot(*self.M(t))

    def tangent(self, t):
        a, b = self.M(t - 0.004), self.M(t + 0.004)
        return math.atan2(b[1] - a[1], b[0] - a[0])


# ── 메시 조립 ───────────────────────────────────────────
class MeshBuilder:
    def __init__(self):
        self.bm = bmesh.new()

    def slab(self, poly, h0, h1, frame, top_mi, bot_mi, side_mi):
        """2D 다각형을 날개 좌표계에서 두께 방향(h0~h1)으로 세운 판. 윗면·아랫면은 다각형 면."""
        if signed_area(poly) < 0:
            poly = list(reversed(poly))
        V = self.bm.verts.new
        top = [V(frame(p[0], p[1], h1)) for p in poly]
        bot = [V(frame(p[0], p[1], h0)) for p in poly]
        n = len(poly)
        caps = [self.bm.faces.new(top), self.bm.faces.new(list(reversed(bot)))]
        caps[0].material_index = top_mi
        caps[1].material_index = bot_mi
        for i in range(n):
            j = (i + 1) % n
            f = self.bm.faces.new((bot[i], bot[j], top[j], top[i]))
            f.material_index = side_mi
        # 오목한 칸도 겹치지 않게 귀 자르기(ear clip)로 삼각분할
        bmesh.ops.triangulate(self.bm, faces=caps, quad_method='BEAUTY', ngon_method='EAR_CLIP')

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


def wing_frame(side, base, dihedral, sweep):
    """날개 2D(u, v, 두께 h) → 몸 좌표(mm). side=+1 오른쪽, -1 왼쪽. 머리는 -Y."""
    d, s = math.radians(dihedral), math.radians(sweep)
    eu = Vector((side * math.cos(d) * math.cos(s), -math.cos(d) * math.sin(s), math.sin(d)))
    ev = Vector((side * math.sin(s), -math.cos(s), 0.0))
    ev = (ev - eu * ev.dot(eu)).normalized()
    en = eu.cross(ev).normalized()
    if en.z < 0:
        en = -en
    b = Vector(base)
    return lambda u, v, h: tuple(b + eu * u + ev * v + en * h)


def bump(t, c, hw):
    x = (t - c) / hw
    return 0.5 * (1 + math.cos(math.pi * x)) if abs(x) < 1 else 0.0


def build_wing(name, shape, veins, discal, border_fn, vein_w, frame, under_mat, spot_rows, apex=None, hide_front=0.0, scent=None):
    """
    veins: 시맥이 가장자리에 닿는 t 값 목록(0, …, 1)
    discal: (t0, t1, k) — 중실(discal cell) 범위와 바깥 경계 r
    border_fn(t): 가장자리 검은 띠 폭(mm)
    spot_rows: 흰 점 열 [(가장자리로부터 깊이 mm, 칸당 개수, 반지름)]
    """
    mb = MeshBuilder()
    # 0 검은 막, 1 주황(윗면), 2 아랫면 색, 3 흰 점, 4 앞날개 끝 주황 반점
    HM, HC, HS = 0.10, 0.24, 0.26
    # 날개막: 기부 둘레 부채꼴 + 가장자리 곡선
    outline = [(0.0, 0.0)] + [shape.M(i / 240) for i in range(241)]
    outline = [(p[0] * 1.012, p[1] * 1.012) for p in outline]  # 가장자리 테가 칸보다 살짝 크다
    mb.slab(outline, -HM, HM, frame, 0, 0, 0)

    t0d, t1d, kd = discal
    r_in = 0.13

    def cell(ta, tb, ra, outer=True):
        n = max(4, int((tb - ta) * 160))
        pts = []
        for i in range(n + 1):
            t = ta + (tb - ta) * i / n
            pts.append(shape.P(t, ra))
        for i in range(n, -1, -1):
            t = ta + (tb - ta) * i / n
            rb = 1 - border_fn(t) / shape.R(t) if outer else kd
            pts.append(shape.P(t, rb))
        if signed_area(pts) < 0:
            pts.reverse()
        poly = inset(pts, vein_w)
        if signed_area(poly) < 1.0:
            return
        mb.slab(poly, -HC, HC, frame, 1, 2, 1)

    for ta, tb in zip(veins, veins[1:]):
        if ta < hide_front:
            continue
        inside = t0d - 1e-6 <= ta and tb <= t1d + 1e-6
        cell(ta, tb, kd if inside else r_in + 0.05)
    cell(t0d, t1d, r_in, outer=False)  # 중실

    # 가장자리 흰 점
    for ta, tb in zip(veins, veins[1:]):
        if ta < hide_front:
            continue
        for depth, count, rad in spot_rows:
            for k in range(count):
                t = ta + (tb - ta) * (k + 1) / (count + 1)
                if border_fn(t) < depth + rad + 0.3:
                    continue
                r = 1 - depth / shape.R(t)
                c = shape.P(t, r)
                mb.slab(ellipse(c, rad * 1.25, rad, shape.tangent(t), 10), -HS, HS, frame, 3, 3, 3)
    # 앞날개 끝 반점(주황 한 줄 + 흰 한 줄)
    if apex:
        for ta, tb in zip(veins, veins[1:]):
            tm = (ta + tb) / 2
            if not (apex[0] <= tm <= apex[1]):
                continue
            bw = border_fn(tm)
            for depth_f, a, b, mi in ((0.78, 1.9, 1.1, 4), (0.47, 1.0, 0.75, 3)):
                c = shape.P(tm, 1 - bw * depth_f / shape.R(tm))
                mb.slab(ellipse(c, a, b, shape.tangent(tm), 12), -HS, HS, frame, mi, mi, mi)
    # 수컷 뒷날개 시맥 위의 검은 향기 비늘 반점
    if scent:
        ts, rs = scent
        c = shape.P(ts, rs)
        mb.slab(ellipse(c, 2.3, 1.35, math.atan2(*reversed(shape.M(ts))), 14), -HS - 0.02, HS + 0.02, frame, 0, 0, 0)
    return mb.finish(name, [M_VEIN, M_ORANGE, under_mat, M_WHITE, M_APEX])


# ── 날개 모양(mm, 기부 기준) ───────────────────────────────
FORE = WingShape([(1.2, 1.6), (6, 3.8), (14, 6.0), (25, 8.0), (36, 9.4), (46, 9.4), (52, 7.6), (54.6, 4.0),
                  (53.0, -1.2), (48.2, -7.6), (43.0, -13.6), (37.6, -19.4), (32.0, -23.6), (29.0, -24.4),
                  (22, -19.2), (14, -12.6), (7, -6.4), (2.0, -1.8)], rot=-3)
HIND = WingShape([(1.0, 1.8), (8, 4.2), (17, 4.0), (25, 1.2), (31.5, -4.5), (35, -11.5), (36, -19), (34.5, -26.5),
                  (30.5, -33), (24.5, -37.8), (17.5, -40), (11, -38.5), (6, -33), (2.8, -24), (1.0, -13), (0.4, -5)], rot=6)

FORE_VEINS = [0.0, 0.30, 0.355, 0.41, 0.465, 0.52, 0.575, 0.63, 0.69, 1.0]
HIND_VEINS = [0.0, 0.17, 0.26, 0.335, 0.41, 0.485, 0.56, 0.635, 0.715, 0.80, 1.0]
FORE.scallop(FORE_VEINS[2:-1])
HIND.scallop(HIND_VEINS[2:-1], depth=0.016)


def fore_border(t):
    b = 3.6 + 8.5 * bump(t, 0.375, 0.12)  # 앞날개 끝은 넓게 검다
    if t < 0.30:
        b = 2.2 + 1.2 * bump(t, 0.30, 0.06)  # 앞 가장자리(전연) 검은 띠
    if t > 0.69:
        b = 1.6 + 1.8 * bump(t, 0.69, 0.06)  # 뒤 가장자리
    return b


def hind_border(t):
    if t < 0.17:
        return 1.6
    if t > 0.80:
        return 1.5 + 1.9 * bump(t, 0.80, 0.07)
    return 3.6


wings = []
for side, tag in ((1, 'R'), (-1, 'L')):
    ff = wing_frame(side, (side * 1.3, -2.6, 1.7), 16, 0)
    hf = wing_frame(side, (side * 1.3, 0.1, 0.75), 16, 0)
    wings.append(build_wing(f'Forewing_{tag}', FORE, FORE_VEINS, (0.355, 0.63, 0.50), fore_border, 0.72, ff,
                            M_ORANGE_UNDER, [(1.05, 2, 0.42), (2.45, 1, 0.5)], apex=(0.30, 0.47)))
    wings.append(build_wing(f'Hindwing_{tag}', HIND, HIND_VEINS, (0.26, 0.56, 0.44), hind_border, 0.85, hf,
                            M_TAN_UNDER, [(1.05, 2, 0.4), (2.5, 2, 0.42)], scent=(HIND_VEINS[6], 0.6)))


# ── 관(tube) 메시: 다리·더듬이·주둥이 ───────────────────────
def tube(name, pts, radii, material, seg=10, smooth_path=True):
    if smooth_path:
        pts = catmull3(pts)
    n = len(pts)
    if not isinstance(radii, (list, tuple)):
        radii = [radii] * n
    elif len(radii) != n:
        radii = [radii[0] + (radii[-1] - radii[0]) * i / (n - 1) for i in range(n)]
    bm = bmesh.new()
    rings = []
    prev_x = None
    for i in range(n):
        p = Vector(pts[i])
        d = (Vector(pts[min(i + 1, n - 1)]) - Vector(pts[max(i - 1, 0)])).normalized()
        if prev_x is None:
            ref = Vector((0, 0, 1)) if abs(d.z) < 0.9 else Vector((1, 0, 0))
            x = d.cross(ref).normalized()
        else:
            x = (prev_x - d * prev_x.dot(d)).normalized()
        y = d.cross(x)
        prev_x = x
        rings.append([bm.verts.new(p + (x * math.cos(2 * math.pi * k / seg) + y * math.sin(2 * math.pi * k / seg)) * radii[i])
                      for k in range(seg)])
    for i in range(n - 1):
        for k in range(seg):
            bm.faces.new((rings[i][k], rings[i][(k + 1) % seg], rings[i + 1][(k + 1) % seg], rings[i + 1][k]))
    bm.faces.new(list(reversed(rings[0])))
    bm.faces.new(rings[-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)
    assign(ob, material)
    return smooth(ob)


def catmull3(ctrl, per=8):
    pts = [ctrl[0]] + list(ctrl) + [ctrl[-1]]
    out = []
    for i in range(1, len(pts) - 2):
        p0, p1, p2, p3 = pts[i - 1], pts[i], pts[i + 1], pts[i + 2]
        for k in range(per):
            s = k / per
            out.append(tuple(0.5 * (2 * p1[j] + (-p0[j] + p2[j]) * s + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * s * s
                                    + (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * s ** 3) for j in range(3)))
    out.append(tuple(ctrl[-1]))
    return out


# ── 몸통 ───────────────────────────────────────────────
thorax = sphere('Thorax', 1.0, loc=(0, -1.0, 0.2), material=M_BODY, seg=40, rings=24, scale=(2.55, 4.4, 2.5))
head = sphere('Head', 1.0, loc=(0, -6.6, 0.3), material=M_BODY, seg=32, rings=20, scale=(2.0, 1.8, 1.9))
eyes = [sphere(f'CompoundEye_{t}', 1.25, loc=(s * 1.35, -7.3, 0.55), material=M_EYE, seg=24, rings=14)
        for s, t in ((1, 'R'), (-1, 'L'))]

# 배: 마디가 드러나는 회전체, 꼬리 쪽이 약간 아래로 처진다
prof = []
L_AB = 19.0
for i in range(61):
    z = L_AB * i / 60
    x = z / L_AB
    r = 2.05 * math.sin(math.pi * min(1.0, 0.12 + 0.88 * x)) ** 0.55 * (1 - 0.25 * x)
    r *= 1 - 0.07 * abs(math.sin(math.pi * z / 2.35))
    prof.append((max(r, 0.0), z))
prof[0] = (0.0, 0.0)
prof[-1] = (0.0, L_AB)
abdomen = lathe('Abdomen', prof, loc=(0, 2.2, -0.2), rot=(math.radians(-98), 0, 0), material=M_BODY, seg=36)

# 가슴·머리의 흰 점(제왕나비 몸의 특징)
dot = sphere('BodyDot', 0.36, material=M_WHITE, seg=12, rings=8)
dot_pts = []
for sx in (1, -1):
    for (ex, ey, ez) in ((0.55, -0.55, 0.62), (0.75, 0.15, 0.55), (0.85, -0.25, -0.25), (0.5, 0.6, 0.6)):
        n = Vector((sx * ex, ey, ez)).normalized()
        dot_pts.append((sx * 2.55 * n.x, -1.0 + 4.4 * n.y, 0.2 + 2.5 * n.z))
    for (ex, ey, ez) in ((0.35, -0.55, 0.76), (0.82, -0.2, -0.45)):
        n = Vector((sx * ex, ey, ez)).normalized()
        dot_pts.append((sx * 2.0 * n.x, -6.6 + 1.8 * n.y, 0.3 + 1.9 * n.z))
body_dots = [dot] + [duplicate(dot) for _ in dot_pts[1:]]
for ob, p in zip(body_dots, dot_pts):
    ob.location = p
    ob.name = 'BodyWhiteSpot'

# 더듬이: 길고 끝이 곤봉 모양
antennae = []
for s, t in ((1, 'R'), (-1, 'L')):
    pts = [(s * 0.6, -7.6, 1.9), (s * 1.4, -10.5, 3.6), (s * 2.6, -14.5, 5.2), (s * 3.9, -18.5, 6.2), (s * 5.0, -21.5, 6.6)]
    antennae.append(tube(f'Antenna_{t}', pts, [0.22, 0.19], M_BODY, seg=8))
    antennae.append(sphere(f'AntennaClub_{t}', 0.5, loc=(s * 5.1, -21.9, 6.6), material=M_BODY, seg=12, rings=8,
                           scale=(1, 2.4, 1), rot=(0, 0, math.radians(-s * 14))))

# 말린 주둥이(proboscis)
coil = []
for i in range(70):
    a = 2.6 * math.pi * i / 69
    rr = 1.05 * (1 - 0.6 * i / 69)
    coil.append((0.0, -7.9 + 0.4 - rr * math.sin(a) * 0.9 - 0.4, -1.6 - rr * (1 - math.cos(a)) * 0.9 + 0.2))
proboscis = tube('Proboscis', coil, [0.2, 0.12], M_PROB, seg=8, smooth_path=False)
palps = [tube(f'LabialPalp_{t}', [(s * 0.5, -7.6, -0.9), (s * 0.6, -8.6, -0.6), (s * 0.55, -9.4, 0.2)], [0.35, 0.2],
              M_BODY, seg=8) for s, t in ((1, 'R'), (-1, 'L'))]

# 다리: 앞다리는 작게 접혀 있고, 가운데·뒷다리 네 개로 선다(제왕나비과의 특징)
GROUND = -8.0
legs = []
for s, t in ((1, 'R'), (-1, 'L')):
    legs.append(tube(f'Foreleg_Folded_{t}', [(s * 0.9, -3.6, -1.6), (s * 1.6, -5.2, -2.6), (s * 1.3, -6.6, -2.0)],
                     [0.32, 0.2], M_BODY, seg=8))
    legs.append(tube(f'Midleg_{t}', [(s * 0.9, -1.6, -1.8), (s * 3.8, -2.6, -2.2), (s * 6.4, -3.6, -0.6), (s * 8.0, -5.5, -4.5),
                                     (s * 9.0, -7.0, GROUND + 0.12)], [0.36, 0.13], M_BODY, seg=8))
    legs.append(tube(f'Hindleg_{t}', [(s * 0.9, 0.6, -1.7), (s * 3.8, 1.6, -2.0), (s * 6.2, 3.4, -0.4), (s * 7.6, 6.0, -4.4),
                                      (s * 8.4, 8.0, GROUND + 0.12)], [0.36, 0.13], M_BODY, seg=8))


# ── 미터 단위로 변환 ─────────────────────────────────────
done = set()
for ob in bpy.context.scene.objects:
    if ob.type == 'MESH':
        if ob.data.name not in done:
            ob.data.transform(Matrix.Scale(MM, 4))
            done.add(ob.data.name)
        ob.location = ob.location * MM
