import bpy
import bmesh
import math
from mathutils import Vector

# Remove the default startup cube only, if present
for o in list(bpy.data.objects):
    if o.name == "Cube" and o.type == 'MESH':
        bpy.data.objects.remove(o, do_unlink=True)

COL = bpy.context.scene.collection
PARTS = []


def V(*a):
    return Vector(a)


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


M = {
    'back': mk('Plumage_Back', (0.30, 0.29, 0.27), 0.85),
    'belly': mk('Plumage_Belly', (0.10, 0.08, 0.065), 0.85),
    'wing': mk('Wing_Coverts', (0.42, 0.40, 0.37), 0.8),
    'prim': mk('Primaries', (0.04, 0.035, 0.03), 0.7),
    'sec': mk('Secondaries', (0.12, 0.10, 0.09), 0.75),
    'tail': mk('Rectrices', (0.20, 0.18, 0.16), 0.8),
    'neck': mk('Hindneck_Chestnut', (0.20, 0.08, 0.04), 0.8),
    'white': mk('Neck_White', (0.85, 0.84, 0.80), 0.8),
    'head': mk('Head_White', (0.90, 0.88, 0.80), 0.8),
    'crown': mk('Crown_Yellow', (0.85, 0.72, 0.35), 0.8),
    'bill': mk('Bill', (0.42, 0.38, 0.34), 0.45),
    'nail': mk('Bill_Nail', (0.60, 0.22, 0.10), 0.4),
    'pouch': mk('Gular_Pouch', (0.07, 0.08, 0.06), 0.5, 0.1),
    'skin': mk('Orbital_Skin', (0.75, 0.45, 0.42), 0.5, 0.2),
    'iris': mk('Iris', (0.88, 0.88, 0.82), 0.2),
    'pupil': mk('Pupil', (0.01, 0.01, 0.01), 0.1),
    'leg': mk('Leg_Skin', (0.05, 0.05, 0.05), 0.6),
    'claw': mk('Claw', (0.08, 0.07, 0.06), 0.4),
    'nostril': mk('Nostril', (0.02, 0.02, 0.02), 0.5),
}


def link(name, bm, mats, sub=0, solid=0.0):
    me = bpy.data.meshes.new(name + "_mesh")
    try:
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    except Exception:
        pass
    bm.to_mesh(me)
    bm.free()
    for p in me.polygons:
        p.use_smooth = True
    ob = bpy.data.objects.new(name, me)
    COL.objects.link(ob)
    for m in mats:
        me.materials.append(m)
    if solid > 0:
        md = ob.modifiers.new("Solidify", 'SOLIDIFY')
        md.thickness = solid
        md.offset = 0.0
    if sub:
        md = ob.modifiers.new("Subsurf", 'SUBSURF')
        md.levels = sub
        md.render_levels = sub
    PARTS.append(ob)
    return ob


def tube(name, pts, rads, mat, segs=16, ref=(0, 1, 0), caps=(True, True),
         tips=(None, None), sub=1):
    bm = bmesh.new()
    P = [Vector(p) for p in pts]
    n = len(P)
    R = Vector(ref)
    rings = []
    for i, p in enumerate(P):
        t = (P[min(i + 1, n - 1)] - P[max(i - 1, 0)])
        if t.length < 1e-9:
            t = Vector((1, 0, 0))
        t.normalize()
        r = rads[i]
        if isinstance(r, (int, float)):
            rx, rz = r, r
        else:
            rx, rz = r
        s = R - t * R.dot(t)
        if s.length < 1e-4:
            alt = Vector((0, 0, 1)) if abs(t.z) < 0.9 else Vector((1, 0, 0))
            s = alt - t * alt.dot(t)
        s.normalize()
        u = t.cross(s)
        ring = []
        for k in range(segs):
            a = 2 * math.pi * k / segs
            ring.append(bm.verts.new(p + s * (rx * math.cos(a)) + u * (rz * math.sin(a))))
        rings.append(ring)
    for i in range(n - 1):
        a, b = rings[i], rings[i + 1]
        for k in range(segs):
            k2 = (k + 1) % segs
            bm.faces.new((a[k], a[k2], b[k2], b[k]))
    if caps[0]:
        c = bm.verts.new(Vector(tips[0]) if tips[0] is not None else P[0])
        for k in range(segs):
            bm.faces.new((c, rings[0][(k + 1) % segs], rings[0][k]))
    if caps[1]:
        c = bm.verts.new(Vector(tips[1]) if tips[1] is not None else P[-1])
        for k in range(segs):
            bm.faces.new((c, rings[-1][k], rings[-1][(k + 1) % segs]))
    mats = mat if isinstance(mat, list) else [mat]
    return link(name, bm, mats, sub)


def ellipsoid(name, c, axis, a, b, cc, mat, ref=(0, 1, 0), n=12, segs=20, sub=1):
    c = Vector(c)
    ax = Vector(axis).normalized()
    pts, rads = [], []
    for i in range(1, n):
        th = math.pi * i / n
        pts.append(c + ax * (-a * math.cos(th)))
        rads.append((b * math.sin(th), cc * math.sin(th)))
    return tube(name, pts, rads, mat, segs, ref, tips=(c - ax * a, c + ax * a), sub=sub)


def lerp(a, b, t):
    return a + (b - a) * t


# ---------------- BODY ----------------
R0 = V(-0.36, 0.0, 0.41)
R1 = V(0.22, 0.0, 0.53)
BAX = (R1 - R0).normalized()
BT = [(0.0, 0.03, 0.025, 0.0), (0.08, 0.07, 0.06, 0.0), (0.2, 0.11, 0.10, -0.01),
      (0.4, 0.145, 0.135, -0.02), (0.6, 0.15, 0.14, -0.02), (0.78, 0.14, 0.13, -0.01),
      (0.92, 0.11, 0.11, 0.0), (1.0, 0.07, 0.08, 0.0)]


def body_prof(s):
    for i in range(len(BT) - 1):
        s0, s1 = BT[i][0], BT[i + 1][0]
        if s0 <= s <= s1:
            f = (s - s0) / (s1 - s0)
            return tuple(lerp(BT[i][j], BT[i + 1][j], f) for j in (1, 2, 3))
    return BT[-1][1:]


bp, br = [], []
for i in range(16):
    s = i / 15.0
    ry, rz, dz = body_prof(s)
    bp.append(lerp(R0, R1, s) + V(0, 0, dz))
    br.append((ry, rz))
body = tube("Body", bp, br, [M['back'], M['belly']], segs=24,
            tips=(R0 - BAX * 0.02, R1 + BAX * 0.03), sub=2)
slope = (R1.z - R0.z) / (R1.x - R0.x)
for p in body.data.polygons:
    zl = R0.z + (p.center.x - R0.x) * slope
    if p.center.z < zl - 0.04:
        p.material_index = 1

# undertail coverts
ellipsoid("Undertail_Coverts", (-0.30, 0, 0.36), (-1, 0, 0.2), 0.07, 0.05, 0.035, M['belly'])

# ---------------- NECK (S-curve) ----------------
NP = [(0.16, 0, 0.52), (0.24, 0, 0.60), (0.27, 0, 0.70), (0.24, 0, 0.80),
      (0.19, 0, 0.88), (0.18, 0, 0.96), (0.22, 0, 1.02), (0.28, 0, 1.05)]
NR = [0.085, 0.068, 0.055, 0.046, 0.042, 0.042, 0.045, 0.046]
neck = tube("Neck", NP, NR, [M['neck'], M['white']], segs=18, sub=2)
for p in neck.data.polygons:
    if (p.normal.x > 0.25 and p.center.z > 0.6) or p.center.z > 0.99:
        p.material_index = 1

# ---------------- HEAD ----------------
H = V(0.305, 0.0, 1.055)
head = ellipsoid("Head", H, (1, 0, -0.1), 0.07, 0.047, 0.052, [M['head'], M['crown']], segs=22, sub=2)
for p in head.data.polygons:
    if p.normal.z > 0.6:
        p.material_index = 1

# nape crest tuft
for i, yy in enumerate((-0.012, 0.0, 0.012)):
    c0 = H + V(-0.045, yy, 0.03)
    tube("Nape_Crest_%d" % i, [c0, c0 + V(-0.03, yy * 0.5, -0.005), c0 + V(-0.055, yy, -0.02)],
         [0.008, 0.006, 0.0015], M['head'], segs=8)

# ---------------- BILL ----------------
B0 = V(0.355, 0.0, 1.05)
D = V(0.30, 0.0, -0.085).normalized()
L = 0.31
DOWN = V(D.z, 0.0, -D.x)
UP = -DOWN
Y = V(0, 1, 0)

ut = [0.0, 0.1, 0.3, 0.5, 0.7, 0.85, 0.95]
ur = [(0.03, 0.02), (0.027, 0.014), (0.024, 0.011), (0.022, 0.010),
      (0.020, 0.009), (0.018, 0.009), (0.014, 0.009)]
tube("Upper_Mandible", [B0 + D * L * t for t in ut], ur, M['bill'], segs=18, sub=2)

# culmen ridge
ct = [0.05, 0.3, 0.6, 0.9]
tube("Culmen_Ridge", [B0 + D * L * t + UP * lerp(0.013, 0.008, t) for t in ct],
     [0.004, 0.0035, 0.003, 0.0025], M['bill'], segs=8)

# hooked nail at tip
tube("Bill_Nail", [B0 + D * L * 0.9, B0 + D * L * 0.98 + DOWN * 0.002,
                   B0 + D * L * 1.02 + DOWN * 0.012, B0 + D * L * 1.025 + DOWN * 0.024],
     [(0.012, 0.009), (0.011, 0.008), (0.007, 0.007), (0.002, 0.002)], M['nail'], segs=12)

# nostril slits
for sg in (1, -1):
    tube("Nostril_%s" % ("L" if sg > 0 else "R"),
         [B0 + D * L * 0.05 + UP * 0.012 + Y * sg * 0.017,
          B0 + D * L * 0.13 + UP * 0.010 + Y * sg * 0.017],
         [0.0015, 0.0012], M['nostril'], segs=6, sub=0)

# lower mandible rami
rt = [-0.05, 0.2, 0.45, 0.7, 0.9, 0.97]
for sg in (1, -1):
    tube("Lower_Mandible_%s" % ("L" if sg > 0 else "R"),
         [B0 + D * L * t + DOWN * 0.017 + Y * (sg * lerp(0.024, 0.007, max(t, 0))) for t in rt],
         [0.0065, 0.006, 0.0055, 0.005, 0.0045, 0.004], M['bill'], segs=10)

# gular pouch
pt = [-0.35, -0.2, -0.05, 0.15, 0.35, 0.55, 0.75, 0.9]
plat = [0.018, 0.022, 0.024, 0.022, 0.019, 0.015, 0.010, 0.005]
pver = [0.025, 0.035, 0.040, 0.038, 0.032, 0.024, 0.015, 0.006]
tube("Gular_Pouch", [B0 + D * L * t + DOWN * (0.017 + pver[i] * 0.7) for i, t in enumerate(pt)],
     list(zip(plat, pver)), M['pouch'], segs=18, sub=2)

# ---------------- EYES ----------------
for sg in (1, -1):
    tag = "L" if sg > 0 else "R"
    ellipsoid("Orbital_Skin_" + tag, H + V(0.03, sg * 0.036, 0.018), (0, sg, 0),
              0.004, 0.013, 0.016, M['skin'], ref=(0, 0, 1), n=8, segs=16)
    ellipsoid("Eye_" + tag, H + V(0.03, sg * 0.037, 0.018), (0, sg, 0),
              0.0095, 0.0095, 0.0095, M['iris'], ref=(0, 0, 1), n=10, segs=16)
    ellipsoid("Pupil_" + tag, H + V(0.03, sg * 0.0455, 0.018), (0, sg, 0),
              0.0015, 0.0045, 0.0045, M['pupil'], ref=(0, 0, 1), n=6, segs=12, sub=0)

# ---------------- WINGS (folded) ----------------
W = [((0.16, 0.10, 0.55), (0.02, 0.04)), ((0.08, 0.145, 0.52), (0.028, 0.08)),
     ((-0.04, 0.158, 0.49), (0.03, 0.10)), ((-0.16, 0.15, 0.47), (0.026, 0.095)),
     ((-0.28, 0.125, 0.45), (0.02, 0.07)), ((-0.38, 0.10, 0.44), (0.012, 0.04))]
for sg in (1, -1):
    tag = "L" if sg > 0 else "R"
    tube("Wing_" + tag, [V(p[0], sg * p[1], p[2]) for p, r in W], [r for p, r in W],
         M['wing'], segs=18, sub=2)
    # primaries: long dark flight feathers crossing over the tail
    for i in range(6):
        s0 = V(-0.22, sg * (0.125 - 0.004 * i), 0.43 + 0.012 * i)
        s1 = V(-0.38, sg * (0.10 - 0.005 * i), 0.425 + 0.010 * i)
        s2 = V(-0.56 + 0.025 * i, sg * (0.045 - 0.004 * i), 0.42 + 0.008 * i)
        tube("Primary_%s_%d" % (tag, i + 1), [s0, s1, s2],
             [(0.003, 0.02), (0.003, 0.016), (0.002, 0.004)], M['prim'], segs=8)
    # secondaries along the lower wing edge
    for i in range(5):
        x0 = 0.0 - 0.05 * i
        tube("Secondary_%s_%d" % (tag, i + 1),
             [V(x0, sg * 0.16, 0.44), V(x0 - 0.07, sg * 0.157, 0.42), V(x0 - 0.13, sg * 0.15, 0.405)],
             [(0.003, 0.025), (0.003, 0.022), (0.002, 0.006)], M['sec'], segs=8)
    # tertials lying over the back
    for i in range(3):
        a0 = V(-0.05 - 0.04 * i, sg * (0.10 - 0.01 * i), 0.575 - 0.008 * i)
        tube("Tertial_%s_%d" % (tag, i + 1),
             [a0, a0 + V(-0.12, -sg * 0.01, -0.02), a0 + V(-0.22, -sg * 0.02, -0.045)],
             [(0.003, 0.028), (0.003, 0.024), (0.002, 0.006)], M['wing'], segs=8, ref=(0, 0, 1))

# ---------------- TAIL (rectrices) ----------------
T0 = V(-0.33, 0.0, 0.41)
NT = 12
for i in range(NT):
    ang = math.radians(lerp(-35, 35, i / (NT - 1)))
    d = V(-math.cos(ang), math.sin(ang), -0.15).normalized()
    Lt = 0.16 + 0.012 * (1 - abs(i - (NT - 1) / 2) / ((NT - 1) / 2))
    zoff = 0.003 * (NT / 2 - abs(i - (NT - 1) / 2))
    base = T0 + V(0, math.sin(ang) * 0.03, zoff)
    tube("Rectrix_%d" % (i + 1), [base, base + d * Lt * 0.5, base + d * Lt],
         [(0.003, 0.012), (0.003, 0.02), (0.0025, 0.016)], M['tail'], segs=8, ref=(0, 0, 1))

# ---------------- LEGS & FEET ----------------
TOES = [("Hallux", -70, 0.055), ("Toe_II", -22, 0.085), ("Toe_III", 3, 0.115), ("Toe_IV", 28, 0.105)]
for sg in (1, -1):
    tag = "L" if sg > 0 else "R"
    ellipsoid("Thigh_" + tag, (-0.01, sg * 0.085, 0.29), (0.2, 0, -1), 0.06, 0.035, 0.035, M['belly'])
    tube("Leg_" + tag,
         [V(-0.01, sg * 0.09, 0.27), V(0.0, sg * 0.095, 0.17), V(0.005, sg * 0.095, 0.12),
          V(0.02, sg * 0.093, 0.06), V(0.028, sg * 0.09, 0.03)],
         [0.018, 0.015, 0.016, 0.013, 0.013], M['leg'], segs=12)
    F = V(0.03, sg * 0.09, 0.03)
    dirs = []
    for nm, deg, Lg in TOES:
        a = math.radians(deg)
        dirs.append((nm, V(math.cos(a), sg * math.sin(a), 0.0), Lg))

    def tp(dv, Lg, f, F=F):
        return F + dv * (Lg * f) + V(0, 0, -0.018 * (1 - (1 - f) ** 2))

    for nm, dv, Lg in dirs:
        fs = [0.0, 0.25, 0.5, 0.75, 1.0]
        tube("%s_%s" % (nm, tag), [tp(dv, Lg, f) for f in fs],
             [0.009 * (1 - 0.5 * f) for f in fs], M['leg'], segs=10, ref=(0, 0, 1))
        tip = tp(dv, Lg, 1.0)
        tube("Claw_%s_%s" % (nm, tag),
             [tip, tip + dv * 0.008 + V(0, 0, -0.003), tip + dv * 0.014 + V(0, 0, -0.009)],
             [0.0035, 0.0022, 0.0005], M['claw'], segs=8, ref=(0, 0, 1), sub=0)
    # totipalmate webbing: all four toes joined
    bm = bmesh.new()
    rows = [0.1, 0.28, 0.46, 0.64, 0.82, 0.97]
    ncol = 5
    for j in range(3):
        _, dA, LA = dirs[j]
        _, dB, LB = dirs[j + 1]
        grid = []
        for ri, fr in enumerate(rows):
            row = []
            for c in range(ncol):
                cf = c / (ncol - 1)
                p = lerp(tp(dA, LA, fr), tp(dB, LB, fr), cf)
                if ri == len(rows) - 1:
                    q = lerp(tp(dA, LA, 0.62), tp(dB, LB, 0.62), cf)
                    p = lerp(p, q, 0.35 * math.sin(math.pi * cf))
                row.append(bm.verts.new(p + V(0, 0, -0.002)))
            grid.append(row)
        for ri in range(len(rows) - 1):
            for c in range(ncol - 1):
                bm.faces.new((grid[ri][c], grid[ri][c + 1], grid[ri + 1][c + 1], grid[ri + 1][c]))
    link("Foot_Web_" + tag, bm, [M['leg']], sub=1, solid=0.0015)

# ---------------- ASSEMBLE ----------------
root = bpy.data.objects.new("Brown_Pelican_Pelecanus_occidentalis", None)
COL.objects.link(root)
for ob in PARTS:
    try:
        ob.parent = root
    except Exception:
        pass