import bmesh
from .. import g
import mathutils
from .. import const
import bpy
import heapq
import math
import numpy as np
from bpy_extras import view3d_utils
import random
from collections import defaultdict
from functools import wraps
import time


def safe_execute(func, *args, **kwargs):
    try:
        func(*args, **kwargs)
    except Exception as e:
        print(f"An exception occurred in {func.__name__}: {e}")


def get_snap_option_target_obj():
    snap_obj = bpy.context.scene.easy_patch_properties.snap_obj
    if (
        snap_obj
        and snap_obj.name in bpy.context.scene.objects
        and snap_obj.type == "MESH"
    ):
        return snap_obj
    else:
        return None


def get_obj_bvh_tree():
    obj = g.obj
    # 获取依赖图
    depsgraph = bpy.context.evaluated_depsgraph_get()

    evaluated_obj = obj.evaluated_get(depsgraph)
    target_mesh = evaluated_obj.to_mesh()

    temp_bm = bmesh.new()
    temp_bm.from_mesh(target_mesh)
    temp_bm.transform(obj.matrix_world)  # 应用对象的变换
    bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)

    temp_bm.free()
    evaluated_obj.to_mesh_clear()

    return bvh_tree


# 获取bvh树，如果用户填了snap选项，则用snap，没填则用可见物体
def get_snap_bvh_trees(exclude_object=[]):
    snap_obj = get_snap_option_target_obj()
    # 获取依赖图
    depsgraph = bpy.context.evaluated_depsgraph_get()

    # 构建BVH树，只针对非当前对象的其他网格对象
    bvh_trees = []
    if snap_obj:
        # 获取评估后的对象数据，包含变换和修改器
        evaluated_obj = snap_obj.evaluated_get(depsgraph)
        target_mesh = evaluated_obj.to_mesh()

        temp_bm = bmesh.new()
        temp_bm.from_mesh(target_mesh)
        temp_bm.transform(snap_obj.matrix_world)  # 应用对象的变换

        bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
        bvh_trees.append(bvh_tree)
        temp_bm.free()
    else:
        # 获取所有可见的网格对象（排除当前对象）
        visible_objects = [
            o
            for o in bpy.context.visible_objects
            if o.type == "MESH"
            and o not in exclude_object
            and o.visible_get()
            and o != get_edit_obj()
        ]

        # 构建场景中所有其他网格对象的BVH树，这里要排除不可见的物体、不在视图层的物体，否则会出现奇怪的捕捉，同时减轻构建BVH性能负担
        for scene_obj in visible_objects:
            # 获取评估后的对象数据，包含变换和修改器
            evaluated_obj = scene_obj.evaluated_get(depsgraph)
            target_mesh = evaluated_obj.to_mesh()

            temp_bm = bmesh.new()
            temp_bm.from_mesh(target_mesh)
            temp_bm.transform(scene_obj.matrix_world)  # 应用对象的变换
            bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
            bvh_trees.append(bvh_tree)

            temp_bm.free()
            evaluated_obj.to_mesh_clear()

    return bvh_trees


def get_bvh_tree_from_bm(bm: bmesh.types.BMesh):
    # Prepare a list to store the bounding boxes (aabb) for each face
    faces = []
    coords = []

    # Loop through each face in the BMesh
    for face in bm.faces:
        # Calculate the bounding box for the current face
        min_bound = mathutils.Vector((float("inf"), float("inf"), float("inf")))
        max_bound = mathutils.Vector((float("-inf"), float("-inf"), float("-inf")))

        # Get the vertices of the face
        for loop in face.loops:
            vertex = loop.vert.co
            min_bound = min(min_bound, vertex)
            max_bound = max(max_bound, vertex)

        # Add the bounding box corners (min and max points)
        faces.append((min_bound, max_bound))
        coords.append([v.co for v in face.verts])

    # Create a BVHTree
    bvh_tree = bpy.types.BVHTree()

    # Build the BVH tree from the faces (using the coords)
    bvh_tree.build(coords)

    return bvh_tree


def log(input):
    if bpy.app.debug:
        print(f"Easy Patch: {input}")


def hsv_to_rgb(h, s, v, a=None):
    if s == 0.0:
        rgb = v, v, v
    else:
        i = int(h * 6.0)
        f = h * 6.0 - i
        p = v * (1.0 - s)
        q = v * (1.0 - s * f)
        t = v * (1.0 - s * (1.0 - f))
        i %= 6
        if i == 0:
            rgb = v, t, p
        if i == 1:
            rgb = q, v, p
        if i == 2:
            rgb = p, v, t
        if i == 3:
            rgb = p, q, v
        if i == 4:
            rgb = t, p, v
        if i == 5:
            rgb = v, p, q

    return rgb if a is None else (*rgb, a)


# 只会删除边
def remove_bm_edges(bm, bm_edges):
    for bm_edge in bm_edges:
        if bm_edge in bm.edges:
            bm.edges.remove(bm_edge)


def delete_bm_verts(bm, bm_verts):
    # # 先把记录的变量删除
    # for b in g.boundaries:
    #     for v in bm_verts:
    #         if v in b.

    temp_vertices = [v for v in bm_verts if v in bm.verts]
    temp_vertices = list(set(temp_vertices))
    bmesh.ops.delete(bm, geom=temp_vertices, context="VERTS")


def remove_bm_faces(bm, faces):
    for face in faces:
        if face in bm.faces:
            bm.faces.remove(face)


def uniform_vertices_co_along_path(verts, mid_vertex_num):
    if len(verts) < 2 or mid_vertex_num < 0:
        return ([], [])

    # total length
    total_length = sum(
        (mathutils.Vector(verts[i]) - mathutils.Vector(verts[i - 1])).length
        for i in range(1, len(verts))
    )

    # inverval distance of new vertex
    new_verts_interval = total_length / (mid_vertex_num + 1)

    new_verts = []
    new_verts_correspond_path_idx = []
    path_verts_accumulated_length = 0.0
    segment_start = mathutils.Vector(verts[0])

    for i in range(1, len(verts)):
        segment_end = mathutils.Vector(verts[i])
        segment_length = (segment_end - segment_start).length
        path_verts_accumulated_length += segment_length

        new_verts_accumulated_length = new_verts_interval * len(new_verts)

        if not segment_length > 0:
            continue

        while (
            path_verts_accumulated_length > new_verts_accumulated_length
        ):  # if >= will include last. when mid_vertex_num too large >= will stuck in a number when increase, > will stuck in a number when decrease,
            ratio = (
                path_verts_accumulated_length - new_verts_accumulated_length
            ) / segment_length

            new_vert = segment_end.lerp(segment_start, ratio)
            new_verts.append(new_vert)
            new_verts_correspond_path_idx.append(i - 1)
            new_verts_accumulated_length = new_verts_interval * len(new_verts)

        segment_start = segment_end

    # 未知原因导致多一个点,可能是最后一个点距离过近导致上述循环把最后一个点也包含进去了
    if len(new_verts) >= mid_vertex_num + 2:
        new_verts[-1] = verts[-1]
        new_verts_correspond_path_idx[-1] = len(verts) - 1
    else:
        new_verts.append(verts[-1])
        new_verts_correspond_path_idx.append(len(verts) - 1)
    return new_verts, new_verts_correspond_path_idx


def get_midpoint_of_path(bm, edge_path):
    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()

    if len(edge_path) == 0:
        return
    num_edges = len(edge_path)

    if num_edges % 2 == 0:
        middle_index = num_edges // 2
        edge1 = edge_path[middle_index - 1]
        edge2 = edge_path[middle_index]

        verts_edge1 = set(edge1.verts)
        verts_edge2 = set(edge2.verts)
        common_verts = verts_edge1.intersection(verts_edge2)
        common_vert = next(iter(common_verts), None)
        if common_vert:
            return common_vert.co
        else:
            return mathutils.Vector((0, 0, 0))

    # 如果边的数量为奇数，中点是中间边的中点
    else:
        total_length = sum(edge.calc_length() for edge in edge_path)
        half_length = total_length / 2
        accum_length = 0
        for edge in edge_path:
            edge_length = edge.calc_length()
            accum_length += edge_length

            if accum_length >= half_length:
                overshoot = accum_length - half_length
                factor = (edge_length - overshoot) / edge_length
                v1, v2 = edge.verts
                midpoint = v1.co.lerp(v2.co, factor)
                return midpoint

    return mathutils.Vector((0, 0, 0))  # 如果路径为空，返回原点


def get_edit_obj():
    return bpy.context.edit_object


def normal_local_to_world(obj, normal):
    matrix = obj.matrix_world.to_3x3().inverted().transposed()
    normal_world = matrix @ normal
    if normal_world.length_squared == 0:
        return mathutils.Vector((0.0, 0.0, 1.0))
    return normal_world.normalized()


def dijkstra(
    bm, start, end, exclude_edges=[], exclude_verts=[], is_along_boundary=False
):
    # 该函数计算的是累计路径长度，而不是累计拓扑路径长度

    distances = {
        v: float("inf") for v in bm.verts
    }  # 初始化距离字典，所有顶点的初始距离设为无穷大
    previous = {v: None for v in bm.verts}  # 初始化前驱顶点字典，用于重建路径
    edge_to = {v: None for v in bm.verts}  # 初始化边字典，用于存储到达每个顶点的边
    distances[start] = 0  # 起始顶点到自身的距离设为0
    queue = []
    heapq.heappush(queue, (0, 0, start))  # 元组包含距离、计数器和顶点

    count = 1  # 计数器，用于解决堆中的比较冲突

    while queue:
        current_distance, _, current_vert = heapq.heappop(
            queue
        )  # 从队列中弹出当前距离最短的顶点
        if current_vert == end:  # 如果到达终点，重建并返回路径
            path_verts = []
            path_edges = []
            while previous[current_vert]:
                path_verts.insert(0, current_vert)
                path_edges.insert(0, edge_to[current_vert])
                current_vert = previous[current_vert]
            path_verts.insert(0, current_vert)
            return path_verts, path_edges

        # 跳过处理距离更长的路径
        if current_distance > distances[current_vert]:
            continue

        # 遍历当前顶点的所有相连边
        for edge in current_vert.link_edges:
            if is_along_boundary and not edge.is_boundary:
                continue

            if edge in exclude_edges:
                continue

            neighbor = edge.other_vert(current_vert)  # 获取邻接顶点
            # if neighbor in restricted_verts:
            #     continue  # 跳过受限顶点
            if neighbor in exclude_verts and neighbor != end:
                continue  # 跳过受限顶点，除非它是终点

            distance = current_distance + edge.calc_length()  # 计算新的距离

            # 更新邻接顶点的最短路径
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                previous[neighbor] = current_vert
                edge_to[neighbor] = edge
                heapq.heappush(queue, (distance, count, neighbor))
                count += 1

    return [], []


# 更新drawing_path的路径，处理solid和非solid
def update_solid_path(context, event):
    # if not g.drawing_path_boundary:
    #     return

    if not g.drawing_verts_snap_vertex or not event.shift:
        g.drawing_path_boundary.solid_path = []
        g.drawing_path_boundary.edges = []
        g.drawing_path_boundary.verts = []
        return

    if (not event.type == "MOUSEMOVE") and (
        not (event.type == "LEFT_SHIFT" and event.value == "PRESS")
    ):
        return

    # update solid path
    verts_path = []
    boundary = g.drawing_path_boundary
    if boundary.start_vertex and boundary.start_vertex != g.drawing_verts_snap_vertex:
        exclude_edges = g.temp_bm_edges
        exclude_verts = g.temp_bm_verts
        verts_path, edges_path = dijkstra(
            g.obj_bm,
            boundary.start_vertex,
            g.drawing_verts_snap_vertex,
            exclude_edges=exclude_edges,
            exclude_verts=exclude_verts,
        )

    # 特殊情况：路径长度为1，且路径的结束点在temp中且不是patch结尾，
    if len(verts_path) == 2 and verts_path[-1] in g.temp_bm_verts:
        verts_path = []

    if verts_path:
        g.drawing_path_boundary.solid_path = [
            g.obj.matrix_world @ v.co for v in verts_path
        ]
        g.drawing_path_boundary.edges = edges_path
        g.drawing_path_boundary.verts = verts_path


def update_snap_vertex(event, is_snap_opposite=False):
    start_time = time.time()
    if not event.ctrl:
        g.drawing_verts_snap_vertex = None
        return

    # # 注意使用，鼠标移动的事件是离散的，这个时间限制是连续，设置时间戳要放在最后，不然事件全部被阻塞了
    current_timestamp = time.time()
    if current_timestamp - g.last_update_snap_timestamp < 1 / 10:
        return

    if event.type != "MOUSEMOVE" and not (
        event.type == "LEFT_CTRL" and event.value == "PRESS"
    ):
        return

    if not bpy.context.tool_settings.mesh_select_mode[0]:
        bpy.context.tool_settings.mesh_select_mode = (True, False, False)

    bpy.ops.mesh.select_all(action="DESELECT")
    moust_co_2d = event.mouse_region_x, event.mouse_region_y
    ret = bpy.ops.view3d.select(extend=True, location=moust_co_2d)
    if "FINISHED" not in ret:
        # 判断是否选中，即使判断也有可能获得None
        g.drawing_verts_snap_vertex = None
        return

    vertex = g.obj_bm.select_history[-1]

    # 上一个snap残留和新的不一样的话，直接用新的，如果一样，则判断阈值
    # 这里要加上None，不然的话一闪一闪
    if (
        vertex == g.drawing_verts_snap_vertex or g.drawing_verts_snap_vertex is None
    ) and vertex:
        # 做2d阈值
        context = bpy.context
        region = context.region
        rv3d = context.region_data
        mouse_co_2d = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
        vertex_co_2d = view3d_utils.location_3d_to_region_2d(
            region, rv3d, g.obj.matrix_world @ vertex.co
        )
        if not vertex_co_2d:
            vertex = None
        if (mouse_co_2d - vertex_co_2d).length > const.snap_vertex_threshold:
            vertex = None

    # # 禁用2条边
    # if len(self.patch.boundaries) == 1 and vertex == self.patch.start_vertex:
    #     vertex = None

    # TODO:使用插件单独创建的时候,可能是反向的,导致吸附不上

    # 处理法线方向
    if not is_snap_opposite and vertex and vertex.link_faces:
        vert_normal_global = normal_local_to_world(g.obj, vertex.normal)
        view_vector = bpy.context.region_data.view_rotation @ mathutils.Vector(
            (0.0, 0.0, -1.0)
        )
        # 法线和视图同向，不选择
        if vert_normal_global.dot(view_vector) > 0:
            vertex = None

    g.drawing_verts_snap_vertex = vertex

    bpy.ops.mesh.select_all(action="DESELECT")

    g.last_update_snap_timestamp = current_timestamp  # 这个要放到最后更新，不然中间有return了，就相当于没更新就设置了这个timestamp

    return


def create_temp_vertex(coord):
    vertex = g.obj_bm.verts.new(g.obj.matrix_world.inverted() @ coord)
    g.temp_bm_verts.add(vertex)
    return vertex


def create_temp_edge(vertice_1, vertice_2):
    edge = g.obj_bm.edges.new((vertice_1, vertice_2))
    g.temp_bm_edges.add(edge)
    return edge


def get_nearest_boundary_from_mouse(event):
    context = bpy.context
    region = context.region
    rv3d = context.region_data
    mouse_co_2d = (event.mouse_region_x, event.mouse_region_y)

    threshold = const.hover_threshold
    for boundary in g.boundaries:

        for edge in boundary.edges:
            obj = g.obj
            # vert.co获取的是局部坐标
            edge_coords_3d = [obj.matrix_world @ vert.co for vert in edge.verts]
            edge_coords_2d = [
                view3d_utils.location_3d_to_region_2d(region, rv3d, coord)
                for coord in edge_coords_3d
            ]
            if None in edge_coords_2d:
                continue

            distance = distance_point_to_line(
                mathutils.Vector(mouse_co_2d),
                mathutils.Vector(edge_coords_2d[0]),
                mathutils.Vector(edge_coords_2d[1]),
            )

            if distance < threshold:
                return boundary


def distance_point_to_line(pt, v1, v2):
    line_vec = v2 - v1

    # in case 0 length
    if line_vec.length == 0:
        return (pt - v1).length

    pt_vec = pt - v1
    line_len = line_vec.length
    line_unitvec = line_vec.normalized()
    pt_vec_scaled = pt_vec * (1.0 / line_len)
    t = line_unitvec.dot(pt_vec_scaled)
    if t < 0.0:
        t = 0.0
    elif t > 1.0:
        t = 1.0
    nearest = v1 + line_vec * t
    dist = (nearest - pt).length
    return dist


def stable_random_from_obj(obj):
    obj_id = id(obj)
    random.seed(obj_id)
    stable_random_value = random.random()
    return stable_random_value


def get_loops_from_boundary(boundary):
    loops = []
    for loop in g.loops:
        if boundary in loop.boundaries:
            loops.append(loop)
    return loops


def on_seperate_boundary_(boundary):
    # 已在 done_from_temp写了
    pass


def on_change_boundary_length(boundary):
    # boundary长度改变，loop不变，pattern形状会变，
    loops = get_loops_from_boundary(boundary)
    for loop in loops:
        loop.recreate_pattern()

    g.is_redraw_boundary = True
    g.is_redraw_pattern = True


def on_delete_boundary():
    # boundary删除，loop可能会被删除，loop被删pattern也会被删
    pass


def on_create_boundary():
    # boundary创建，loop可能增加，pattern可能增加
    pass


def on_change_patch_pattern():
    pass


def rotate_list(lst, n):
    if not lst:
        return []
    n = n % len(lst)
    return lst[n:] + lst[:n]


def edge_loops(bm, edge):
    # 获取循环边
    def walk(edge):
        yield edge
        edge.tag = True
        for l in edge.link_loops:
            loop = l.link_loop_radial_next.link_loop_next.link_loop_next
            if not (len(loop.face.verts) != 4 or loop.edge.tag):
                yield from walk(loop.edge)

    for e in bm.edges:
        e.tag = False
    return list(walk(edge))


def get_verts_and_edges_from_faces(bm, faces):
    faces = [i for i in faces if i in bm.faces]
    verts = set()
    edges = set()
    for face in faces:
        for edge in face.edges:
            edges.add(edge)
        for vert in face.verts:
            verts.add(vert)

    return list(verts), list(edges)


def smooth_verts_by_normal(obj, bm, verts, smooth_type=0):
    # https://github.com/fedackb/mesh-fairing

    # ##### BEGIN GPL LICENSE BLOCK #####
    #
    # This program is free software: you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation, either version 3 of the License, or
    # (at your option) any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    #
    # You should have received a copy of the GNU General Public License
    # along with this program.  If not, see <http://www.gnu.org/licenses/>.
    #
    # ##### END GPL LICENSE BLOCK #####
    def setup_fairing(
        v: bmesh.types.BMVert,
        i,
        A,
        b,
        multiplier,
        depth,
        vert_col_map,
        vert_weights,
        loop_weights,
    ):
        if depth == 0:
            if v in vert_col_map:
                j = vert_col_map[v]
                if (i, j) not in A:
                    A[i, j] = 0
                A[i, j] -= multiplier
            else:
                b[i][0] += multiplier * v.co.x
                b[i][1] += multiplier * v.co.y
                b[i][2] += multiplier * v.co.z
        else:
            w_ij_sum = 0
            w_i = vert_weights(v)
            for l in v.link_loops:
                other = l.link_loop_next.vert
                w_ij = loop_weights(l)
                w_ij_sum += w_ij
                setup_fairing(
                    other,
                    i,
                    A,
                    b,
                    w_i * w_ij * multiplier,
                    depth - 1,
                    vert_col_map,
                    vert_weights,
                    loop_weights,
                )
            setup_fairing(
                v,
                i,
                A,
                b,
                -1 * w_i * w_ij_sum * multiplier,
                depth - 1,
                vert_col_map,
                vert_weights,
                loop_weights,
            )

    def calc_circumcenter(a, b, c):
        ab = b - a
        ac = c - a
        ab_cross_ac = ab.cross(ac)
        if ab_cross_ac.length_squared > 0:
            d = ac.length_squared * ab_cross_ac.cross(ab)
            d += ab.length_squared * ac.cross(ab_cross_ac)
            d /= 2 * ab_cross_ac.length_squared
            return a + d
        else:
            return a

    def calc_uniform_vertex_weight(v):
        n = len(v.link_edges)
        return 1 / n if n != 0 else float("inf")

    def calc_uniform_loop_weight(l):
        return 1

    def calc_voronoi_vertex_weight(v):
        area = 0
        a = v.co
        acute_threshold = math.pi / 2
        for l in v.link_loops:
            b = l.link_loop_next.vert.co
            c = l.link_loop_prev.vert.co
            if l.calc_angle() < acute_threshold:
                d = calc_circumcenter(a, b, c)
            else:
                d = (b + c) / 2
            area += mathutils.geometry.area_tri(a, (a + b) / 2, d)
            area += mathutils.geometry.area_tri(a, d, (a + c) / 2)
        return 1 / area if area != 0 else 1e12

    def calc_cotangent_loop_weight(l):
        weight = 0
        co_a = l.vert.co
        co_b = l.link_loop_next.vert.co
        coords = [l.link_loop_prev.vert.co]
        if not l.edge.is_boundary:
            coords.append(l.link_loop_radial_next.link_loop_next.link_loop_next.vert.co)
        for co_c in coords:
            try:
                angle = (co_a - co_c).angle(co_b - co_c)
                weight += 1 / math.tan(angle)
            except (ValueError, ZeroDivisionError):
                weight += 1e-4
        weight /= 2
        return weight

    def _do(verts, order, vert_weights, loop_weights):
        interior_verts = (v for v in verts if not v.is_boundary and not v.is_wire)
        vert_col_map = {v: col for col, v in enumerate(interior_verts)}
        A = dict()
        b = [[0 for i in range(3)] for j in range(len(vert_col_map))]
        for v, col in vert_col_map.items():
            setup_fairing(
                v, col, A, b, 1, order, vert_col_map, vert_weights, loop_weights
            )

        x = None
        n = len(b)
        A_numpy = np.zeros((n, n), dtype="d")
        b = np.asarray(b, dtype="d")
        for key, val in A.items():
            A_numpy[key] = val
        x = np.linalg.solve(A_numpy, b)

        for v, co in zip(verts, x):
            v.co = co

    verts = [v for v in verts if v in bm.verts]
    if smooth_type == 0:
        _do(verts, 1, calc_uniform_vertex_weight, calc_uniform_loop_weight)
        _do(verts, 1, calc_voronoi_vertex_weight, calc_cotangent_loop_weight)
    else:
        _do(verts, 1, calc_uniform_vertex_weight, calc_uniform_loop_weight)
        _do(verts, 2, calc_voronoi_vertex_weight, calc_cotangent_loop_weight)

    bm.normal_update()
    bmesh.update_edit_mesh(obj.data)


# TODO: 位于边缘的顶点法线方向不准,看看有没有现成api,没有就采样旁边的面
def snap_verts_to_surface_along_normal(
    obj, bm, verts, bvh_trees, allow_opposite_direction=False
):

    snap_count = 0
    # 遍历所有选中的顶点
    for v in verts:
        # 获取顶点的全局坐标和法向
        global_co = obj.matrix_world @ v.co

        global_normal = normal_local_to_world(obj, v.normal)

        # if v.is_boundary:
        #     # 如果是边界顶点，计算链接面的平均法向量
        #     face_normals = []
        #     for face in v.link_faces:
        #         face_normals.append(face.normal)
        #     average_normal = (
        #         sum(face_normals, mathutils.Vector()) / len(face_normals)
        #         if face_normals
        #         else mathutils.Vector((0, 0, 0))
        #     )
        #     global_normal = (obj.matrix_world.to_3x3() @ average_normal).normalized()
        # else:
        #     # 否则，直接使用顶点的法向量
        #     global_normal = (obj.matrix_world.to_3x3() @ v.normal).normalized()

        closest_location = None
        closest_distance = float("inf")
        closest_location_opposite = None
        closest_distance_opposite = float("inf")

        # 遍历每个 BVH 树，优先正方向投射，避免吸附到物体另一侧造成拉伸
        for bvh_tree in bvh_trees:
            # 正方向射线投射
            result_pos = bvh_tree.ray_cast(global_co, global_normal)
            if result_pos[0]:
                location_pos, _, _, distance_pos = result_pos
                if distance_pos < closest_distance:
                    closest_location = location_pos
                    closest_distance = distance_pos

            if allow_opposite_direction:
                result_neg = bvh_tree.ray_cast(global_co, -global_normal)
                if result_neg[0]:
                    location_neg, _, _, distance_neg = result_neg
                    if distance_neg < closest_distance_opposite:
                        closest_location_opposite = location_neg
                        closest_distance_opposite = distance_neg

        # 如果找到最近的投影点，则将顶点移动到该点
        if not closest_location and closest_location_opposite:
            closest_location = closest_location_opposite

        if closest_location:
            # 将全局坐标转换回局部坐标
            new_local_co = obj.matrix_world.inverted() @ closest_location
            v.co = new_local_co
            snap_count += 1

    # 将顶点更改应用到网格上，loop_triangles提高效率
    bm.normal_update()
    bmesh.update_edit_mesh(obj.data, loop_triangles=True)


# 666case will be failed
def get_all_reduced_inputs(loop_shape, is_remove_duplicated_rotation=True):
    def reduce_and_explore(current_lst, results):
        reduced = False
        for k in range(len(current_lst)):
            prev_index = (k - 1) % len(current_lst)
            next_index = (k + 1) % len(current_lst)
            if current_lst[prev_index] > 1 and current_lst[next_index] > 1:
                new_lst = current_lst.copy()
                reduction = min(new_lst[prev_index], new_lst[next_index]) - 1
                new_lst[prev_index] -= reduction
                new_lst[next_index] -= reduction
                reduce_and_explore(new_lst, results)
                reduced = True

        if not reduced:
            results.add(tuple(current_lst))

    def is_rotate_dup(tup1, tup2):
        if len(tup1) != len(tup2):
            return False
        return any(tup1 == tup2[i:] + tup2[:i] for i in range(len(tup2)))

    def max_consecutive_ones(tup):
        count = 0
        max_count = 0
        for num in tup:
            if num == 1:
                count += 1
                max_count = max(max_count, count)
            else:
                count = 0
        return max_count

    # 两条边的时候特殊处理
    if len(loop_shape) == 2:
        if loop_shape[0] == loop_shape[1]:
            return [(2, 2)]
        else:
            if loop_shape[0] > loop_shape[1]:
                return [(3, 1)]
            else:
                return [(1, 3)]

    all_results = set()
    reduce_and_explore(list(loop_shape), all_results)

    if not is_remove_duplicated_rotation:
        return all_results

    # 删除重复
    unique_results = []
    for tup in list(all_results):
        if not any(is_rotate_dup(tup, existing) for existing in unique_results):
            unique_results.append(tup)

    # # 排序,1越少越往后,防止缩减错误和复杂度
    # sorted_results = sorted(unique_results, key=lambda x: x.count(1), reverse=True)

    sorted_results = sorted(
        unique_results,
        key=lambda x: (max_consecutive_ones(x), x.count(1)),
        reverse=True,
    )

    return sorted_results


def flip_A(A_):
    row_num = len(A_)
    A = []
    for row_idx, row in enumerate(A_):
        flip_row_idx = row_num - row_idx - 1
        row = row[:row_num] + A_[flip_row_idx][row_num:]
        A.append(row)
    return A


# def process_path_to_mirror_axis(path):
#     # 首先，x坐标全部归零
#     for co in path:
#         co.x = 0

#     # 然后，投射到表面（如果用户操作不是太离谱，这一步不用）
#     # 首先投射方向是在yz平面上的（即垂直于x轴），然后方向垂直于连线，有两个方向，都raycast，然后投射到距离最近的那个

#     for i, co in enumerate(path):
#         # 获取相邻的路径点
#         if i == len(path) - 1:
#             neighbor_co = path[-2]
#         else:
#             neighbor_co = path[i + 1]

#         line_vector = neighbor_co - co
#         x_normal = mathutils.Vector((1, 0, 0))

#         cross_product = line_vector.cross(
#             x_normal
#         )  # 两向量叉乘，得到垂直于两个向量的向量
#         raycast_direction = cross_product.normalized()

#         # 获取顶点的全局坐标
#         global_co = co

#         closest_location = None
#         closest_distance = float("inf")

#         # 遍历每个 BVH 树，进行双向射线投射
#         for bvh_tree in g.snap_bvh_trees:
#             # 正方向射线投射
#             result_pos = bvh_tree.ray_cast(global_co, raycast_direction)
#             if result_pos[0]:
#                 location_pos, _, _, distance_pos = result_pos
#                 if distance_pos < closest_distance:
#                     closest_location = location_pos
#                     closest_distance = distance_pos

#             # 负方向射线投射
#             result_neg = bvh_tree.ray_cast(global_co, -raycast_direction)
#             if result_neg[0]:
#                 location_neg, _, _, distance_neg = result_neg
#                 if distance_neg < closest_distance:
#                     closest_location = location_neg
#                     closest_distance = distance_neg

#         # 如果找到最近的投影点，则将坐标改为该投射点
#         if closest_location:
#             closest_location.x = 0
#             path[i] = closest_location


def project_point_to_plane(point_coord, plane_normal, plane_coord):
    # 确保法向量是单位向量
    plane_normal.normalize()

    # 计算点到平面的向量
    vector_to_plane = point_coord - plane_coord

    # 计算点到平面的垂直距离
    distance = vector_to_plane.dot(plane_normal)

    # 计算投影点
    projected_point = point_coord - distance * plane_normal

    return projected_point


def process_path_to_mirror_axis(path):
    obj_x_vector = g.obj.matrix_world.col[
        0
    ].xyz  # 获取 X 正方向的向量（第1列）,只取前三个分量 (X, Y, Z)
    obj_coord = g.obj.location

    # 先投影到轴上
    for i in range(len(path)):
        point_coord = path[i]
        path[i] = project_point_to_plane(point_coord, obj_x_vector, obj_coord)

    # 然后，投射到表面（如果用户操作不是太离谱，这一步不用）
    # 首先投射方向是在yz平面上的（即垂直于x轴），然后方向垂直于连线，有两个方向，都raycast，然后投射到距离最近的那个

    for i, co in enumerate(path):
        # 获取相邻的路径点
        if i == len(path) - 1:
            neighbor_co = path[-2]
        else:
            neighbor_co = path[i + 1]

        line_vector = neighbor_co - co
        x_normal = obj_x_vector

        cross_product = line_vector.cross(
            x_normal
        )  # 两向量叉乘，得到垂直于两个向量的向量
        raycast_direction = cross_product.normalized()

        # 获取顶点的全局坐标，这个就是全局，别转了
        global_co = co

        closest_location = None
        closest_distance = float("inf")

        # 遍历每个 BVH 树，进行双向射线投射
        for bvh_tree in g.snap_bvh_trees:
            # 正方向射线投射
            result_pos = bvh_tree.ray_cast(global_co, raycast_direction)
            if result_pos[0]:
                location_pos, _, _, distance_pos = result_pos
                if distance_pos < closest_distance:
                    closest_location = location_pos
                    closest_distance = distance_pos

            # 负方向射线投射
            result_neg = bvh_tree.ray_cast(global_co, -raycast_direction)
            if result_neg[0]:
                location_neg, _, _, distance_neg = result_neg
                if distance_neg < closest_distance:
                    closest_location = location_neg
                    closest_distance = distance_neg

        # 如果找到最近的投影点，则将坐标改为该投射点，这里再投影一次，不过理论上来说没有必要
        if closest_location:
            path[i] = project_point_to_plane(closest_location, obj_x_vector, obj_coord)


def coords_3d_to_2d(region, rv3d, coords_3d):
    coords_2d = []
    for coord in coords_3d:
        result = view3d_utils.location_3d_to_region_2d(region, rv3d, coord)
        if result is not None:
            coords_2d.append(result)
    return coords_2d


def set_block_before_done(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 函数开始前设置为
        g.is_changing_mesh = True
        try:
            # 执行函数
            return func(*args, **kwargs)
        finally:
            # 函数结束后设置为
            g.is_changing_mesh = False
            g.hovering_loop = None
            g.hovering_boundary = None

    return wrapper


def smooth_verts_by_avg(obj, bm, verts):
    verts = [v for v in verts if v in bm.verts]

    iterations = 5
    for _ in range(iterations):
        new_positions = []
        max_movement = 0

        for v in verts:
            avg = mathutils.Vector((0, 0, 0))
            for e in v.link_edges:
                avg += e.other_vert(v).co
            avg /= len(v.link_edges)

            # 计算移动量
            movement = (v.co - avg).length
            max_movement = max(max_movement, movement)
            new_positions.append((v, avg))

        for v, new_co in new_positions:
            v.co = new_co

    bm.normal_update()
    bmesh.update_edit_mesh(obj.data)


def flip_normal(obj, bm, faces):
    faces = [i for i in faces if i in bm.faces]
    for face in faces:
        face.normal_flip()
    bm.normal_update()
    bmesh.update_edit_mesh(obj.data)


# 法向全部朝视图
def faces_normal_toward_viewport(obj, bm, faces):
    # 确保faces只包含有效的面
    faces = [i for i in faces if i in bm.faces]

    region = bpy.context.region
    view_direction = view3d_utils.region_2d_to_vector_3d(
        region, region.data, (region.width / 2.0, region.height / 2.0)
    )
    # 变换物体空间的法向量到世界空间
    for face in faces:
        normal_world = normal_local_to_world(obj, face.normal)
        if normal_world.dot(view_direction) > 0:  # 和视图同向，反转
            face.normal_flip()
    bm.normal_update()
    bmesh.update_edit_mesh(obj.data)


def get_3d_area():
    try:
        for a in bpy.data.window_managers[0].windows[0].screen.areas:
            if a.type == "VIEW_3D":
                return a
        return None
    except:
        return None


def get_created_verts_num():
    g.temp_bm_verts = [v for v in g.temp_bm_verts if v in g.obj_bm.verts]
    return len(g.temp_bm_verts)


def get_package_name():
    return __name__.split(".")[0]


def get_preferences():
    return bpy.context.preferences.addons[get_package_name()].preferences


def is_mouse_move(event):
    if not g.last_mouse_co_2d:
        return True

    if (
        g.last_mouse_co_2d
        - mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
    ).length < const.mouse_move_threshold:
        return False
    else:
        return True
