# 사와로 선인장 (Carnegiea gigantea) — 소노라 사막의 거대 기둥선인장.
# 줄기와 팔은 3D 경로를 따라 '주름진 관'을 bmesh 로 직접 짜서 만들었다:
#   경로 위 각 지점에서 평행이동 프레임을 구하고, 단면 반지름을 |cos|^p 함수로 깎아 세로 능선(rib)을 낸 뒤
#   끝은 반타원 돔으로 닫는다. 팔은 줄기 속에서 출발해 수평으로 나왔다가 사분원으로 꺾여 위로 자란다.
# 능선 마루를 따라 일정 간격으로 가시자리(펠트)와 가시 다발을 수학적으로 배치해 한 메시로 묶고,
# 줄기·팔 꼭대기에는 회전체로 만든 흰 꽃과 꽃봉오리·붉은 열매를 얹었다. 밑동은 코르크화된 회갈색 재질로 칠했다.
import random
from atelier import *

random.seed(1907)

SHOT = {"azimuth": -96, "elevation": 9, "lens": 50, "light": "day", "background": "#2e3644"}

# ---------------------------------------------------------------- 재질
M_SKIN = mat('SaguaroSkin', '#66795c', rough=0.62)
M_BARK = mat('CorkyBark', '#7c7366', rough=0.9)
M_SPINE = mat('Spine', '#b9ab90', rough=0.55)
M_FELT = mat('AreoleFelt', '#5c4c3c', rough=0.95)
M_PETAL = mat('FlowerPetal', '#efe8d6', rough=0.5)
M_STAMEN = mat('Stamen', '#e2b437', rough=0.55)
M_BUD = mat('FlowerTube', '#7f9150', rough=0.6)
M_FRUIT = mat('Fruit', '#a63c35', rough=0.45)
M_SOIL = mat('DesertSand', '#bb9c7a', rough=0.95)
M_ROCK = mat('Rock', '#87695a', rough=0.85)


# ---------------------------------------------------------------- 도우미
def frames(pts):
    """경로 점 → (점, 접선, 법선, 종법선) — 평행이동(비틀림 없는) 프레임."""
    n = len(pts)
    T = []
    for i in range(n):
        a = pts[max(i - 1, 0)]
        b = pts[min(i + 1, n - 1)]
        T.append((b - a).normalized())
    up = Vector((0, 0, 1)) if abs(T[0].z) < 0.9 else Vector((1, 0, 0))
    N0 = (up - T[0] * up.dot(T[0])).normalized()
    out = []
    N = N0
    for i in range(n):
        if i > 0:
            q = T[i - 1].rotation_difference(T[i])
            N = q @ N
            N = (N - T[i] * N.dot(T[i])).normalized()
        B = T[i].cross(N)
        out.append((pts[i], T[i], N, B))
    return out


def rib_profile(theta, nribs, depth):
    """능선 마루=1, 골=1-depth. 마루는 둥글고 골은 V 자."""
    ph = theta * nribs / (2 * math.pi)
    f = abs(math.cos(math.pi * ph)) ** 0.75
    return 1.0 - depth * (1.0 - f)


def ribbed_tube(name, path, radius_fn, nribs, depth=0.16, vpr=8, dome=0.85, phase=0.0,
                spine_step=0.07, spine_len=(0.025, 0.05)):
    """path: Vector 목록(대략 균일 간격). radius_fn(s) — s=경로 길이(m).
    반환: (오브젝트, 가시자리 목록[(위치, 바깥방향, 가시길이배율)])"""
    fr = frames(path)
    seglen = [0.0]
    for i in range(1, len(path)):
        seglen.append(seglen[-1] + (path[i] - path[i - 1]).length)
    L = seglen[-1]
    rings = []  # (center, N, B, r, s, isdome)
    for (p, t, n, b), s in zip(fr, seglen):
        rings.append((p, n, b, radius_fn(s), s, 0.0))
    # 끝 돔: 마지막 접선 방향으로 반타원
    p_end, t_end, n_end, b_end = fr[-1]
    R_end = rings[-1][3]
    steps = 12
    for k in range(1, steps + 1):
        phi = (math.pi / 2) * k / steps
        r = R_end * math.cos(phi)
        p = p_end + t_end * (R_end * dome * math.sin(phi))
        rings.append((p, n_end, b_end, max(r, 0.0), L + R_end * dome * math.sin(phi), k / steps))

    bm = bmesh.new()
    m = nribs * vpr
    grid = []
    for (c, n, b, r, s, dm) in rings:
        ring = []
        for j in range(m):
            th = 2 * math.pi * j / m + phase
            rr = r * rib_profile(th, nribs, depth)
            ring.append(bm.verts.new(c + (n * math.cos(th) + b * math.sin(th)) * rr))
        grid.append(ring)
    for i in range(len(grid) - 1):
        for j in range(m):
            jn = (j + 1) % m
            if i == len(grid) - 2:
                pass
            bm.faces.new((grid[i][j], grid[i][jn], grid[i + 1][jn], grid[i + 1][j]))
    bm.faces.new(list(reversed(grid[0])))
    bmesh.ops.remove_doubles(bm, verts=grid[-1], dist=1e-6)
    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)
    smooth(ob)

    # 가시자리: 능선 마루를 따라
    areoles = []
    acc = -1.0
    for idx, (c, n, b, r, s, dm) in enumerate(rings):
        if r < 0.02:
            continue
        if s - acc < spine_step * (0.55 if dm > 0 else 1.0):
            continue
        acc = s
        for k in range(nribs):
            th = 2 * math.pi * k / nribs + phase
            d = (n * math.cos(th) + b * math.sin(th))
            pos = c + d * r
            if dm > 0:  # 돔 위에서는 바깥방향을 접선 쪽으로 기울인다
                t_dir = (rings[min(idx + 1, len(rings) - 1)][0] - c)
                if t_dir.length > 1e-6:
                    d = (d * math.cos(dm * math.pi / 2) + t_dir.normalized() * math.sin(dm * math.pi / 2)).normalized()
            areoles.append((pos, d, 1.0 + 0.8 * dm))
    return ob, areoles


def build_spines(name, areoles, base_len=0.035):
    """가시자리 펠트(작은 사각뿔) + 가시 5~7개(가는 삼각뿔)를 한 메시로."""
    bm = bmesh.new()
    felt_faces = []
    for pos, d, lmul in areoles:
        d = d.normalized()
        a = d.orthogonal().normalized()
        b2 = d.cross(a)
        # 펠트 혹
        fw = 0.009
        base = [bm.verts.new(pos - d * 0.004 + (a * math.cos(q) + b2 * math.sin(q)) * fw)
                for q in (0, math.pi / 2, math.pi, 1.5 * math.pi)]
        tip = bm.verts.new(pos + d * 0.006)
        for i in range(4):
            felt_faces.append(bm.faces.new((base[i], base[(i + 1) % 4], tip)))
        # 가시
        ns = random.randint(5, 7)
        for k in range(ns):
            q = 2 * math.pi * k / ns + random.uniform(-0.3, 0.3)
            spread = random.uniform(0.45, 0.95)
            dirv = (d * math.cos(spread) + (a * math.cos(q) + b2 * math.sin(q)) * math.sin(spread)).normalized()
            if k == 0:
                dirv = (d + a * 0.15).normalized()  # 가운데 큰 가시
            ln = base_len * lmul * random.uniform(0.6, 1.2) * (1.5 if k == 0 else 1.0)
            o = dirv.orthogonal().normalized()
            o2 = dirv.cross(o)
            w = 0.0018
            root = pos + d * 0.002
            bv = [bm.verts.new(root + (o * math.cos(t) + o2 * math.sin(t)) * w) for t in (0, 2.094, 4.189)]
            tv = bm.verts.new(root + dirv * ln)
            for i in range(3):
                bm.faces.new((bv[i], bv[(i + 1) % 3], tv))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    for f in felt_faces:
        f.material_index = 1
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    me.materials.append(M_SPINE)
    me.materials.append(M_FELT)
    return ob


def paint_bark(ob, z_lim):
    """밑동의 코르크화: z_lim(각도) 아래 면에 껍질 재질."""
    me = ob.data
    me.materials.clear()
    me.materials.append(M_SKIN)
    me.materials.append(M_BARK)
    for p in me.polygons:
        c = p.center
        ang = math.atan2(c.y, c.x)
        if c.z < z_lim(ang):
            p.material_index = 1


# ---------------------------------------------------------------- 줄기
TRUNK_H = 9.2


def trunk_r(s):
    # 밑동에서 살짝 좁고, 1~2m 에서 가장 굵고, 위로 갈수록 아주 조금 가늘어진다
    base = 0.27 + 0.07 * min(s / 1.4, 1.0) ** 0.7
    taper = -0.03 * max(s - 2.0, 0) / (TRUNK_H - 2.0)
    growth = 0.006 * math.sin(s * 2.3 + 0.7) + 0.004 * math.sin(s * 5.1)  # 가뭄·우기 해에 따른 미세한 굵기 변화
    return base + taper + growth


trunk_path = [Vector((0, 0, z)) for z in [i * 0.05 for i in range(int(TRUNK_H / 0.05) + 1)]]
trunk, trunk_areoles = ribbed_tube('Saguaro_Trunk', trunk_path, trunk_r, nribs=20, depth=0.17, vpr=8, dome=0.8)
paint_bark(trunk, lambda a: 0.75 + 0.22 * math.sin(3 * a) + 0.08 * math.sin(7 * a + 1.0))


# ---------------------------------------------------------------- 팔
def arm_path(h0, az, out_len, bend_r, up_len, lean=0.0, droop=0.0):
    """줄기 속(축에서 0.1m)에서 출발 → 수평으로 out_len → 사분원(bend_r)으로 꺾여 → 위로 up_len."""
    d = Vector((math.cos(az), math.sin(az), 0))
    z = Vector((0, 0, 1))
    pts = []
    step = 0.045
    start = d * 0.1 + z * h0
    # 수평 구간 (약간 아래로 처졌다가)
    n1 = max(int(out_len / step), 2)
    for i in range(n1):
        t = i / n1
        pts.append(start + d * (out_len * t) - z * (droop * math.sin(math.pi * t)))
    c0 = start + d * out_len
    # 사분원 꺾임
    n2 = max(int(bend_r * math.pi / 2 / step), 4)
    for i in range(n2):
        a = (math.pi / 2) * i / n2
        pts.append(c0 + d * (bend_r * math.sin(a)) + z * (bend_r * (1 - math.cos(a))))
    c1 = c0 + d * bend_r + z * bend_r
    # 위로 (lean 만큼 바깥으로 기울어짐)
    n3 = max(int(up_len / step), 2)
    for i in range(n3 + 1):
        t = i / n3
        pts.append(c1 + z * (up_len * t) + d * (lean * t * t))
    return pts


def make_arm_r(L, r_max, r_join):
    def f(s):
        # 줄기와 붙는 곳은 잘록, 0.6m 뒤로 최대 굵기, 끝으로 조금 가늘게
        k = min(max((s - 0.25) / 0.55, 0.0), 1.0)
        k = k * k * (3 - 2 * k)
        r = r_join + (r_max - r_join) * k
        return r * (1.0 - 0.08 * s / L)
    return f


ARMS = [
    # 높이, 방위(도), 수평길이, 꺾임반경, 위길이, 바깥기울기, 최대반경
    (2.9, 8, 0.45, 0.45, 2.7, 0.12, 0.25),
    (3.8, 186, 0.40, 0.42, 3.2, 0.07, 0.245),
    (4.3, 112, 0.32, 0.40, 1.9, 0.08, 0.225),
    (5.6, -62, 0.30, 0.36, 1.3, 0.05, 0.21),
]
arms = []
arm_areoles = []
for i, (h0, azd, ol, br, ul, lean, rmax) in enumerate(ARMS):
    pts = arm_path(h0, math.radians(azd), ol, br, ul, lean=lean, droop=0.04)
    L = sum((pts[k] - pts[k - 1]).length for k in range(1, len(pts)))
    ob, ar = ribbed_tube(f'Saguaro_Arm_{i + 1}', pts, make_arm_r(L, rmax, rmax * 0.62), nribs=15,
                         depth=0.17, vpr=8, dome=0.8, phase=0.13 * i)
    assign(ob, M_SKIN)
    # 줄기 속에 묻힌 가시자리는 버린다
    tr = trunk_r(h0)
    ar = [a for a in ar if Vector((a[0].x, a[0].y)).length > tr * 0.98 + 0.02]
    arms.append(ob)
    arm_areoles += ar

spines = build_spines('Saguaro_Spines_and_Areoles', trunk_areoles + arm_areoles)


# ---------------------------------------------------------------- 꽃·봉오리·열매 (5~6월 개화기)
def flower_mesh():
    """회전체 꽃: 녹색 꽃통 + 흰 꽃잎 컵(꽃잎 12장 물결) + 노란 수술 방석."""
    bm = bmesh.new()
    seg = 36
    petals = 12
    prof_tube = [(0.0, 0.0), (0.012, 0.0), (0.016, 0.03), (0.022, 0.055), (0.0, 0.055)]
    prof_cup = [(0.018, 0.05), (0.03, 0.065), (0.042, 0.08), (0.05, 0.095), (0.046, 0.099),
                (0.036, 0.086), (0.024, 0.072), (0.014, 0.064)]
    prof_st = [(0.0, 0.066), (0.016, 0.066), (0.017, 0.076), (0.008, 0.08), (0.0, 0.08)]
    parts = []
    for prof, scal in ((prof_tube, False), (prof_cup, True), (prof_st, False)):
        rings = []
        for i in range(seg):
            a = 2 * math.pi * i / seg
            ring = []
            for r, z in prof:
                rr = r
                zz = z
                if scal and r > 0.025:
                    w = abs(math.cos(a * petals / 2)) ** 0.6
                    rr = r * (0.9 + 0.1 * w)
                    zz = z + 0.006 * (w - 0.5)
                ring.append(bm.verts.new((rr * math.cos(a), rr * math.sin(a), zz)))
            rings.append(ring)
        faces = []
        for i in range(seg):
            r0, r1 = rings[i], rings[(i + 1) % seg]
            for j in range(len(prof) - 1):
                faces.append(bm.faces.new((r0[j], r1[j], r1[j + 1], r0[j + 1])))
        if scal:  # 컵은 고리: 마지막-처음 이어 닫기
            for i in range(seg):
                r0, r1 = rings[i], rings[(i + 1) % seg]
                faces.append(bm.faces.new((r0[-1], r1[-1], r1[0], r0[0])))
        parts.append(faces)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-6)
    for fi, mi in ((1, 1), (2, 2)):
        for f in parts[fi]:
            if f.is_valid:
                f.material_index = mi
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new('SaguaroFlower')
    bm.to_mesh(me)
    bm.free()
    for p in me.polygons:
        p.use_smooth = True
    me.materials.append(M_BUD)
    me.materials.append(M_PETAL)
    me.materials.append(M_STAMEN)
    return me


FLOWER_ME = flower_mesh()
BUD = sphere('Saguaro_Bud_proto', r=0.022, material=M_BUD, seg=12, rings=8, scale=(1, 1, 1.6))
FRUIT = sphere('Saguaro_Fruit_proto', r=0.026, material=M_FRUIT, seg=12, rings=8, scale=(1, 1, 1.45))


def crown(tag, tip, axis, R, n_fl, n_bud, n_fruit):
    """돔 꼭대기 둘레에 꽃·봉오리·열매를 바깥을 향해 꽂는다."""
    axis = axis.normalized()
    a = axis.orthogonal().normalized()
    b = axis.cross(a)
    items = [('F', n_fl), ('B', n_bud), ('R', n_fruit)]
    kinds = []
    for k, n in items:
        kinds += [k] * n
    random.shuffle(kinds)
    total = len(kinds)
    for i, k in enumerate(kinds):
        q = 2 * math.pi * i / total + random.uniform(-0.15, 0.15)
        pol = random.uniform(0.25, 0.75)  # 돔 위 극각(rad)
        radial = a * math.cos(q) + b * math.sin(q)
        nrm = (axis * math.cos(pol) + radial * math.sin(pol)).normalized()
        pos = tip - axis * (R * 0.8) + axis * (R * 0.8 * math.cos(pol)) + radial * (R * math.sin(pol))
        pos -= nrm * 0.01  # 표면에 살짝 묻는다
        rot = Vector((0, 0, 1)).rotation_difference(nrm).to_euler()
        if k == 'F':
            ob = bpy.data.objects.new(f'Flower_{tag}_{i}', FLOWER_ME)
            link(ob)
        elif k == 'B':
            ob = duplicate(BUD, name=f'Bud_{tag}_{i}')
            pos += nrm * 0.025
        else:
            ob = duplicate(FRUIT, name=f'Fruit_{tag}_{i}')
            pos += nrm * 0.028
        ob.location = pos
        ob.rotation_euler = rot
        s = random.uniform(0.9, 1.15)
        ob.scale = (s, s, s)


# 줄기 꼭대기
tR = trunk_r(TRUNK_H)
crown('Trunk', Vector((0, 0, TRUNK_H + tR * 0.8)), Vector((0, 0, 1)), tR, 9, 6, 2)
for i, (h0, azd, ol, br, ul, lean, rmax) in enumerate(ARMS):
    d = Vector((math.cos(math.radians(azd)), math.sin(math.radians(azd)), 0))
    top = d * (0.1 + ol + br + lean) + Vector((0, 0, h0 + br + ul))
    L = ol + br * math.pi / 2 + ul
    rt = make_arm_r(L, rmax, rmax * 0.62)(L)
    axis = (Vector((0, 0, ul)) + d * (2 * lean)).normalized()
    crown(f'Arm{i + 1}', top + axis * (rt * 0.8), axis, rt, 5, 3, 1 if i % 2 == 0 else 0)
# 원형 프로토타입은 숨긴다(메시만 공유)
BUD.hide_render = True
BUD.hide_viewport = True
FRUIT.hide_render = True
FRUIT.hide_viewport = True
BUD['atelier_cutter'] = True
FRUIT['atelier_cutter'] = True


# ---------------------------------------------------------------- 사막 바닥 둔덕과 바위
def sand_mound(R=2.4, h=0.14):
    bm = bmesh.new()
    nr, na = 14, 72
    center = bm.verts.new((0, 0, h))
    rings = []
    for i in range(1, nr + 1):
        t = i / nr
        ring = []
        for j in range(na):
            a = 2 * math.pi * j / na
            edge = 1 + 0.06 * math.sin(3 * a + 0.4) + 0.035 * math.sin(5 * a + 2.0) + 0.015 * math.sin(11 * a)
            r = R * t * edge
            z = h * (1 - t ** 1.6) ** 1.5 + 0.012 * math.sin(9 * a + 7 * t) * (1 - t) if i < nr else 0.0
            ring.append(bm.verts.new((r * math.cos(a), r * math.sin(a), max(z, 0.0))))
        rings.append(ring)
    for j in range(na):
        bm.faces.new((center, rings[0][j], rings[0][(j + 1) % na]))
    for i in range(nr - 1):
        for j in range(na):
            jn = (j + 1) % na
            bm.faces.new((rings[i][j], rings[i + 1][j], rings[i + 1][jn], rings[i][jn]))
    bm.faces.new(list(reversed(rings[-1])))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new('Desert_Sand_Mound')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new('Desert_Sand_Mound', me)
    link(ob)
    smooth(ob)
    return assign(ob, M_SOIL)


sand_mound()


def rock(name, loc, size, rotz):
    bm = bmesh.new()
    bmesh.ops.create_icosphere(bm, subdivisions=3, radius=1.0)
    for v in bm.verts:
        n = v.co.normalized()
        k = 1 + 0.18 * math.sin(3.1 * n.x + 1.7 * n.y * 2) + 0.12 * math.sin(4.3 * n.z + n.x * 3) + random.uniform(-0.05, 0.05)
        v.co = n * k
    bmesh.ops.scale(bm, vec=Vector(size), verts=bm.verts)
    for v in bm.verts:  # 바닥 아래로 내려가지 않게 납작하게 자른다(반쯤 묻힌 모양)
        v.co.z = max(v.co.z, -size[2] * 0.25)
    bmesh.ops.translate(bm, vec=(0, 0, size[2] * 0.25), verts=bm.verts)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    ob.location = Vector(loc)
    ob.rotation_euler.z = rotz
    smooth(ob)
    return assign(ob, M_ROCK)


rock('Rock_1', (1.25, -0.9, 0.0), (0.38, 0.28, 0.2), 0.4)
rock('Rock_2', (1.55, -0.45, 0.0), (0.2, 0.16, 0.12), 1.3)
rock('Rock_3', (-1.1, 1.3, 0.0), (0.3, 0.24, 0.16), 2.2)
rock('Rock_4', (-1.6, -0.7, 0.0), (0.16, 0.13, 0.09), 0.8)
for i in range(10):  # 자갈
    a = random.uniform(0, 2 * math.pi)
    r = random.uniform(0.7, 1.9)
    sz = random.uniform(0.04, 0.08)
    rock(f'Pebble_{i + 1}', (r * math.cos(a), r * math.sin(a), 0.0), (sz, sz * 0.8, sz * 0.55), a)
