"""Alle Operatoren des Add-ons."""

import os

import bpy

from . import csv_io
from . import previews
from .material import FIELDS, material_manager

# --- Node-Layout-Konstanten fuer die prozeduralen Effekte ---------------------
TEXTURE_NODE_LOCATION = (-600, 400)
TEX_COORD_LOCATION = (-1150, 400)
MAPPING_LOCATION = (-1100, 400)
MAP_RANGE_LOCATION = (-750, 400)
MATH_NODE_LOCATION = (-700, 400)
SCALE_VALUE = (0.2, 0.2, 0.2)
TO_MIN_VALUE = -0.05
TO_MAX_VALUE = 0.05
NODE_VERTICAL_SPACING = -300 * 1.5
NODE_HORIZONTAL_SPACING = 150
VERTICAL_OFFSET = -400
HORIZONTAL_OFFSET = -200


def set_base_color(bsdf, farbe):
    """Basisfarbe setzen — funktioniert auch, wenn 'Base Color' belegt ist.

    Nach einem AO-Bake haengt an Base Color eine Multiply-Kette
    (RGB-Node -> Mix(Multiply) <- AO-Textur); der BSDF-Wert allein waere dann
    wirkungslos. Deshalb: BSDF-Wert IMMER mitsetzen (greift wieder, sobald
    jemand absteckt) und zusaetzlich rekursiv aufwaerts durch den Baum bis zum
    ersten RGB-Node laufen und die Farbe dort einschreiben.
    Rueckgabe False, wenn verlinkt, aber kein RGB-Node zu finden ist
    (z. B. echte Textur als Farbquelle) - dann wurde nur der BSDF-Wert gesetzt.
    """
    socket = bsdf.inputs['Base Color']
    socket.default_value = farbe
    if not socket.is_linked:
        return True

    besucht = set()

    def erster_rgb_node(node):
        if node.name in besucht:
            return None
        besucht.add(node.name)
        if node.type == 'RGB':
            return node
        for eingang in node.inputs:
            for link in eingang.links:
                treffer = erster_rgb_node(link.from_node)
                if treffer is not None:
                    return treffer
        return None

    rgb = erster_rgb_node(socket.links[0].from_node)
    if rgb is None:
        return False
    rgb.outputs['Color'].default_value = farbe
    return True


class MATERIAL_OT_shared_geo(bpy.types.Operator):
    bl_idname = "material.shared_geo"
    bl_label = "Share Geo"
    bl_description = "Separate loose parts, share geometry, and create a collection"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}
        if obj.type != 'MESH':
            self.report({'ERROR'}, "Selected object is not a mesh.")
            return {'CANCELLED'}

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.separate(type='LOOSE')
        bpy.ops.object.mode_set(mode='OBJECT')
        collection = bpy.data.collections.new(obj.name)
        context.scene.collection.children.link(collection)
        for old_coll in obj.users_collection:
            old_coll.objects.unlink(obj)
        collection.objects.link(obj)
        for new_obj in context.selected_objects:
            if new_obj != obj:
                for old_coll in new_obj.users_collection:
                    old_coll.objects.unlink(new_obj)
                collection.objects.link(new_obj)
        for new_obj in collection.objects:
            new_obj.select_set(True)
            context.view_layer.objects.active = new_obj
            bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        bpy.ops.object.make_links_data(type='OBDATA')
        return {'FINISHED'}


class MATERIAL_OT_UpdateMaterial(bpy.types.Operator):
    bl_idname = "material.update_material"
    bl_label = "Update Material"
    bl_description = "Aktive Material Update"
    material_name: bpy.props.StringProperty()

    def execute(self, context):
        if context.object and context.object.active_material:
            blender_material = context.object.active_material
            bsdf = blender_material.node_tree.nodes.get("Principled BSDF")
            if not bsdf:
                self.report({'ERROR'}, "Principled BSDF node not found in material.")
                return {'CANCELLED'}
            csv_io.update_material(self.material_name, bsdf)
            previews.starte_render(self.material_name)   # Vorschaubild im Hintergrund
            self.report({'INFO'}, f"{self.material_name} updated in CSV with values from {blender_material.name}.")
        else:
            self.report({'ERROR'}, "No object with active material selected.")
        return {'FINISHED'}


class MATERIAL_OT_reload_csv(bpy.types.Operator):
    bl_idname = "material.reload_csv"
    bl_label = "Reload CSV"

    def execute(self, context):
        csv_io.reload()
        previews.leere_cache()
        n = len(material_manager.material_data)
        if material_manager.zuletzt_migriert:
            self.report({'INFO'}, f"Old CSV (v1.4) detected and converted: {n} materials loaded. Backup: materials.csv.bak-v14")
        else:
            self.report({'INFO'}, f"CSV reloaded: {n} materials.")
        return {'FINISHED'}


class MATERIAL_OT_apply_color(bpy.types.Operator):
    bl_idname = "material.apply_color"
    bl_label = "Apply Color"
    bl_description = "Apply this Color to active Material"
    material_name: bpy.props.StringProperty()

    def execute(self, context):
        material = next((m for m in material_manager.material_data if m.name == self.material_name), None)
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}

        if not obj.active_material:
            self.report({'ERROR'}, "Active object does not have an active material.")
            return {'CANCELLED'}

        if not material:
            self.report({'ERROR'}, "Material not found in the material manager.")
            return {'CANCELLED'}

        mat = obj.active_material
        mat.use_nodes = True
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            if not set_base_color(bsdf, material.base_color):
                self.report({'WARNING'}, "Base Color is driven by a texture - color set on BSDF only.")
        else:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}
        return {'FINISHED'}


class MATERIAL_OT_apply_material(bpy.types.Operator):
    bl_idname = "material.apply_material"
    bl_label = "Apply Material"
    bl_description = "Apply this to active Material"
    material_name: bpy.props.StringProperty()

    def execute(self, context):
        material = next((m for m in material_manager.material_data if m.name == self.material_name), None)
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}

        if not material:
            self.report({'ERROR'}, "Material not found in the material manager.")
            return {'CANCELLED'}

        if not obj.active_material:
            # Ein neues Material namens 'BaseMaterial' erstellen und zuweisen
            base_material = bpy.data.materials.get("BaseMaterial")
            if not base_material:
                base_material = bpy.data.materials.new(name="BaseMaterial")
            obj.active_material = base_material
            mat = base_material
        else:
            mat = obj.active_material

        mat.use_nodes = True
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if not bsdf:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}

        # Alle Felder aus der zentralen FIELDS-Definition uebertragen.
        for field in FIELDS:
            if field.bsdf_input is None:
                continue
            value = getattr(material, field.attr)
            socket = bsdf.inputs[field.bsdf_input]
            # Base Color kann belegt sein (AO-Multiply) -> Farbe im RGB-Node setzen.
            if field.attr == "base_color":
                if not set_base_color(bsdf, value):
                    self.report({'WARNING'}, "Base Color is driven by a texture - color set on BSDF only.")
                continue
            socket.default_value = value
            # Wenn Roughness ueber den 'AddRoughness'-Node laeuft, dort einschreiben.
            if field.attr == "roughness" and socket.is_linked:
                from_node = socket.links[0].from_node
                if from_node.type == 'MATH' and from_node.label == 'AddRoughness':
                    from_node.inputs[1].default_value = value
        return {'FINISHED'}


class MATERIAL_OT_unhide_render(bpy.types.Operator):
    bl_idname = "material.unhide_render"
    bl_label = "Unhide Render"
    bl_description = "Unhide objects that are hidden in viewport but visible in render, and select them"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        num_affected = 0
        for obj in bpy.data.objects:
            # Pruefen, ob das Objekt im Viewport versteckt ist (global oder in der Ansichtsebene)
            is_hidden_in_viewport = obj.hide_viewport or obj.hide_get()
            if is_hidden_in_viewport and not obj.hide_render:
                obj.hide_viewport = False
                obj.hide_set(False)
                obj.select_set(True)
                num_affected += 1
        self.report({'INFO'}, f"{num_affected} objects unhidden and selected.")
        return {'FINISHED'}


class MATERIAL_OT_apply_roughness_texture(bpy.types.Operator):
    bl_idname = "material.apply_roughness_texture"
    bl_label = "Add Imperfection"
    bl_description = "Apply Roughness Map to active Material"
    material_name: bpy.props.StringProperty()

    def execute(self, context):
        if not context.object:
            self.report({'ERROR'}, "No active object found.")
            return {'CANCELLED'}
        if not context.object.active_material:
            self.report({'ERROR'}, "Active object has no material.")
            return {'CANCELLED'}
        mat = context.object.active_material
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        bsdf = nodes.get("Principled BSDF")
        if not bsdf:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}
        if bsdf.inputs['Roughness'].is_linked:
            self.report({'INFO'}, "Roughness input is already linked. Skipping.")
            return {'FINISHED'}

        # Erstellen der notwendigen Nodes
        tex_image = nodes.new('ShaderNodeTexImage')
        tex_image.location = (TEXTURE_NODE_LOCATION[0] + HORIZONTAL_OFFSET, TEXTURE_NODE_LOCATION[1] + VERTICAL_OFFSET)
        tex_image.projection = 'BOX'
        tex_image.projection_blend = 0.5

        tex_coord = nodes.new('ShaderNodeTexCoord')
        tex_coord.location = (TEX_COORD_LOCATION[0] + HORIZONTAL_OFFSET + NODE_HORIZONTAL_SPACING, TEX_COORD_LOCATION[1] + VERTICAL_OFFSET)

        mapping = nodes.new('ShaderNodeMapping')
        mapping.location = (MAPPING_LOCATION[0] + HORIZONTAL_OFFSET + 2 * NODE_HORIZONTAL_SPACING, MAPPING_LOCATION[1] + VERTICAL_OFFSET)
        mapping.inputs['Scale'].default_value = SCALE_VALUE

        map_range = nodes.new('ShaderNodeMapRange')
        map_range.location = (MAP_RANGE_LOCATION[0] + HORIZONTAL_OFFSET + 3 * NODE_HORIZONTAL_SPACING, MAP_RANGE_LOCATION[1] + VERTICAL_OFFSET)
        map_range.inputs['To Min'].default_value = TO_MIN_VALUE
        map_range.inputs['To Max'].default_value = TO_MAX_VALUE

        math_node = nodes.new('ShaderNodeMath')
        math_node.location = (MATH_NODE_LOCATION[0] + HORIZONTAL_OFFSET + 4 * NODE_HORIZONTAL_SPACING, MATH_NODE_LOCATION[1] + VERTICAL_OFFSET)
        math_node.label = 'AddRoughness'
        math_node.operation = 'ADD'
        math_node.inputs[1].default_value = bsdf.inputs['Roughness'].default_value

        # Verbindungen herstellen
        links.new(tex_coord.outputs['Object'], mapping.inputs['Vector'])
        links.new(mapping.outputs['Vector'], tex_image.inputs['Vector'])
        links.new(tex_image.outputs['Color'], map_range.inputs['Value'])
        links.new(map_range.outputs['Result'], math_node.inputs[0])
        links.new(math_node.outputs['Value'], bsdf.inputs['Roughness'])

        # Laden der Textur
        addon_dir = os.path.dirname(os.path.realpath(__file__))
        roughness_texture_path = os.path.join(addon_dir, 'Roughness', 'Roughness1.png')
        if os.path.exists(roughness_texture_path):
            tex_image.image = bpy.data.images.load(roughness_texture_path)
            tex_image.image.colorspace_settings.name = 'Non-Color'
        else:
            self.report({'WARNING'}, f"Roughness texture not found at {roughness_texture_path}.")

        self.report({'INFO'}, "Roughness texture applied successfully.")
        return {'FINISHED'}


class MATERIAL_OT_apply_random_brightness(bpy.types.Operator):
    bl_idname = "material.apply_random_brightness"
    bl_label = "Random Brightness"
    bl_description = "Apply Random Brightness to active Material"

    def execute(self, context):
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}
        mat = obj.active_material
        if not mat:
            self.report({'ERROR'}, "Active object does not have an active material.")
            return {'CANCELLED'}
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        bsdf = nodes.get("Principled BSDF")
        if not bsdf:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}
        if not bsdf.inputs['Base Color'].is_linked:
            self.report({'WARNING'}, "Color input of BSDF is not connected. Skipping.")
            return {'CANCELLED'}

        # Erstellen der notwendigen Nodes
        object_info_node = next((node for node in nodes if node.type == 'OBJECT_INFO' and not node.name.startswith("BM_")), None)
        if not object_info_node:
            object_info_node = nodes.new(type='ShaderNodeObjectInfo')
            object_info_node.name = "BM_Object Info"

        map_range_node = nodes.new(type='ShaderNodeMapRange')
        map_range_node.name = "BM_Map Range"
        map_range_node.inputs['To Min'].default_value = 0.8
        map_range_node.inputs['To Max'].default_value = 1.0

        hsv_node = nodes.new(type='ShaderNodeHueSaturation')
        hsv_node.name = "BM_Hue Saturation Value"

        # Entfernen der bestehenden Verbindung
        color_input_link = bsdf.inputs['Base Color'].links[0]
        from_socket = color_input_link.from_socket
        links.remove(color_input_link)

        # Neue Verbindungen herstellen
        links.new(object_info_node.outputs['Random'], map_range_node.inputs['Value'])
        links.new(map_range_node.outputs['Result'], hsv_node.inputs['Value'])
        links.new(from_socket, hsv_node.inputs['Color'])
        links.new(hsv_node.outputs['Color'], bsdf.inputs['Base Color'])

        # Positionieren der Nodes
        object_info_node.location.x = bsdf.location.x - 600
        object_info_node.location.y = bsdf.location.y - 400
        map_range_node.location.x = bsdf.location.x - 400
        map_range_node.location.y = bsdf.location.y - 400
        hsv_node.location.x = bsdf.location.x - 200
        hsv_node.location.y = bsdf.location.y - 200

        self.report({'INFO'}, "Random brightness applied successfully.")
        return {'FINISHED'}


class MATERIAL_OT_kill_shared_geo(bpy.types.Operator):
    bl_idname = "material.kill_shared_geo"
    bl_label = "Kill Shared Geo"
    bl_description = "Set geometry of selected mesh objects to single user"

    def execute(self, context):
        selected_objects = context.selected_objects
        if not selected_objects:
            self.report({'ERROR'}, "No objects selected.")
            return {'CANCELLED'}
        for obj in selected_objects:
            if obj.type == 'MESH':
                obj.data = obj.data.copy()
        self.report({'INFO'}, "Shared geometry has been unlinked.")
        return {'FINISHED'}


class MATERIAL_OT_name_collection(bpy.types.Operator):
    bl_idname = "material.name_collection"
    bl_label = "Name like Collection"
    bl_description = "Prefix objects in the selected collection with the collection name"

    def execute(self, context):
        collection = context.view_layer.active_layer_collection.collection
        if not collection:
            self.report({'ERROR'}, "No collection selected.")
            return {'CANCELLED'}
        prefix = collection.name + "_"
        for obj in collection.objects:
            if not obj.name.startswith(prefix):
                obj.name = prefix + obj.name
        self.report({'INFO'}, f"Objects in collection '{collection.name}' have been prefixed with '{prefix}'.")
        return {'FINISHED'}


class MATERIAL_OT_apply_scratches_texture(bpy.types.Operator):
    bl_idname = "material.apply_scratches_texture"
    bl_label = "Add Scratches"
    bl_description = "Apply Scratches to active Material"
    material_name: bpy.props.StringProperty()

    def execute(self, context):
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}

        mat = obj.active_material
        if not mat:
            mat = bpy.data.materials.new(name="New Material")
            obj.data.materials.append(mat)

        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        bsdf = nodes.get("Principled BSDF")

        if not bsdf:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}
        if bsdf.inputs['Normal'].is_linked:
            self.report({'INFO'}, "Normal already in use.")
            return {'CANCELLED'}

        for node in nodes:
            if node.label == "Bump Scratches":
                self.report({'INFO'}, "Scratches already in place.")
                return {'CANCELLED'}

        base_x = bsdf.location.x
        base_y = bsdf.location.y - 650

        created_nodes = {}
        node_values = [
            ("ShaderNodeValue", "Value Scratches 2", -1800, -300),
            ("ShaderNodeValue", "Value Scratches", -1800, -100),
            ("ShaderNodeMath", "Math Add Scratches", -1600, -250),
            ("ShaderNodeTexNoise", "Noise Texture Scratches", -1400, 0),
            ("ShaderNodeTexNoise", "Noise Texture Scratches 2", -1400, -250),
            ("ShaderNodeValToRGB", "ColorRamp Scratches 3", -1200, -250),
            ("ShaderNodeValToRGB", "ColorRamp Scratches 2", -1200, 0),
            ("ShaderNodeMixRGB", "ColorMix Difference Scratches", -900, 0),
            ("ShaderNodeValToRGB", "ColorRamp Scratches", -700, 0),
            ("ShaderNodeMixRGB", "ColorMix Screen Scratches", -400, 0),
            ("ShaderNodeBump", "Bump Scratches", -200, 0),
        ]

        for node_type, label, x_offset, y_offset in node_values:
            node = nodes.new(type=node_type)
            node.location.x = base_x + x_offset
            node.location.y = base_y + y_offset
            node.label = label
            created_nodes[label] = node

        # Einstellungen der Nodes
        created_nodes["Value Scratches 2"].outputs[0].default_value = 28.0
        created_nodes["Value Scratches"].outputs[0].default_value = 7.0

        color_ramp_scratches_2 = created_nodes["ColorRamp Scratches 2"]
        color_ramp_scratches_2.color_ramp.elements[0].position = 0.484
        color_ramp_scratches_2.color_ramp.elements[1].position = 0.5

        color_ramp_scratches_3 = created_nodes["ColorRamp Scratches 3"]
        color_ramp_scratches_3.color_ramp.elements[0].position = 0.435
        color_ramp_scratches_3.color_ramp.elements[1].position = 0.468

        color_ramp_scratches_1 = created_nodes["ColorRamp Scratches"]
        color_ramp_scratches_1.color_ramp.elements[0].position = 0.033
        color_ramp_scratches_1.color_ramp.elements[1].position = 0.042

        color_mix_difference_scratches = created_nodes["ColorMix Difference Scratches"]
        color_mix_difference_scratches.inputs['Fac'].default_value = 1.0
        color_mix_difference_scratches.blend_type = 'DIFFERENCE'

        color_mix_screen_scratches = created_nodes["ColorMix Screen Scratches"]
        color_mix_screen_scratches.inputs['Fac'].default_value = 1.0
        color_mix_screen_scratches.blend_type = 'SCREEN'

        created_nodes["Bump Scratches"].inputs["Strength"].default_value = 0.1
        created_nodes["Bump Scratches"].inputs["Distance"].default_value = 0.001

        # Verbindungen herstellen
        links.new(created_nodes["Value Scratches"].outputs[0], created_nodes["Noise Texture Scratches"].inputs["Scale"])
        links.new(created_nodes["Value Scratches 2"].outputs[0], created_nodes["Math Add Scratches"].inputs[1])
        links.new(created_nodes["Value Scratches"].outputs[0], created_nodes["Math Add Scratches"].inputs[0])
        links.new(created_nodes["ColorRamp Scratches 3"].outputs["Color"],
                  created_nodes["ColorMix Screen Scratches"].inputs[2])
        links.new(created_nodes["Bump Scratches"].outputs["Normal"], bsdf.inputs["Normal"])
        links.new(created_nodes["ColorMix Screen Scratches"].outputs["Color"], created_nodes["Bump Scratches"].inputs["Height"])
        links.new(created_nodes["ColorRamp Scratches"].outputs["Color"], created_nodes["ColorMix Screen Scratches"].inputs[1])
        links.new(created_nodes["ColorMix Difference Scratches"].outputs["Color"], created_nodes["ColorRamp Scratches"].inputs["Fac"])
        links.new(created_nodes["ColorRamp Scratches 2"].outputs["Color"], created_nodes["ColorMix Difference Scratches"].inputs[1])
        links.new(created_nodes["Noise Texture Scratches"].outputs["Fac"], created_nodes["ColorRamp Scratches 2"].inputs["Fac"])
        links.new(created_nodes["Noise Texture Scratches 2"].outputs["Fac"], created_nodes["ColorRamp Scratches 3"].inputs["Fac"])
        links.new(created_nodes["Math Add Scratches"].outputs["Value"], created_nodes["Noise Texture Scratches 2"].inputs["Scale"])

        self.report({'INFO'}, "Scratches texture applied successfully.")
        return {'FINISHED'}


class MATERIAL_OT_add_material_to_csv(bpy.types.Operator):
    bl_idname = "material.add_material_to_csv"
    bl_label = "Add Material to CSV"
    bl_description = "Add current Material to CSV"
    material_name: bpy.props.StringProperty(name="Material Name")
    new_category: bpy.props.StringProperty(name="New Category", description="Create a new category if needed")

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

    def execute(self, context):
        obj = context.object
        if obj is None or not obj.data.materials:
            self.report({'WARNING'}, "No object selected or object has no materials!")
            return {'CANCELLED'}
        mat = obj.active_material
        if mat is None:
            self.report({'WARNING'}, "No active material on selected object!")
            return {'CANCELLED'}
        if self.new_category:
            category = self.new_category
        else:
            category = context.scene.material_category
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if not bsdf:
            self.report({'ERROR'}, "Principled BSDF node not found in material.")
            return {'CANCELLED'}
        path = csv_io.append_material(bsdf, category, self.material_name)
        previews.starte_render(self.material_name)   # Vorschaubild im Hintergrund
        self.report({'INFO'}, f"Added material {mat.name} to {path}")
        return {'FINISHED'}


class MATERIAL_OT_delete_material(bpy.types.Operator):
    bl_idname = "material.delete_material"
    bl_label = "Delete Material"
    bl_description = "Delete this Material from CSV"
    material_name: bpy.props.StringProperty()
    category: bpy.props.StringProperty()

    def invoke(self, context, event):
        self.category = context.scene.material_category
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):
        csv_io.delete_material(self.material_name, self.category)
        self.report({'INFO'}, f"Material {self.material_name} from category {self.category} deleted.")
        return {'FINISHED'}


class MATERIAL_OT_apply_random_uv(bpy.types.Operator):
    bl_idname = "material.apply_random_uv"
    bl_label = "Random UV"
    bl_description = "Apply Random UV to active Material"

    def execute(self, context):
        obj = context.object
        if not obj:
            self.report({'ERROR'}, "No object selected.")
            return {'CANCELLED'}
        mat = obj.active_material
        if not mat:
            self.report({'ERROR'}, "Active object does not have an active material.")
            return {'CANCELLED'}
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        texture_nodes = [node for node in nodes if node.type in ['TEX_IMAGE', 'TEX_NOISE', 'TEX_VORONOI', 'TEX_MUSGRAVE'] and not node.name.startswith("BM_")]
        if not texture_nodes:
            self.report({'WARNING'}, "No textures found in the material.")
            return {'CANCELLED'}
        tex_coord_node = next((node for node in nodes if node.type == 'TEX_COORD' and not node.name.startswith("BM_")), None)
        object_info_node = next((node for node in nodes if node.type == 'OBJECT_INFO' and not node.name.startswith("BM_")), None)
        for texture_node in texture_nodes:
            connected_tex_coord_node = None
            for link in texture_node.inputs[0].links:
                if link.from_node.type == 'TEX_COORD':
                    connected_tex_coord_node = link.from_node
                    break
            if not connected_tex_coord_node:
                if not tex_coord_node:
                    tex_coord_node = nodes.new(type='ShaderNodeTexCoord')
                    tex_coord_node.name = "BM_Texture Coordinate"
                connected_tex_coord_node = tex_coord_node
            existing_mapping_node = None
            for link in texture_node.inputs[0].links:
                if link.from_node.type == 'MAPPING':
                    existing_mapping_node = link.from_node
                    break
            mapping_node = nodes.new(type='ShaderNodeMapping')
            mapping_node.name = "BM_Mapping"
            if existing_mapping_node:
                for link in texture_node.inputs[0].links:
                    if link.from_node == existing_mapping_node:
                        links.remove(link)
                links.new(existing_mapping_node.inputs[0].links[0].from_socket, mapping_node.inputs['Vector'])
                links.new(mapping_node.outputs['Vector'], existing_mapping_node.inputs['Vector'])
                links.new(existing_mapping_node.outputs['Vector'], texture_node.inputs[0])
            else:
                for link in texture_node.inputs[0].links:
                    if link.from_node == connected_tex_coord_node:
                        links.remove(link)
                if texture_node.type == 'TEX_IMAGE' and texture_node.projection != 'BOX':
                    links.new(connected_tex_coord_node.outputs['UV'], mapping_node.inputs['Vector'])
                else:
                    links.new(connected_tex_coord_node.outputs['Object'], mapping_node.inputs['Vector'])
                links.new(mapping_node.outputs['Vector'], texture_node.inputs[0])
            if not object_info_node:
                object_info_node = nodes.new(type='ShaderNodeObjectInfo')
                object_info_node.name = "BM_Object Info"
            math_node = nodes.new(type='ShaderNodeMath')
            math_node.name = "BM_Math"
            math_node.operation = 'MULTIPLY'
            math_node.inputs[1].default_value = 100
            links.new(object_info_node.outputs['Random'], math_node.inputs[0])
            links.new(math_node.outputs['Value'], mapping_node.inputs['Location'])
            connected_tex_coord_node.location.x = texture_node.location.x - 400
            mapping_node.location.x = texture_node.location.x - 200
            object_info_node.location.x = texture_node.location.x - 600
            object_info_node.location.y = texture_node.location.y - 200
            math_node.location.x = texture_node.location.x - 400
            math_node.location.y = texture_node.location.y - 200

        self.report({'INFO'}, "Random UV applied successfully.")
        return {'FINISHED'}


# Reihenfolge spielt nur fuer eine konsistente Anzeige keine Rolle; alle werden registriert.
OPERATOR_CLASSES = (
    MATERIAL_OT_shared_geo,
    MATERIAL_OT_UpdateMaterial,
    MATERIAL_OT_reload_csv,
    MATERIAL_OT_apply_color,
    MATERIAL_OT_apply_material,
    MATERIAL_OT_unhide_render,
    MATERIAL_OT_apply_roughness_texture,
    MATERIAL_OT_apply_random_brightness,
    MATERIAL_OT_kill_shared_geo,
    MATERIAL_OT_name_collection,
    MATERIAL_OT_apply_scratches_texture,
    MATERIAL_OT_add_material_to_csv,
    MATERIAL_OT_delete_material,
    MATERIAL_OT_apply_random_uv,
)
