# gpl: author Nobuyuki Hirakata

import bpy

import bmesh
from random import (
        gauss,
        seed,
        )
from math import radians
from mathutils import Euler


# Allow changing the original material names from the .blend file
# by replacing them with the UI Names from the EnumProperty
def get_ui_mat_name(mat_name):
    mat_ui_name = "CrackIt Material"
    try:
        # access the Scene type directly to get the name from the enum
        mat_items = bpy.types.Scene.crackit[1]["type"].bl_rna.material_preset[1]["items"]
        for mat_id, mat_list in enumerate(mat_items):
            if mat_name in mat_list:
                mat_ui_name = mat_items[mat_id][1]
                break
        del mat_items
    except Exception as e:
        error_handlers(
                False, "get_ui_mat_name", e,
                "Retrieving the EnumProperty key UI Name could not be completed", True
                )
        pass

    return mat_ui_name


def error_handlers(self, op_name, error, reports="ERROR", func=False):
    if self and reports:
        self.report({'WARNING'}, reports + " (See Console for more info)")

    is_func = "Function" if func else "Operator"
    print("\n[Cell Fracture Crack It]\n{}: {}\nError: "
          "{}\nReport: {}\n".format(is_func, op_name, error, reports))


# -------------------- Crack -------------------
# Cell fracture and post-process:
def makeFracture(child_verts=False, division=100, noise=0.00,
                scaleX=1.00, scaleY=1.00, scaleZ=1.00, recursion=0, margin=0.001):

    # Get active object name and active layer
    active_name = bpy.context.view_layer.objects.active.name
    active_layer = bpy.context.scene.active_layer

    # source method of whether use child verts
    if child_verts is True:
        crack_source = 'VERT_CHILD'
    else:
        crack_source = 'PARTICLE_OWN'

    bpy.ops.object.add_fracture_cell_objects(
            source={crack_source}, source_limit=division, source_noise=noise,
            cell_scale=(scaleX, scaleY, scaleZ), recursion=recursion,
            recursion_source_limit=8, recursion_clamp=250, recursion_chance=0.25,
            recursion_chance_select='SIZE_MIN', use_smooth_faces=False,
            use_sharp_edges=False, use_sharp_edges_apply=True, use_data_match=True,
            use_island_split=True, margin=margin, material_index=0,
            use_interior_vgroup=False, mass_mode='VOLUME', mass=1, use_recenter=True,
            use_remove_original=True, use_layer_index=0, use_layer_next=False,
            group_name="", use_debug_points=False, use_debug_redraw=True, use_debug_bool=False
            )

    _makeJoin(active_name, active_layer)


# Join fractures into an object
def _makeJoin(active_name, active_layer):
    # Get object by name
    bpy.ops.object.select_all(action='DESELECT')
    bpy.ops.object.select_pattern(pattern=active_name + '_cell*')
    fractures = bpy.context.selected_objects

    if fractures:
        # Execute join
        bpy.context.view_layer.objects.active = fractures[0]
        fractures[0].select_set(True)
        bpy.ops.object.join()
    else:
        error_handlers(
            False, "_makeJoin", "if fractures condition has not passed",
            "Warning: No objects could be joined", True
            )

    # Change name
    bpy.context.view_layer.objects.active.name = active_name + '_crack'

    # Change origin
    bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN')


# Add modifier and setting
def addModifiers():
    bpy.ops.object.modifier_add(type='DECIMATE')
    decimate = bpy.context.object.modifiers[-1]
    decimate.name = 'DECIMATE_crackit'
    decimate.ratio = 0.4

    bpy.ops.object.modifier_add(type='SUBSURF')
    subsurf = bpy.context.object.modifiers[-1]
    subsurf.name = 'SUBSURF_crackit'

    bpy.ops.object.modifier_add(type='SMOOTH')
    smooth = bpy.context.object.modifiers[-1]
    smooth.name = 'SMOOTH_crackit'


# -------------- multi extrude --------------------
# var1=random offset, var2=random rotation, var3=random scale
def multiExtrude(off=0.1, rotx=0, roty=0, rotz=0, sca=1.0,
                var1=0.01, var2=0.3, var3=0.3, num=1, ran=0):

    obj = bpy.context.object
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    # bmesh operations
    bpy.ops.object.mode_set()
    bm = bmesh.new()
    bm.from_mesh(obj.data)
    sel = [f for f in bm.faces if f.select]

    # faces loop
    for i, of in enumerate(sel):
        rot = _vrot(r=i, ran=ran, rotx=rotx, var2=var2, roty=roty, rotz=rotz)
        off = _vloc(r=i, ran=ran, off=off, var1=var1)
        of.normal_update()

        # extrusion loop
        for r in range(num):
            nf = of.copy()
            nf.normal_update()
            no = nf.normal.copy()
            ce = nf.calc_center_bounds()
            s = _vsca(r=i + r, ran=ran, var3=var3, sca=sca)

            for v in nf.verts:
                v.co -= ce
                v.co.rotate(rot)
                v.co += ce + no * off
                v.co = v.co.lerp(ce, 1 - s)

            # extrude code from TrumanBlending
            for a, b in zip(of.loops, nf.loops):
                sf = bm.faces.new((a.vert, a.link_loop_next.vert,
                                   b.link_loop_next.vert, b.vert))
                sf.normal_update()

            bm.faces.remove(of)
            of = nf

    for v in bm.verts:
        v.select = False

    for e in bm.edges:
        e.select = False

    bm.to_mesh(obj.data)
    obj.data.update()


def _vloc(r, ran, off, var1):
    seed(ran + r)
    return off * (1 + gauss(0, var1 / 3))


def _vrot(r, ran, rotx, var2, roty, rotz):
    seed(ran + r)
    return Euler((radians(rotx) + gauss(0, var2 / 3),
                radians(roty) + gauss(0, var2 / 3),
                radians(rotz) + gauss(0, var2 / 3)), 'XYZ')


def _vsca(r, ran, sca, var3):
    seed(ran + r)
    return sca * (1 + gauss(0, var3 / 3))


# Centroid of a selection of vertices
def _centro(ver):
    vvv = [v for v in ver if v.select]
    if not vvv or len(vvv) == len(ver):
        return ('error')

    x = sum([round(v.co[0], 4) for v in vvv]) / len(vvv)
    y = sum([round(v.co[1], 4) for v in vvv]) / len(vvv)
    z = sum([round(v.co[2], 4) for v in vvv]) / len(vvv)

    return (x, y, z)


# Retrieve the original state of the object
def _volver(obj, copia, om, msm, msv):
    for i in copia:
        obj.data.vertices[i].select = True
    bpy.context.tool_settings.mesh_select_mode = msm

    for i in range(len(msv)):
        obj.modifiers[i].show_viewport = msv[i]


# -------------- Material preset --------------------------
def appendMaterial(addon_path, material_name, mat_ui_names="Nameless Material"):
    # Load material from the addon directory
    file_path = _makeFilePath(addon_path=addon_path)
    bpy.ops.wm.append(filename=material_name, directory=file_path)

    # If material is loaded some times, select the last-loaded material
    last_material = _getAppendedMaterial(material_name)

    if last_material:
        mat = bpy.data.materials[last_material]
        # skip renaming if the prop is True
        if not bpy.context.scene.crackit.material_lib_name:
            mat.name = mat_ui_names

        # Apply Only one material in the material slot
        for m in bpy.context.object.data.materials:
            bpy.ops.object.material_slot_remove()

        bpy.context.object.data.materials.append(mat)

        return True

    return False


# Make file path of addon
def _makeFilePath(addon_path):
    material_folder = "/materials"
    blend_file = "/materials1.blend"
    category = "\\Material\\"

    file_path = addon_path + material_folder + blend_file + category
    return file_path


# Get last-loaded material, such as ~.002
def _getAppendedMaterial(material_name):
    # Get material name list
    material_names = [m.name for m in bpy.data.materials if material_name in m.name]
    if material_names:
        # Return last material in the sorted order
        material_names.sort()

        return material_names[-1]

    return None
