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

# ----------------------------------------------------------------------------
# Brown Pelican (Pelecanus occidentalis) procedural model for Blender 4.2
# ----------------------------------------------------------------------------

def clean_scene():
    """Remove pre-existing mesh objects so we build exactly one specimen."""
    bpy.ops.object.select_all(action='DESELECT')
    for obj in list(bpy.data.objects):
        if obj.type in {'MESH', 'CURVE', 'EMPTY'}:
            obj.select_set(True)
    if bpy.context.selected_objects:
        bpy.ops.object.delete()


def make_material(name, base_color, rough=0.6, subsurf=0.0, subsurf_color=None,
                  metallic=0.0, specular=0.5):
    """Create a Principled BSDF material, guarding renamed 4.x sockets."""
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    bsdf = None
    for n in mat.node_tree.nodes:
        if n.type == 'BSDF_PRINCIPLED':
            bsdf = n
            break
    if bsdf is None:
        return mat
    inputs = bsdf.inputs

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

    set_socket('Base Color', base_color)
    set_socket('Roughness', float(rough))
    set_socket('Metallic', float(metallic))
    # Specular socket differs across versions
    try:
        if 'Specular IOR Level' in inputs:
            inputs['Specular IOR Level'].default_value = float(specular)
        elif 'Specular' in inputs:
            inputs['Specular'].default_value = float(specular)
    except Exception:
        pass
    # Subsurface (renamed 'Subsurface' -> 'Subsurface Weight' in 4.x)
    try:
        if subsurf > 0.0:
            if 'Subsurface Weight' in inputs:
                inputs['Subsurface Weight'].default_value = float(subsurf)
            elif 'Subsurface' in inputs:
                inputs['Subsurface'].default_value = float(subsurf)
            if subsurf_color is not None and 'Subsurface Color' in inputs:
                inputs['Subsurface Color'].default_value = subsurf_color
    except Exception:
        pass
    return mat


def assign_material(obj, mat):
    if obj is None or obj.type != 'MESH':
        return
    obj.data.materials.clear()
    obj.data.materials.append(mat)


def add_uv_sphere(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)
    return bpy.context.active_object


def add_cylinder(radius=1.0, depth=2.0, location=(0, 0, 0), verts=24,
                 rot=(0, 0, 0)):
    bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=depth,
                                         location=location, vertices=verts,
                                         rotation=rot)
    return bpy.context.active_object


def shade_smooth(obj):
    if obj is None:
        return
    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.shade_smooth()


def add_subsurf(obj, levels=2, render=2):
    if obj is None:
        return
    m = obj.modifiers.new(name="Subsurf", type='SUBSURF')
    m.levels = levels
    m.render_levels = render


# ----------------------------------------------------------------------------
clean_scene()

# Materials -------------------------------------------------------------------
mat_body   = make_material("PelicanBody",   (0.14, 0.11, 0.09, 1.0), rough=0.7,
                           subsurf=0.05, subsurf_color=(0.3, 0.2, 0.15, 1.0))
mat_neck   = make_material("PelicanNeck",   (0.55, 0.42, 0.28, 1.0), rough=0.75)
mat_head   = make_material("PelicanHead",   (0.95, 0.92, 0.80, 1.0), rough=0.65)
mat_nape   = make_material("PelicanNape",   (0.35, 0.12, 0.10, 1.0), rough=0.7)
mat_bill   = make_material("PelicanBill",   (0.72, 0.66, 0.55, 1.0), rough=0.4,
                           specular=0.6)
mat_pouch  = make_material("PelicanPouch",  (0.20, 0.16, 0.14, 1.0), rough=0.55,
                           subsurf=0.2, subsurf_color=(0.5, 0.2, 0.15, 1.0))
mat_leg    = make_material("PelicanLeg",    (0.15, 0.15, 0.16, 1.0), rough=0.5)
mat_eye    = make_material("PelicanEye",    (0.02, 0.02, 0.02, 1.0), rough=0.15,
                           specular=1.0)
mat_iris   = make_material("PelicanIris",   (0.75, 0.72, 0.60, 1.0), rough=0.25,
                           specular=0.9)

parts = []

# ----------------------------------------------------------------------------
# BODY (torso) - large barrel body of the pelican
# ----------------------------------------------------------------------------
body = add_uv_sphere(radius=1.0, location=(0.0, 0.0, 1.3), segments=40, rings=24)
body.scale = (2.2, 1.15, 1.1)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
# Sculpt a slightly raised back and lower belly
me = body.data
for v in me.vertices:
    if v.co.x < -0.5:            # tail-ward taper
        v.co.y *= 0.6
        v.co.z += 0.15
    if v.co.z < 0:              # rounded belly
        v.co.z *= 1.1
shade_smooth(body)
add_subsurf(body, 2, 2)
assign_material(body, mat_body)
parts.append(body)

# ----------------------------------------------------------------------------
# NECK - long S-curved neck built from stacked, offset segments
# ----------------------------------------------------------------------------
neck_segments = []
# Path points describing the classic pelican S-curve (x forward, z up)
neck_path = [
    (1.7, 0.0, 1.7, 0.55),
    (2.05, 0.0, 2.15, 0.42),
    (2.25, 0.0, 2.65, 0.34),
    (2.20, 0.0, 3.10, 0.30),
    (1.95, 0.0, 3.45, 0.28),
    (2.05, 0.0, 3.80, 0.27),
    (2.35, 0.0, 4.05, 0.30),
]
for i in range(len(neck_path) - 1):
    x0, y0, z0, r0 = neck_path[i]
    x1, y1, z1, r1 = neck_path[i + 1]
    mid = ((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2)
    vec = Vector((x1 - x0, y1 - y0, z1 - z0))
    length = vec.length
    seg = add_cylinder(radius=(r0 + r1) / 2, depth=length, location=mid, verts=20)
    # orient the cylinder (default +Z) along the segment direction
    quat = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    seg.rotation_mode = 'QUATERNION'
    seg.rotation_quaternion = quat
    assign_material(seg, mat_neck)
    neck_segments.append(seg)

# Join neck segments together
bpy.ops.object.select_all(action='DESELECT')
for s in neck_segments:
    s.select_set(True)
bpy.context.view_layer.objects.active = neck_segments[0]
bpy.ops.object.join()
neck = bpy.context.active_object
neck.name = "Neck"
shade_smooth(neck)
add_subsurf(neck, 2, 2)
assign_material(neck, mat_neck)
parts.append(neck)

# ----------------------------------------------------------------------------
# HEAD
# ----------------------------------------------------------------------------
head = add_uv_sphere(radius=0.42, location=(2.55, 0.0, 4.25), segments=32, rings=20)
head.scale = (1.25, 0.95, 1.05)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
shade_smooth(head)
add_subsurf(head, 2, 2)
assign_material(head, mat_head)
parts.append(head)

# Nape / crest patch (breeding brown pelican has dark chestnut nape)
nape = add_uv_sphere(radius=0.30, location=(2.30, 0.0, 4.05), segments=24, rings=14)
nape.scale = (0.9, 0.9, 1.4)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
shade_smooth(nape)
add_subsurf(nape, 1, 1)
assign_material(nape, mat_nape)
parts.append(nape)

# ----------------------------------------------------------------------------
# BILL - the huge characteristic pelican bill (upper mandible + lower mandible)
# ----------------------------------------------------------------------------
def build_upper_bill():
    bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0))
    bill = bpy.context.active_object
    bm = bmesh.new()
    bm.from_mesh(bill.data)
    bmesh.ops.subdivide_edges(bm, edges=bm.edges[:], cuts=8, use_grid_fill=True)
    bm.to_mesh(bill.data)
    bm.free()
    # Shape into a long, slightly drooping upper mandible
    length = 2.6
    for v in bill.data.vertices:
        t = (v.co.x + 0.5)          # 0..1 along the bill
        # length along +X starting from head
        v.co.x = t * length
        # taper width and depth toward the tip
        taper = 1.0 - 0.75 * t
        v.co.y = v.co.y * (0.34 * taper + 0.04)
        # top ridge + slight droop toward tip
        v.co.z = v.co.z * (0.16 * taper + 0.03) - 0.10 * t * t
    return bill

upper_bill = build_upper_bill()
upper_bill.location = (2.95, 0.0, 4.25)
upper_bill.rotation_euler = (0, math.radians(4), 0)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
shade_smooth(upper_bill)
assign_material(upper_bill, mat_bill)
upper_bill.name = "UpperBill"
parts.append(upper_bill)

# Small hooked nail at the tip of the upper mandible
nail = add_uv_sphere(radius=0.05, location=(5.5, 0.0, 4.02), segments=12, rings=8)
nail.scale = (1.6, 0.9, 1.2)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
shade_smooth(nail)
assign_material(nail, mat_bill)
parts.append(nail)

# Lower mandible (thin bony frame that supports the pouch)
def build_lower_mandible():
    bpy_obj = add_cylinder(radius=0.035, depth=2.4, location=(0, 0, 0),
                           verts=10, rot=(0, math.radians(90), 0))
    return bpy_obj

# Two thin rami of the lower jaw
for side in (-1, 1):
    ramus = build_lower_mandible()
    ramus.location = (4.05, side * 0.14, 4.02)
    ramus.rotation_euler = (0, math.radians(88), side * math.radians(4))
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    shade_smooth(ramus)
    assign_material(ramus, mat_bill)
    parts.append(ramus)

# ----------------------------------------------------------------------------
# GULAR POUCH - the iconic expandable throat pouch
# ----------------------------------------------------------------------------
pouch = add_uv_sphere(radius=0.5, location=(3.55, 0.0, 3.75), segments=32, rings=20)
pouch.scale = (1.9, 0.7, 0.9)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
# Flatten the top where it attaches under the bill; sag the bottom
for v in pouch.data.vertices:
    if v.co.z > 0.05:
        v.co.z *= 0.35
    if v.co.z < 0:
        v.co.z *= 1.3
        v.co.y *= 1.15
shade_smooth(pouch)
add_subsurf(pouch, 2, 2)
assign_material(pouch, mat_pouch)
pouch.name = "GularPouch"
parts.append(pouch)

# ----------------------------------------------------------------------------
# EYES
# ----------------------------------------------------------------------------
for side in (-1, 1):
    eye = add_uv_sphere(radius=0.075, location=(2.85, side * 0.33, 4.42),
                        segments=18, rings=12)
    shade_smooth(eye)
    assign_material(eye, mat_eye)
    parts.append(eye)
    iris = add_uv_sphere(radius=0.09, location=(2.85, side * 0.30, 4.42),
                         segments=18, rings=12)
    iris.scale = (0.6, 1.0, 1.0)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    shade_smooth(iris)
    assign_material(iris, mat_iris)
    parts.append(iris)

# ----------------------------------------------------------------------------
# WINGS - folded against the body (each with a subtle airfoil taper)
# ----------------------------------------------------------------------------
def build_wing(side):
    bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0))
    wing = bpy.context.active_object
    bm = bmesh.new()
    bm.from_mesh(wing.data)
    bmesh.ops.subdivide_edges(bm, edges=bm.edges[:], cuts=10, use_grid_fill=True)
    bm.to_mesh(wing.data)
    bm.free()
    length = 3.0
    for v in wing.data.vertices:
        t = (v.co.x + 0.5)             # 0 shoulder -> 1 tip
        v.co.x = -t * length           # sweep back along body (-X)
        taper = 1.0 - 0.7 * t
        v.co.y = v.co.y * (0.7 * taper + 0.05)
        v.co.z = v.co.z * (0.28 * taper + 0.03)
    return wing

for side in (-1, 1):
    wing = build_wing(side)
    wing.location = (0.9, side * 1.0, 1.65)
    wing.rotation_euler = (side * math.radians(6), math.radians(-6),
                           side * math.radians(10))
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    shade_smooth(wing)
    add_subsurf(wing, 2, 2)
    assign_material(wing, mat_body)
    wing.name = "Wing_%s" % ("L" if side < 0 else "R")
    parts.append(wing)

# ----------------------------------------------------------------------------
# TAIL - short, wedge-shaped
# ----------------------------------------------------------------------------
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0))
tail = bpy.context.active_object
bm = bmesh.new()
bm.from_mesh(tail.data)
bmesh.ops.subdivide_edges(bm, edges=bm.edges[:], cuts=6, use_grid_fill=True)
bm.to_mesh(tail.data)
bm.free()
for v in tail.data.vertices:
    t = (v.co.x + 0.5)
    v.co.x = -t * 1.3
    taper = 1.0 - 0.55 * t
    v.co.y = v.co.y * (0.6 * taper + 0.05)
    v.co.z = v.co.z * (0.12 + 0.02)
tail.location = (-2.15, 0.0, 1.35)
tail.rotation_euler = (0, math.radians(8), 0)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
shade_smooth(tail)
add_subsurf(tail, 2, 2)
assign_material(tail, mat_body)
tail.name = "Tail"
parts.append(tail)

# ----------------------------------------------------------------------------
# LEGS + WEBBED FEET (totipalmate - all four toes webbed)
# ----------------------------------------------------------------------------
def build_leg(side):
    seg_objs = []
    # Upper leg (tibiotarsus) - mostly inside body
    thigh = add_cylinder(radius=0.13, depth=0.55, location=(0, 0, 0), verts=14)
    thigh.location = (-0.2, side * 0.35, 0.55)
    seg_objs.append(thigh)
    # Tarsus (lower leg)
    tarsus = add_cylinder(radius=0.08, depth=0.55, location=(0, 0, 0), verts=12)
    tarsus.location = (-0.15, side * 0.4, 0.15)
    tarsus.rotation_euler = (math.radians(10), 0, 0)
    seg_objs.append(tarsus)

    bpy.ops.object.select_all(action='DESELECT')
    for s in seg_objs:
        s.select_set(True)
    bpy.context.view_layer.objects.active = seg_objs[0]
    bpy.ops.object.join()
    leg = bpy.context.active_object
    leg.name = "Leg_%s" % ("L" if side < 0 else "R")
    return leg

def build_foot(side):
    """Webbed foot: four toes joined by a solid triangular web."""
    foot_parts = []
    # Toe directions (fan forward). Brown pelicans are totipalmate.
    toe_dirs = [(-0.35, side * 0.28), (0.05, side * 0.34),
                (0.4, side * 0.2), (0.35, side * -0.18)]
    base = Vector((-0.05, side * 0.42, -0.12))
    for dx, dy in toe_dirs:
        end = base + Vector((dx, dy, -0.02))
        mid = (base + end) / 2
        vec = end - base
        toe = add_cylinder(radius=0.028, depth=vec.length, location=mid, verts=8)
        quat = Vector((0, 0, 1)).rotation_difference(vec.normalized())
        toe.rotation_mode = 'QUATERNION'
        toe.rotation_quaternion = quat
        foot_parts.append(toe)

    # Web: a flattened disc spanning the toes
    web = add_uv_sphere(radius=0.3, location=(base.x + 0.12, base.y, base.z - 0.03),
                        segments=16, rings=8)
    web.scale = (1.3, 1.1, 0.08)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    foot_parts.append(web)

    bpy.ops.object.select_all(action='DESELECT')
    for s in foot_parts:
        s.select_set(True)
    bpy.context.view_layer.objects.active = foot_parts[0]
    bpy.ops.object.join()
    foot = bpy.context.active_object
    foot.name = "Foot_%s" % ("L" if side < 0 else "R")
    return foot

for side in (-1, 1):
    leg = build_leg(side)
    shade_smooth(leg)
    assign_material(leg, mat_leg)
    parts.append(leg)
    foot = build_foot(side)
    shade_smooth(foot)
    assign_material(foot, mat_leg)
    parts.append(foot)

# ----------------------------------------------------------------------------
# Group all parts under one empty and center the specimen
# ----------------------------------------------------------------------------
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0, 0, 0))
root = bpy.context.active_object
root.name = "BrownPelican"

for p in parts:
    if p is not None and p.name in bpy.data.objects:
        p.parent = root

# Recompute normals defensively for every mesh part
for p in parts:
    if p is None or p.type != 'MESH':
        continue
    bpy.ops.object.select_all(action='DESELECT')
    p.select_set(True)
    bpy.context.view_layer.objects.active = p
    try:
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.normals_make_consistent(inside=False)
        bpy.ops.object.mode_set(mode='OBJECT')
    except Exception:
        try:
            bpy.ops.object.mode_set(mode='OBJECT')
        except Exception:
            pass

bpy.ops.object.select_all(action='DESELECT')
if root.name in bpy.data.objects:
    root.select_set(True)
    bpy.context.view_layer.objects.active = root