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

# Remove existing mesh objects, such as the default cube, so only the stork remains.
for o in list(bpy.data.objects):
    if o.type == 'MESH':
        bpy.data.objects.remove(o, do_unlink=True)

OBJS = []


def make_mat(name, col, rough=0.5):
    m = bpy.data.materials.new(name)
    try:
        m.diffuse_color = (col[0], col[1], col[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 = (col[0], col[1], col[2], 1.0)
            b.inputs["Roughness"].default_value = float(rough)
    except Exception:
        pass
    return m


M_WHITE = make_mat("Stork_WhiteFeather", (0.92, 0.91, 0.88), 0.85)
M_BLACK = make_mat("Stork_BlackFeather", (0.015, 0.015, 0.018), 0.6)
M_BEAK = make_mat("Stork_Beak", (0.80, 0.07, 0.03), 0.35)
M_LEG = make_mat("Stork_Leg", (0.78, 0.13, 0.08), 0.5)
M_EYE = make_mat("Stork_Eye", (0.04, 0.02, 0.01), 0.05)
M_SKIN = make_mat("Stork_BareSkin", (0.02, 0.02, 0.02), 0.5)
M_CLAW = make_mat("Stork_Claw", (0.10, 0.07, 0.05), 0.4)


def mk(name, bm, mat):
    try:
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    except Exception:
        pass
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    for p in me.polygons:
        p.use_smooth = True
    me.materials.append(mat)
    ob = bpy.data.objects.new(name, me)
    bpy.context.scene.collection.objects.link(ob)
    OBJS.append(ob)
    return ob


def loft(name, pts, rads, mat, segs=16, up=None, cap=True):
    pts = [Vector(p) for p in pts]
    n = len(pts)
    tans = []
    for i in range(n):
        if i == 0:
            t = pts[1] - pts[0]
        elif i == n - 1:
            t = pts[-1] - pts[-2]
        else:
            t = pts[i + 1] - pts[i - 1]
        if t.length < 1e-9:
            t = Vector((1, 0, 0))
        tans.append(t.normalized())
    u0 = Vector(up).normalized() if up is not None else Vector((0, 0, 1))
    if abs(tans[0].dot(u0)) > 0.95:
        u0 = Vector((1, 0, 0)) if abs(tans[0].x) < 0.9 else Vector((0, 1, 0))
    side = tans[0].cross(u0).normalized()
    bm = bmesh.new()
    rings = []
    for i in range(n):
        t = tans[i]
        side = side - t * side.dot(t)
        if side.length < 1e-8:
            side = t.orthogonal()
        side.normalize()
        u = side.cross(t).normalized()
        r = rads[i]
        if isinstance(r, (int, float)):
            rx, ry = float(r), float(r)
        else:
            rx, ry = float(r[0]), float(r[1])
        rx = max(rx, 0.0004)
        ry = max(ry, 0.0004)
        ring = []
        for j in range(segs):
            a = 2.0 * math.pi * j / segs
            ring.append(bm.verts.new(pts[i] + side * (rx * math.cos(a)) + u * (ry * math.sin(a))))
        rings.append(ring)
    for i in range(n - 1):
        for j in range(segs):
            a = rings[i][j]
            b = rings[i][(j + 1) % segs]
            c = rings[i + 1][(j + 1) % segs]
            d = rings[i + 1][j]
            try:
                bm.faces.new((a, b, c, d))
            except Exception:
                pass
    if cap:
        try:
            bm.faces.new(list(reversed(rings[0])))
        except Exception:
            pass
        try:
            bm.faces.new(rings[-1])
        except Exception:
            pass
    return mk(name, bm, mat)


def ellip(name, c, s, mat, rot=(0, 0, 0), seg=20):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=max(8, seg // 2 + 2), radius=1.0)
    R = Euler(rot).to_matrix()
    cv = Vector(c)
    for v in bm.verts:
        v.co = R @ Vector((v.co.x * s[0], v.co.y * s[1], v.co.z * s[2])) + cv
    return mk(name, bm, mat)


def feather(name, base, tip, width, mat, up, th=0.0035, bend=(0, 0, 0), n=8):
    base = Vector(base)
    tip = Vector(tip)
    bend = Vector(bend)
    pts = []
    rads = []
    for k in range(n):
        f = k / (n - 1)
        pts.append(base.lerp(tip, f) + bend * (4.0 * f * (1.0 - f)))
        w = width * max(0.06, math.sin(math.pi * (0.15 + 0.85 * f)) ** 0.6)
        rads.append((w, th))
    return loft(name, pts, rads, mat, segs=8, up=up)


# ---------------- BODY ----------------
loft("Body",
     [(-0.27, 0, 0.805), (-0.22, 0, 0.795), (-0.15, 0, 0.785), (-0.07, 0, 0.78),
      (0.0, 0, 0.782), (0.07, 0, 0.79), (0.13, 0, 0.805), (0.18, 0, 0.83), (0.21, 0, 0.86)],
     [(0.025, 0.02), (0.06, 0.045), (0.095, 0.075), (0.115, 0.095), (0.12, 0.10),
      (0.115, 0.10), (0.10, 0.09), (0.075, 0.07), (0.05, 0.05)],
     M_WHITE, segs=24, up=(0, 0, 1))

# Upper tail coverts and rump.
ellip("UpperTailCoverts", (-0.25, 0, 0.815), (0.07, 0.04, 0.02), M_WHITE, rot=(0, 0.1, 0))
# Undertail coverts.
ellip("UnderTailCoverts", (-0.22, 0, 0.775), (0.07, 0.035, 0.022), M_WHITE, rot=(0, -0.15, 0))
# Breast bulge.
ellip("Breast", (0.12, 0, 0.78), (0.09, 0.09, 0.08), M_WHITE)

# ---------------- NECK ----------------
loft("Neck",
     [(0.17, 0, 0.84), (0.215, 0, 0.895), (0.238, 0, 0.955), (0.238, 0, 1.01),
      (0.24, 0, 1.06), (0.25, 0, 1.10), (0.272, 0, 1.13), (0.30, 0, 1.148)],
     [(0.058, 0.06), (0.046, 0.05), (0.037, 0.04), (0.031, 0.033),
      (0.028, 0.03), (0.028, 0.03), (0.029, 0.03), (0.028, 0.028)],
     M_WHITE, segs=20, up=(0, 0, 1))

# Elongated breast and lower-neck plumes.
for i in range(9):
    yy = (i - 4) * 0.0085
    zb = 0.965 - abs(i - 4) * 0.006
    b = (0.262 - abs(i - 4) * 0.004, yy, zb)
    t = (b[0] + 0.035, yy * 1.4, zb - 0.105)
    feather("BreastPlume_%d" % i, b, t, 0.011, M_WHITE, up=(1, 0, 0.3), th=0.0025, bend=(0.006, 0, 0))

# ---------------- HEAD ----------------
ellip("Head", (0.30, 0, 1.152), (0.056, 0.035, 0.041), M_WHITE, rot=(0, 0.12, 0))
ellip("Crown", (0.29, 0, 1.17), (0.035, 0.028, 0.022), M_WHITE)

# Eyes, black periocular skin, and bare chin skin.
for s in (-1, 1):
    ellip("PeriocularSkin_%d" % s, (0.318, s * 0.0305, 1.157), (0.013, 0.0045, 0.010), M_SKIN)
    ellip("Eye_%d" % s, (0.319, s * 0.0325, 1.158), (0.0072, 0.0072, 0.0072), M_EYE, seg=16)
ellip("ChinSkin", (0.335, 0, 1.128), (0.022, 0.012, 0.007), M_SKIN)

# ---------------- BEAK ----------------
loft("UpperMandible",
     [(0.325, 0, 1.153), (0.36, 0, 1.146), (0.40, 0, 1.135), (0.45, 0, 1.119),
      (0.50, 0, 1.101), (0.53, 0, 1.089), (0.545, 0, 1.083)],
     [(0.0135, 0.013), (0.012, 0.0115), (0.0105, 0.0098), (0.0082, 0.0078),
      (0.0055, 0.0055), (0.0028, 0.0032), (0.0006, 0.0008)],
     M_BEAK, segs=18, up=(0, 0, 1))
loft("LowerMandible",
     [(0.325, 0, 1.136), (0.36, 0, 1.131), (0.40, 0, 1.122), (0.45, 0, 1.108),
      (0.50, 0, 1.092), (0.525, 0, 1.084), (0.537, 0, 1.080)],
     [(0.0125, 0.010), (0.011, 0.0092), (0.0095, 0.008), (0.0075, 0.0065),
      (0.005, 0.0045), (0.0026, 0.0025), (0.0006, 0.0007)],
     M_BEAK, segs=18, up=(0, 0, 1))
# Commissure line where the mandibles meet.
for s in (-1, 1):
    loft("Commissure_%d" % s,
         [(0.34, s * 0.0118, 1.1425), (0.40, s * 0.0098, 1.1285), (0.46, s * 0.0072, 1.110),
          (0.51, s * 0.0038, 1.093)],
         [0.0012, 0.0011, 0.0009, 0.0005], M_SKIN, segs=6)
    # Nostril slits.
    ellip("Nostril_%d" % s, (0.362, s * 0.0112, 1.153), (0.009, 0.0018, 0.0022), M_SKIN,
          rot=(0, 0.25, 0), seg=10)

# ---------------- WINGS (folded) ----------------
for s in (-1, 1):
    # White coverts and scapulars.
    loft("WingCoverts_%d" % s,
         [(0.15, s * 0.098, 0.866), (0.08, s * 0.124, 0.862), (0.0, s * 0.134, 0.857),
          (-0.10, s * 0.128, 0.846), (-0.18, s * 0.108, 0.832), (-0.235, s * 0.088, 0.822)],
         [(0.02, 0.012), (0.045, 0.02), (0.05, 0.022), (0.045, 0.02), (0.03, 0.015), (0.01, 0.006)],
         M_WHITE, segs=16, up=(0, s, 0))
    # Scapular feathers that overlap the back.
    for i in range(5):
        x0 = 0.06 - i * 0.05
        feather("Scapular_%d_%d" % (s, i), (x0, s * 0.07, 0.875), (x0 - 0.12, s * 0.085, 0.855),
                0.03, M_WHITE, up=(0, s * 0.3, 1), th=0.003, bend=(0, 0, 0.006))
    # Black secondaries along the lower edge of the folded wing.
    for i in range(11):
        x0 = 0.09 - i * 0.02
        base = (x0, s * 0.133, 0.838 - i * 0.001)
        tip = (x0 - 0.15, s * (0.128 - i * 0.002), 0.785 - i * 0.002)
        feather("Secondary_%d_%d" % (s, i), base, tip, 0.034, M_BLACK, up=(0, s, 0),
                th=0.003, bend=(0, s * 0.004, 0))
    # Black greater coverts.
    for i in range(8):
        x0 = 0.07 - i * 0.025
        feather("GreaterCovert_%d_%d" % (s, i), (x0, s * 0.14, 0.845),
                (x0 - 0.09, s * 0.138, 0.815), 0.026, M_BLACK, up=(0, s, 0), th=0.0028)
    # Long black primaries that cross over the tail.
    for i in range(10):
        base = (-0.03 - i * 0.012, s * (0.132 - 0.002 * i), 0.83 - i * 0.004)
        tip = (-0.44 + i * 0.014, s * (0.045 + 0.004 * i), 0.808 - i * 0.004)
        feather("Primary_%d_%d" % (s, i), base, tip, 0.03, M_BLACK, up=(0, s, 0.1),
                th=0.003, bend=(0, s * 0.012, 0.004), n=10)
    # Alula and wrist.
    ellip("Wrist_%d" % s, (0.13, s * 0.11, 0.85), (0.03, 0.018, 0.022), M_WHITE)

# ---------------- TAIL ----------------
for i in range(12):
    f = (i - 5.5) / 5.5
    yb = f * 0.035
    base = (-0.235, yb, 0.803)
    tip = (-0.405 + abs(f) * 0.015, yb * 2.6, 0.782)
    feather("Rectrix_%d" % i, base, tip, 0.028, M_WHITE, up=(0, 0, 1), th=0.003,
            bend=(0, 0, 0.004))

# ---------------- LEGS ----------------
for s in (-1, 1):
    y = s * 0.055
    # Feathered upper thigh.
    loft("Thigh_%d" % s,
         [(0.03, y, 0.73), (0.03, y, 0.68), (0.027, y, 0.635), (0.025, y, 0.61)],
         [0.038, 0.03, 0.018, 0.011], M_WHITE, segs=14)
    # Bare tibiotarsus, intertarsal joint, and tarsometatarsus.
    loft("Leg_%d" % s,
         [(0.028, y, 0.665), (0.02, y, 0.56), (0.009, y, 0.43), (0.001, y, 0.375),
          (0.004, y, 0.34), (0.012, y, 0.20), (0.019, y, 0.07), (0.022, y, 0.028)],
         [0.0115, 0.0102, 0.0092, 0.0135, 0.0098, 0.0085, 0.0088, 0.0115],
         M_LEG, segs=14)
    ellip("Ankle_%d" % s, (0.001, y, 0.375), (0.014, 0.0135, 0.016), M_LEG, seg=14)
    foot = Vector((0.022, y, 0.022))
    ellip("FootPad_%d" % s, tuple(foot + Vector((0.004, 0, -0.006))), (0.016, 0.014, 0.009), M_LEG, seg=14)
    dirs = []
    for ang, L in ((-32, 0.082), (0, 0.10), (32, 0.078)):
        a = math.radians(ang)
        d = Vector((math.cos(a), math.sin(a), 0))
        dirs.append((d, L))
        p1 = foot + d * (L * 0.25)
        p1.z = 0.010
        p2 = foot + d * (L * 0.6)
        p2.z = 0.0085
        tp = foot + d * L
        tp.z = 0.007
        loft("Toe_%d_%d" % (s, ang), [foot, p1, p2, tp], [0.0085, 0.0068, 0.0055, 0.0032], M_LEG, segs=10)
        loft("Claw_%d_%d" % (s, ang), [tp - d * 0.003, tp + d * 0.007, tp + d * 0.013 + Vector((0, 0, -0.004))],
             [0.0032, 0.0018, 0.0004], M_CLAW, segs=8)
    # Hallux (hind toe).
    hd = Vector((-1, 0, 0))
    ht = foot + hd * 0.035
    ht.z = 0.012
    loft("Hallux_%d" % s, [foot + Vector((0, 0, 0.004)), foot + hd * 0.018 + Vector((0, 0, -0.006)), ht],
         [0.006, 0.005, 0.003], M_LEG, segs=10)
    loft("HalluxClaw_%d" % s, [ht - hd * 0.002, ht + hd * 0.006, ht + hd * 0.011 + Vector((0, 0, -0.004))],
         [0.0028, 0.0015, 0.0004], M_CLAW, segs=8)
    # Small interdigital webs at the base of the front toes.
    for k in range(2):
        da, La = dirs[k]
        db, Lb = dirs[k + 1]
        pa = foot + da * (La * 0.38)
        pa.z = 0.0095
        pb = foot + db * (Lb * 0.38)
        pb.z = 0.0095
        bm = bmesh.new()
        c0 = bm.verts.new(foot + Vector((0.004, 0, -0.003)))
        va = bm.verts.new(pa)
        vb = bm.verts.new(pb)
        c1 = bm.verts.new(foot + Vector((0.004, 0, -0.0045)))
        va2 = bm.verts.new(pa + Vector((0, 0, -0.0015)))
        vb2 = bm.verts.new(pb + Vector((0, 0, -0.0015)))
        bm.faces.new((c0, va, vb))
        bm.faces.new((c1, vb2, va2))
        bm.faces.new((c0, c1, va2, va))
        bm.faces.new((c0, vb, vb2, c1))
        bm.faces.new((va, va2, vb2, vb))
        mk("Web_%d_%d" % (s, k), bm, M_LEG)

# ---------------- FINALIZE: join into one specimen ----------------
try:
    bpy.ops.object.select_all(action='DESELECT')
except Exception:
    pass
live = [o for o in OBJS if o.name in bpy.data.objects]
for o in live:
    try:
        o.select_set(True)
    except Exception:
        pass
stork = None
if live:
    bpy.context.view_layer.objects.active = live[0]
    try:
        bpy.ops.object.join()
        stork = bpy.context.view_layer.objects.active
    except Exception:
        stork = None
if stork is not None:
    stork.name = "WhiteStork_Ciconia_ciconia"
    try:
        stork.data.name = "WhiteStork_Mesh"
    except Exception:
        pass
else:
    root = bpy.data.objects.new("WhiteStork_Ciconia_ciconia", None)
    bpy.context.scene.collection.objects.link(root)
    for o in bpy.data.objects:
        if o.type == 'MESH' and o.parent is None:
            o.parent = root