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

import bpy

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


from .enum_values import *
from .functions import *

from math import radians

# -----------------------------------------------------------------------------
# operator classes

class VIEW3D_OT_materialutilities_assign_material_edit(bpy.types.Operator):
    """Assign a material to the current selection"""

    bl_idname = "view3d.materialutilities_assign_material_edit"
    bl_label = "Assign Material (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    material_name: StringProperty(
            name = 'Material Name',
            description = 'Name of Material to assign to current selection',
            default = "",
            maxlen = 63
            )
    new_material: BoolProperty(
            name = '',
            description = 'Add a new material, enter the name in the box',
            default = False
            )
    show_dialog: BoolProperty(
            name = 'Show Dialog',
            default = False
            )

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def invoke(self, context, event):
        if self.show_dialog:
            return context.window_manager.invoke_props_dialog(self)
        else:
            return self.execute(context)

    def draw(self, context):
        layout = self.layout

        col = layout.column()
        row = col.split(factor = 0.9, align = True)

        if self.new_material:
            row.prop(self, "material_name")
        else:
            row.prop_search(self, "material_name", bpy.data, "materials")

        row.prop(self, "new_material", expand = True, icon = 'ADD')

    def execute(self, context):
        material_name = self.material_name

        if self.new_material:
            material_name = mu_new_material_name(material_name)
        elif material_name == "":
            self.report({'WARNING'}, "No Material Name given!")
            return {'CANCELLED'}

        return mu_assign_material(self, material_name, 'APPEND_MATERIAL')


class VIEW3D_OT_materialutilities_assign_material_object(bpy.types.Operator):
    """Assign a material to the current selection
    (See the operator panel [F9] for more options)"""

    bl_idname = "view3d.materialutilities_assign_material_object"
    bl_label = "Assign Material (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    material_name: StringProperty(
            name = 'Material Name',
            description = 'Name of Material to assign to current selection',
            default = "",
            maxlen = 63
            )
    override_type: EnumProperty(
            name = 'Assignment method',
            description = '',
            items = mu_override_type_enums
            )
    new_material: BoolProperty(
            name = '',
            description = 'Add a new material, enter the name in the box',
            default = False
            )
    show_dialog: BoolProperty(
            name = 'Show Dialog',
            default = False
            )

    @classmethod
    def poll(cls, context):
        return len(context.selected_editable_objects) > 0

    def invoke(self, context, event):
        if self.show_dialog:
            return context.window_manager.invoke_props_dialog(self)
        else:
            return self.execute(context)

    def draw(self, context):
        layout = self.layout

        col = layout.column()
        row = col.split(factor=0.9, align = True)

        if self.new_material:
            row.prop(self, "material_name")
        else:
            row.prop_search(self, "material_name", bpy.data, "materials")

        row.prop(self, "new_material", expand = True, icon = 'ADD')

        layout.prop(self, "override_type")


    def execute(self, context):
        material_name = self.material_name
        override_type = self.override_type

        if self.new_material:
            material_name = mu_new_material_name(material_name)
        elif material_name == "":
            self.report({'WARNING'}, "No Material Name given!")
            return {'CANCELLED'}

        result = mu_assign_material(self, material_name, override_type)
        return result

class VIEW3D_OT_materialutilities_select_by_material_name(bpy.types.Operator):
    """Select geometry that has the chosen material assigned to it
    (See the operator panel [F9] for more options)"""

    bl_idname = "view3d.materialutilities_select_by_material_name"
    bl_label = "Select By Material Name (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    extend_selection: BoolProperty(
            name = 'Extend Selection',
            description = 'Keeps the current selection and adds faces with the material to the selection'
            )
    material_name: StringProperty(
            name = 'Material Name',
            description = 'Name of Material to find and Select',
            maxlen = 63
            )
    show_dialog: BoolProperty(
            name = 'Show Dialog',
            default = False
    )

    @classmethod
    def poll(cls, context):
        return len(context.visible_objects) > 0

    def invoke(self, context, event):
        if self.show_dialog:
            return context.window_manager.invoke_props_dialog(self)
        else:
            return self.execute(context)

    def draw(self, context):
        layout = self.layout
        layout.prop_search(self, "material_name", bpy.data, "materials")

        layout.prop(self, "extend_selection", icon = "SELECT_EXTEND")

    def execute(self, context):
        material_name = self.material_name
        ext = self.extend_selection
        return mu_select_by_material_name(self, material_name, ext)


class VIEW3D_OT_materialutilities_copy_material_to_others(bpy.types.Operator):
    """Copy the material(s) of the active object to the other selected objects"""

    bl_idname = "view3d.materialutilities_copy_material_to_others"
    bl_label = "Copy material(s) to others (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')

    def execute(self, context):
        return mu_copy_material_to_others(self)


class VIEW3D_OT_materialutilities_clean_material_slots(bpy.types.Operator):
    """Removes any material slots from the selected objects that are not used"""

    bl_idname = "view3d.materialutilities_clean_material_slots"
    bl_label = "Clean Material Slots (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    # affect: EnumProperty(
    #         name = "Affect",
    #         description = "Which objects material slots should be cleaned",
    #         items = mu_clean_slots_enums,
    #         default = 'ACTIVE'
    #         )

    only_active: BoolProperty(
            name = 'Only active object',
            description = 'Only remove the material slots for the active object ' +
                            '(otherwise do it for every selected object)',
            default = True
            )

    @classmethod
    def poll(cls, context):
        return len(context.selected_editable_objects) > 0

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")

    def execute(self, context):
        affect = "ACTIVE" if self.only_active else "SELECTED"

        return mu_cleanmatslots(self, affect)


class VIEW3D_OT_materialutilities_remove_material_slot(bpy.types.Operator):
    """Remove the active material slot from selected object(s)
    (See the operator panel [F9] for more options)"""

    bl_idname = "view3d.materialutilities_remove_material_slot"
    bl_label = "Remove Active Material Slot (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    only_active: BoolProperty(
            name = 'Only active object',
            description = 'Only remove the active material slot for the active object ' +
                            '(otherwise do it for every selected object)',
            default = True
            )

    @classmethod
    def poll(cls, context):
        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")

    def execute(self, context):
        return mu_remove_material(self, self.only_active)

class VIEW3D_OT_materialutilities_remove_all_material_slots(bpy.types.Operator):
    """Remove all material slots from selected object(s)
    (See the operator panel [F9] for more options)"""

    bl_idname = "view3d.materialutilities_remove_all_material_slots"
    bl_label = "Remove All Material Slots (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    only_active: BoolProperty(
            name = 'Only active object',
            description = 'Only remove the material slots for the active object ' +
                            '(otherwise do it for every selected object)',
            default = True
            )

    @classmethod
    def poll(cls, context):
        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")

    def execute(self, context):
        return mu_remove_all_materials(self, self.only_active)


class VIEW3D_OT_materialutilities_replace_material(bpy.types.Operator):
    """Replace a material by name"""
    bl_idname = "view3d.materialutilities_replace_material"
    bl_label = "Replace Material (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    matorg: StringProperty(
            name = "Original",
            description = "Material to find and replace",
            maxlen = 63,
            )
    matrep: StringProperty(name="Replacement",
            description = "Material that will be used instead of the Original material",
            maxlen = 63,
            )
    all_objects: BoolProperty(
            name = "All Objects",
            description = "Replace for all objects in this blend file (otherwise only selected objects)",
            default = True,
            )
    update_selection: BoolProperty(
            name = "Update Selection",
            description = "Select affected objects and deselect unaffected",
            default = True,
            )

    def draw(self, context):
        layout = self.layout

        layout.prop_search(self, "matorg", bpy.data, "materials")
        layout.prop_search(self, "matrep", bpy.data, "materials")
        layout.separator()

        layout.prop(self, "all_objects", icon = "BLANK1")
        layout.prop(self, "update_selection", icon = "SELECT_INTERSECT")

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        return mu_replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection)


class VIEW3D_OT_materialutilities_fake_user_set(bpy.types.Operator):
    """Enable/disable fake user for materials"""

    bl_idname = "view3d.materialutilities_fake_user_set"
    bl_label = "Set Fake User (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    fake_user: EnumProperty(
            name = "Fake User",
            description = "Turn fake user on or off",
            items = mu_fake_user_set_enums,
            default = 'TOGGLE'
            )

    affect: EnumProperty(
            name = "Affect",
            description = "Which materials of objects to affect",
            items = mu_fake_user_affect_enums,
            default = 'UNUSED'
            )

    @classmethod
    def poll(cls, context):
        return (context.active_object is not None)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "fake_user", expand = True)
        layout.separator()

        layout.prop(self, "affect")

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        return mu_set_fake_user(self, self.fake_user, self.affect)


class VIEW3D_OT_materialutilities_change_material_link(bpy.types.Operator):
    """Link the materials to Data or Object, while keepng materials assigned"""

    bl_idname = "view3d.materialutilities_change_material_link"
    bl_label = "Change Material Linking (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    override: BoolProperty(
            name = "Override Data material",
            description = "Override the materials assigned to the object data/mesh when switching to 'Linked to Data'\n" +
                            "(WARNING: This will override the materials of other linked objects, " +
                             "which have the materials linked to Data)",
            default = False,
            )
    link_to: EnumProperty(
            name = "Link",
            description = "What should the material be linked to",
            items = mu_link_to_enums,
            default = 'OBJECT'
            )

    affect: EnumProperty(
            name = "Affect",
            description = "Which materials of objects to affect",
            items = mu_link_affect_enums,
            default = 'SELECTED'
            )

    @classmethod
    def poll(cls, context):
        return (context.active_object is not None)

    def draw(self, context):
        layout = self.layout

        layout.prop(self, "link_to", expand = True)
        layout.separator()

        layout.prop(self, "affect")
        layout.separator()

        layout.prop(self, "override", icon = "DECORATE_OVERRIDE")

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        return mu_change_material_link(self, self.link_to, self.affect, self.override)

class MATERIAL_OT_materialutilities_merge_base_names(bpy.types.Operator):
    """Merges materials that has the same base names but ends with .xxx (.001, .002 etc)"""

    bl_idname = "material.materialutilities_merge_base_names"
    bl_label = "Merge Base Names"
    bl_description = "Merge materials that has the same base names but ends with .xxx (.001, .002 etc)"

    material_base_name: StringProperty(
                            name = "Material Base Name",
                            default = "",
                            description = 'Base name for materials to merge ' +
                                          '(e.g. "Material" is the base name of "Material.001", "Material.002" etc.)'
                            )
    is_auto: BoolProperty(
                            name = "Auto Merge",
                            description = "Find all available duplicate materials and Merge them"
                            )

    is_not_undo = False
    material_error = []          # collect mat for warning messages


    def replace_name(self):
        """If the user chooses a material like 'Material.042', clean it up to get a base name ('Material')"""

        # use the chosen material as a base one, check if there is a name
        self.check_no_name = (False if self.material_base_name in {""} else True)

        # No need to do this if it's already "clean"
        #  (Also lessens the potential of error given about the material with the Base name)
        if '.' not in self.material_base_name:
            return

        if self.check_no_name is True:
            for mat in bpy.data.materials:
                name = mat.name

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

                        # trigger the exception
                        num = int(suffix, 10)
                        self.material_base_name = base
                        mat.name = self.material_base_name
                        return
                    except ValueError:
                        if name not in self.material_error:
                            self.material_error.append(name)
                        return

        return

    def split_name(self, material):
        """Split the material name into a base and a suffix"""

        name = material.name

        # No need to do this if it's already "clean"/there is no suffix
        if '.' not in name:
            return name, None

        base, suffix = name.rsplit('.', 1)

        try:
            # trigger the exception
            num = int(suffix, 10)
        except ValueError:
            # Not a numeric suffix
            # Don't report on materials not actually included in the merge!
            if ((self.is_auto or base == self.material_base_name)
                 and (name not in self.material_error)):
                self.material_error.append(name)
            return name, None

        if self.is_auto is False:
            if base == self.material_base_name:
                return base, suffix
            else:
                return name, None

        return base, suffix

    def fixup_slot(self, slot):
        """Fix material slots that was assigned to materials now removed"""

        if not slot.material:
            return

        base, suffix = self.split_name(slot.material)
        if suffix is None:
            return

        try:
            base_mat = bpy.data.materials[base]
        except KeyError:
            print("\n[Materials Utilities Specials]\nLink to base names\nError:"
                  "Base material %r not found\n" % base)
            return

        slot.material = base_mat

    def main_loop(self, context):
        """Loops through all objects and material slots to make sure they are assigned to the right material"""

        for obj in context.scene.objects:
            for slot in obj.material_slots:
                self.fixup_slot(slot)

    @classmethod
    def poll(self, context):
        return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0)

    def draw(self, context):
        layout = self.layout

        box_1 = layout.box()
        box_1.prop_search(self, "material_base_name", bpy.data, "materials")
        box_1.enabled = not self.is_auto
        layout.separator()

        layout.prop(self, "is_auto", text = "Auto Rename/Replace", icon = "SYNTAX_ON")

    def invoke(self, context, event):
        self.is_not_undo = True
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        # Reset Material errors, otherwise we risk reporting errors erroneously..
        self.material_error = []

        if not self.is_auto:
            self.replace_name()

            if self.check_no_name:
                self.main_loop(context)
            else:
                self.report({'WARNING'}, "No Material Base Name given!")

                self.is_not_undo = False
                return {'CANCELLED'}

        self.main_loop(context)

        if self.material_error:
            materials = ", ".join(self.material_error)

            if len(self.material_error) == 1:
                waswere = " was"
                suff_s = ""
            else:
                waswere = " were"
                suff_s = "s"

            self.report({'WARNING'}, materials + waswere + " not removed or set as Base" + suff_s)

        self.is_not_undo = False
        return {'FINISHED'}

class MATERIAL_OT_materialutilities_material_slot_move(bpy.types.Operator):
    """Move the active material slot"""

    bl_idname = "material.materialutilities_slot_move"
    bl_label = "Move Slot"
    bl_description = "Move the material slot"
    bl_options = {'REGISTER', 'UNDO'}

    movement: EnumProperty(
                name = "Move",
                description = "How to move the material slot",
                items = mu_material_slot_move_enums
                )

    @classmethod
    def poll(self, context):
        # would prefer to access self.movement here, but can't..
        obj = context.active_object
        if not obj:
            return False
        if (obj.active_material_index < 0) or (len(obj.material_slots) <= 1):
            return False
        return True

    def execute(self, context):
        active_object = context.active_object
        active_material = context.object.active_material

        if self.movement == 'TOP':
            dir = 'UP'

            steps = active_object.active_material_index
        else:
            dir = 'DOWN'

            last_slot_index = len(active_object.material_slots) - 1
            steps = last_slot_index - active_object.active_material_index

        if steps == 0:
            self.report({'WARNING'}, active_material.name + " already at " + self.movement.lower() + '!')
        else:
            for i in range(steps):
                bpy.ops.object.material_slot_move(direction = dir)

            self.report({'INFO'}, active_material.name + ' moved to ' + self.movement.lower())

        return {'FINISHED'}



class MATERIAL_OT_materialutilities_join_objects(bpy.types.Operator):
    """Join objects that have the same (selected) material(s)"""

    bl_idname = "material.materialutilities_join_objects"
    bl_label = "Join by material (Material Utilities)"
    bl_description = "Join objects that share the same material"
    bl_options = {'REGISTER', 'UNDO'}

    material_name: StringProperty(
                            name = "Material",
                            default = "",
                            description = 'Material to use to join objects'
                            )
    is_auto: BoolProperty(
                            name = "Auto Join",
                            description = "Join objects for all materials"
                            )

    is_not_undo = True
    material_error = []          # collect mat for warning messages


    @classmethod
    def poll(self, context):
        # This operator only works in Object mode
        return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0)

    def draw(self, context):
        layout = self.layout

        box_1 = layout.box()
        box_1.prop_search(self, "material_name", bpy.data, "materials")
        box_1.enabled = not self.is_auto
        layout.separator()

        layout.prop(self, "is_auto", text = "Auto Join", icon = "SYNTAX_ON")

    def invoke(self, context, event):
        self.is_not_undo = True
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        # Reset Material errors, otherwise we risk reporting errors erroneously..
        self.material_error = []
        materials = []

        if not self.is_auto:
            if self.material_name == "":
                self.report({'WARNING'}, "No Material Name given!")

                self.is_not_undo = False
                return {'CANCELLED'}
            materials = [self.material_name]
        else:
            materials = bpy.data.materials.keys()

        result = mu_join_objects(self, materials)
        self.is_not_undo = False

        return result


class MATERIAL_OT_materialutilities_auto_smooth_angle(bpy.types.Operator):
    """Set Auto smooth values for selected objects"""
    # Inspired by colkai

    bl_idname = "view3d.materialutilities_auto_smooth_angle"
    bl_label = "Set Auto Smooth Angle (Material Utilities)"
    bl_options = {'REGISTER', 'UNDO'}

    affect: EnumProperty(
            name = "Affect",
            description = "Which objects of to affect",
            items = mu_affect_enums,
            default = 'SELECTED'
            )
    angle: FloatProperty(
            name = "Angle",
            description = "Maximum angle between face normals that will be considered as smooth",
            subtype = 'ANGLE',
            min = 0,
            max = radians(180),
            default = radians(35)
            )
    set_smooth_shading: BoolProperty(
            name = "Set Smooth",
            description = "Set Smooth shading for the affected objects\n"
                   "This overrides the current smooth/flat shading that might be set to different parts of the object",
            default = True
            )

    @classmethod
    def poll(cls, context):
        return (len(bpy.data.objects) > 0) and (context.mode == 'OBJECT')

    def invoke(self, context, event):
        self.is_not_undo = True
        return context.window_manager.invoke_props_dialog(self)

    def draw(self, context):
        layout = self.layout

        layout.prop(self, "angle")
        layout.prop(self, "affect")

        layout.prop(self, "set_smooth_shading", icon = "BLANK1")

    def execute(self, context):
        return mu_set_auto_smooth(self, self.angle, self.affect, self.set_smooth_shading)
