# Copyright (C) 2022 Thomas Hoppe (h0bB1T). All rights reserved.
#
# Unauthorized copying of this file via any medium is strictly prohibited.
# Proprietary and confidential.

import bpy

from typing import Any, List, Set, Tuple, Union

from bpy.types import ShaderNodeTree, Node, NodeSocket, NodeSocketVector, NodeSocketFloat

from .node_builder import NodeBuilder
from .blender import is_400_or_gt, is_ls_410

NodeSourceFloat = Union[NodeSocket, float]

class NodeBuilderCycles(NodeBuilder):
    """
    Utility class to build Cycles/Eevee node trees.
    """
    def build_node_tree(self, name: str):
        """
        Create a new node tree with the given name.
        """
        self.tree = bpy.data.node_groups.new(name, 'ShaderNodeTree')
        self.input_group = self.add('NodeGroupInput') # type: bpy.types.NodeGroupInput
        self.output_group = self.add('NodeGroupOutput')
        self.output_group.location = (1000, 0)


    @staticmethod
    def edit_tree_by_name(name: str, clear_all_except_io: bool) -> Union['NodeBuilderCycles', None]:
        if name and name in bpy.data.node_groups:
            nb = NodeBuilderCycles()
            nb.edit(bpy.data.node_groups[name])
            if clear_all_except_io:
                nb.clear_nodes([ 'NodeGroupInput', 'NodeGroupOutput' ])
            return nb


    def edit(self, tree: ShaderNodeTree):
        self.tree_from_group(tree, create_io=False, clear_all=False)


    def tree_from_group(self, tree: bpy.types.ShaderNodeTree, create_io=True, clear_all=False):
        """
        Create a new node tree with the given name.
        """
        self.tree = tree
        if create_io:
            if clear_all:
                self.clear_nodes()
                
            self.input_group = self.add('NodeGroupInput') # type: bpy.types.NodeGroupInput
            self.output_group = self.add('NodeGroupOutput')
        else:
            self.input_group = self.find_node_by_idname('NodeGroupInput')
            self.output_group = self.find_node_by_idname('NodeGroupOutput')
        self.output_group.location = (1000, 0)        


    def tree_from_material(self, material: bpy.types.Material):
        """
        Access tree from material.
        """
        self.input_group, self.outputs_group = None, None
        self.tree = material.node_tree


    def find_node_by_idname(self, idname: str) -> Union[Node, None]:
        """
        ...
        """
        for n in self.tree.nodes:
            if n.bl_idname == idname:
                return n


    def find_nodes_by_idname(self, idname: str) -> List[Node]:
        """
        Find all nodes in this tree with given type.
        """
        r = []
        for n in self.tree.nodes:
            if n.bl_idname == idname:
                r.append(n)
        return r


    def find_node_by_label(self, label: str) -> Union[Node, None]:
        """
        Find first node with given label in tree.
        """
        for n in self.tree.nodes:
            if n.label == label:
                return n


    def add_inputs(self, inputs: List[Tuple[str, str, any]], after: List[str] = list(), hidden: Set[str] = set()):
        """
        All in one input add if tree is a shader group.
        """
        # Find highest index of entries in 'after'.
        index = -1
        if after:
            for a in after:
                i = self.tree_input_index(a)
                if i >= 0:
                    index = i + 1
                    break

        # Create the inputs.
        for i in inputs:
            if not self.tree_has_input(i[1]):
                n = self.tree_add_input(i[1], i[0])
                if index >= 0:
                    self.tree_move_input(n, index)
                    index += 1

                n = self.tree_get_input(i[1])
                if len(i) > 2: n.default_value = i[2]
                if len(i) > 3: n.min_value = i[3]
                if len(i) > 4: n.max_value = i[4]

                if i[1] in hidden:
                    n.hide_value = True


    def remove_inputs(self, inputs: List[str]):
        """
        Eventually remove inputs.
        """
        for i in inputs:
            self.tree_remove_input(i)


    def show_inputs(self, inputs: List[str], enable: bool):
        """
        Show or hide inputs.
        """
        for i in inputs:
            socket = self.tree_get_input(i)
            if socket:
                socket.hide_value = not enable


    def add_inputs_if_not(self, inputs: List[Tuple[str, str, any]]):
        """
        All in one input add if tree is a shader group (only missing)
        """
        to_add = []
        for i in inputs:
            if not self.tree_has_input(i[1]):
                to_add.append(i)

        self.add_inputs(to_add)


    def input(self, name_or_id: Any) -> bpy.types.NodeSocket:
        return self.input_group.outputs[name_or_id]


    def add_outputs(self, outputs: List[Tuple[str, str]], after: List[str] = list()):
        """
        All in one output add if tree is a shader group.
        """
        index = -1
        if after:
            for a in after:
                i = self.tree_output_index(a)
                if i >= 0:
                    index = i + 1
                    break

        for o in outputs:
            n = self.tree_add_output(o[1], o[0])
            if index >= 0:
                self.tree_move_output(n, index)
                index += 1


    def add_outputs_if_not(self, outputs: List[Tuple[str, str]]):
        """
        All in one output add if tree is a shader group (only missing)
        """
        to_add = []
        for o in outputs:
            if not self.tree_has_output(o[1]):
                to_add.append(o)

        self.add_outputs(to_add)


    def remove_outputs(self, outputs: List[str]):
        """
        Eventually remove inputs.
        """
        for o in outputs:
            self.tree_remove_output(o)


    def output(self, name_or_id: Any) -> bpy.types.NodeSocket:
        return self.output_group.inputs[name_or_id]


    def clear_nodes(self, preserve_by_idname: List[str] = []):
        """
        Remove all nodes in the tree.
        """
        to_remove = []
        for n in self.tree.nodes:
            if n.bl_idname not in preserve_by_idname:
                to_remove.append(n)
        
        for n in to_remove:
            self.tree.nodes.remove(n)


    def resolve_wire(self, node: bpy.types.Node, input: Union[int, str], source: any):
        """
        Generic wiring.
        """
        if source == None:
            return


        if isinstance(input, int) or (isinstance(input, str) and input in node.inputs):
            if isinstance(source, bpy.types.NodeSocket):
                self.tree.links.new(source, node.inputs[input])
            else:
                node.inputs[input].default_value = source


    def resolve_wires(self, node: bpy.types.Node, wires: List[Tuple[str, any]]):
        """
        Multi-wire version.
        """
        for input, source in wires:
            self.resolve_wire(node, input, source)


    def wire(
        self, 
        source: bpy.types.Node, 
        output: str, 
        dest: bpy.types.Node, 
        input: str
        ):
        """
        Manual wiring.
        """
        self.tree.links.new(source.outputs[output], dest.inputs[input])


    def wire_sockets(self, source, dest):
        self.tree.links.new(source, dest)


    def add_reroute(self) -> bpy.types.NodeReroute:
        """
        Create a reoute-point.
        """
        return self.add('NodeReroute')


    def add_value(self, value) -> bpy.types.ShaderNodeValue:
        """
        A float value.
        """
        n = self.add('ShaderNodeValue')
        n.outputs[0].default_value = value
        return n


    def add_color(self, value) -> bpy.types.ShaderNodeRGB:
        """
        A float value.
        """
        n = self.add('ShaderNodeRGB')
        n.outputs[0].default_value = value
        return n


    def add_group(
        self, 
        name: str,
        wires: List[Tuple[Any, Any]] = []
        ) -> bpy.types.ShaderNodeGroup:
        """
        Create instance of an existing group.
        """
        n = self.add('ShaderNodeGroup')
        idx = bpy.data.node_groups.find(name)
        if idx != -1:
            n.node_tree = bpy.data.node_groups[idx]
        self.resolve_wires(n, wires)
        return n
    

    def add_load_group(
        self,
        name: str,
        source_blend_file: str,
        wires: List[Tuple[Any, Any]] = []
        ) -> bpy.types.ShaderNodeGroup:
        """
        Create instance of an existing group. If the group
        does not exists, it is appended from the given .blend.
        """
        idx = bpy.data.node_groups.find(name)
        if idx < 0:
            # Group does not exist, append it.
            found = False
            with bpy.data.libraries.load(source_blend_file, link=False) as (data_from, data_to):
                if name in data_from.node_groups:
                    data_to.node_groups = [name, ]
                    found = True
            if not found:
                raise Exception(f'Can\'t find node {name} for import')
        return self.add_group(name, wires)


    def add_ambient_occlusion(
        self, 
        samples=16, 
        inside=False,
        only_local=False,
        color=None,
        distance=None,
        normal=None
        ) -> bpy.types.ShaderNodeAmbientOcclusion:
        n = self.add('ShaderNodeAmbientOcclusion')
        n.samples = samples
        n.inside = inside
        n.only_local = only_local
        self.resolve_wires(n, [
            ('Color', color),
            ('Distance', distance),
            ('Normal', normal)
        ])
        return n


    def add_bevel(self, samples=4, radius=0.1, normal=None) -> bpy.types.ShaderNodeBevel:
        n = self.add('ShaderNodeBevel')
        n.samples = samples
        self.resolve_wires(n, [
            ('Radius', radius),
            ('Normal', normal)
        ])
        return n


    def add_geometry(self) -> bpy.types.ShaderNodeNewGeometry:
        return self.add('ShaderNodeNewGeometry')


    def add_light_path(self) -> bpy.types.ShaderNodeLightPath:
        return self.add('ShaderNodeLightPath')


    def add_output_material(
        self,
        surface=None,
        volume=None,
        displacement=None
        ) -> bpy.types.ShaderNodeOutputMaterial:
        n = self.add('ShaderNodeOutputMaterial')
        self.resolve_wires(n, [
            ('Surface', surface),
            ('Volume', volume),
            ('Displacement', displacement)
        ])
        return n


    def add_separate_xyz(self, vector=None) -> bpy.types.ShaderNodeSeparateXYZ:
        n = self.add('ShaderNodeSeparateXYZ')
        self.resolve_wire(n, 'Vector', vector)
        return n


    def add_combine_xyz(self, x=None, y=None, z=None) -> bpy.types.ShaderNodeCombineXYZ:
        n = self.add('ShaderNodeCombineXYZ')
        self.resolve_wires(n, [
            ('X', x),
            ('Y', y),
            ('Z', z),
        ])
        return n


    def add_separate_rgb(self, image=None) -> bpy.types.ShaderNodeSeparateRGB:
        n = self.add('ShaderNodeSeparateRGB')
        self.resolve_wire(n, 'Image', image)
        return n        


    def add_combine_rgb(self, r=None, g=None, b=None) -> bpy.types.ShaderNodeCombineRGB:
        n = self.add('ShaderNodeCombineRGB')
        self.resolve_wires(n, [
            ('R', r),
            ('G', g),
            ('B', b),
        ])
        return n


    def add_map_range(
        self,
        value=None,
        clamp=False,
        fmin=None,
        fmax=None,
        tmin=None,
        tmax=None
        ) -> bpy.types.ShaderNodeMapRange:
        n = self.add('ShaderNodeMapRange')
        n.clamp = clamp
        self.resolve_wires(n, [
            (0, value),
            (1, fmin),
            (2, fmax),
            (3, tmin),
            (4, tmax),
        ])
        return n


    def add_curve(self, color=None, factor=None) -> bpy.types.ShaderNodeRGBCurve:
        n = self.add('ShaderNodeRGBCurve')
        self.resolve_wires(n, [
            ('Fac', factor),
            ('Color', color)
        ])
        return n


    def add_color_ramp(
        self, 
        factor=None, 
        interpolation='LINEAR',
        values=None # List of (index, (color)), first must be 0.0
        ) -> bpy.types.ShaderNodeValToRGB:
        n = self.add('ShaderNodeValToRGB') # type: bpy.types.ShaderNodeValToRGB
        cr = n.color_ramp
        cr.interpolation = interpolation
        self.resolve_wire(n, 'Fac', factor)
        if values:
            # Remove all but one (can't be removed).
            while (len(cr.elements) > 1):
                cr.elements.remove(cr.elements[1])
            first = True
            for idx, col in values:
                e = cr.elements[0] if first else cr.elements.new(idx)
                first = False
                e.color = col
        n.update()
        return n


    def add_math(
        self,
        op,
        clamp=False,
        value0=None,
        value1=None,
        threshold=None,
        base=None,
        exponent=None,
        distance=None,
        addend=None,
        increment=None,
        label: str = None
        ) -> bpy.types.ShaderNodeMath:
        """
        op = ADD | SUBTRACT | MULTIPLY | DIVIDE | POWER | MINIMUM | MAXIMUM | GREATER_THAN | LESS_THAN | ...
        """
        n = self.add('ShaderNodeMath')
        n.operation = op
        n.use_clamp = clamp
        self.resolve_wires(n, [
            (0, value0),
            (0, base),
            (1, value1),
            (1, threshold),
            (1, exponent),
            (1, increment),
            (2, distance),
            (2, addend),
        ])
        if label: n.label = label
        return n

    def add_math_add(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('ADD', clamp=clamp, value0=val0, value1=val1, label=label).outputs[0]          

    def add_math_sub(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('SUBTRACT', clamp=clamp, value0=val0, value1=val1, label=label).outputs[0]          

    def add_math_mul(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('MULTIPLY', clamp=clamp, value0=val0, value1=val1, label=label).outputs[0]    

    def add_math_mul_add(self, val0, val1, val2, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('MULTIPLY_ADD', clamp=clamp, value0=val0, value1=val1, addend=val2, label=label).outputs[0]               

    def add_math_div(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('DIVIDE', clamp=clamp, value0=val0, value1=val1, label=label).outputs[0]          

    def add_math_minimum(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_math('MINIMUM', value0=val0, value1=val1, label=label).outputs[0]        

    def add_math_maximum(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_math('MAXIMUM', value0=val0, value1=val1, label=label).outputs[0]

    def add_math_greater_than(self, val, threshold, label: str = None) -> NodeSocket:
        return self.add_math('GREATER_THAN', value0=val, threshold=threshold, label=label).outputs[0]        

    def add_math_less_than(self, val, threshold, label: str = None) -> NodeSocket:
        return self.add_math('LESS_THAN', value0=val, threshold=threshold, label=label).outputs[0]        

    def add_math_power(self, val, exp, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('POWER', clamp=clamp, value0=val, exponent=exp, label=label).outputs[0]   

    def add_math_ping_pong(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('PINGPONG', clamp=clamp, value0=val0, value1=val1, label=label).outputs[0]      

    def add_math_snap(self, val0, val1, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('SNAP', clamp=clamp, value0=val0, increment=val1, label=label).outputs[0]      

    def add_math_abs(self, val, label: str = None) -> NodeSocket:
        return self.add_math('ABSOLUTE', value0=val, label=label).outputs[0]               

    def add_math_fraction(self, val, label: str = None) -> NodeSocket:
        return self.add_math('FRACT', value0=val, label=label).outputs[0]               

    def add_inv_float_01(self, value, clamp=False, label: str = None) -> NodeSocket:
        return self.add_math('SUBTRACT', clamp=clamp, value0=1.0, value1=value, label=label).outputs[0]

    def add_math_inv(self, value, clamp=False, label: str = None) -> NodeSocket:
        """
        1 - x
        """
        return self.add_math('SUBTRACT', clamp=clamp, value0=1.0, value1=value, label=label).outputs[0]        

    def add_math_reciprocal(self, value, clamp=False, label: str = None) -> NodeSocket:
        """
        1 / x
        """
        return self.add_math('DIVIDE', clamp=clamp, value0=1.0, value1=value, label=label).outputs[0]        


    def add_vector_math(
        self,
        op,
        value0=None,
        value1=None,
        scale=None,
        label: str = None
        ) -> bpy.types.ShaderNodeVectorMath:
        """
        op = ADD | SUBTRACT | MULTIPLY | DIVIDE | CROSS_PRODUCT | DOT_PRODUCT | ...
        """
        n = self.add('ShaderNodeVectorMath')
        n.operation = op
        self.resolve_wires(n, [
            (0, value0),
            (1, value1),
            ('Scale', scale)
        ])
        if label: n.label = label
        return n    

    def add_vector_math_add(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('ADD', value0=val0, value1=val1, label=label).outputs[0]

    def add_vector_math_sub(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('SUBTRACT', value0=val0, value1=val1, label=label).outputs[0]

    def add_vector_math_mul(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('MULTIPLY', value0=val0, value1=val1, label=label).outputs[0]

    def add_vector_math_div(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('DIVIDE', value0=val0, value1=val1, label=label).outputs[0]

    def add_vector_math_length(self, val, label: str = None) -> NodeSocket:
        return self.add_vector_math('LENGTH', value0=val, label=label).outputs[1]        

    def add_vector_math_dot(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('DOT_PRODUCT', value0=val0, value1=val1, label=label).outputs[1]

    def add_vector_math_cross(self, val0, val1, label: str = None) -> NodeSocket:
        return self.add_vector_math('CROSS_PRODUCT', value0=val0, value1=val1, label=label).outputs[0]   

    def add_vector_math_scale(self, val0, scl, label: str = None) -> NodeSocket:
        return self.add_vector_math('SCALE', value0=val0, scale=scl, label=label).outputs[0]             

    def add_vector_math_floor(self, val, label: str = None) -> NodeSocket:
        return self.add_vector_math('FLOOR', value0=val, label=label).outputs[0]      

    def add_vector_math_fraction(self, val, label: str = None) -> NodeSocket:
        return self.add_vector_math('FRACTION', value0=val, label=label).outputs[0]   

    def add_vector_math_abs(self, val, label: str = None) -> NodeSocket:
        return self.add_vector_math('ABSOLUTE', value0=val, label=label).outputs[0]      

    def add_vector_math_normalize(self, val, label: str = None) -> NodeSocket:
        return self.add_vector_math('NORMALIZE', value0=val, label=label).outputs[0]


    def add_mapping(
        self,
        vector: Tuple,
        type: str = 'POINT', # POINT, TEXTURE, VECTOR, NORMAL
        location: Tuple = (0, 0, 0),
        rotation: Tuple = (0, 0, 0),
        scale: Tuple = (1, 1, 1),
        label: str = None
        ) -> bpy.types.ShaderNodeMapping:
        n = self.add('ShaderNodeMapping') # type: bpy.types.ShaderNodeMapping
        n.vector_type = type
        self.resolve_wires(n, [
            ('Vector', vector),
            ('Location', location),
            ('Rotation', rotation),
            ('Scale', scale),
        ])
        if label: n.label = label
        return n


    def add_vector_rotate(
        self,
        op,
        vector=None,
        center=None,
        axis=None,
        angle=None
        ) -> bpy.types.ShaderNodeVectorRotate:
        n = self.add('ShaderNodeVectorRotate') # type: bpy.types.ShaderNodeVectorRotate
        n.rotation_type = op
        self.resolve_wires(n, [
            ('Vector', vector),
            ('Center', center),
            ('Axis', axis),
            ('Angle', angle),
        ])
        return n
    

    def add_vector_transform(
        self,
        type: str, # 'POINT', 'VECTOR', 'NORMAL'
        c_from: str, # 'WORLD, 'OBJECT, 'CAMERA'
        c_to: str, # 'WORLD, 'OBJECT, 'CAMERA'
        vector=None
        ) -> bpy.types.ShaderNodeVectorTransform:
        n = self.add('ShaderNodeVectorTransform') # type: bpy.types.ShaderNodeVectorTransform
        n.vector_type = type
        n.convert_from = c_from
        n.convert_to = c_to
        self.resolve_wires(n, [
            ('Vector', vector),
        ])
        return n


    def add_uv_mapping(
        self,
        vector=None,
        location=None,
        rotation=None,
        scale=None,
        ) -> bpy.types.ShaderNodeMapping:
        n = self.add('ShaderNodeMapping')
        self.resolve_wires(n, [
            (0, vector),
            (1, location),
            (2, rotation),
            (3, scale),
        ])
        return n


    def add_vector_displacement(self, vector=None) -> bpy.types.ShaderNodeVectorDisplacement:
        n = self.add('ShaderNodeVectorDisplacement')
        self.resolve_wires(n, [
            ('Vector', vector)
        ])
        return n


    def add_displacement(
        self, 
        height=None,
        midlevel=None,
        scale=None
        ) -> bpy.types.ShaderNodeDisplacement:
        n = self.add('ShaderNodeDisplacement')
        self.resolve_wires(n, [
            ('Height', height),
            ('Midlevel', midlevel),
            ('Scale', scale),
        ])
        return n


    def add_uv_map(self, uv_map) -> bpy.types.ShaderNodeUVMap:
        n = self.add('ShaderNodeUVMap')
        n.uv_map = uv_map
        return n


    def add_normal_map(self, strength=None, color=None) -> bpy.types.ShaderNodeNormalMap:
        n = self.add('ShaderNodeNormalMap')
        self.resolve_wires(n, [
            ('Strength', strength),
            ('Color', color)
        ])
        return n


    def add_bump_map(self, height=None, strength=None, distance=None, normal=None) -> bpy.types.ShaderNodeBump:
        n = self.add('ShaderNodeBump')
        self.resolve_wires(n, [
            ('Height', height),
            ('Strength', strength),
            ('Distance', distance),
            ('Normal', normal)
        ])
        return n


    def add_tangent(self, direction='UV_MAP') -> bpy.types.ShaderNodeTangent:
        n = self.add('ShaderNodeTangent')
        n.direction_type=direction
        return n


    def add_texcoord(self) -> bpy.types.ShaderNodeTexCoord:
        return self.add('ShaderNodeTexCoord')    


    def add_object_info(self) -> bpy.types.ShaderNodeObjectInfo:
        return self.add('ShaderNodeObjectInfo')


    def add_mix_color(self, operation=None, clamp=False, fac=None, color0=None, color1=None) -> bpy.types.ShaderNodeMixRGB:
        n = self.add('ShaderNodeMixRGB')
        if operation:
            n.blend_type = operation
        n.use_clamp = clamp
        self.resolve_wires(n, [
            ('Fac', fac),
            (1, color0),
            (2, color1)
        ])
        return n


    def add_color_invert(self, input) -> bpy.types.ShaderNodeInvert:
        n = self.add('ShaderNodeInvert')
        self.resolve_wire(n, 'Color', input)
        return n


    def add_gamma(
        self, 
        color=None,
        gamma=None,
        ) -> bpy.types.ShaderNodeGamma:
        n = self.add('ShaderNodeGamma')
        self.resolve_wires(n, [
            ('Color', color),
            ('Gamma', gamma),
        ])
        return n


    def add_image(
        self,
        image,
        interpolation='Linear',
        projection='FLAT', # FLAT, BOX, SPHERE, TUBE
        colorspace=None, #'sRGB', # Non-Color, ..
        extension='REPEAT', # CLIP ..
        blend=0.5,
        vector=None
        ) -> bpy.types.ShaderNodeTexImage:
        n = self.add('ShaderNodeTexImage')
        if image != None:
            n.image = image
            if colorspace:
                n.image.colorspace_settings.name = colorspace
        n.interpolation = interpolation
        n.projection = projection
        n.extension = extension
        n.projection_blend = blend
        self.resolve_wire(n, 'Vector', vector)
        return n


    def update_projection_blend(self, blend: float):
        """
        Update blend factor in all tex-image nodes.
        """
        for n in self.find_nodes_by_idname('ShaderNodeTexImage'):
            n.projection_blend = blend


    def add_noise_texture(
        self,
        dimensions='3D', # 1D-4D
        vector=None,
        scale=5,
        detail=2,
        roughness=0.5,
        distortion=0.0,
        w=0,
        label: str = None
        ) -> bpy.types.ShaderNodeTexNoise:
        n = self.add('ShaderNodeTexNoise')
        n.noise_dimensions = dimensions
        self.resolve_wires(n, [
            ('Vector', vector),
            ('Scale', scale),
            ('Detail', detail),
            ('Roughness', roughness),
            ('Distortion', distortion),
            ('W', w),
        ])
        if label: n.label = label
        return n


    def add_voronoi_texture(
        self,
        dimensions='3D', # 1D-4D
        feature='F1',
        distance='EUCLIDEAN',
        vector=None,
        scale=5,
        randomness=1,
        label: str = None
        ) -> bpy.types.ShaderNodeTexVoronoi:
        n = self.add('ShaderNodeTexVoronoi')
        n.voronoi_dimensions = dimensions
        n.feature = feature
        n.distance = distance
        self.resolve_wires(n, [
            ('Vector', vector),
            ('Scale', scale),
            ('Randomness', randomness),
        ])
        if label: n.label = label
        return n


    if is_ls_410():
        def add_musgrave_texture(
            self,
            dimensions='3D', # 1D-4D
            type='FBM',
            vector=None,
            scale=5,
            detail=2,
            dimension=2,
            lacunarity=2,
            offset=0,
            gain=1,
            w=0
            ) -> bpy.types.ShaderNodeTexMusgrave:
            n = self.add('ShaderNodeTexMusgrave')
            n.musgrave_type = type
            n.musgrave_dimensions = dimensions
            self.resolve_wires(n, [
                ('Vector', vector),
                ('Scale', scale),
                ('Detail', detail),
                ('Dimension', dimension),
                ('Lacunarity', lacunarity),
                ('Offset', offset),
                ('Gain', gain),
                ('W', w)
            ])
            return n       


    def add_white_noise(
        self,
        dimensions='3D', # 1D-4D
        vector=None,
        w=0
        ) -> bpy.types.ShaderNodeTexWhiteNoise:
        n = self.add('ShaderNodeTexWhiteNoise')
        n.noise_dimensions = dimensions
        self.resolve_wires(n, [
            ('Vector', vector),
            ('W', w)
        ])
        return n           


    def add_principled_bsdf(
        self,
        basecolor=None,
        metallic=None,
        specular=None,
        roughness=None,
        normal=None,
        alpha=None,
        emission=None,
        subsurface=None,
        ) -> bpy.types.ShaderNodeBsdfPrincipled:
        n = self.add('ShaderNodeBsdfPrincipled')
        if is_400_or_gt():
            self.resolve_wires(n, [
                ('Base Color', basecolor),
                ('Metallic', metallic),
                ('Specular IOR Level', specular),
                ('Roughness', roughness),
                ('Normal', normal),
                ('Alpha', alpha),
                ('Emission Color', emission),
                ('Subsurface Weight', subsurface),
            ])
        else:
            self.resolve_wires(n, [
                ('Base Color', basecolor),
                ('Metallic', metallic),
                ('Specular', specular),
                ('Roughness', roughness),
                ('Normal', normal),
                ('Alpha', alpha),
                ('Emission', emission),
                ('Subsurface', subsurface),
            ])
        return n


    def add_emission(
        self, 
        color=None,
        strength=None
        ) -> bpy.types.ShaderNodeEmission:
        n = self.add('ShaderNodeEmission')
        self.resolve_wires(n, [
            ('Color', color),
            ('Strength', strength)
        ])
        return n


    def add_glass_bsdf(
        self,
        color=None,
        roughness=None,
        ior=None,
        normal=None
        ) -> bpy.types.ShaderNodeBsdfGlass:
        n = self.add('ShaderNodeBsdfGlass')
        self.resolve_wires(n, [
            ('Color', color),
            ('IOR', ior),
            ('Roughness', roughness),
            ('Normal', normal),
        ])
        return n


    def add_mix_shader(
        self,
        fac=None,
        shader0=None,
        shader1=None
        ) -> bpy.types.ShaderNodeMixShader:
        n = self.add('ShaderNodeMixShader')
        self.resolve_wires(n, [
            (0, fac),
            (1, shader0),
            (2, shader1)
        ])
        return n


    def add_add_shader(
        self,
        shader0=None,
        shader1=None
        ) -> bpy.types.ShaderNodeAddShader:
        n = self.add('ShaderNodeAddShader')
        self.resolve_wires(n, [
            (0, shader0),
            (1, shader1)
        ])
        return n        


    def add_translucent_bsdf(
        self,
        color=None,
        normal=None,
        ) -> bpy.types.ShaderNodeBsdfTranslucent:
        n = self.add('ShaderNodeBsdfTranslucent')
        self.resolve_wires(n, [
            ('Color', color),
            ('Normal', normal)
        ])
        return n 


    def add_transparent_bsdf(self) -> bpy.types.ShaderNodeBsdfTransparent:
        return self.add('ShaderNodeBsdfTransparent')


    def add_hsv(
        self, 
        hue=None, 
        saturation=None, 
        value=None, 
        factor=None, 
        color=None
        ) -> bpy.types.ShaderNodeHueSaturation:
        n = self.add('ShaderNodeHueSaturation')
        self.resolve_wires(n, [
            ('Hue', hue), 
            ('Saturation', saturation), 
            ('Value', value), 
            ('Fac', factor),
            ('Color', color)
        ])
        return n


    def add_brightness_contrast(
        self,
        brightness=None,
        contrast=None,
        color=None
        ) -> bpy.types.ShaderNodeBrightContrast:
        n = self.add('ShaderNodeBrightContrast') # type: bpy.types.ShaderNodeBrightContrast
        self.resolve_wires(n, [
            ('Bright', brightness), 
            ('Contrast', contrast), 
            ('Color', color),
        ])
        return n


    def __add_uv_scale_translate(
        self, 
        source: NodeSocketVector,
        scale: NodeSocketFloat,
        translate: Union[NodeSocketVector, None]
        ) -> bpy.types.NodeSocketVector:
        """
        Adds scaling and optionally translation to a UV vector, used in functions below.
        """
        out = self.add_vector_math_scale(source, scale)
        if translate:
            out = self.add_vector_math_add(out, translate)

        return out


    def add_auto_uv(
        self, 
        uv_in: NodeSocketVector,
        scale: NodeSocketFloat,
        translate: Union[NodeSocketVector, None] = None
        ) -> NodeSocketVector:
        """
        Tries to approach if uv_in is a valid system (e.g. is connected), otherwise use default UV.
        This is somewhat experimental. It just checks if len(uv_in) == 0, than default UV is used. This is
        also valid if uv_in is connected, but only at an infinite small point, which than has a wrong
        mapping, but this shouldn't be visible. .. it's experimental.
        """
        decision = self.add_math_less_than(
            val=self.add_vector_math_length(uv_in),
            threshold=0.000001
        )

        out = self.add_vector_math_add(
            self.add_vector_math_scale(
                val0=self.add_texcoord().outputs['UV'], 
                scl=decision
            ),
            self.add_vector_math_scale(
                val0=uv_in, 
                scl=self.add_math_sub(val0=1, val1=decision)
            )
        )    

        return self.__add_uv_scale_translate(out, scale, translate)    


    def add_local_box_uv_mapping(
        self,
        scale: NodeSocketVector,
        translate: Union[NodeSocket, None] = None
        ) -> bpy.types.NodeSocketVector:
        """
        Create local BOX UV Mapping, textures require Box mapping!
        """
        return self.__add_uv_scale_translate(
            self.add_texcoord().outputs['Object'],
            scale,
            translate
        )


    def add_generate_box_uv_mapping(
        self,
        scale: NodeSocketVector,
        translate: Union[NodeSocket, None] = None
        ) -> bpy.types.NodeSocketVector:
        """
        Create local BOX UV Mapping, textures require Box mapping!
        """
        return self.__add_uv_scale_translate(
            self.add_texcoord().outputs['Generated'],
            scale,
            translate
        )        


    def add_global_box_uv_mapping(
        self,
        scale: NodeSocketVector,
        translate: Union[NodeSocket, None] = None
        ) -> bpy.types.NodeSocketVector:
        """
        Create global BOX UV Mapping, textures require Box mapping!
        """
        return self.__add_uv_scale_translate(
            self.add_vector_math_add(
                self.add_texcoord().outputs['Object'],
                self.add_object_info().outputs['Location']
            ),
            scale,
            translate
        )


    def add_uv_anti_repeat(
        self,
        uv: NodeSocketVector,
        style: str = 'NOISE', # NOISE or VORONOI
        scale: float = 2.0,
        roughness: float = 0.1,
        steps: float = 0.1,
        seed: float = 0
        ) -> NodeSocketVector:
        """
        Create a Anti-Repeat setup.
        """
        if style == 'NOISE':
            noise = self.add_math_mul_add(
                self.add_noise_texture(
                    vector=uv,
                    scale=scale,
                    detail=8,
                    roughness=roughness * 0.3 + 0.5,
                    label='uv_ar_noise'
                ).outputs['Fac'],
                2.5,
                -0.75,
                clamp=True
            )

            snap = self.add_math_snap(
                self.add_math_mul(noise, steps + 0.1, label='uv_ar_steps'), 
                .1
            )

            wnoise = self.add_white_noise(
                dimensions='1D', 
                w=self.add_math_add(snap, seed, label='uv_ar_seed')
            ).outputs['Color']

            return self.add_vector_math_add(uv, wnoise)

        elif style == 'VORONOI':
            noise = self.add_vector_math_sub(
                self.add_noise_texture(
                    vector=uv,
                    scale=steps * 500,
                    detail=0,
                    roughness=0,
                    label='uv_ar_distortion'
                ).outputs['Color'],
                (0.5, 0.5, 0.5)
            )

            voronoi = self.add_voronoi_texture(
                vector=self.add_vector_math_add(
                    uv,
                    self.add_vector_math_scale(
                        noise,
                        roughness,
                        label='uv_ar_intensity'
                    )
                ),
                scale=scale,
                label='uv_ar_voronoi'
            ).outputs['Color']

            wnoise = self.add_white_noise(
                dimensions='1D', 
                w=self.add_math_add(voronoi, seed, label='uv_ar_seed')
            ).outputs['Color']

            return self.add_vector_math_add(uv, wnoise)


    def update_uv_anti_repeat_settings(
        self,
        scale: float,
        roughness: float,
        steps: float,
        seed: float
        ):
        """
        Update configuration values for the settings from add_uv_anti_repeat.
        """
        voronoi = self.find_node_by_label('uv_ar_voronoi')
        # Detect style based on presence of this node.
        if voronoi:
            distortion = self.find_node_by_label('uv_ar_distortion')
            intensity = self.find_node_by_label('uv_ar_intensity')
            s33d = self.find_node_by_label('uv_ar_seed')
            if distortion and intensity and s33d:
                voronoi.inputs['Scale'].default_value = scale
                intensity.inputs['Scale'].default_value = roughness
                distortion.inputs['Scale'].default_value = steps * 500
                s33d.inputs[1].default_value = seed
        else:
            noise = self.find_node_by_label('uv_ar_noise')
            snap = self.find_node_by_label('uv_ar_steps')
            s33d = self.find_node_by_label('uv_ar_seed')
            if noise and snap and s33d:
                noise.inputs['Scale'].default_value = scale
                noise.inputs['Roughness'].default_value = roughness * 0.3 + 0.5
                snap.inputs[1].default_value = steps + 0.1
                s33d.inputs[1].default_value = seed


    def add_normal_mixer(
        self, 
        nrm0: bpy.types.NodeSocketVector, 
        nrm1: bpy.types.NodeSocketVector, 
        weight: bpy.types.NodeSocketFloat,
        ):
        return self.add_vector_math_add(
            self.add_vector_math_scale(nrm0, weight),
            self.add_vector_math_scale(nrm1, self.add_math_inv(weight))
        )


    # https://blender.stackexchange.com/questions/190126/how-to-convert-object-space-normals-to-tangent-space
    def object_to_tangent_normal(self, bmp: NodeSourceFloat) -> NodeSocket:
        """
        Transforms an object space normal into a tangent space normal.
        """
        tangent = self.add_tangent().outputs['Tangent']
        normal = self.add_geometry().outputs['Normal']
        bitangent = self.add_vector_math_cross(tangent, normal)

        nrm_x = self.add_vector_math_dot(bmp, tangent)
        nrm_y = self.add_math_sub(0, self.add_vector_math_dot(bmp, bitangent))
        nrm_z = self.add_vector_math_dot(bmp, normal)

        # Transform back -1 .. 1 -> 0 .. 1
        return self.add_vector_math_add(
            self.add_vector_math_mul(
                self.add_combine_xyz(x=nrm_x, y=nrm_y, z=nrm_z).outputs[0],
                (0.5, 0.5, 0.5)
            ),
            (0.5, 0.5, 0.5)
        )


    def scale_normal(self, normal: NodeSourceFloat, factor: NodeSourceFloat) -> NodeSocket:
        """
        Scale normal by scaling X & Y.
        """
        return self.add_vector_math_add(
            self.add_vector_math_normalize(
                self.add_vector_math_mul(
                    self.add_vector_math_sub(normal, (0.5, 0.5, 0.5)),
                    self.add_combine_xyz(x=factor, y=factor, z=1.0).outputs[0]
                ),
            ),
            (0.5, 0.5, 0.5)
        )
