# SPDX-License-Identifier: GPL-2.0-or-later

# Script copyright (C) Blender Foundation 2012

import bpy
import bmesh

if bpy.app.background:
    def _redraw_yasiamevil():
        pass
else:
    def _redraw_yasiamevil():
        _redraw_yasiamevil.opr(**_redraw_yasiamevil.arg)
    _redraw_yasiamevil.opr = bpy.ops.wm.redraw_timer
    _redraw_yasiamevil.arg = dict(type='DRAW_WIN_SWAP', iterations=1)


def _points_from_object(depsgraph, scene, obj, source):

    _source_all = {
        'PARTICLE_OWN', 'PARTICLE_CHILD',
        'PENCIL',
        'VERT_OWN', 'VERT_CHILD',
    }

    # print(source - _source_all)
    # print(source)
    assert(len(source | _source_all) == len(_source_all))
    assert(len(source))

    points = []

    def edge_center(mesh, edge):
        v1, v2 = edge.vertices
        return (mesh.vertices[v1].co + mesh.vertices[v2].co) / 2.0

    def poly_center(mesh, poly):
        from mathutils import Vector
        co = Vector()
        tot = 0
        for i in poly.loop_indices:
            co += mesh.vertices[mesh.loops[i].vertex_index].co
            tot += 1
        return co / tot

    def points_from_verts(obj):
        """Takes points from _any_ object with geometry"""
        if obj.type == 'MESH':
            mesh = obj.data
            matrix = obj.matrix_world.copy()
            points.extend([matrix @ v.co for v in mesh.vertices])
        else:
            ob_eval = ob.evaluated_get(depsgraph)
            try:
                mesh = ob_eval.to_mesh()
            except:
                mesh = None

            if mesh is not None:
                matrix = obj.matrix_world.copy()
                points.extend([matrix @ v.co for v in mesh.vertices])
                ob_eval.to_mesh_clear()

    def points_from_particles(obj):
        obj_eval = obj.evaluated_get(depsgraph)
        points.extend([p.location.copy()
                       for psys in obj_eval.particle_systems
                       for p in psys.particles])

    # geom own
    if 'VERT_OWN' in source:
        points_from_verts(obj)

    # geom children
    if 'VERT_CHILD' in source:
        for obj_child in obj.children:
            points_from_verts(obj_child)

    # geom particles
    if 'PARTICLE_OWN' in source:
        points_from_particles(obj)

    if 'PARTICLE_CHILD' in source:
        for obj_child in obj.children:
            points_from_particles(obj_child)

    # grease pencil
    def get_points(stroke):
        return [point.co.copy() for point in stroke.points]

    def get_splines(gp):
        if gp.layers.active:
            frame = gp.layers.active.active_frame
            return [get_points(stroke) for stroke in frame.strokes]
        else:
            return []

    if 'PENCIL' in source:
        # Used to be from object in 2.7x, now from scene.
        gp = scene.grease_pencil
        if gp:
            points.extend([p for spline in get_splines(gp) for p in spline])

    print("Found %d points" % len(points))

    return points


def cell_fracture_objects(
        context, collection, obj,
        source={'PARTICLE_OWN'},
        source_limit=0,
        source_noise=0.0,
        clean=True,
        # operator options
        use_smooth_faces=False,
        use_data_match=False,
        use_debug_points=False,
        margin=0.0,
        material_index=0,
        use_debug_redraw=False,
        cell_scale=(1.0, 1.0, 1.0),
):
    from . import fracture_cell_calc
    depsgraph = context.evaluated_depsgraph_get()
    scene = context.scene
    view_layer = context.view_layer

    # -------------------------------------------------------------------------
    # GET POINTS

    points = _points_from_object(depsgraph, scene, obj, source)

    if not points:
        # print using fallback
        points = _points_from_object(depsgraph, scene, obj, {'VERT_OWN'})

    if not points:
        print("no points found")
        return []

    # apply optional clamp
    if source_limit != 0 and source_limit < len(points):
        import random
        random.shuffle(points)
        points[source_limit:] = []

    # sadly we can't be sure there are no doubles
    from mathutils import Vector
    to_tuple = Vector.to_tuple
    points = list({to_tuple(p, 4): p for p in points}.values())
    del to_tuple
    del Vector

    # end remove doubles
    # ------------------

    if source_noise > 0.0:
        from random import random
        # boundbox approx of overall scale
        from mathutils import Vector
        matrix = obj.matrix_world.copy()
        bb_world = [matrix @ Vector(v) for v in obj.bound_box]
        scalar = source_noise * ((bb_world[0] - bb_world[6]).length / 2.0)

        from mathutils.noise import random_unit_vector

        points[:] = [p + (random_unit_vector() * (scalar * random())) for p in points]

    if use_debug_points:
        bm = bmesh.new()
        for p in points:
            bm.verts.new(p)
        mesh_tmp = bpy.data.meshes.new(name="DebugPoints")
        bm.to_mesh(mesh_tmp)
        bm.free()
        obj_tmp = bpy.data.objects.new(name=mesh_tmp.name, object_data=mesh_tmp)
        collection.objects.link(obj_tmp)
        del obj_tmp, mesh_tmp

    mesh = obj.data
    matrix = obj.matrix_world.copy()
    verts = [matrix @ v.co for v in mesh.vertices]

    cells = fracture_cell_calc.points_as_bmesh_cells(
        verts,
        points,
        cell_scale,
        margin_cell=margin,
    )

    # some hacks here :S
    cell_name = obj.name + "_cell"

    objects = []

    for center_point, cell_points in cells:

        # ---------------------------------------------------------------------
        # BMESH

        # create the convex hulls
        bm = bmesh.new()

        # WORKAROUND FOR CONVEX HULL BUG/LIMIT
        # XXX small noise
        import random

        def R():
            return (random.random() - 0.5) * 0.001
        # XXX small noise

        for i, co in enumerate(cell_points):

            # XXX small noise
            co.x += R()
            co.y += R()
            co.z += R()
            # XXX small noise

            bm_vert = bm.verts.new(co)

        import mathutils
        bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005)
        try:
            bmesh.ops.convex_hull(bm, input=bm.verts)
        except RuntimeError:
            import traceback
            traceback.print_exc()

        if clean:
            bm.normal_update()
            try:
                bmesh.ops.dissolve_limit(bm, verts=bm.verts, angle_limit=0.001)
            except RuntimeError:
                import traceback
                traceback.print_exc()
        # Smooth faces will remain only inner faces, after applying boolean modifier.
        if use_smooth_faces:
            for bm_face in bm.faces:
                bm_face.smooth = True

        if material_index != 0:
            for bm_face in bm.faces:
                bm_face.material_index = material_index

        # ---------------------------------------------------------------------
        # MESH
        mesh_dst = bpy.data.meshes.new(name=cell_name)

        bm.to_mesh(mesh_dst)
        bm.free()
        del bm

        if use_data_match:
            # match materials and data layers so boolean displays them
            # currently only materials + data layers, could do others...
            mesh_src = obj.data
            for mat in mesh_src.materials:
                mesh_dst.materials.append(mat)
            for lay_attr in ("vertex_colors", "uv_layers"):
                lay_src = getattr(mesh_src, lay_attr)
                lay_dst = getattr(mesh_dst, lay_attr)
                for key in lay_src.keys():
                    lay_dst.new(name=key)

        # ---------------------------------------------------------------------
        # OBJECT

        obj_cell = bpy.data.objects.new(name=cell_name, object_data=mesh_dst)
        collection.objects.link(obj_cell)
        # scene.objects.active = obj_cell
        obj_cell.location = center_point

        objects.append(obj_cell)

        # support for object materials
        if use_data_match:
            for i in range(len(mesh_dst.materials)):
                slot_src = obj.material_slots[i]
                slot_dst = obj_cell.material_slots[i]

                slot_dst.link = slot_src.link
                slot_dst.material = slot_src.material

        if use_debug_redraw:
            view_layer.update()
            _redraw_yasiamevil()

    view_layer.update()

    return objects


def cell_fracture_boolean(
        context, collection, obj, objects,
        use_debug_bool=False,
        clean=True,
        use_island_split=False,
        use_interior_hide=False,
        use_debug_redraw=False,
        level=0,
        remove_doubles=True
):

    objects_boolean = []
    scene = context.scene
    view_layer = context.view_layer

    if use_interior_hide and level == 0:
        # only set for level 0
        obj.data.polygons.foreach_set("hide", [False] * len(obj.data.polygons))

    for obj_cell in objects:
        mod = obj_cell.modifiers.new(name="Boolean", type='BOOLEAN')
        mod.object = obj
        mod.operation = 'INTERSECT'

        if not use_debug_bool:

            if use_interior_hide:
                obj_cell.data.polygons.foreach_set("hide", [True] * len(obj_cell.data.polygons))

    # Calculates all booleans at once (faster).
    depsgraph = context.evaluated_depsgraph_get()

    for obj_cell in objects:

        if not use_debug_bool:

            obj_cell_eval = obj_cell.evaluated_get(depsgraph)
            mesh_new = bpy.data.meshes.new_from_object(obj_cell_eval)
            mesh_old = obj_cell.data
            obj_cell.data = mesh_new
            obj_cell.modifiers.remove(obj_cell.modifiers[-1])

            # remove if not valid
            if not mesh_old.users:
                bpy.data.meshes.remove(mesh_old)
            if not mesh_new.vertices:
                collection.objects.unlink(obj_cell)
                if not obj_cell.users:
                    bpy.data.objects.remove(obj_cell)
                    obj_cell = None
                    if not mesh_new.users:
                        bpy.data.meshes.remove(mesh_new)
                        mesh_new = None

            # avoid unneeded bmesh re-conversion
            if mesh_new is not None:
                bm = None

                if clean:
                    if bm is None:  # ok this will always be true for now...
                        bm = bmesh.new()
                        bm.from_mesh(mesh_new)
                    bm.normal_update()
                    try:
                        bmesh.ops.dissolve_limit(bm, verts=bm.verts, edges=bm.edges, angle_limit=0.001)
                    except RuntimeError:
                        import traceback
                        traceback.print_exc()

                if remove_doubles:
                    if bm is None:
                        bm = bmesh.new()
                        bm.from_mesh(mesh_new)
                    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005)

                if bm is not None:
                    bm.to_mesh(mesh_new)
                    bm.free()

            del mesh_new
            del mesh_old

        if obj_cell is not None:
            objects_boolean.append(obj_cell)

            if use_debug_redraw:
                _redraw_yasiamevil()

    if (not use_debug_bool) and use_island_split:
        # this is ugly and Im not proud of this - campbell
        for ob in view_layer.objects:
            ob.select_set(False)
        for obj_cell in objects_boolean:
            obj_cell.select_set(True)

        bpy.ops.mesh.separate(type='LOOSE')

        objects_boolean[:] = [obj_cell for obj_cell in view_layer.objects if obj_cell.select_get()]

    context.view_layer.update()

    return objects_boolean


def cell_fracture_interior_handle(
        objects,
        use_interior_vgroup=False,
        use_sharp_edges=False,
        use_sharp_edges_apply=False,
):
    """Run after doing _all_ booleans"""

    assert(use_interior_vgroup or use_sharp_edges or use_sharp_edges_apply)

    for obj_cell in objects:
        mesh = obj_cell.data
        bm = bmesh.new()
        bm.from_mesh(mesh)

        if use_interior_vgroup:
            for bm_vert in bm.verts:
                bm_vert.tag = True
            for bm_face in bm.faces:
                if not bm_face.hide:
                    for bm_vert in bm_face.verts:
                        bm_vert.tag = False

            # now add all vgroups
            defvert_lay = bm.verts.layers.deform.verify()
            for bm_vert in bm.verts:
                if bm_vert.tag:
                    bm_vert[defvert_lay][0] = 1.0

            # add a vgroup
            obj_cell.vertex_groups.new(name="Interior")

        if use_sharp_edges:
            for bm_edge in bm.edges:
                if len({bm_face.hide for bm_face in bm_edge.link_faces}) == 2:
                    bm_edge.smooth = False

            if use_sharp_edges_apply:
                edges = [edge for edge in bm.edges if edge.smooth is False]
                if edges:
                    bm.normal_update()
                    bmesh.ops.split_edges(bm, edges=edges)

        for bm_face in bm.faces:
            bm_face.hide = False

        bm.to_mesh(mesh)
        bm.free()
