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

# ------------------------------------------------------------
# Setup: remove default objects so only the specimen remains
# ------------------------------------------------------------
for _ob in list(bpy.data.objects):
    try:
        bpy.data.objects.remove(_ob, do_unlink=True)
    except Exception:
        pass

COLL = bpy.context.scene.collection
PARTS = []


# ------------------------------------------------------------
# Materials (minimal, defensive)
# ------------------------------------------------------------
def make_mat(name, color, rough=0.5, metallic=0.0, sheen=0.0):
    mat = bpy.data.materials.get(name)
    if mat is None:
        mat = bpy.data.materials.new(name)
    try:
        mat.diffuse_color = (color[0], color[1], color[2], 1.0)
    except Exception:
        pass
    bsdf = None
    try:
        mat.use_nodes = True
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
    except Exception:
        bsdf = None
    if bsdf is not None:
        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
        try:
            bsdf.inputs["Metallic"].default_value = float(metallic)
        except Exception:
            pass
        try:
            if sheen > 0.0:
                bsdf.inputs["Sheen Weight"].default_value = float(sheen)
        except Exception:
            pass
    return mat


M_HEAD = make_mat("Mallard_IridescentGreen", (0.01, 0.20, 0.05), 0.3, 0.35)
M_RING = make_mat("Mallard_WhiteRing", (0.92, 0.92, 0.90), 0.6)
M_BREAST = make_mat("Mallard_ChestnutBreast", (0.28, 0.10, 0.04), 0.6, 0.0, 0.3)
M_FLANK = make_mat("Mallard_GreyFlank", (0.56, 0.56, 0.53), 0.7, 0.0, 0.3)
M_BACK = make_mat("Mallard_GreyBrownBack", (0.30, 0.27, 0.22), 0.7, 0.0, 0.3)
M_BLACK = make_mat("Mallard_BlackRump", (0.02, 0.02, 0.025), 0.4, 0.1)
M_TAIL = make_mat("Mallard_WhiteTail", (0.82, 0.82, 0.80), 0.6)
M_TAILC = make_mat("Mallard_GreyTailCentral", (0.45, 0.44, 0.42), 0.6)
M_WING = make_mat("Mallard_WingCoverts", (0.34, 0.31, 0.27), 0.7)
M_PRIM = make_mat("Mallard_Primaries", (0.19, 0.17, 0.15), 0.6)
M_SPEC = make_mat("Mallard_Speculum", (0.08, 0.07, 0.48), 0.25, 0.45)
M_BILL = make_mat("Mallard_Bill", (0.72, 0.66, 0.12), 0.4)
M_NAIL = make_mat("Mallard_BillNail", (0.05, 0.05, 0.04), 0.4)
M_LEG = make_mat("Mallard_OrangeLeg", (0.95, 0.38, 0.04), 0.45)
M_CLAW = make_mat("Mallard_Claw", (0.10, 0.08, 0.07), 0.35)
M_EYE = make_mat("Mallard_Iris", (0.12, 0.05, 0.02), 0.1)
M_PUPIL = make_mat("Mallard_Pupil", (0.005, 0.005, 0.005), 0.05)


# ------------------------------------------------------------
# Geometry helpers
# ------------------------------------------------------------
def finish(name, bm, mat, matrix=None, smooth=True):
    me = bpy.data.meshes.new(name + "_mesh")
    bm.to_mesh(me)
    bm.free()
    if smooth:
        for p in me.polygons:
            p.use_smooth = True
    me.materials.append(mat)
    ob = bpy.data.objects.new(name, me)
    COLL.objects.link(ob)
    if matrix is not None:
        ob.matrix_world = matrix
    PARTS.append(ob)
    return ob


def rot_matrix(loc, rot):
    return Matrix.Translation(Vector(loc)) @ Euler(rot, 'XYZ').to_matrix().to_4x4()


def ellipsoid(name, scale, mat, loc=(0, 0, 0), rot=(0, 0, 0), matrix=None,
              deform=None, segs=(24, 16)):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=segs[0], v_segments=segs[1], radius=1.0)
    sx, sy, sz = scale
    for v in bm.verts:
        x, y, z = v.co.x, v.co.y, v.co.z
        if deform is not None:
            x, y, z = deform(x, y, z)
        v.co = Vector((x * sx, y * sy, z * sz))
    if matrix is None:
        matrix = rot_matrix(loc, rot)
    return finish(name, bm, mat, matrix)


def tube(name, pts, radii, mat, segs=12, cap_start=True, cap_end=True):
    pts = [Vector(p) for p in pts]
    n_pts = len(pts)
    bm = bmesh.new()
    rings = []
    prev_n = None
    angles = [2.0 * math.pi * k / segs for k in range(segs)]
    for i, p in enumerate(pts):
        if i == 0:
            t = pts[1] - pts[0]
        elif i == n_pts - 1:
            t = pts[-1] - pts[-2]
        else:
            t = pts[i + 1] - pts[i - 1]
        if t.length < 1e-9:
            t = Vector((1, 0, 0))
        t.normalize()
        if prev_n is None:
            ref = Vector((0, 0, 1)) if abs(t.z) < 0.9 else Vector((1, 0, 0))
            n = ref - t * ref.dot(t)
        else:
            n = prev_n - t * prev_n.dot(t)
            if n.length < 1e-9:
                ref = Vector((0, 0, 1)) if abs(t.z) < 0.9 else Vector((1, 0, 0))
                n = ref - t * ref.dot(t)
        n.normalize()
        b = t.cross(n)
        prev_n = n.copy()
        r = radii[i]
        if isinstance(r, (tuple, list)):
            rn, rb = float(r[0]), float(r[1])
        else:
            rn = rb = float(r)
        ring = [bm.verts.new(p + n * (math.cos(a) * rn) + b * (math.sin(a) * rb)) for a in angles]
        rings.append(ring)
    for i in range(n_pts - 1):
        for k in range(segs):
            k2 = (k + 1) % segs
            bm.faces.new((rings[i][k], rings[i][k2], rings[i + 1][k2], rings[i + 1][k]))
    if cap_start:
        c = bm.verts.new(pts[0])
        for k in range(segs):
            k2 = (k + 1) % segs
            bm.faces.new((c, rings[0][k2], rings[0][k]))
    if cap_end:
        c = bm.verts.new(pts[-1])
        for k in range(segs):
            k2 = (k + 1) % segs
            bm.faces.new((c, rings[-1][k], rings[-1][k2]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    return finish(name, bm, mat, Matrix.Identity(4))


def web(name, path_a, path_b, mat, q=5, thick=0.0012, scallop=0.005):
    m = len(path_a)
    bm = bmesh.new()
    top = []
    bot = []
    half = Vector((0, 0, thick * 0.5))
    for j in range(m):
        A = Vector(path_a[j])
        B = Vector(path_b[j])
        if j > 0:
            dirj = (A - Vector(path_a[j - 1])) + (B - Vector(path_b[j - 1]))
        else:
            dirj = (Vector(path_a[1]) - A) + (Vector(path_b[1]) - B)
        if dirj.length > 1e-9:
            dirj.normalize()
        rt = []
        rb = []
        for k in range(q + 1):
            f = k / q
            P = A.lerp(B, f)
            if j == m - 1:
                P = P - dirj * (scallop * math.sin(math.pi * f))
            rt.append(bm.verts.new(P + half))
            rb.append(bm.verts.new(P - half))
        top.append(rt)
        bot.append(rb)
    for j in range(m - 1):
        for k in range(q):
            bm.faces.new((top[j][k], top[j][k + 1], top[j + 1][k + 1], top[j + 1][k]))
            bm.faces.new((bot[j][k], bot[j + 1][k], bot[j + 1][k + 1], bot[j][k + 1]))
    perim = []
    for j in range(m):
        perim.append((j, 0))
    for k in range(1, q + 1):
        perim.append((m - 1, k))
    for j in range(m - 2, -1, -1):
        perim.append((j, q))
    for k in range(q - 1, 0, -1):
        perim.append((0, k))
    for i in range(len(perim)):
        a = perim[i]
        b = perim[(i + 1) % len(perim)]
        try:
            bm.faces.new((top[a[0]][a[1]], top[b[0]][b[1]], bot[b[0]][b[1]], bot[a[0]][a[1]]))
        except Exception:
            pass
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    return finish(name, bm, mat, Matrix.Identity(4), smooth=True)


def linspace(a, b, n):
    if n == 1:
        return [a]
    return [a + (b - a) * i / (n - 1) for i in range(n)]


# ------------------------------------------------------------
# BODY (torso) - drake mallard, X forward, Z up, metres
# ------------------------------------------------------------
def body_def(x, y, z):
    if x < 0:
        t = -x
        y *= 1.0 - 0.38 * t ** 1.5
        z = z * (1.0 - 0.28 * t) + 0.22 * t * t
    else:
        z *= 1.0 + 0.05 * x
    if z < 0:
        z *= 0.92
    return x, y, z


ellipsoid("Body_Torso", (0.20, 0.112, 0.09), M_FLANK, loc=(0, 0, 0.22),
          deform=body_def, segs=(40, 24))

# Chestnut breast
ellipsoid("Breast_Chestnut", (0.09, 0.094, 0.083), M_BREAST, loc=(0.128, 0, 0.225),
          segs=(32, 20))

# Grey-brown mantle / scapulars over the back
ellipsoid("Back_Mantle", (0.15, 0.075, 0.045), M_BACK, loc=(-0.01, 0, 0.27),
          deform=lambda x, y, z: (x, y * (1.0 - 0.25 * max(0.0, -x)), z), segs=(32, 16))

# Black rump & uppertail coverts
ellipsoid("Rump_Black", (0.05, 0.048, 0.022), M_BLACK, loc=(-0.155, 0, 0.262))
# Black undertail coverts
ellipsoid("Undertail_Coverts", (0.045, 0.05, 0.03), M_BLACK, loc=(-0.165, 0, 0.225))

# ------------------------------------------------------------
# NECK with white collar ring
# ------------------------------------------------------------
tube("Neck_Lower",
     [(0.135, 0, 0.245), (0.165, 0, 0.28), (0.19, 0, 0.315)],
     [0.052, 0.044, 0.038], M_BREAST, segs=20)
tube("Neck_WhiteRing",
     [(0.186, 0, 0.309), (0.197, 0, 0.327)],
     [0.0405, 0.0385], M_RING, segs=20)
tube("Neck_Upper",
     [(0.194, 0, 0.322), (0.21, 0, 0.35), (0.225, 0, 0.375), (0.24, 0, 0.39)],
     [0.036, 0.033, 0.033, 0.036], M_HEAD, segs=20)

# ------------------------------------------------------------
# HEAD
# ------------------------------------------------------------
def head_def(x, y, z):
    if x > 0 and z > 0:
        z *= 1.0 - 0.18 * x
    if x < 0:
        z *= 1.0 + 0.05 * (-x)
    return x, y, z


ellipsoid("Head", (0.058, 0.041, 0.046), M_HEAD, loc=(0.255, 0, 0.40),
          deform=head_def, segs=(32, 20))

# Eyes (iris + pupil)
for s in (1, -1):
    ellipsoid("Eye_Iris_%s" % ("L" if s > 0 else "R"), (0.0062, 0.0045, 0.0062), M_EYE,
              loc=(0.275, s * 0.0345, 0.41), segs=(16, 12))
    ellipsoid("Eye_Pupil_%s" % ("L" if s > 0 else "R"), (0.003, 0.0012, 0.003), M_PUPIL,
              loc=(0.2755, s * 0.0388, 0.41), segs=(12, 8))

# ------------------------------------------------------------
# BILL: upper mandible, lower mandible, nail, nostrils
# ------------------------------------------------------------
BILL_PITCH = math.radians(12)


def upper_bill_def(x, y, z):
    f = (x + 1.0) * 0.5
    y *= 0.78 + 0.30 * f
    if z > 0:
        z *= 1.0 - 0.25 * f
    return x, y, z


def lower_bill_def(x, y, z):
    f = (x + 1.0) * 0.5
    y *= 0.80 + 0.25 * f
    return x, y, z


ellipsoid("Bill_UpperMandible", (0.036, 0.021, 0.0085), M_BILL, loc=(0.332, 0, 0.387),
          rot=(0, BILL_PITCH, 0), deform=upper_bill_def, segs=(28, 14))
ellipsoid("Bill_LowerMandible", (0.032, 0.018, 0.005), M_BILL, loc=(0.328, 0, 0.379),
          rot=(0, BILL_PITCH, 0), deform=lower_bill_def, segs=(24, 12))
ellipsoid("Bill_Nail", (0.007, 0.009, 0.0035), M_NAIL, loc=(0.3635, 0, 0.3835),
          rot=(0, BILL_PITCH + math.radians(10), 0), segs=(16, 10))
for s in (1, -1):
    ellipsoid("Bill_Nostril_%s" % ("L" if s > 0 else "R"), (0.0045, 0.0018, 0.0015), M_NAIL,
              loc=(0.315, s * 0.0065, 0.3968), rot=(0, BILL_PITCH, 0), segs=(12, 8))

# ------------------------------------------------------------
# WINGS (folded): coverts, speculum with white bars, tertials, primaries
# ------------------------------------------------------------
def wing_def(x, y, z):
    if x < 0:
        t = -x
        z *= 1.0 - 0.35 * t
        y *= 1.0 - 0.3 * t
    return x, y, z


def feather_def(x, y, z):
    if x > 0:
        y *= 1.0 - 0.35 * x
    return x, y, z


for s in (1, -1):
    side = "L" if s > 0 else "R"
    Mw = rot_matrix((-0.03, s * 0.097, 0.258), (s * 0.25, 0.0, s * 0.08))

    ellipsoid("Wing_Coverts_" + side, (0.15, 0.017, 0.05), M_WING, matrix=Mw,
              deform=wing_def, segs=(32, 16))

    # Speculum (iridescent blue) framed by white bars
    ellipsoid("Wing_Speculum_" + side, (0.042, 0.004, 0.012), M_SPEC,
              matrix=Mw @ Matrix.Translation(Vector((0.02, s * 0.0165, -0.016))), segs=(20, 10))
    ellipsoid("Wing_SpecBarUpper_" + side, (0.044, 0.0035, 0.0028), M_RING,
              matrix=Mw @ Matrix.Translation(Vector((0.022, s * 0.0168, -0.0015))), segs=(20, 8))
    ellipsoid("Wing_SpecBarLower_" + side, (0.044, 0.0035, 0.0028), M_RING,
              matrix=Mw @ Matrix.Translation(Vector((0.018, s * 0.0155, -0.0305))), segs=(20, 8))

    # Tertials
    for i in range(2):
        ellipsoid("Wing_Tertial_%s_%d" % (side, i), (0.06, 0.003, 0.014), M_BACK,
                  matrix=Mw @ Matrix.Translation(Vector((-0.06 - i * 0.012, s * (0.012 - i * 0.003),
                                                         0.034 - i * 0.008))),
                  deform=feather_def, segs=(16, 8))

    # Primaries, folded and stacked, extending over the tail
    for i in range(6):
        local = Vector((-0.13 - i * 0.006, s * (0.006 - i * 0.0022), 0.018 - i * 0.005))
        yaw = Matrix.Rotation(-s * math.radians(2.0 + i * 0.8), 4, 'Z')
        ellipsoid("Wing_Primary_%s_%d" % (side, i), (0.08 - i * 0.003, 0.0028, 0.013), M_PRIM,
                  matrix=Mw @ Matrix.Translation(local) @ yaw,
                  deform=lambda x, y, z: (x, y, z * (1.0 - 0.45 * max(0.0, -x))), segs=(16, 8))

# ------------------------------------------------------------
# TAIL: fan of rectrices + black curled drake feathers
# ------------------------------------------------------------
TAIL_ROOT = Vector((-0.185, 0.0, 0.248))
N_RECT = 10
for i in range(N_RECT):
    a = math.radians(-30.0 + i * 60.0 / (N_RECT - 1))
    d = Vector((-math.cos(a), math.sin(a), math.tan(math.radians(12))))
    d.normalize()
    stack = 0.0012 * (4.5 - abs(i - 4.5))
    center = TAIL_ROOT + d * 0.038 + Vector((0, 0, stack))
    q = d.to_track_quat('X', 'Z')
    mat = M_TAILC if 3 <= i <= 6 else M_TAIL
    ellipsoid("Tail_Rectrix_%d" % i, (0.042, 0.011, 0.0022), mat,
              matrix=Matrix.Translation(center) @ q.to_matrix().to_4x4(),
              deform=feather_def, segs=(16, 8))

# Drake curls (curled central uppertail coverts)
for ci, yoff in enumerate((-0.006, -0.002, 0.002, 0.006)):
    R0 = 0.018 - 0.0012 * abs(ci - 1.5)
    cx = -0.175 + 0.002 * abs(ci - 1.5)
    base_z = 0.268
    cz = base_z + R0
    th_max = 1.45 * math.pi
    pts = []
    rads = []
    n = 18
    for k in range(n):
        th = th_max * k / (n - 1)
        R = R0 * (1.0 - 0.4 * th / th_max)
        pts.append((cx - R * math.sin(th), yoff, cz - R * math.cos(th)))
        f = k / (n - 1)
        rads.append((0.0015 * (1.0 - 0.5 * f) + 0.0003, 0.004 * (1.0 - 0.6 * f) + 0.0005))
    tube("Tail_DrakeCurl_%d" % ci, pts, rads, M_BLACK, segs=8)

# ------------------------------------------------------------
# LEGS & WEBBED FEET
# ------------------------------------------------------------
def toe_point(F, yaw, L, t, z_end=0.0042):
    d = Vector((math.cos(yaw), math.sin(yaw), 0.0))
    p = F + d * (L * t)
    p.z = F.z * (1.0 - t) + z_end * t + 0.0018 * math.sin(math.pi * t)
    return p


for s in (1, -1):
    side = "L" if s > 0 else "R"
    knee = Vector((-0.022, s * 0.052, 0.165))
    ankle = Vector((-0.05, s * 0.06, 0.092))
    F = Vector((-0.028, s * 0.066, 0.013))

    # Feathered thigh (upper tibiotarsus) emerging from belly
    ellipsoid("Leg_Thigh_" + side, (0.028, 0.02, 0.024), M_FLANK,
              loc=(-0.03, s * 0.055, 0.158), segs=(16, 10))
    # Exposed lower tibiotarsus
    tube("Leg_Tibiotarsus_" + side, [knee, knee.lerp(ankle, 0.5), ankle],
         [0.011, 0.0085, 0.0072], M_LEG, segs=12)
    # Intertarsal (ankle) joint
    ellipsoid("Leg_AnkleJoint_" + side, (0.0078, 0.0072, 0.0078), M_LEG,
              loc=tuple(ankle), segs=(12, 8))
    # Tarsometatarsus (laterally compressed)
    tube("Leg_Tarsometatarsus_" + side,
         [ankle, ankle.lerp(F, 0.5), F],
         [(0.0068, 0.0048), (0.0064, 0.0045), (0.006, 0.0043)], M_LEG, segs=12)
    # Foot pad at base of toes
    ellipsoid("Foot_Pad_" + side, (0.0075, 0.0068, 0.005), M_LEG, loc=tuple(F), segs=(12, 8))

    toes = {
        "II_inner": (-s * math.radians(22), 0.045),
        "III_middle": (s * math.radians(3), 0.058),
        "IV_outer": (s * math.radians(28), 0.054),
    }
    for tname, (yaw, L) in toes.items():
        tpts = [toe_point(F, yaw, L, t) for t in linspace(0.0, 1.0, 7)]
        trad = [(0.0035 - 0.0012 * f, 0.0042 - 0.0016 * f) for f in linspace(0.0, 1.0, 7)]
        tube("Foot_Toe_%s_%s" % (tname, side), tpts, trad, M_LEG, segs=10)
        # Claw
        d = Vector((math.cos(yaw), math.sin(yaw), 0.0))
        tip = tpts[-1]
        cpts = [tip - d * 0.001,
                tip + d * 0.003 + Vector((0, 0, -0.0005)),
                tip + d * 0.0065 + Vector((0, 0, -0.0022))]
        tube("Foot_Claw_%s_%s" % (tname, side), cpts,
             [(0.0022, 0.0020), (0.0015, 0.0012), (0.0003, 0.0003)], M_CLAW, segs=8)

    # Hallux (hind toe, elevated, with small lobe)
    hy = -s * 0.35
    hd = Vector((-1.0, hy, 0.0)).normalized()
    hbase = F + Vector((-0.004, 0, 0.004))
    hpts = [hbase, hbase + hd * 0.006 + Vector((0, 0, -0.001)),
            hbase + hd * 0.012 + Vector((0, 0, -0.003))]
    tube("Foot_Hallux_" + side, hpts, [(0.0028, 0.003), (0.0024, 0.0028), (0.0018, 0.0018)],
         M_LEG, segs=8)
    ellipsoid("Foot_HalluxLobe_" + side, (0.005, 0.003, 0.0012), M_LEG,
              loc=tuple(hbase + hd * 0.008 + Vector((0, 0, -0.003))), segs=(10, 6))
    hc = hpts[-1]
    tube("Foot_HalluxClaw_" + side, [hc, hc + hd * 0.004 + Vector((0, 0, -0.0015))],
         [0.0014, 0.0003], M_CLAW, segs=6)

    # Interdigital webbing (II-III and III-IV), fully webbed to near toe tips
    yaw2, L2 = toes["II_inner"]
    yaw3, L3 = toes["III_middle"]
    yaw4, L4 = toes["IV_outer"]
    ts = linspace(0.12, 0.93, 7)
    p2 = [toe_point(F, yaw2, L2, t) for t in ts]
    p3 = [toe_point(F, yaw3, L3, t) for t in ts]
    p4 = [toe_point(F, yaw4, L4, t) for t in ts]
    web("Foot_Web_Inner_" + side, p2, p3, M_LEG)
    web("Foot_Web_Outer_" + side, p3, p4, M_LEG)

# ------------------------------------------------------------
# Join all parts into one specimen object
# ------------------------------------------------------------
joined = False
try:
    bpy.ops.object.select_all(action='DESELECT')
    valid = [o for o in PARTS if o is not None and o.name in bpy.data.objects]
    for o in valid:
        o.select_set(True)
    if valid:
        bpy.context.view_layer.objects.active = valid[0]
        bpy.ops.object.join()
        duck = bpy.context.view_layer.objects.active
        if duck is not None:
            duck.name = "Mallard_Duck_Anas_platyrhynchos"
            if duck.data is not None:
                duck.data.name = "Mallard_Duck_mesh"
            joined = True
except Exception:
    joined = False

if not joined:
    try:
        root = bpy.data.objects.new("Mallard_Duck_Anas_platyrhynchos", None)
        COLL.objects.link(root)
        for o in list(bpy.data.objects):
            if o is not root and o.type == 'MESH' and o.parent is None:
                mw = o.matrix_world.copy()
                o.parent = root
                o.matrix_world = mw
    except Exception:
        pass

try:
    bpy.ops.object.select_all(action='DESELECT')
except Exception:
    pass