import bpy
import bmesh
import math
from mathutils import Vector

# ---------------------------------------------------------------------------
# Brown Pelican (Pelecanus occidentalis) - procedural model, Blender 4.2
# Units: meters. +X = anterior (bill direction), +Z = up, Y = lateral.
# ---------------------------------------------------------------------------

for _ob in list(bpy.data.objects):
    try:
        bpy.data.objects.remove(_ob, do_unlink=True)
    except Exception:
        pass

SCENE = bpy.context.scene
COLL = SCENE.collection
PARTS = []


# ----------------------------- materials -----------------------------------
def make_mat(name, color, rough=0.6, sss=0.0):
    mat = bpy.data.materials.new(name)
    try:
        mat.diffuse_color = (color[0], color[1], color[2], 1.0)
    except Exception:
        pass
    bsdf = None
    try:
        mat.use_nodes = True
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf is not None:
            bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
            bsdf.inputs["Roughness"].default_value = float(rough)
    except Exception:
        pass
    if sss > 0.0 and bsdf is not None:
        try:
            bsdf.inputs["Subsurface Weight"].default_value = float(sss)
        except Exception:
            pass
    return mat


M = {
    "body": make_mat("Pel_Body_GrayBrown", (0.13, 0.115, 0.10), 0.75),
    "belly": make_mat("Pel_Belly_DarkBrown", (0.055, 0.045, 0.04), 0.8),
    "neck_white": make_mat("Pel_Neck_White", (0.80, 0.78, 0.72), 0.8),
    "chestnut": make_mat("Pel_Hindneck_Chestnut", (0.15, 0.05, 0.025), 0.75),
    "silver": make_mat("Pel_Coverts_Silver", (0.30, 0.28, 0.25), 0.7),
    "flight": make_mat("Pel_FlightFeathers_Dark", (0.03, 0.028, 0.026), 0.6),
    "head_white": make_mat("Pel_Head_White", (0.82, 0.80, 0.74), 0.8),
    "crown": make_mat("Pel_Crown_Yellow", (0.85, 0.70, 0.35), 0.8),
    "bill": make_mat("Pel_Bill_Gray", (0.35, 0.30, 0.26), 0.45),
    "nail": make_mat("Pel_Bill_Nail", (0.45, 0.12, 0.06), 0.4),
    "pouch": make_mat("Pel_Gular_Pouch", (0.05, 0.055, 0.04), 0.5, sss=0.1),
    "leg": make_mat("Pel_Leg_Skin", (0.03, 0.03, 0.03), 0.55),
    "web": make_mat("Pel_Foot_Web", (0.04, 0.038, 0.036), 0.6),
    "claw": make_mat("Pel_Claw", (0.02, 0.018, 0.016), 0.35),
    "iris": make_mat("Pel_Iris_Pale", (0.80, 0.80, 0.65), 0.15),
    "pupil": make_mat("Pel_Pupil", (0.005, 0.005, 0.005), 0.1),
    "orbital": make_mat("Pel_Orbital_Skin", (0.50, 0.30, 0.30), 0.5),
    "tail": make_mat("Pel_Tail", (0.07, 0.062, 0.055), 0.65),
}


# ----------------------------- helpers -------------------------------------
def catmull(spec, k=3):
    if k <= 1 or len(spec) < 2:
        return [tuple(s) for s in spec]
    out = []
    n = len(spec)
    for i in range(n - 1):
        p0 = spec[max(i - 1, 0)]
        p1 = spec[i]
        p2 = spec[i + 1]
        p3 = spec[min(i + 2, n - 1)]
        for s in range(k):
            t = s / float(k)
            t2 = t * t
            t3 = t2 * t
            pt = []
            for c in range(len(p1)):
                v = 0.5 * ((2.0 * p1[c]) + (-p0[c] + p2[c]) * t +
                           (2.0 * p0[c] - 5.0 * p1[c] + 4.0 * p2[c] - p3[c]) * t2 +
                           (-p0[c] + 3.0 * p1[c] - 3.0 * p2[c] + p3[c]) * t3)
                pt.append(v)
            out.append(tuple(pt))
    out.append(tuple(spec[-1]))
    return out


def frame(t, ref):
    s = ref - t * ref.dot(t)
    if s.length < 1e-6:
        alt = Vector((1.0, 0.0, 0.0)) if abs(t.x) < 0.9 else Vector((0.0, 0.0, 1.0))
        s = alt - t * alt.dot(t)
    s.normalize()
    u = t.cross(s)
    u.normalize()
    return s, u


def finish_object(name, bm, mats=None, mat_fn=None, subsurf=2, solidify=0.0):
    me = bpy.data.meshes.new(name + "_Mesh")
    bm.to_mesh(me)
    bm.free()
    for p in me.polygons:
        p.use_smooth = True
    ob = bpy.data.objects.new(name, me)
    COLL.objects.link(ob)
    if mats:
        for m in mats:
            me.materials.append(m)
        if mat_fn is not None and len(mats) > 1:
            for p in me.polygons:
                try:
                    p.material_index = int(mat_fn(Vector(p.center), Vector(p.normal)))
                except Exception:
                    p.material_index = 0
    if solidify > 0.0:
        try:
            md = ob.modifiers.new("Solidify", 'SOLIDIFY')
            md.thickness = solidify
            md.offset = 0.0
        except Exception:
            pass
    if subsurf > 0:
        try:
            md = ob.modifiers.new("Subsurf", 'SUBSURF')
            md.levels = subsurf
            md.render_levels = subsurf
        except Exception:
            pass
    PARTS.append(ob.name)
    return ob


def loft(name, spec, ref=(0.0, 1.0, 0.0), segs=16, mats=None, mat_fn=None,
         subsurf=2, profile=None, resample=3):
    spec = catmull(spec, resample)
    ref = Vector(ref)
    pts = [Vector((p[0], p[1], p[2])) for p in spec]
    n = len(pts)
    bm = bmesh.new()
    rings = []
    for i in range(n):
        if i == 0:
            t = pts[1] - pts[0]
        elif i == n - 1:
            t = pts[-1] - pts[-2]
        else:
            a = pts[i + 1] - pts[i]
            b = pts[i] - pts[i - 1]
            if a.length > 1e-9:
                a.normalize()
            if b.length > 1e-9:
                b.normalize()
            t = a + b
        if t.length < 1e-9:
            t = Vector((1.0, 0.0, 0.0))
        t.normalize()
        s, u = frame(t, ref)
        rw = max(spec[i][3], 0.0006)
        rh = max(spec[i][4], 0.0006)
        ring = []
        for j in range(segs):
            ang = 2.0 * math.pi * j / segs
            ca, sa = math.cos(ang), math.sin(ang)
            if profile is not None:
                ca, sa = profile(i, n, ca, sa)
            ring.append(bm.verts.new(pts[i] + s * (ca * rw) + u * (sa * rh)))
        rings.append(ring)
    for i in range(n - 1):
        for j in range(segs):
            jn = (j + 1) % segs
            bm.faces.new((rings[i][j], rings[i][jn], rings[i + 1][jn], rings[i + 1][j]))
    d0 = pts[0] - pts[1]
    if d0.length > 1e-9:
        d0.normalize()
    c0 = bm.verts.new(pts[0] + d0 * min(spec[0][3], spec[0][4]) * 0.5)
    for j in range(segs):
        bm.faces.new((rings[0][(j + 1) % segs], rings[0][j], c0))
    d1 = pts[-1] - pts[-2]
    if d1.length > 1e-9:
        d1.normalize()
    c1 = bm.verts.new(pts[-1] + d1 * min(spec[-1][3], spec[-1][4]) * 0.5)
    for j in range(segs):
        bm.faces.new((rings[-1][j], rings[-1][(j + 1) % segs], c1))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    return finish_object(name, bm, mats, mat_fn, subsurf)


def feather(name, base, tip, half_w, ref, mat, bow=(0.0, 0.0, 0.0), thick=0.0022, subsurf=1):
    base = Vector(base)
    tip = Vector(tip)
    bow = Vector(bow)
    ts = [0.0, 0.12, 0.35, 0.6, 0.82, 0.95, 1.0]
    wf = [0.25, 0.75, 1.0, 0.95, 0.7, 0.35, 0.05]
    spec = []
    for t, w in zip(ts, wf):
        p = base.lerp(tip, t) + bow * math.sin(math.pi * t)
        spec.append((p.x, p.y, p.z, max(half_w * w, 0.0008), thick * (1.0 - 0.5 * t)))
    return loft(name, spec, ref=ref, segs=8, mats=[mat], subsurf=subsurf, resample=1)


def sphere(name, loc, r, scale=(1.0, 1.0, 1.0), mat=None):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, radius=r, location=loc)
    ob = bpy.context.view_layer.objects.active
    if ob is None:
        ob = bpy.context.object
    if ob is None or ob.data is None:
        return None
    ob.name = name
    ob.scale = scale
    for p in ob.data.polygons:
        p.use_smooth = True
    if mat is not None:
        ob.data.materials.append(mat)
    PARTS.append(ob.name)
    return ob


def bill_profile(i, n, ca, sa):
    # flatter ventral (cutting-edge) side, gently arched culmen
    if sa < 0.0:
        sa *= 0.55
    return ca, sa


def pouch_profile(i, n, ca, sa):
    # skin attached along the rami (flat top), sagging bag below
    if sa > 0.0:
        sa *= 0.45
    return ca, sa


# ----------------------------- body + neck ---------------------------------
def body_mat_fn(c, nrm):
    if c.z > 0.66:
        return 3 if nrm.x < -0.15 else 2
    if nrm.z < -0.4 and c.x > -0.38:
        return 1
    if nrm.z > 0.5 and c.x < 0.2:
        return 4
    return 0


body_spec = [
    (-0.42, 0.0, 0.470, 0.030, 0.030),
    (-0.38, 0.0, 0.465, 0.080, 0.070),
    (-0.30, 0.0, 0.460, 0.130, 0.120),
    (-0.18, 0.0, 0.455, 0.165, 0.150),
    (-0.05, 0.0, 0.460, 0.180, 0.160),
    (0.07, 0.0, 0.480, 0.170, 0.160),
    (0.17, 0.0, 0.520, 0.140, 0.140),
    (0.24, 0.0, 0.580, 0.100, 0.105),
    (0.27, 0.0, 0.650, 0.072, 0.075),
    (0.28, 0.0, 0.720, 0.058, 0.060),
    (0.265, 0.0, 0.800, 0.052, 0.054),
    (0.255, 0.0, 0.870, 0.049, 0.050),
    (0.27, 0.0, 0.940, 0.047, 0.048),
    (0.30, 0.0, 0.990, 0.047, 0.050),
    (0.335, 0.0, 1.020, 0.045, 0.050),
]
BODY = loft("Pelican_Body_Neck", body_spec, ref=(0, 1, 0), segs=24,
            mats=[M["body"], M["belly"], M["neck_white"], M["chestnut"], M["silver"]],
            mat_fn=body_mat_fn, subsurf=2)
BODY_NAME = BODY.name

# ----------------------------- head ----------------------------------------
head_spec = [
    (0.29, 0.0, 1.025, 0.035, 0.040),
    (0.32, 0.0, 1.035, 0.050, 0.055),
    (0.36, 0.0, 1.045, 0.055, 0.058),
    (0.40, 0.0, 1.047, 0.050, 0.052),
    (0.44, 0.0, 1.042, 0.040, 0.040),
    (0.475, 0.0, 1.035, 0.030, 0.025),
]
loft("Pelican_Head", head_spec, ref=(0, 1, 0), segs=20,
     mats=[M["head_white"], M["crown"]],
     mat_fn=lambda c, n: 1 if (n.z > 0.55 and c.x < 0.43) else 0, subsurf=2)

# ----------------------------- bill ----------------------------------------
upper_spec = [
    (0.455, 0.0, 1.037, 0.030, 0.022),
    (0.50, 0.0, 1.030, 0.027, 0.015),
    (0.58, 0.0, 1.020, 0.023, 0.011),
    (0.66, 0.0, 1.009, 0.021, 0.0095),
    (0.74, 0.0, 0.999, 0.020, 0.009),
    (0.785, 0.0, 0.993, 0.018, 0.009),
    (0.808, 0.0, 0.986, 0.013, 0.009),
    (0.818, 0.0, 0.974, 0.008, 0.007),
    (0.816, 0.0, 0.962, 0.003, 0.003),
]
loft("Pelican_Upper_Mandible", upper_spec, ref=(0, 1, 0), segs=16,
     mats=[M["bill"]], subsurf=2, profile=bill_profile)

nail_spec = [
    (0.790, 0.0, 0.9925, 0.0180, 0.0100),
    (0.808, 0.0, 0.9860, 0.0145, 0.0100),
    (0.8185, 0.0, 0.9740, 0.0092, 0.0080),
    (0.8165, 0.0, 0.9610, 0.0036, 0.0036),
]
loft("Pelican_Bill_Nail", nail_spec, ref=(0, 1, 0), segs=16,
     mats=[M["nail"]], subsurf=2, profile=bill_profile)

for sgn, tag in ((1.0, "L"), (-1.0, "R")):
    ramus_spec = [
        (0.44, sgn * 0.030, 1.020, 0.006, 0.006),
        (0.50, sgn * 0.026, 1.017, 0.005, 0.005),
        (0.60, sgn * 0.021, 1.008, 0.005, 0.0045),
        (0.70, sgn * 0.017, 0.998, 0.0045, 0.0045),
        (0.78, sgn * 0.010, 0.986, 0.004, 0.004),
        (0.805, sgn * 0.004, 0.980, 0.003, 0.003),
    ]
    loft("Pelican_Lower_Mandible_Ramus_" + tag, ramus_spec, ref=(0, 1, 0), segs=10,
         mats=[M["bill"]], subsurf=2)

pouch_spec = [
    (0.33, 0.0, 1.000, 0.024, 0.024),
    (0.37, 0.0, 0.998, 0.030, 0.034),
    (0.44, 0.0, 1.005, 0.028, 0.034),
    (0.52, 0.0, 1.0045, 0.023, 0.026),
    (0.60, 0.0, 1.0015, 0.019, 0.017),
    (0.68, 0.0, 0.997, 0.014, 0.010),
    (0.76, 0.0, 0.9885, 0.007, 0.005),
]
loft("Pelican_Gular_Pouch", pouch_spec, ref=(0, 1, 0), segs=18,
     mats=[M["pouch"]], subsurf=2, profile=pouch_profile)

# ----------------------------- eyes ----------------------------------------
for sgn, tag in ((1.0, "L"), (-1.0, "R")):
    sphere("Pelican_Orbital_Skin_" + tag, (0.415, sgn * 0.041, 1.058), 0.016,
           scale=(1.1, 0.35, 0.85), mat=M["orbital"])
    sphere("Pelican_Eye_" + tag, (0.415, sgn * 0.040, 1.058), 0.0105, mat=M["iris"])
    sphere("Pelican_Pupil_" + tag, (0.4155, sgn * 0.0495, 1.0582), 0.0042,
           scale=(1.0, 0.4, 1.0), mat=M["pupil"])

# ----------------------------- folded wings --------------------------------
def wing_mat_fn(c, nrm):
    if c.z < 0.455 or c.x < -0.22:
        return 1
    return 0


for sgn, tag in ((1.0, "L"), (-1.0, "R")):
    wing_spec = [
        (0.20, sgn * 0.150, 0.570, 0.040, 0.030),
        (0.12, sgn * 0.172, 0.550, 0.080, 0.035),
        (0.00, sgn * 0.182, 0.525, 0.110, 0.035),
        (-0.12, sgn * 0.175, 0.505, 0.110, 0.030),
        (-0.24, sgn * 0.155, 0.495, 0.090, 0.025),
        (-0.34, sgn * 0.130, 0.495, 0.065, 0.020),
    ]
    loft("Pelican_Wing_Folded_" + tag, wing_spec, ref=(0, 0, 1), segs=18,
         mats=[M["silver"], M["flight"]], mat_fn=wing_mat_fn, subsurf=2)

    # primaries (outer flight feathers) crossing over the tail
    for k in range(7):
        base = (-0.20 - 0.012 * k, sgn * (0.150 - 0.004 * k), 0.500 + 0.006 * k)
        tip = (-0.50 - 0.015 * k, sgn * (0.085 - 0.005 * k), 0.515 + 0.004 * k)
        feather("Pelican_Primary_%s_%d" % (tag, k + 1), base, tip, 0.022, (0, 0, 1),
                M["flight"], bow=(0.0, sgn * 0.010, 0.004))

    # tertials (inner feathers draping over the back)
    for k in range(3):
        base = (-0.05 - 0.04 * k, sgn * (0.120 - 0.01 * k), 0.600 - 0.01 * k)
        tip = (-0.30 - 0.03 * k, sgn * (0.080 - 0.008 * k), 0.560 - 0.012 * k)
        feather("Pelican_Tertial_%s_%d" % (tag, k + 1), base, tip, 0.030, (0, 1, 0),
                M["body"], bow=(0.0, 0.0, 0.012), thick=0.003)

# ----------------------------- tail ----------------------------------------
NREC = 12
for i in range(NREC):
    f = (i - (NREC - 1) / 2.0) / ((NREC - 1) / 2.0)
    base = (-0.37, f * 0.030, 0.470)
    tip = (-0.56 + 0.03 * f * f, f * 0.075, 0.466 + 0.003 * (1.0 - abs(f)))
    feather("Pelican_Rectrix_%02d" % (i + 1), base, tip, 0.014, (0, 1, 0),
            M["tail"], bow=(0.0, 0.0, 0.004), thick=0.002)

# ----------------------------- legs & feet ---------------------------------
TOES = [("I_Hallux", -105.0, 0.045), ("II", -30.0, 0.075),
        ("III", -2.0, 0.095), ("IV", 26.0, 0.090)]
TS = [0.05, 0.2, 0.4, 0.6, 0.8, 0.95]

for sgn, tag in ((1.0, "L"), (-1.0, "R")):
    y = sgn * 0.08
    # feathered thigh / upper tibiotarsus emerging from belly
    loft("Pelican_Thigh_" + tag,
         [(0.03, sgn * 0.070, 0.37, 0.028, 0.028),
          (0.02, sgn * 0.076, 0.31, 0.024, 0.024),
          (0.015, sgn * 0.078, 0.26, 0.017, 0.017)],
         ref=(1, 0, 0), segs=14, mats=[M["belly"]], subsurf=2)
    # bare tibiotarsus, intertarsal joint, tarsometatarsus
    loft("Pelican_Leg_" + tag,
         [(0.015, y, 0.275, 0.014, 0.014),
          (0.006, y, 0.200, 0.012, 0.012),
          (0.000, y, 0.160, 0.013, 0.014),
          (0.012, y + sgn * 0.002, 0.100, 0.010, 0.012),
          (0.028, y + sgn * 0.005, 0.045, 0.011, 0.012),
          (0.035, y + sgn * 0.006, 0.026, 0.013, 0.011)],
         ref=(0, 1, 0), segs=12, mats=[M["leg"]], subsurf=2)

    F = Vector((0.035, y + sgn * 0.006, 0.022))
    toe_samples = []
    for tname, ang, L in TOES:
        a = math.radians(ang)
        d = Vector((math.cos(a), sgn * math.sin(a), 0.0))
        spec = []
        samples = []
        for t in [0.0] + TS + [1.0]:
            p = F + d * (L * t)
            z = 0.022 - 0.014 * min(t / 0.3, 1.0)
            if tname.startswith("I_"):
                z = 0.020 - 0.010 * min(t / 0.4, 1.0)
            p.z = z
            r = 0.0065 - 0.0025 * t
            spec.append((p.x, p.y, p.z, r, r * 0.85))
            if t in TS:
                samples.append(p.copy())
        toe_samples.append(samples)
        loft("Pelican_Toe_%s_%s" % (tname, tag), spec, ref=(0, 0, 1), segs=10,
             mats=[M["leg"]], subsurf=2, resample=2)
        tip = F + d * L
        tip.z = spec[-1][2]
        c1 = tip + d * 0.008 + Vector((0.0, 0.0, -0.002))
        c2 = tip + d * 0.013 + Vector((0.0, 0.0, -0.006))
        loft("Pelican_Claw_%s_%s" % (tname, tag),
             [(tip.x, tip.y, tip.z, 0.0035, 0.003),
              (c1.x, c1.y, c1.z, 0.0025, 0.002),
              (c2.x, c2.y, c2.z, 0.0006, 0.0006)],
             ref=(0, 0, 1), segs=8, mats=[M["claw"]], subsurf=1, resample=2)

    # totipalmate webbing: all four toes joined (I-II, II-III, III-IV)
    for wi, (ia, ib) in enumerate(((0, 1), (1, 2), (2, 3))):
        A = toe_samples[ia]
        B = toe_samples[ib]
        bm = bmesh.new()
        rows = []
        nrows = min(len(A), len(B))
        for i in range(nrows):
            a = A[i]
            b = B[i]
            m = (a + b) * 0.5
            if i == nrows - 1:
                m = F + (m - F) * 0.78
                m.z = (a.z + b.z) * 0.5
            rows.append([bm.verts.new(a), bm.verts.new(m), bm.verts.new(b)])
        for i in range(nrows - 1):
            for j in range(2):
                bm.faces.new((rows[i][j], rows[i][j + 1], rows[i + 1][j + 1], rows[i + 1][j]))
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
        finish_object("Pelican_Web_%d_%s" % (wi + 1, tag), bm, [M["web"]],
                      None, subsurf=1, solidify=0.0016)

# ----------------------------- unify into one specimen ---------------------
final_ok = False
try:
    bpy.ops.object.select_all(action='DESELECT')
    objs = [bpy.data.objects.get(n) for n in PARTS]
    objs = [o for o in objs if o is not None]
    for o in objs:
        o.select_set(True)
    body_ob = bpy.data.objects.get(BODY_NAME)
    if body_ob is None and objs:
        body_ob = objs[0]
    bpy.context.view_layer.objects.active = body_ob
    bpy.ops.object.convert(target='MESH')

    objs = [bpy.data.objects.get(n) for n in PARTS]
    objs = [o for o in objs if o is not None]
    bpy.ops.object.select_all(action='DESELECT')
    for o in objs:
        o.select_set(True)
    body_ob = bpy.data.objects.get(BODY_NAME)
    if body_ob is None and objs:
        body_ob = objs[0]
    bpy.context.view_layer.objects.active = body_ob
    bpy.ops.object.join()
    final = bpy.context.view_layer.objects.active
    if final is not None:
        final.name = "BrownPelican_Pelecanus_occidentalis"
        if final.data is not None:
            for p in final.data.polygons:
                p.use_smooth = True
        final_ok = True
except Exception as e:
    print("Join step skipped:", e)

if not final_ok:
    try:
        root = bpy.data.objects.new("BrownPelican_Pelecanus_occidentalis", None)
        COLL.objects.link(root)
        for n in PARTS:
            o = bpy.data.objects.get(n)
            if o is not None and o != root:
                o.parent = root
    except Exception as e:
        print("Parenting fallback failed:", e)

print("Brown pelican generated with", len(PARTS), "parts.")