bl_info = {
    "name": "Extract Surfaces by Distance",
    "author": "Luis Bugin - Zellerfeld",
    "version": (1, 1),
    "blender": (5, 0, 1),
    "location": "View3D > Tool Shelf > Extract Surfaces",
    "description": "Split shoe mesh faces based on distance to collar and surface meshes",
    "category": "Object",
}

import bpy
import bmesh
from mathutils.bvhtree import BVHTree

# ============================================================
# Functions
# ============================================================
def get_face_centers(obj):
    print(f"[get_face_centers] Object: {obj.name}")
    mesh = obj.data
    bm = bmesh.new()
    bm.from_mesh(mesh)
    centers = [face.calc_center_median() for face in bm.faces]
    print(f"[get_face_centers] Faces found: {len(bm.faces)}, centers computed: {len(centers)}")
    bm.free()
    return centers


def build_bvh(obj):
    print(f"[build_bvh] Building BVH for: {obj.name}")
    depsgraph = bpy.context.evaluated_depsgraph_get()
    eval_obj = obj.evaluated_get(depsgraph)
    mesh = eval_obj.to_mesh()
    bm = bmesh.new()
    bm.from_mesh(mesh)
    bm.verts.ensure_lookup_table()
    bm.faces.ensure_lookup_table()
    verts = [v.co.copy() for v in bm.verts]
    faces = [[v.index for v in f.verts] for f in bm.faces]
    print(f"[build_bvh] Verts: {len(verts)}, Faces: {len(faces)}")
    bm.free()
    bvh = BVHTree.FromPolygons(verts, faces)
    eval_obj.to_mesh_clear()
    return bvh


def project_points_onto_mesh(points, target_obj, tolerance):
    bvh = build_bvh(target_obj)
    results = []

    hits_count = 0
    for pt in points:
        hit = bvh.find_nearest(pt)
        if hit is not None:
            location, normal, index, dist = hit
            results.append(location)
            hits_count += 1
        else:
            results.append(pt)

    return results


def split_shoe_faces(obj, boolean_map, surface_name):
    print(f"[split_shoe_faces] Base object: {obj.name}, Surface name: {surface_name}")
    mesh = obj.data
    bm = bmesh.new()
    bm.from_mesh(mesh)
    bm.faces.ensure_lookup_table()
    bm.verts.ensure_lookup_table()

    print(f"[split_shoe_faces] Faces in base mesh: {len(bm.faces)}, Boolean map length: {len(boolean_map)}")

    if len(boolean_map) != len(bm.faces):
        bm.free()
        raise ValueError("Boolean map length does not match number of faces!")

    bm_collared = bmesh.new()
    bm_remaining = bmesh.new()

    vert_map_collared = {}
    vert_map_remaining = {}

    def get_mapped_vert(src_vert, bm_target, vert_map):
        if src_vert.index not in vert_map:
            v_new = bm_target.verts.new(src_vert.co)
            vert_map[src_vert.index] = v_new
        return vert_map[src_vert.index]

    collared_faces = 0
    remaining_faces = 0

    for i, face in enumerate(bm.faces):
        src_verts = face.verts

        if boolean_map[i]:
            new_verts = [get_mapped_vert(v, bm_collared, vert_map_collared) for v in src_verts]
            try:
                bm_collared.faces.new(new_verts)
                collared_faces += 1
            except ValueError:
                pass
        else:
            new_verts = [get_mapped_vert(v, bm_remaining, vert_map_remaining) for v in src_verts]
            try:
                bm_remaining.faces.new(new_verts)
                remaining_faces += 1
            except ValueError:
                pass

    print(f"[split_shoe_faces] Collared faces: {collared_faces}, Remaining faces: {remaining_faces}")

    bm.free()

    bm_collared.verts.ensure_lookup_table()
    bm_collared.faces.ensure_lookup_table()
    bm_remaining.verts.ensure_lookup_table()
    bm_remaining.faces.ensure_lookup_table()

    collared_mesh = bpy.data.meshes.new(f"new_{surface_name}")
    bm_collared.to_mesh(collared_mesh)
    collared_mesh.update()
    bm_collared.free()

    remaining_mesh = bpy.data.meshes.new(f"new_{obj.name}_remaining")
    bm_remaining.to_mesh(remaining_mesh)
    remaining_mesh.update()
    bm_remaining.free()

    collared_obj = bpy.data.objects.new(collared_mesh.name, collared_mesh)
    remaining_obj = bpy.data.objects.new(remaining_mesh.name, remaining_mesh)

    collection = bpy.context.collection
    collection.objects.link(collared_obj)
#    collection.objects.link(remaining_obj)

    print(f"[split_shoe_faces] Created objects: {collared_obj.name}, {remaining_obj.name}")
    return collared_obj, remaining_obj


def extract_faces(base, surface, tolerance):
    print(f"[extract_faces] Base: {base.name}, Surface: {surface.name}, Tol: {tolerance}")
    shoe_face_centers = get_face_centers(base)
    projected_points = project_points_onto_mesh(shoe_face_centers, surface, tolerance)

    if len(shoe_face_centers) != len(projected_points):
        print(f"[extract_faces][WARN] centers: {len(shoe_face_centers)}, projected: {len(projected_points)}")

    distances = [(shoe_face_centers[i] - projected_points[i]).length for i in range(len(shoe_face_centers))]
    boolean_map = [dist <= tolerance for dist in distances]

    print(f"[extract_faces] Distances computed: {len(distances)}")
    print(f"[extract_faces] Faces within tolerance: {sum(boolean_map)}/{len(boolean_map)}")

    output_A, output_B = split_shoe_faces(base, boolean_map, surface.name)
    return output_A, output_B


def extract_surfaces_by_distance(shoe_obj, collar_obj, surface_obj, tolerance):
    print(f"[extract_surfaces_by_distance] Shoe: {shoe_obj.name}, Collar: {collar_obj.name}, "
          f"Surface: {surface_obj.name if surface_obj else 'None'}, Tol: {tolerance}")

    collar_part, remaining_after_collar = extract_faces(shoe_obj, collar_obj, tolerance)

    if surface_obj:
        extract_faces(remaining_after_collar, surface_obj, tolerance)


# ============================================================
# Operator
# ============================================================
class OBJECT_OT_extract_surfaces(bpy.types.Operator):
    bl_idname = "object.extract_surfaces"
    bl_label = "Extract"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.extract_surfaces_props
        shoe = props.shoe_obj
        collar = props.collar_obj
        surface = props.surface_obj
        tol = props.tolerance

        if not shoe or not collar:
            self.report({'ERROR'}, "Please select at least Shoe and Collar")
            print("[Operator] Missing required objects, cancelling")
            return {'CANCELLED'}

        extract_surfaces_by_distance(shoe, collar, surface, tol)

        self.report({'INFO'}, "Extraction complete")
        return {'FINISHED'}


# ============================================================
# Properties
# ============================================================
class ExtractSurfacesProperties(bpy.types.PropertyGroup):
    shoe_obj: bpy.props.PointerProperty(
        name="Shoe",
        type=bpy.types.Object,
        poll=lambda self, obj: obj.type == 'MESH'
    )
    collar_obj: bpy.props.PointerProperty(
        name="Collar",
        type=bpy.types.Object,
        poll=lambda self, obj: obj.type == 'MESH'
    )
    surface_obj: bpy.props.PointerProperty(
        name="Surface",
        type=bpy.types.Object,
        poll=lambda self, obj: obj.type == 'MESH'
    )
    tolerance: bpy.props.FloatProperty(
        name="Tolerance",
        default=0.01,
        min=0.01,
        description="Distance tolerance in Blender units"
    )

# ============================================================
# Panel
# ============================================================
class OBJECT_PT_extract_surfaces(bpy.types.Panel):
    bl_label = "Extract Surfaces from Shoe"
    bl_idname = "OBJECT_PT_extract_surfaces"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Zellerfeld'

    def draw(self, context):
        layout = self.layout
        props = context.scene.extract_surfaces_props

        layout.prop(props, "shoe_obj")
        layout.prop(props, "collar_obj")
        layout.prop(props, "surface_obj")
        layout.prop(props, "tolerance")
        layout.operator("object.extract_surfaces")

# ============================================================
# Registration
# ============================================================
classes = (
    ExtractSurfacesProperties,
    OBJECT_OT_extract_surfaces,
    OBJECT_PT_extract_surfaces,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.extract_surfaces_props = bpy.props.PointerProperty(type=ExtractSurfacesProperties)
    print("[Addon] Registered Extract Surfaces by Distance")

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.extract_surfaces_props
    print("[Addon] Unregistered Extract Surfaces by Distance")

if __name__ == "__main__":
    register()
