import bpy
import bmesh
from mathutils import Vector
import math

# ==========================================================
# ADAPTIVE ORGANIC BRIDGE 
# Une 2 edge loops seleccionados en Multi-Object Edit Mode
# Robusto para distintas densidades / formas orgánicas
# ==========================================================

# ---------------- CONFIG ----------------
new_object_name = "OrganicBridge"

segments = 8          # rings intermedios
twist_degrees = 0.0   # giro manual
taper = 1.0           # 1 = normal | <1 estrecha | >1 ensancha
preserve_volume = True
auto_merge = False    # unir con objetos originales
merge_distance = 0.001
smooth_shading = True
# ---------------------------------------


# ==========================================================
# HELPERS
# ==========================================================

def get_edit_mesh_objects():
    return [o for o in bpy.context.objects_in_mode if o.type == 'MESH']


def selected_edges(obj):
    bm = bmesh.from_edit_mesh(obj.data)
    return bm, [e for e in bm.edges if e.select]


def ordered_loop(edges):
    adj = {}

    for e in edges:
        a, b = e.verts
        adj.setdefault(a, []).append(b)
        adj.setdefault(b, []).append(a)

    start = list(adj.keys())[0]
    loop = [start]

    prev = None
    cur = start

    while True:
        nxts = adj[cur]
        nxt = nxts[0] if nxts[0] != prev else nxts[1]

        if nxt == start:
            break

        loop.append(nxt)
        prev = cur
        cur = nxt

        if len(loop) > len(adj) + 5:
            break

    return loop


def world_points(obj, verts):
    return [obj.matrix_world @ v.co for v in verts]


def perimeter(loop):
    total = 0
    for i in range(len(loop)):
        total += (loop[(i+1)%len(loop)] - loop[i]).length
    return total


def sample_loop(loop, u):
    """
    Sample estable por porcentaje de perímetro (0..1)
    """
    seg_lengths = []
    total = 0

    for i in range(len(loop)):
        l = (loop[(i+1)%len(loop)] - loop[i]).length
        seg_lengths.append(l)
        total += l

    target = (u % 1.0) * total
    acc = 0

    for i, l in enumerate(seg_lengths):
        if acc + l >= target:
            t = (target - acc) / l if l > 0 else 0
            return loop[i].lerp(loop[(i+1)%len(loop)], t)
        acc += l

    return loop[-1]


def resample_stable(loop, count):
    return [sample_loop(loop, i / count) for i in range(count)]


def loop_center(loop):
    return sum(loop, Vector()) / len(loop)


def align_shift(loopA, loopB):
    """
    Busca mejor offset entre loops
    """
    n = len(loopB)
    best = 0
    best_score = 1e18

    for shift in range(n):
        score = 0
        for i in range(len(loopA)):
            a = loopA[i]
            b = loopB[(i + shift) % n]
            score += (a - b).length

        if score < best_score:
            best_score = score
            best = shift

    return [loopB[(i + best) % n] for i in range(n)]


def rotate_vec(v, axis, angle):
    return v.rotated(
        axis.rotation_difference(axis).to_matrix().to_4x4()
    ) if angle == 0 else v.copy().rotate(
        mathutils.Matrix.Rotation(angle, 4, axis)
    )


def bridge_diff_counts(bm, ringA, ringB):
    ia = ib = 0
    na = len(ringA)
    nb = len(ringB)

    while ia < na or ib < nb:
        a0 = ringA[ia % na]
        a1 = ringA[(ia + 1) % na]
        b0 = ringB[ib % nb]
        b1 = ringB[(ib + 1) % nb]

        ta = (ia + 1) / na
        tb = (ib + 1) / nb

        try:
            if ta < tb:
                bm.faces.new((a0, a1, b0))
                ia += 1
            elif tb < ta:
                bm.faces.new((a0, b1, b0))
                ib += 1
            else:
                bm.faces.new((a0, a1, b1))
                bm.faces.new((a0, b1, b0))
                ia += 1
                ib += 1
        except:
            ia += (ta <= tb)
            ib += (tb <= ta)


# ==========================================================
# MAIN
# ==========================================================

objs = get_edit_mesh_objects()

if len(objs) != 2:
    raise Exception("Selecciona exactamente 2 objetos en Multi-Edit Mode.")

loops = []

for obj in objs:
    bm, edges = selected_edges(obj)

    if not edges:
        raise Exception(f"No hay edges seleccionados en {obj.name}")

    verts = ordered_loop(edges)
    loops.append(world_points(obj, verts))

loopA = loops[0]
loopB = loops[1]

# menor densidad primero
if len(loopA) > len(loopB):
    loopA, loopB = loopB, loopA

countA = len(loopA)
countB = len(loopB)

# igualar count temporal para alinear
tmpB = resample_stable(loopB, countA)
tmpB = align_shift(loopA, tmpB)

# orientación reversa si conviene
normal = sum((loopA[i] - tmpB[i]).length for i in range(countA))
revB = list(reversed(tmpB))
revscore = sum((loopA[i] - revB[i]).length for i in range(countA))

if revscore < normal:
    loopB.reverse()

# centros
centerA = loop_center(loopA)
centerB = loop_center(loopB)

axis = (centerB - centerA)
dist = axis.length

if dist == 0:
    raise Exception("Los loops están en la misma posición.")

forward = axis.normalized()

# base ortogonal
tmp = Vector((0,0,1))
if abs(forward.dot(tmp)) > 0.95:
    tmp = Vector((1,0,0))

right = forward.cross(tmp).normalized()
up = right.cross(forward).normalized()

rings_pts = []

for s in range(segments + 2):
    t = s / (segments + 1)

    count = round(countA + (countB - countA) * t)

    ringA = resample_stable(loopA, count)
    ringB = resample_stable(loopB, count)

    center = centerA.lerp(centerB, t)

    ring = []

    for i in range(count):
        pa = ringA[i]
        pb = ringB[i]

        da = pa - centerA
        db = pb - centerB

        dirv = da.lerp(db, t)

        if preserve_volume:
            ra = da.length
            rb = db.length
            radius = (ra * (1-t) + rb * t) * taper
            if dirv.length > 0.00001:
                dirv = dirv.normalized() * radius
        else:
            dirv *= taper

        p = center + dirv
        ring.append(p)

    rings_pts.append(ring)

# ==========================================================
# BUILD MESH
# ==========================================================

mesh = bpy.data.meshes.new(new_object_name)
obj_new = bpy.data.objects.new(new_object_name, mesh)
bpy.context.collection.objects.link(obj_new)

bm_new = bmesh.new()
rings = []

for ring in rings_pts:
    verts = [bm_new.verts.new(p) for p in ring]
    rings.append(verts)

bm_new.verts.ensure_lookup_table()

for i in range(len(rings)-1):
    bridge_diff_counts(bm_new, rings[i], rings[i+1])

bm_new.normal_update()
bm_new.to_mesh(mesh)
bm_new.free()

if smooth_shading:
    for p in obj_new.data.polygons:
        p.use_smooth = True

# ==========================================================
# AUTO MERGE
# ==========================================================

if auto_merge:
    bpy.ops.object.mode_set(mode='OBJECT')

    for o in objs:
        o.select_set(True)

    obj_new.select_set(True)
    bpy.context.view_layer.objects.active = obj_new

    bpy.ops.object.join()

    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.remove_doubles(threshold=merge_distance)
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')

print("Organic Bridge PRO creado.")