import bpy
from math import radians, degrees

# -----------------------------------------------------------------------------
# utility functions

def mu_assign_material_slots(object, material_list):
    """Given an object and a list of material names removes all material slots from the object
       adds new ones for each material in the material list, adds the materials to the slots as well."""

    scene = bpy.context.scene
    active_object = bpy.context.active_object
    bpy.context.view_layer.objects.active = object

    for s in object.material_slots:
        bpy.ops.object.material_slot_remove()

    # re-add them and assign material
    i = 0
    for mat in material_list:
        material = bpy.data.materials[mat]
        object.data.materials.append(material)
        i += 1

    # restore active object:
    bpy.context.view_layer.objects.active = active_object

def mu_assign_to_data(object, material, index, edit_mode, all = True):
    """Assign the material to the object data (polygons/splines)"""

    if object.type == 'MESH':
        # now assign the material to the mesh
        mesh = object.data
        if all:
            for poly in mesh.polygons:
                poly.material_index = index
        else:
            for poly in mesh.polygons:
                if poly.select:
                    poly.material_index = index

        mesh.update()

    elif object.type in {'CURVE', 'SURFACE', 'TEXT'}:
        bpy.ops.object.mode_set(mode = 'EDIT')    # This only works in Edit mode

        # If operator was run in Object mode
        if not edit_mode:
            # Select everything in Edit mode
            bpy.ops.curve.select_all(action = 'SELECT')

        bpy.ops.object.material_slot_assign()   # Assign material of the current slot to selection

        if not edit_mode:
            bpy.ops.object.mode_set(mode = 'OBJECT')

def mu_new_material_name(material):
    for mat in bpy.data.materials:
        name = mat.name

        if (name == material):
            try:
                base, suffix = name.rsplit('.', 1)

                # trigger the exception
                num = int(suffix, 10)
                material = base + "." + '%03d' % (num + 1)
            except ValueError:
                material = material + ".001"

    return material


def mu_clear_materials(object):
    #obj.data.materials.clear()

    for mat in object.material_slots:
        bpy.ops.object.material_slot_remove()


def mu_assign_material(self, material_name = "Default", override_type = 'APPEND_MATERIAL', link_override = 'KEEP'):
    """Assign the defined material to selected polygons/objects"""

    # get active object so we can restore it later
    active_object = bpy.context.active_object

    edit_mode = False
    all_polygons = True
    if (not active_object is None) and active_object.mode == 'EDIT':
        edit_mode = True
        all_polygons = False
        bpy.ops.object.mode_set()

    # check if material exists, if it doesn't then create it
    found = False
    for material in bpy.data.materials:
        if material.name == material_name:
            target = material
            found = True
            break

    if not found:
        target = bpy.data.materials.new(mu_new_material_name(material_name))
        target.use_nodes = True         # When do we not want nodes today?


    index = 0
    objects = bpy.context.selected_editable_objects

    for obj in objects:
        # Apparently selected_editable_objects includes objects as cameras etc
        if not obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
            continue

        # set the active object to our object
        scene = bpy.context.scene
        bpy.context.view_layer.objects.active = obj

        if link_override == 'KEEP':
            if len(obj.material_slots) > 0:
                link = obj.material_slots[0].link
            else:
                link = 'DATA'
        else:
            link = link_override

        # If we should override all current material slots
        if override_type == 'OVERRIDE_ALL' or obj.type == 'META':

            # If there's more than one slot, Clear out all the material slots
            if len(obj.material_slots) > 1:
                mu_clear_materials(obj)

            # If there's no slots left/never was one, add a slot
            if len(obj.material_slots) == 0:
                bpy.ops.object.material_slot_add()

            # Assign the material to that slot
            obj.material_slots[0].link = link
            obj.material_slots[0].material = target

            if obj.type == 'META':
                self.report({'INFO'}, "Meta balls only support one material, all other materials overridden!")

        # If we should override each material slot
        elif override_type == 'OVERRIDE_SLOTS':
            i = 0
            # go through each slot
            for material in obj.material_slots:
                # assign the target material to current slot
                if not link_override == 'KEEP':
                    obj.material_slots[i].link = link
                obj.material_slots[i].material = target
                i += 1

        elif override_type == 'OVERRIDE_CURRENT':
            active_slot = obj.active_material_index

            if len(obj.material_slots) == 0:
                self.report({'INFO'}, 'No material slots found! A material slot was added!')
                bpy.ops.object.material_slot_add()

            obj.material_slots[active_slot].material = target

        # if we should keep the material slots and just append the selected material (if not already assigned)
        elif override_type == 'APPEND_MATERIAL':
            found = False
            i = 0
            material_slots = obj.material_slots

            if (obj.data.users > 1) and (len(material_slots) >= 1 and material_slots[0].link == 'OBJECT'):
                self.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' +
                                            'Unwanted results might happen!')

            # check material slots for material_name materia
            for material in material_slots:
                if material.name == material_name:
                    found = True
                    index = i

                    # make slot active
                    obj.active_material_index = i
                    break
                i += 1

            if not found:
                # In Edit mode, or if there's not a slot, append the assigned material
                #  If we're overriding, there's currently no materials at all, so after this there will be 1
                #  If not, this adds another slot with the assigned material

                index = len(obj.material_slots)
                bpy.ops.object.material_slot_add()
                obj.material_slots[index].link = link
                obj.material_slots[index].material = target
                obj.active_material_index = index

            mu_assign_to_data(obj, target, index, edit_mode, all_polygons)

    # We shouldn't risk unsetting the active object
    if not active_object is None:
        # restore the active object
        bpy.context.view_layer.objects.active = active_object

    if edit_mode:
        bpy.ops.object.mode_set(mode='EDIT')

    return {'FINISHED'}


def mu_select_by_material_name(self, find_material_name, extend_selection = False, internal = False):
    """Searches through all objects, or the polygons/curves of the current object
    to find and select objects/data with the desired material"""

    # in object mode selects all objects with material find_material_name
    # in edit mode selects all polygons with material find_material_name

    find_material = bpy.data.materials.get(find_material_name)

    if find_material is None:
        self.report({'INFO'}, "The material " + find_material_name + " doesn't exists!")
        return {'CANCELLED'} if not internal else -1

    # check for edit_mode
    edit_mode = False
    found_material = False

    scene = bpy.context.scene

    # set selection mode to polygons
    scene.tool_settings.mesh_select_mode = False, False, True

    active_object = bpy.context.active_object

    if (not active_object is None) and (active_object.mode == 'EDIT'):
        edit_mode = True

    if not edit_mode:
        objects = bpy.context.visible_objects

        for obj in objects:
            if obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
                mat_slots = obj.material_slots
                for material in mat_slots:
                    if material.material == find_material:
                        obj.select_set(state = True)

                        found_material = True

                        # the active object may not have the material!
                        # set it to one that does!
                        bpy.context.view_layer.objects.active = obj
                        break
                    else:
                        if not extend_selection:
                            obj.select_set(state=False)

            #deselect non-meshes
            elif not extend_selection:
                obj.select_set(state=False)

        if not found_material:
            if not internal:
                self.report({'INFO'}, "No objects found with the material " +
                                        find_material_name + "!")
            return {'FINISHED'} if not internal else 0

    else:
        # it's edit_mode, so select the polygons

        if active_object.type == 'MESH':
            # if not extending the selection, deselect all first
            #  (Without this, edges/faces were still selected
            #   while the faces were deselcted)
            if not extend_selection:
                bpy.ops.mesh.select_all(action = 'DESELECT')

        objects = bpy.context.selected_editable_objects

        for obj in objects:
            bpy.context.view_layer.objects.active = obj

            if obj.type == 'MESH':
                bpy.ops.object.mode_set()

                mat_slots = obj.material_slots

                # same material can be on multiple slots
                slot_indeces = []
                i = 0
                for material in mat_slots:
                    if material.material == find_material:
                        slot_indeces.append(i)
                    i += 1

                mesh = obj.data

                for poly in mesh.polygons:
                    if poly.material_index in slot_indeces:
                        poly.select = True
                        found_material = True
                    elif not extend_selection:
                        poly.select = False

                mesh.update()

                bpy.ops.object.mode_set(mode = 'EDIT')


            elif obj.type in {'CURVE', 'SURFACE'}:
                # For Curve objects, there can only be one material per spline
                #  and thus each spline is linked to one material slot.
                #  So to not have to care for different data structures
                #  for different curve types, we use the material slots
                #  and the built in selection methods
                #  (Technically, this should work for meshes as well)

                mat_slots = obj.material_slots

                i = 0
                for material in mat_slots:
                    bpy.context.active_object.active_material_index = i

                    if material.material == find_material:
                        bpy.ops.object.material_slot_select()
                        found_material = True
                    elif not extend_selection:
                        bpy.ops.object.material_slot_deselect()

                    i += 1

            elif not internal:
                # Some object types are not supported
                #  mostly because don't really support selecting by material (like Font/Text objects)
                #  ore that they don't support multiple materials/are just "weird" (i.e. Meta balls)
                self.report({'WARNING'}, "The type '" +
                                            obj.type +
                                            "' isn't supported in Edit mode by Material Utilities!")
                #return {'CANCELLED'}

        bpy.context.view_layer.objects.active = active_object

        if (not found_material) and (not internal):
            self.report({'INFO'}, "Material " + find_material_name + " isn't assigned to anything!")

    return {'FINISHED'} if not internal else 1


def mu_copy_material_to_others(self):
    """Copy the material to of the current object to the other seleceted all_objects"""
    # Currently uses the built-in method
    #  This could be extended to work in edit mode as well

    #active_object = context.active_object

    bpy.ops.object.material_slot_copy()

    return {'FINISHED'}


def mu_cleanmatslots(self, affect):
    """Clean the material slots of the seleceted objects"""

    # check for edit mode
    edit_mode = False
    active_object = bpy.context.active_object
    if active_object.mode == 'EDIT':
        edit_mode = True
        bpy.ops.object.mode_set()

    objects = []

    if affect == 'ACTIVE':
        objects = [active_object]
    elif affect == 'SELECTED':
        objects = bpy.context.selected_editable_objects
    elif affect == 'SCENE':
        objects = bpy.context.scene.objects
    else: # affect == 'ALL'
        objects = bpy.data.objects

    for obj in objects:
        used_mat_index = []  # we'll store used materials indices here
        assigned_materials = []
        material_list = []
        material_names = []

        materials = obj.material_slots.keys()

        if obj.type == 'MESH':
            # check the polygons on the mesh to build a list of used materials
            mesh = obj.data

            for poly in mesh.polygons:
                # get the material index for this face...
                material_index = poly.material_index

                if material_index >= len(materials):
                    poly.select = True
                    self.report({'ERROR'},
                                "A poly with an invalid material was found, this should not happen! Canceling!")
                    return {'CANCELLED'}

                # indices will be lost: Store face mat use by name
                current_mat = materials[material_index]
                assigned_materials.append(current_mat)

                # check if index is already listed as used or not
                found = False
                for mat in used_mat_index:
                    if mat == material_index:
                        found = True

                if not found:
                    # add this index to the list
                    used_mat_index.append(material_index)

            # re-assign the used materials to the mesh and leave out the unused
            for u in used_mat_index:
                material_list.append(materials[u])
                # we'll need a list of names to get the face indices...
                material_names.append(materials[u])

            mu_assign_material_slots(obj, material_list)

            # restore face indices:
            i = 0
            for poly in mesh.polygons:
                material_index = material_names.index(assigned_materials[i])
                poly.material_index = material_index
                i += 1

        elif obj.type in {'CURVE', 'SURFACE'}:

            splines = obj.data.splines

            for spline in splines:
                # Get the material index of this spline
                material_index = spline.material_index

                # indices will be last: Store material use by name
                current_mat = materials[material_index]
                assigned_materials.append(current_mat)

                # check if indek is already listed as used or not
                found = False
                for mat in used_mat_index:
                    if mat == material_index:
                        found = True

                if not found:
                    # add this index to the list
                    used_mat_index.append(material_index)

            # re-assigned the used materials to the curve and leave out the unused
            for u in used_mat_index:
                material_list.append(materials[u])
                # we'll need a list of names to get the face indices
                material_names.append(materials[u])

            mu_assign_material_slots(obj, material_list)

            # restore spline indices
            i = 0
            for spline in splines:
                material_index = material_names.index(assigned_materials[i])
                spline.material_index = material_index
                i += 1

        else:
            # Some object types are not supported
            self.report({'WARNING'},
                        "The type '" + obj.type + "' isn't currently supported " +
                        "for Material slots cleaning by Material Utilities!")

    if edit_mode:
        bpy.ops.object.mode_set(mode='EDIT')

    return {'FINISHED'}

def mu_remove_material(self, for_active_object = False):
    """Remove the active material slot from selected object(s)"""

    if for_active_object:
        bpy.ops.object.material_slot_remove()
    else:
        last_active = bpy.context.active_object
        objects = bpy.context.selected_editable_objects

        for obj in objects:
            bpy.context.view_layer.objects.active = obj
            bpy.ops.object.material_slot_remove()

        bpy.context.view_layer.objects.active =  last_active

    return {'FINISHED'}

def mu_remove_all_materials(self, for_active_object = False):
    """Remove all material slots from selected object(s)"""

    if for_active_object:
        obj = bpy.context.active_object

        # Clear out the material slots
        obj.data.materials.clear()

    else:
        last_active = bpy.context.active_object
        objects = bpy.context.selected_editable_objects

        for obj in objects:
            obj.data.materials.clear()

        bpy.context.view_layer.objects.active = last_active

    return {'FINISHED'}


def mu_replace_material(material_a, material_b, all_objects=False, update_selection=False):
    """Replace one material with another material"""

    # material_a is the name of original material
    # material_b is the name of the material to replace it with
    # 'all' will replace throughout the blend file

    mat_org = bpy.data.materials.get(material_a)
    mat_rep = bpy.data.materials.get(material_b)

    if mat_org != mat_rep and None not in (mat_org, mat_rep):
        # Store active object
        scn = bpy.context.scene

        if all_objects:
            objs = bpy.data.objects
        else:
            objs = bpy.context.selected_editable_objects

        for obj in objs:
            if obj.type == 'MESH':
                match = False

                for mat in obj.material_slots:
                    if mat.material == mat_org:
                        mat.material = mat_rep

                        # Indicate which objects were affected
                        if update_selection:
                            obj.select_set(state = True)
                            match = True

                if update_selection and not match:
                    obj.select_set(state = False)

    return {'FINISHED'}


def mu_set_fake_user(self, fake_user, materials):
    """Set the fake user flag for the objects material"""

    if materials == 'ALL':
        mats = (mat for mat in bpy.data.materials if mat.library is None)
    elif materials == 'UNUSED':
        mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0)
    else:
        mats = []
        if materials == 'ACTIVE':
            objs = [bpy.context.active_object]
        elif materials == 'SELECTED':
            objs = bpy.context.selected_objects
        elif materials == 'SCENE':
            objs = bpy.context.scene.objects
        else: # materials == 'USED'
            objs = bpy.data.objects
            # Maybe check for users > 0 instead?

        mats = (mat for ob in objs
                    if hasattr(ob.data, "materials")
                        for mat in ob.data.materials
                            if mat.library is None)

    if fake_user == 'TOGGLE':
        done_mats = []
        for mat in mats:
            if  not mat.name in done_mats:
                mat.use_fake_user = not mat.use_fake_user
            done_mats.append(mat.name)
    else:
        fake_user_val = fake_user == 'ON'
        for mat in mats:
            mat.use_fake_user = fake_user_val

    for area in bpy.context.screen.areas:
        if area.type in ('PROPERTIES', 'NODE_EDITOR'):
            area.tag_redraw()

    return {'FINISHED'}


def mu_change_material_link(self, link, affect, override_data_material = False):
    """Change what the materials are linked to (Object or Data), while keeping materials assigned"""

    objects = []

    if affect == "ACTIVE":
        objects = [bpy.context.active_object]
    elif affect == "SELECTED":
        objects = bpy.context.selected_objects
    elif affect == "SCENE":
        objects = bpy.context.scene.objects
    elif affect == "ALL":
        objects = bpy.data.objects

    for object in objects:
        index = 0
        for slot in object.material_slots:
            present_material = slot.material

            if link == 'TOGGLE':
                slot.link = ('DATA' if slot.link == 'OBJECT' else 'OBJECT')
            else:
                slot.link = link

            if slot.link == 'OBJECT':
                override_data_material = True
            elif slot.material is None:
                override_data_material = True
            elif not override_data_material:
                self.report({'INFO'},
                            'The object Data for object ' + object.name_full + ' already had a material assigned ' +
                            'to slot #' + str(index) + ' (' + slot.material.name + '), it was not overridden!')

            if override_data_material:
                slot.material = present_material

            index = index + 1

    return {'FINISHED'}

def mu_join_objects(self, materials):
    """Join objects together based on their material"""

    for material in materials:
        mu_select_by_material_name(self, material, False, True)

        bpy.ops.object.join()

    return {'FINISHED'}

def mu_set_auto_smooth(self, angle, affect, set_smooth_shading):
    """Set Auto smooth values for selected objects"""
    # Inspired by colkai

    objects = []
    objects_affected = 0

    if affect == "ACTIVE":
        objects = [bpy.context.active_object]
    elif affect == "SELECTED":
        objects = bpy.context.selected_editable_objects
    elif affect == "SCENE":
        objects = bpy.context.scene.objects
    elif affect == "ALL":
        objects = bpy.data.objects

    if len(objects) == 0:
        self.report({'WARNING'}, 'No objects available to set Auto Smooth on')
        return {'CANCELLED'}

    for object in objects:
        if object.type == "MESH":
            if set_smooth_shading:
                for poly in object.data.polygons:
                    poly.use_smooth = True

                #bpy.ops.object.shade_smooth()

            object.data.use_auto_smooth = 1
            object.data.auto_smooth_angle = angle  # 35 degrees as radians

            objects_affected += 1

    self.report({'INFO'}, 'Auto smooth angle set to %.0f° on %d of %d objects' %
                            (degrees(angle), objects_affected, len(objects)))

    return {'FINISHED'}
