import os
import sys
import threading, queue
import subprocess
from datetime import datetime, timedelta
import bpy
from bpy.types import NodeTree, Node, NodeSocket, NodeSocketColor, NodeSocketFloat, NodeFrame, NodeReroute
try:
    from BakeWrangler.status_bar.status_bar_icon import ensure_bw_icon as update_status_bar
except:
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    from status_bar.status_bar_icon import ensure_bw_icon as update_status_bar

BW_TREE_VERSION = 9
BW_VERSION_STRING = '1.5.b11'


# Message formatter
def _print(str, node=None, ret=False, tag=False, wrap=True, enque=None, textdata="BakeWrangler"):
    output = "%s" % (str)
    endl = ''
    flsh = False

    if node:
        output = "[%s]: %s" % (node.get_name(), output)

    if tag:
        output.replace("<", "<_")
        output = "<%s>%s" % ("PBAKE", output)
        flsh = True
        if wrap:
            output = "%s</%s>" % (output, "PWRAP")
        else:
            output = "%s</%s>" % (output, "PBAKE")

    if wrap:
        endl = '\n'

    if enque != None:
        eout = "%s%s" % (output, endl)
        enque.put(eout)
        return None

    if ret:
        return output

    if textdata != None and _prefs('text_msgs'):
        if not textdata in bpy.data.texts.keys():
            bpy.data.texts.new(textdata)
        text = bpy.data.texts[textdata]
        end = len(text.lines[len(text.lines) - 1].body) - 1
        text.cursor_set(len(text.lines) - 1, character=end)
        tout = "%s%s" % (output, endl)
        text.write(tout)

    print(output, end=endl, flush=flsh)



# Preference reader
default_true  = ["text_msgs", "clear_msgs", "def_filter_mesh", "def_filter_curve", "def_filter_surface",
                 "def_filter_meta", "def_filter_font", "def_filter_light", "auto_open", "fact_start", "show_icon"]
default_false = ["def_filter_collection", "def_show_adv", "ignore_vis", "make_dirs", "wind_close",
                 "invert_bakemod", "wind_msgs", "save_packed", "save_images",]
default_res   = ["def_xres", "def_yres", "def_xout", "def_yout",]
default_zero  = ["def_margin", "def_mask_margin", "def_max_ray_dist", "retrys",]
def _prefs(key):
    try:
        name = __package__.split('.')
        prefs = bpy.context.preferences.addons[name[0]].preferences
    except:
        pref = False
    else:
        pref = True

    if pref and key in prefs:
        return prefs[key]
    else:
        # Default values to fall back on
        if key == 'debug':
            if pref:
                return False
            else:
                #return False
                return True
        elif key in default_true:
            return True
        elif key in default_false:
            return False
        elif key in default_res:
            return 1024
        elif key in default_zero:
            return 0
        elif key == 'def_device':
            return 0 # CPU
        elif key == 'def_samples':
            return 1
        elif key == 'def_format':
            return 2 # PNG
        elif key == 'def_raydist':
            return 0.01
        elif key == 'def_outpath':
            return ""
        elif key == 'def_outname':
            return "Image"
        elif key == 'img_non_color':
            return None
        else:
            return None



# Material validation recursor (takes a shader node and descends the tree via recursion)
def material_recursor(node, link=None, parent=None):
    # Accepted node types are OUTPUT_MATERIAL, BSDF_PRINCIPLED, MIX/ADD_SHADER and GROUP (plus REROUTE)
    if node.type == 'BSDF_PRINCIPLED':
        return True
    if node.type == 'OUTPUT_MATERIAL' and node.inputs['Surface'].is_linked:
        return material_recursor(node.inputs['Surface'].links[0].from_node, node.inputs['Surface'].links[0], parent)
    if node.type in ['MIX_SHADER', 'ADD_SHADER']:
        if node.type == 'MIX_SHADER':
            input1 = 1
            input2 = 2
        else:
            input1 = 0
            input2 = 1
        inputA = False
        if node.inputs[input1].is_linked:
            inputA = material_recursor(node.inputs[input1].links[0].from_node, node.inputs[input1].links[0], parent)
        inputB = False
        if node.inputs[input2].is_linked:
            inputB = material_recursor(node.inputs[input2].links[0].from_node, node.inputs[input2].links[0], parent)
        return inputA and inputB
    if node.type == 'REROUTE' and node.inputs[0].is_linked:
        return material_recursor(node.inputs[0].links[0].from_node, node.inputs[0].links[0], parent)
    if node.type == 'GROUP' and link:
        # Entering a group, requires similar steps to exiting, but will duplicate code for now
        if parent:
            # Parent modification is always performed on a copy due to branching of recursion
            gparent = parent.copy()
            gparent.append(node)
        else:
            gparent = [node]
        gout = None
        gsoc = 0
        # Find active group socket to begin from. Names may not be unique, so get index
        for soc in node.outputs:
            if link.from_socket == soc: break
            else: gsoc += 1
        for gnode in node.node_tree.nodes:
            if gnode.type == 'GROUP_OUTPUT' and gnode.is_active_output:
                gout = gnode
                break
        if gout and gout.inputs[gsoc].is_linked:
            return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent)
    if node.type == 'GROUP_INPUT' and link and parent:
        # Exiting a group, requires similar steps to entering, but will duplicate code for now
        # Parent modification is always performed on a copy due to branching of recursion
        gparent = parent.copy()
        gout = gparent.pop()
        gsoc = 0
        for soc in node.outputs:
            if link.from_socket == soc: break
            else: gsoc += 1
        if gout and gout.inputs[gsoc].is_linked:
            return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent)
    return False



# Return the node connected to an input, dealing with re-routes
def get_input(input):
    if not input.is_output and input.islinked() and input.valid:
        link = follow_input_link(input.links[0])
        return link.from_node
    return None



# Prune error messages to remove duplicates
def prune_messages(messages):
    unique = []
    for msg in messages:
        if not unique.count(msg):
            unique.append(msg)
    return unique



# Prune a list of objects to remove duplicate references
def prune_objects(objs, allow_dups=False):
    count = []
    dups = []
    # First remove duplicates
    for obj in objs:
        if objs.count(obj) > 1:
            objs.remove(obj)
    # Then remove non duplicate entries that reference the same object where appropriate
    for obj in objs:
        # Get a list of just the referenced objects to count them
        count.append(obj[0])
    for obj in count:
        # Create a list of objects with multiple refs and count how many
        if count.count(obj) > 1:
            found = False
            for dup in dups:
                if dup[0] == obj:
                    found = True
                    dup[1] += 1
                    break
            if not found:
                dups.append([obj, 1])
    for obj in dups:
        # Go over all the duplicate entries and prune appropriately
        num = obj[1]
        for dup in objs:
            if dup[0] == obj[0]:
                # For target set, remove only dups that came from a group (the user may
                # want the same object with different settings)
                if allow_dups:
                    if len(dup) == 1:
                        objs.remove(dup)
                        num -= 1
                # For other sets just reduce to one reference
                else:
                    objs.remove(dup)
                    num -= 1
                # Break out when/if one dup remains
                if num == 1:
                    break

    # Return pruned object list
    return objs



# Follow an input link through any reroutes
def follow_input_link(link):
    if link.from_node.type == 'REROUTE':
        if link.from_node.inputs[0].is_linked:
            try: # During link insertion this can have weird states
                return follow_input_link(link.from_node.inputs[0].links[0])
            except:
                pass
    return link



# Gather all links from an output, going past any reroutes
def gather_output_links(output):
    links = []
    for link in output.links:
        if link.is_valid:
            if link.to_node.type == 'REROUTE':
                if link.to_node.outputs[0].is_linked:
                    links += gather_output_links(link.to_node.outputs[0])
            else:
                links.append(link)
    return links
    
    
# Switch mode to object/back again - Returns the mode being switched from unless no swich is needed
def switch_mode(mode='OBJECT'):
    curr_mode = bpy.context.mode
    # Convert mode to mode set enum (why are these different?!)
    if curr_mode in ['OBJECT', 'SCULPT']:
        enum_mode = curr_mode
    elif curr_mode.startswith('PAINT_'):
        enum_mode = curr_mode[6:] + "_PAINT"
    else:
        enum_mode = 'EDIT'
    
    if enum_mode != mode:
        bpy.ops.object.mode_set(mode=mode)
        return enum_mode
    else:
        return None


#
# Bake Wrangler Operators
#

# Base class for all bakery operators, provides data to find owning node, etc.
class BakeWrangler_Operator:
    # Use strings to store their names, since Node isn't a subclass of ID it can't be stored as a pointer
    tree: bpy.props.StringProperty()
    node: bpy.props.StringProperty()
    sock: bpy.props.IntProperty(default=-1)

    @classmethod
    def poll(type, context):
        return True
        if context.area is not None:
            return True
            #return context.area.type == "NODE_EDITOR" and context.space_data.tree_type == "BakeWrangler_Tree"
        else:
            return False


# Dummy operator to draw when no vertex colors are in list
class BakeWrangler_Operator_Dummy_VCol(BakeWrangler_Operator, bpy.types.Operator):
    '''No vertex data currently in cache'''
    bl_idname = "bake_wrangler.dummy_vcol"
    bl_label = ""

    @classmethod
    def poll(type, context):
        # This operator is always supposed to be disabled
        return False



# Dummy operator to draw when a bake is in progress
class BakeWrangler_Operator_Dummy(BakeWrangler_Operator, bpy.types.Operator):
    '''Bake currently in progress, either cancel the current bake or wait for it to finish'''
    bl_idname = "bake_wrangler.dummy"
    bl_label = ""

    @classmethod
    def poll(type, context):
        # This operator is always supposed to be disabled
        return False



# Contol filter selection by allowing modifier keys
class BakeWrangler_Operator_FilterToggle(BakeWrangler_Operator, bpy.types.Operator):
    '''Ctrl-Click to deselect others, Shift-Click to select all others'''
    bl_idname = "bake_wrangler.filter_toggle"
    bl_label = ""
    bl_options = {"REGISTER", "UNDO"}
    
    filters = ('filter_mesh',
               'filter_curve',
               'filter_surface',
               'filter_meta',
               'filter_font',
               'filter_light')
    
    filter: bpy.props.StringProperty()
    
    @classmethod
    def description(self, context, properties):
        return properties.filter.split("_")[1].title() + " filter toggle. Ctrl-Click to deselect others, Shift-Click to select all others"
    
    def execute(self, context):
        return {'FINISHED'}
        
    def invoke(self, context, event):
        mod_shift = event.shift
        mod_ctrl = event.ctrl
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        if node and node.bl_idname == 'BakeWrangler_Input_ObjectList':
            # Toggle
            if not mod_ctrl and not mod_shift:
                setattr(node, self.filter, not getattr(node, self.filter))
            # Enable self and disable others
            elif mod_ctrl and not mod_shift:
                for fltr in self.filters:
                    setattr(node, fltr, False)
                setattr(node, self.filter, True)
            # Disable self and enable others
            elif not mod_ctrl and mod_shift:
                for fltr in self.filters:
                    setattr(node, fltr, True)
                setattr(node, self.filter, False)
            # Invert current states
            elif mod_ctrl and mod_shift:
                for fltr in self.filters:
                    setattr(node, fltr, not getattr(node, fltr))
        return {'FINISHED'}



# Double/Halve value
class BakeWrangler_Operator_DoubleVal(BakeWrangler_Operator, bpy.types.Operator):
    '''Description set by function'''
    bl_idname = "bake_wrangler.double_val"
    bl_label = ""
    bl_options = {"REGISTER", "UNDO"}
    
    val: bpy.props.StringProperty()
    half: bpy.props.BoolProperty()
    
    @classmethod
    def set_props(self, inst, node, tree, value, half=False):
            inst.tree = tree
            inst.node = node
            inst.val = value
            inst.half = half
            
    @classmethod
    def description(self, context, properties):
        if properties.half: return "Halve value"
        else: return "Double value"
        
    def execute(self, context):
        return {'FINISHED'}
        
    def invoke(self, context, event):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        value = getattr(node, self.val)
        if self.half: value /= 2
        else: value *= 2
        setattr(node, self.val, int(value))
        return {'FINISHED'}

    
    
# Pick an enum from a menu
class BakeWrangler_Operator_PickMenuEnum(BakeWrangler_Operator, bpy.types.Operator):
    '''Description set by function'''
    bl_idname = "bake_wrangler.pick_menu_enum"
    bl_label = ""
    bl_options = {"REGISTER", "UNDO"}
    
    enum_id: bpy.props.StringProperty()
    enum_desc: bpy.props.StringProperty()
    enum_prop: bpy.props.StringProperty()
    
    @classmethod
    def set_props(self, inst, e_id, e_desc, e_prop, node, tree):
            inst.tree = tree
            inst.node = node
            inst.enum_prop = e_prop
            inst.enum_desc = e_desc
            inst.enum_id = e_id
            
    @classmethod
    def description(self, context, properties):
        return properties.enum_desc
    
    def execute(self, context):
        return {'FINISHED'}
        
    def invoke(self, context, event):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        setattr(node, self.enum_prop, self.enum_id)
        return {'FINISHED'}



# Add selected objects to an ObjectList node (ignoring duplicates unless Shift held)
class BakeWrangler_Operator_AddSelected(BakeWrangler_Operator, bpy.types.Operator):
    '''Adds selected objects to the node, respecting filter and ignoring duplicates\nShift-Click: Adds items even if they are duplicates'''
    bl_idname = "bake_wrangler.add_selected"
    bl_label = "Add Selected"
    bl_options = {"REGISTER", "UNDO"}

    mod_shift = False

    # Called either after invoke from UI or directly from script
    def execute(self, context):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]

        existing_objs = []
        # Get all the objects in the current node (ignoring connected nodes and groups)
        for input in node.inputs:
            if not input.is_linked and input.value:
                existing_objs.append(input.value)

        selected_objs = []
        # Get a list of all selected objects that also match current filter
        for obj in context.selected_objects:
            if node.input_filter(obj.name, obj):
                selected_objs.append(obj)

        # Add non duplicate objects to the end of the node (includes duplicates if Shift)
        for obj in selected_objs:
            if self.mod_shift or obj not in existing_objs:
                node.inputs[-1].value = obj

        return {'FINISHED'}

    # Called from button press, set modifier key states
    def invoke(self, context, event):
        self.mod_shift = event.shift
        return self.execute(context)


# Read vertex color data from temp files and apply to current file
class BakeWrangler_Operator_DiscardBakedVertCols(BakeWrangler_Operator, bpy.types.Operator):
    '''Discard baked vertex color data'''
    bl_idname = "bake_wrangler.discard_vertcols"
    bl_label = "Discard Data"
    
    # Read and apply vertex colors
    def execute(self, context):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']:
            return {'CANCELLED'}
        files = node.vert_files
        while len(node.vert_files):
            jar = node.vert_files.pop()
            try:
                os.remove(jar)
            except:
                pass
                
        self.report({'INFO'}, "Data Removed")
        return {'FINISHED'}

    # Confirm the action
    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)


# Read vertex color data from temp files and apply to current file
class BakeWrangler_Operator_ApplyBakedVertCols(BakeWrangler_Operator, bpy.types.Operator):
    '''Apply baked vertex color data to current blend file objects'''
    bl_idname = "bake_wrangler.apply_vertcols"
    bl_label = "Apply Data"
    
    # Read and apply vertex colors
    def execute(self, context):
        try:
            from BakeWrangler.vert import ipc
        except:
            sys.path.append(os.path.dirname(os.path.abspath(__file__)))
            from vert import ipc
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']:
            return {'CANCELLED'}
        files = node.vert_files
        if not len(files):
            return {'CANCELLED'}
        
        # Open each file and apply the data one at a time
        oerr = 0
        for jar in files:
            fd = ipc.open_pickle_jar(file=jar)
            data = ipc.depickle_verts(file=fd)
            err, str = ipc.import_verts(cols=data)
            if err:
                _print("Error applying vertex data: %s" % (str), node=node)
                oerr += 1
            else:
                _print("Applied %s" % (str), node=node)
            if fd: fd.close()
            
        if oerr:
            self.report({'ERROR'}, "Apply Failed")
            return {'CANCELLED'}
        
        # Remove temp files if no errors
        while len(node.vert_files):
            jar = node.vert_files.pop()
            try:
                os.remove(jar)
            except:
                pass
                
        self.report({'INFO'}, "Data Applied")
        return {'FINISHED'}

    # Confirm the action
    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)
        
        
# Kill switch to stop a bake in progress
class BakeWrangler_Operator_BakeStop(BakeWrangler_Operator, bpy.types.Operator):
    '''Cancel currently running bake'''
    bl_idname = "bake_wrangler.bake_stop"
    bl_label = "Cancel Bake"

    # Stop the currently running bake
    def execute(self, context):
        tree = bpy.data.node_groups[self.tree]
        if tree.baking != None:
            tree.baking.stop()
            tree.interface_update(context)
        return {'FINISHED'}

    # Ask the user if they really want to cancel bake
    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)


# Operator for bake pass node
class BakeWrangler_Operator_BakePass(BakeWrangler_Operator, bpy.types.Operator):
    '''Perform requested bake action(s)'''
    bl_idname = "bake_wrangler.bake_pass"
    bl_label = "Bake Pass"

    _timer = None

    _thread = None
    _kill = False
    _success = False
    _finish = False
    _lock = threading.Lock()
    _queue = queue.SimpleQueue()
    _ifileq = queue.SimpleQueue()
    _vfileq = queue.SimpleQueue()
    stopping = False

    open_win = None
    open_ed = None
    node_ed = None

    start = None
    valid = None
    blend_copy = None
    blend_log = None
    bake_proc = None
    was_dirty = False
    img_list = []
    vert_list = []
    shutdown = False

    # Stop this bake if it's currently running
    def stop(self, kill=True):
        if self._thread and self._thread.is_alive() and kill:
            with self._lock:
                self.stopping = self._kill = True
        return self.stopping

    # Runs a blender subprocess
    def thread(self, node_name, tree_name, file_name, exec_name, script_name):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        sock = self.sock
        debug = _prefs('debug')
        ignorevis = _prefs('ignore_vis')
        factory = ""
        rend_dev_str = ""
        rend_dev_val = ""
        rend_use_str = ""
        rend_use_val = ""
        solution_itr = 0
        frames_itr = 0
        batch_itr = 0
        retry = _prefs('retrys') + 2
        if _prefs('fact_start'):
            factory = "--factory-startup"
            # Need to reselect gpu render some how when fact starting
            rend_type = bpy.context.preferences.addons['cycles'].preferences.compute_device_type
            rend_use = ""
            for dev in bpy.context.preferences.addons['cycles'].preferences.devices:
                if dev.use: rend_use = "%s1" % rend_use
                else: rend_use = "%s0" % rend_use
            rend_dev_str = "--rend_dev"
            rend_dev_val = str(rend_type)
            rend_use_str = "--rend_use"
            rend_use_val = str(rend_use)

        _print("Launching background process:", node=node, enque=self._queue)
        _print("================================================================================", enque=self._queue)
        while not self._finish and retry > 0:
            sub = subprocess.Popen([
                'blender',
                file_name,
                "--background",
                "--python", script_name,
                factory,
                "--",
                "--tree", tree_name,
                "--node", node_name,
                "--sock", str(int(sock)),
                "--debug", str(int(debug)),
                "--ignorevis", str(int(ignorevis)),
                "--solitr", str(solution_itr),
                "--frameitr", str(frames_itr),
                "--batchitr", str(batch_itr),
                rend_dev_str, rend_dev_val,
                rend_use_str, rend_use_val,
                ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace")

            # Read output from subprocess and print tagged lines
            out = ""
            kill = False
            while sub.poll() == None:
                # Check for kill flag
                if self._lock.acquire(blocking=False):
                    if self._kill:
                        _print("Bake canceled, terminating process...", enque=self._queue)
                        sub.kill()
                        out, err = sub.communicate()
                        kill = True
                    self._lock.release()

                if not kill:
                    out = sub.stdout.read(1)
                    # Collect tagged lines and display them in console
                    if out == '<':
                        out += sub.stdout.read(6)
                        if out == "<PBAKE>":
                            tag_end = False
                            tag_line = ""
                            out = ""
                            tag_out = ""
                            # Read until end tag is found
                            while not tag_end:
                                tag = sub.stdout.read(1)

                                if tag == '<':
                                    tag_line = sub.stdout.read(1)
                                    if tag_line != '_':
                                        tag_line = tag + tag_line + sub.stdout.read(6)
                                        if tag_line == "</PBAKE>":
                                            tag_end = True
                                            out += '\n'
                                        elif tag_line == "</PWRAP>":
                                            tag_end = True
                                            tag_out += '\n'
                                            #sys.stdout.write('\n')
                                            #sys.stdout.flush()
                                        elif tag_line == "<FINISH>":
                                            tag_line += sub.stdout.read(8)
                                            tag_end = True
                                            self._success = True
                                            self._finish = True
                                        elif tag_line == "<ERRORS>":
                                            tag_line += sub.stdout.read(8)
                                            tag_end = True
                                            self._success = False
                                            self._finish = True
                                if tag != '' and not tag_end:
                                    tag_out += tag
                                    #sys.stdout.write(tag_line)
                                    #sys.stdout.flush()
                                    out += tag
                            _print(tag_out, enque=self._queue, wrap=False)
                        if out == "<PFILE>" or out == "<PVERT>":
                            tag_end = False
                            tag_line = ""
                            files = ""
                            # Set output queue
                            if out == "<PFILE>":
                                que = self._ifileq
                            else:
                                que = self._vfileq
                            while not tag_end:
                                tag = sub.stdout.read(1)
                                if tag == '<':
                                    tag_line = sub.stdout.read(1)
                                    if tag_line != '_':
                                        tag_line = tag + tag_line + sub.stdout.read(6)
                                        if tag_line == "</PFILE>" or tag_line == "</PVERT>":
                                            tag_end = True
                                            _print(files, enque=que, wrap=False)
                                if tag != '' and not tag_end:
                                    files += tag
                            out = ''
                        if out == "<PFRAM>" or out == "<PSOLU>" or out == "<PBATC>":
                            tag_end = False
                            tag_line = ""
                            num = ""
                            while not tag_end:
                                tag = sub.stdout.read(1)
                                if tag == '<':
                                    tag_line = sub.stdout.read(1)
                                    if tag_line != '_':
                                        tag_line = tag + tag_line + sub.stdout.read(6)
                                        if tag_line == "</PFRAM>":
                                            tag_end = True
                                            frames_itr = int(num)
                                        elif tag_line == "</PSOLU>":
                                            tag_end = True
                                            solution_itr = int(num)
                                        elif tag_line == "</PBATC>":
                                            tag_end = True
                                            batch_itr = int(num)
                                if tag != '' and not tag_end:
                                    num += tag
                            out = ''
                # Write to log
                if out != '' and self.blend_log:
                    self.blend_log.write(out)
                    self.blend_log.flush()
            _print("================================================================================", enque=self._queue)
            _print("Background process ended", node=node, enque=self._queue)
            retry -= 1
            if not self._finish and retry > 0:
                _print("Process did not complete, retry from last known success (%s tries remain)" % (retry - 1), node=node, enque=self._queue)

    # Event handler
    def modal(self, context, event):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]

        # Check if the bake thread has ended every timer event
        if event.type == 'TIMER':
            self.print_queue(context)
            # Reapply dirt by pushing something to undo stack (not ideal)
            if self.was_dirty and not bpy.data.is_dirty and bpy.ops.node.select_all.poll():
                bpy.ops.node.select_all(action='INVERT')
                bpy.ops.node.select_all(True, action='INVERT')
                self.was_dirty = False
            if not self._thread.is_alive():
                self.cancel(context)
                if self._kill:
                    _print("Bake canceled after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
                    _print("Canceled\n", node=node, enque=self._queue)
                    self.report({'WARNING'}, "Bake Canceled")
                    self.update_images()
                    self.print_queue(context)
                    if self.blend_log:
                        context.window_manager.bw_lastlog = self.blend_copy + ".log"
                        context.window_manager.bw_lastfile = self.blend_copy
                    return {'CANCELLED'}
                else:
                    if self._success and self._finish:
                        _print("Bake finished in %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
                        _print("Success\n", node=node, enque=self._queue)
                        self.report({'INFO'}, "Bake Completed")
                        context.window_manager.bw_status = 0 # Bake finished / idle status
                    elif self._finish:
                        _print("Bake finished with errors after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
                        _print("Errors\n", node=node, enque=self._queue)
                        self.report({'WARNING'}, "Bake Finished with Errors")
                        context.window_manager.bw_status = 2 # Bake error status
                    else:
                        _print("Bake failed after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
                        _print("Failed\n", node=node, enque=self._queue)
                        self.report({'ERROR'}, "Bake Failed")
                        context.window_manager.bw_status = 2 # Bake error status
                    self.print_queue(context)
                    if self.blend_log:
                        context.window_manager.bw_lastlog = self.blend_copy + ".log"
                        context.window_manager.bw_lastfile = self.blend_copy
                    # Update images
                    self.dequeue_files(context, self._ifileq, self.img_list)
                    if _prefs('debug'): _print("Img list: %s" % self.img_list)
                    self.update_images()
                    # Send vertex file names to node
                    if hasattr(node, 'vert_files'):
                        self.dequeue_files(context, self._vfileq, self.vert_list)
                        if _prefs('debug'): _print("Vert list: %s" % self.vert_list)
                        for vfile in self.vert_list:
                            node.vert_files.append(vfile)
                    # Check if a post-bake user script should be run
                    if self._finish and node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_post != 'NON':
                        _print("Running user created post-bake script: ", node=node, wrap=False)
                        if node.loc_post == 'INT':
                            post_scr = node.script_post_int.as_string()
                        elif node.loc_post == 'EXT':
                            with open(node.script_post_can, "r") as scr:
                                post_scr = scr.read()
                        try:
                            exec(post_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')})
                        except Exception as err:
                            _print(" Failed - %s" % (str(err)))
                        else:
                            _print(" Done")
                    if _prefs("wind_msgs") and self.open_win:
                        if _prefs("wind_close"):
                            bpy.ops.wm.window_close({"window": self.open_win})
                    # Do batch shutdown
                    if self.shutdown:
                        if sys.platform == 'win32':
                            os.system('shutdown /s /t 60')
                        else:
                            os.system('sudo shutdown +1')
                    if _prefs("show_icon"): update_status_bar()
                    return {'FINISHED'}
        return {'PASS_THROUGH'}
    
    # Get queued file list
    def dequeue_files(self, context, queue, list):
        fstr = ""
        try:
            # An Empty exception will be raised when nothing is in the queue
            while True:
                fstr += queue.get_nowait()
        except:
            list += fstr.split(",")
            return
            
    # Print queued messages
    def print_queue(self, context):
        try:
            # An Empty exception will be raised when nothing is in the queue
            while True:
                msg = self._queue.get_nowait()
                _print(msg, wrap=False)
        except:
            return

    # Display log file if debugging is enabled and the bake failed or had errors
    def show_log(self):
        if _prefs('debug') and self.blend_log and self.node_ed:
            bpy.ops.screen.area_dupli({'area': self.node_ed}, 'INVOKE_DEFAULT')
            open_ed = bpy.context.window_manager.windows[len(bpy.context.window_manager.windows) - 1].screen.areas[0]
            open_ed.type = 'TEXT_EDITOR'
            log = bpy.data.texts.load(self.blend_copy + ".log")
            open_ed.spaces[0].text = log
            open_ed.spaces[0].show_line_numbers = False
            open_ed.spaces[0].show_syntax_highlight = False

    # Update any loaded images that might be changed by the bake
    def update_images(self):
        if len(self.img_list):
            cwd = os.path.dirname(bpy.data.filepath)
            open_imgs = {}
            for img in bpy.data.images:
                open_imgs[os.path.normpath(os.path.join(cwd, bpy.path.abspath(img.filepath_raw)))] = img
            for img in self.img_list:
                if img in open_imgs.keys():
                    open_imgs[img].reload()
                elif _prefs("auto_open"):
                    try:
                        bpy.data.images.load(img)
                    except:
                        pass

    # Called after invoke to perform the bake if everything passed validation
    def execute(self, context):
        # If called from script, do prepare now
        if self.valid == None:
            if self.tree and self.node:
                tree = bpy.data.node_groups[self.tree]
                node = tree.nodes[self.node]
                self.prepare(context, tree, node)
            else:
                self.valid = [False]
                self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"])
                self.report({'ERROR'}, "Operator required arguments missing")
                return {'CANCELLED'}
        # Do any interactive actions if called from invoke
        else:
            # If message log in new window is enabled
            if _prefs("text_msgs") and _prefs("wind_msgs"):
                bpy.ops.screen.area_dupli('INVOKE_DEFAULT')
                self.open_win = context.window_manager.windows[len(context.window_manager.windows) - 1]
                self.open_ed = self.open_win.screen.areas[0]
                self.open_ed.type = 'TEXT_EDITOR'
                self.open_ed.spaces[0].text = bpy.data.texts["BakeWrangler"]
                self.open_ed.spaces[0].show_line_numbers = False
                self.open_ed.spaces[0].show_syntax_highlight = False

        if not self.valid[0]:
            self.cancel(context)
            self.report({'ERROR'}, "Validation failed")
            return {'CANCELLED'}

        self.start = datetime.now()
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]

        # Check for batch shutdown
        if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.shutdown_after:
            self.shutdown = True

        # Save a temporary copy of the blend file and store the path. Make sure the path doesn't exist first.
        # All baking will be done using this copy so the user can continue working in this session.
        blend_name = bpy.path.clean_name(bpy.path.display_name_from_filepath(bpy.data.filepath))
        blend_temp = bpy.path.abspath(bpy.app.tempdir)
        node_cname = bpy.path.clean_name(node.get_name())
        blend_copy = os.path.join(blend_temp, "BW_" + blend_name)

        # Increment file name until it doesn't exist
        if os.path.exists(blend_copy + ".blend"):
            fno = 1
            while os.path.exists(blend_copy + "_%03i.blend" % (fno)):
                fno = fno + 1
            blend_copy = blend_copy + "_%03i.blend" % (fno)
        else:
            blend_copy = blend_copy + ".blend"
            
        # Check if a pre-bake user script should be run
        if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_pre != 'NON':
            _print("Running user created pre-bake script: ", node=node, wrap=False)
            if node.loc_pre == 'INT':
                pre_scr = node.script_pre_int.as_string()
            elif node.loc_pre == 'EXT':
                with open(node.script_pre_can, "r") as scr:
                    pre_scr = scr.read()
            try:
                exec(pre_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')})
            except Exception as err:
                _print(" Failed - %s" % (str(err)))
                return {'CANCELLED'}
            else:
                _print(" Done")

        # Print out start message and temp path
        _print("")
        _print("=== Bake starts ===", node=node)
        _print("Creating temporary files in %s" % (blend_temp), node=node)

        # Maintain dirt
        if bpy.data.is_dirty:
            self.was_dirty = True
            
        # Save dirty images based on preferences as unsaved changes will not effect bake
        if _prefs("save_packed") or _prefs("save_images"):
            for img in bpy.data.images:
                if img.is_dirty:
                    if (img.packed_file is not None and _prefs("save_packed")) or (img.packed_file is None and _prefs("save_images")):
                        bpy.ops.image.save({'edit_image': img})
        
        try:
            bpy.ops.wm.save_as_mainfile(filepath=blend_copy, copy=True)
        except RuntimeError as err:
            _print("Temporary file creation failed: %s" % (str(err)), node=node)
            self.report({'ERROR'}, "Blend file copy failed")
            return {'CANCELLED'}
        else:            
            # Check copy exists
            if not os.path.exists(blend_copy):
                _print("Temporary file creation failed", node=node)
                self.report({'ERROR'}, "Blend file copy failed")
                return {'CANCELLED'}
            else:
                self.blend_copy = blend_copy                

        # Open a log file at the same location with a .log appended to the name
        log_err = None
        blend_log = None
        try:
            blend_log = open(blend_copy + ".log", "w", encoding="utf-8", errors="replace")
        except OSError as err:
            self.report({'WARNING'}, "Couldn't create log file")
            log_err = err.strerror
        else:
            self.blend_log = blend_log
            tree.last_log = blend_copy + ".log"

        # Print out blend copy and log names
        _print(" - %s" % (os.path.basename(self.blend_copy)), node=node)
        if self.blend_log and not log_err:
            _print(" - %s" % (os.path.basename(self.blend_copy + ".log")), node=node)
        else:
            _print(" - Log file creation failed: %s" % (log_err), node=node)
        _print("Blender: %s Addon: %s" % (bpy.app.version_string, BW_VERSION_STRING), node=node)
        
        # Create a thread which will launch a background instance of blender running a script that does all the work.
        # Process is complete when thread exits. Will need full path to blender, node, temp file and proc script.
        blend_exec = bpy.path.abspath(bpy.app.binary_path)
        self._thread = threading.Thread(target=self.thread, args=(self.node, self.tree, self.blend_copy, blend_exec, self.bake_proc,))

        # Get a list of image file names that will be updated by the bake so they can be reloaded on success
        self.img_list = []
        '''if node.bl_idname == 'BakeWrangler_Output_Batch_Bake':
            for input in node.inputs:
                outnode = get_input(input)
                if outnode and outnode.bl_idname == 'BakeWrangler_Output_Image_Path':
                    files = outnode.get_output_files()
                    for name in files.keys():
                        img_name = os.path.join(outnode.img_path, name)
                        if not self.img_list.count(img_name):
                            self.img_list.append(img_name)
        elif node.bl_idname == 'BakeWrangler_Output_Image_Path':
            files = node.get_output_files()
            for name in files.keys():
                img_name = os.path.join(node.img_path, name)
                if not self.img_list.count(img_name):
                    self.img_list.append(img_name)'''
                    
        # Init vert file list
        self.vert_list = []
        bpy.ops.bake_wrangler.discard_vertcols(node=self.node, tree=self.tree)

        # Add a timer to periodically check if the bake has finished
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.5, window=context.window)
        wm.modal_handler_add(self)

        self._thread.start()

        # Go modal
        context.window_manager.bw_status = 1 # Baking status
        if _prefs("show_icon"): update_status_bar()
        return {'RUNNING_MODAL'}

    # Called by UI when the button is clicked. Will validate settings and prepare files for execute
    def invoke(self, context, event):
        # Prep for bake
        self.node_ed = context.area
        if self.tree and self.node:
            tree = bpy.data.node_groups[self.tree]
            node = tree.nodes[self.node]
            self.prepare(context, tree, node)
        else:
            self.valid = [False]
            self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"])
            self.report({'ERROR'}, "Operator properties not set")
            return {'CANCELLED'}
        
        if not self.valid[0] or len(self.valid) > 1:
            # Draw pop-up that will use custom draw function to display any validation errors
            return context.window_manager.invoke_props_dialog(self, width=400)
        else:
            return self.execute(context)

    # Prepare for bake, do all validation tasks and set properties
    def prepare(self, context, tree, node):
        # Init variables
        self.valid = [False]

        # Are text editor messages enabled?
        if _prefs("text_msgs"):
            # Make sure the text block exists
            if not "BakeWrangler" in bpy.data.texts.keys():
                bpy.data.texts.new("BakeWrangler")
            # Clear the block if option set
            if _prefs("clear_msgs"):
                bpy.data.texts["BakeWrangler"].clear()

        # Do full validation of bake so it can be reported
        tree.baking = self
        tree.interface_update(context)
        self.valid = node.validate(is_primary=True)
        
        # Remove UV errors if node is a vertex color output, kinda dumb, but... Least changes required this way
        if node.bl_idname == 'BakeWrangler_Output_Vertex_Cols' and not self.valid[0]:
            idx = 0
            for msg in self.valid:
                if idx == 0: 
                    idx += 1
                    continue
                if msg[0].endswith("UV error"):
                    self.valid.pop(idx)
                idx += 1
            if len(self.valid) == 1:
                self.valid[0] = True

        # Check tree is of the current version
        if tree.tree_version != BW_TREE_VERSION:
            self.valid[0] = False
            if tree.tree_version < BW_TREE_VERSION:
                self.valid.append([_print("Bake Recipe for older version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. Please use the auto-update function if available, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)])
            else:
                self.valid.append([_print("Bake Recipe for newer version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. You need to update Bake Wrangler to a version that supports this recipe, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)])
        # Check processing script exists
        bake_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
        bake_proc = bpy.path.abspath(os.path.join(bake_path, "baker.py"))
        if not os.path.exists(bake_proc):
            self.valid[0] = False
            self.valid.append([_print("File missing", node=node, ret=True), ": Bake processing script wasn't found at '%s'" % (bake_proc)])
        else:
            self.bake_proc = bake_proc

        # Check baking scene file exists
        scene_file = bpy.path.abspath(os.path.join(bake_path, "resources", "BakeWrangler_Scene.blend"))
        if not os.path.exists(scene_file):
            self.valid[0] = False
            self.valid.append([_print("File missing", node=node, ret=True), ": Bake scene library wasn't found at '%s'" % (scene_file)])
        self.valid = prune_messages(self.valid)

    # Cancel the bake
    def cancel(self, context):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        if self._timer:
            wm = context.window_manager
            wm.event_timer_remove(self._timer)
        if self.blend_log:
            self.blend_log.close()
        if tree.baking != None:
            tree.baking = None
            tree.interface_update(context)
        if self.blend_copy and os.path.exists(self.blend_copy):
            if not _prefs("debug"):
                try:
                    os.remove(self.blend_copy)
                except OSError as err:
                    _print("Temporary file removal failed: %s\n" % (err.strerror), node=node, enque=self._queue)

    # Draw custom pop-up
    def draw(self, context):
        tree = bpy.data.node_groups[self.tree]
        node = tree.nodes[self.node]
        layout = self.layout.column(align=True)
        if not self.valid[0]:
            layout.label(text="!!! Validation FAILED:")
            _print("")
            _print("!!! Validation FAILED:", node=node)
            col = layout.column()
            for i in range(len(self.valid) - 1):
                col.label(text=self.valid[i + 1][0])
                _print(self.valid[i + 1][0] + self.valid[i + 1][1])
            layout.label(text="See console for details")
            _print("")
        elif len(self.valid) > 1:
            layout.label(text="")
            layout.label(text="!!! Material Warnings:")
            _print("")
            _print("!!! Material Warnings:")
            col = layout.column()
            for i in range(len(self.valid) - 1):
                col.label(text=self.valid[i + 1][0])
                _print(self.valid[i + 1][0] + self.valid[i + 1][1])
            layout.label(text="See console for details")
            _print("")            



#
# Bake Wrangler nodes system
#


# Node tree definition that shows up in the editor type list. Sets the name, icon and description.
class BakeWrangler_Tree(NodeTree):
    '''Improved baking system to extend and replace existing internal bake system'''
    bl_label = 'Bake Recipe'
    bl_icon = 'NODETREE'

    def __init__(self):
        pass

    # Get pinned mesh settings if exists
    def get_pinned_settings(self, setting):
        for node in self.nodes:
            if node.bl_idname == 'BakeWrangler_' + setting and node.pinned:
                return node
        return None

    # Get the active global resolution node in a tree
    def get_active_res(self):
        for node in self.nodes:
            if node.bl_idname == 'BakeWrangler_Global_Resolution' and node.is_active:
                return node
        return None

    # Does this need a lock for modal event access?
    baking = None

    # Do some initial set up when a new tree is created
    initialised: bpy.props.BoolProperty(name="Initialized", default=False)
    tree_version: bpy.props.IntProperty(name="Tree Version", default=0)
    last_log: bpy.props.StringProperty(name="Last Log", default="")



# Custom Sockets:

# Base class for all bakery sockets
class BakeWrangler_Tree_Socket:
    # Workaround for link.is_valid being un-usable
    valid: bpy.props.BoolProperty()

    def socket_label(self, text):
        if self.is_output or (self.is_linked and self.valid) or (not self.is_output and not self.is_linked):
            return text
        else:
            return text + " [invalid]"

    def socket_color(self, color):
        if not self.is_output and self.is_linked and not self.valid:
            return (1.0, 0.0, 0.0, 1.0)
        else:
            return color

    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return [self.bl_idname]

    # Follows through reroutes
    def islinked(self):
        if self.is_linked and not self.is_output:
            try: # During link removal this can be in a weird state
                node = self.links[0].from_node
                while node.type == "REROUTE":
                    if node.inputs[0].is_linked and node.inputs[0].links[0].is_valid:
                        node = node.inputs[0].links[0].from_node
                    else:
                        return False
                return True
            except:
                pass
        return False



# Socket for an object or list of objects to be used in a bake pass in some way
class BakeWrangler_Socket_Object(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for baking relevant objects'''
    bl_label = 'Object'

    object_types = ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'LIGHT']

    # Called to filter objects listed in the value search field
    def value_prop_filter(self, object):
        return self.node.input_filter(self.name, object)

    def cage_prop_filter(self, cage):
        return cage.type == 'MESH'

    # Try to auto locate the cage by name when enabled
    def use_cage_update(self, context):
        if self.use_cage and not self.cage and self.value:
            for obj in bpy.data.objects:
                if obj.name.startswith(self.value.name) and obj.name.lower().startswith("cage", len(self.value.name) + 1):
                    self.cage = obj
                    break

    # Called when the value property changes
    def value_prop_update(self, context):
        self.type = 'NONE'
        if self.value:
            if self.value.rna_type.identifier == 'Collection':
                self.type = 'GROUP'
            elif self.value.rna_type.identifier == 'Object':
                if self.value.type in self.object_types:
                    self.type = '%s_DATA' % (self.value.type)
        if self.node:
            self.node.update_inputs()

    # Get own objects or the full linked tree
    def get_objects(self, only_mesh=False, no_lights=False, only_groups=False):
        objects = []
        # Follow links
        if self.islinked() and self.valid:
            return follow_input_link(self.links[0]).from_node.get_objects(only_mesh, no_lights, only_groups)
        # Otherwise return self values
        if self.value and self.type and self.type != 'NONE' and not self.is_linked:
            # Only interested in mesh types?
            if self.type not in ['MESH_DATA', 'GROUP'] and only_mesh:
                return []
            if self.type == 'LIGHT_DATA' and no_lights:
                return []
            if only_groups and self.type != 'GROUP':
                return []
            # Need to get all the grouped objects
            if self.type == 'GROUP':
                filter = list(self.object_types)
                if no_lights:
                    filter.remove('LIGHT')
                if only_mesh:
                    filter = ['MESH']
                # Iterate over the objects applying the type filter
                for obj in self.get_grouped():
                    if obj.type in filter:
                        objects.append([obj])
                if only_groups:
                    return [[self.value, objects]]
            # Mesh data can have a few extra properties
            elif self.type == 'MESH_DATA':
                uv_map = ""
                if self.pick_uv and self.uv_map:
                    uv_map = self.uv_map
                cage = None
                if self.use_cage and self.cage:
                    cage = self.cage
                objects.append([self.value, uv_map, cage])
            else:
                objects.append([self.value])
        return objects

    # Return objects contained in a group
    def get_grouped(self):
        if self.recursive:
            return self.value.all_objects
        else:
            return self.value.objects

    # Validate value(s)
    def validate(self, check_materials=False, check_as_active=False, check_multi=False):
        valid = [True]
        # Follow links
        if self.islinked() and self.valid:
            return follow_input_link(self.links[0]).from_node.validate(check_materials, check_as_active, check_multi)
        # Has a value and isn't linked
        if self.value and self.type and not self.islinked():
            objs = [self.value]
            if self.type == 'GROUP':
                objs = self.get_grouped()

            # Iterate over objs, it will just be one object unless the type is group (but maintains a single algo for both)
            for obj in objs:
                # Perform checks needed for an active bake target
                if check_as_active:
                    # Only a mesh type object can be a valid target, it will just be silently ignored
                    if obj.type != 'MESH':
                        return valid
                    # Any UV map?
                    if len(obj.data.uv_layers) < 1:
                        valid[0] = False
                        valid.append([_print("UV error", node=self.node, ret=True), ": No UV Maps found on object <%s>." % (obj.name)])
                    # Custom UV map still exists? (can't be done for grouped objects)
                    if self.type != 'GROUP' and self.pick_uv and self.uv_map not in obj.data.uv_layers and self.uv_map != "":
                        valid[0] = False
                        valid.append([_print("UV error", node=self.node, ret=True), ": Selected UV map <%s> not present on object <%s> (it could have been deleted or renamed)" % (self.uv_map, obj.name)])
                    # Check for a valid multi-res mod if enabled
                    if check_multi:
                        has_multi_mod = False
                        if len(obj.modifiers):
                            for mod in obj.modifiers:
                                if mod.type == 'MULTIRES' and mod.total_levels > 0:
                                    has_multi_mod = True
                                    break
                        if not has_multi_mod:
                            valid[0] = False
                            valid.append([_print("Multires error", node=self.node, ret=True), ": No multires data on object <%s>." % (obj.name)])
                # Check that materials can be converted to enable PBR data bakes
                if check_materials and obj.type in self.object_types:
                    mats = []
                    if len(obj.material_slots):
                        for slot in obj.material_slots:
                            mat = slot.material
                            if mat != None and not mat in mats:
                                mats.append(mat)
                                # Is node based?
                                if not mat.node_tree or not mat.node_tree.nodes:
                                    valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)])
                                    continue
                                # Is a 'principled' material?
                                passed = False
                                for node in mat.node_tree.nodes:
                                    if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']:
                                        if material_recursor(node):
                                            passed = True
                                            break
                                if not passed:
                                    valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix/Add shaders. Baked values may not be correct for this material." % (mat.name)])
        return valid

    # Blender Properties
    value: bpy.props.PointerProperty(name="Object", description="Object to be used in some way in a bake pass", type=bpy.types.ID, poll=value_prop_filter, update=value_prop_update)
    type: bpy.props.StringProperty(name="Type", description="ID String of value type", default="NONE")
    recursive: bpy.props.BoolProperty(name="Recursive Selection", description="When enabled all collections within the selected collection will be used", default=False)
    pick_uv: bpy.props.BoolProperty(name="Pick UV Map", description="Enables selecting which UV map to use instead of the active one", default=False)
    uv_map: bpy.props.StringProperty(name="UV Map", description="UV Map to use instead of active if value is a mesh", default="")
    use_cage: bpy.props.BoolProperty(name="Use Cage", description="Enables cage usage and selection of cage mesh", default=False, update=use_cage_update)
    cage: bpy.props.PointerProperty(name="Cage", description="Mesh to use a cage", type=bpy.types.Object, poll=cage_prop_filter)

    def draw(self, context, layout, node, text):
        if not self.is_output and not self.islinked() and not self.node.bl_idname == 'BakeWrangler_Bake_Material':
            row = layout.row(align=True)
            label = ""
            if self.name in ['Target', 'Source', 'Scene']:
                split_fac = 44 / self.node.width
                split = row.split(factor=split_fac)
                rowl = split.row(align=True)
                rowl.label(text=self.name)
                row = split.row(align=True)
            if self.name in ['Target', 'Source'] or (hasattr(node, "filter_collection") and not node.filter_collection):
                row.prop_search(self, "value", bpy.data, "objects", text=label, icon=self.type)
            else:
                row.prop_search(self, "value", bpy.data, "collections", text=label, icon=self.type)
            if self.value and self.type:
                if self.type == 'GROUP':
                    row.prop(self, "recursive", icon='OUTLINER', text="")
                if self.type == 'MESH_DATA':
                    row.prop(self, "pick_uv", icon='UV', text="")
                    if self.pick_uv:
                        row.prop_search(self, "uv_map", self.value.data, "uv_layers", text="", icon='UV_DATA')
                    row.prop(self, "use_cage", icon='FILE_VOLUME', text="")
                    if self.use_cage:
                        row.prop_search(self, "cage", bpy.data, "objects", text="", icon='MESH_DATA')
        elif self.is_output:
            row = layout.row(align=False)
            row0 = row.row()
            row0.ui_units_x = 50
            op = row0.operator("bake_wrangler.add_selected")
            op.tree = self.node.id_data.name
            op.node = self.node.name
            row2 = row.row(align=False)
            row2.alignment = 'RIGHT'
            row2.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))


    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.2, 1.0, 1.0))


# Socket for materials baking
class BakeWrangler_Socket_Material(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for specifying a material to bake'''
    bl_label = 'Material'
    
    # Called when the value property changes
    def value_prop_update(self, context):
        if self.node:
            self.node.update_inputs()
            
    value: bpy.props.PointerProperty(name="Material", description="Material to be used in a bake pass", type=bpy.types.Material, update=value_prop_update)
    
    def draw(self, context, layout, node, text):
        if not self.is_output:
            row = layout.row(align=True)
            split_fac = 52 / self.node.width
            split = row.split(factor=split_fac)
            rowl = split.row(align=True)
            rowl.label(text=self.name)
            rowr = split.row(align=True)
            rowr.prop_search(self, "value", bpy.data, "materials", icon='MATERIAL_DATA', text="")
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        if self.is_output:
            return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))
        return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0))


# Socket for sharing a target mesh
class BakeWrangler_Socket_Mesh(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting a mesh node'''
    bl_label = 'Mesh'
    
    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return [self.bl_idname, 'BakeWrangler_Socket_Material']
        
    def draw(self, context, layout, node, text):
        if node.bl_idname == 'BakeWrangler_Bake_Pass':
            if self.islinked() and self.valid:
                if get_input(self).bl_idname == 'BakeWrangler_Bake_Material':
                    label = "Material"
                else:
                    label = "Mesh"
            else:
                label = "Mesh / Material"
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, label))
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))


# Socket for RGB(A) data, extends the base color node
class BakeWrangler_Socket_Color(NodeSocketColor, BakeWrangler_Tree_Socket):
    '''Socket for RGB(A) data'''
    bl_label = 'Color'
    
    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return [self.bl_idname, 'BakeWrangler_Socket_Float']
        
    #Props
    suffix: bpy.props.StringProperty(name="Suffix", description="Suffix appended to filename for this output")
    value_rgb: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.5,0.5,0.5,1.0], size=4)

    def draw(self, context, layout, node, text):
        if self.is_linked and self.valid and node.bl_idname in ['BakeWrangler_Output_Image_Path', 'BakeWrangler_Output_Vertex_Cols']:
            row = layout.row(align=True)
            sfac = 40 / node.width
            split = row.split(factor=sfac)
            if node.bl_idname == 'BakeWrangler_Output_Image_Path':
                split.label(text="Suffix")
            else:
                split.label(text="Name")
            srow = split.row(align=True)
            srow.prop(self, "suffix", text="")
            idx = 0
            for input in node.inputs:
                if input == self:
                    break
                idx += 1
            BakeWrangler_Tree_Node.draw_bake_button(node, srow, 'IMAGE', "", True, idx)
        elif node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.is_linked and not self.is_output:
            layout.prop(self, "value_rgb", text=self.name)
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0))


# Socket to map input value to output channel. Works like a separator/combiner node pair
class BakeWrangler_Socket_ChanMap(NodeSocketColor, BakeWrangler_Tree_Socket):
    '''Socket for splitting and joining color channels'''
    bl_label = 'Channel Map'

    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return ['BakeWrangler_Socket_Color']

    channels = (
        ('Red', "Red", "Red"),
        ('Green', "Green", "Green"),
        ('Blue', "Blue", "Blue"),
        ('Value', "Value", "Value"),
    )

    # Props
    input_channel: bpy.props.EnumProperty(name="Input Channel", description="Channel of input color data to take values from", items=channels, default='Value')

    def draw(self, context, layout, node, text):
        row = layout.row(align=True)
        label = row.row()
        if not self.is_output and self.is_linked and self.valid:
            chan = row.row()
            chan.prop(self, "input_channel", text="From")
        label.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        if self.name == 'Alpha':
            return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0))
        else:
            return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0))


# Socket for Float data, extends the base float node
class BakeWrangler_Socket_Float(NodeSocketFloat, BakeWrangler_Tree_Socket):
    '''Socket for Float data'''
    bl_label = 'Float'
    
    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return [self.bl_idname, 'BakeWrangler_Socket_Color']
    
    value_fac: bpy.props.FloatProperty(name="Fac", default=0.5, subtype='FACTOR', min=0.0, max=1.0, precision=3, step=10)
    value_col: bpy.props.FloatProperty(name="Fac", soft_min=0.0, soft_max=1.0, precision=3, step=10)
    value_gam: bpy.props.FloatProperty(name="Fac", default=1.0, min=0.0, soft_min=0.001, soft_max=10.0, precision=3, step=1)
    value: bpy.props.FloatProperty(name="Value", precision=3, step=10)
    
    def draw(self, context, layout, node, text):
        if node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.islinked() and not self.is_output:
            layout.prop(self, "value_fac")
        elif node.bl_idname == 'BakeWrangler_Post_JoinRGB' and not self.islinked() and not self.is_output:
            layout.prop(self, "value_col", text=self.name)
        elif node.bl_idname == 'BakeWrangler_Post_Math' and not self.islinked() and not self.is_output:
            if node.op == 'POWER':
                if self.identifier == '0':
                    layout.prop(self, "value", text="Base")
                elif self.identifier == '1':
                    layout.prop(self, "value", text="Exponent")
            elif node.op == 'LOGARITHM' and self.identifier == '1':
                layout.prop(self, "value", text="Base")
            else:
                layout.prop(self, "value", text=self.name)
        elif node.bl_idname == 'BakeWrangler_Post_Gamma' and not self.islinked() and not self.is_output:
            layout.prop(self, "value_gam", text=self.name)
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0))


# Socket for connecting an output image to a batch job node
class BakeWrangler_Socket_Bake(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting an output image node to a batch node'''
    bl_label = 'Bake'

    def draw(self, context, layout, node, text):
        row = layout.row(align=True)
        if self.is_output:
            row0 = row.row()
            row0.ui_units_x = 50
            row1 = row.row(align=False)
            row1.alignment = 'RIGHT'
            if self.node.bl_idname == 'BakeWrangler_Output_Image_Path':
                BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Image")
            elif self.node.bl_idname == 'BakeWrangler_Output_Vertex_Cols':
                BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Colors")
            row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        else:
            row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.5, 1.0, 1.0))


# Socket for connecting a mesh settings node to a mesh
class BakeWrangler_Socket_MeshSetting(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting a mesh settings node to a mesh node'''
    bl_label = 'Mesh Settings'

    def draw(self, context, layout, node, text):
        row = layout.row(align=False)
        if self.is_output:
            row0 = row.row()
            row1 = row.row(align=False)
            row1.alignment = 'RIGHT'
            row1.ui_units_x = 100
            if self.node.pinned:
                row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
            else:
                row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
            row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        else:
            row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.3, 0.0, 1.0))


# Socket for connecting a pass settings node to a pass
class BakeWrangler_Socket_PassSetting(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting a pass settings node to a pass node'''
    bl_label = 'Pass Settings'

    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return [self.bl_idname, 'BakeWrangler_Socket_SampleSetting']
        
    def draw(self, context, layout, node, text):
        row = layout.row(align=False)
        if self.is_output:
            row0 = row.row()
            row1 = row.row(align=False)
            row1.alignment = 'RIGHT'
            row1.ui_units_x = 100
            if self.node.pinned:
                row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
            else:
                row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
            row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        elif not self.is_linked:
            split = row.split(factor=0.35)
            split.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
            split.prop(self.node, "bake_samples", text="Samples")
        else:
            row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.37, 0.08, 1.0, 1.0))


# Socket for connecting a pass settings node to a pass
class BakeWrangler_Socket_SampleSetting(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting a pass settings node to a pass node'''
    bl_label = 'Sample Settings'

    def draw(self, context, layout, node, text):
        row = layout.row(align=False)
        if self.is_output:
            row0 = row.row()
            row1 = row.row(align=False)
            row1.alignment = 'RIGHT'
            row1.ui_units_x = 100
            if self.node.pinned:
                row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
            else:
                row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
            row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        else:
            row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.4, 0.08, 1.0, 1.0))
        
        
# Socket for connecting an output settings node to an output
class BakeWrangler_Socket_OutputSetting(BakeWrangler_Tree_Socket, NodeSocket):
    '''Socket for connecting an output settings node to an output node'''
    bl_label = 'Output Settings'

    def draw(self, context, layout, node, text):
        row = layout.row(align=False)
        if self.is_output:
            row0 = row.row()
            row1 = row.row(align=False)
            row1.alignment = 'RIGHT'
            row1.ui_units_x = 100
            if self.node.pinned:
                row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
            else:
                row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
            row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
        else:
            row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))

    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.14, 1.0, 1.0, 1.0))


# Socket that takes the names of all objects input to use for filename outputs
class BakeWrangler_Socket_ObjectNames(BakeWrangler_Tree_Socket, NodeSocket):
    '''Take the names of input objects'''
    bl_label = 'Object Names'
    
    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        return ['BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material']
    
    def draw(self, context, layout, node, text):
        layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
    
    def draw_color(self, context, node):
        return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))


# Socket to take objects to use for splitting output into separate files
class BakeWrangler_Socket_SplitOutput(BakeWrangler_Tree_Socket, NodeSocket):
    '''Split output into files based on input'''
    bl_label = 'Split Output'
    
    # Returns a list of valid bl_idnames that can connect
    def valid_inputs(self):
        if not self.name == "Path/Filename":
            return [self.bl_idname, 'BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material']
        return [None]
    
    # Get what ever the split list input node is, if there is one
    def get_split(self):
        linked = get_input(self)
        if linked:
            if linked.bl_idname == 'BakeWrangler_Input_Filenames':
                return linked.get_names()
            return [linked]
        return None        
    
    # Get full path, removing any relative references
    def get_full_path(self):
        linked = get_input(self)
        if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
            return linked.inputs["Path/Filename"].get_full_path()
        cwd = os.path.dirname(bpy.data.filepath)
        self.img_path = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.disp_path)))
        return self.img_path

    # Deal with any path components that may be in the filename and remove recognised extensions
    def update_filename(self, context):
        if self.img_name == "":
            return
        fullpath = os.path.normpath(bpy.path.abspath(self.img_name))
        path, name = os.path.split(fullpath)
        if path:
            self.disp_path = self.img_name[:-len(name)]
        if name:
            # Remove file extension if recognised
            nname, ext = os.path.splitext(name)
            if ext not in [".", "", None]:
                for enum, iext in self.img_ext:
                    if ext.lower() == iext:
                        name = nname
                        break
            if self.img_name != name:
                self.img_name = name
    
    # Return the file name with the correct image type extension (unless it has an existing unknown extension)
    def name_with_ext(self, suffix="", type=""):
        linked = get_input(self)
        if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
            return linked.inputs["Path/Filename"].name_with_ext(suffix, type)
        for enum, iext in self.img_ext:
            if type == enum:
                return (self.img_name + suffix + iext)
                
    def frame_range(self, padding=False, animated=False):
        linked = get_input(self)
        if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
            return linked.get_frames(padding, animated)
        if padding: return None
        elif animated: return False
        else: return []
                
    def get_path(self):
        linked = get_input(self)
        if linked and  linked.bl_idname == 'BakeWrangler_Input_Filenames':
            return linked.inputs["Path/Filename"].get_path()
        return self.disp_path
        
    def get_name(self):
        linked = get_input(self)
        if linked and  linked.bl_idname == 'BakeWrangler_Input_Filenames':
            return linked.inputs["Path/Filename"].get_name()
        return self.img_name
            
    # Properties
    disp_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
    img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
    img_name: bpy.props.StringProperty(name="Output File", description="File prefix to save image as", default="Image", subtype='FILE_PATH', update=update_filename)
    
    img_ext = (
        ('BMP', ".bmp"),
        ('IRIS', ".rgb"),
        ('PNG', ".png"),
        ('JPEG', ".jpg"),
        ('JPEG2000', ".jp2"),
        ('TARGA', ".tga"),
        ('TARGA_RAW', ".tga"),
        ('CINEON', ".cin"),
        ('DPX', ".dpx"),
        ('OPEN_EXR_MULTILAYER', ".exr"),
        ('OPEN_EXR', ".exr"),
        ('HDR', ".hdr"),
        ('TIFF', ".tif"),
    )
    
    def draw(self, context, layout, node, text):
        colpath = layout.column(align=True)
        linked = get_input(self)
        if node.bl_idname == 'BakeWrangler_Output_Image_Path' and linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
            colpath.label(text="Path: " + linked.inputs["Path/Filename"].get_path())
            colpath.label(text="Name: " + linked.inputs["Path/Filename"].get_name())
        elif not self.is_output:
            colpath.prop(self, "disp_path", text="")
            colpath.prop(self, "img_name", text="")
        else:
            layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
    
    def draw_color(self, context, node):
        if self.node.bl_idname == 'BakeWrangler_Input_Filenames' and not self.is_output:
            return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0))
        return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.35, 1.0, 1.0))


# Custom Nodes:

# Base class for all bakery nodes. Identifies that they belong in the bakery tree.
class BakeWrangler_Tree_Node:
    # Tree version that created the node
    tree_version: bpy.props.IntProperty(name="Tree Version", default=0)
    def init(self, context):
        self.tree_version = BW_TREE_VERSION
        
    @classmethod
    def poll(cls, ntree):
        return ntree.bl_idname == 'BakeWrangler_Tree'

    def get_name(self):
        name = self.name
        #if self.label:
        #    name += ".%s" % (self.label)
        return name

    def validate(self, inputs=False, endl=False):
        if not inputs:
            return [True]
        valid = [True]
        # Validate inputs
        has_valid_input = False
        for input in self.inputs:
            if input.islinked() and input.valid:
                input_valid = get_input(input).validate()
                valid[0] = input_valid.pop(0)
                if valid[0]:
                    has_valid_input = True
                valid += input_valid
        errs = len(valid)
        if not has_valid_input and errs < 2:
            if endl: return valid
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        return valid

    # Makes sure there is always one empty input socket at the bottom by adding and removing sockets
    def update_inputs(self, socket_type=None, socket_name=None, sub_socket_dict=None):
        if socket_type is None or self.tree_version != BW_TREE_VERSION:
            return
        idx = 0
        sub = 0
        if sub_socket_dict:
            sub = len(sub_socket_dict.keys())
        for socket in self.inputs:
            if socket.bl_idname != socket_type:
                idx = idx + 1
                continue
            if socket.is_linked or (hasattr(socket, 'value') and socket.value):
                if len(self.inputs) == idx + 1 + sub:
                    self.inputs.new(socket_type, socket_name)
                    if sub_socket_dict:
                        for key in sub_socket_dict.keys():
                            self.inputs.new(sub_socket_dict[key], key)
            else:
                if len(self.inputs) > idx + 1 + sub:
                    self.inputs.remove(socket)
                    rem = idx
                    idx = idx - 1
                    if sub_socket_dict:
                        for key in sub_socket_dict.keys():
                            self.inputs.remove(self.inputs[rem])
                            idx = idx - 1
            idx = idx + 1

    # Update inputs and links on updates
    def update(self):
        if self.tree_version != BW_TREE_VERSION:
            return
        self.update_inputs()
        # Links can get inserted without calling insert_link, but update is called.
        for socket in self.inputs:
            if socket.islinked():
                self.insert_link(socket.links[0])

    # Validate incoming links
    def insert_link(self, link):
        if link.to_node == self:
            if follow_input_link(link).from_socket.bl_idname in link.to_socket.valid_inputs() and link.is_valid:
                link.to_socket.valid = True
            else:
                link.to_socket.valid = False
    
    # Draw a double/halve button set
    def draw_double_halve(self, layout, value):
        op = layout.operator("bake_wrangler.double_val", icon='SORT_DESC', text="")
        BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value)
        op = layout.operator("bake_wrangler.double_val", icon='SORT_ASC', text="")
        BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value, True)
    
    # Draw bake button in correct state
    def draw_bake_button(self, layout, icon, label, icon_only=False, socket=-1):
        is_self = False
        baking_valid = False
        try:
            if self.id_data.baking:
                baking_valid = True
                if self.id_data.baking.node == self.name:
                    is_self = True
        except ReferenceError:
            is_self = False
            baking_valid = False

        if baking_valid:
            if is_self:
                if self.id_data.baking.stop(kill=False):
                    if icon_only:
                        stext = ""
                    else:
                        stext = "Stopping..."
                    layout.operator("bake_wrangler.dummy", icon='CANCEL', text=stext)
                else:
                    if icon_only:
                        stext = ""
                    else:
                        stext = "Cancel Bake"
                    op = layout.operator("bake_wrangler.bake_stop", icon='CANCEL', text=stext)
                    op.tree = self.id_data.name
                    op.node = self.name
                    op.sock = socket
            else:
                layout.operator("bake_wrangler.dummy", icon=icon, text=label)
        else:
            op = layout.operator("bake_wrangler.bake_pass", icon=icon, text=label)
            op.tree = self.id_data.name
            op.node = self.name
            op.sock = socket


# All settings which are not pass specific
class BakeWrangler_Settings(BakeWrangler_Tree_Node, Node):
    '''Settings node'''
    bl_label = 'Settings'
    bl_width_default = 173

    # Inputs are static (none)
    def update_inputs(self):
        pass

    # Only one of this node can be pinned at a time
    def pin_node(self, context):
        if self.pinned:
            tree = self.id_data
            for node in tree.nodes:
                if node != self and node.bl_idname == 'BakeWrangler_Settings':
                    node.pinned = False
                    
    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH')
    max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH')
    margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
    margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True)
    #mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL')
    auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False)
    acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH')
    acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179)
    material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False)
    material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material)
    material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False)
    bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False)
    
    cycles_devices = (
        ('CPU', "CPU", "Use CPU for baking"),
        ('GPU', "GPU", "Use GPU for baking"),
    )
    
    tile_sizes = (
        ('DEF', "Default", "Use Bake Wrangler default"),
        ('IMG', "Bake Size", "Use size of bake as tile size"),
        ('CUST', "Custom", "Enter your own custom tile size"),
    )

    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL')
    res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL')
    bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU')
    interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False)

    adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)
    use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False)
    the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World)
    cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False)
    cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene)
    render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL')
    use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF')
    render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024)
    use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
    bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
   
    bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1)
    bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0)
    bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False)
    bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100)
    
    # Update output nodes to display alpha input or not depending on setting
    def check_alpha(self, context):
        tree = self.id_data
        for node in tree.nodes:
            if node.bl_idname == 'BakeWrangler_Output_Image_Path':
                node.update_inputs()

    # Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings
    # for the built in image settings selector don't seem applicable to saving from script...
    img_format = (
        ('BMP', "BMP", "Output image in bitmap format."),
        ('IRIS', "Iris", "Output image in (old!) SGI IRIS format."),
        ('PNG', "PNG", "Output image in PNG format."),
        ('JPEG', "JPEG", "Output image in JPEG format."),
        ('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."),
        ('TARGA', "Targa", "Output image in Targa format."),
        ('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."),
        ('CINEON', "Cineon", "Output image in Cineon format."),
        ('DPX', "DPX", "Output image in DPX format."),
        ('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."),
        ('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."),
        ('HDR', "Radiance HDR", "Output image in Radiance HDR format."),
        ('TIFF', "TIFF", "Output image in TIFF format."),
    )

    img_color_modes = (
        ('BW', "BW", "Image saved in 8 bit grayscale"),
        ('RGB', "RGB", "Image saved with RGB (color) data"),
        ('RGBA', "RGBA", "Image saved with RGB and Alpha data"),
    )

    img_color_modes_noalpha = (
        ('BW', "BW", "Image saved in 8 bit grayscale"),
        ('RGB', "RGB", "Image saved with RGB (color) data"),
    )

    img_color_depths_8_16 = (
        ('8', "8", "8 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_8_12_16 = (
        ('8', "8", "8 bit color channels"),
        ('12', "12", "12 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_8_10_12_16 = (
        ('8', "8", "8 bit color channels"),
        ('10', "10", "10 bit color channels"),
        ('12', "12", "12 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_16_32 = (
        ('16', "Float (Half)", "16 bit color channels"),
        ('32', "Float (Full)", "32 bit color channels"),
    )

    img_codecs_jpeg2k = (
        ('JP2', "JP2", ""),
        ('J2K', "J2K", ""),
    )

    img_codecs_openexr = (
        ('DWAA', "DWAA (lossy)", ""),
        ('B44A', "B44A (lossy)", ""),
        ('ZIPS', "ZIPS (lossless)", ""),
        ('RLE', "RLE (lossless)", ""),
        ('RLE', "RLE (lossless)", ""),
        ('PIZ', "PIZ (lossless)", ""),
        ('ZIP', "ZIP (lossless)", ""),
        ('PXR24', "Pxr24 (lossy)", ""),
        ('NONE', "None", ""),
    )

    img_codecs_tiff = (
        ('PACKBITS', "Pack Bits", ""),
        ('LZW', "LZW", ""),
        ('DEFLATE', "Deflate", ""),
        ('NONE', "None", ""),
    )

    img_color_spaces = []
    for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
       img_color_spaces.append((space, space, space))
    img_color_spaces = tuple(img_color_spaces)
    
    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
    img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
    img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False)
    img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False)
    img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG')
    fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False)
    fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9)

    marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False)
    marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
    marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False)
    
    adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False)

    # Core settings
    img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces)
    img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False)
    img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha)
    img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB')

    # Color Depths
    img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8')
    img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8')
    img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8')
    img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16')

    # Compression / Quality
    img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE')
    img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE')

    # Codecs
    img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2')
    img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP')
    img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE')

    # Other random image format settings
    img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True)
    img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False)
    img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False)
    img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False)
    img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True)

    def copy(self, node):
        self.pinned = False

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN (none)
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings")
        # Prefs
        self.ray_dist = _prefs("def_raydist")
        self.max_ray_dist = _prefs("def_max_ray_dist")
        self.margin = _prefs("def_margin")
        #self.mask_margin = _prefs("def_mask_margin")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings")
        # Prefs
        self.res_bake_x = _prefs("def_xres")
        self.res_bake_y = _prefs("def_yres")
        self.bake_samples = _prefs("def_samples")
        self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0]
        self.adv_settings = _prefs("def_show_adv")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings")
        # Prefs
        self.bake_samples = _prefs("def_samples")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings")
        # Prefs
        self.img_type = self.img_format[_prefs("def_format")][0]
        self.img_xres = _prefs("def_xout")
        self.img_yres = _prefs("def_yout")
        self.adv_settings = _prefs("def_show_adv")
        self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name

    def draw_buttons(self, context, layout):
        col = layout.column(align=True)
        row = col.row(align=True)
        row.prop(self, "margin")
        row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE')
        #col.prop(self, "mask_margin")
        col.prop(self, "ray_dist")
        col.prop(self, "max_ray_dist")
        if not self.auto_cage:
            col.prop(self, "auto_cage", toggle=True)
        else:
            row = col.row(align=True)
            row.prop(self, "auto_cage", toggle=True)
            row.prop(self, "acage_expansion", text="")
            row.prop(self, "acage_smooth", text="")
        if not self.material_replace:
            col.prop(self, "material_replace", toggle=True)
        else:
            row = col.row(align=True)
            row.prop(self, "material_replace", toggle=True)
            row.prop_search(self, "material_override", bpy.data, "materials", text="")
            row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT')
        col.prop(self, "bake_mods", toggle=True)
        
        colnode = layout.column(align=False)
        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "res_bake_x", text="X")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "res_bake_y", text="Y")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y")
        #colbasic.prop(self, "bake_samples", text="Samples")
        colbasic.prop(self, "interpolate")
        
        split = colnode.split(factor=0.35)
        split.label(text="Device:")
        split.prop(self, "bake_device", text="")

        advrow = colnode.row()
        advrow.alignment = 'LEFT'
        if not self.adv_settings:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
            advrow.separator()
        else:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
            advrow.separator()
            advcol = colnode.column(align=True)

            row = advcol.row(align=True)
            row.prop(self, "use_world", text="Use My World", toggle=True)
            if self.use_world:
                row.prop_search(self, "the_world", bpy.data, "worlds", text="")

            row = advcol.row(align=True)
            row.prop(self, "cpy_render", text="Use My Settings", toggle=True)
            if self.cpy_render:
                row.prop_search(self, "cpy_from", bpy.data, "scenes", text="")
                
            row = advcol.row(align=True)
            row.prop(self, "use_tiles")
            if self.use_tiles == 'CUST':
                row.prop(self, "render_tile", text="")
                
            row = advcol.row(align=True)
            row.prop(self, "render_threads")
            
            row = advcol.row(align=True)
            row.prop(self, "use_bg_col", toggle=True)
            if self.use_bg_col:
                row.prop(self, "bg_color", text="")
        
        colnode = layout.column(align=False)
        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.label(text="Noise")
        rowbasic.prop(self, "bake_usethresh", text="")
        rowbasic.prop(self, "bake_threshold", text="")
        colbasic.prop(self, "bake_samples", text="Samples")
        colbasic.prop(self, "bake_timelimit")
        
        colnode = layout.column(align=False)
        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_xres", text="X")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_yres", text="Y")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_clear", text="Clear", toggle=True)
        rowbasic.prop(self, "img_udim", toggle=True)
        rowbasic = colbasic.row(align=True)
        if not self.fast_aa:
            rowbasic.prop(self, "fast_aa", toggle=True)
        if self.fast_aa:
            rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:")
            rowbasic.prop(self, "fast_aa_lvl", text="")

        split = colnode.split(factor=0.35)
        split.label(text="Format:")
        split.prop(self, "img_type", text="")

        advrow = colnode.row()
        advrow.alignment = 'LEFT'
        if not self.adv_settings:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
            advrow.separator()
        else:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
            advrow.separator()
            coladv = colnode.column(align=True)
            
            row = coladv.row(align=True)
            if self.marginer:
                row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF')
            else:
                row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF')
            if self.marginer:
                row_size = row.row(align=True)
                row_size.prop(self, "marginer_size", text="")
                if self.marginer_fill:
                    row_size.enabled = False
                row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT')
                
            coladv.prop(self, "img_use_float", toggle=True)

            splitadv = coladv.split(factor=0.4)
            coladvtxt = splitadv.column(align=True)
            coladvopt = splitadv.column(align=True)

            # Color Spaces
            if self.img_type != 'CINEON':
                coladvtxt.label(text="Space:")
                coladvopt.prop(self, "img_color_space", text="")
            # Color Modes
            if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
                coladvtxt.label(text="Color:")
                coladvopt.prop(self, "img_color_mode_noalpha", text="")
            if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
                coladvtxt.label(text="Color:")
                coladvopt.prop(self, "img_color_mode", text="")
            # Color Depths
            if self.img_type in ['PNG', 'TIFF']:
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_16", text="")
            if self.img_type == 'JPEG2000':
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_12_16", text="")
            if self.img_type == 'DPX':
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_10_12_16", text="")
            if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_16_32", text="")
            # Compression / Quality
            if self.img_type == 'PNG':
                coladvtxt.label(text="Compression:")
                coladvopt.prop(self, "img_compression", text="")
            if self.img_type in ['JPEG', 'JPEG2000']:
                coladvtxt.label(text="Quality:")
                coladvopt.prop(self, "img_quality", text="")
            # Codecs
            if self.img_type == 'JPEG2000':
                coladvtxt.label(text="Codec:")
                coladvopt.prop(self, "img_codec_jpeg2k", text="")
            if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
                coladvtxt.label(text="Codec:")
                coladvopt.prop(self, "img_codec_openexr", text="")
            if self.img_type == 'TIFF':
                coladvtxt.label(text="Compression:")
                coladvopt.prop(self, "img_codec_tiff", text="")
            # Other random image settings
            if self.img_type == 'JPEG2000':
                coladv.prop(self, "img_jpeg2k_cinema")
                coladv.prop(self, "img_jpeg2k_cinema48")
                coladv.prop(self, "img_jpeg2k_ycc")
            if self.img_type == 'DPX':
                coladv.prop(self, "img_dpx_log")
            if self.img_type == 'OPEN_EXR':
                coladv.prop(self, "img_openexr_zbuff")


# Node to configure pass settings, which can be pinned as global
class BakeWrangler_MeshSettings(BakeWrangler_Tree_Node, Node):
    '''Mesh settings node'''
    bl_label = 'Mesh Settings'
    bl_width_default = 173

    # Inputs are static (none)
    def update_inputs(self):
        pass

    # Only one of this node can be pinned at a time
    def pin_node(self, context):
        if self.pinned:
            tree = self.id_data
            for node in tree.nodes:
                if node != self and node.bl_idname == 'BakeWrangler_MeshSettings':
                    node.pinned = False

    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH')
    max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH')
    margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
    margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True)
    margin_auto: bpy.props.BoolProperty(name="Auto Margin", description="Automatically set margin size based on smallest dimension of texture", default=True)
    #mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL')
    auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False)
    acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH')
    acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179)
    material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False)
    material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material)
    material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False)
    bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False)
    
    def copy(self, node):
        self.pinned = False

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN (none)
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings")
        # Prefs
        self.ray_dist = _prefs("def_raydist")
        self.max_ray_dist = _prefs("def_max_ray_dist")
        self.margin = _prefs("def_margin")
        #self.mask_margin = _prefs("def_mask_margin")

    def draw_buttons(self, context, layout):
        col = layout.column(align=True)
        row = col.row(align=True)
        row.prop(self, "margin_auto", toggle=True, icon_only=True, icon='MOD_MESHDEFORM')
        mrg = row.row(align=True)
        mrg.prop(self, "margin")
        if self.margin_auto:
            mrg.enabled = False
        row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE')
        #col.prop(self, "mask_margin")
        col.prop(self, "ray_dist")
        col.prop(self, "max_ray_dist")
        if not self.auto_cage:
            col.prop(self, "auto_cage", toggle=True)
        else:
            row = col.row(align=True)
            row.prop(self, "auto_cage", toggle=True)
            row.prop(self, "acage_expansion", text="")
            row.prop(self, "acage_smooth", text="")
        if not self.material_replace:
            col.prop(self, "material_replace", toggle=True)
        else:
            row = col.row(align=True)
            row.prop(self, "material_replace", toggle=True)
            row.prop_search(self, "material_override", bpy.data, "materials", text="")
            row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT')
        col.prop(self, "bake_mods", toggle=True)


# Node to configure pass settings, which can be pinned as global
class BakeWrangler_PassSettings(BakeWrangler_Tree_Node, Node):
    '''Pass settings node'''
    bl_label = 'Pass Settings'
    bl_width_default = 144

    # Inputs are static (none)
    def update_inputs(self):
        pass

    # Only one of this node can be pinned at a time
    def pin_node(self, context):
        if self.pinned:
            tree = self.id_data
            for node in tree.nodes:
                if node != self and node.bl_idname == 'BakeWrangler_PassSettings':
                    node.pinned = False

    cycles_devices = (
        ('CPU', "CPU", "Use CPU for baking"),
        ('GPU', "GPU", "Use GPU for baking"),
    )
    
    tile_sizes = (
        ('DEF', "Default", "Use Bake Wrangler default"),
        ('IMG', "Bake Size", "Use size of bake as tile size"),
        ('CUST', "Custom", "Enter your own custom tile size"),
    )

    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL')
    res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL')
    bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU')
    interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False)

    adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)
    use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False)
    the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World)
    cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False)
    cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene)
    render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL')
    use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF')
    render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024)
    use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
    bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
    
    def copy(self, node):
        self.pinned = False

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN (none)
        self.inputs.new('BakeWrangler_Socket_SampleSetting', "Samples")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings")
        # Prefs
        self.res_bake_x = _prefs("def_xres")
        self.res_bake_y = _prefs("def_yres")
        self.bake_samples = _prefs("def_samples")
        self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0]
        self.adv_settings = _prefs("def_show_adv")

    def draw_buttons(self, context, layout):
        colnode = layout.column(align=False)

        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "res_bake_x", text="X")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "res_bake_y", text="Y")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y")
        #colbasic.prop(self, "bake_samples", text="Samples")
        colbasic.prop(self, "interpolate")
        
        split = colnode.split(factor=0.35)
        split.label(text="Device:")
        split.prop(self, "bake_device", text="")

        advrow = colnode.row()
        advrow.alignment = 'LEFT'
        if not self.adv_settings:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
            advrow.separator()
        else:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
            advrow.separator()
            advcol = colnode.column(align=True)

            row = advcol.row(align=True)
            row.prop(self, "use_world", text="Use My World", toggle=True)
            if self.use_world:
                row.prop_search(self, "the_world", bpy.data, "worlds", text="")

            row = advcol.row(align=True)
            row.prop(self, "cpy_render", text="Use My Settings", toggle=True)
            if self.cpy_render:
                row.prop_search(self, "cpy_from", bpy.data, "scenes", text="")
                
            row = advcol.row(align=True)
            row.prop(self, "use_tiles")
            if self.use_tiles == 'CUST':
                row.prop(self, "render_tile", text="")
                
            row = advcol.row(align=True)
            row.prop(self, "render_threads")
            
            row = advcol.row(align=True)
            row.prop(self, "use_bg_col", toggle=True)
            if self.use_bg_col:
                row.prop(self, "bg_color", text="")
            


# Node to configure pass settings, which can be pinned as global
class BakeWrangler_SampleSettings(BakeWrangler_Tree_Node, Node):
    '''Pass settings node'''
    bl_label = 'Sample Settings'
    bl_width_default = 144

    # Inputs are static (none)
    def update_inputs(self):
        pass

    # Only one of this node can be pinned at a time
    def pin_node(self, context):
        if self.pinned:
            tree = self.id_data
            for node in tree.nodes:
                if node != self and node.bl_idname == 'BakeWrangler_SampleSettings':
                    node.pinned = False

    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1)
    bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0)
    bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False)
    bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100)
    
    def copy(self, node):
        self.pinned = False

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN (none)
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings")
        # Prefs
        self.bake_samples = _prefs("def_samples")
        
    def draw_buttons(self, context, layout):
        colnode = layout.column(align=False)

        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.label(text="Noise")
        rowbasic.prop(self, "bake_usethresh", text="")
        rowbasic.prop(self, "bake_threshold", text="")
        colbasic.prop(self, "bake_samples", text="Samples")
        colbasic.prop(self, "bake_timelimit")        
        
        
# Node to configure output settings, which can be pinned as global
class BakeWrangler_OutputSettings(BakeWrangler_Tree_Node, Node):
    '''Output settings node'''
    bl_label = 'Output Settings'
    bl_width_default = 152

    # Inputs are static (none)
    def update_inputs(self):
        pass

    # Only one of this node can be pinned at a time
    def pin_node(self, context):
        if self.pinned:
            tree = self.id_data
            for node in tree.nodes:
                if node != self and node.bl_idname == 'BakeWrangler_OutputSettings':
                    node.pinned = False
                    
    # Update output nodes to display alpha input or not depending on setting
    def check_alpha(self, context):
        tree = self.id_data
        for node in tree.nodes:
            if node.bl_idname == 'BakeWrangler_Output_Image_Path':
                node.update_inputs()

    # Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings
    # for the built in image settings selector don't seem applicable to saving from script...
    img_format = (
        ('BMP', "BMP", "Output image in bitmap format."),
        ('IRIS', "Iris", "Output image in (old!) SGI IRIS format."),
        ('PNG', "PNG", "Output image in PNG format."),
        ('JPEG', "JPEG", "Output image in JPEG format."),
        ('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."),
        ('TARGA', "Targa", "Output image in Targa format."),
        ('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."),
        ('CINEON', "Cineon", "Output image in Cineon format."),
        ('DPX', "DPX", "Output image in DPX format."),
        ('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."),
        ('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."),
        ('HDR', "Radiance HDR", "Output image in Radiance HDR format."),
        ('TIFF', "TIFF", "Output image in TIFF format."),
    )

    img_color_modes = (
        ('BW', "BW", "Image saved in 8 bit grayscale"),
        ('RGB', "RGB", "Image saved with RGB (color) data"),
        ('RGBA', "RGBA", "Image saved with RGB and Alpha data"),
    )

    img_color_modes_noalpha = (
        ('BW', "BW", "Image saved in 8 bit grayscale"),
        ('RGB', "RGB", "Image saved with RGB (color) data"),
    )

    img_color_depths_8_16 = (
        ('8', "8", "8 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_8_12_16 = (
        ('8', "8", "8 bit color channels"),
        ('12', "12", "12 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_8_10_12_16 = (
        ('8', "8", "8 bit color channels"),
        ('10', "10", "10 bit color channels"),
        ('12', "12", "12 bit color channels"),
        ('16', "16", "16 bit color channels"),
    )

    img_color_depths_16_32 = (
        ('16', "Float (Half)", "16 bit color channels"),
        ('32', "Float (Full)", "32 bit color channels"),
    )

    img_codecs_jpeg2k = (
        ('JP2', "JP2", ""),
        ('J2K', "J2K", ""),
    )

    img_codecs_openexr = (
        ('DWAA', "DWAA (lossy)", ""),
        ('B44A', "B44A (lossy)", ""),
        ('ZIPS', "ZIPS (lossless)", ""),
        ('RLE', "RLE (lossless)", ""),
        ('RLE', "RLE (lossless)", ""),
        ('PIZ', "PIZ (lossless)", ""),
        ('ZIP', "ZIP (lossless)", ""),
        ('PXR24', "Pxr24 (lossy)", ""),
        ('NONE', "None", ""),
    )

    img_codecs_tiff = (
        ('PACKBITS', "Pack Bits", ""),
        ('LZW', "LZW", ""),
        ('DEFLATE', "Deflate", ""),
        ('NONE', "None", ""),
    )

    img_color_spaces = []
    for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
       img_color_spaces.append((space, space, space))
    img_color_spaces = tuple(img_color_spaces)
    
    # Props
    pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
    img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
    img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
    img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False)
    img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False)
    img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG')
    fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False)
    fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9)

    marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False)
    marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
    marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False)
    
    adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False)

    # Core settings
    img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces)
    img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False)
    img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha)
    img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB')
    img_non_color: bpy.props.StringProperty(name="Non Color Space", default="NONE")
    
    # Color Depths
    img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8')
    img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8')
    img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8')
    img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16')

    # Compression / Quality
    img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE')
    img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE')

    # Codecs
    img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2')
    img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP')
    img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE')

    # Other random image format settings
    img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True)
    img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False)
    img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False)
    img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False)
    img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True)

    def copy(self, node):
        self.pinned = False

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN (none)
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings")
        # Prefs
        self.img_type = self.img_format[_prefs("def_format")][0]
        self.img_xres = _prefs("def_xout")
        self.img_yres = _prefs("def_yout")
        self.adv_settings = _prefs("def_show_adv")
        self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name

    def draw_buttons(self, context, layout):
        colnode = layout.column(align=False)

        colbasic = colnode.column(align=True)
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_xres", text="X")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_yres", text="Y")
        BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres")
        rowbasic = colbasic.row(align=True)
        rowbasic.prop(self, "img_clear", text="Clear", toggle=True)
        rowbasic.prop(self, "img_udim", toggle=True)
        rowbasic = colbasic.row(align=True)
        if not self.fast_aa:
            rowbasic.prop(self, "fast_aa", toggle=True)
        if self.fast_aa:
            rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:")
            rowbasic.prop(self, "fast_aa_lvl", text="")

        split = colnode.split(factor=0.35)
        split.label(text="Format:")
        split.prop(self, "img_type", text="")

        advrow = colnode.row()
        advrow.alignment = 'LEFT'
        if not self.adv_settings:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
            advrow.separator()
        else:
            advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
            advrow.separator()
            coladv = colnode.column(align=True)
            
            row = coladv.row(align=True)
            if self.marginer:
                row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF')
            else:
                row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF')
            if self.marginer:
                row_size = row.row(align=True)
                row_size.prop(self, "marginer_size", text="")
                if self.marginer_fill:
                    row_size.enabled = False
                row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT')
                
            coladv.prop(self, "img_use_float", toggle=True)

            splitadv = coladv.split(factor=0.4)
            coladvtxt = splitadv.column(align=True)
            coladvopt = splitadv.column(align=True)

            # Color Spaces
            if self.img_type != 'CINEON':
                coladvtxt.label(text="Space:")
                coladvopt.prop(self, "img_color_space", text="")
            # Color Modes
            if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
                coladvtxt.label(text="Color:")
                coladvopt.prop(self, "img_color_mode_noalpha", text="")
            if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
                coladvtxt.label(text="Color:")
                coladvopt.prop(self, "img_color_mode", text="")
            # Color Depths
            if self.img_type in ['PNG', 'TIFF']:
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_16", text="")
            if self.img_type == 'JPEG2000':
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_12_16", text="")
            if self.img_type == 'DPX':
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_8_10_12_16", text="")
            if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
                coladvtxt.label(text="Depth:")
                coladvopt.prop(self, "img_color_depth_16_32", text="")
            # Compression / Quality
            if self.img_type == 'PNG':
                coladvtxt.label(text="Compression:")
                coladvopt.prop(self, "img_compression", text="")
            if self.img_type in ['JPEG', 'JPEG2000']:
                coladvtxt.label(text="Quality:")
                coladvopt.prop(self, "img_quality", text="")
            # Codecs
            if self.img_type == 'JPEG2000':
                coladvtxt.label(text="Codec:")
                coladvopt.prop(self, "img_codec_jpeg2k", text="")
            if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
                coladvtxt.label(text="Codec:")
                coladvopt.prop(self, "img_codec_openexr", text="")
            if self.img_type == 'TIFF':
                coladvtxt.label(text="Compression:")
                coladvopt.prop(self, "img_codec_tiff", text="")
            # Other random image settings
            if self.img_type == 'JPEG2000':
                coladv.prop(self, "img_jpeg2k_cinema")
                coladv.prop(self, "img_jpeg2k_cinema48")
                coladv.prop(self, "img_jpeg2k_ycc")
            if self.img_type == 'DPX':
                coladv.prop(self, "img_dpx_log")
            if self.img_type == 'OPEN_EXR':
                coladv.prop(self, "img_openexr_zbuff")



# File names input to allow attaching prefixes to outputs and make object->filename system more intuitive
class BakeWrangler_Input_Filenames(BakeWrangler_Tree_Node, Node):
    '''File Names node'''
    bl_label = 'File Names'
    bl_width_default = 198
    
    def get_names(self):
        names = []
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_ObjectNames' and get_input(input):
                names.append(get_input(input))
        return names
    
    def get_frames(self, padding=False, animated=False):
        if animated: return self.use_rnd_seed
        frames = []
        pad = None
        # Parse string
        ranges = self.frame_ranges.split(sep=",")
        import re
        extract = re.compile(r'(\D*#([0-9]*)\D*)|(\D*([0-9]*)-?([0-9]*):?([0-9]*)\D*)')
        for arange in ranges:
            match = extract.match(arange)
            if match.group(1) and padding:
                pad = int(match.group(2))
            elif match.group(3) and not padding:
                start = int(match.group(4))
                end = int(match.group(5)) if match.group(5) else None
                step = int(match.group(6)) if match.group(6) else 1
                if end:
                    if end < start:
                        step *= -1
                        end -= 1
                    else: end +=1
                    for f in range(start, end, step): frames.append(f)
                else:
                    frames.append(start)
            else: continue
        if padding:
            return pad
        return set(frames)

    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_ObjectNames', "Object Names")
        
    # Props
    frame_ranges: bpy.props.StringProperty(name="Frame Ranges", description="Comma separated list of frame ranges to bake (eg: 1,3,4-12). For non-default zero padding include #number_of_zeros as one of your ranges (eg: #3,1-3,10 to pad all numbers to 3). Frame numbers are added to the end of file names", default="")
    use_rnd_seed: bpy.props.BoolProperty(name="Use Animated Seed", description="Use different seed values (and hence noise patterns) at different frames", default=True)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_SplitOutput', "Path/Filename")
        self.inputs.new('BakeWrangler_Socket_ObjectNames', "Object Names")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_SplitOutput', "Path / Filenames / Frames")
        
    def draw_buttons(self, context, layout):
        col = layout.column(align=True)
        row0 = col.row()
        row0.label(text="Frame Ranges:")
        row1 = col.row(align=True)
        row1.prop(self, "frame_ranges", text="")
        row1.prop(self, "use_rnd_seed", text="", icon='TIME')


    
# Input node that contains a list of objects relevant to baking
class BakeWrangler_Input_ObjectList(BakeWrangler_Tree_Node, Node):
    '''Object list node'''
    bl_label = 'Objects'
    bl_width_default = 198

    # Makes sure there is always one empty input socket at the bottom by adding and removing sockets
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Object', "Object")

    # Determine if object meets current input filter
    def input_filter(self, input_name, object):
        if self.filter_collection:
            if object.rna_type.identifier == 'Collection':
                return True
        elif object.rna_type.identifier == 'Object':
            if (self.filter_mesh and object.type == 'MESH') or \
               (self.filter_curve and object.type == 'CURVE') or \
               (self.filter_surface and object.type == 'SURFACE') or \
               (self.filter_meta and object.type == 'META') or \
               (self.filter_font and object.type == 'FONT') or \
               (self.filter_light and object.type == 'LIGHT'):
                return True
        return False

    # Get all objects in tree from this node (mostly just uses the sockets methods)
    def get_objects(self, only_mesh=False, no_lights=False, only_groups=False):
        objects = []
        for input in self.inputs:
            in_objs = input.get_objects(only_mesh, no_lights, only_groups)
            if len(in_objs):
                objects += in_objs
        return objects

    # Validate all objects in tree from this node (mostly just uses the sockets methods)
    def validate(self, check_materials=False, check_as_active=False, check_multi=False):
        valid = [True]
        for input in self.inputs:
            valid_input = input.validate(check_materials, check_as_active, check_multi)
            if not valid_input.pop(0):
                valid[0] = False
            if len(valid_input):
                valid += valid_input
        return valid

    filter_mesh: bpy.props.BoolProperty(name="Meshes", description="Show mesh type objects", default=True)
    filter_curve: bpy.props.BoolProperty(name="Curves", description="Show curve type objects", default=True)
    filter_surface: bpy.props.BoolProperty(name="Surfaces", description="Show surface type objects", default=True)
    filter_meta: bpy.props.BoolProperty(name="Metas", description="Show meta type objects", default=True)
    filter_font: bpy.props.BoolProperty(name="Fonts", description="Show font type objects", default=True)
    filter_light: bpy.props.BoolProperty(name="Lights", description="Show light type objects", default=True)
    filter_collection: bpy.props.BoolProperty(name="Collections", description="Toggle only collections", default=False)
    
    def copy(self, node):
        self.inputs.clear()
        for sok in node.inputs:
            csok = self.inputs.new('BakeWrangler_Socket_Object', "Object")
            csok.value = sok.value
            csok.type = sok.type
            csok.recursive = sok.recursive
            csok.pick_uv = sok.pick_uv
            csok.uv_map = sok.uv_map
            csok.use_cage = sok.use_cage
            csok.cage = sok.cage

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Object', "Object")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Object', "Objects")
        # Prefs
        self.filter_mesh = _prefs("def_filter_mesh")
        self.filter_curve = _prefs("def_filter_curve")
        self.filter_surface = _prefs("def_filter_surface")
        self.filter_meta = _prefs("def_filter_meta")
        self.filter_font = _prefs("def_filter_font")
        self.filter_light = _prefs("def_filter_light")
        self.filter_collection = _prefs("def_filter_collection")

    def draw_buttons(self, context, layout):
        row = layout.row(align=True)
        row0 = row.row()
        row0.label(text="Filter:")

        row1 = row.row(align=True)
        row1.alignment = 'RIGHT'
        for fltr in BakeWrangler_Operator_FilterToggle.filters:
            icn = fltr.split("_")[1].upper() + "_DATA"
            op = row1.operator("bake_wrangler.filter_toggle", icon=icn, text="", depress=getattr(self, fltr))
            op.tree = self.id_data.name
            op.node = self.name
            op.filter = fltr
        if self.filter_collection:
            row1.enabled = False

        row2 = row.row(align=False)
        row2.alignment = 'RIGHT'
        row2.prop(self, "filter_collection", text="", icon='GROUP')



# Automatic sorting of meshes into groups for baking
class BakeWrangler_Sort_Meshes(BakeWrangler_Tree_Node, Node):
    '''Sorting and grouping of meshes by name'''
    bl_label = 'Auto Sort Meshes'
    bl_width_default = 240
    
    # Inputs are static on this node
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh")
    
    # Try to get nodes settings from local or global node
    def get_settings(self, validating=False, input=None):
        if input:
            settings = get_input(input)
            if settings:
                return settings.get_settings()
        return {}
    
    # Check node settings are valid to bake. Returns true/false, plus error message.
    def validate(self, check_materials=False, multires=False):
        valid = [True]
        has_valid_input = False
        for inpt in self.inputs:
            if inpt.islinked() and inpt.valid:
                mesh = get_input(inpt)
                if mesh:
                    input_valid = mesh.validate(check_materials, multires)
                    if not input_valid.pop(0):
                        valid[0] = False
                    else:
                        has_valid_input = True
                    if len(input_valid):
                        valid += input_valid
        if not has_valid_input and len(valid) < 2:
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        return valid
    
    # Return name with ident removed or empty string if ident not in name
    def find_ident(self, type, name, ident, case_sens=True):
        if type == 'PASS' or not ident:
            return name
        ident_idx = 0
        ident_len = len(ident)
        cname = name
        cident = ident
        if not case_sens:
            cname = name.lower()
            cident = ident.lower()
        if type == 'STARTS':
            if not cname.startswith(cident):
                return ''
            else: return name[ident_len:]
        elif type == 'ENDS':
            if not cname.endswith(cident):
                return ''
            else: return name[:-ident_len]
        elif type in ['SCONTAINS', 'ECONTAINS']:
            if type == 'SCONTAINS':
                ident_idx = cname.find(cident)
            if type == 'ECONTAINS':
                ident_idx = cname.rfind(cident)
            if ident_idx == -1:
                return ''
            else: return name[:ident_idx] + name[ident_idx + ident_len:]
        
    # Return a list of object pairings
    def get_objects(self, set, input):
        objs = []
        mesh = get_input(input)
        if not mesh:
            return []
        if set == 'TARGET' and 'Target' in mesh.inputs.keys():
            targets = prune_objects(mesh.inputs["Target"].get_objects(only_mesh=True), True)
            sources = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True))
            sourgrp = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True, only_groups=True))
            scenes  = prune_objects(mesh.inputs["Scene"].get_objects())
            scengrp = prune_objects(mesh.inputs["Scene"].get_objects(only_groups=True))
            if not len(sources) and self.high_search != 'PASS':
                sources = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True))
                sourgrp = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True, only_groups=True))
            
            # Create pairings of high to low, first ident possible low polys
            for obj in targets:
                low_name = self.find_ident(self.low_search, obj[0].name, self.low_string, self.low_case)
                high_obj = []
                scen_obj = []
                if not low_name: continue
                # Create the high poly name string based on the possible low objects name
                if self.high_search == 'PASS':
                    high_obj = sources
                else:
                    if self.high_collect:
                        for high in sourgrp:
                            high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case)
                            if high_name == low_name:
                                high_obj += high[1]
                    if not len(high_obj):
                        for high in sources:
                            high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case)
                            if high_name == low_name:
                                high_obj.append(high)
                # Check scene objects (if no scene string is set, scene is included as is)
                if self.scene_search == 'PASS':
                    scen_obj = scenes
                else:
                    if self.scene_collect:
                        for scen in scengrp:
                            scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case)
                            if scen_name == low_name:
                                scen_obj += scen[1]
                    if not len(scen_obj):
                        for scen in scenes:
                            scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case)
                            if scen_name == low_name:
                                scen_obj.append(scen)
                # Only paired objects will be added
                if (len(high_obj) and self.high_search != 'PASS') or (len(scen_obj) and self.scene_search != 'PASS') or self.low_search == 'PASS':
                    objs.append([obj, high_obj, scen_obj, low_name])
        else: pass

        # Return pruned object list
        return objs
    
    # Get a list of unique objects used as either source or target
    def get_unique_objects(self, type, for_auto_cage=False, input=None):
        if type not in ['TARGET', 'SOURCE']:
            return []
        objs_set = []
        objs = self.get_objects('TARGET', input)
        if len(objs):
            if for_auto_cage:
                settings = self.get_settings(input=input)
                if not settings['auto_cage']:
                    return []
            for obj in objs:
                if type == 'TARGET':
                    if for_auto_cage:
                        if len(obj[0]) > 2 and obj[0][2]:
                            continue
                        objs_set.append([obj[0][0], settings['acage_expansion'], settings['acage_smooth']])
                    else:
                        objs_set.append(obj[0])
                elif type == 'SOURCE':
                    objs_set.append(obj[1])
                else:
                    return []
        return objs_set        
    
    search_type = (
        ('PASS', "Pass Through", "Pass all items through without doing any matching", 'ANIM', 0),
        ('STARTS', "Starts", "Starts with ID", 'TRIA_RIGHT', 1),
        ('ENDS', "Ends", "Ends with ID", 'TRIA_LEFT', 2),
        ('SCONTAINS', "Contains ->", "Contains ID, searching from start", 'NEXT_KEYFRAME', 3),
        ('ECONTAINS', "Contains <-", "Contains ID, searching from end", 'PREV_KEYFRAME', 4),
    )
    
    low_search: bpy.props.EnumProperty(name="Target Search", description="How to search for target identifier", items=search_type, default='PASS')
    low_string: bpy.props.StringProperty(name="Target Ident", description="Target identifier")
    low_case: bpy.props.BoolProperty(name="Target case sensitivity", description="Use case sensitive matching", default=True)
    high_string: bpy.props.StringProperty(name="Source Ident", description="Source identifier")
    high_search: bpy.props.EnumProperty(name="Source Search", description="How to search for source identifier", items=search_type, default='PASS')
    high_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a source collection before trying the items inside it", default=True)
    high_case: bpy.props.BoolProperty(name="Source case sensitivity", description="Use case sensitive matching", default=True)
    scene_string: bpy.props.StringProperty(name="Scene Ident", description="Scene identifier")
    scene_search: bpy.props.EnumProperty(name="Scene Search", description="How to search for scene identifier", items=search_type, default='PASS')
    scene_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a scene collection before trying the items inside it", default=True)
    scene_case: bpy.props.BoolProperty(name="Scene case sensitivity", description="Use case sensitive matching", default=True)
    
    show_groupings: bpy.props.BoolProperty(name="Show Groupings", default=False)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")
    
    def draw_buttons(self, context, layout):
        colnode = layout.column(align=True)
        split_fac = 60 / self.width
        split = colnode.split(factor=split_fac)
        split.label(text="Target ID:")
        row = split.row(align=True)
        row1 = row.row(align=True)
        row2 = row.row(align=True)
        row1.prop(self, "low_string", text="")
        row1.prop(self, "low_case", text="", icon_only=True, icon='SMALL_CAPS')
        row2.prop(self, "low_search", text="", icon_only=True)
        if self.low_search == 'PASS':
            row1.enabled = False
        
        split = colnode.split(factor=split_fac)
        split.label(text="Source ID:")
        row = split.row(align=True)
        row1 = row.row(align=True)
        row2 = row.row(align=True)
        row1.prop(self, "high_string", text="")
        row1.prop(self, "high_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION')
        row1.prop(self, "high_case", text="", icon_only=True, icon='SMALL_CAPS')
        row2.prop(self, "high_search", text="", icon_only=True)
        if self.high_search == 'PASS':
            row1.enabled = False
        
        split = colnode.split(factor=split_fac)
        split.label(text="Scene ID:")
        row = split.row(align=True)
        row1 = row.row(align=True)
        row2 = row.row(align=True)
        row1.prop(self, "scene_string", text="")
        row1.prop(self, "scene_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION')
        row1.prop(self, "scene_case", text="", icon_only=True, icon='SMALL_CAPS')
        row2.prop(self, "scene_search", text="", icon_only=True)
        if self.scene_search == 'PASS':
            row1.enabled = False
            
    def draw_buttons_ext(self, context, layout):
        layout.prop(self, 'show_groupings')
        if self.show_groupings:
            for input in self.inputs:
                mesh = get_input(input)
                if mesh:
                    layout.label(text=mesh.get_name())
                    box = layout.box()
                    for obj_grp in self.get_objects('TARGET', input):
                        boxin = box.box()
                        col = boxin.column(align=True)
                        row = col.row()
                        row.label(text=obj_grp[0][0].name)
                        row.label(text="", icon=obj_grp[0][0].type + '_DATA')
                        col.label(text="-Source:")
                        for hi in obj_grp[1]:
                            row = col.row()
                            row.label(text="  " + hi[0].name)
                            row.label(text="", icon=hi[0].type + '_DATA')
                        col.label(text="-Scene:")
                        for scen in obj_grp[2]:
                            row = col.row()
                            row.label(text="  " + scen[0].name)
                            row.label(text="", icon=scen[0].type + '_DATA')



# Settings to be used when baking a billboard
class BakeWrangler_Bake_Billboard(BakeWrangler_Tree_Node, Node):
    '''Mesh input node'''
    bl_label = 'Input Billboard'
    bl_width_default = 240

    # Inputs are static on this node
    def update_inputs(self):
        pass

    # Determine if object meets current input filter
    def input_filter(self, input_name, object):
        if input_name == "Target":
            if object.type == 'MESH':
                return True
        elif input_name == "Source":
            if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']:
                return True
        return False

    # Try to get nodes settings from local or global node
    def get_settings(self, validating=False):
        settings = get_input(self.inputs["Settings"])
        if not settings:
            settings = self.id_data.get_pinned_settings("MeshSettings")
        if validating:
            return settings
        mesh_settings = {}
        mesh_settings['ray_dist']           = 0
        mesh_settings['max_ray_dist']       = 0
        mesh_settings['margin']             = settings.margin
        mesh_settings['margin_extend']      = settings.margin_extend
        mesh_settings['margin_auto']        = settings.margin_auto
        #mesh_settings['marginer']           = settings.marginer
        #mesh_settings['marginer_fill']      = settings.marginer_fill
        #mesh_settings['mask_margin']        = settings.mask_margin
        mesh_settings['auto_cage']          = False
        mesh_settings['acage_expansion']    = 0
        mesh_settings['acage_smooth']       = 0
        mesh_settings['material_replace']   = False
        mesh_settings['material_override']  = None
        mesh_settings['material_osl']       = False
        mesh_settings['bake_mods']          = False
        mesh_settings['bake_mods_invert']   = False
        mesh_settings['alpha_bounce']       = self.alpha_bounce
        return mesh_settings

    # Check node settings are valid to bake. Returns true/false, plus error message.
    def validate(self, check_materials=False, multires=False):
        valid = [True]
        # Check settings are set somewhere
        if not self.get_settings(validating=True):
            valid[0] = False
            valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"])
        # Check source objects
        has_selected = False
        if not multires:
            has_selected = len(self.inputs["Source"].get_objects()) > 0
        if has_selected and check_materials:
            valid_selected = self.inputs["Source"].validate(check_materials)
            # Add any generated messages to the stack. Material errors wont stop bake
            if len(valid_selected) > 1:
                valid_selected.pop(0)
                valid += valid_selected
        # Check target meshes
        has_active = len(self.inputs["Target"].get_objects(True)) > 0
        if has_active:
            valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires)
            valid[0] = valid_active.pop(0)
            # Add any generated messages to the stack. Errors here will stop bake
            if len(valid_active):
                valid += valid_active
        else:
            valid[0] = False
            valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"])
        return valid

    # Return the requested set of objects from the appropriate input socket
    def get_objects(self, set):
        #if _prefs("debug"): _print("Getting objects in %s" % (set))
        if set == 'TARGET':
            objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True)
        elif set == 'SOURCE':
            objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True))
        elif set == 'SCENE':
            objs = []
        # Return pruned object list
        return objs
    
    # Get a list of unique objects used as either source or target
    def get_unique_objects(self, type, for_auto_cage=False):
        if for_auto_cage:
            return []
        objs = self.get_objects(type)
        return objs
        
    alpha_bounce: bpy.props.IntProperty(name="Alpha Bounces", description="Number of times a ray can pass through transparent surfaces", default=3, min=1)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings")
        self.inputs.new('BakeWrangler_Socket_Object', "Target")
        self.inputs.new('BakeWrangler_Socket_Object', "Source")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")

    def draw_buttons(self, context, layout):
        layout.prop(self, "alpha_bounce")


# Bake materials as a texture by projecting them on a plane
class BakeWrangler_Bake_Material(BakeWrangler_Tree_Node, Node):
    '''Material input node'''
    bl_label = 'Input Material'
    bl_width_default = 240
    
    # Makes sure there is always one empty input socket at the bottom by adding and removing sockets
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Material', "Material")
    
    # Hard coded settings as the values are either not used or have only one meaningful possible value for this node
    def get_settings(self, validating=False):
        mesh_settings = {}
        mesh_settings['ray_dist']           = 0
        mesh_settings['max_ray_dist']       = 0
        mesh_settings['margin']             = 0
        mesh_settings['margin_extend']      = False
        mesh_settings['margin_auto']        = False
        #mesh_settings['marginer']           = False
        #mesh_settings['mask_margin']        = 0
        mesh_settings['auto_cage']          = False
        mesh_settings['acage_expansion']    = 0
        mesh_settings['acage_smooth']       = 0
        mesh_settings['material_replace']   = False
        mesh_settings['material_override']  = None
        mesh_settings['material_osl']       = False
        mesh_settings['bake_mods']          = False
        mesh_settings['bake_mods_invert']   = False
        mesh_settings['matbk_width']        = self.mat_width
        mesh_settings['matbk_height']       = self.mat_height
        return mesh_settings
        
    # Check node settings are valid to bake. Returns true/false, plus error message.
    def validate(self, check_materials=False, multires=False):
        valid = [True]
        mats = self.get_materials()
        # Check is has some materials
        if not len(mats):
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs"])
            return valid
        # Check the materials can be baked if required
        if check_materials:
            for mat in mats:
                # Is node based?
                if not mat[0].node_tree or not mat[0].node_tree.nodes:
                    valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)])
                    continue
                # Is a 'principled' material?
                passed = False
                for node in mat[0].node_tree.nodes:
                    if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']:
                        if material_recursor(node):
                            passed = True
                            break
                if not passed:
                    valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix shaders. Baked values will not be correct for this material." % (mat.name)])
        return valid            
    
    # Create a list of unique materials from node inputs
    def get_materials(self):
        materials = []
        # First collect any objects in the object input
        objs = self.inputs[0].get_objects(no_lights=True)
        # If there are any objects, collect their materials
        for obj in objs:
            for mat in obj[0].data.materials:
                materials.append([mat])
        # Next just add materials from the remaining inputs
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Material':
                if input.value is not None:
                    materials.append([input.value])
        # Now prune out any duplicates
        return prune_objects(materials)
    
    mat_width: bpy.props.FloatProperty(name="Width", description="Width of plane to project material on", precision=3, unit='LENGTH', default=1, min=0)
    mat_height: bpy.props.FloatProperty(name="Height", description="Height of plane to project material on", precision=3, unit='LENGTH', default=1, min=0)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Object', "Materials from Objects")
        self.inputs.new('BakeWrangler_Socket_Material', "Material")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Material', "Material")
        
    def draw_buttons(self, context, layout):
        col = layout.column(align=True)
        col.prop(self, "mat_width")
        col.prop(self, "mat_height")


# Mesh settings to be used when baking attached objects
class BakeWrangler_Bake_Mesh(BakeWrangler_Tree_Node, Node):
    '''Mesh input node'''
    bl_label = 'Input Mesh'
    bl_width_default = 240

    # Inputs are static on this node
    def update_inputs(self):
        pass

    # Determine if object meets current input filter
    def input_filter(self, input_name, object):
        if input_name == "Target":
            if object.type == 'MESH':
                return True
        elif input_name == "Source":
            if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']:
                return True
        elif input_name == "Scene":
            if object.rna_type.identifier == 'Collection':
                return True
        return False

    # Try to get nodes settings from local or global node
    def get_settings(self, validating=False):
        settings = get_input(self.inputs["Settings"])
        if not settings:
            settings = self.id_data.get_pinned_settings("MeshSettings")
        if validating:
            return settings
        mesh_settings = {}
        mesh_settings['ray_dist']           = settings.ray_dist
        mesh_settings['max_ray_dist']       = settings.max_ray_dist
        mesh_settings['margin']             = settings.margin
        mesh_settings['margin_extend']      = settings.margin_extend
        mesh_settings['margin_auto']        = settings.margin_auto
        #mesh_settings['marginer']           = settings.marginer
        #mesh_settings['marginer_fill']      = settings.marginer_fill
        #mesh_settings['mask_margin']        = settings.mask_margin
        mesh_settings['auto_cage']          = settings.auto_cage
        mesh_settings['acage_expansion']    = settings.acage_expansion
        mesh_settings['acage_smooth']       = settings.acage_smooth
        mesh_settings['material_replace']   = settings.material_replace
        mesh_settings['material_override']  = settings.material_override
        mesh_settings['material_osl']       = settings.material_osl
        mesh_settings['bake_mods']          = settings.bake_mods
        mesh_settings['bake_mods_invert']   = _prefs("invert_bakemod")
        if self.view_from == 'CAM' and self.view_cam:
            mesh_settings['view_from']      = self.view_cam
        return mesh_settings

    # Check node settings are valid to bake. Returns true/false, plus error message.
    def validate(self, check_materials=False, multires=False):
        valid = [True]
        # Check settings are set somewhere
        if not self.get_settings(validating=True):
            valid[0] = False
            valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"])
        # Check source objects
        has_selected = False
        if not multires:
            has_selected = len(self.inputs["Source"].get_objects()) > 0
        if has_selected and check_materials:
            valid_selected = self.inputs["Source"].validate(check_materials)
            # Add any generated messages to the stack. Material errors wont stop bake
            if len(valid_selected) > 1:
                valid_selected.pop(0)
                valid += valid_selected
        # Check target meshes
        has_active = len(self.inputs["Target"].get_objects(True)) > 0
        if has_active:
            valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires)
            valid[0] = valid_active.pop(0)
            # Add any generated messages to the stack. Errors here will stop bake
            if len(valid_active):
                valid += valid_active
        else:
            valid[0] = False
            valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"])
        return valid

    # Return the requested set of objects from the appropriate input socket
    def get_objects(self, set):
        #if _prefs("debug"): _print("Getting objects in %s" % (set))
        if set == 'TARGET':
            objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True)
        elif set == 'SOURCE':
            objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True))
        elif set == 'SCENE':
            objs = prune_objects(self.inputs["Scene"].get_objects())
        # Return pruned object list
        return objs
    
    # Get a list of unique objects used as either source or target
    def get_unique_objects(self, type, for_auto_cage=False):
        if for_auto_cage:
            settings = self.get_settings()
            if not settings['auto_cage']:
                return []
        objs = self.get_objects(type)
        if for_auto_cage:
            objs_cage = []
            for obj in objs:
                if len(obj) > 2 and obj[2]:
                    continue
                else:
                    objs_cage.append([obj[0], settings['acage_expansion'], settings['acage_smooth']])
            return objs_cage
        return objs
    
    view_orig = (
        ('ABV', "Above Surface", "Cast rays from above surface"),
        ('CAM', "Camera", "Cast rays from camera location"),
    )
    
    view_from: bpy.props.EnumProperty(name="View from", description="Ray casting origin (where applicable)", items=view_orig, default='ABV')
    view_cam: bpy.props.PointerProperty(name="View camera", description="Camera to use for ray origins", type=bpy.types.Object)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings")
        self.inputs.new('BakeWrangler_Socket_Object', "Target")
        self.inputs.new('BakeWrangler_Socket_Object', "Source")
        self.inputs.new('BakeWrangler_Socket_Object', "Scene")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")

    def draw_buttons(self, context, layout):
        row0 = layout.row(align=True)
        row1 = row0.row(align=True)
        row2 = row0.row(align=True)
        
        row1.prop(self, "view_from", text="View")
        row2.prop_search(self, "view_cam", bpy.data, "objects", text="", icon='CAMERA_DATA')
        
        if self.view_from == 'CAM':
            row2.enabled = True
        else:
            row2.enabled = False


# Baking node that holds all the settings for a type of bake 'pass'. Takes one or more mesh input nodes as input.
class BakeWrangler_Bake_Pass(BakeWrangler_Tree_Node, Node):
    '''Baking pass node'''
    bl_label = 'Bake Pass'
    bl_width_default = 160

    # Returns the most identifing string for the node
    def get_name(self):
        name = BakeWrangler_Tree_Node.get_name(self)
        if self.bake_picked:
            name += " (%s)" % (self.bake_picked)
        return name

    # Makes sure there is always one empty input socket at the bottom by adding and removing sockets
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh")

    # Update node label based on selected pass
    def update_pass(self, context):
        if self.bake_cat == 'PBR':
            pass_enum = self.passes_pbr
            pass_bake = self.bake_pbr
        elif self.bake_cat == 'CORE':
            pass_enum = self.passes_core
            pass_bake = self.bake_core
        else:
            pass_enum = self.passes_wrang
            pass_bake = self.bake_wrang

        # Update picked value
        self.bake_picked = pass_bake

        if self.label == "":
            pass_label = "Pass: "
        elif ":" in self.label:
            start, sep, end = self.label.rpartition(":")
            pass_label = start + ": "
        for pas in pass_enum:
            if pas[0] == pass_bake:
                self.label = pass_label + pas[1]
                break
    
    # Get Mesh node inputs
    def get_inputs(self):
        meshes = []
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Mesh':
                mesh = get_input(input)
                if mesh:
                    if mesh.bl_idname == 'BakeWrangler_Sort_Meshes':
                        for inpt in mesh.inputs:
                            if inpt.islinked() and inpt.valid:
                                meshes.append([mesh, inpt])
                    else:
                        meshes.append([mesh])
        return meshes
    
    # Try to get nodes settings from local or global node
    def get_settings(self, validating=False):
        pass_settings = {}
        settings = sampsets = get_input(self.inputs["Settings"])
        if not settings or settings.bl_idname == 'BakeWrangler_SampleSettings':
            settings = self.id_data.get_pinned_settings("PassSettings")
        # Handle sample settings if pass settings found
        if settings:
            if not sampsets or sampsets.bl_idname != 'BakeWrangler_SampleSettings':
                sampsets = None
                # See if the settings has a connected samples settings
                if "Samples" in settings.inputs.keys():
                    sampsets = get_input(settings.inputs["Samples"])
                if not sampsets:
                    # See if there is a pinned samples settings
                    sampsets = self.id_data.get_pinned_settings("SampleSettings")
        if validating:
            return settings
        # Make it so having no sample settings node still works
        if not sampsets:
            pass_settings['bake_samples'] = self.bake_samples if self.bake_samples != 0 else 1
            pass_settings['bake_threshold'] = 0.0
            pass_settings['bake_usethresh'] = False
            pass_settings['bake_timelimit'] = 0.0
        else:
            if not get_input(self.inputs["Settings"]) and self.bake_samples != 0:
                pass_settings['bake_samples'] = self.bake_samples
            pass_settings['bake_samples']   = sampsets.bake_samples if 'bake_samples' not in pass_settings else pass_settings['bake_samples']
            pass_settings['bake_threshold'] = sampsets.bake_threshold
            pass_settings['bake_usethresh'] = sampsets.bake_usethresh
            pass_settings['bake_timelimit'] = sampsets.bake_timelimit
        pass_settings['x_res']          = settings.res_bake_x
        pass_settings['y_res']          = settings.res_bake_y
        pass_settings['bake_device']    = settings.bake_device
        pass_settings['interpolate']    = settings.interpolate
        pass_settings['use_world']      = settings.use_world
        pass_settings['the_world']      = settings.the_world
        pass_settings['cpy_render']     = settings.cpy_render
        pass_settings['cpy_from']       = settings.cpy_from
        pass_settings['tiles']          = settings.use_tiles
        pass_settings['tile_size']      = settings.render_tile
        pass_settings['threads']        = settings.render_threads
        pass_settings['use_bg_col']     = settings.use_bg_col
        pass_settings['bg_color']       = settings.bg_color
        pass_settings['bake_cat']       = self.bake_cat
        pass_settings['bake_type']      = self.bake_picked
        pass_settings['use_mask']       = self.use_mask           
        pass_settings['node_name']      = self.get_name()
        pass_settings['norm_s']         = self.norm_space
        pass_settings['norm_r']         = self.norm_R
        pass_settings['norm_g']         = self.norm_G
        pass_settings['norm_b']         = self.norm_B
        pass_settings['multi_pass']     = self.multi_pass
        pass_settings['multi_samp']     = self.multi_samp
        pass_settings['multi_targ']     = self.multi_targ
        pass_settings['multi_sorc']     = self.multi_sorc
        pass_settings['bev_rad']        = self.bev_rad
        pass_settings['bev_samp']       = self.bev_samp
        pass_settings['cavity_samp']    = self.cavity_samp
        pass_settings['cavity_dist']    = self.cavity_dist
        pass_settings['cavity_gamma']   = self.cavity_gamma
        pass_settings['cavity_edges']   = self.cavity_edges
        pass_settings['curv_mid']       = self.curv_mid
        pass_settings['curv_vex']       = self.curv_vex
        pass_settings['curv_cav']       = self.curv_cav
        pass_settings['curv_vex_max']   = self.curv_vex_max
        pass_settings['curv_cav_min']   = self.curv_cav_min
        pass_settings['osl_curv_dist']  = self.osl_curv_dist
        pass_settings['osl_curv_samp']  = self.osl_curv_samp
        pass_settings['osl_curv_cont']  = self.osl_curv_cont
        pass_settings['osl_curv_srgb']  = False
        pass_settings['osl_height_dist'] = self.osl_height_dist
        pass_settings['osl_height_samp'] = self.osl_height_samp
        pass_settings['osl_height_midl'] = self.osl_height_midl
        pass_settings['osl_height_void'] = self.osl_height_void
        pass_settings['vert_col']       = self.vert_col
        pass_settings['influences']     = set()
        pass_settings['aov_name']       = self.aov_name
        pass_settings['aov_input']      = self.aov_input
        pass_settings['use_material_vpcolor'] = self.use_material_vpcolor
        pass_settings['osl_bentnorm_dist'] = self.osl_bentnorm_dist
        pass_settings['osl_bentnorm_samp'] = self.osl_bentnorm_samp
        
        if self.use_direct:
            pass_settings['influences'].add('DIRECT')
        if self.use_indirect:
            pass_settings['influences'].add('INDIRECT')
        if self.use_color:
            pass_settings['influences'].add('COLOR')
        if self.bake_picked == 'COMBINED':
            if self.use_diffuse:
                pass_settings['influences'].add('DIFFUSE')
            if self.use_glossy:
                pass_settings['influences'].add('GLOSSY')
            if self.use_transmission:
                pass_settings['influences'].add('TRANSMISSION')
            #if self.use_ao:
            #    pass_settings['influences'].add('AO')
            if self.use_emit:
                pass_settings['influences'].add('EMIT')
        if self.use_bg_col:
            pass_settings['use_bg_col'] = self.use_bg_col
            pass_settings['bg_color'] = self.bg_color
        return pass_settings

    # Check node settings are valid to bake. Returns true/false, plus error message(s).
    def validate(self, is_primary=False):
        valid = [True]
        # Validate has Settings
        self.update_pass(None)
        if not self.get_settings(validating=True):
            valid[0] = False
            valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected pass settings found"])
        # Validate inputs
        has_valid_input = False
        is_multires = (self.bake_cat == 'CORE' and self.bake_core == 'MULTIRES')
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Mesh' and input.islinked() and input.valid:
                if self.bake_cat == 'PBR':
                    input_valid = get_input(input).validate(check_materials=True)
                else:
                    input_valid = get_input(input).validate(multires=is_multires)
                if not input_valid.pop(0):
                    valid[0] = False
                else:
                    has_valid_input = True
                if len(input_valid):
                    valid += input_valid
        errs = len(valid)
        if not has_valid_input and errs < 2:
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        # Validate outputs
        if is_primary:
            has_valid_output = False
            for output in self.outputs:
                if output.is_linked:
                    for link in gather_output_links(output):
                        if link.is_valid and link.to_socket.valid:
                            output_valid = link.to_node.validate()
                            if not output_valid.pop(0):
                                valid[0] = False
                            else:
                                has_valid_output = True
                            if len(output_valid):
                                valid += output_valid
            if not has_valid_output and errs == len(valid):
                valid[0] = False
                valid.append([_print("Output error", node=self, ret=True), ": No valid outputs connected"])
        # Validated
        return valid

    pass_cats = (
        ('PBR', "PBR", "Bake passes for PBR materials. Most require the material to use a Principled BSDF"),
        ('CORE', "Blender", "Standard bake passes normally found in blender. Passes with the same name in a different category will have different behavior"),
        ('WRANG', "Wrangler", "Additional passes provided in Bake Wrangler that aren't PBR specific"),
    )

    passes_pbr = (
        ('ALBEDO', "Albedo", "Surface color without lighting (Principled shader only)"),
        ('SUBSURFACE', "Subsurface", "Multiplies with the subsurface radius to determine distance light travels below the surface (Principled shader only)"),
        ('SUBRADIUS', "Subsurf Radius", "Distance light scatters below the surface, each channel is for that R/G/B color of light (a higher R value will mean red light travels deeper) (Principled shader only)"),
        ('SUBCOLOR', "Subsurf Color", "Base subsurface scattering color (Principled shader only)"),
        ('METALLIC', "Metallic", "Surface 'metalness' values (Principled shader only)"),
        ('SPECULAR', "Specular", "Surface dielectric specular reflection values (Princpled shader only)"),
        ('SPECTINT', "Specular Tint", "Tint of direct facing reflections (Note: dielectrics have colorless reflections, but this allows faking some effects) (Principled shader only)"),
        ('ROUGHNESS', "Roughness", "Surface roughness values with no other influences (Principled shader only)"),
        ('SMOOTHNESS', "Smoothness", "Surface inverted roughness values with no other influences (Principled shader only)"),
        ('ANISOTROPIC', "Anisotropic", "Amount of anisotropy for specular reflections (Principled shader only)"),
        ('ANISOROTATION', "Aniso Rotation", "Rotation direction of anisotropy (Principled shader only)"),
        ('SHEEN', "Sheen", "Amount of soft velvet like reflections near edges for cloth like materials (Principled shader only)"),
        ('SHEENTINT', "Sheen Tint", "Color mixed with white for sheen reflections (Principled shader only)"),
        ('CLEARCOAT', "Clearcoat", "Extra white specular layer on surface of material (Principled shader only)"),
        ('CLEARROUGH', "Clear Roughness", "Roughness values of the clearcoat (Principled shader only)"),
        ('TRANSIOR', "IOR", "Index of refraction used for transmission values (Principled shader only)"),
        ('TRANSMISSION', "Transmission", "Opacity values of surface with no other influences (Principled shader only)"),
        ('TRANSROUGH', "Trans Roughness", "Roughness values for light transmitted completely through the surface (Principled shader only)"),
        ('EMIT', "Emission", "Surface self emission color values with no other influences (Principled shader only)"),
        ('ALPHA', "Alpha", "Surface transparency values (Principled shader only)"),
        ('TEXNORM', "Texture Normals", "Surface normals influenced only by texture values (no geometry)"),
        ('CLEARNORM', "Clearcoat Normals", "Surface normals influenced only by the clearcoat values"),
        ('OBJNORM', "Geometry Normals", "Surface normals ignoring any influence from textures"),
        ('AOV', "AOV Node", "The color or value channel of a named AOV node in the materials"),
        ('BBNORM', "Billboard Normals", "Normals using target billboards rotation as tangent space"),
    )

    passes_core = (
        ('COMBINED', "Combined", "Combine multiple passes into a single bake"),
        ('AO', "Ambient Occlusion", "Surface self occlusion values"),
        ('SHADOW', "Shadow", "Shadow map"),
        ('NORMAL', "Normal", "Surface normals"),
        ('UV', "UV", "UV Layout"),
        ('ROUGHNESS', "Roughness", "Surface roughness values"),
        ('SMOOTHNESS', "Smoothness", "Surface inverted roughness values"),
        ('EMIT', "Emit", "Surface self emission color values"),
        ('ENVIRONMENT', "Environment", "Colors coming from the environment"),
        ('DIFFUSE', "Diffuse", "Colors of a surface generated by a diffuse shader"),
        ('GLOSSY', "Glossy", "Colors of a surface generated by a glossy shader"),
        ('TRANSMISSION', "Transmission", "Colors of light passing through a material"),
        ('MULTIRES', "Multiresolution", "Data from a multiresolution modifier"),
    )

    passes_wrang = (
        ('BEVMASK', "Bevel Mask", "Map of bevels where beveled areas will be baked in white"),
        ('BEVNORMEMIT', "Bevel Normals (Emit)", "Normal map with only bevel influences (can bake from one object to another, but inverted faces will be backwards)"),
        ('BEVNORMNORM', "Bevel Normals (Norm)", "Normal map with only bevel influences (does not work for baking from one object to another, but handles inverted faces)"),
        ('CAVITY', "Cavity/Edges", "Surface cavity occlusion or edges map"),
        ('CURVATURE', "Curvature", "Surface curvature map"),
        #('OSL_CURV', "Curvature (OSL)", "OSL implementation of surface curvature map (CPU only)"),
        ('OSL_HEIGHT', "Height (OSL)", "Height map created by the distance between two surfaces (OSL shader only supports CPU)"),
        ('ISLANDID', "Island ID", "Map where each island of faces are baked in a different color"),
        ('MATID', "Material ID", "Map where each material is baked in a random solid color (based on their name)"),
        ('OBJCOL', "Object Color", "Each object is baked using its assigned viewport color"),
        ('WORLDPOS', "Position", "Areas of the object are baked with colors representing their position in the world"),
        ('THICKNESS', "Thickness", "Mesh thickness is baked from white (thin) to black (thick)"),
        ('VERTCOL', "Vertex Color", "Selected vertex colors are baked as the surface color"),
        ('OSL_BENTNORM', "Bent Normals (OSL)", "Bends normals based on ambient occlusion giving a directional bias"),
        ('MASKPASS', "UV Mask", "Black and white mask of pixels covered by UV islands"),
    )

    passes_all = passes_pbr + passes_core + passes_wrang

    bake_has_influence = ['SUBSURFACE', 'TRANSMISSION', 'GLOSSY', 'DIFFUSE', 'COMBINED']

    normal_spaces = (
        ('TANGENT', "Tangent", "Bake the normals in tangent space"),
        ('OBJECT', "Object", "Bake the normals in object space"),
    )

    normal_swizzle = (
        ('POS_X', "+X", ""),
        ('POS_Y', "+Y", ""),
        ('POS_Z', "+Z", ""),
        ('NEG_X', "-X", ""),
        ('NEG_Y', "-Y", ""),
        ('NEG_Z', "-Z", ""),
    )

    multires_subpasses = (
        ('NORMALS', "Normals", "Bake Normals"),
        ('DISPLACEMENT', "Displacement", "Bake Displacement"),
    )

    multires_sampling = (
        ('MAXIMUM', "Max to Min", "Bake the highest resolution down to the lowest"),
        ('FROMMOD', "Modifier Values", "Bake from the current render resolution to the current preview resolution"),
        ('CUSTOM', "Custom Values", "Choose custom values for the target and source resolutions"),
    )
    
    aov_inputs = (
        ('COL', "Color", "Use color data"),
        ('VAL', "Value", "Use value data"),
    )
    
    bake_result: bpy.props.PointerProperty(name="bake_result", description="Used internally by BW", type=bpy.types.Image)
    mask_result: bpy.props.PointerProperty(name="mask_result", description="Used internally by BW", type=bpy.types.Image)
    sbake_result: bpy.props.PointerProperty(name="sbake_result", description="Used internally by BW", type=bpy.types.Image)
    smask_result: bpy.props.PointerProperty(name="smask_result", description="Used internally by BW", type=bpy.types.Image)
    
    bake_cat: bpy.props.EnumProperty(name="Group", description="Category to select bake passes from", items=pass_cats, default='PBR', update=update_pass)
    bake_pbr: bpy.props.EnumProperty(name="Pass", description="Type of PBR pass to bake", items=passes_pbr, default='ALBEDO', update=update_pass)
    bake_core: bpy.props.EnumProperty(name="Pass", description="Type of Blender standard pass to bake", items=passes_core, default='COMBINED', update=update_pass)
    bake_wrang: bpy.props.EnumProperty(name="Pass", description="Type of Wrangler pass to bake", items=passes_wrang, default='BEVMASK', update=update_pass)
    bake_picked: bpy.props.EnumProperty(name="Picked", description="Selected bake type", items=passes_all)
    bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 25 to 50 samples for most bake types (AO may look better with more).\nQuality is gained by increaseing resolution rather than samples past that point", default=0, min=0)

    adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)

    use_mask: bpy.props.BoolProperty(name="Use Masking", description="Generate a map of changed UV islands to use as a mask when updating pixel values. Allows layering of multiple passes onto a single image so long as they don't overlap", default=False)
    use_direct: bpy.props.BoolProperty(name="Direct", description="Add direct lighting contribution", default=True)
    use_indirect: bpy.props.BoolProperty(name="Indirect", description="Add indirect lighting contribution", default=True)
    use_color: bpy.props.BoolProperty(name="Color", description="Color the pass", default=True)
    use_diffuse: bpy.props.BoolProperty(name="Diffuse", description="Add diffuse contribution", default=True)
    use_glossy: bpy.props.BoolProperty(name="Glossy", description="Add glossy contribution", default=True)
    use_transmission: bpy.props.BoolProperty(name="Transmission", description="Add transmission contribution", default=True)
    #use_subsurface: bpy.props.BoolProperty(name="Subsurface", description="Add subsurface contribution", default=True)
    #use_ao: bpy.props.BoolProperty(name="Ambient Occlusion", description="Add ambient occlusion contribution", default=True)
    use_emit: bpy.props.BoolProperty(name="Emit", description="Add emission contribution", default=True)

    norm_space: bpy.props.EnumProperty(name="Space", description="Space to bake the normals in", items=normal_spaces, default='TANGENT')
    norm_R: bpy.props.EnumProperty(name="R", description="Axis to bake in Red channel", items=normal_swizzle, default='POS_X')
    norm_G: bpy.props.EnumProperty(name="G", description="Axis to bake in Green channel", items=normal_swizzle, default='POS_Y')
    norm_B: bpy.props.EnumProperty(name="B", description="Axis to bake in Blue channel", items=normal_swizzle, default='POS_Z')
    multi_pass: bpy.props.EnumProperty(name="Multires Type", description="Type of multiresolution pass to bake", items=multires_subpasses, default='NORMALS')
    multi_samp: bpy.props.EnumProperty(name="Multires Method", description="Method to pick multiresolution source and target", items=multires_sampling, default='MAXIMUM')
    multi_targ: bpy.props.IntProperty(name="Multires Target", description="Subdivision level for target of bake", default=0, min=0, soft_max=16)
    multi_sorc: bpy.props.IntProperty(name="Multires Source", description="Subdivision level for source of bake", default=8, min=0, soft_max=16)

    bev_rad: bpy.props.FloatProperty(name="Bevel Radius", description="Width of bevel on edges", default=0.05, min=0)
    bev_samp: bpy.props.IntProperty(name="Bevel Samples", description="Number of samples to take (more gives greater accuracy at cost of time. A value of 4 works well in most cases and noise can be resolved by using more bake pass samples instead of increasing this value)", min=2, max=16, default=4)
    cavity_samp: bpy.props.IntProperty(name="Cavity Over Samples", description="Number of samples to take (more gives a more accurate result but takes longer, increase bake pass samples to reduce noise before increasing this value)", default=16, min=1, max=128)
    cavity_dist: bpy.props.FloatProperty(name="Cavity Sample Distance", description="How far away a face can be to contribute to the calculation (may need larger distances for larger objects)", default=0.4, step=1, min=0.0, unit='LENGTH')
    cavity_gamma: bpy.props.FloatProperty(name="Cavity Gamma", description="Gamma transform to be performed on cavity values", default=1.0, step=1)
    cavity_edges: bpy.props.BoolProperty(name="Edge Mode", description="Inverts the cavity map normals to find edges. If it's too dark, lower distance value", default=False)
    curv_mid: bpy.props.FloatProperty(name="Curvature Flat", description="Value to assign to the mid-point between curves (flat area)", default=0.5, min=0.0, max=1.0)
    curv_vex: bpy.props.FloatProperty(name="Curvature Convex", description="Value to assign to the sharpest (maximum) point of a convex curve", default=1.0, min=0.0, max=1.0)
    curv_cav: bpy.props.FloatProperty(name="Curvature Concave", description="Value to assign to the sharpest (minimum) point of a concave curve", default=0.0, min=0.0, max=1.0)
    curv_vex_max: bpy.props.FloatProperty(name="Curvature Convex Max", description="Convex value to consider the maximum curvature (higher values create a greater range)", default=0.2, min=0.0001, max=1.0)
    curv_cav_min: bpy.props.FloatProperty(name="Curvature Concave Min", description="Concave value to consider the minimum curvature (lower values create a greater range)", default=0.8, min=0.0, max=0.9999)
    osl_curv_dist: bpy.props.FloatProperty(name="Curvature Distance (OSL)", description="Distance to search for a neighboring surface", default=0.1, min=0.0)
    osl_curv_samp: bpy.props.IntProperty(name="Curvature Samples (OSL)", description="Number of attempts to try finding a neighboring surface within the distance value", default=16, min=1)
    osl_curv_cont: bpy.props.FloatProperty(name="Curvature Contrast (OSL)", description="Contrast level applied to curvature value", default=0.0, min=0.0)
    osl_height_dist: bpy.props.FloatProperty(name="Height Distance", description="Maximum distance from source to look for a surface", default=0.1, min=0.0)
    osl_height_samp: bpy.props.IntProperty(name="Height Samples", description="How many surfaces to consider for each point. Must be at least two but complex objects with faces close together may need more for correct results", default=2, min=2)
    osl_height_midl: bpy.props.FloatProperty(name="Height Mid Level", description="Value to use as the middel level (neither high or low)", default=0.5, min=0.0, max=1.0)
    osl_height_void: bpy.props.FloatVectorProperty(name="Height Void Color", description="Color to fill in areas where no height value could be found within the search distance", default=[0.5,0.5,0.5,1.0], size=4, subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10)
    vert_col: bpy.props.StringProperty(name="Vertex Color Layer", description="Vertex color layer to bake from (leaving blank will use active)", default="")
    use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
    bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
    aov_name: bpy.props.StringProperty(name="AOV Name", description="Name assigned to AOV node you want to bake from", default="")
    aov_input: bpy.props.EnumProperty(name="AOV Source", description="Take data from either color or value input of AOV node", items=aov_inputs, default='COL')
    use_material_vpcolor: bpy.props.BoolProperty(name="Use Viewport Color", description="Use the materials assigned viewport color instead of generating a random one based on its name", default=False)
    use_subtraction: bpy.props.BoolProperty(name="Use Subtraction", description="Subtract the object normals from the material normals to isolate them. This results in correct tangent normal rotations but may not be as clean a result", default=True)
    osl_bentnorm_dist: bpy.props.FloatProperty(name="Bent Normals Distance", description="Maximum distance for a surface to contribute to ambient occlusion", default=1.0, min=0.001)
    osl_bentnorm_samp: bpy.props.IntProperty(name="Height Samples", description="Number of ambient light samples to take for each surface point", default=8, min=1)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Set label to pass
        self.update_pass(context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_PassSetting', "Settings")
        self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Color', "Color")
        # Prefs
        self.adv_settings = _prefs("def_show_adv")

    def draw_buttons(self, context, layout):
        colnode = layout.column(align=False)

        colpass = colnode.column(align=True)
        colpass.prop(self, "bake_cat")
        if self.bake_cat == 'PBR':
            colpass.prop(self, "bake_pbr")
        elif self.bake_cat == 'CORE':
            colpass.prop(self, "bake_core")
        else:
            colpass.prop(self, "bake_wrang")

        advrow = colnode.row()
        advrow.alignment = 'LEFT'

        if not self.adv_settings:
            adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
            advrow.separator()
        else:
            adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
            advrow.separator()

            col = colnode.column(align=True)
            col.prop(self, "use_mask", toggle=True)
            bg_col = col.row(align=True)
            bg_col.prop(self, "use_bg_col", toggle=True)
            if self.use_bg_col:
                bg_col.prop(self, "bg_color", toggle=True, text="")

            splitopt = colnode.split(factor=0.5)
            colopttxt = splitopt.column(align=True)
            colopttxt.alignment = 'RIGHT'
            coloptval = splitopt.column(align=True)
            
            # Additional options for PBR passes
            if self.bake_cat == 'PBR':
                # AOV ouput
                if self.bake_pbr == 'AOV':
                    colopttxt.label(text="Name:")
                    colopttxt.label(text="Input:")

                    coloptval.prop(self, "aov_name", text="")
                    coloptval.prop(self, "aov_input", text="")
            # Additional options for 'Wrangler' passes
            if self.bake_cat == 'WRANG':
                # Bevel mask
                if self.bake_wrang in ['BEVMASK', 'BEVNORMEMIT', 'BEVNORMNORM']:
                    colopttxt.label(text="Samples:")
                    colopttxt.label(text="Radius:")

                    coloptval.prop(self, "bev_samp", text="")
                    coloptval.prop(self, "bev_rad", text="")
                elif self.bake_wrang == 'CAVITY':
                    colopttxt.label(text="Edge Mode:")
                    colopttxt.label(text="Over Samples:")
                    colopttxt.label(text="Distance:")
                    colopttxt.label(text="Gamma:")

                    coloptval.prop(self, "cavity_edges", text="")
                    coloptval.prop(self, "cavity_samp", text="")
                    coloptval.prop(self, "cavity_dist", text="")
                    coloptval.prop(self, "cavity_gamma", text="")
                elif self.bake_wrang == 'THICKNESS':
                    colopttxt.label(text="Over Samples:")
                    colopttxt.label(text="Distance:")

                    coloptval.prop(self, "cavity_samp", text="")
                    coloptval.prop(self, "cavity_dist", text="")
                elif self.bake_wrang == 'CURVATURE':
                    colopttxt.label(text="Flat:")
                    colopttxt.label(text="Convex:")
                    colopttxt.label(text="Convex Max:")
                    colopttxt.label(text="Concave:")
                    colopttxt.label(text="Concave Min:")

                    coloptval.prop(self, "curv_mid", text="")
                    coloptval.prop(self, "curv_vex", text="")
                    coloptval.prop(self, "curv_vex_max", text="")
                    coloptval.prop(self, "curv_cav", text="")
                    coloptval.prop(self, "curv_cav_min", text="")
                elif self.bake_wrang == 'OSL_CURV':
                    colopttxt.label(text="Distance:")
                    colopttxt.label(text="Samples:")
                    colopttxt.label(text="Contrast:")

                    coloptval.prop(self, "osl_curv_dist", text="")
                    coloptval.prop(self, "osl_curv_samp", text="")
                    coloptval.prop(self, "osl_curv_cont", text="")
                elif self.bake_wrang == 'OSL_HEIGHT':
                    colopttxt.label(text="Distance:")
                    colopttxt.label(text="Samples:")
                    colopttxt.label(text="Mid Level:")
                    colopttxt.label(text="Void Color:")

                    coloptval.prop(self, "osl_height_dist", text="")
                    coloptval.prop(self, "osl_height_samp", text="")
                    coloptval.prop(self, "osl_height_midl", text="")
                    coloptval.prop(self, "osl_height_void", text="")
                elif self.bake_wrang == 'VERTCOL':
                    colopttxt.label(text="Layer:")

                    coloptval.prop(self, "vert_col", text="")
                elif self.bake_wrang == 'MATID':
                    colnode.prop(self, "use_material_vpcolor")
                elif self.bake_wrang == 'OSL_BENTNORM':
                    colopttxt.label(text="Distance:")
                    colopttxt.label(text="Samples:")
                    
                    coloptval.prop(self, "osl_bentnorm_dist", text="")
                    coloptval.prop(self, "osl_bentnorm_samp", text="")
                
            # Additional options for 'core' passes
            if self.bake_cat == 'CORE':
                # Multires
                if self.bake_core == 'MULTIRES':
                    colopttxt.label(text="Type:")
                    colopttxt.label(text="Method:")

                    coloptval.prop(self, "multi_pass", text="")
                    coloptval.prop(self, "multi_samp", text="")
                    if self.multi_samp == 'CUSTOM':
                        colopttxt.label(text="Target Divs:")
                        colopttxt.label(text="Source Divs:")

                        coloptval.prop(self, "multi_targ", text="")
                        coloptval.prop(self, "multi_sorc", text="")
                elif self.bake_core in self.bake_has_influence:
                    row = colnode.row(align=True)
                    row.use_property_split = False
                    row.prop(self, "use_direct", toggle=True)
                    row.prop(self, "use_indirect", toggle=True)
                    if self.bake_core != 'COMBINED':
                        row.prop(self, "use_color", toggle=True)
                    else:
                        col = colnode.column(align=True)
                        col.prop(self, "use_diffuse")
                        col.prop(self, "use_glossy")
                        col.prop(self, "use_transmission")
                        #col.prop(self, "use_subsurface")
                        #col.prop(self, "use_ao")
                        col.prop(self, "use_emit")
            # Any normal map passes
            if (self.bake_cat == 'PBR' and self.bake_pbr in ['TEXNORM', 'CLEARNORM', 'OBJNORM', 'BBNORM']) \
                or (self.bake_cat == 'CORE' and self.bake_core in ['NORMAL']) \
                or (self.bake_cat == 'WRANG' and self.bake_wrang in ['BEVNORMEMIT', 'BEVNORMNORM', 'OSL_BENTNORM']):
                if self.bake_picked == 'TEXNORM':
                    colnode.prop(self, "use_subtraction")
                colopttxt.label(text="Space:")
                colopttxt.label(text="R:")
                colopttxt.label(text="G:")
                colopttxt.label(text="B:")

                coloptval.prop(self, "norm_space", text="")
                coloptval.prop(self, "norm_R", text="")
                coloptval.prop(self, "norm_G", text="")
                coloptval.prop(self, "norm_B", text="")



# The channel map combines inputs by mapping them to RGBA channels of the output and sits between passes and output
# images
class BakeWrangler_Channel_Map(BakeWrangler_Tree_Node, Node):
    '''Channel map node'''
    bl_label = 'Channel Map'

    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True)

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Color', "Color")
        self.inputs.new('BakeWrangler_Socket_ChanMap', "Red")
        self.inputs.new('BakeWrangler_Socket_ChanMap', "Green")
        self.inputs.new('BakeWrangler_Socket_ChanMap', "Blue")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Color', "Color")



# Mix RGB via a factor and operation
class BakeWrangler_Post_MixRGB(BakeWrangler_Tree_Node, Node):
    '''Mix RGB node'''
    bl_label = 'Mix RGB'
    
    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True, True)
    
    ops = (
        ('MIX', "Mix", ""),
        ('ADD', "Add", ""),
        ('SUBTRACT', "Subtract", ""),
        ('MULTIPLY', "Multiply", ""),
        ('DIVIDE', "Divide", ""),
        ('OVERLAY', "Overlay", ""),
    )
    
    op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, default='MIX')
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Float', "Fac")
        self.inputs.new('BakeWrangler_Socket_Color', "Color1")
        self.inputs.new('BakeWrangler_Socket_Color', "Color2")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Color', "Color")
    
    def draw_buttons(self, context, layout):
        layout.prop(self, "op", text="")



# Split RGB ouput into channels
class BakeWrangler_Post_SplitRGB(BakeWrangler_Tree_Node, Node):
    '''Split RGB node'''
    bl_label = 'Split RGB'
    
    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Color', "Color")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Float', "Red")
        self.outputs.new('BakeWrangler_Socket_Float', "Green")
        self.outputs.new('BakeWrangler_Socket_Float', "Blue")



# Join channels into single RGB color
class BakeWrangler_Post_JoinRGB(BakeWrangler_Tree_Node, Node):
    '''Join RGB node'''
    bl_label = 'Join RGB'
    
    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True, True)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Float', "Red")
        self.inputs.new('BakeWrangler_Socket_Float', "Green")
        self.inputs.new('BakeWrangler_Socket_Float', "Blue")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Color', "Color")



# Perform math functions on image data
class BakeWrangler_Post_Math(BakeWrangler_Tree_Node, Node):
    '''Math node'''
    bl_label = 'Math'
    
    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True, True)
    
    # Disable inputs based on function
    def chg_op(self, context):
        if self.op in ['FLOOR', 'CEIL']:
            self.inputs[1].enabled = False
        else:
            self.inputs[1].enabled = True
        
    ops = (
        ('ADD', "Add", "A + B"),
        ('SUBTRACT', "Subtract", "A - B"),
        ('MULTIPLY', "Multiply", "A * B"),
        ('DIVIDE', "Divide", "A / B"),
        
        ('POWER', "Power", "A^B"),
        ('LOGARITHM', "Logarithm", "Log A base B"),
        
        ('FLOOR', "Floor", "Largest integer <= A"),
        ('CEIL', "Ceil", "Smallest integer >= A"),
        ('MODULO', "Modulo", "Mod A / B"),
    )
    
    op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, update=chg_op, default='ADD')
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="0")
        self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="1")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Float', "Value")
    
    def draw_buttons(self, context, layout):
        layout.menu("BW_MT_math_ops", text=layout.enum_item_name(self, "op", self.op))

# Menu displayed in the math node to select function
#
# Found out headers can be created in enums by setting all but name to empty string
# eg ("", "ColumnName", "") - So this could probably be replaced with that, but w/e
#
class BakeWrangler_Post_Math_OpMenu(bpy.types.Menu):
    bl_idname = "BW_MT_math_ops"
    bl_label = "Ops"
    
    def draw(self, context):
        layout = self.layout.row()
        layout.alignment = 'LEFT'
        col1 = layout.column()
        col1.alignment = 'LEFT'
        col1.label(text="Functions")
        col1.separator()
        for op in context.node.ops:
            if op[0] in ['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']:
                itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
                BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)
        col1.separator()
        for op in context.node.ops:
            if op[0] in ['POWER', 'LOGARITHM']:
                itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
                BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)
        col2 = layout.column()
        col2.alignment = 'LEFT'
        col2.label(text="Rounding")
        col2.separator()
        for op in context.node.ops:
            if op[0] in ['FLOOR', 'CEIL', 'MODULO']:
                itm = col2.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
                BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)



# Apply gamma transform to color
class BakeWrangler_Post_Gamma(BakeWrangler_Tree_Node, Node):
    '''Gamma node'''
    bl_label = 'Gamma'
    
    def update_inputs(self):
        pass
        
    def validate(self):
        return BakeWrangler_Tree_Node.validate(self, True)
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Color', "Color")
        self.inputs.new('BakeWrangler_Socket_Float', "Gamma")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Color', "Color")



# Output node that specifies the path to a file where a bake should be saved along with size and format information.
# Takes input from the outputs of a bake pass node. Connecting multiple inputs will cause higher position inputs to
# be over written by lower ones. Eg: Having a color input and an R input would cause the R channel of the color data
# to be overwritten by the data connected tot he R input.
class BakeWrangler_Output_Image_Path(BakeWrangler_Tree_Node, Node):
    '''Output image path node'''
    bl_label = 'Output Image Path'
    bl_width_default = 160

    # Returns the most identifying string for the node
    def get_name(self):
        name = BakeWrangler_Tree_Node.get_name(self)
        if self.inputs['Split Output'].get_name():
            name += " (%s)" % (self.inputs['Split Output'].get_name())
        return name

    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color", {'Alpha':'BakeWrangler_Socket_ChanMap'})
        settings = self.get_settings()
        if settings:
            if settings['img_color_mode'] != 'RGBA':
                for input in self.inputs:
                    if input.name == 'Alpha' and input.enabled:
                        input.enabled = False
            else:
                idx = 0
                for input in self.inputs:
                    if input.name == 'Color':
                        if input.is_linked:
                            self.inputs[idx+1].enabled = True
                        else:
                            self.inputs[idx+1].enabled = False
                    idx += 1

    # Try to get nodes settings from local or global node
    def get_settings(self, validating=False):
        settings = None
        if 'Settings' in self.inputs:
            settings = get_input(self.inputs["Settings"])
        if not settings:
            settings = self.id_data.get_pinned_settings("OutputSettings")
        if validating:
            img_non_color = None
            if 'Non-Color' in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
                img_non_color = 'Non-Color'
            elif _prefs('img_non_color') is not None and (len(bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()) - 1) >= int(_prefs('img_non_color')):
                img_non_color = bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()[_prefs('img_non_color')]
            settings.img_non_color = img_non_color
            return settings, img_non_color
        if not settings:
            # No local or pinned settings
            return None
        output_settings = {}
        output_settings['img_non_color']        = settings.img_non_color
        output_settings['img_xres']             = settings.img_xres
        output_settings['img_yres']             = settings.img_yres
        output_settings['img_clear']            = settings.img_clear
        output_settings['fast_aa']              = settings.fast_aa
        output_settings['fast_aa_lvl']          = settings.fast_aa_lvl
        output_settings['img_type']             = settings.img_type
        output_settings['img_udim']             = settings.img_udim
        output_settings['img_color_space']      = settings.img_color_space
        output_settings['img_use_float']        = settings.img_use_float
        output_settings['img_jpeg2k_cinema']    = settings.img_jpeg2k_cinema
        output_settings['img_jpeg2k_cinema48']  = settings.img_jpeg2k_cinema48
        output_settings['img_jpeg2k_ycc']       = settings.img_jpeg2k_ycc
        output_settings['img_dpx_log']          = settings.img_dpx_log
        output_settings['img_openexr_zbuff']    = settings.img_openexr_zbuff
        output_settings['marginer']             = settings.marginer
        output_settings['marginer_size']        = settings.marginer_size
        output_settings['marginer_fill']        = settings.marginer_fill

        output_settings['img_color_mode']       = None
        if settings.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
            output_settings['img_color_mode']   = settings.img_color_mode_noalpha
        elif settings.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
            output_settings['img_color_mode']   = settings.img_color_mode

        output_settings['img_color_depth']      = None
        if settings.img_type in ['PNG', 'TIFF']:
            output_settings['img_color_depth']  = settings.img_color_depth_8_16
        elif settings.img_type == 'JPEG2000':
            output_settings['img_color_depth']  = settings.img_color_depth_8_12_16
        elif settings.img_type == 'DPX':
            output_settings['img_color_depth']  = settings.img_color_depth_8_10_12_16
        elif settings.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
            output_settings['img_color_depth']  = settings.img_color_depth_16_32

        output_settings['img_quality']          = None
        if settings.img_type == 'PNG':
            output_settings['img_quality']      = settings.img_compression
        elif settings.img_type in ['JPEG', 'JPEG2000']:
            output_settings['img_quality']      = settings.img_quality

        output_settings['img_codec']            = None
        if settings.img_type == 'JPEG2000':
            output_settings['img_codec']        = settings.img_codec_jpeg2k
        elif settings.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
            output_settings['img_codec']        = settings.img_codec_openexr
        elif settings.img_type == 'TIFF':
            output_settings['img_codec']        = settings.img_codec_tiff

        output_settings['img_path']             = self.get_full_path()
        
        return output_settings

    # Check node settings are valid to bake. Returns true/false, plus error message(s).
    def validate(self, is_primary=False):
        valid = [True]
        # Validate has Settings
        settings, none_color = self.get_settings(True)
        if not settings:
            valid[0] = False
            valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected output settings found"])
            return valid
        if not none_color:
            valid[0] = False
            valid.append([_print("Non-standard color spaces", node=self, ret=True), ": Please set up your color space in addon preferences"])
            return valid
        # Validate inputs
        has_valid_input = False
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
                if not is_primary:
                    has_valid_input = True
                    break
                else:
                    input_valid = get_input(input).validate()
                    valid[0] = input_valid.pop(0)
                    if valid[0]:
                        has_valid_input = True
                    valid += input_valid
        errs = len(valid)
        if not has_valid_input and errs < 2:
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        # Validate file path
        self.img_path = self.get_full_path()
        if not os.path.isdir(os.path.abspath(self.img_path)):
            # Try creating the path if enabled in prefs
            if _prefs("make_dirs") and not os.path.exists(os.path.abspath(self.img_path)):
                try:
                    os.makedirs(os.path.abspath(self.img_path))
                except OSError as err:
                    valid[0] = False
                    valid.append([_print("Path error", node=self, ret=True), ": Trying to create path at '%s'" % (err.strerror)])
                    return valid
            else:
                valid[0] = False
                valid.append([_print("Path error", node=self, ret=True), ": Invalid path '%s'" % (os.path.abspath(self.img_path))])
                return valid
        # Check if there is read/write access to the file/directory
        settings = self.get_settings()
        file_path = os.path.join(os.path.abspath(self.img_path), self.name_with_ext(settings['img_type']))
        if os.path.exists(file_path):
            if os.path.isfile(file_path):
                # It exists so try to open it r/w
                try:
                    file = open(file_path, "a")
                except OSError as err:
                    valid[0] = False
                    valid.append([_print("File error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
                else:
                    # See if it can be read as an image
                    file.close()
                    file_img = bpy.data.images.load(file_path)
                    if not len(file_img.pixels):
                        valid[0] = False
                        valid.append([_print("File error", node=self, ret=True), ": File exists but doesn't seem to be a known image format"])
                    bpy.data.images.remove(file_img)
            else:
                # It exists but isn't a file
                valid[0] = False
                valid.append([_print("File error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
        else:
            # See if it can be created
            try:
                file = open(file_path, "a")
            except OSError as err:
                valid[0] = False
                valid.append([_print("File error", node=self, ret=True), ": %s trying to create file at '%s'" % (err.strerror, file_path)])
            else:
                file.close()
                os.remove(file_path)
        # Validated
        return valid

    # Get full path, removing any relative references
    def get_full_path(self):
        return self.inputs["Split Output"].get_full_path()

    # Return the file name with the correct image type extension (unless it has an existing unknown extension)
    def name_with_ext(self, suffix=""):
        settings = self.get_settings()
        return self.inputs["Split Output"].name_with_ext(suffix, settings['img_type'])
        
    # Return frame ranges or padding or animated seed if set, otherwise empty list, None or False
    def frame_range(self, padding=False, animated=False):
        return self.inputs["Split Output"].frame_range(padding, animated)

    # Return a dict of format settings
    def get_format(self):
        format = {}
        for prop in self.rna_type.properties.keys():
            format[prop] = getattr(self, prop)
        return format
    
    # Return a dict of output files with their connected input
    def get_output_files(self):
        output_files = {}
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
                output_files[self.name_with_ext(suffix=input.suffix)] = input
        return output_files
    
    # Get list of mesh objects from the split output socket
    def get_split_objects(self):
        objs = []
        split = self.inputs["Split Output"].get_split()
        if split and len(split):
            for splt in split:
                if splt.bl_idname == 'BakeWrangler_Bake_Mesh':
                    objs += prune_objects(splt.inputs["Target"].get_objects(only_mesh=True))
                elif splt.bl_idname == 'BakeWrangler_Input_ObjectList':
                    objs += prune_objects(splt.get_objects(only_mesh=True))
                elif splt.bl_idname == 'BakeWrangler_Bake_Material':
                    objs += splt.get_materials()
                elif splt.bl_idname == 'BakeWrangler_Sort_Meshes':
                    sobj = []
                    for inpt in splt.inputs:
                        if inpt.islinked() and inpt.valid:
                            if get_input(inpt).bl_idname == 'BakeWrangler_Bake_Material':
                                for mat in get_input(inpt).get_materials():
                                    sobj += [[mat]]
                            sobj += splt.get_objects('TARGET', inpt)
                    for grp in sobj:
                        grp[0].append(grp[3])
                        objs.append(grp[0])
            objs = prune_objects(objs)
        if len(objs): return objs
        else: return None
    
    # Get a list of unique objects used as either source or target
    def get_unique_objects(self, type, for_auto_cage=False):
        def get_meshes(socket, meshes):
            pas = get_input(socket)
            if pas:
                if pas.bl_idname == 'BakeWrangler_Bake_Pass':
                    meshes += pas.get_inputs()
                else:
                    for input in pas.inputs:
                        get_meshes(input, meshes)
        meshes = []
        for input in self.inputs:
            if input.name in ["Color", "Alpha"]:
                get_meshes(input, meshes)
        objs = []
        for mesh in meshes:
            if len(mesh) > 1:
                objs += mesh[0].get_unique_objects(type, for_auto_cage, mesh[1])
            else:
                objs += mesh[0].get_unique_objects(type, for_auto_cage)
        objs = prune_objects(objs)
        if for_auto_cage:
            return objs
        return objs
        
    img_ext = (
        ('BMP', ".bmp"),
        ('IRIS', ".rgb"),
        ('PNG', ".png"),
        ('JPEG', ".jpg"),
        ('JPEG2000', ".jp2"),
        ('TARGA', ".tga"),
        ('TARGA_RAW', ".tga"),
        ('CINEON', ".cin"),
        ('DPX', ".dpx"),
        ('OPEN_EXR_MULTILAYER', ".exr"),
        ('OPEN_EXR', ".exr"),
        ('HDR', ".hdr"),
        ('TIFF', ".tif"),
    )

    # Core settings
    img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
    
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_SplitOutput', "Split Output")
        self.inputs.new('BakeWrangler_Socket_OutputSetting', "Settings")
        self.inputs.new('BakeWrangler_Socket_Color', "Color")
        self.inputs.new('BakeWrangler_Socket_ChanMap', "Alpha")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Bake', "Bake")
        # Prefs
        self.inputs['Split Output'].disp_path = _prefs("def_outpath")
        self.inputs['Split Output'].img_name = _prefs("def_outname")

    def draw_buttons(self, context, layout):
        pass
        #colnode = layout.column(align=False)
        #colpath = colnode.column(align=True)
        #colpath.prop(self, "disp_path", text="")
        #colpath.prop(self, "img_name", text="")


class BakeWrangler_Output_Vertex_Cols(BakeWrangler_Tree_Node, Node):
    '''Output vertex colors node'''
    bl_label = 'Output Vertex Colors'
    bl_width_default = 160
    
    vert_files = []

    # Returns the most identifying string for the node
    def get_name(self):
        name = BakeWrangler_Tree_Node.get_name(self)
        return name

    # Make sure an empty input is always at the bottom
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color")
    
    # All objects are split objects? We don't use this
    def get_split_objects(self):
        return None
    
    # Image settings are mostly irrelevant here, mostly static values will be set
    def get_settings(self, validating=False):
        output_settings = {}
        output_settings['vcol']                 = True
        output_settings['vcol_type']            = self.vcol_type
        output_settings['vcol_domain']          = self.vcol_domain
        output_settings['img_udim']             = False
        output_settings['marginer']             = False
        output_settings['marginer_size']        = 0
        output_settings['marginer_fill']        = False
        output_settings['img_color_mode']       = None
        output_settings['img_color_depth']      = None
        output_settings['img_path']             = None
        return output_settings
        
    # Check node settings are valid to bake. Returns true/false, plus error message(s).
    def validate(self, is_primary=False):
        valid = [True]
        # Validate inputs
        has_valid_input = False
        for input in self.inputs:
            if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
                if not input.suffix or not len(input.suffix):
                    valid[0] = False
                    valid.append([_print("Input error", node=self, ret=True), ": Connected vertex output requires valid name for data"])
                if not is_primary:
                    has_valid_input = True
                    break
                else:
                    input_valid = get_input(input).validate()
                    valid[0] = input_valid.pop(0)
                    if valid[0]:
                        has_valid_input = True
                    valid += input_valid
        errs = len(valid)
        if not has_valid_input and errs < 2:
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        return valid
    
    vcol_domains = (
        ('POINT', "Vertex", "Vertex"),
        ('CORNER', "Face Corner", "Face Corner"),
    )
    
    vcol_types = (
        ('FLOAT_COLOR', "Color", "32-bit floating point values"),
        ('BYTE_COLOR', "Byte Color", "8-bit integer values"),
    )
    
    # Core settings
    vcol_domain: bpy.props.EnumProperty(name="Domain", description="Type of element the data is stored on", items=vcol_domains, default='POINT')
    vcol_type: bpy.props.EnumProperty(name="Data Type", description="Type of data stored in element", items=vcol_types, default='FLOAT_COLOR')
        
    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        # Sockets IN
        self.inputs.new('BakeWrangler_Socket_Color', "Color")
        # Sockets OUT
        self.outputs.new('BakeWrangler_Socket_Bake', "Bake")
        # Prefs
        
    def draw_buttons(self, context, layout):
        row = layout.row(align=True)
        col = layout.column(align=True)
        if not len(self.vert_files):
            row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply")
            row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard")
        else:
            op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply")
            op1.tree = self.id_data.name
            op1.node = self.name
            op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard")
            op2.tree = self.id_data.name
            op2.node = self.name
        
        col.prop(self, "vcol_domain", text="")
        col.prop(self, "vcol_type", text="")
        
        
# Output controller node provides batch execution of multiple connected bake passes.
class BakeWrangler_Output_Batch_Bake(BakeWrangler_Tree_Node, Node):
    '''Output controller oven node'''
    bl_label = 'Batch Bake'
    
    vert_files = []

    # Makes sure there is always one empty input socket at the bottom by adding and removing sockets
    def update_inputs(self):
        BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Bake', "Bake")

    # Check node settings are valid to bake. Returns true/false, plus error message(s).
    def validate(self, is_primary=True):
        valid = [True]
        # Batch mode needs to avoid validating the same things more than once. Collect a
        # unique list of the passes before validating them.
        img_node_list = []
        pass_node_list = []
        for input in self.inputs:
            if input.islinked() and input.valid:
                img_node = follow_input_link(input.links[0]).from_node
                if not img_node_list.count(img_node):
                    img_node_list.append(img_node)
                    for img_node_input in img_node.inputs:
                        if img_node_input.islinked() and img_node_input.valid:
                            pass_node = follow_input_link(img_node_input.links[0]).from_node
                            if not pass_node_list.count(pass_node):
                                pass_node_list.append(pass_node)
        # Validate all the listed nodes
        has_valid_input = False
        for node in img_node_list:
            img_node_valid = node.validate()
            if not img_node_valid.pop(0):
                valid[0] = False
            if valid[0]:
                has_valid_input = True
            valid += img_node_valid
        for node in pass_node_list:
            pass_node_valid = node.validate()
            if not pass_node_valid.pop(0):
                valid[0] = False
            valid += pass_node_valid
        errs = len(valid)
        if not has_valid_input and errs < 2:
            valid[0] = False
            valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
        # Validate pre and post scripts if set to external file
        if self.loc_pre == 'EXT':
            file_path = self.script_pre_can
            # Validate file path
            if os.path.exists(file_path):
                if os.path.isfile(file_path):
                    # It exists so try to open it for read
                    try:
                        file = open(file_path, "r")
                    except OSError as err:
                        valid[0] = False
                        valid.append([_print("Pre-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
                else:
                    # It exists but isn't a file
                    valid[0] = False
                    valid.append([_print("Pre-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
        # See if the pre script compiles if set
        if self.loc_pre != 'NON':
            if self.loc_pre == 'INT':
                pre_scr = self.script_pre_int.as_string()
            elif self.loc_pre == 'EXT':
                with open(self.script_pre_can, "r") as scr:
                    pre_scr = scr.read()
            try:
                compile(pre_scr, '<string>', 'exec')
            except SyntaxError or ValueError as err:
                valid[0] = False
                valid.append([_print("Pre-Script compile error", node=self, ret=True), ": %s" % (str(err))])
        if self.loc_post == 'EXT':
            file_path = self.script_post_can
            # Validate file path
            if os.path.exists(file_path):
                if os.path.isfile(file_path):
                    # It exists so try to open it for read
                    try:
                        file = open(file_path, "r")
                    except OSError as err:
                        valid[0] = False
                        valid.append([_print("Post-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
                else:
                    # It exists but isn't a file
                    valid[0] = False
                    valid.append([_print("Post-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
        # See if the pre script compiles if set
        if self.loc_post != 'NON':
            if self.loc_post == 'INT':
                post_scr = self.script_post_int.as_string()
            elif self.loc_post == 'EXT':
                with open(self.script_post_can, "r") as scr:
                    post_scr = scr.read()
            try:
                compile(post_scr, '<string>', 'exec')
            except SyntaxError or ValueError as err:
                valid[0] = False
                valid.append([_print("Post-Script compile error", node=self, ret=True), ": %s" % (str(err))])
        # Everything validated
        return valid
    
    # Get a list of unique objects used as either source or target
    def get_unique_objects(self, type):
        objs = []
        for input in self.inputs:
            input = get_input(input)
            if input and input.bl_idname == 'BakeWrangler_Output_Image_Path':
                objs += input.get_unique_objects(type)
        objs = prune_objects(objs)
        objs_single = []
        for obj in objs:
            objs_single.append(obj[0])
        return objs_single
        
    # Generate enum of base collection types
    def get_collection_types(self, context):
        col_types = []
        for prop in bpy.data.bl_rna.properties:
            if prop.type == 'COLLECTION':
                col_types.append((prop.identifier, prop.name, prop.description))
        return col_types

    # Generate enum of props
    def get_user_props(self, context):
        props = []
        if self.user_prop_objt:
            for prop in self.user_prop_objt.keys():
                props.append((prop, prop, prop + " custom property"))
        return props

    # Set the nodes background color to red when shutdown is enabled
    def shutdown_update(self, context):
        if self.shutdown_after:
            self.use_custom_color = True
            self.color = [0.9, 0, 0]
        else:
            self.use_custom_color = False
            self.color = [0.608, 0.608, 0.608]
    
    # Get full path, removing any relative references
    def update_pre_script(self, context):
        cwd = os.path.dirname(bpy.data.filepath)
        self.script_pre_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_pre)))
    
    # Get full path, removing any relative references
    def update_post_script(self, context):
        cwd = os.path.dirname(bpy.data.filepath)
        self.script_post_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_post)))
        
    loc = (
        ('EXT', "External", ""),
        ('INT', "Internal", ""),
        ('NON', "None", ""),
    )
    
    loc_pre: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON')
    script_pre: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_pre_script)
    script_pre_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH')
    script_pre_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text)
    
    loc_post: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON')
    script_post: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_post_script)
    script_post_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH')
    script_post_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text)
    
    user_prop: bpy.props.BoolProperty(name="User Property", description="Enable custom user property incrementer", default=False)
    user_prop_type: bpy.props.EnumProperty(name="Type", description="Type of data property is on", items=get_collection_types)
    user_prop_objt: bpy.props.PointerProperty(name="Data", description="Data property is on", type=bpy.types.ID)
    user_prop_name: bpy.props.EnumProperty(name="Name", description="Name of the property", items=get_user_props)
    user_prop_zero: bpy.props.BoolProperty(name="Zero on Bake", description="Resets property to zero when bake starts", default=True)
    shutdown_after: bpy.props.BoolProperty(name="Shutdown on Completion", description="Attempt to shutdown system after batch bake completes", default=False, update=shutdown_update)

    def init(self, context):
        BakeWrangler_Tree_Node.init(self, context)
        self.inputs.new('BakeWrangler_Socket_Bake', "Bake")

    def draw_buttons(self, context, layout):
        BakeWrangler_Tree_Node.draw_bake_button(self, layout, 'OUTLINER', "Bake All")
        row = layout.row(align=True)
        if not len(self.vert_files):
            row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply")
            row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard")
        else:
            op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply")
            op1.tree = self.id_data.name
            op1.node = self.name
            op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard")
            op2.tree = self.id_data.name
            op2.node = self.name
        layout.label(text="Bake Images:")

    def draw_buttons_ext(self, context, layout):
        col = layout.column(align=False)
        
        col.label(text="Pre-Bake Script:")
        row = col.row(align=True)
        row.prop_enum(self, "loc_pre", "NON")
        row.prop_enum(self, "loc_pre", "INT")
        row.prop_enum(self, "loc_pre", "EXT")
        if self.loc_pre == 'EXT':
            col.prop(self, "script_pre", text="")
        elif self.loc_pre == 'INT':
            col.prop_search(self, "script_pre_int", bpy.data, "texts", text="")

        col.label(text="Post-Bake Script:")
        row = col.row(align=True)
        row.prop_enum(self, "loc_post", "NON")
        row.prop_enum(self, "loc_post", "INT")
        row.prop_enum(self, "loc_post", "EXT")
        if self.loc_post == 'EXT':
            col.prop(self, "script_post", text="")
        elif self.loc_post == 'INT':
            col.prop_search(self, "script_post_int", bpy.data, "texts", text="")
        
        row = col.row(align=True)
        row.prop(self, "user_prop", text="Increment Property")
        if self.user_prop:
            col.prop(self, "user_prop_type")
            col.prop_search(self, "user_prop_objt", bpy.data, self.user_prop_type)
            col.prop(self, "user_prop_name")
            col.prop(self, "user_prop_zero")
        col.prop(self, "shutdown_after")



#
# Node Categories
#

import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem

# Base class for the node category menu system
class BakeWrangler_Node_Category(NodeCategory):
    @classmethod
    def poll(cls, context):
        tree_type = getattr(context.space_data, "tree_type", None)
        return tree_type == 'BakeWrangler_Tree'

# List of all bakery nodes put into categories with identifier, name
BakeWrangler_Node_Categories = [
    BakeWrangler_Node_Category('BakeWrangler_Settings', "Settings", items=[
        NodeItem("BakeWrangler_MeshSettings"),
        NodeItem("BakeWrangler_SampleSettings"),
        NodeItem("BakeWrangler_PassSettings"),
        NodeItem("BakeWrangler_OutputSettings"),
    ]),
    BakeWrangler_Node_Category('BakeWrangler_Nodes', "Bake", items=[
        NodeItem("BakeWrangler_Input_Filenames"),
        NodeItem("BakeWrangler_Input_ObjectList"),
        NodeItem("BakeWrangler_Bake_Billboard"),
        NodeItem("BakeWrangler_Bake_Material"),
        NodeItem("BakeWrangler_Bake_Mesh"),
        NodeItem("BakeWrangler_Sort_Meshes"),
        NodeItem("BakeWrangler_Bake_Pass"),
        NodeItem("BakeWrangler_Output_Image_Path"),
        NodeItem("BakeWrangler_Output_Vertex_Cols"),
        NodeItem("BakeWrangler_Output_Batch_Bake"),
    ]),
    BakeWrangler_Node_Category('BakeWrangler_Post', "Post", items=[
        NodeItem("BakeWrangler_Channel_Map"),
        NodeItem("BakeWrangler_Post_MixRGB"),
        NodeItem("BakeWrangler_Post_SplitRGB"),
        NodeItem("BakeWrangler_Post_JoinRGB"),
        NodeItem("BakeWrangler_Post_Math"),
        NodeItem("BakeWrangler_Post_Gamma"),
    ]),
    BakeWrangler_Node_Category('BakeWrangler_Layout', "Layout", items=[
        NodeItem("NodeFrame"),
        NodeItem("NodeReroute"),
    ]),
]


#
# Registration
#

# All bakery classes that need to be registered
classes = (
    BakeWrangler_Operator_Dummy,
    BakeWrangler_Operator_Dummy_VCol,
    BakeWrangler_Operator_FilterToggle,
    BakeWrangler_Operator_DoubleVal,
    BakeWrangler_Operator_PickMenuEnum,
    BakeWrangler_Operator_AddSelected,
    BakeWrangler_Operator_DiscardBakedVertCols,
    BakeWrangler_Operator_ApplyBakedVertCols,
    BakeWrangler_Operator_BakeStop,
    BakeWrangler_Operator_BakePass,
    BakeWrangler_Tree,
    BakeWrangler_Socket_Object,
    BakeWrangler_Socket_Material,
    BakeWrangler_Socket_Mesh,
    BakeWrangler_Socket_Color,
    BakeWrangler_Socket_ChanMap,
    BakeWrangler_Socket_Float,
    BakeWrangler_Socket_Bake,
    BakeWrangler_Socket_MeshSetting,
    BakeWrangler_Socket_PassSetting,
    BakeWrangler_Socket_SampleSetting,
    BakeWrangler_Socket_OutputSetting,
    BakeWrangler_Socket_ObjectNames,
    BakeWrangler_Socket_SplitOutput,
    BakeWrangler_MeshSettings,
    BakeWrangler_SampleSettings,
    BakeWrangler_PassSettings,
    BakeWrangler_OutputSettings,
    BakeWrangler_Input_ObjectList,
    BakeWrangler_Input_Filenames,
    BakeWrangler_Bake_Billboard,
    BakeWrangler_Bake_Material,
    BakeWrangler_Bake_Mesh,
    BakeWrangler_Sort_Meshes,
    BakeWrangler_Bake_Pass,
    BakeWrangler_Channel_Map,
    BakeWrangler_Post_MixRGB,
    BakeWrangler_Post_SplitRGB,
    BakeWrangler_Post_JoinRGB,
    BakeWrangler_Post_Math,
    BakeWrangler_Post_Math_OpMenu,
    BakeWrangler_Post_Gamma,
    BakeWrangler_Output_Image_Path,
    BakeWrangler_Output_Vertex_Cols,
    BakeWrangler_Output_Batch_Bake,
)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)
    nodeitems_utils.register_node_categories('BakeWrangler_Nodes', BakeWrangler_Node_Categories)


def unregister():
    nodeitems_utils.unregister_node_categories('BakeWrangler_Nodes')
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)



if __name__ == "__main__":
    register()
