import bpy
import bmesh
import math
from mathutils import Vector

# ---------------------------------------------------------------------------
# Utility helpers
# ---------------------------------------------------------------------------

def deselect_all():
    if bpy.context.mode != 'OBJECT':
        try:
            bpy.ops.object.mode_set(mode='OBJECT')
        except Exception:
            pass
    bpy.ops.object.select_all(action='DESELECT')


def make_material(name, base_color, roughness=0.55, metallic=0.0,
                  subsurface=0.0, subsurface_color=None):
    """Create a Principled BSDF material defensively (Blender 4.2 socket names)."""
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nt = mat.node_tree
    bsdf = None
    for n in nt.nodes:
        if n.type == 'BSDF_PRINCIPLED':
            bsdf = n
            break
    if bsdf is None:
        return mat

    def set_socket(sock_name, value):
        try:
            sock = bsdf.inputs.get(sock_name)
            if sock is None:
                return
            # Determine if scalar or vector/color
            if hasattr(sock, "default_value"):
                dv = sock.default_value
                if hasattr(dv, "__len__"):
                    # color / vector socket
                    if isinstance(value, (int, float)):
                        return
                    if len(dv) == 4 and len(value) == 3:
                        sock.default_value = (value[0], value[1], value[2], 1.0)
                    else:
                        sock.default_value = value
                else:
                    # scalar socket
                    if isinstance(value, (int, float)):
                        sock.default_value = value
        except Exception:
            pass

    set_socket("Base Color", base_color)
    set_socket("Roughness", roughness)
    set_socket("Metallic", metallic)
    # Blender 4.x renamed Subsurface -> Subsurface Weight
    if subsurface > 0.0:
        set_socket("Subsurface Weight", subsurface)
        try:
            if subsurface_color is not None:
                rad = bsdf.inputs.get("Subsurface Radius")
                if rad is not None:
                    rad.default_value = (subsurface_color[0] * 0.01,
                                         subsurface_color[1] * 0.01,
                                         subsurface_color[2] * 0.01)
        except Exception:
            pass
    return mat


def assign_material(obj, mat):
    if obj is None or obj.data is None:
        return
    try:
        obj.data.materials.clear()
        obj.data.materials.append(mat)
    except Exception:
        pass


def shade_smooth(obj):
    if obj is None or obj.data is None:
        return
    try:
        for p in obj.data.polygons:
            p.use_smooth = True
    except Exception:
        pass


def add_uv_sphere(name, location=(0, 0, 0), radius=1.0, segments=32, rings=16):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings,
                                          radius=radius, location=location)
    obj = bpy.context.active_object
    if obj is not None:
        obj.name = name
    return obj


def add_cylinder(name, location=(0, 0, 0), radius=1.0, depth=2.0, verts=24):
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=radius, depth=depth,
                                         location=location)
    obj = bpy.context.active_object
    if obj is not None:
        obj.name = name
    return obj


def add_cone(name, location=(0, 0, 0), radius1=1.0, radius2=0.0, depth=2.0, verts=24):
    bpy.ops.mesh.primitive_cone_add(vertices=verts, radius1=radius1, radius2=radius2,
                                    depth=depth, location=location)
    obj = bpy.context.active_object
    if obj is not None:
        obj.name = name
    return obj


def join_objects(objects, target_name):
    """Join a list of objects into the first one; returns the joined object."""
    objects = [o for o in objects if o is not None]
    if not objects:
        return None
    deselect_all()
    for o in objects:
        try:
            o.select_set(True)
        except Exception:
            pass
    ctx_obj = objects[0]
    bpy.context.view_layer.objects.active = ctx_obj
    if len(objects) > 1:
        try:
            bpy.ops.object.join()
        except Exception:
            pass
    result = bpy.context.active_object
    if result is not None:
        result.name = target_name
    return result


def add_subsurf(obj, levels=2):
    if obj is None:
        return
    try:
        mod = obj.modifiers.new("Subsurf", 'SUBSURF')
        mod.levels = levels
        mod.render_levels = levels
    except Exception:
        pass


# ---------------------------------------------------------------------------
# Start clean-ish: work in current scene without deleting user's saved data
# ---------------------------------------------------------------------------

deselect_all()

# ---------------------------------------------------------------------------
# Materials (male mallard breeding plumage)
# ---------------------------------------------------------------------------

mat_head   = make_material("Mallard_Head_Green", (0.02, 0.14, 0.05), roughness=0.25,
                           metallic=0.55)                       # iridescent green head
mat_collar = make_material("Mallard_White_Collar", (0.92, 0.92, 0.90), roughness=0.5)
mat_breast = make_material("Mallard_Breast", (0.30, 0.12, 0.06), roughness=0.55,
                           subsurface=0.1, subsurface_color=(0.4, 0.2, 0.1))
mat_body   = make_material("Mallard_Body_Gray", (0.55, 0.54, 0.52), roughness=0.6)
mat_flank  = make_material("Mallard_Flank", (0.62, 0.61, 0.58), roughness=0.6)
mat_tail   = make_material("Mallard_Tail_Dark", (0.05, 0.05, 0.06), roughness=0.5)
mat_wing   = make_material("Mallard_Wing", (0.42, 0.40, 0.38), roughness=0.6)
mat_spec   = make_material("Mallard_Speculum_Blue", (0.05, 0.10, 0.35), roughness=0.2,
                           metallic=0.6)                        # blue wing patch
mat_bill   = make_material("Mallard_Bill_Yellow", (0.80, 0.62, 0.10), roughness=0.35)
mat_nail   = make_material("Mallard_Bill_Nail", (0.08, 0.06, 0.04), roughness=0.4)
mat_eye    = make_material("Mallard_Eye", (0.02, 0.01, 0.01), roughness=0.1)
mat_leg    = make_material("Mallard_Leg_Orange", (0.85, 0.35, 0.05), roughness=0.4)
mat_nostril= make_material("Mallard_Nostril", (0.05, 0.04, 0.03), roughness=0.4)

# ---------------------------------------------------------------------------
# BODY (torso) - elongated ellipsoid
# ---------------------------------------------------------------------------

body = add_uv_sphere("Duck_Body", location=(0.0, 0.0, 1.0), radius=1.0,
                     segments=48, rings=32)
if body is not None:
    body.scale = (1.9, 0.95, 0.95)
    # Slight downward tilt of rear, plumper front
    deselect_all()
    body.select_set(True)
    bpy.context.view_layer.objects.active = body
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

    # Sculpt body proportions with a bmesh pass: raise back, tuck belly
    me = body.data
    bm = bmesh.new()
    bm.from_mesh(me)
    for v in bm.verts:
        x = v.co.x
        # taper rear (tail region) upward and inward
        if x < -1.2:
            t = (-x - 1.2) / 0.7
            t = max(0.0, min(1.0, t))
            v.co.z += 0.35 * t
            v.co.y *= (1.0 - 0.45 * t)
            v.co.z *= (1.0 - 0.15 * t)
        # broaden chest slightly
        if x > 1.0:
            t = (x - 1.0) / 0.9
            t = max(0.0, min(1.0, t))
            v.co.z += 0.12 * t
    bm.to_mesh(me)
    bm.free()

shade_smooth(body)
assign_material(body, mat_body)
add_subsurf(body, 2)

# ---------------------------------------------------------------------------
# BREAST cap (rich brown front)
# ---------------------------------------------------------------------------

breast = add_uv_sphere("Duck_Breast", location=(1.35, 0.0, 1.05), radius=0.72,
                       segments=32, rings=24)
if breast is not None:
    breast.scale = (0.85, 0.95, 0.95)
shade_smooth(breast)
assign_material(breast, mat_breast)
add_subsurf(breast, 2)

# ---------------------------------------------------------------------------
# NECK - curved cylinder connecting body to head
# ---------------------------------------------------------------------------

neck = add_cylinder("Duck_Neck", location=(1.75, 0.0, 1.75), radius=0.36, depth=1.2,
                    verts=32)
if neck is not None:
    neck.rotation_euler = (0.0, math.radians(-38.0), 0.0)
    deselect_all()
    neck.select_set(True)
    bpy.context.view_layer.objects.active = neck
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
shade_smooth(neck)
assign_material(neck, mat_head)   # lower neck green blends; collar added separately
add_subsurf(neck, 2)

# White collar ring
collar = add_cylinder("Duck_Collar", location=(1.9, 0.0, 1.98), radius=0.40,
                      depth=0.16, verts=32)
if collar is not None:
    collar.rotation_euler = (0.0, math.radians(-38.0), 0.0)
    deselect_all()
    collar.select_set(True)
    bpy.context.view_layer.objects.active = collar
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
shade_smooth(collar)
assign_material(collar, mat_collar)

# ---------------------------------------------------------------------------
# HEAD - iridescent green
# ---------------------------------------------------------------------------

head = add_uv_sphere("Duck_Head", location=(2.35, 0.0, 2.35), radius=0.56,
                     segments=40, rings=28)
if head is not None:
    head.scale = (1.15, 0.95, 1.0)
    deselect_all()
    head.select_set(True)
    bpy.context.view_layer.objects.active = head
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    # flatten top a touch, extend crown forward
    me = head.data
    bm = bmesh.new()
    bm.from_mesh(me)
    for v in bm.verts:
        if v.co.x > 0.3 and v.co.z > 0.0:
            v.co.z += 0.03
    bm.to_mesh(me)
    bm.free()
shade_smooth(head)
assign_material(head, mat_head)
add_subsurf(head, 2)

# ---------------------------------------------------------------------------
# BILL - flattened, spatulate
# ---------------------------------------------------------------------------

bill = add_cylinder("Duck_Bill", location=(3.05, 0.0, 2.22), radius=0.30, depth=0.72,
                    verts=32)
if bill is not None:
    bill.rotation_euler = (0.0, math.radians(84.0), 0.0)
    deselect_all()
    bill.select_set(True)
    bpy.context.view_layer.objects.active = bill
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
    # Flatten vertically and widen toward tip (spatulate)
    me = bill.data
    bm = bmesh.new()
    bm.from_mesh(me)
    minx = min(v.co.x for v in bm.verts)
    maxx = max(v.co.x for v in bm.verts)
    span = (maxx - minx) if (maxx - minx) != 0 else 1.0
    for v in bm.verts:
        t = (v.co.x - minx) / span   # 0 at base, 1 at tip
        v.co.z *= 0.42               # flatten
        v.co.y *= (0.9 + 0.35 * t)   # widen toward tip
        v.co.z *= (1.0 + 0.15 * t)
        # slight downward droop of tip
        v.co.z -= 0.05 * (t ** 2)
    bm.to_mesh(me)
    bm.free()
shade_smooth(bill)
assign_material(bill, mat_bill)

# Bill nail (small dark hook at tip)
nail = add_uv_sphere("Duck_BillNail", location=(3.40, 0.0, 2.12), radius=0.07,
                     segments=16, rings=10)
if nail is not None:
    nail.scale = (0.6, 1.2, 0.8)
shade_smooth(nail)
assign_material(nail, mat_nail)

# Nostrils (two small dark ovals near bill base)
nostrils = []
for side in (-1, 1):
    ns = add_uv_sphere("Duck_Nostril_%d" % side,
                       location=(2.92, 0.09 * side, 2.30), radius=0.035,
                       segments=12, rings=8)
    if ns is not None:
        ns.scale = (1.6, 0.7, 0.7)
    shade_smooth(ns)
    assign_material(ns, mat_nostril)
    nostrils.append(ns)

# ---------------------------------------------------------------------------
# EYES
# ---------------------------------------------------------------------------

eyes = []
for side in (-1, 1):
    eye = add_uv_sphere("Duck_Eye_%d" % side,
                        location=(2.55, 0.44 * side, 2.48), radius=0.10,
                        segments=20, rings=14)
    shade_smooth(eye)
    assign_material(eye, mat_eye)
    eyes.append(eye)

# ---------------------------------------------------------------------------
# WINGS - folded along the body sides
# ---------------------------------------------------------------------------

wings = []
speculums = []
for side in (-1, 1):
    wing = add_uv_sphere("Duck_Wing_%d" % side,
                         location=(0.0, 0.78 * side, 1.25), radius=0.8,
                         segments=32, rings=20)
    if wing is not None:
        wing.scale = (1.7, 0.22, 0.62)
        deselect_all()
        wing.select_set(True)
        bpy.context.view_layer.objects.active = wing
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
        # Taper rear of the folded wing to a point (primary feathers)
        me = wing.data
        bm = bmesh.new()
        bm.from_mesh(me)
        for v in bm.verts:
            if v.co.x < -0.6:
                t = (-v.co.x - 0.6) / 0.9
                t = max(0.0, min(1.0, t))
                v.co.z *= (1.0 - 0.6 * t)
                v.co.y *= (1.0 - 0.3 * t)
                v.co.z -= 0.12 * t
        bm.to_mesh(me)
        bm.free()
    shade_smooth(wing)
    assign_material(wing, mat_wing)
    add_subsurf(wing, 2)
    wings.append(wing)

    # Speculum (blue patch) on each wing
    spec = add_uv_sphere("Duck_Speculum_%d" % side,
                         location=(-0.35, 0.88 * side, 1.30), radius=0.30,
                         segments=20, rings=12)
    if spec is not None:
        spec.scale = (1.3, 0.18, 0.35)
    shade_smooth(spec)
    assign_material(spec, mat_spec)
    speculums.append(spec)

# ---------------------------------------------------------------------------
# TAIL - fan of feathers + upcurled central drake feathers
# ---------------------------------------------------------------------------

tail_parts = []
tail_base = add_cone("Duck_Tail", location=(-2.05, 0.0, 1.45), radius1=0.45,
                     radius2=0.05, depth=1.1, verts=24)
if tail_base is not None:
    tail_base.rotation_euler = (0.0, math.radians(-105.0), 0.0)
    deselect_all()
    tail_base.select_set(True)
    bpy.context.view_layer.objects.active = tail_base
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
    tail_base.scale = (1.0, 1.9, 0.35)
    deselect_all()
    tail_base.select_set(True)
    bpy.context.view_layer.objects.active = tail_base
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
shade_smooth(tail_base)
assign_material(tail_base, mat_body)
tail_parts.append(tail_base)

# Curled black drake tail feathers (characteristic of male mallard)
for i, off in enumerate((-0.08, 0.0, 0.08)):
    curl = add_cone("Duck_TailCurl_%d" % i,
                    location=(-2.35, off, 1.70), radius1=0.10, radius2=0.02,
                    depth=0.6, verts=12)
    if curl is not None:
        curl.rotation_euler = (0.0, math.radians(-150.0), 0.0)
        deselect_all()
        curl.select_set(True)
        bpy.context.view_layer.objects.active = curl
        bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
    shade_smooth(curl)
    assign_material(curl, mat_tail)
    tail_parts.append(curl)

# Dark undertail coverts
undertail = add_uv_sphere("Duck_Undertail", location=(-1.75, 0.0, 1.15), radius=0.35,
                          segments=20, rings=14)
if undertail is not None:
    undertail.scale = (1.4, 0.9, 0.7)
shade_smooth(undertail)
assign_material(undertail, mat_tail)
tail_parts.append(undertail)

# ---------------------------------------------------------------------------
# LEGS & FEET (webbed) - tucked below body
# ---------------------------------------------------------------------------

leg_objs = []
for side in (-1, 1):
    # Thigh/tarsus (visible lower leg)
    tarsus = add_cylinder("Duck_Tarsus_%d" % side,
                          location=(-0.15, 0.32 * side, 0.42), radius=0.10,
                          depth=0.5, verts=16)
    shade_smooth(tarsus)
    assign_material(tarsus, mat_leg)
    leg_objs.append(tarsus)

    # Ankle joint
    ankle = add_uv_sphere("Duck_Ankle_%d" % side,
                          location=(-0.15, 0.32 * side, 0.18), radius=0.10,
                          segments=14, rings=10)
    shade_smooth(ankle)
    assign_material(ankle, mat_leg)
    leg_objs.append(ankle)

    # Foot base (webbed) - flattened triangle/fan
    foot = add_cone("Duck_Foot_%d" % side,
                    location=(0.12, 0.32 * side, 0.06), radius1=0.05, radius2=0.34,
                    depth=0.55, verts=3)
    if foot is not None:
        foot.rotation_euler = (0.0, math.radians(90.0), 0.0)
        deselect_all()
        foot.select_set(True)
        bpy.context.view_layer.objects.active = foot
        bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
        foot.scale = (1.0, 1.15, 0.12)
        deselect_all()
        foot.select_set(True)
        bpy.context.view_layer.objects.active = foot
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    shade_smooth(foot)
    assign_material(foot, mat_leg)
    leg_objs.append(foot)

    # Three front toes ridges on each webbed foot
    for t_i, ty in enumerate((-0.18, 0.0, 0.18)):
        toe = add_cylinder("Duck_Toe_%d_%d" % (side, t_i),
                           location=(0.34, 0.32 * side + ty * 0.6, 0.03),
                           radius=0.025, depth=0.42, verts=8)
        if toe is not None:
            toe.rotation_euler = (0.0, math.radians(90.0), math.radians(ty * 55.0))
            deselect_all()
            toe.select_set(True)
            bpy.context.view_layer.objects.active = toe
            bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
        shade_smooth(toe)
        assign_material(toe, mat_leg)
        leg_objs.append(toe)

# ---------------------------------------------------------------------------
# Assemble: keep parts as one specimen object (parent to body)
# ---------------------------------------------------------------------------

# Re-fetch body from data in case references changed
body = bpy.data.objects.get("Duck_Body", body)

all_parts = [breast, neck, collar, head, bill, nail] + nostrils + eyes + \
            wings + speculums + tail_parts + leg_objs

deselect_all()
if body is not None:
    body.select_set(True)
    bpy.context.view_layer.objects.active = body
for p in all_parts:
    if p is not None:
        try:
            p.select_set(True)
        except Exception:
            pass
try:
    bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
except Exception:
    pass

deselect_all()
if body is not None:
    body.select_set(True)
    bpy.context.view_layer.objects.active = body

# Duck left in scene; not saving, not clearing.