import bpy
import bmesh
import math
import random

# ------------------------------------------------------------------
# Clean up any default mesh objects so only the specimen remains
# ------------------------------------------------------------------
for _o in list(bpy.data.objects):
    if _o.type == 'MESH':
        try:
            bpy.data.objects.remove(_o, do_unlink=True)
        except Exception:
            pass

random.seed(7)

R = 1.0                # cap radius
TAU = math.pi * 2.0


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def make_mat(name, color, rough=0.55, sss=0.0):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        try:
            bsdf.inputs["Base Color"].default_value = color
        except Exception:
            pass
        try:
            bsdf.inputs["Roughness"].default_value = rough
        except Exception:
            pass
        if sss > 0.0:
            for sname in ("Subsurface Weight", "Subsurface"):
                try:
                    bsdf.inputs[sname].default_value = sss
                    break
                except Exception:
                    continue
            for cname in ("Subsurface Color",):
                try:
                    bsdf.inputs[cname].default_value = color
                except Exception:
                    pass
    return mat


def bm_to_obj(bm, name, smooth=True):
    me = bpy.data.meshes.new(name)
    bm.normal_update()
    bm.to_mesh(me)
    bm.free()
    obj = bpy.data.objects.new(name, me)
    bpy.context.collection.objects.link(obj)
    if smooth:
        for p in obj.data.polygons:
            p.use_smooth = True
    return obj


def spin_profile(pts, name, steps=72, smooth=True):
    bm = bmesh.new()
    verts = [bm.verts.new((p[0], 0.0, p[1])) for p in pts]
    for i in range(len(verts) - 1):
        bm.edges.new((verts[i], verts[i + 1]))
    geom = bm.verts[:] + bm.edges[:]
    bmesh.ops.spin(bm, geom=geom, cent=(0.0, 0.0, 0.0),
                   axis=(0.0, 0.0, 1.0), dvec=(0.0, 0.0, 0.0),
                   angle=TAU, steps=steps, use_duplicate=False)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm_to_obj(bm, name, smooth)


def apply_mods(obj):
    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    for m in list(obj.modifiers):
        try:
            bpy.ops.object.modifier_apply(modifier=m.name)
        except Exception:
            pass


def cap_top_z(rn):
    rn = min(max(rn, 0.0), 1.0)
    return 0.62 + 0.45 * (1.0 - rn * rn) ** 0.7


def cap_under_z(r):
    return 0.60 + 0.06 * (1.0 - r / R)


# ------------------------------------------------------------------
# Materials
# ------------------------------------------------------------------
mat_cap = make_mat("CapRed", (0.72, 0.04, 0.02, 1.0), rough=0.42, sss=0.12)
mat_veil = make_mat("Veil", (0.95, 0.93, 0.87, 1.0), rough=0.6, sss=0.05)
mat_stem = make_mat("Stem", (0.96, 0.94, 0.88, 1.0), rough=0.6, sss=0.15)
mat_gill = make_mat("Gills", (0.93, 0.90, 0.81, 1.0), rough=0.65, sss=0.1)
mat_ring = make_mat("Ring", (0.95, 0.93, 0.86, 1.0), rough=0.6, sss=0.05)


# ------------------------------------------------------------------
# CAP (pileus) - solid dome with rim, built by lathe
# ------------------------------------------------------------------
top = []
NT = 28
for i in range(NT + 1):
    rn = i / NT
    top.append((rn * R, cap_top_z(rn)))

under = []
NU = 16
for i in range(NU + 1):
    r = R * (1.0 - i / NU)      # R -> 0
    under.append((r, cap_under_z(r)))

cap_profile = top + under
cap = spin_profile(cap_profile, "Cap", steps=80)
cap.data.materials.append(mat_cap)


# ------------------------------------------------------------------
# GILLS (lamellae) - radial plates under the cap
# ------------------------------------------------------------------
def build_gills():
    bm = bmesh.new()
    NG = 74
    r_in = 0.17
    r_out = 0.95 * R
    for i in range(NG):
        a = TAU * i / NG
        ca, sa = math.cos(a), math.sin(a)
        zui = cap_under_z(r_in)
        zuo = cap_under_z(r_out)
        dep_in = 0.06 * (1.0 - r_in / R) + 0.02
        dep_out = 0.06 * (1.0 - r_out / R) + 0.015
        v1 = bm.verts.new((r_in * ca, r_in * sa, zui))
        v2 = bm.verts.new((r_out * ca, r_out * sa, zuo))
        v3 = bm.verts.new((r_out * ca, r_out * sa, zuo - dep_out))
        v4 = bm.verts.new((r_in * ca, r_in * sa, zui - dep_in))
        bm.faces.new((v1, v2, v3, v4))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm_to_obj(bm, "Gills", smooth=False)


gills = build_gills()
gills.data.materials.append(mat_gill)
_m = gills.modifiers.new("Solidify", 'SOLIDIFY')
_m.thickness = 0.007
_m.offset = 0.0
apply_mods(gills)


# ------------------------------------------------------------------
# STEM (stipe) with bulbous base
# ------------------------------------------------------------------
stem_profile = [
    (0.0, -0.03),
    (0.20, -0.03),
    (0.30, 0.00),
    (0.335, 0.10),
    (0.30, 0.20),
    (0.235, 0.32),
    (0.185, 0.46),
    (0.165, 0.58),
    (0.150, 0.66),
    (0.135, 0.72),
    (0.0, 0.72),
]
stem = spin_profile(stem_profile, "Stem", steps=64)
stem.data.materials.append(mat_stem)


# ------------------------------------------------------------------
# RING (annulus) - hanging skirt near top of stem
# ------------------------------------------------------------------
ring_profile = [
    (0.158, 0.55),
    (0.205, 0.535),
    (0.255, 0.485),
    (0.280, 0.415),
    (0.272, 0.395),
]
ring = spin_profile(ring_profile, "Ring", steps=64)
ring.data.materials.append(mat_ring)
_mr = ring.modifiers.new("Solidify", 'SOLIDIFY')
_mr.thickness = 0.01
_mr.offset = 0.0
apply_mods(ring)


# ------------------------------------------------------------------
# UNIVERSAL VEIL REMNANTS: warts on cap + volva scales on the bulb
# ------------------------------------------------------------------
veil_objs = []

# White warts scattered over the cap
for _ in range(48):
    rn = random.uniform(0.04, 0.9)
    r = rn * R
    ang = random.uniform(0.0, TAU)
    x = r * math.cos(ang)
    y = r * math.sin(ang)
    z = cap_top_z(rn)
    s = random.uniform(0.035, 0.06) * (1.1 - 0.5 * rn)
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=s,
                                           location=(x, y, z - s * 0.25))
    w = bpy.context.active_object
    if w is not None:
        w.scale = (1.0, 1.0, 0.45)
        veil_objs.append(w)

# Volva scales in concentric rings around the bulb base
bulb_rings = [(0.02, 0.30, 14), (0.08, 0.335, 15),
              (0.15, 0.285, 13), (0.21, 0.225, 11)]
for zc, rr, n in bulb_rings:
    off = random.uniform(0.0, TAU)
    for k in range(n):
        ang = TAU * k / n + off
        x = rr * math.cos(ang)
        y = rr * math.sin(ang)
        s = random.uniform(0.022, 0.038)
        bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=s,
                                               location=(x, y, zc))
        w = bpy.context.active_object
        if w is not None:
            nx, ny = math.cos(ang), math.sin(ang)
            w.rotation_euler = (0.0, 0.0, ang)
            w.scale = (0.6, 1.0, 0.9)
            veil_objs.append(w)

veil = None
if veil_objs:
    bpy.ops.object.select_all(action='DESELECT')
    for o in veil_objs:
        o.select_set(True)
    bpy.context.view_layer.objects.active = veil_objs[0]
    try:
        bpy.ops.object.join()
    except Exception:
        pass
    veil = bpy.context.active_object
    if veil is not None:
        veil.name = "VeilRemnants"
        veil.data.materials.append(mat_veil)
        for p in veil.data.polygons:
            p.use_smooth = True


# ------------------------------------------------------------------
# Group all parts of the single specimen under one empty
# ------------------------------------------------------------------
parts = [cap, gills, stem, ring]
if veil is not None:
    parts.append(veil)

root = bpy.data.objects.new("Amanita_muscaria", None)
bpy.context.collection.objects.link(root)
for o in parts:
    if o is not None:
        o.parent = root

bpy.ops.object.select_all(action='DESELECT')