import bpy
import bmesh
import math
from mathutils import Vector

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

def deselect_all():
    if bpy.context.view_layer.objects.active is not None:
        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.6, metallic=0.0,
                  subsurface=0.0, sheen=0.0):
    """Create a simple Principled BSDF material, defensively."""
    mat = bpy.data.materials.get(name)
    if mat is None:
        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:
        bsdf = nt.nodes.new('ShaderNodeBsdfPrincipled')

    def set_input(key, value):
        try:
            if key in bsdf.inputs:
                bsdf.inputs[key].default_value = value
        except Exception:
            pass

    # Base color is a 4-tuple color socket
    try:
        col = (base_color[0], base_color[1], base_color[2],
               base_color[3] if len(base_color) > 3 else 1.0)
        set_input('Base Color', col)
    except Exception:
        pass

    set_input('Roughness', float(roughness))
    set_input('Metallic', float(metallic))

    # Blender 4.x renamed sockets; guard each optional tweak.
    for key in ('Subsurface Weight', 'Subsurface'):
        try:
            if key in bsdf.inputs:
                bsdf.inputs[key].default_value = float(subsurface)
                break
        except Exception:
            pass
    for key in ('Sheen Weight', 'Sheen'):
        try:
            if key in bsdf.inputs:
                bsdf.inputs[key].default_value = float(sheen)
                break
        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_subsurf(obj, levels=2):
    if obj is None:
        return
    try:
        m = obj.modifiers.new("Subsurf", 'SUBSURF')
        m.levels = levels
        m.render_levels = levels
    except Exception:
        pass


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


def new_cylinder(name, radius=1.0, depth=1.0, location=(0, 0, 0), verts=24):
    bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=depth,
                                         location=location, vertices=verts)
    obj = bpy.context.active_object
    obj.name = name
    return obj


def new_cone(name, radius1=1.0, radius2=0.0, depth=1.0, location=(0, 0, 0), verts=24):
    bpy.ops.mesh.primitive_cone_add(radius1=radius1, radius2=radius2, depth=depth,
                                    location=location, vertices=verts)
    obj = bpy.context.active_object
    obj.name = name
    return obj


# ---------------------------------------------------------------------------
# Materials (mallard drake colouration)
# ---------------------------------------------------------------------------

mat_body_gray   = make_material("Mallard_BodyGray", (0.62, 0.60, 0.55, 1.0), roughness=0.55, sheen=0.4)
mat_breast      = make_material("Mallard_Breast",   (0.35, 0.16, 0.09, 1.0), roughness=0.5, sheen=0.3)
mat_head_green  = make_material("Mallard_HeadGreen",(0.02, 0.14, 0.05, 1.0), roughness=0.22, metallic=0.55, sheen=0.6)
mat_collar      = make_material("Mallard_Collar",   (0.96, 0.96, 0.94, 1.0), roughness=0.5)
mat_bill        = make_material("Mallard_Bill",     (0.85, 0.70, 0.12, 1.0), roughness=0.35)
mat_nail        = make_material("Mallard_BillNail", (0.06, 0.05, 0.04, 1.0), roughness=0.4)
mat_eye         = make_material("Mallard_Eye",      (0.02, 0.01, 0.01, 1.0), roughness=0.1)
mat_wing_dark   = make_material("Mallard_WingDark", (0.30, 0.28, 0.26, 1.0), roughness=0.5, sheen=0.3)
mat_speculum    = make_material("Mallard_Speculum", (0.05, 0.06, 0.35, 1.0), roughness=0.2, metallic=0.5)
mat_tail        = make_material("Mallard_Tail",     (0.10, 0.10, 0.10, 1.0), roughness=0.5)
mat_tail_white  = make_material("Mallard_TailWhite",(0.94, 0.94, 0.92, 1.0), roughness=0.5)
mat_curl        = make_material("Mallard_Curl",     (0.03, 0.03, 0.03, 1.0), roughness=0.35, metallic=0.3)
mat_legs        = make_material("Mallard_Legs",     (0.90, 0.55, 0.10, 1.0), roughness=0.4)

created_objects = []


def track(obj):
    if obj is not None:
        created_objects.append(obj)
    return obj


# ---------------------------------------------------------------------------
# Start clean-ish: we work additively but reset selection state
# ---------------------------------------------------------------------------
deselect_all()

# ---------------------------------------------------------------------------
# BODY  (main torso) - elongated ellipsoid tapering toward tail
# ---------------------------------------------------------------------------
body = new_uv_sphere("Duck_Body", radius=1.0, location=(0.0, 0.0, 1.0),
                     segments=40, rings=24)
# Sculpt the sphere into a duck torso by scaling and tapering per-vertex.
mesh = body.data
for v in mesh.vertices:
    x, y, z = v.co.x, v.co.y, v.co.z
    # elongate along X (body axis, +X = tail, -X = head/breast)
    x *= 2.1
    y *= 0.95
    z *= 0.85
    # taper the tail end (positive X)
    if x > 0:
        t = x / 2.1
        y *= (1.0 - 0.55 * t)
        z *= (1.0 - 0.35 * t)
        z += 0.35 * t  # lift the rear (tail up)
    # fuller breast at front (negative X)
    if x < 0:
        t = -x / 2.1
        z -= 0.10 * t
    v.co = Vector((x, y, z))
body.location = (0.0, 0.0, 1.0)
shade_smooth(body)
add_subsurf(body, 2)
assign_material(body, mat_body_gray)
track(body)

# BREAST patch (chestnut) - a slightly protruding shell at the front underside
breast = new_uv_sphere("Duck_Breast", radius=0.62, location=(-1.55, 0.0, 0.85),
                       segments=28, rings=18)
breast.scale = (1.0, 1.0, 1.05)
shade_smooth(breast)
add_subsurf(breast, 2)
assign_material(breast, mat_breast)
track(breast)

# ---------------------------------------------------------------------------
# NECK  - curved cylinder connecting body to head
# ---------------------------------------------------------------------------
neck = new_cone("Duck_Neck", radius1=0.42, radius2=0.34, depth=1.1,
                location=(-1.95, 0.0, 1.55), verts=24)
neck.rotation_euler = (0.0, math.radians(-35.0), 0.0)
shade_smooth(neck)
add_subsurf(neck, 2)
assign_material(neck, mat_body_gray)
track(neck)

# WHITE COLLAR ring around the neck
collar = new_cylinder("Duck_Collar", radius=0.38, depth=0.12,
                      location=(-2.05, 0.0, 1.85), verts=28)
collar.rotation_euler = (0.0, math.radians(-35.0), 0.0)
collar.scale = (1.0, 1.0, 1.0)
shade_smooth(collar)
assign_material(collar, mat_collar)
track(collar)

# ---------------------------------------------------------------------------
# HEAD  - green iridescent
# ---------------------------------------------------------------------------
head = new_uv_sphere("Duck_Head", radius=0.52, location=(-2.35, 0.0, 2.25),
                     segments=32, rings=20)
mesh = head.data
for v in mesh.vertices:
    x, y, z = v.co.x, v.co.y, v.co.z
    x *= 1.15   # elongate slightly toward bill
    z *= 1.05
    v.co = Vector((x, y, z))
shade_smooth(head)
add_subsurf(head, 2)
assign_material(head, mat_head_green)
track(head)

# ---------------------------------------------------------------------------
# BILL  - flattened, rounded rectangular beak
# ---------------------------------------------------------------------------
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(-2.95, 0.0, 2.18))
bill = bpy.context.active_object
bill.name = "Duck_Bill"
mesh = bill.data
for v in mesh.vertices:
    x, y, z = v.co.x, v.co.y, v.co.z
    x *= 0.55   # length
    y *= 0.34   # width
    z *= 0.14   # flat
    # widen the tip a touch (spatulate)
    if x < 0:
        y *= 1.15
    v.co = Vector((x, y, z))
# round it with bevel + subsurf
try:
    bev = bill.modifiers.new("Bevel", 'BEVEL')
    bev.width = 0.05
    bev.segments = 3
except Exception:
    pass
shade_smooth(bill)
add_subsurf(bill, 2)
assign_material(bill, mat_bill)
track(bill)

# Bill nail (dark tip)
nail = new_uv_sphere("Duck_BillNail", radius=0.06, location=(-3.24, 0.0, 2.17),
                     segments=16, rings=10)
nail.scale = (0.7, 1.3, 0.6)
shade_smooth(nail)
assign_material(nail, mat_nail)
track(nail)

# Nostrils (two small indentations represented as tiny dark spheres)
for side in (-1, 1):
    nos = new_uv_sphere("Duck_Nostril", radius=0.028,
                        location=(-3.02, side * 0.09, 2.24),
                        segments=10, rings=6)
    assign_material(nos, mat_nail)
    track(nos)

# ---------------------------------------------------------------------------
# EYES - one on each side of head
# ---------------------------------------------------------------------------
for side in (-1, 1):
    eye = new_uv_sphere("Duck_Eye", radius=0.10,
                        location=(-2.52, side * 0.42, 2.38),
                        segments=20, rings=12)
    shade_smooth(eye)
    assign_material(eye, mat_eye)
    track(eye)

# ---------------------------------------------------------------------------
# WINGS - folded along the sides of the body, one per side
# ---------------------------------------------------------------------------
def make_wing(side):
    wing = new_uv_sphere("Duck_Wing", radius=0.5,
                         location=(0.15, side * 0.72, 1.15),
                         segments=28, rings=18)
    mesh = wing.data
    for v in mesh.vertices:
        x, y, z = v.co.x, v.co.y, v.co.z
        x *= 2.2    # long along body
        y *= 0.28   # thin against body
        z *= 0.75
        # taper toward tail (positive x)
        if x > 0:
            t = x / (2.2 * 0.5)
            z *= (1.0 - 0.4 * min(t, 1.0))
        v.co = Vector((x, y, z))
    wing.rotation_euler = (0.0, math.radians(-6.0), side * math.radians(-5.0))
    shade_smooth(wing)
    add_subsurf(wing, 2)
    assign_material(wing, mat_wing_dark)
    track(wing)

    # Speculum patch (iridescent blue) on each wing
    spec = new_uv_sphere("Duck_Speculum", radius=0.2,
                         location=(0.35, side * 0.86, 1.05),
                         segments=18, rings=12)
    spec.scale = (1.6, 0.4, 0.55)
    shade_smooth(spec)
    assign_material(spec, mat_speculum)
    track(spec)

make_wing(1)
make_wing(-1)

# ---------------------------------------------------------------------------
# TAIL - fanned feathers at rear, plus the drake's curled tail feather
# ---------------------------------------------------------------------------
tail = new_cone("Duck_Tail", radius1=0.5, radius2=0.08, depth=1.0,
                location=(2.05, 0.0, 1.55), verts=20)
tail.rotation_euler = (0.0, math.radians(80.0), 0.0)
mesh = tail.data
for v in mesh.vertices:
    v.co.y *= 1.4   # widen the fan
    v.co.z *= 0.35  # flatten
shade_smooth(tail)
add_subsurf(tail, 2)
assign_material(tail, mat_tail)
track(tail)

# White outer tail feathers
for side in (-1, 1):
    tw = new_cone("Duck_TailWhite", radius1=0.14, radius2=0.03, depth=0.85,
                  location=(2.0, side * 0.28, 1.5), verts=12)
    tw.rotation_euler = (0.0, math.radians(80.0), 0.0)
    tw.scale = (1.0, 1.3, 0.4)
    shade_smooth(tw)
    assign_material(tw, mat_tail_white)
    track(tw)

# Drake curled tail feather (signature recurved black feathers)
bpy.ops.mesh.primitive_cylinder_add(radius=0.05, depth=1.0, location=(2.15, 0.0, 1.95),
                                     vertices=12)
curl = bpy.context.active_object
curl.name = "Duck_CurlFeather"
mesh = curl.data
# Bend into a curl by displacing verts based on z along the cylinder axis.
for v in mesh.vertices:
    z = v.co.z
    t = (z + 0.5)  # 0..1
    ang = t * math.pi * 1.3
    v.co.x += math.sin(ang) * 0.25
    v.co.z = -math.cos(ang) * 0.22
    v.co.x *= 1.0
curl.rotation_euler = (0.0, math.radians(20.0), 0.0)
shade_smooth(curl)
add_subsurf(curl, 2)
assign_material(curl, mat_curl)
track(curl)

# ---------------------------------------------------------------------------
# LEGS + WEBBED FEET (one on each side, tucked under body)
# ---------------------------------------------------------------------------
def make_leg(side):
    y = side * 0.30
    # Upper leg / tarsus
    leg = new_cylinder("Duck_Leg", radius=0.075, depth=0.55,
                       location=(0.35, y, 0.45), verts=14)
    leg.rotation_euler = (math.radians(8.0), 0.0, 0.0)
    shade_smooth(leg)
    assign_material(leg, mat_legs)
    track(leg)

    # Foot base (webbed) - flat triangular web
    bpy.ops.mesh.primitive_cone_add(radius1=0.30, radius2=0.02, depth=0.05,
                                    location=(0.15, y, 0.16), vertices=3)
    foot = bpy.context.active_object
    foot.name = "Duck_Foot"
    foot.rotation_euler = (0.0, 0.0, math.radians(180.0))
    foot.scale = (1.4, 1.0, 1.0)
    shade_smooth(foot)
    assign_material(foot, mat_legs)
    track(foot)

    # Three front toes as thin cylinders spreading forward
    for toe_i, ty in enumerate((-0.14, 0.0, 0.14)):
        toe = new_cylinder("Duck_Toe", radius=0.025, depth=0.34,
                           location=(-0.10, y + ty, 0.15), verts=8)
        toe.rotation_euler = (0.0, math.radians(90.0), 0.0)
        shade_smooth(toe)
        assign_material(toe, mat_legs)
        track(toe)

make_leg(1)
make_leg(-1)

# ---------------------------------------------------------------------------
# Finalise: parent all parts to the body so it reads as ONE specimen,
# and center the view selection state.
# ---------------------------------------------------------------------------
deselect_all()
if body is not None and body.name in bpy.data.objects:
    body = bpy.data.objects[body.name]
    for obj in created_objects:
        if obj is None:
            continue
        if obj.name in bpy.data.objects and obj is not body:
            o = bpy.data.objects[obj.name]
            o.select_set(True)
    body.select_set(True)
    bpy.context.view_layer.objects.active = body
    try:
        bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
    except Exception:
        pass

deselect_all()
if body is not None and body.name in bpy.data.objects:
    bpy.data.objects[body.name].select_set(True)
    bpy.context.view_layer.objects.active = bpy.data.objects[body.name]

print("Mallard duck generation complete: %d objects created." % len(created_objects))