# SPDX-FileCopyrightText: 2010-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "Cell Fracture",
    "author": "ideasman42, phymec, Sergey Sharybin",
    "version": (0, 2),
    "blender": (2, 80, 0),
    "location": "Viewport Object Menu -> Quick Effects",
    "description": "Fractured Object Creation",
    "warning": "",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/object/cell_fracture.html",
    "category": "Object",
}


# if "bpy" in locals():
#     import importlib
#     importlib.reload(fracture_cell_setup)

import bpy
from bpy.props import (
    StringProperty,
    BoolProperty,
    IntProperty,
    FloatProperty,
    FloatVectorProperty,
    EnumProperty,
)

from bpy.types import Operator


def main_object(context, collection, obj, level, **kw):
    import random

    # pull out some args
    kw_copy = kw.copy()
    use_recenter = kw_copy.pop("use_recenter")
    use_remove_original = kw_copy.pop("use_remove_original")
    recursion = kw_copy.pop("recursion")
    recursion_source_limit = kw_copy.pop("recursion_source_limit")
    recursion_clamp = kw_copy.pop("recursion_clamp")
    recursion_chance = kw_copy.pop("recursion_chance")
    recursion_chance_select = kw_copy.pop("recursion_chance_select")
    use_island_split = kw_copy.pop("use_island_split")
    use_debug_bool = kw_copy.pop("use_debug_bool")
    use_interior_vgroup = kw_copy.pop("use_interior_vgroup")
    use_sharp_edges = kw_copy.pop("use_sharp_edges")
    use_sharp_edges_apply = kw_copy.pop("use_sharp_edges_apply")

    scene = context.scene

    if level != 0:
        kw_copy["source_limit"] = recursion_source_limit

    from . import fracture_cell_setup

    # not essential but selection is visual distraction.
    obj.select_set(False)

    if kw_copy["use_debug_redraw"]:
        obj_display_type_prev = obj.display_type
        obj.display_type = 'WIRE'

    objects = fracture_cell_setup.cell_fracture_objects(context, collection, obj, **kw_copy)
    objects = fracture_cell_setup.cell_fracture_boolean(
        context, collection, obj, objects,
        use_island_split=use_island_split,
        use_interior_hide=(use_interior_vgroup or use_sharp_edges),
        use_debug_bool=use_debug_bool,
        use_debug_redraw=kw_copy["use_debug_redraw"],
        level=level,
    )

    # must apply after boolean.
    if use_recenter:
        context_override = {"selected_editable_objects": objects}
        with bpy.context.temp_override(**context_override):
            bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')

    # ----------
    # Recursion
    if level == 0:
        for level_sub in range(1, recursion + 1):

            objects_recurse_input = [(i, o) for i, o in enumerate(objects)]

            if recursion_chance != 1.0:
                from mathutils import Vector
                if recursion_chance_select == 'RANDOM':
                    random.shuffle(objects_recurse_input)
                elif recursion_chance_select in {'SIZE_MIN', 'SIZE_MAX'}:
                    objects_recurse_input.sort(key=lambda ob_pair: (
                        Vector(ob_pair[1].bound_box[0]) -
                        Vector(ob_pair[1].bound_box[6])
                    ).length_squared)
                    if recursion_chance_select == 'SIZE_MAX':
                        objects_recurse_input.reverse()
                elif recursion_chance_select in {'CURSOR_MIN', 'CURSOR_MAX'}:
                    c = scene.cursor.location.copy()
                    objects_recurse_input.sort(key=lambda ob_pair: (ob_pair[1].location - c).length_squared)
                    if recursion_chance_select == 'CURSOR_MAX':
                        objects_recurse_input.reverse()

                objects_recurse_input[int(recursion_chance * len(objects_recurse_input)):] = []
                objects_recurse_input.sort()

            # reverse index values so we can remove from original list.
            objects_recurse_input.reverse()

            objects_recursive = []
            for i, obj_cell in objects_recurse_input:
                assert(objects[i] is obj_cell)
                objects_recursive += main_object(context, collection, obj_cell, level_sub, **kw)
                if use_remove_original:
                    collection.objects.unlink(obj_cell)
                    del objects[i]
                if recursion_clamp and len(objects) + len(objects_recursive) >= recursion_clamp:
                    break
            objects.extend(objects_recursive)

            if recursion_clamp and len(objects) > recursion_clamp:
                break

    # --------------
    # Level Options
    if level == 0:
        # import pdb; pdb.set_trace()
        if use_interior_vgroup or use_sharp_edges:
            fracture_cell_setup.cell_fracture_interior_handle(
                objects,
                use_interior_vgroup=use_interior_vgroup,
                use_sharp_edges=use_sharp_edges,
                use_sharp_edges_apply=use_sharp_edges_apply,
            )

    if kw_copy["use_debug_redraw"]:
        obj.display_type = obj_display_type_prev

    # testing only!
    # obj.hide = True
    return objects


def main(context, **kw):
    import time
    t = time.time()
    objects_context = context.selected_editable_objects

    kw_copy = kw.copy()

    # mass
    mass_mode = kw_copy.pop("mass_mode")
    mass = kw_copy.pop("mass")
    collection_name = kw_copy.pop("collection_name")

    collection = context.collection
    if collection_name:
        collection_current = collection
        collection = bpy.data.collections.get(collection_name)
        if collection is None:
            collection = bpy.data.collections.new(collection_name)
            collection_current.children.link(collection)
        del collection_current

    objects = []
    for obj in objects_context:
        if obj.type == 'MESH':
            objects += main_object(context, collection, obj, 0, **kw_copy)

    bpy.ops.object.select_all(action='DESELECT')
    for obj_cell in objects:
        obj_cell.select_set(True)

    # FIXME(campbell): we should be able to initialize rigid-body data.
    if mass_mode == 'UNIFORM':
        for obj_cell in objects:
            rb = obj_cell.rigid_body
            if rb is not None:
                rb.mass = mass
    elif mass_mode == 'VOLUME':
        from mathutils import Vector

        def _get_volume(obj_cell):
            def _getObjectBBMinMax():
                min_co = Vector((1000000.0, 1000000.0, 1000000.0))
                max_co = -min_co
                matrix = obj_cell.matrix_world.copy()
                for i in range(0, 8):
                    bb_vec = matrix @ Vector(obj_cell.bound_box[i])
                    min_co[0] = min(bb_vec[0], min_co[0])
                    min_co[1] = min(bb_vec[1], min_co[1])
                    min_co[2] = min(bb_vec[2], min_co[2])
                    max_co[0] = max(bb_vec[0], max_co[0])
                    max_co[1] = max(bb_vec[1], max_co[1])
                    max_co[2] = max(bb_vec[2], max_co[2])
                return (min_co, max_co)

            def _getObjectVolume():
                min_co, max_co = _getObjectBBMinMax()
                x = max_co[0] - min_co[0]
                y = max_co[1] - min_co[1]
                z = max_co[2] - min_co[2]
                volume = x * y * z
                return volume

            return _getObjectVolume()

        obj_volume_ls = [_get_volume(obj_cell) for obj_cell in objects]
        obj_volume_tot = sum(obj_volume_ls)
        if obj_volume_tot > 0.0:
            mass_fac = mass / obj_volume_tot
            for i, obj_cell in enumerate(objects):
                rb = obj_cell.rigid_body
                if rb is not None:
                    rb.mass = obj_volume_ls[i] * mass_fac
    else:
        assert(0)

    print("Done! %d objects in %.4f sec" % (len(objects), time.time() - t))


class FractureCell(Operator):
    bl_idname = "object.add_fracture_cell_objects"
    bl_label = "Cell fracture selected mesh objects"
    bl_options = {'PRESET', 'UNDO'}

    # -------------------------------------------------------------------------
    # Source Options
    source: EnumProperty(
        name="Source",
        items=(
            ('VERT_OWN', "Own Verts", "Use own vertices"),
            ('VERT_CHILD', "Child Verts", "Use child object vertices"),
            ('PARTICLE_OWN', "Own Particles", (
                "All particle systems of the "
                "source object"
            )),
            ('PARTICLE_CHILD', "Child Particles", (
                "All particle systems of the "
                "child objects"
            )),
            ('PENCIL', "Annotation Pencil", "Annotation Grease Pencil."),
        ),
        options={'ENUM_FLAG'},
        default={'PARTICLE_OWN'},
    )

    source_limit: IntProperty(
        name="Source Limit",
        description="Limit the number of input points, 0 for unlimited",
        min=0, max=5000,
        default=100,
    )

    source_noise: FloatProperty(
        name="Noise",
        description="Randomize point distribution",
        min=0.0, max=1.0,
        default=0.0,
    )

    cell_scale: FloatVectorProperty(
        name="Scale",
        description="Scale Cell Shape",
        size=3,
        min=0.0, max=1.0,
        default=(1.0, 1.0, 1.0),
    )

    # -------------------------------------------------------------------------
    # Recursion

    recursion: IntProperty(
        name="Recursion",
        description="Break shards recursively",
        min=0, max=5000,
        default=0,
    )

    recursion_source_limit: IntProperty(
        name="Source Limit",
        description="Limit the number of input points, 0 for unlimited (applies to recursion only)",
        min=0, max=5000,
        default=8,
    )

    recursion_clamp: IntProperty(
        name="Clamp Recursion",
        description=(
            "Finish recursion when this number of objects is reached "
            "(prevents recursing for extended periods of time), zero disables"
        ),
        min=0, max=10000,
        default=250,
    )

    recursion_chance: FloatProperty(
        name="Random Factor",
        description="Likelihood of recursion",
        min=0.0, max=1.0,
        default=0.25,
    )

    recursion_chance_select: EnumProperty(
        name="Recurse Over",
        items=(
            ('RANDOM', "Random", ""),
            ('SIZE_MIN', "Small", "Recursively subdivide smaller objects"),
            ('SIZE_MAX', "Big", "Recursively subdivide bigger objects"),
            ('CURSOR_MIN', "Cursor Close", "Recursively subdivide objects closer to the cursor"),
            ('CURSOR_MAX', "Cursor Far", "Recursively subdivide objects farther from the cursor"),
        ),
        default='SIZE_MIN',
    )

    # -------------------------------------------------------------------------
    # Mesh Data Options

    use_smooth_faces: BoolProperty(
        name="Smooth Interior",
        description="Smooth Faces of inner side",
        default=False,
    )

    use_sharp_edges: BoolProperty(
        name="Sharp Edges",
        description="Set sharp edges when disabled",
        default=True,
    )

    use_sharp_edges_apply: BoolProperty(
        name="Apply Split Edge",
        description="Split sharp hard edges",
        default=True,
    )

    use_data_match: BoolProperty(
        name="Match Data",
        description="Match original mesh materials and data layers",
        default=True,
    )

    use_island_split: BoolProperty(
        name="Split Islands",
        description="Split disconnected meshes",
        default=True,
    )

    margin: FloatProperty(
        name="Margin",
        description="Gaps for the fracture (gives more stable physics)",
        min=0.0, max=1.0,
        default=0.001,
    )

    material_index: IntProperty(
        name="Material",
        description="Material index for interior faces",
        default=0,
    )

    use_interior_vgroup: BoolProperty(
        name="Interior VGroup",
        description="Create a vertex group for interior verts",
        default=False,
    )

    # -------------------------------------------------------------------------
    # Physics Options

    mass_mode: EnumProperty(
        name="Mass Mode",
        items=(
            ('VOLUME', "Volume", "Objects get part of specified mass based on their volume"),
            ('UNIFORM', "Uniform", "All objects get the specified mass"),
        ),
        default='VOLUME',
    )

    mass: FloatProperty(
        name="Mass",
        description="Mass to give created objects",
        min=0.001, max=1000.0,
        default=1.0,
    )

    # -------------------------------------------------------------------------
    # Object Options

    use_recenter: BoolProperty(
        name="Recenter",
        description="Recalculate the center points after splitting",
        default=True,
    )

    use_remove_original: BoolProperty(
        name="Remove Original",
        description="Removes the parents used to create the shatter",
        default=True,
    )

    # -------------------------------------------------------------------------
    # Scene Options
    #
    # .. different from object options in that this controls how the objects
    #    are setup in the scene.

    collection_name: StringProperty(
        name="Collection",
        description=(
            "Create objects in a collection "
            "(use existing or create new)"
        ),
    )

    # -------------------------------------------------------------------------
    # Debug
    use_debug_points: BoolProperty(
        name="Debug Points",
        description="Create mesh data showing the points used for fracture",
        default=False,
    )

    use_debug_redraw: BoolProperty(
        name="Show Progress Realtime",
        description="Redraw as fracture is done",
        default=True,
    )

    use_debug_bool: BoolProperty(
        name="Debug Boolean",
        description="Skip applying the boolean modifier",
        default=False,
    )

    def execute(self, context):
        keywords = self.as_keywords()  # ignore=("blah",)

        main(context, **keywords)

        return {'FINISHED'}

    def invoke(self, context, event):
        # print(self.recursion_chance_select)
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=600)

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        col = box.column()
        col.label(text="Point Source")
        rowsub = col.row()
        rowsub.prop(self, "source")
        rowsub = col.row()
        rowsub.prop(self, "source_limit")
        rowsub.prop(self, "source_noise")
        rowsub = col.row()
        rowsub.prop(self, "cell_scale")

        box = layout.box()
        col = box.column()
        col.label(text="Recursive Shatter")
        rowsub = col.row(align=True)
        rowsub.prop(self, "recursion")
        rowsub.prop(self, "recursion_source_limit")
        rowsub.prop(self, "recursion_clamp")
        rowsub = col.row()
        rowsub.prop(self, "recursion_chance")
        rowsub.prop(self, "recursion_chance_select", expand=True)

        box = layout.box()
        col = box.column()
        col.label(text="Mesh Data")
        rowsub = col.row()
        rowsub.prop(self, "use_smooth_faces")
        rowsub.prop(self, "use_sharp_edges")
        rowsub.prop(self, "use_sharp_edges_apply")
        rowsub.prop(self, "use_data_match")
        rowsub = col.row()

        # on same row for even layout but in fact are not all that related
        rowsub.prop(self, "material_index")
        rowsub.prop(self, "use_interior_vgroup")

        # could be own section, control how we subdiv
        rowsub.prop(self, "margin")
        rowsub.prop(self, "use_island_split")

        box = layout.box()
        col = box.column()
        col.label(text="Physics")
        rowsub = col.row(align=True)
        rowsub.prop(self, "mass_mode")
        rowsub.prop(self, "mass")

        box = layout.box()
        col = box.column()
        col.label(text="Object")
        rowsub = col.row(align=True)
        rowsub.prop(self, "use_recenter")

        box = layout.box()
        col = box.column()
        col.label(text="Scene")
        rowsub = col.row(align=True)
        rowsub.prop(self, "collection_name")

        box = layout.box()
        col = box.column()
        col.label(text="Debug")
        rowsub = col.row(align=True)
        rowsub.prop(self, "use_debug_redraw")
        rowsub.prop(self, "use_debug_points")
        rowsub.prop(self, "use_debug_bool")


def menu_func(self, context):
    layout = self.layout
    layout.separator()
    layout.operator("object.add_fracture_cell_objects", text="Cell Fracture")


def register():
    bpy.utils.register_class(FractureCell)
    bpy.types.VIEW3D_MT_object_quick_effects.append(menu_func)


def unregister():
    bpy.utils.unregister_class(FractureCell)
    bpy.types.VIEW3D_MT_object_quick_effects.remove(menu_func)


if __name__ == "__main__":
    register()
