import bpy
import bmesh
import math
import random
from mathutils import Vector, Matrix

random.seed(42)

# ---------------------------------------------------------------------------
# Remove leftover mesh objects (e.g. default cube) so only one specimen exists
# ---------------------------------------------------------------------------
for _ob in list(bpy.data.objects):
    if _ob.type == 'MESH':
        try:
            bpy.data.objects.remove(_ob, do_unlink=True)
        except Exception:
            pass

MAT_CAP, MAT_WART, MAT_GILL, MAT_STIPE, MAT_RING, MAT_VOLVA = range(6)

# ---------------------------------------------------------------------------
# Dimensions (metres): cap ~15 cm across, stipe ~18 cm tall, bulb ~4 cm wide
# ---------------------------------------------------------------------------
R_CAP = 0.075       # cap radius
Z_MARGIN = 0.163    # cap margin height
H_CAP = 0.034       # cap dome height above margin
R_UNDER = 0.0722    # outer radius of gill-bearing surface
R_STIPE_TOP = 0.0100


def clamp(x, a, b):
    return max(a, min(b, x))


def lerp(a, b, t):
    return a + (b - a) * t


def lerp3(c1, c2, t):
    return tuple(lerp(c1[i], c2[i], t) for i in range(3))


def z_top(r):
    u = min(r / R_CAP, 1.0)
    return Z_MARGIN + H_CAP * (max(0.0, 1.0 - u * u)) ** 0.8


def z_under(r):
    t = clamp((R_UNDER - r) / (R_UNDER - R_STIPE_TOP), 0.0, 1.0)
    return 0.1625 + 0.0145 * (math.sin(t * math.pi / 2.0) ** 0.8)


def wobble(r, t):
    u = r / R_CAP
    return 0.0018 * (u ** 1.6) * (math.sin(3 * t + 0.7)
                                  + 0.5 * math.sin(5 * t + 2.1)
                                  + 0.3 * math.sin(2 * t + 4.0))


STRI = 72  # marginal striations


def striation(u, t):
    if u <= 0.78:
        return 0.0
    s = ((u - 0.78) / 0.22) ** 1.5
    return s * (0.5 + 0.5 * math.cos(STRI * t)) ** 3


# ---------------------------------------------------------------------------
# Generic surface-of-revolution builder
# ---------------------------------------------------------------------------
def lathe(bm, profile, segs, mat, disp=None, closed=False, phase=0.0):
    rings = []
    for (r, z, kind) in profile:
        if r < 1e-7:
            dz = 0.0
            if disp:
                _, dz = disp(0.0, z, 0.0, kind)
            rings.append([bm.verts.new((0.0, 0.0, z + dz))])
        else:
            ring = []
            for j in range(segs):
                t = phase + 2.0 * math.pi * j / segs
                dr = dz = 0.0
                if disp:
                    dr, dz = disp(r, z, t, kind)
                rr = max(r + dr, 1e-5)
                ring.append(bm.verts.new((rr * math.cos(t), rr * math.sin(t), z + dz)))
            rings.append(ring)
    n = len(rings)
    pairs = [(i, i + 1) for i in range(n - 1)]
    if closed:
        pairs.append((n - 1, 0))
    for a, b in pairs:
        A, B = rings[a], rings[b]
        if len(A) == 1 and len(B) == 1:
            continue
        for j in range(segs):
            jn = (j + 1) % segs
            try:
                if len(A) == 1:
                    f = bm.faces.new((A[0], B[jn], B[j]))
                elif len(B) == 1:
                    f = bm.faces.new((A[j], A[jn], B[0]))
                else:
                    f = bm.faces.new((A[j], A[jn], B[jn], B[j]))
                f.material_index = mat
                f.smooth = True
            except ValueError:
                pass


def chaikin(pts, iterations):
    for _ in range(iterations):
        new = [pts[0]]
        for a, b in zip(pts[:-1], pts[1:]):
            new.append((0.75 * a[0] + 0.25 * b[0], 0.75 * a[1] + 0.25 * b[1]))
            new.append((0.25 * a[0] + 0.75 * b[0], 0.25 * a[1] + 0.75 * b[1]))
        new.append(pts[-1])
        pts = new
    return pts


bm = bmesh.new()

# ---------------------------------------------------------------------------
# PILEUS (cap): convex to plano-convex, striate margin, solid flesh
# ---------------------------------------------------------------------------
cap_profile = []
NT = 44
for k in range(NT + 1):
    u = math.sin((k / NT) * math.pi / 2.0)
    r = R_CAP * u
    cap_profile.append((r, z_top(r), 'top'))
cap_profile.append((R_CAP - 0.0010, 0.1617, 'margin'))
cap_profile.append((R_CAP - 0.0026, 0.1620, 'margin'))
NU = 32
for k in range(NU + 1):
    r = R_UNDER - (R_UNDER - R_STIPE_TOP) * k / NU
    cap_profile.append((r, z_under(r), 'under'))
cap_profile.append((0.006, z_under(R_STIPE_TOP) + 0.0003, 'under'))
cap_profile.append((0.0, z_under(R_STIPE_TOP) + 0.0005, 'under'))


def cap_disp(r, z, t, kind):
    dz = wobble(r, t)
    dr = 0.0
    if kind in ('top', 'margin'):
        u = r / R_CAP
        dz -= 0.0005 * striation(u, t)
        dr += 0.0008 * math.sin(7 * t + 1.3) * (u ** 4)
        dz += 0.0004 * math.sin(4 * t + 0.3) * math.sin(u * 9.0)
    return dr, dz


lathe(bm, cap_profile, 288, MAT_CAP, disp=cap_disp)

# ---------------------------------------------------------------------------
# LAMELLAE (gills): free, crowded, white, with lamellulae
# ---------------------------------------------------------------------------
def add_gill(theta, r0, r1, D, thick):
    n = 18
    c, s = math.cos(theta), math.sin(theta)
    rad = Vector((c, s, 0.0))
    tan = Vector((-s, c, 0.0))
    rows = []
    for i in range(n + 1):
        a = i / n
        r = r0 + (r1 - r0) * a
        zt = z_under(r) + wobble(r, theta) + 0.0015
        shape = ((a ** 0.45) * ((1.0 - a) ** 0.5)) / 0.518
        depth = D * shape * (1.0 + 0.05 * math.sin(r * 700.0 + theta * 7.0))
        zb = zt - 0.0015 - depth
        ht = thick * (0.55 + 0.45 * min(1.0, shape)) / 2.0
        hb = ht * 0.5
        base = rad * r
        tl = bm.verts.new(base + tan * ht + Vector((0, 0, zt)))
        tr = bm.verts.new(base - tan * ht + Vector((0, 0, zt)))
        bl = bm.verts.new(base + tan * hb + Vector((0, 0, zb)))
        br = bm.verts.new(base - tan * hb + Vector((0, 0, zb)))
        rows.append((tl, tr, bl, br))
    faces = []
    for i in range(n):
        tl0, tr0, bl0, br0 = rows[i]
        tl1, tr1, bl1, br1 = rows[i + 1]
        faces.append((tl0, tl1, bl1, bl0))
        faces.append((tr0, br0, br1, tr1))
        faces.append((bl0, bl1, br1, br0))
        faces.append((tl0, tr0, tr1, tl1))
    tl, tr, bl, br = rows[0]
    faces.append((tl, bl, br, tr))
    tl, tr, bl, br = rows[-1]
    faces.append((tl, tr, br, bl))
    for fv in faces:
        try:
            f = bm.faces.new(fv)
            f.material_index = MAT_GILL
            f.smooth = True
        except ValueError:
            pass


N_FULL = 64
R_GILL_OUT = 0.0712
step = 2.0 * math.pi / N_FULL
for i in range(N_FULL):
    th = i * step + random.uniform(-0.01, 0.01)
    add_gill(th, 0.0128 + random.uniform(0.0, 0.0010), R_GILL_OUT, 0.0085, 0.0007)
    # half-length lamellula
    r0 = random.uniform(0.030, 0.040)
    L = R_GILL_OUT - r0
    add_gill(th + step * 0.5 + random.uniform(-0.01, 0.01), r0, R_GILL_OUT,
             0.0085 * min(1.0, (L / 0.05) ** 0.5), 0.0006)
    # quarter-length lamellulae
    for q in (0.25, 0.75):
        r0 = random.uniform(0.050, 0.060)
        L = R_GILL_OUT - r0
        add_gill(th + step * q + random.uniform(-0.008, 0.008), r0, R_GILL_OUT,
                 0.0085 * min(1.0, (L / 0.05) ** 0.5), 0.0005)

# ---------------------------------------------------------------------------
# STIPE with basal BULB
# ---------------------------------------------------------------------------
stipe_raw = [
    (0.0, 0.0), (0.006, 0.0005), (0.012, 0.003), (0.0165, 0.007),
    (0.0195, 0.012), (0.021, 0.018), (0.0205, 0.024), (0.0185, 0.030),
    (0.0155, 0.036), (0.0132, 0.042), (0.0122, 0.050), (0.0115, 0.065),
    (0.0108, 0.085), (0.0101, 0.105), (0.0096, 0.125), (0.0092, 0.140),
    (0.0090, 0.155), (0.0092, 0.165), (0.0098, 0.172), (0.0102, 0.1785),
    (0.0, 0.1795),
]
stipe_pts = chaikin(stipe_raw, 2)


def stipe_radius(z):
    for a, b in zip(stipe_pts[:-1], stipe_pts[1:]):
        if a[1] <= z <= b[1]:
            t = (z - a[1]) / max(1e-9, (b[1] - a[1]))
            return lerp(a[0], b[0], t)
    return stipe_pts[-2][0]


def stipe_disp(r, z, t, kind):
    dr = 0.00018 * math.sin(11 * t + z * 350.0) + 0.0001 * math.sin(29 * t - z * 800.0)
    if z < 0.045:
        dr += 0.0005 * math.sin(5 * t + z * 200.0) * clamp((0.045 - z) / 0.02, 0, 1)
    return dr, 0.0


stipe_profile = [(max(0.0, p[0]) if i not in (0, len(stipe_pts) - 1) else 0.0, p[1], 's')
                 for i, p in enumerate(stipe_pts)]
lathe(bm, stipe_profile, 64, MAT_STIPE, disp=stipe_disp)

# ---------------------------------------------------------------------------
# ANNULUS (pendant membranous ring / skirt)
# ---------------------------------------------------------------------------
ZA = 0.1428
ra = stipe_radius(0.142)
ring_profile = [
    (ra - 0.0008, 0.1432, 'o'), (ra + 0.0012, 0.1428, 'o'), (ra + 0.0030, 0.1415, 'o'),
    (ra + 0.0048, 0.1390, 'o'), (ra + 0.0064, 0.1355, 'o'), (ra + 0.0078, 0.1315, 'o'),
    (ra + 0.0088, 0.1275, 'o'), (ra + 0.0094, 0.1245, 'tip'),
    (ra + 0.0086, 0.1248, 'i'), (ra + 0.0079, 0.1278, 'i'), (ra + 0.0069, 0.1318, 'i'),
    (ra + 0.0055, 0.1356, 'i'), (ra + 0.0039, 0.1388, 'i'), (ra + 0.0021, 0.1410, 'i'),
    (ra + 0.0004, 0.1420, 'i'), (ra - 0.0008, 0.1418, 'i'),
]


def ring_disp(r, z, t, kind):
    f = clamp((ZA - z) / 0.018, 0.0, 1.0)
    dr = (f ** 1.3) * (0.0016 * math.sin(9 * t + 0.5) + 0.0008 * math.sin(17 * t + 2.0))
    dz = (f ** 3) * (0.0010 * math.sin(13 * t + 1.0) + 0.0006 * math.sin(29 * t + 3.0))
    return dr, dz


lathe(bm, ring_profile, 128, MAT_RING, disp=ring_disp, closed=True)

# ---------------------------------------------------------------------------
# VOLVAL REMNANTS: concentric ragged collars on top of the bulb
# ---------------------------------------------------------------------------
for idx, (zv, sc) in enumerate(((0.029, 1.0), (0.034, 0.85), (0.039, 0.7))):
    rb = stipe_radius(zv)
    prof = [
        (rb - 0.0008, zv - 0.0022 * sc, 'b'),
        (rb + 0.0006 * sc, zv - 0.0016 * sc, 'b'),
        (rb + 0.0018 * sc, zv - 0.0002 * sc, 'm'),
        (rb + 0.0026 * sc, zv + 0.0014 * sc, 'tip'),
        (rb + 0.0012 * sc, zv + 0.0008 * sc, 'm'),
        (rb - 0.0008, zv + 0.0004 * sc, 'b'),
    ]
    ph = 1.7 * idx + 0.4

    def volva_disp(r, z, t, kind, ph=ph, sc=sc):
        nz = (math.sin(11 * t + ph) + 0.6 * math.sin(23 * t + 2 * ph)
              + 0.4 * math.sin(37 * t + 3 * ph))
        if kind == 'tip':
            return 0.0009 * sc * nz, 0.0006 * sc * nz
        if kind == 'm':
            return 0.0005 * sc * nz, 0.0003 * sc * nz
        return 0.0001 * nz, 0.0

    lathe(bm, prof, 96, MAT_VOLVA, disp=volva_disp, closed=True, phase=ph)

# ---------------------------------------------------------------------------
# WARTS: white universal-veil remnants scattered on the cap
# ---------------------------------------------------------------------------
def cap_point(r, t):
    z = z_top(r) + wobble(r, t)
    return Vector((r * math.cos(t), r * math.sin(t), z))


def cap_normal(r, t):
    e = 1e-4
    dr_v = cap_point(r + e, t) - cap_point(max(r - e, 1e-5), t)
    dt_v = cap_point(r, t + e) - cap_point(r, t - e)
    n = dr_v.cross(dt_v)
    if n.length < 1e-12:
        return Vector((0, 0, 1))
    n.normalize()
    if n.z < 0:
        n = -n
    return n


N_W = 190
golden = math.pi * (3.0 - math.sqrt(5.0))
for k in range(N_W):
    if random.random() < 0.08:
        continue  # some warts washed off by rain
    u = math.sqrt((k + 0.5) / N_W) * 0.94 + random.uniform(-0.015, 0.015)
    u = clamp(u, 0.03, 0.95)
    t = k * golden + random.uniform(-0.15, 0.15)
    r = u * R_CAP
    size = 0.0042 * (1.0 - 0.55 * u) * random.uniform(0.6, 1.25)
    h = size * random.uniform(0.45, 0.8)
    p = cap_point(r, t)
    n = cap_normal(r, t)
    rot = n.to_track_quat('Z', 'Y').to_matrix().to_4x4()
    spin = Matrix.Rotation(random.uniform(0, 2 * math.pi), 4, 'Z')
    scl = Matrix.Diagonal((size, size * random.uniform(0.75, 1.1), h, 1.0))
    M = Matrix.Translation(p - n * (h * 0.25)) @ rot @ spin @ scl
    res = bmesh.ops.create_icosphere(bm, subdivisions=2, radius=1.0,
                                     matrix=Matrix.Identity(4), calc_uvs=False)
    verts = res.get('verts', [])
    for v in verts:
        x, y, z = v.co.x, v.co.y, v.co.z
        if z < 0:
            z *= 0.25
        else:
            z *= 1.0 + 0.5 * max(0.0, 1.0 - (x * x + y * y))
        j = 1.0 + random.uniform(-0.1, 0.1)
        v.co = M @ Vector((x * j, y * j, z * j))
    fs = set()
    for v in verts:
        for f in v.link_faces:
            fs.add(f)
    for f in fs:
        f.material_index = MAT_WART
        f.smooth = True

# ---------------------------------------------------------------------------
# Finalize mesh / object
# ---------------------------------------------------------------------------
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
mesh = bpy.data.meshes.new("Amanita_muscaria_mesh")
bm.to_mesh(mesh)
bm.free()
mesh.update()

obj = bpy.data.objects.new("Amanita_muscaria", mesh)
bpy.context.scene.collection.objects.link(obj)
try:
    bpy.ops.object.select_all(action='DESELECT')
except Exception:
    pass
try:
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
except Exception:
    pass

# ---------------------------------------------------------------------------
# Per-vertex colours
# ---------------------------------------------------------------------------
use_attr = False
try:
    nverts = len(mesh.vertices)
    vmat = [MAT_STIPE] * nverts
    for poly in mesh.polygons:
        for vi in poly.vertices:
            vmat[vi] = poly.material_index
    cols = [0.0] * (nverts * 4)
    for vi, v in enumerate(mesh.vertices):
        x, y, z = v.co
        r = math.hypot(x, y)
        t = math.atan2(y, x)
        m = vmat[vi]
        if m == MAT_CAP:
            u = r / R_CAP
            if r < R_UNDER - 0.0003 and z < z_under(r) + wobble(r, t) + 0.002:
                c = (0.93, 0.90, 0.82)
            else:
                if u < 0.5:
                    c = lerp3((0.45, 0.012, 0.008), (0.72, 0.03, 0.012), u / 0.5)
                else:
                    c = lerp3((0.72, 0.03, 0.012), (0.85, 0.22, 0.03), (u - 0.5) / 0.5)
                s = striation(min(u, 1.0), t)
                c = lerp3(c, (0.95, 0.45, 0.10), 0.3 * s)
        elif m == MAT_WART:
            c = lerp3((0.92, 0.88, 0.72), (0.95, 0.93, 0.85), clamp(r / R_CAP, 0, 1))
        elif m == MAT_GILL:
            c = (0.95, 0.93, 0.86)
        elif m == MAT_RING:
            f = clamp((ZA - z) / 0.018, 0.0, 1.0)
            c = lerp3((0.95, 0.93, 0.87), (0.90, 0.75, 0.30), f ** 3)
        elif m == MAT_VOLVA:
            c = (0.90, 0.86, 0.72)
        else:
            if z < 0.045:
                c = (0.90, 0.86, 0.76)
            else:
                c = lerp3((0.92, 0.89, 0.80), (0.95, 0.93, 0.87), clamp((z - 0.045) / 0.1, 0, 1))
        cols[vi * 4:vi * 4 + 4] = [c[0], c[1], c[2], 1.0]
    attr = mesh.color_attributes.new(name="Col", type='FLOAT_COLOR', domain='POINT')
    attr.data.foreach_set("color", cols)
    use_attr = True
except Exception:
    use_attr = False

# ---------------------------------------------------------------------------
# Materials (minimal, defensive)
# ---------------------------------------------------------------------------
def make_mat(name, color, rough, sss=0.0, coat=0.0):
    m = bpy.data.materials.new(name)
    try:
        m.diffuse_color = (color[0], color[1], color[2], 1.0)
    except Exception:
        pass
    try:
        m.use_nodes = True
        nt = m.node_tree
        bsdf = nt.nodes.get("Principled BSDF")
        if bsdf is None:
            for nd in nt.nodes:
                if nd.type == 'BSDF_PRINCIPLED':
                    bsdf = nd
                    break
        if bsdf is None:
            return m
        try:
            bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
        except Exception:
            pass
        try:
            bsdf.inputs["Roughness"].default_value = float(rough)
        except Exception:
            pass
        if sss > 0:
            try:
                bsdf.inputs["Subsurface Weight"].default_value = float(sss)
            except Exception:
                pass
            try:
                bsdf.inputs["Subsurface Radius"].default_value = (1.0, 0.8, 0.6)
            except Exception:
                pass
            try:
                bsdf.inputs["Subsurface Scale"].default_value = 0.003
            except Exception:
                pass
        if coat > 0:
            try:
                bsdf.inputs["Coat Weight"].default_value = float(coat)
            except Exception:
                pass
            try:
                bsdf.inputs["Coat Roughness"].default_value = 0.25
            except Exception:
                pass
        if use_attr:
            try:
                an = nt.nodes.new("ShaderNodeAttribute")
                an.attribute_name = "Col"
                an.location = (-400, 200)
                nt.links.new(an.outputs["Color"], bsdf.inputs["Base Color"])
            except Exception:
                pass
    except Exception:
        pass
    return m


mats = [
    make_mat("Amanita_Cap", (0.75, 0.04, 0.015), 0.35, sss=0.05, coat=0.3),
    make_mat("Amanita_Warts", (0.93, 0.90, 0.78), 0.85),
    make_mat("Amanita_Gills", (0.95, 0.93, 0.86), 0.6, sss=0.15),
    make_mat("Amanita_Stipe", (0.94, 0.91, 0.84), 0.6, sss=0.1),
    make_mat("Amanita_Annulus", (0.94, 0.91, 0.80), 0.55, sss=0.15),
    make_mat("Amanita_Volva", (0.90, 0.86, 0.72), 0.8),
]
for m in mats:
    mesh.materials.append(m)

mesh.update()