# 그랑디디에바오바브(Adansonia grandidieri) — 마다가스카르 모론다바 '바오바브 거리'의 그 나무, 높이 약 26m.
# 줄기는 높이·방위각마다 반지름을 계산하는 회전체 격자를 bmesh 로 직접 짰다. 세로 골, 비스듬히 도는 주름,
#   가로 테 자국, 밑동의 뿌리 버팀을 사인 함수의 합으로 새기고, 면마다 재질(먼지 낀 밑동·골·매끈한 수피)을 나눴다.
# 가지는 '꼭대기에만 모여 납작한 왕관을 이루는' 특징을 살려 줄기 끝에서 바깥으로 1차 가지 9개, 위로 3개를 뻗고, 재귀적으로 2·3차
#   가지를 나눴다. 가지는 평행 이동 프레임으로 굵기가 줄어드는 관(tube)을 만들어 부모 가지 속에서 시작한다.
# 잔가지 끝에는 납작한 잎 뭉치를, 2차 가지에는 긴 꼭지에 매달린 열매를 같은 메시를 공유하는 복제로 달았다.
import random
from atelier import *

random.seed(20261023)

SHOT = {"azimuth": -42, "elevation": 9, "lens": 55, "light": "golden", "background": "#2a3040"}

# ---------------------------------------------------------------- 재질
M_BARK = mat('Bark_ReddishGrey', '#8d7d76', rough=0.85)
M_BARK_DARK = mat('Bark_Furrow', '#66574f', rough=0.9)
M_BARK_PALE = mat('Bark_Upper_Smooth', '#a0918a', rough=0.75)
M_DUST = mat('Bark_LateriteDust', '#7f6457', rough=0.95)
M_SCAR = mat('Bark_RingScar', '#776a64', rough=0.9)
M_TWIG = mat('Twig_Grey', '#857265', rough=0.85)
M_LEAF = mat('Leaf_Green', '#5f7d36', rough=0.7)
M_LEAF2 = mat('Leaf_DarkGreen', '#46622b', rough=0.7)
M_FRUIT = mat('Fruit_VelvetPod', '#8c7650', rough=0.9)

# ---------------------------------------------------------------- 치수
H_TRUNK = 19.4          # 줄기 꼭대기(가지가 갈라지는 곳)
R_BASE = 1.55


def trunk_center(z):
    # 아주 약한 굽음
    return Vector((0.18 * math.sin(z / 7.0), 0.12 * math.sin(z / 5.0 + 1.0), 0.0))


def trunk_radius(z, th):
    # 기본 윤곽: 밑동이 넓고, 몸통은 거의 원기둥, 위로 살짝 가늘어지다 꼭대기에서 둥글게 모인다
    r = 1.62 - 0.016 * z
    r += 0.06 * math.sin(z / 19.4 * math.pi * 1.1)           # 가운데 살짝 불룩
    r += 0.42 * math.exp(-z / 0.7)                           # 밑동 퍼짐
    # 뿌리 버팀(밑동에만)
    but = max(0.0, math.cos(5 * th + 0.4)) ** 3 + 0.6 * max(0.0, math.cos(3 * th + 2.0)) ** 4
    r += 0.38 * but * math.exp(-z / 0.55)
    # 세로 골과 비스듬한 주름
    ridge = 0.030 * math.sin(9 * th + 0.25 * z) + 0.018 * math.sin(17 * th - 0.4 * z + 1.3) \
        + 0.012 * math.sin(29 * th + 0.9 * z)
    r *= 1.0 + ridge
    # 가로 테 자국
    r *= 1.0 - 0.010 * scar_value(z, th)
    # 꼭대기: 둥글게 좁아짐
    if z > 16.0:
        t = (z - 16.0) / (H_TRUNK - 16.0)
        r *= math.sqrt(max(0.0, 1.0 - 0.72 * t ** 1.6))
    return r


def scar_value(z, th):
    # 드문드문 끊긴 가로 테 자국
    band = max(0.0, math.sin(z * 1.35 + 0.6 * math.sin(th * 2 + z))) ** 10
    patch = 0.5 + 0.5 * math.sin(3 * th + 1.7 * z)
    return band * patch


def ridge_value(z, th):
    return math.sin(9 * th + 0.25 * z) + 0.6 * math.sin(17 * th - 0.4 * z + 1.3)


def build_trunk():
    seg, nz = 96, 150
    bm = bmesh.new()
    rings = []
    zs = [H_TRUNK * (i / (nz - 1)) ** 1.0 for i in range(nz)]
    for z in zs:
        c = trunk_center(z)
        ring = []
        for j in range(seg):
            th = 2 * math.pi * j / seg
            r = trunk_radius(z, th)
            ring.append(bm.verts.new((c.x + r * math.cos(th), c.y + r * math.sin(th), z)))
        rings.append(ring)
    faces = []
    for i in range(nz - 1):
        for j in range(seg):
            f = bm.faces.new((rings[i][j], rings[i][(j + 1) % seg], rings[i + 1][(j + 1) % seg], rings[i + 1][j]))
            zc = (zs[i] + zs[i + 1]) / 2
            th = 2 * math.pi * (j + 0.5) / seg
            rv = ridge_value(zc, th)
            if zc < 0.55 + 0.25 * math.sin(th * 4):
                f.material_index = 3
            elif scar_value(zc, th) > 0.55:
                f.material_index = 4
            elif rv < -1.15:
                f.material_index = 1
            elif zc > 16.6 + 0.9 * math.sin(3 * th + 0.8) + 0.4 * math.sin(7 * th):
                f.material_index = 2
            else:
                f.material_index = 0
            faces.append(f)
    # 바닥 뚜껑
    bm.faces.new(list(reversed(rings[0])))
    # 꼭대기 뚜껑(가지가 덮는다)
    top_c = bm.verts.new(trunk_center(H_TRUNK) + Vector((0, 0, H_TRUNK + 0.25)))
    for j in range(seg):
        f = bm.faces.new((rings[-1][j], rings[-1][(j + 1) % seg], top_c))
        f.material_index = 2
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new('Baobab_Trunk')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new('Baobab_Trunk', me)
    link(ob)
    for m in (M_BARK, M_BARK_DARK, M_BARK_PALE, M_DUST, M_SCAR):
        me.materials.append(m)
    smooth(ob)
    return ob


# ---------------------------------------------------------------- 가지(관)
def tube(bm, pts, radii, seg, cap_end=True):
    n = len(pts)
    tans = []
    for i in range(n):
        a = pts[max(i - 1, 0)]
        b = pts[min(i + 1, n - 1)]
        tans.append((b - a).normalized())
    ref = Vector((0, 0, 1)) if abs(tans[0].z) < 0.9 else Vector((1, 0, 0))
    nrm = tans[0].cross(ref).normalized()
    rings = []
    for i in range(n):
        if i > 0:
            # 평행 이동
            axis = tans[i - 1].cross(tans[i])
            if axis.length > 1e-6:
                ang = tans[i - 1].angle(tans[i])
                nrm = (Matrix.Rotation(ang, 3, axis.normalized()) @ nrm).normalized()
        bin_ = tans[i].cross(nrm).normalized()
        ring = []
        for j in range(seg):
            a = 2 * math.pi * j / seg
            off = (nrm * math.cos(a) + bin_ * math.sin(a)) * radii[i]
            ring.append(bm.verts.new(pts[i] + off))
        rings.append(ring)
    for i in range(n - 1):
        for j in range(seg):
            f = bm.faces.new((rings[i][j], rings[i][(j + 1) % seg], rings[i + 1][(j + 1) % seg], rings[i + 1][j]))
            f.smooth = True
    bm.faces.new(list(reversed(rings[0]))).smooth = True
    if cap_end:
        tip = bm.verts.new(pts[-1] + tans[-1] * radii[-1] * 0.8)
        for j in range(seg):
            bm.faces.new((rings[-1][j], rings[-1][(j + 1) % seg], tip)).smooth = True


def grow_path(p0, d, length, steps, bend_up, jitter):
    pts = [p0.copy()]
    dirs = [d.normalized()]
    step = length / steps
    cur = d.normalized()
    p = p0.copy()
    for i in range(steps):
        j = Vector((random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-0.5, 0.5))) * jitter
        cur = (cur + Vector((0, 0, bend_up)) + j).normalized()
        p = p + cur * step
        pts.append(p.copy())
        dirs.append(cur.copy())
    return pts, dirs


def taper(r0, r1, n, power=1.0):
    return [r0 + (r1 - r0) * (i / (n - 1)) ** power for i in range(n)]


def lumpy(radii):
    # 마디마다 약간 부풀고 잘록한 굵기 변화
    ph = random.uniform(0, 6.28)
    n = len(radii)
    return [r * (1.0 + 0.07 * math.sin(i * 1.9 + ph)) if 0 < i < n - 1 else r for i, r in enumerate(radii)]


def dir_from(az, el):
    return Vector((math.cos(el) * math.cos(az), math.cos(el) * math.sin(az), math.sin(el)))


bm_primary = bmesh.new()
bm_secondary = bmesh.new()
bm_twig = bmesh.new()
tips = []           # 잎 뭉치를 달 잔가지 끝 (위치, 방향)
fruit_spots = []    # 열매를 매달 지점


def spawn_children(pts, dirs, radii, level, count, t_range):
    n = len(pts)
    for k in range(count):
        t = t_range[0] + (t_range[1] - t_range[0]) * (k + random.uniform(0.0, 0.8)) / count
        idx = min(n - 2, max(1, int(t * (n - 1))))
        base = pts[idx]
        pd = dirs[idx]
        side = Vector((0, 0, 1)).cross(pd)
        if side.length < 1e-4:
            side = Vector((1, 0, 0))
        side.normalize()
        sgn = 1 if k % 2 == 0 else -1
        ang = math.radians(random.uniform(28, 50)) * sgn
        d = (Matrix.Rotation(ang, 3, Vector((0, 0, 1))) @ pd)
        d.z += random.uniform(0.12, 0.4)
        d.normalize()
        pr = radii[idx]
        start = base - d * pr * 0.6
        if level == 2:
            L = random.uniform(2.4, 3.6)
            steps = 7
            p2, d2 = grow_path(start, d, L, steps, 0.045, 0.14)
            r2 = lumpy(taper(pr * 0.62, 0.045, len(p2), 0.9))
            tube(bm_secondary, p2, r2, 10)
            spawn_children(p2, d2, r2, 3, random.randint(2, 3), (0.35, 0.95))
            fruit_spots.append((p2[len(p2) // 2], r2[len(p2) // 2]))
            tips.append((p2[-1], d2[-1]))
        else:
            L = random.uniform(0.9, 1.6)
            steps = 4
            p3, d3 = grow_path(start, d, L, steps, 0.10, 0.18)
            r3 = taper(min(pr * 0.65, 0.07), 0.018, len(p3))
            tube(bm_twig, p3, r3, 6)
            tips.append((p3[-1], d3[-1]))


def build_crown():
    n_main = 9
    for i in range(n_main):
        az = 2 * math.pi * i / n_main + random.uniform(-0.2, 0.2)
        el = math.radians(random.uniform(10, 28))
        zb = H_TRUNK - random.uniform(0.3, 2.0)
        c = trunk_center(zb)
        rr = trunk_radius(zb, az) * 0.35
        p0 = c + Vector((rr * math.cos(az), rr * math.sin(az), zb))
        L = random.uniform(6.3, 8.0)
        pts, dirs = grow_path(p0, dir_from(az, el), L, 14, 0.055, 0.11)
        radii = lumpy(taper(random.uniform(0.50, 0.62), 0.10, len(pts), 0.75))
        tube(bm_primary, pts, radii, 18)
        spawn_children(pts, dirs, radii, 2, random.randint(3, 4), (0.40, 0.98))
        tips.append((pts[-1], dirs[-1]))
    # 가운데 위로 솟는 짧은 가지
    for i in range(3):
        az = random.uniform(0, 2 * math.pi)
        p0 = trunk_center(H_TRUNK) + Vector((0.2 * math.cos(az), 0.2 * math.sin(az), H_TRUNK - 0.6))
        pts, dirs = grow_path(p0, dir_from(az, math.radians(68)), random.uniform(3.0, 4.0), 9, 0.02, 0.06)
        radii = taper(0.38, 0.08, len(pts), 0.8)
        tube(bm_primary, pts, radii, 14)
        spawn_children(pts, dirs, radii, 2, 2, (0.5, 0.95))


def build_stubs():
    # 줄기 윗부분의 부러진 옛 가지 자국(짧은 뭉툭한 혹)
    for i in range(5):
        z = random.uniform(11.5, 16.5)
        az = random.uniform(0, 2 * math.pi)
        c = trunk_center(z)
        r = trunk_radius(z, az)
        d = dir_from(az, math.radians(random.uniform(20, 40)))
        p0 = c + Vector((math.cos(az), math.sin(az), 0)) * (r * 0.75) + Vector((0, 0, z))
        L = r * 0.25 + random.uniform(0.25, 0.45)
        pts = [p0 + d * (L * k / 4) for k in range(5)]
        rb = random.uniform(0.16, 0.24)
        tube(bm_primary, pts, [rb * 1.3, rb * 1.15, rb, rb * 0.9, rb * 0.75], 12)


def finish(bm, name, material):
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    me.materials.append(material)
    return ob


# ---------------------------------------------------------------- 잎 뭉치 / 열매
def leaf_clump_mesh(name, seed):
    rnd = random.Random(seed)
    bm = bmesh.new()
    # 손바닥 모양 겹잎: 잎자루 끝에서 퍼지는 5~7장의 납작한 타원 잎
    for c in range(4):
        cx, cy, cz = rnd.uniform(-0.25, 0.25), rnd.uniform(-0.25, 0.25), rnd.uniform(-0.1, 0.15)
        nleaf = rnd.randint(5, 7)
        rot0 = rnd.uniform(0, 2 * math.pi)
        for k in range(nleaf):
            a = rot0 + 2 * math.pi * k / nleaf
            ln = rnd.uniform(0.22, 0.32)
            w = ln * 0.32
            # 잎 한 장: 4점 마름모꼴 + 가운데 접힘
            dx, dy = math.cos(a), math.sin(a)
            px, py = -dy, dx
            droop = -0.06
            v0 = bm.verts.new((cx, cy, cz))
            v1 = bm.verts.new((cx + dx * ln * 0.5 + px * w, cy + dy * ln * 0.5 + py * w, cz + 0.02))
            v2 = bm.verts.new((cx + dx * ln, cy + dy * ln, cz + droop))
            v3 = bm.verts.new((cx + dx * ln * 0.5 - px * w, cy + dy * ln * 0.5 - py * w, cz + 0.02))
            bm.faces.new((v0, v1, v2, v3))
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    return me


def build_leaves():
    meshes = [leaf_clump_mesh('LeafClump_A', 1), leaf_clump_mesh('LeafClump_B', 2), leaf_clump_mesh('LeafClump_C', 3)]
    for m, mt in zip(meshes, (M_LEAF, M_LEAF2, M_LEAF)):
        m.materials.append(mt)
    for i, (p, d) in enumerate(tips):
        for k in range(2):
            ob = bpy.data.objects.new(f'LeafClump_{i:03d}_{k}', meshes[(i + k) % 3])
            link(ob)
            ob.location = p + Vector((random.uniform(-0.25, 0.25), random.uniform(-0.25, 0.25), random.uniform(-0.05, 0.25)))
            ob.rotation_euler = (random.uniform(-0.3, 0.3), random.uniform(-0.3, 0.3), random.uniform(0, 6.28))
            s = random.uniform(0.9, 1.3)
            ob.scale = (s, s, s)


def build_fruits():
    pod = lathe('Fruit_Pod_Mesh', [(0.0, 0.0), (0.035, 0.01), (0.06, 0.05), (0.07, 0.10), (0.065, 0.15),
                                   (0.045, 0.19), (0.015, 0.21), (0.0, 0.215)], material=M_FRUIT, seg=14)
    pod.name = 'Baobab_Fruit_000'
    first = True
    for i, (p, r) in enumerate(fruit_spots):
        stalk_len = random.uniform(0.25, 0.45)
        top = p + Vector((0, 0, -r * 0.6))
        bot = top + Vector((0, 0, -stalk_len))
        curve_tube(f'Fruit_Stalk_{i:03d}', [top + Vector((0, 0, 0.05)), bot], radius=0.008, material=M_TWIG,
                   resolution=1, bevel_res=1)
        ob = pod if first else duplicate(pod, name=f'Baobab_Fruit_{i:03d}')
        first = False
        ob.location = bot + Vector((0, 0, -0.205))
        ob.rotation_euler = (random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1), 0)


build_trunk()
build_crown()
build_stubs()
finish(bm_primary, 'Baobab_Branches_Primary', M_BARK_PALE)
finish(bm_secondary, 'Baobab_Branches_Secondary', M_BARK_PALE)
finish(bm_twig, 'Baobab_Twigs', M_TWIG)
build_leaves()
build_fruits()
