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

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

def clear_scene():
    """Remove existing objects so we start with a single clean specimen."""
    bpy.ops.object.select_all(action='DESELECT')
    for obj in list(bpy.data.objects):
        obj.select_set(True)
    if bpy.data.objects:
        bpy.ops.object.delete()


def new_mesh_object(name):
    mesh = bpy.data.meshes.new(name + "_mesh")
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    return obj, mesh


def bm_to_object(bm, obj):
    bm.normal_update()
    bm.to_mesh(obj.data)
    bm.free()
    obj.data.update()


def make_material(name, base_color, roughness=0.6, metallic=0.0):
    """Create a minimal, defensive Principled BSDF material."""
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nt = mat.node_tree
    bsdf = None
    for node in nt.nodes:
        if node.type == 'BSDF_PRINCIPLED':
            bsdf = node
            break
    if bsdf is None:
        bsdf = nt.nodes.new('ShaderNodeBsdfPrincipled')
    # Base color (color socket wants a 4-tuple)
    try:
        bsdf.inputs['Base Color'].default_value = (
            base_color[0], base_color[1], base_color[2], 1.0)
    except Exception:
        pass
    # Roughness (scalar socket wants a float)
    try:
        bsdf.inputs['Roughness'].default_value = float(roughness)
    except Exception:
        pass
    try:
        bsdf.inputs['Metallic'].default_value = float(metallic)
    except Exception:
        pass
    # Optional 4.x subsurface tweak, fully guarded
    try:
        if 'Subsurface Weight' in bsdf.inputs:
            bsdf.inputs['Subsurface Weight'].default_value = 0.05
    except Exception:
        pass
    return mat


def assign_material(obj, mat):
    if obj is None or obj.data is None:
        return
    if len(obj.data.materials) == 0:
        obj.data.materials.append(mat)
    else:
        obj.data.materials[0] = mat


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


# ----------------------------------------------------------------------
# Primitive builders based on bmesh (valid in Blender 4.2)
# ----------------------------------------------------------------------

def add_uv_sphere(name, radius=1.0, u=24, v=16, location=(0, 0, 0)):
    obj, mesh = new_mesh_object(name)
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=u, v_segments=v, radius=radius)
    bm_to_object(bm, obj)
    obj.location = Vector(location)
    return obj


def add_icosphere(name, radius=1.0, subdiv=2, location=(0, 0, 0)):
    obj, mesh = new_mesh_object(name)
    bm = bmesh.new()
    bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=radius)
    bm_to_object(bm, obj)
    obj.location = Vector(location)
    return obj


def add_cylinder(name, radius=1.0, depth=2.0, segments=20, location=(0, 0, 0),
                 cap=True):
    """A cone with equal radii + capped ends is a cylinder."""
    obj, mesh = new_mesh_object(name)
    bm = bmesh.new()
    bmesh.ops.create_cone(
        bm,
        cap_ends=cap,
        cap_tris=False,
        segments=segments,
        radius1=radius,
        radius2=radius,
        depth=depth,
    )
    bm_to_object(bm, obj)
    obj.location = Vector(location)
    return obj


def add_cone(name, radius1=1.0, radius2=0.0, depth=2.0, segments=20,
             location=(0, 0, 0), cap=True):
    obj, mesh = new_mesh_object(name)
    bm = bmesh.new()
    bmesh.ops.create_cone(
        bm,
        cap_ends=cap,
        cap_tris=False,
        segments=segments,
        radius1=radius1,
        radius2=radius2,
        depth=depth,
    )
    bm_to_object(bm, obj)
    obj.location = Vector(location)
    return obj


def join_objects(objects, name):
    """Join a list of objects into the first; return the surviving object."""
    objects = [o for o in objects if o is not None]
    if not objects:
        return None
    bpy.ops.object.select_all(action='DESELECT')
    target = objects[0]
    for o in objects:
        o.select_set(True)
    bpy.context.view_layer.objects.active = target
    if len(objects) > 1:
        bpy.ops.object.join()
    # Re-fetch surviving object (others were removed by the join operator)
    survivor = bpy.context.view_layer.objects.active
    survivor.name = name
    return survivor


def add_subsurf(obj, levels=1):
    if obj is None:
        return
    mod = obj.modifiers.new("Subsurf", 'SUBSURF')
    mod.levels = levels
    mod.render_levels = levels


# ----------------------------------------------------------------------
# Start clean
# ----------------------------------------------------------------------

clear_scene()

# ----------------------------------------------------------------------
# Materials
# ----------------------------------------------------------------------
mat_white = make_material("Stork_Plumage_White", (0.92, 0.92, 0.90), 0.55)
mat_black = make_material("Stork_Flight_Feathers_Black", (0.02, 0.02, 0.02), 0.5)
mat_red = make_material("Stork_Bill_Legs_Red", (0.72, 0.08, 0.05), 0.35)
mat_eye = make_material("Stork_Eye", (0.02, 0.02, 0.02), 0.15)
mat_skin = make_material("Stork_Facial_Skin", (0.08, 0.02, 0.02), 0.4)
mat_claw = make_material("Stork_Claw", (0.06, 0.05, 0.05), 0.3)

body_parts_white = []
body_parts_black = []

# ----------------------------------------------------------------------
# TORSO / BODY
# ----------------------------------------------------------------------
body = add_uv_sphere("Body", radius=1.0, u=32, v=20, location=(0, 0, 3.4))
body.scale = (1.15, 2.15, 1.05)
body_parts_white.append(body)

# Breast / crop fullness at front of body
breast = add_uv_sphere("Breast", radius=0.7, u=24, v=16,
                        location=(0, 1.55, 3.35))
breast.scale = (1.0, 1.0, 1.15)
body_parts_white.append(breast)

# Rump toward tail
rump = add_uv_sphere("Rump", radius=0.65, u=24, v=16,
                     location=(0, -1.75, 3.5))
rump.scale = (0.9, 1.1, 0.95)
body_parts_white.append(rump)

# ----------------------------------------------------------------------
# NECK  (long, S-curved, held mostly upright/forward)
# ----------------------------------------------------------------------
neck_segments = []
# Control points describing the graceful stork neck curve
neck_pts = [
    Vector((0.0, 1.5, 3.9)),
    Vector((0.0, 1.9, 4.5)),
    Vector((0.0, 2.1, 5.3)),
    Vector((0.0, 2.05, 6.1)),
    Vector((0.0, 1.85, 6.9)),
    Vector((0.0, 1.75, 7.5)),
]
neck_radii = [0.55, 0.48, 0.42, 0.38, 0.34, 0.3]

for i in range(len(neck_pts) - 1):
    p0 = neck_pts[i]
    p1 = neck_pts[i + 1]
    mid = (p0 + p1) * 0.5
    vec = p1 - p0
    length = vec.length
    seg = add_cylinder("NeckSeg_%d" % i,
                       radius=(neck_radii[i] + neck_radii[i + 1]) * 0.5,
                       depth=length * 1.05, segments=18, cap=True)
    # Orient cylinder (default axis Z) to point along vec
    z = Vector((0, 0, 1))
    if vec.length > 1e-6:
        rot = z.rotation_difference(vec.normalized())
        seg.rotation_euler = rot.to_euler()
    seg.location = mid
    neck_segments.append(seg)
    # joint sphere for smooth blend
    joint = add_uv_sphere("NeckJoint_%d" % i, radius=neck_radii[i + 1],
                          u=16, v=12, location=p1)
    neck_segments.append(joint)

body_parts_white.extend(neck_segments)

# ----------------------------------------------------------------------
# HEAD
# ----------------------------------------------------------------------
head_center = Vector((0.0, 1.75, 7.75))
head = add_uv_sphere("Head", radius=0.42, u=24, v=18, location=head_center)
head.scale = (0.95, 1.25, 0.95)
body_parts_white.append(head)

# ----------------------------------------------------------------------
# BILL  (long, straight, pointed — bright red-orange)
# ----------------------------------------------------------------------
bill_len = 2.4
bill_base = head_center + Vector((0, 0.5, -0.02))
bill = add_cone("Bill", radius1=0.22, radius2=0.02, depth=bill_len,
                segments=20, cap=True)
# point it forward along +Y, slightly downward
bill.rotation_euler = (math.radians(90 + 4), 0, 0)
bill.location = bill_base + Vector((0, bill_len * 0.5, -0.05))
shade_smooth(bill)
assign_material(bill, mat_red)

# Facial skin patch around eye / bill base (dark)
face_patch = add_uv_sphere("FaceSkin", radius=0.2, u=14, v=10,
                           location=head_center + Vector((0, 0.4, 0.08)))
face_patch.scale = (0.9, 0.7, 0.7)
shade_smooth(face_patch)
assign_material(face_patch, mat_skin)

# ----------------------------------------------------------------------
# EYES
# ----------------------------------------------------------------------
eye_objs = []
for side in (-1, 1):
    eye = add_uv_sphere("Eye_%d" % side, radius=0.09, u=14, v=10,
                        location=head_center + Vector((0.28 * side, 0.28, 0.12)))
    shade_smooth(eye)
    assign_material(eye, mat_eye)
    eye_objs.append(eye)

# ----------------------------------------------------------------------
# WINGS  (folded along the body — white with black trailing/flight feathers)
# ----------------------------------------------------------------------
wing_objs_white = []
wing_objs_black = []

for side in (-1, 1):
    # Main wing coverts (white) — a flattened elongated dome over the flank
    wing = add_uv_sphere("Wing_%d" % side, radius=0.8, u=24, v=16,
                         location=(0.95 * side, -0.15, 3.55))
    wing.scale = (0.35, 2.0, 0.95)
    wing.rotation_euler = (0, 0, math.radians(-4 * side))
    wing_objs_white.append(wing)

    # Black flight feathers (primaries/secondaries) — extend past the rump
    flight = add_uv_sphere("FlightFeathers_%d" % side, radius=0.5, u=20, v=14,
                           location=(0.75 * side, -1.85, 3.55))
    flight.scale = (0.28, 1.45, 0.7)
    flight.rotation_euler = (math.radians(6), 0, math.radians(-6 * side))
    wing_objs_black.append(flight)

    # A few individual primary feather tips for silhouette
    for k in range(4):
        tip = add_cone("PrimaryTip_%d_%d" % (side, k),
                       radius1=0.12, radius2=0.01, depth=1.1,
                       segments=8, cap=True)
        tip.rotation_euler = (math.radians(100), 0,
                              math.radians(-6 * side + (k - 1.5) * 4))
        tip.location = (0.7 * side, -2.7 - k * 0.06, 3.55 - k * 0.03)
        wing_objs_black.append(tip)

body_parts_white.extend(wing_objs_white)
body_parts_black.extend(wing_objs_black)

# ----------------------------------------------------------------------
# TAIL  (short white tail behind, black flight feathers overhang it)
# ----------------------------------------------------------------------
tail = add_uv_sphere("Tail", radius=0.5, u=20, v=14,
                     location=(0, -2.6, 3.55))
tail.scale = (0.7, 1.4, 0.35)
tail.rotation_euler = (math.radians(8), 0, 0)
body_parts_white.append(tail)

# ----------------------------------------------------------------------
# LEGS  (long, red; thigh, drumstick, tarsus, foot with 4 toes)
# ----------------------------------------------------------------------
leg_objs = []


def build_leg(side):
    parts = []
    hip = Vector((0.45 * side, -0.9, 3.0))

    # Feathered thigh (short, near body) - white; but we make it red-skinned lower
    thigh_top = hip
    thigh_bot = Vector((0.5 * side, -0.75, 2.1))
    # Tarsus (the long visible bare leg) goes down to ground level ~0
    knee = thigh_bot
    ankle = Vector((0.5 * side, -0.55, 0.55))
    foot = Vector((0.5 * side, -0.35, 0.06))

    def segment(name, p0, p1, r0, r1):
        vec = p1 - p0
        length = vec.length
        seg = add_cone(name, radius1=r0, radius2=r1, depth=length,
                       segments=14, cap=True)
        z = Vector((0, 0, 1))
        if vec.length > 1e-6:
            rot = z.rotation_difference(vec.normalized())
            seg.rotation_euler = rot.to_euler()
        seg.location = (p0 + p1) * 0.5
        return seg

    # thigh
    parts.append(segment("Thigh_%d" % side, thigh_top, knee, 0.2, 0.15))
    # knee joint
    parts.append(add_uv_sphere("Knee_%d" % side, radius=0.15, u=12, v=8,
                               location=knee))
    # tarsus (long lower leg)
    parts.append(segment("Tarsus_%d" % side, knee, ankle, 0.13, 0.1))
    # ankle joint
    parts.append(add_uv_sphere("Ankle_%d" % side, radius=0.1, u=12, v=8,
                               location=ankle))
    # metatarsus down to foot
    parts.append(segment("Metatarsus_%d" % side, ankle, foot, 0.1, 0.08))

    # Toes: three forward, one back (anisodactyl), splayed on the ground
    toe_dirs = [
        Vector((0.0, 1.0, 0.0)),      # middle forward
        Vector((0.45 * side, 0.85, 0.0)),  # outer forward
        Vector((-0.45 * side, 0.85, 0.0)),  # inner forward
        Vector((0.0, -0.7, 0.0)),     # hind toe
    ]
    toe_len = [0.75, 0.6, 0.6, 0.35]
    for ti, d in enumerate(toe_dirs):
        d = d.normalized()
        base = foot + Vector((0, 0, -0.02))
        end = base + d * toe_len[ti]
        end.z = 0.02
        toe = segment("Toe_%d_%d" % (side, ti), base, end, 0.07, 0.035)
        parts.append(toe)
        # claw
        claw = add_cone("Claw_%d_%d" % (side, ti), radius1=0.04, radius2=0.005,
                        depth=0.13, segments=8, cap=True)
        cvec = d.copy()
        cvec.z = -0.2
        z = Vector((0, 0, 1))
        claw.rotation_euler = z.rotation_difference(cvec.normalized()).to_euler()
        claw.location = end + d * 0.05
        assign_material(claw, mat_claw)
        shade_smooth(claw)

    return parts


for s in (-1, 1):
    leg_objs.extend(build_leg(s))

# ----------------------------------------------------------------------
# Assemble the plumage bodies
# ----------------------------------------------------------------------
# White plumage cluster
white_body = join_objects(body_parts_white, "Stork_Body_White")
if white_body is not None:
    shade_smooth(white_body)
    assign_material(white_body, mat_white)
    add_subsurf(white_body, 1)

# Black flight feathers cluster
black_body = join_objects(body_parts_black, "Stork_FlightFeathers_Black")
if black_body is not None:
    shade_smooth(black_body)
    assign_material(black_body, mat_black)
    add_subsurf(black_body, 1)

# Legs (red skin)
red_legs = join_objects(leg_objs, "Stork_Legs")
if red_legs is not None:
    shade_smooth(red_legs)
    assign_material(red_legs, mat_red)

# ----------------------------------------------------------------------
# Final tidy: select nothing so the scene is left clean with the specimen
# ----------------------------------------------------------------------
bpy.ops.object.select_all(action='DESELECT')
if white_body is not None:
    bpy.context.view_layer.objects.active = white_body