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

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

def clean_scene():
    bpy.ops.object.select_all(action='DESELECT')
    for obj in list(bpy.data.objects):
        obj.select_set(True)
    bpy.ops.object.delete()
    for block in (bpy.data.meshes, bpy.data.materials, bpy.data.curves):
        for b in list(block):
            if b.users == 0:
                block.remove(b)


def make_material(name, base_color, rough=0.6, metallic=0.0, subsurf=0.0, sss_color=None):
    """Create a Principled BSDF material, defensively handling 4.x socket names."""
    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:
        return mat
    inp = bsdf.inputs

    def set_socket(key, value):
        try:
            sock = inp.get(key)
            if sock is not None:
                sock.default_value = value
        except Exception:
            pass

    # Base color (RGBA)
    try:
        set_socket('Base Color', (base_color[0], base_color[1], base_color[2], 1.0))
    except Exception:
        pass
    set_socket('Roughness', float(rough))
    set_socket('Metallic', float(metallic))
    # Subsurface changed name in 4.x
    if subsurf > 0.0:
        set_socket('Subsurface Weight', float(subsurf))
        set_socket('Subsurface', float(subsurf))  # fallback for older naming
        if sss_color is not None:
            try:
                set_socket('Subsurface Color',
                           (sss_color[0], sss_color[1], sss_color[2], 1.0))
            except Exception:
                pass
    return mat


def assign_material(obj, mat):
    if obj is None or obj.data is None or mat 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
    try:
        for p in obj.data.polygons:
            p.use_smooth = True
    except Exception:
        pass


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 add_cone(r1=1.0, r2=0.0, depth=2.0, location=(0, 0, 0), verts=24,
             rot=(0, 0, 0)):
    bpy.ops.mesh.primitive_cone_add(radius1=r1, radius2=r2, depth=depth,
                                    location=location, vertices=verts,
                                    rotation=rot)
    return bpy.context.active_object


def apply_all_transforms(obj):
    if obj is None:
        return
    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    try:
        bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    except Exception:
        pass


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


# ----------------------------------------------------------------------
# Body-part builders
# Coordinate convention: +X forward (head), +Z up, +Y = left side.
# ----------------------------------------------------------------------

def build_body():
    """Large elongated torso of the pelican."""
    obj = add_uv_sphere(radius=1.0, location=(0.0, 0.0, 0.0), segments=48, rings=28)
    obj.name = "Pelican_Body"
    # Elongate along X (front-back) and shape like a plump waterbird body.
    me = obj.data
    for v in me.vertices:
        co = v.co
        x = co.x
        # stretch length
        co.x = x * 2.3
        # taper toward tail (-X) and slightly at breast
        taper = 1.0
        if x < 0:
            taper = 1.0 + 0.15 * x  # narrows toward tail
        co.y *= (0.85 * taper)
        co.z *= (1.0 * taper)
        # deepen the belly / breast
        if co.z < 0:
            co.z *= 1.15
    # Tilt body so breast is a little lower than back
    obj.rotation_euler = (0, math.radians(-6), 0)
    apply_all_transforms(obj)
    obj.location = (0.0, 0.0, 1.6)
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_neck():
    """Long S-curved neck built from a swept series of spheres via a curve+bevel."""
    curve_data = bpy.data.curves.new('NeckCurve', 'CURVE')
    curve_data.dimensions = '3D'
    curve_data.resolution_u = 12
    spline = curve_data.splines.new('BEZIER')
    pts = [
        (1.4, 0.0, 1.9),
        (2.1, 0.0, 2.7),
        (2.2, 0.0, 3.6),
        (2.6, 0.0, 4.2),
        (3.3, 0.0, 4.4),
    ]
    spline.bezier_points.add(len(pts) - 1)
    for i, p in enumerate(pts):
        bp = spline.bezier_points[i]
        bp.co = Vector(p)
        bp.handle_left_type = 'AUTO'
        bp.handle_right_type = 'AUTO'
    curve_data.bevel_depth = 0.32
    curve_data.bevel_resolution = 8
    # Taper the neck: thicker at base, thinner near head
    curve_data.use_fill_caps = True
    obj = bpy.data.objects.new('Pelican_Neck', curve_data)
    bpy.context.collection.objects.link(obj)
    # taper via radius per point
    radii = [1.15, 1.0, 0.85, 0.75, 0.7]
    for i, r in enumerate(radii):
        spline.bezier_points[i].radius = r
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    bpy.ops.object.convert(target='MESH')
    obj = bpy.context.active_object
    shade_smooth(obj)
    return obj


def build_head():
    obj = add_uv_sphere(radius=0.55, location=(3.35, 0.0, 4.45), segments=32, rings=20)
    obj.name = "Pelican_Head"
    me = obj.data
    for v in me.vertices:
        co = v.co
        co.x *= 1.35   # elongate snout direction
        co.z *= 1.0
        co.y *= 0.95
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_upper_bill():
    """Long flat upper mandible characteristic of pelicans."""
    obj = add_cone(r1=0.28, r2=0.05, depth=2.6, location=(4.9, 0.0, 4.45),
                   verts=20, rot=(0, math.radians(90), 0))
    obj.name = "Pelican_UpperBill"
    me = obj.data
    for v in me.vertices:
        co = v.co
        # flatten vertically, widen slightly horizontally
        co.z *= 0.35
        co.y *= 1.15
    # slight downward hook at tip
    apply_all_transforms(obj)
    for v in obj.data.vertices:
        if v.co.x > 5.8:
            v.co.z -= (v.co.x - 5.8) * 0.35
    obj.location.z += 0.08
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_lower_bill():
    """Lower mandible - thin bony frame supporting the pouch."""
    obj = add_cone(r1=0.24, r2=0.04, depth=2.6, location=(4.9, 0.0, 4.2),
                   verts=16, rot=(0, math.radians(90), 0))
    obj.name = "Pelican_LowerBill"
    for v in obj.data.vertices:
        v.co.z *= 0.25
        v.co.y *= 1.25
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_pouch():
    """The iconic gular pouch hanging below the lower mandible."""
    obj = add_uv_sphere(radius=0.6, location=(4.7, 0.0, 3.7), segments=32, rings=20)
    obj.name = "Pelican_Pouch"
    me = obj.data
    for v in me.vertices:
        co = v.co
        co.x *= 1.9    # extend along the bill length
        co.y *= 0.9
        # make it sag downward
        if co.z < 0:
            co.z *= 1.9
        else:
            co.z *= 0.4
    apply_all_transforms(obj)
    # position so top edge meets bill line and it droops down
    obj.location = (4.7, 0.0, 3.75)
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_eye(side):
    y = 0.42 * side
    obj = add_uv_sphere(radius=0.11, location=(3.55, y, 4.62), segments=20, rings=12)
    obj.name = "Pelican_Eye_%s" % ('L' if side > 0 else 'R')
    shade_smooth(obj)
    return obj


def build_wing(side):
    """A folded wing along each flank."""
    y = 0.85 * side
    obj = add_uv_sphere(radius=0.6, location=(-0.1, y, 1.9), segments=28, rings=16)
    obj.name = "Pelican_Wing_%s" % ('L' if side > 0 else 'R')
    me = obj.data
    for v in me.vertices:
        co = v.co
        co.x *= 2.6     # long folded wing front-to-back
        co.z *= 0.9
        co.y *= 0.45    # flatten against body
    apply_all_transforms(obj)
    # taper the trailing (rear) tip like primaries
    for v in obj.data.vertices:
        if v.co.x < -1.3:
            v.co.z *= 0.4
            v.co.y *= 0.5
    obj.location.x -= 0.3
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_tail():
    obj = add_cone(r1=0.5, r2=0.12, depth=1.4, location=(-2.6, 0.0, 1.65),
                   verts=16, rot=(0, math.radians(-90), 0))
    obj.name = "Pelican_Tail"
    for v in obj.data.vertices:
        v.co.z *= 0.35   # flatten fan
        v.co.y *= 1.6
    apply_all_transforms(obj)
    shade_smooth(obj)
    return obj


def build_leg(side):
    """Short leg: thigh region hidden, visible tarsus + webbed foot."""
    y = 0.35 * side
    parts = []
    # tarsus (lower leg)
    tarsus = add_cylinder(radius=0.11, depth=0.7, location=(-0.6, y, 0.75),
                          verts=14)
    tarsus.name = "Pelican_Tarsus_%s" % ('L' if side > 0 else 'R')
    shade_smooth(tarsus)
    parts.append(tarsus)

    # foot base
    foot = add_uv_sphere(radius=0.12, location=(-0.55, y, 0.42), segments=16, rings=10)
    foot.name = "Pelican_Foot_%s" % ('L' if side > 0 else 'R')
    for v in foot.data.vertices:
        v.co.x *= 2.4
        v.co.z *= 0.35
        v.co.y *= 1.6
    apply_all_transforms(foot)
    foot.location = (-0.35, y, 0.4)
    apply_all_transforms(foot)
    shade_smooth(foot)
    parts.append(foot)

    # webbed toes (totipalmate - all four toes joined)
    web = add_cone(r1=0.34, r2=0.06, depth=0.65, location=(0.05, y, 0.38),
                   verts=8, rot=(0, math.radians(90), 0))
    web.name = "Pelican_Web_%s" % ('L' if side > 0 else 'R')
    for v in web.data.vertices:
        v.co.z *= 0.12
        v.co.y *= 1.5
    apply_all_transforms(web)
    shade_smooth(web)
    parts.append(web)
    return parts


# ----------------------------------------------------------------------
# Assemble the pelican
# ----------------------------------------------------------------------

def main():
    clean_scene()

    # Materials
    mat_feather_body = make_material("Pelican_Feathers_Body", (0.42, 0.36, 0.30),
                                     rough=0.75, subsurf=0.05,
                                     sss_color=(0.5, 0.42, 0.35))
    mat_feather_dark = make_material("Pelican_Feathers_Dark", (0.20, 0.18, 0.16),
                                     rough=0.8)
    mat_head = make_material("Pelican_Head_Feathers", (0.95, 0.92, 0.80),
                             rough=0.7)
    mat_bill = make_material("Pelican_Bill", (0.75, 0.68, 0.55), rough=0.4)
    mat_pouch = make_material("Pelican_Pouch", (0.55, 0.28, 0.18), rough=0.35,
                              subsurf=0.15, sss_color=(0.7, 0.3, 0.2))
    mat_eye = make_material("Pelican_Eye", (0.02, 0.02, 0.02), rough=0.15)
    mat_leg = make_material("Pelican_Leg", (0.15, 0.15, 0.16), rough=0.5)

    parts = []

    body = build_body()
    assign_material(body, mat_feather_body)
    parts.append(body)

    neck = build_neck()
    assign_material(neck, mat_head)
    parts.append(neck)

    head = build_head()
    assign_material(head, mat_head)
    parts.append(head)

    upper_bill = build_upper_bill()
    assign_material(upper_bill, mat_bill)
    parts.append(upper_bill)

    lower_bill = build_lower_bill()
    assign_material(lower_bill, mat_bill)
    parts.append(lower_bill)

    pouch = build_pouch()
    assign_material(pouch, mat_pouch)
    parts.append(pouch)

    for side in (1, -1):
        eye = build_eye(side)
        assign_material(eye, mat_eye)
        parts.append(eye)

        wing = build_wing(side)
        assign_material(wing, mat_feather_dark)
        parts.append(wing)

        for lp in build_leg(side):
            assign_material(lp, mat_leg)
            parts.append(lp)

    tail = build_tail()
    assign_material(tail, mat_feather_dark)
    parts.append(tail)

    # Subsurf the soft-body organic parts (leave feet flat-ish)
    for p in parts:
        if p is None:
            continue
        if any(k in p.name for k in ("Body", "Neck", "Head", "Wing", "Pouch",
                                     "Tail", "Foot")):
            add_subsurf(p, levels=2)

    # Group everything into one specimen by joining.
    bpy.ops.object.select_all(action='DESELECT')
    valid = [p for p in parts if p is not None and p.name in bpy.data.objects]
    for p in valid:
        p.select_set(True)
    if valid:
        bpy.context.view_layer.objects.active = valid[0]
        try:
            bpy.ops.object.join()
        except Exception:
            pass
        pelican = bpy.context.active_object
        if pelican is not None:
            pelican.name = "Pelecanus_occidentalis"

    bpy.ops.object.select_all(action='DESELECT')


main()