#Copyright 2024 Greyscalegorilla, Inc.

import hou
import threading
from queue import Queue
import socket
import json
import re
import os
import errno

IS_DEBUG = False

IS_PARAMETERS_DEBUG = False

ATTACH_DEBUGGER = False
if ATTACH_DEBUGGER:
    import ptvsd
    ptvsd.enable_attach(address=('localhost', 5678))

MESSAGING_VERSION = "1.0"

version = 'v1.4.2'

# Logging class. On level
# <= 0 : none
# 1    : errors
# 2    : info (default)
# >= 3 : debug
#
class Log:
    def __init__(self):
        self.level = int(os.environ.get('GREYSCALEGORILLA_HOUDINI_LOG_LEVEL') or 2)
        if IS_DEBUG:
            self.level = 3
        self.prefix = "Greyscalegorilla Connect " + version

    def debug(self, message):
        if self.level >= 3:
            print(f"{self.prefix} - Debug: {message}")

    def info(self, message):
        if self.level >= 2:
            print(f"{self.prefix} - Info: {message}")

    def error(self, message):
        if self.level >= 1:
            print(f"{self.prefix} - Error: {message}")

log = Log()

error_codes = {
    "InvalidMetadata": 1,
    "ModelsNotFound": 2,
    "ModelsNotLoaded": 3,
    "MaterialsNotFound": 4,
    "MaterialsNotLoaded": 5,
}


# Communications

class CRCException(Exception):
    pass


class Protocol:
    def __init__(self):
        self.start_delimiter = b"!!L:"
        self.end_delimiter = b'E!!'
        self.start_delimiter_size = len(self.start_delimiter)
        self.end_delimiter_size = len(self.end_delimiter)
        self.length_size = 2
        self.crc_size = 2
        self.prefix_size = len(self.start_delimiter) + self.length_size
        self.suffix_size = len(self.end_delimiter) + self.crc_size

    def encode_message(self, message):
        prefix = self.generate_prefix(message)
        suffix = self.generate_suffix(message)
        encoded_message = prefix + message.encode() + suffix
        return encoded_message

    def generate_prefix(self, message):
        length = len(message)
        prefix = self.start_delimiter + length.to_bytes(2, byteorder='big')
        return prefix

    def generate_suffix(self, message):
        crc = self.crc16(message.encode())
        suffix = crc.to_bytes(2, byteorder='big') + self.end_delimiter
        return suffix

    def decode_buffer(self, buffer):
        messages = buffer.split(self.end_delimiter)
        decoded_messages = []
        for message in messages:
            if message.startswith(self.start_delimiter):
                prefix = message[0:self.prefix_size]
                length = int.from_bytes(
                    prefix[self.start_delimiter_size:self.start_delimiter_size + self.length_size], byteorder='big')
                received_message = message[self.prefix_size:self.prefix_size + length]
                suffix = message[self.prefix_size + length:]
                crc_received = int.from_bytes(
                    suffix[0:self.crc_size], byteorder='big')
                crc_calculated = self.crc16(received_message)
                if crc_received == crc_calculated:
                    decoded_messages.append(received_message.decode())
                else:
                    raise CRCException(
                        f'Calculated and received checksum do not match.\nExpected checksum: {crc_calculated}, received checksum: {crc_received}')
        return decoded_messages

    def crc16(self, message):
        poly = 0x1021
        crc = 0xffff

        for byte in message:
            v = 0x80
            for _ in range(8):
                if crc & 0x8000:
                    xor_flag = 1
                else:
                    xor_flag = 0

                crc = crc << 1
                if byte & v:
                    crc += 1
                if xor_flag:
                    crc = crc ^ poly
                v = v >> 1

        return crc & 0xffff


protocol = Protocol()


class SocketServer:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connections = []
        self.threading_event = threading.Event()
        self.on_message = None
        self.server_thread = None

    def start_server(self, on_message):
        try:
            self.on_message = on_message
            self.socket.bind((self.host, self.port))
            self.socket.listen(5)
            log.info(f"Running on port {self.port}")
            self.accept_connections()
        except Exception:
            log.error(
                f"Server could not start at port: {self.port}. " +
                "Please make sure this is the only Houdini instance running " +
                "and there\'s no other applications occupying this port."
            )

    def accept_connections(self):
        while not self.threading_event.is_set():
            try:
                client_socket, client_address = self.socket.accept()
                log.info("Connection to Studio established.")
                log.debug(f"Connected to client at {client_address}")

                self.client_handler = threading.Thread(
                    target=self.handle_client, args=(client_socket, client_address))
                self.client_handler.start()
                self.connections.append(
                    (self.client_handler, client_socket, client_address))
            except socket.error as e:
                win_connection_error = hasattr(
                    errno, 'ENOTSOCK') and errno.ENOTSOCK == e.errno
                unix_connection_error = hasattr(
                    errno, 'WSAENOTSOCK') and errno.WSAENOTSOCK == e.errno
                if win_connection_error or unix_connection_error:
                    log.error(f"Socket error: {e}")
                    break

    def handle_client(self, client_socket, client_address):
        try:
            ack_message = protocol.encode_message(
                "greyscalegorillaconnect-connected")
            client_socket.sendall(ack_message)
            while not self.threading_event.is_set():
                messages = self.receive_messages(client_socket)
                if not messages:
                    continue
                self.handle_messages(messages, client_socket, client_address)
        except socket.error as e:
            win_connection_reset = hasattr(
                errno, 'ECONNRESET') and errno.ECONNRESET == e.errno
            unix_connection_reset = hasattr(
                errno, 'WSAECONNRESET') and errno.WSAECONNRESET == e.errno
            if win_connection_reset or unix_connection_reset:
                log.info("Sockets connection aborted.")
            else:
                log.error(f"Error with client {client_address}: {e}")
        finally:
            log.debug(f"Client {client_address} disconnected")
            log.info("Studio disconnected.")
            client_socket.close()
            self.remove_connection(client_address)

    def handle_messages(self, messages, client_socket, client_address):
        log.debug(f"Received messages from client {client_address}: {messages}")
        if len(messages) == 0:
            return
        message = messages.pop(0)
        if message == "ping":
            response = "pong"
            encoded_message = protocol.encode_message(response)
            client_socket.sendall(encoded_message)
        else:
            log.debug(f"Received message: {message}") 
            threading.Thread(
                target=self.on_message,
                args=(message, client_socket)
            ).start()

    def receive_messages(self, client_socket):
        try:
            data = client_socket.recv(1024)
            return protocol.decode_buffer(data)
        except CRCException:
            self.handle_crc_exception(client_socket)
            return []

    def handle_crc_exception(self, client_socket):
        error_message = "Error decoding the message from Studio."
        log.error(error_message)
        encoded_message = protocol.encode_message(error_message)
        client_socket.sendall(encoded_message)

    def remove_connection(self, client_address):
        for i, (_, _, addr) in enumerate(self.connections):
            if addr == client_address:
                del self.connections[i]
                break

    def start(self, on_message):
        self.server_thread = threading.Thread(
            target=self.start_server, args=(on_message,))
        self.server_thread.start()

    def stop(self):
        self.threading_event.set()
        for _, sock, _ in self.connections:
            sock.close()
        self.socket.close()
        self.server_thread.join()

    def on_task_completed(self, messages, client_socket, client_address):
        log.debug('Replying...')
        response = "completed"
        encoded_message = protocol.encode_message(response)
        client_socket.sendall(encoded_message)
        self.handle_messages(messages, client_socket, client_address)


class TaskManager:
    def __init__(self):
        self.queue = Queue()
        self.runners = {}

    def add(self, task_type, payload):
        task = self.create_task(task_type, payload)
        self.queue.put(task)

    def check_pending(self):
        if not self.queue.empty():
            task = self.queue.get()
            self.execute(task)

    def execute(self, task):
        task_runner = self.runners.get(task.get('type'))
        if not task_runner:
            log.debug(f'No task runner registered for task type: {task["type"]}')
            return
        task_runner(task['payload'])

    def register(self, task_type, runner):
        self.runners[task_type] = runner

    def create_task(self, task_type, payload):
        return {"type": task_type, "payload": payload}


# Import Materials utils

ocio = False

try:
    import PyOpenColorIO
    ocio = True
except ImportError:
    ocio = False

supported_extensions = ['.jpg', '.tif', '.tiff', '.exr']

known_maps = [
    'ambientocclusion',
    'specularlevel',
    'specularedgecolor',
    'opacity',
    'metallic',
    'roughness',
    'basecolor',
    'puzzlematte',
    'normal',
    'normal-wrinkle',
    'height',
    'anisotropylevel',
    'anisotropyangle',
    'emissioncolor',
    'emissionintensity',
    'sheenroughness',
    'sheenopacity',
    'sheencolor',
    'indexofrefraction',
    'absorptiondistance',
    'dispersion',
    'absorptioncolor',
    'translucency',
    'scatteringweight',
    'redshift',
    'scatteringdistance',
    'rayleighscattering',
    'scatteringdistancescale',
    'scatteringcolor',
    'coatnormalscale',
    'coatindexofrefraction',
    'coatopacity',
    'coatspecularlevel',
    'coatnormal',
    'coatroughness',
    'coatcolor',
    "transcolor",
    "transweight",
    "transdepth",
    "transscattercolor",
    "transscatteranisotropy",
    "transdispersion",
    "transroughness"
]

nextNodeId = [0]


def isNumeric(value):
    t = type(value)
    return (t == float) or (t == int)


def makeNode(nodeType, nodeName, isVectorOutput, nodePayload):
    nodeId = nextNodeId[0]
    nextNodeId[0] += 1
    return {"nodeId": nodeId, "nodeType": nodeType, "nodeName": nodeName, "isVectorOutput": isVectorOutput, "nodePayload": nodePayload}


def multiply(input1, input2):
    if input1 and input2:
        return makeNode("multiply", None, input1["isVectorOutput"] or input2["isVectorOutput"], {"multiplier": input1, "multiplicant": input2})
    else:
        return input1 or input2


def get_model_metadata(mesh_path):
    mesh_params = {}
    with open(mesh_path, mode='r', encoding='utf8') as f:
        meta = json.loads(f.read())
        mesh_params = {'meta': meta}
        dir_path = os.path.dirname(mesh_path)
        for entry in os.listdir(dir_path):
            if not entry.startswith('.'):
                (root, ext) = os.path.splitext(os.fsdecode(entry).lower())
        mesh_params['path'] = mesh_path
    base_name = os.path.basename(mesh_path)
    file_name, _ = os.path.splitext(base_name)
    return mesh_params


def get_models_metadata(path, on_metadata_received):
    def process_path(path, models_metadata=[]):
        if os.path.isdir(path):
            for entry in os.listdir(path):
                process_path(os.path.join(path, entry), models_metadata)
        else:
            (root, ext) = os.path.splitext(
                os.path.basename(os.fsdecode(path)))
            if ext.lower() == '.gsga':
                try:
                    model_metadata = get_model_metadata(path)
                    is_model = model_metadata.get('meta') and model_metadata['meta'].get('type') == 'model'
                    if model_metadata and is_model:
                        model_metadata['name'] = root.replace('_', ' ').strip()
                        models_formats = ['.obj', '.fbx', '.abc']
                        model_directory = os.path.dirname(path)

                        for file_name in os.listdir(model_directory):
                            file_path = os.path.join(model_directory, file_name)
                            if os.path.isfile(file_path) and any(file_name.lower().endswith(format) for format in models_formats):
                                model_metadata['path'] = file_path
                                break
                        models_metadata.append(model_metadata)
                except Exception:
                    error_message = f"Error retrieving models metadata at path: {path}"
                    log.error(error_message)
                    models_metadata.append({
                        'error': {
                            "error_code": error_codes['ModelsNotFound'],
                            "message": error_message,
                            "path_to_asset": path,
                        }
                    })

        return models_metadata

    try:
        models_metadata = process_path(path)
        if not len(models_metadata):
            error_message = "No Greyscalegorilla models found at path."
            log.error(error_message)
            models_metadata.append({
                'error': {
                    "error_code": error_codes['ModelsNotFound'],
                    "message": error_message,
                }
            })
        on_metadata_received(models_metadata)

    except Exception as e:
        log.error(f'Error: {e}')


def process_models(payload):
    models_metadata = payload.get('metadata')
    on_model_success = payload.get('on_success')
    on_model_error = payload.get('on_error')
    for model_metadata in models_metadata:
        if 'error' not in model_metadata:
            load_fbx_model(model_metadata)

    errors = []
    succeses = []
    for model_metadata in models_metadata:
        if 'error' in model_metadata:
            errors = [model_metadata['error']]
        else:
            succeses.append(
                {"path_to_asset": model_metadata['path'].replace('\\', '/')})

    if errors:
        on_model_error(errors)
    on_model_success(succeses)



def get_sequence_range(directory_path, extension):
    files = [f for f in os.listdir(directory_path) if f.lower().endswith(extension)]
    
    if not files:
        return False, ''
        
    # Find files that match the pattern name_###.<extension>
    pattern = re.compile(r'(.+?)(\d+)\.(jpg|tif)$', re.IGNORECASE)
    
    first_file_name, matches = '', 0
    padded_filename = ''

    padding = 0

    for file in files:
        match = pattern.match(file)
        if match:
            if first_file_name == '':
                first_file_name = file

            # count the number of digits
            if padding == 0:
                padding = len(match.group(2))
                padded_filename = match.group(1) + '$F' + str(int(padding)) + '.' + match.group(3)

            matches += 1

    is_sequence = matches > 1
    return is_sequence, padded_filename if is_sequence else first_file_name


def get_textures_metadata(path, on_metadata_received):
    def process_path(path, textures_metadata=[]):
        if os.path.isdir(path):
            for entry in os.listdir(path):
                process_path(os.path.join(path, entry), textures_metadata)
        else:
            (root, ext) = os.path.splitext(os.path.basename(os.fsdecode(path)))
            if ext.lower() == '.gsga':
                with open(path, mode='r', encoding='utf8') as f:
                    texture_meta = {}
                    json_meta = json.loads(f.read())
                    texture_type = json_meta.get('type')
                    is_texture = texture_type in ('hdri', 'texture', 'gobo')
                    is_sequence = False
                    if is_texture:
                        texture_meta['name'] = root.replace(' ', '_').strip()
                        texture_meta['type'] = texture_type
                        texture_directory = os.path.dirname(path)

                        if texture_type == 'gobo':
                            # Check if the texture is a jpg sequence
                            extension = '.jpg'
                            is_sequence, first_file_name = get_sequence_range(texture_directory, extension)
                            if not is_sequence:
                                # Try again with .tif
                                extension = '.tif'
                                is_sequence, first_file_name = get_sequence_range(texture_directory, extension)

                            if is_sequence:
                                if extension == '.jpg':
                                    texture_meta['jpg_texture_path'] = os.path.join(texture_directory, first_file_name)
                                else:
                                    texture_meta['tif_texture_path'] = os.path.join(texture_directory, first_file_name)

                        if not is_sequence:
                            # Collect the image files in this directory, regardless of the texture type
                            # defined in the gsga file.
                            for file_name in os.listdir(texture_directory):
                                file_path = os.path.join(texture_directory, file_name)
                                if os.path.isfile(file_path):
                                    lower_file_name = file_name.lower()
                                    # check if file_name contains 'preview'
                                    if 'preview' in lower_file_name:
                                        continue
                                    if lower_file_name.endswith('.hdr'):
                                        texture_meta['hdr_texture_path'] = file_path
                                    elif lower_file_name.endswith('.exr'):
                                        texture_meta['exr_texture_path'] = file_path
                                    elif lower_file_name.endswith('.jpg'):
                                        texture_meta['jpg_texture_path'] = file_path
                                    elif lower_file_name.endswith('.tif'):
                                        texture_meta['tif_texture_path'] = file_path

                        textures_metadata.append(texture_meta)

        return textures_metadata

    try:
        textures_metadata = process_path(path)
        if not len(textures_metadata):
            error_message = "No textures found at path."
            log.error(error_message)
            textures_metadata.append({
                'error': {
                    "error_code": error_codes['ModelsNotFound'],
                    "message": error_message,
                }
            })
        on_metadata_received(textures_metadata)

    except Exception as e:
        log.error(f'Error: {e}')


def process_textures(payload):
    
    def get_best_texture(in_texture_metadata):
        path = ''
        # Look for the path independently from the type (hdri, gobo...), because any extension could be
        # used for any type of light. I think our current convention is to use
        # - hdri for dome and area light
        # - jpg for gobo
        # but nothing prevents to change this. For now, let's give precedence to hdr and exr.
        if 'hdr_texture_path' in in_texture_metadata:
            path = in_texture_metadata['hdr_texture_path']
        elif 'exr_texture_path' in in_texture_metadata:
            path = in_texture_metadata['exr_texture_path']
        elif 'jpg_texture_path' in in_texture_metadata:
            path = in_texture_metadata['jpg_texture_path']
        elif 'tif_texture_path' in in_texture_metadata:
            path = in_texture_metadata['tif_texture_path']
        else:
            pass
            #log_error(f"No valid file extension found")
        return path
    
    def create_light_or_switch_texture(light, in_path, in_user_selected):
        if in_user_selected:
            if not light.IsValid():
                return
        else:
            light.Create()
        light.SetTexture(in_path)

    def area_light(in_path, in_renderer, in_user_selected):
        light = None
        if in_user_selected:
            # We can't rely on in_renderer, because on just selecting a texture
            # the renderer is not part of the message
            light = RsAreaLight(True)
            if not light.IsValid():
                light = KaAreaLight(True)

        # No valid light is currently selected
        if in_user_selected and light is None:
            return

        if light is None or not light.IsValid():
            log.info(f'Creating new area light for {in_path}')
            light = RsAreaLight() if in_renderer == 'REDSHIFT' else KaAreaLight()

        create_light_or_switch_texture(light, in_path, in_user_selected)

    def gobo_light(in_path, in_renderer, in_user_selected):
        light = None
        if in_user_selected:
            light = RsGoboLight(True)
            if not light.IsValid():
                light = KaGoboLight(True)

        if in_user_selected and light is None:
            return

        if light is None or not light.IsValid():
            log.info(f'Creating new gobo light for {in_path}')
            light = RsGoboLight() if in_renderer == 'REDSHIFT' else KaGoboLight()

        create_light_or_switch_texture(light, in_path, in_user_selected)

    def dome_light(in_path, in_renderer, in_user_selected):
        light = None
        if in_user_selected:
            light = RsDomeLight(True)
            if not light.IsValid():
                light = KaDomeLight(True)

        if in_user_selected and light is None:
            return

        if light is None or not light.IsValid():
            log.info(f'Creating new dome light for {in_path}')
            light = RsDomeLight() if in_renderer == 'REDSHIFT' else KaDomeLight()

        create_light_or_switch_texture(light, in_path, in_user_selected)

    def load_surface_imperfections(in_path, in_renderer):
        si = SurfaceImperfections(in_renderer)

        if si.IsValid():
            log.info(f'Loading surface imperfections {in_path}')
            si.Create(in_path)
        else:
            log.error('Invalid selection or context for loading surface imperfections')
        return


    textures_metadata = payload.get('metadata')
    textures_metadata = textures_metadata
    user_selected = payload.get('user_selected')
    renderer = payload.get('renderer')
    surface_imperfections = payload.get('surface_imperfections')

    # Surface Imperfections:
    if surface_imperfections:
        for texture_metadata in textures_metadata:
            path = get_best_texture(texture_metadata)
            if not path == '':
                load_surface_imperfections(path, renderer)
        return
    
    # Textured Lights:
    for texture_metadata in textures_metadata:
        path = get_best_texture(texture_metadata)
        if path == '':
            continue

        if texture_metadata['type'] == 'hdri':
            dome_light(path, renderer, user_selected)
        elif texture_metadata['type'] == 'texture':
            area_light(path, renderer, user_selected)
        elif texture_metadata['type'] == 'gobo':
            gobo_light(path, renderer, user_selected)


def get_materials_metadata(path, on_metadata_received=None):
    paths_materials = []
    for (dirpath, dirnames, filenames) in os.walk(path):
        for filename in filenames:
            (root, ext) = os.path.splitext(filename)
            if ext.lower() == ".gsgm":
                file_path = os.path.join(dirpath, filename)
                readable = os.access(file_path, os.R_OK)
                if readable:
                    try:
                        path_material, has_maps = getPathMaterial(file_path)
                        if path_material:
                            path_material["name"] = root.replace("_", " ").strip()
                            path_material["has_maps"] = has_maps
                            paths_materials.append(path_material)
                        else:
                            error_message = f"No materials metadata found at path: {path_material}"
                            paths_materials.append({
                                'error': {
                                    "error_code": error_codes['MaterialsNotFound'],
                                    "message": error_message,
                                    "path_to_asset": path,
                                },
                            })
                    except Exception:
                        error_message = f"No materials metadata found at path: {file_path}"
                        paths_materials.append({
                            'error': {
                                "error_code": error_codes['MaterialsNotFound'],
                                "message": error_message,
                                "path_to_asset": path,
                            },
                        })
                        log.error(error_message)


    if on_metadata_received:
        on_metadata_received(paths_materials)
    return paths_materials


def process_material(payload):
    materials_metadata = payload.get('metadata')
    renderer = payload.get('renderer')
    is_triplanar = payload.get('triplanar')
    on_material_success = payload.get('on_success')
    on_material_error = payload.get('on_error')
    ropNodeNamesMap = {
        'Karma_ROP': 'karma',
        'Redshift_ROP': 'redshift'
    }

    candidates_cnt = len(materials_metadata)
    if candidates_cnt == 0:
        error_message = "No Greyscalegorilla materials found."
        on_material_error([{
            'error': {
                "error_code": error_codes['MaterialsNotFound'],
                "message": error_message,
            }
        }])
        if on_material_success:
            on_material_success([])
        log.error(error_message)
        return

    availableRenderers = [
        r for r in renderers if r.isAvailable() and ropNodeNamesMap.get(r.RopNodeName) == renderer.lower()]

    if not availableRenderers:
        error_message = f'{renderer} is not available'
        on_material_error([{
            'error': {
                "error_code": error_codes['MaterialsNotLoaded'],
                "message": error_message,
            }
        }])
        if on_material_success:
            on_material_success([])
        log.error(error_message)
        return

    parent = hou.node("/mat")
    for renderer in availableRenderers:
        for material_metadata in materials_metadata:
            if 'error' not in material_metadata:
                try:
                    materialNode = renderer.addMaterial(parent, material_metadata, is_triplanar)
                    materialNode.layoutChildren()

                    # temp fix for Houdini 20.5, CON-411, crashing on materialNode.setCurrent
                    setCurrent = True
                    # Not sure how RS behaves in 20.5, let's be difensive
                    # if renderer.RopNodeName == 'Karma_ROP':
                    versionStr = hou.applicationVersionString()
                    versionData = versionStr.split(".")
                    majorVersion = int(versionData[0])
                    minorVersion = int(versionData[1])
                    isAtLeast205 = majorVersion > 20 or (majorVersion == 20 and minorVersion >= 5)
                    setCurrent = not isAtLeast205

                    if setCurrent:
                        materialNode.setCurrent(True)
                    
                    #log.info(f'Imported material: {materialNode.name()}')
                except Exception:
                    material_metadata['error'] = f"Error creating material: {material_metadata['name']}"

    # Iteate over all materials metadata having parsed and processed all of the materials metadata
    errors = []
    successes = []
    for material_metadata in materials_metadata:
        if 'error' in material_metadata:
            errors.append(material_metadata['error'])
        else:
            successes.append({"path_to_asset": material_metadata['path'].replace('\\', '/')})
    if errors:
        on_material_error(errors)
    if on_material_success:
        on_material_success(successes)


def createEmptyBuilder(parent, node_type_name, name):
    builder = parent.createNode(
        node_type_name, name, force_valid_node_name=True)
    if not builder.name().startswith(name.replace(" ", "_")):
        # Prevent auto-incrementing material names that end with a digit
        builder.destroy()
        builder = parent.createNode(node_type_name, name + "_1", force_valid_node_name=True)
    builder.moveToGoodPosition()
    builder.deleteItems(builder.children())
    return builder


def makeNodeCreator(parent):
    def createBuilderNode(node_type_name, node_name=None, params=None):
        node = parent.createNode(node_type_name, node_name=node_name, force_valid_node_name=True)
        if params:
            for (name, value) in params.items():
                node.parm(name).set(value)
        node.setDetailMediumFlag(True)

        class Node():
            def setParameter(self, inputParmName, value):
                if isinstance(value, (float, int, str)):
                    node.parm(inputParmName).set(value)
                elif isinstance(value, tuple):
                    for idx, ch in enumerate(("r", "g", "b")):
                        node.parm(inputParmName + ch).set(value[idx])
                elif isinstance(value, hou.ParmTuple):
                    dst = node.parmTuple(inputParmName)
                    assert (len(dst) >= len(value))
                    relPath = dst.node().relativePathTo(value.node())
                    for i in range(len(value)):
                        dst[i].setExpression("ch('" + relPath + "/" + value[i].name() + "')")
                else:
                    node.setNamedInput(inputParmName, value["socketNode"], value["socketName"])
                    
            def getParameter(self, inputParmName):
                return node.parm(inputParmName).eval()

            def setVectorParameter(self, inputParmName, value):
                if isinstance(value, tuple):
                    for idx, ch in enumerate(("x", "y", "z")):
                        node.parm(inputParmName + ch).set(value[idx])

            def getHoudiniNode(self):
                return node

            def outputSocket(self, socketName):
                return {"socketNode": node, "socketName": socketName}

        return Node()
    return createBuilderNode


class ColorConverter:
    def __init__(self, in_is_ocio):
        self.is_ocio = False
        # in_is_ocio will be False for Redshift, we want to manually convert to Linear Rec.709
        if in_is_ocio:
            ocio_config_file = os.environ.get('OCIO')
            if ocio_config_file is not None and os.path.exists(ocio_config_file):
                # For Karma, we look up the scene_linear role (that is the rendering color space)
                self.config = PyOpenColorIO.Config.CreateFromFile(ocio_config_file)
                self.narrow_space = ''
                self.linear_space = self.GetRole('scene_linear')
                if self.linear_space != '':
                    if self.HasColorSpace('sRGB - Texture'):
                        self.narrow_space = 'sRGB - Texture'
                        self.is_ocio = True
                    # other config version may use sRGB instead of sRGB - Texture
                    elif self.HasColorSpace('sRGB'):
                        self.narrow_space = 'sRGB'
                        self.is_ocio = True

        # if something goes wrong, we default to Linear Rec.709

    def GetRole(self, in_name: str) -> str:
        for role_name, space_name in self.config.getRoles():
            if role_name == in_name:
                return space_name
        return ''
    
    def HasColorSpace(self, in_name: str) -> bool:
        return self.config.getColorSpace(in_name) is not None

    def SrgbToLinearRec709(self, in_srgb: dict) -> dict:
        def inverse_gamma_correct(channel):
            if channel <= 0.04045:
                return channel / 12.92
            else:
                return ((channel + 0.055) / 1.055) ** 2.4

        linear_r = inverse_gamma_correct(in_srgb['r'])
        linear_g = inverse_gamma_correct(in_srgb['g'])
        linear_b = inverse_gamma_correct(in_srgb['b'])
        return {'r':linear_r, 'g':linear_g, 'b':linear_b}
    
    def ToLinear(self, in_srgb: dict, in_clamp: bool = True) -> dict:
        if self.is_ocio:
            color = hou.Color(in_srgb['r'], in_srgb['g'], in_srgb['b'])
            # convert to the rendering color space
            color_t = color.ocio_transform(self.narrow_space, self.linear_space, '').rgb()
            if not in_clamp:
                return {'r': color_t[0], 'g': color_t[1], 'b': color_t[2]}
            return {'r': max(0, min(1, color_t[0])), 'g': max(0, min(1, color_t[1])), 'b': max(0, min(1, color_t[2]))}
        else:
            color_t = self.SrgbToLinearRec709(in_srgb)
            if not in_clamp:
                return color_t
            return {'r': max(0, min(1, color_t['r'])), 'g': max(0, min(1, color_t['g'])), 'b': max(0, min(1, color_t['b']))}


class ShaderParams:
    def __init__(self, meta, shaderName, render_id):

        # mapping of the Arnold standard_surface parameters to the redshift and karma ones
        #
        self.standard_surface_parameters = {
                    "base":                    ["base_color_weight",   "base"],
                    "base_color":              ["base_color",          "base_color"],
                    "caustics":                ["UNSUPPORTED",         "UNSUPPORTED"],
                    "coat":                    ["coat_weight",         "coat"],
                    "coat_affect_color":       ["UNSUPPORTED",         "coat_affect_color"],
                    "coat_affect_roughness":   ["UNSUPPORTED",         "coat_affect_roughness"],
                    "coat_anisotropy":         ["coat_aniso",          "coat_anisotropy"],
                    "coat_color":              ["coat_color",          "coat_color"],
                    "coat_IOR":                ["coat_ior",            "coat_IOR"],
                    "coat_normal":             ["UNSUPPORTED",         "coat_normal"],
                    "coat_rotation":           ["coat_aniso_rotation", "coat_rotation"],
                    "coat_roughness":          ["coat_roughness",      "coat_roughness"],
                    "diffuse_roughness":       ["diffuse_roughness",   "diffuse_roughness"],
                    "emission":                ["emission_weight",     "emission"],
                    "emission_color":          ["emission_color",      "emission_color"],
                    "internal_reflections":    ["UNSUPPORTED",         "UNSUPPORTED"],
                    "metalness":               ["metalness",           "metalness"],
                    "opacity":                 ["opacity_color",       "opacity"],
                    "sheen":                   ["sheen_weight",        "sheen"],
                    "sheen_color":             ["sheen_color",         "sheen_color"],
                    "sheen_roughness":         ["sheen_roughness",     "sheen_roughness"],
                    "specular":                ["refl_weight",         "specular"],
                    "specular_anisotropy":     ["refl_aniso",          "specular_anisotropy"],
                    "specular_color":          ["refl_color",          "specular_color"],
                    "specular_IOR":            ["refl_ior",            "specular_IOR"],
                    "specular_rotation":       ["refl_aniso_rotation", "specular_rotation"],
                    "specular_roughness":      ["refl_roughness",      "specular_roughness"],
                    "subsurface":              ["ms_amount",           "subsurface"],
                    "subsurface_anisotropy":   ["ms_phase",            "subsurface_anisotropy"],
                    "subsurface_color":        ["ms_color",            "subsurface_color"],
                    "subsurface_radius":       ["ms_radius",           "subsurface_radius"],
                    "subsurface_scale":        ["ms_radius_scale",     "subsurface_scale"],
                    "subsurface_type":         ["ms_mode",             "UNSUPPORTED"],
                    "thin_film_thickness":     ["thinfilm_thickness",  "thin_film_thickness"],
                    "thin_film_IOR":           ["thinfilm_ior",        "thin_film_IOR"],
                    "thin_walled":             ["refr_thin_walled",    "thin_walled"],
                    "transmission":            ["refr_weight",         "transmission"],
                    "transmission_color":      ["refr_color",          "transmission_color"],
                    "transmission_depth":      ["ss_depth",            "transmission_depth"],
                    "transmission_dispersion": ["refr_abbe",           "transmission_dispersion"],
                    "transmission_scatter":    ["ss_scatter_color",    "transmission_scatter"]
        }

        # mapping of the Arnold normal_map parameters to the redshift and karma ones
        #
        self.normal_map_parameters = {
                    "invert_y": ["flipY", "UNSUPPORTED"], # in RS, flipY is flipy in C4D
                    "strength": ["scale", "scale"]
        }

        self.shaderName = shaderName
        self.render_id = render_id
        self.shaderDict = {}
        self.params = {}

        if shaderName == "standard_surface":
            self.shaderDict = self.standard_surface_parameters
        elif shaderName == "normal_map":
            self.shaderDict = self.normal_map_parameters
        elif shaderName == "ramp_rgb":
            pass
        else:
            return

        # Setup to convert all the color parameters from sRGB to linear
        # We won't use OCIO for Redshift, Karma goes with OCIO being available (should always be)
        use_ocio = False if self.render_id == 0 else ocio
        self.color_converter = ColorConverter(use_ocio)

        # fill the default param dict
        self.params = {}

        if shaderName == "standard_surface":
            white = self.Color(1, 1, 1)
            self.params["base"] = 1.0
            self.params["base_color"] = white
            self.params["coat"] = 0
            self.params["coat_affect_color"] = 0              # Not in the unique params json
            self.params["coat_affect_roughness"] = 0          # Not in the unique params json
            self.params["coat_anisotropy"] = 0                # Not in the unique params json
            self.params["coat_color"] = white
            self.params["coat_IOR"] = 1.5
            self.params["coat_normal"] = self.Vector(0.0, 0.0, 0.0) # Not in the unique params json
            self.params["coat_rotation"] = 0                  # Not in the unique params json
            self.params["coat_roughness"] = 0.1
            self.params["diffuse_roughness"] = 0
            self.params["emission"] = 0
            self.params["emission_color"] = white             # Not in the unique params json
            self.params["metalness"] = 0
            self.params["opacity"] = white                    # Not in the unique params json
            self.params["specular"] = 1
            self.params["specular_anisotropy"] = 0
            self.params["specular_color"] = white
            self.params["specular_IOR"] = 1.5
            self.params["specular_rotation"] = 0
            self.params["specular_roughness"] = 0.2
            self.params["subsurface"] = 0
            self.params["subsurface_color"] = white
            self.params["subsurface_radius"] = self.Color(0.1, 0.1, 0.1)
            self.params["subsurface_type"] = "randomwalk"
            self.params["thin_film_IOR"] = 1.5
            self.params["thin_film_thickness"] = 0
            self.params["thin_walled"] = False
            self.params["transmission"] = 0

        # Special handling for the ramp_rgb metadata, whose position and color are arrays
        # We need to read the nb_keys first, then the position_i and color_i
        if shaderName == "ramp_rgb":
            value = self.GetSimpleParamValue(meta, "nb_keys")
            if value is None:
                self.Add("nb_keys", 0)
            else:
                self.Add("nb_keys", value)
                self.params["position"] = []
                self.params["color"] = []
                for i in range(int(value)):
                    value = self.GetSimpleParamValue(meta, "position_" + str(i))
                    self.params["position"].append(value)
                    value = self.GetSimpleParamValue(meta, "color_" + str(i))
                    value = self.color_converter.ToLinear(value)
                    self.params["color"].append(value)
                # falloff:
                value = self.GetSimpleParamValue(meta, "falloff")
                self.Add("falloff", 1.0 if value is None else value)
                # neon_color:
                value = self.GetSimpleParamValue(meta, "neon_color")
                if value is not None:
                    value = self.color_converter.ToLinear(value)
                    self.Add("neon_color", value)
                # roughness weight:
                value = self.GetSimpleParamValue(meta, "carpaint_roughness_weight")
                self.Add("carpaint_roughness_weight", 1.0 if value is None else value)
        
        else:
            for arnoldName in self.shaderDict.keys():
                if not self.shaderDict[arnoldName][self.render_id] == "UNSUPPORTED":
                    value = self.GetSimpleParamValue(meta, arnoldName)
                    if value is not None:
                        if (IS_PARAMETERS_DEBUG):
                            log.info(self.__class__.__name__ + ": reading metadata " + self.shaderName + "." + arnoldName + " = " + str(value))
                        self.Add(arnoldName, value)

            self.ConvertColors()

    def Color(self, r, g, b):
        return {'r': r, 'g': g, 'b': b}

    def Vector(self, x, y, z):
        return {'x': x, 'y': y, 'z': z}

    def Add(self, name, value):
        self.params[name] = value

    def Has(self, name):
        return name in self.params
    
    def GetSimpleParamValue(self, meta, param_name):
        if 'params' in meta and self.shaderName in meta['params'] and param_name in meta['params'][self.shaderName]:
            return meta['params'][self.shaderName][param_name]
        return None

    def Set(self, node, name, value):
        # we must do this extra check, against parameters entered in the dict
        # because of the default set, but possibly not supported by the renderer
        if name == "UNSUPPORTED":
            return
        
        try:
            v = value
            if name == "ms_mode": # handling subsurface_type for Redshift only
                v = "0" if value == "diffusion" else "2" # crazy

            if isinstance(v, dict):
                if 'r' in v:
                    node.setParameter(name, (v['r'], v['g'], v['b']))
                elif 'x' in v: # only case is coat_normal by now
                    node.setVectorParameter(name, (v['x'], v['y'], v['z']))
            else:
                node.setParameter(name, v)

            if (IS_PARAMETERS_DEBUG):
                log.info(self.__class__.__name__ + ": setting " + name + " = " + str(v) + " (from " + self.shaderName + ")")

        except Exception:
            error_message = f"Error setting parameter name: {name}"
            log.error(error_message)

    def ConvertColors(self):
        if len(self.shaderDict) == 0:
            return
        
        for arnoldName in self.params.keys():
            # skipping subsurface_radius, for CON-714
            if arnoldName == "subsurface_radius":
                continue
            value = self.params[arnoldName]
            if isinstance(value, dict) and 'r' in value:
                self.params[arnoldName] = self.color_converter.ToLinear(value)

    def SetAll(self, node):
        if len(self.shaderDict) == 0:
            return
        
        for arnoldName in self.params.keys():
            value = self.params[arnoldName]
            name = self.shaderDict[arnoldName][self.render_id]
            self.Set(node, name, value)

def getPathMaterial(materialPath):
    has_maps = False
    with open(materialPath, mode='r', encoding='utf8') as f:
        meta = json.loads(f.read())

        if meta['version'] != 1:
            raise Exception('Only material version "1" supported')

        if meta['type'] == 'material':
            materialMaps = {}
            dirPath = os.path.dirname(materialPath)
            for entry in os.listdir(dirPath):
                entry_path = os.path.join(dirPath, entry)
                readable = os.access(entry_path, os.R_OK)
                if readable and (not entry.startswith(".")):
                    (root, ext) = os.path.splitext(os.fsdecode(entry).lower())
                    if ext in supported_extensions:
                        for known_map in known_maps:
                            if root.endswith("_" + known_map):
                                if known_map not in materialMaps:
                                    materialMaps[known_map] = []
                                materialMaps[known_map].append(
                                    {"map_ext": ext, "map_path": entry_path})
                                # at least one map exists, so we'll create the texture support nodes
                                has_maps = True
            
            def makeMaterialData(rendererName, heightScaleCoefficient, scatteringweightCoefficient):
                def isNumeric(value):
                    return isinstance(value, (float, int))

                def asNumeric(value):
                    return value if isNumeric(value) else None

                def asVector(value):
                    if not (hasattr(value, "__iter__") and (len(value) == 3)):
                        return None
                    for v in value:
                        if not isNumeric(v):
                            return None
                    return value

                def asColor(value):
                    if not (hasattr(value, "__iter__") and ("r" in value) and ("g" in value) and ("b" in value)):
                        return None
                    for ch in ["r", "g", "b", "a"]:
                        if (ch in value) and not isNumeric(value[ch]):
                            return None
                    return (value["r"], value["g"], value["b"], value["a"] if ("a" in value) else 1)

                def metaValue(path, defaultValue):
                    def settingsValue(settingsPath, defaultValue):
                        value = meta
                        for k in (settingsPath + path):
                            if not (hasattr(value, "__iter__") and (k in value)):
                                return defaultValue
                            value = value[k]
                        return value

                    houdiniRendererValue = settingsValue(
                        ["houdini", rendererName], None)
                    if houdiniRendererValue is not None:
                        return houdiniRendererValue

                    rendererValue = settingsValue([rendererName], None)
                    if rendererValue is not None:
                        return rendererValue

                    houdiniValue = settingsValue(["houdini"], None)
                    if houdiniValue is not None:
                        return houdiniValue
                    
                    return settingsValue(["standardsurface"], defaultValue)
                
                def createTexture(mapName, isSrgb, isVectorOutput):
                    if (mapName is not None) and (mapName in materialMaps):
                        return makeNode("texture", mapName, isVectorOutput, {"isSrgb": isSrgb, "path": sorted(materialMaps[mapName], key=lambda i: supported_extensions.index(i["map_ext"]))[0]["map_path"]})
                    else:
                        None

                def typedMetaValue(metaPath, valueConvert, defaultValue=None):
                    value = valueConvert(metaValue(metaPath, defaultValue))
                    if value is not None:
                        return parameter("_".join(metaPath), " ".join(s.capitalize() for s in metaPath), value)
                    else:
                        return None

                def parameter(name, label, value, folder=None, activeWhen=None):
                    if isinstance(value, (bool, float, int, tuple, list)):
                        return makeNode("parameter", None, isinstance(value, tuple), {"paramName": name, "label": label, "value": value, "folder": folder, "activeWhen": activeWhen})
                    else:
                        raise Exception(
                            "Unhandled \"" + name + "\" parameter type " + str(type(value)) + " (" + str(value) + ")")

                def floatChannel(mapName):
                    return createTexture(mapName, False, False)

                def vectorChannel(mapName):
                    return createTexture(mapName, False, True)

                def colorChannel(mapName):
                    return createTexture(mapName, True, True)

                def getPuzzlematteData(puzzlematteChannel):
                    if puzzlematteChannel:
                        def metaPuzzlematteValue(name, defaultValue): return metaValue(["base", "puzzlemattecolors", name], defaultValue)
                        parmFolderName = "Puzzlematte"
                        usePuzzlematteParmName = "use_puzzlematte"

                        def puzzlematteColor(name, paramLabel, defaultColor):
                            value = asColor(metaPuzzlematteValue(name, None)) or defaultColor
                            return parameter(name, paramLabel, value, parmFolderName, "{ " + usePuzzlematteParmName + " == 0 }")

                        return {
                            "weights":        puzzlematteChannel,
                            "enabled":        parameter(usePuzzlematteParmName, "Use Puzzlematte", metaPuzzlematteValue("enabled", True), parmFolderName),
                            "baseColor":      puzzlematteColor("baseColor",   "Base Color",    (0.694, 0.694, 0.694, 1)),
                            "layer1Color":    puzzlematteColor("layer1Color", "Layer 1 Color", (0.0,   0.078, 0.133, 1)),
                            "layer2Color":    puzzlematteColor("layer2Color", "Layer 2 Color", (0.509, 0.392, 0.246, 1)),
                            "layer3Color":    puzzlematteColor("layer3Color", "Layer 3 Color", (1.0,   0.15,  0.102, 1))
                        }
                    else:
                        return None

                def displaceTextureCreator(nodeType):
                    def createDisplaceTexture(mapName, textureScale=None):
                        texNode = floatChannel(mapName)
                        if texNode:
                            assert (texNode["nodeType"] == "texture")
                            return makeNode(nodeType, None, True, {"textureNode": texNode, "textureScale": textureScale})
                        else:
                            return None
                    return createDisplaceTexture
                
                def carpaintOrNeonTexture(is_neon):
                    if is_neon:
                        return makeNode("neon", 'Neon', False, {})
                    else:
                        return makeNode("carpaint", 'CarPaint', False, {})

                def roughnessWeight(roughnessChannel):
                    if roughnessChannel is None:
                        return None
                    return makeNode("roughnessweight", 'Roughness Weight', False, {"channel": roughnessChannel})

                normalTexture = displaceTextureCreator("normalTexture")
                displacementTexture = displaceTextureCreator("displacementTexture")

                normalData = normalTexture("normal")
                normalWrinkleData = normalTexture("normal-wrinkle")

                basecolorData = colorChannel("basecolor")
                if 'subtype' in meta and meta['subtype'] == 'carpaint':
                    basecolorData = carpaintOrNeonTexture(False)

                puzzlematteData = getPuzzlematteData(floatChannel("puzzlematte"))

                if 'subtype' in meta and meta['subtype'] == 'neon':
                    emissionColorData = carpaintOrNeonTexture(True)
                else:
                    emissionColorData = colorChannel("emissioncolor")

                emissionIntensityData = floatChannel("emissionintensity")

                scatteringweight = floatChannel("scatteringweight")
                if scatteringweight and (scatteringweightCoefficient is not None):
                    scatteringweight = multiply(makeNode("constant", None, False, scatteringweightCoefficient), scatteringweight)

                standardSurfaceParams = {}
                normalMapParams = {}
                rampRgbParams = {}
                render_param_id = 0 if rendererName == "redshift" else 1
                standardSurfaceParams = ShaderParams(meta, "standard_surface", render_param_id)
                normalMapParams = ShaderParams(meta, "normal_map", render_param_id)
                rampRgbParams = ShaderParams(meta, "ramp_rgb", render_param_id)

                if 'subtype' in meta and meta['subtype'] == 'carpaint':
                    roughnessData = roughnessWeight(floatChannel("roughness"))
                else:
                    roughnessData = floatChannel("roughness")


                return {
                    "ambientocclusion":        floatChannel("ambientocclusion"),
                    "specularlevel":           floatChannel("specularlevel"),
                    "specularedgecolor":       colorChannel("specularedgecolor"),
                    "opacity":                 floatChannel("opacity"),
                    "metallic":                floatChannel("metallic"),
                    "roughness":               roughnessData,
                    "basecolorWithPuzzlematte": makeNode("colorWithPuzzlematte", None, True, {"colorData": basecolorData, "puzzlematteData": puzzlematteData}) if (basecolorData and puzzlematteData) else basecolorData,
                    "normalWithWrinkle":       makeNode("wrinkledNormal", None, True, {
                        "baseTexture":    normalData,
                        "wrinkleTexture": normalWrinkleData,
                        # The default = 0.15 based on C4D "Paper Hairy Fibers"
                        "wrinkleWeight":  parameter("wrinkle_weight", "Wrinkle Weight", 0.15)
                    }) if (normalData and normalWrinkleData) else normalData,
                    "normal-wrinkle":          normalWrinkleData,
                    "normal":                  normalData,
                    "heightWithScale":         displacementTexture("height", multiply(makeNode("constant", None, False, heightScaleCoefficient), typedMetaValue(["height", "scale"], asNumeric, 1.0))),
                    "anisotropylevel":         floatChannel("anisotropylevel"),
                    "anisotropyangle":         floatChannel("anisotropyangle"),
                    "emissioncolor":           emissionColorData,
                    "emissionscale":           emissionIntensityData,
                    "sheenroughness":          floatChannel("sheenroughness"),
                    "sheenopacity":            floatChannel("sheenopacity"),
                    "sheencolor":              colorChannel("sheencolor"),
                    "indexofrefraction":       floatChannel("indexofrefraction"),
                    "absorptiondistance":      floatChannel("absorptiondistance"),
                    "dispersion":              floatChannel("dispersion"),
                    "absorptioncolor":         colorChannel("absorptioncolor"),
                    "translucency":            floatChannel("translucency"),
                    "scatteringweight":        scatteringweight,
                    "redshift":                floatChannel("redshift"),
                    "scatteringdistance":      floatChannel("scatteringdistance"),
                    "scatteringdistancescale": vectorChannel("scatteringdistancescale"),
                    "rayleighscattering":      floatChannel("rayleighscattering"),
                    "scatteringcolor":         colorChannel("scatteringcolor"),
                    "coatindexofrefraction":   floatChannel("coatindexofrefraction"),
                    "coatopacity":             floatChannel("coatopacity"),
                    "coatspecularlevel":       floatChannel("coatspecularlevel"),
                    "coatnormalWithScale":     normalTexture("coatnormal", floatChannel("coatnormalscale")),
                    "coatroughness":           floatChannel("coatroughness"),
                    "coatcolor":               colorChannel("coatcolor"),
                    "transcolor":              colorChannel("transcolor"),
                    "transweight":             floatChannel("transweight"),
                    "transdepth":              floatChannel("transdepth"),
                    "transroughness":          floatChannel("transroughness"),
                    "transscattercolor":       colorChannel("transscattercolor"),
                    "transscatteranisotropy":  floatChannel("transscatteranisotropy"),
                    "transdispersion":         floatChannel("transdispersion")
                }, standardSurfaceParams, normalMapParams, rampRgbParams
            
            material_params = {"path": materialPath,
                               "makeMaterialData": makeMaterialData}
            # return on material type
            return material_params, has_maps
        
    return None, False


# Karma Loader

KARMA_INHERIT_DEFAULT_PATH = '/__class_mtl__/`$OS`'
KARMA_INHERIT_PARM_EXPRESSION = '''n = hou.pwd()
n_hasFlag = n.isMaterialFlagSet()
i = n.evalParm('inherit_ctrl')
r = 'none'
if i == 1 or (n_hasFlag and i == 2):
    r = 'inherit'
return r'''

# Used for version 20 only


def _setupUsdMtlInherits(inherits_default=KARMA_INHERIT_DEFAULT_PATH, control_parm=True):
    _parm_templates = []
    control_parm_pt = hou.IntParmTemplate('inherit_ctrl', 'Inherit from Class',
                                          num_components=1, default_value=(2,),
                                          menu_items=(['0', '1', '2']),
                                          menu_labels=(['Never', 'Always', 'Material Flag']))
    _parm_templates.append(control_parm_pt)

    class_path_pt = hou.properties.parmTemplate(
        'vopui', 'shader_referencetype')
    class_path_pt.setLabel('Class Arc')
    class_path_pt.setDefaultExpressionLanguage((hou.scriptLanguage.Python,))
    class_path_pt.setDefaultExpression((KARMA_INHERIT_PARM_EXPRESSION,))

    _parm_templates.append(class_path_pt)

    ref_type_pt = hou.properties.parmTemplate('vopui', 'shader_baseprimpath')
    ref_type_pt.setDefaultValue([inherits_default])
    ref_type_pt.setLabel('Class Prim Path')
    _parm_templates.append(ref_type_pt)

    return _parm_templates


# Used for version 20 only


def _addMtlSubnetParms(subnet_node, menu_mask='', render_context='', folder_label=None, inherits_default=KARMA_INHERIT_DEFAULT_PATH):
    parms = subnet_node.parmTemplateGroup()
    # if menu_mask:
    tabmenu_mask_pt = hou.properties.parmTemplate('op', 'tabmenumask')
    tabmenu_mask_pt.setDefaultValue([menu_mask])
    if folder_label:
        subnet_folder = hou.FolderParmTemplate('folder1', folder_label,
                                               folder_type=hou.folderType.Collapsible,
                                               tags={'sidefx::shader_isparm': '0'})

        inherits_parms = _setupUsdMtlInherits(
            inherits_default=inherits_default)
        for _parm in inherits_parms:
            subnet_folder.addParmTemplate(_parm)

        subnet_folder.addParmTemplate(hou.SeparatorParmTemplate('separator1'))
        subnet_folder.addParmTemplate(tabmenu_mask_pt)

        render_context_pt = hou.properties.parmTemplate(
            'vopui', 'shader_rendercontextname')
        render_context_pt.setDefaultValue([render_context])
        subnet_folder.addParmTemplate(render_context_pt)
        parms.append(subnet_folder)
    else:
        parms.append(tabmenu_mask_pt)
    subnet_node.setParmTemplateGroup(parms)


class KarmaRenderer():
    RopNodeName = "Karma_ROP"

    def __init__(self):
        self.is_triplanar = False
        self.triplanar_nodes = []

    def isAvailable(self):
        return True

    def rendererName(self):
        return "Karma"
   
    # Different handling for karma, that has a different in-stage karma render settings node and used render rop,
    # at least in the test scene we use for the testsuite
    #  
    def setupRenderImage(self, ropNode, rendersettingsNode, size, samplesStr):
        sx = 500 if size is None else size[0]
        sy = 500 if size is None else size[1]
        rendersettingsNode.parm('resolutionx').set(sx)
        rendersettingsNode.parm('resolutiony').set(sy)
        samples = 2 if samplesStr is None else int(samplesStr)
        rendersettingsNode.parm('samplesperpixel').set(int(samples))

        return lambda outputFilePath: ropNode.render(output_file=outputFilePath, verbose=True, output_progress=True)

    def addMaterial(self, parent, materialDefinition, in_is_triplanar):

        folder_label = 'MaterialX Subnet'
        self.is_triplanar = in_is_triplanar
        self.triplanar_nodes.clear()

        materialData, standardSurfaceParams, normalMapParams, rgbParams = materialDefinition["makeMaterialData"](
            "karma", 0.005, None)

        self.ramp_rgb_params = rgbParams.params
        """
        Creates a subnet, configured to be masked for specific VOP nodes, by using the 'tabmenumask' spare parameter.
        """

        versionStr = hou.applicationVersionString()
        versionData = versionStr.split(".")
        majorVersion = int(versionData[0])

        subnet_node = createEmptyBuilder(
            parent, "subnet", materialDefinition["name"])

        if majorVersion == 19:
            KARMAMTLX_TAB_MASK = "MaterialX parameter collect subnet null subnetconnector karma USD"

            parms = subnet_node.parmTemplateGroup()
            pt = hou.properties.parmTemplate('op', 'tabmenumask')
            pt.setDefaultValue([KARMAMTLX_TAB_MASK])
            if folder_label:
                subnet_folder = hou.FolderParmTemplate('folder1', folder_label,
                                                       folder_type=hou.folderType.Collapsible)
                subnet_folder.addParmTemplate(pt)
                parms.append(subnet_folder)
            else:
                parms.append(pt)

            subnet_node.setParmTemplateGroup(parms)
        else:
            UTILITY_NODES = 'parameter constant collect null genericshader'
            SUBNET_NODES = 'subnet subnetconnector suboutput subinput'
            MTLX_TAB_MASK = 'MaterialX {} {}'.format(
                UTILITY_NODES, SUBNET_NODES)
            KARMAMTLX_TAB_MASK = "karma USD ^mtlxramp* ^hmtlxramp* ^hmtlxcubicramp* {}".format(
                MTLX_TAB_MASK)

            subnet_node.setShaderLanguageName('MaterialX')
            mask = MTLX_TAB_MASK
            render_context = 'mtlx'
            folder_label = 'MaterialX Builder'
            _addMtlSubnetParms(subnet_node, mask, render_context, folder_label)

        # Get a list of all the nodes inside the subnet
        nodes = subnet_node.children()

        # Delete each node in the list
        for node in nodes:
            node.destroy()

        createBuilderNode = makeNodeCreator(subnet_node)

        matNode = createBuilderNode("mtlxstandard_surface", "mtlxstandard_surface")
        #set all the standard_surface parameters
        standardSurfaceParams.SetAll(matNode)

        output_node20 = None

        if majorVersion == 19:
            shader_subnet_input = createBuilderNode("subnetconnector", 'surface_output', {
                                                    "connectorkind": "output", "parmname": "shader", "parmlabel": "Shader", "parmtype": 24, 'useasparmdefiner': 0})
            shader_subnet_input.setParameter("suboutput", matNode.outputSocket("out"))
        else:
            output_node20 = subnet_node.createNode('suboutput', 'Material_Outputs_and_AOVs')
            properties_node = subnet_node.createNode('kma_material_properties', 'material_properties')

            output_node20.setInput(0, matNode.outputSocket("out")["socketNode"])
            output_node20.setInput(2, properties_node)
            output_node20.parm('name1').set('surface')

        # create the texture transforms if at least one map exists
        if materialDefinition['has_maps']:
            if not self.is_triplanar:
                scalingNode = createBuilderNode("parameter", "scale", {
                                                "parmname": "scale", "parmlabel": "Scale", "parmtype": 5, "parmscope": "subnet", "float2def1": 1.0, "float2def2": 1.0}).outputSocket("_scale")
                offsetNode = createBuilderNode("parameter", "offset", {
                                            "parmname": "offset", "parmlabel": "Offset", "parmtype": 5, "parmscope": "subnet", "float2def1": 0.0, "float2def2": 0.0}).outputSocket("_offset")
                rotateNode = createBuilderNode("parameter", "rotate", {"parmname": "rotate", "parmlabel": "Rotate", "parmtype": 3,
                                            "parmscope": "subnet", "float2def1": 0.0, "rangeflt1": 0.0, "rangeflt2": 360.0}).outputSocket("_rotate")

                texCoordNode = createBuilderNode("mtlxtexcoord", "texcoord", {
                                                "signature": "vector2"}).outputSocket("out")
                place2DNode = createBuilderNode("mtlxplace2d", "transform", {})

                place2DNode.setParameter("scale", scalingNode)
                place2DNode.setParameter("offset", offsetNode)
                place2DNode.setParameter("rotate", rotateNode)
                place2DNode.setParameter("texcoord", texCoordNode)

                place2DNodeOut = place2DNode.outputSocket("out")

        compileCache = {}
        # quick and dirty placeholder for the normal map node
        self.normalMapNode = None

        def getColorSpace(texNode):
            if texNode["isSrgb"]:
                if ocio:
                    return "srgb_tx"
                else:
                    return "srgb_texture"
            else:
                if ocio:
                    return "Raw"
                else:
                    return "lin_rec709"

        def compiledNode(typedNode):
            if typedNode["nodeId"] not in compileCache:
                def compileNodeForCache():
                    nodeType = typedNode["nodeType"]
                    nodePayload = typedNode["nodePayload"]

                    def compileAsDisplaceTexture():
                        typedTexNode = nodePayload["textureNode"]
                        texNode = typedTexNode["nodePayload"]

                        displacement = createBuilderNode("mtlxdisplacement", typedTexNode["nodeName"], {
                            "displacement": 1,
                            "scale": 1
                        })

                        file_path = texNode["path"]
                        file_cs = getColorSpace(texNode)
                        if self.is_triplanar:
                            tex = createBuilderNode("mtlxtriplanarprojection", typedTexNode["nodeName"] + "_triplanar", {
                                "signature": "float",
                                "filex": file_path, "filey": file_path, "filez": file_path,
                                "filexcolorspace": file_cs, "fileycolorspace": file_cs, "filezcolorspace": file_cs
                            })
                            self.triplanar_nodes.append(tex)
                        else:
                            tex = createBuilderNode("mtlximage", typedTexNode["nodeName"], {
                                "signature": "float",
                                "file": file_path,
                                "filecolorspace": file_cs
                            })
                            tex.setParameter("texcoord", place2DNodeOut)

                        displacement.setParameter("displacement", tex.outputSocket("out"))

                        textureScale = ("textureScale" in nodePayload) and nodePayload["textureScale"]
                        if textureScale:
                            displacement.setParameter("scale", compiledNode(textureScale))

                        return displacement.outputSocket("out")

                    def compileAsNormalTexture(normalType, normalOutput):
                        typedTexNode = nodePayload["textureNode"]
                        texNode = typedTexNode["nodePayload"]

                        normalmap = createBuilderNode("mtlxnormalmap")
                        self.normalMapNode = normalmap


                        file_path = texNode["path"]
                        file_cs = getColorSpace(texNode)
                        if self.is_triplanar:
                            tex = createBuilderNode("mtlxtriplanarprojection", typedTexNode["nodeName"] + "_triplanar", {
                                "signature": "vector3",
                                "filex": file_path, "filey": file_path, "filez": file_path,
                                "filexcolorspace": file_cs, "fileycolorspace": file_cs, "filezcolorspace": file_cs
                            })
                            self.triplanar_nodes.append(tex)
                        else:
                            tex = createBuilderNode("mtlximage", typedTexNode["nodeName"], {
                                "signature": "vector3",
                                "file": file_path,
                                "filecolorspace": file_cs
                            })
                            tex.setParameter("texcoord", place2DNodeOut)

                        normalmap.setParameter("in", tex.outputSocket("out"))
                        return normalmap.outputSocket(normalOutput)

                    if nodeType == "constant":
                        return nodePayload

                    elif nodeType == "parameter":
                        p = nodePayload
                        name = p["paramName"]
                        value = p["value"]

                        def createParameter(parmtype, valueDict):
                            params = {
                                "parmname": name,
                                "parmlabel": p["label"],
                                "parmtype": parmtype,
                                "parmscope": "subnet"
                            }

                            for k, v in valueDict.items():
                                params[k] = v

                            if p["activeWhen"]:
                                params["hidewhen"] = p["activeWhen"]

                            return createBuilderNode("parameter", name, params).outputSocket("_" + name)

                        if isinstance(value, bool):
                            return createParameter("toggle", {"toggledef": value})

                        elif isinstance(value, (float, int)):
                            return createParameter("float", {"floatdef":  value})

                        elif isinstance(value, (tuple, list)):
                            return createParameter("color", {
                                "colordefr": value[0],
                                "colordefg": value[1],
                                "colordefb": value[2]
                            })

                        else:
                            raise Exception(
                                "Unhandled \"" + name + "\" parameter type " + str(type(value)) + " (" + str(value) + ")")

                    elif nodeType == "texture":

                        file_path = nodePayload["path"]
                        file_cs = getColorSpace(nodePayload)
                        signature = "color3" if nodePayload["isSrgb"] else "float"
                        if self.is_triplanar:
                            tex = createBuilderNode("mtlxtriplanarprojection", typedNode["nodeName"] + "_triplanar", {
                            "signature": signature,
                            "filex": file_path, "filey": file_path, "filez": file_path,
                            "filexcolorspace": file_cs, "fileycolorspace": file_cs, "filezcolorspace": file_cs                            
                            })
                            self.triplanar_nodes.append(tex)
                        else:
                            tex = createBuilderNode("mtlximage", typedNode["nodeName"], {
                                "signature": signature,
                                "file": file_path,
                                "filecolorspace": file_cs
                            })
                            tex.setParameter("texcoord", place2DNodeOut)

                        # Special case for anisotropy angle, set the filter type to "closest"
                        if typedNode["nodeName"] == "anisotropyangle":
                            tex.setParameter("filtertype", "closest")

                        return tex.outputSocket("out")

                    elif nodeType == "normalTexture":
                        return compileAsNormalTexture("normal", "out")

                    elif nodeType == "displacementTexture":
                        return compileAsDisplaceTexture()

                    elif nodeType == "wrinkledNormal":
                        globalNormalOutputSocket = createBuilderNode(
                            "global", None, {"contexttype": "displace"}).outputSocket("N")  # TODO

                        normalWrinkleMapOutputSocket = compiledNode(
                            nodePayload["wrinkleTexture"])

                        dotNode = createBuilderNode("mtlxdotproduct")
                        dotNode.setParameter("in1", globalNormalOutputSocket)
                        dotNode.setParameter("in2", normalWrinkleMapOutputSocket)

                        trigNode = createBuilderNode("mtlxacos")
                        trigNode.setParameter("in", dotNode.outputSocket("out"))

                        angleMulNode = createBuilderNode("mtlxmultiply")
                        angleMulNode.setParameter("in1", trigNode.outputSocket("out"))
                        angleMulNode.setParameter("in2", compiledNode(nodePayload["wrinkleWeight"]))

                        crossNode = createBuilderNode("mtlxcrossproduct")
                        crossNode.setParameter("in1", globalNormalOutputSocket)
                        crossNode.setParameter("in2", normalWrinkleMapOutputSocket)

                        crossNormNode = createBuilderNode("mtlxnormalize")
                        crossNormNode.setParameter("in", crossNode.outputSocket("out"))

                        quatNode = createBuilderNode("quaternion")  # TODO
                        quatNode.setParameter("angle", angleMulNode.outputSocket("out"))
                        quatNode.setParameter("axis", crossNormNode.outputSocket("out"))

                        rotateNode = createBuilderNode("qrotate")  # TODO
                        rotateNode.setParameter("quaternion", quatNode.outputSocket("quat"))
                        rotateNode.setParameter("vec", compiledNode(nodePayload["baseTexture"]))

                        return rotateNode.outputSocket("result")

                    elif nodeType == "multiply":
                        def isNumericConst(v):
                            return isinstance(v, (float, int))

                        def mulConst(mult, inNode):
                            multiplyNode = createBuilderNode("mtlxmultiply")
                            multiplyNode.setParameter("in1", in1)
                            multiplyNode.setParameter("in2", inNode)
                            return multiplyNode.outputSocket("out")

                        in1 = compiledNode(nodePayload["multiplier"])
                        in2 = compiledNode(nodePayload["multiplicant"])

                        if isNumericConst(in1) and isNumericConst(in2):
                            return in1 * in2
                        elif isNumericConst(in1):
                            return mulConst(in1, in2)
                        elif isNumericConst(in2):
                            return mulConst(in2, in1)
                        else:
                            multiplyNode = createBuilderNode("mtlxmultiply")
                            multiplyNode.setParameter("in1", in1)
                            multiplyNode.setParameter("in2", in2)
                            return multiplyNode.outputSocket("out")
                        
                    elif nodeType in ("carpaint", "neon"):
                        nb_keys = self.ramp_rgb_params['nb_keys'] if 'nb_keys' in self.ramp_rgb_params else 0
                        if nb_keys == 0:
                            return None
                        
                        position = self.ramp_rgb_params['position']
                        color = self.ramp_rgb_params['color']

                        # Using a Karma ramp
                        ramp = createBuilderNode("kma_rampconst", nodeType + " ramp")
                        ramp.setParameter('signature', 'vector3')
                        ramp.setParameter('vramp', nb_keys)
                        houdini_node = ramp.getHoudiniNode()
                        parm_instances = houdini_node.parm('vramp').multiParmInstances()
                        for i in range(nb_keys):
                            # position
                            parm_instances[i * 5].set(position[i])
                            # r, g, b
                            parm_instances[i * 5 + 1].set(color[i]['r'])
                            parm_instances[i * 5 + 2].set(color[i]['g'])
                            parm_instances[i * 5 + 3].set(color[i]['b'])
                            # interpolation
                            parm_instances[i * 5 + 4].set(2)

                        ray_import = createBuilderNode("kma_rayimport", "ray direction")
                        ray_import.setParameter('signature', 'vector3')
                        ray_import.setParameter('var', 'ray:direction')

                        facing_ratio = createBuilderNode("hmtlxfacingratio", nodeType + " incidence")
                        normal = createBuilderNode("mtlxnormal","normal")
                        normal.setParameter('space', 'world')
                        # normal and ray direction to the facing ratio shader
                        facing_ratio.setParameter('in1', ray_import.outputSocket("out"))
                        facing_ratio.setParameter('in2', normal.outputSocket("out"))


                        # capitalize the first letter of the node name to show as such in the subnetwork main parameters
                        # (paramscope set to subnet)
                        label_prefix = nodeType[0].upper() + nodeType[1:]
                        falloff = createBuilderNode("parameter", nodeType + " falloff", {
                                                "parmname": "falloff", "parmlabel": label_prefix + " Falloff", "parmtype": 0, "parmscope": "subnet", 
                                                "floatdef": self.ramp_rgb_params['falloff'], "rangeflt2": 10.0}).outputSocket("_falloff")

                        # power the facing ratio by the falloff parameter
                        power = createBuilderNode("mtlxpower", "power")
                        power.setParameter('in1', facing_ratio.outputSocket("out"))
                        power.setParameter('in2', falloff)
                        # power output to the ramp shader
                        ramp.setParameter('t', power.outputSocket("out"))

                        if nodeType == "neon":
                            # multiply the ramp output by the neon color
                            color = self.ramp_rgb_params['neon_color']
                            multiply = createBuilderNode("mtlxmultiply")
                            neon_color = createBuilderNode("parameter", "neon_color", {
                                                            "parmname": "neon_color", "parmlabel": "Neon Color", "parmtype": 19, "parmscope": "subnet", 
                                                            "colordefr": color['r'], "colordefg": color['g'], "colordefb": color['b']}).outputSocket("_neon_color")

                            multiply.setParameter("in1", neon_color)
                            multiply.setParameter("in2", ramp.outputSocket("out"))
                            # return the multiply node, that will plug into the emission color
                            return multiply.outputSocket("out")
                        else:
                            # return the ramp node, that will plug into the base color
                            return ramp.outputSocket("out")
                        
                    elif nodeType == "roughnessweight":
                        if (nodePayload["channel"] is None) or (nodePayload["channel"] == ""):
                            return None
                        # Carpaint roughness weight. Multiply the roughness texture by the weight parameter
                        roughness = compiledNode(nodePayload["channel"])
                        multiply = createBuilderNode("mtlxmultiply", "multiply")
                        weight = createBuilderNode("parameter", nodeType, {
                                                "parmname": "weight", "parmlabel": "Roughness Weight", "parmtype": 0, "parmscope": "subnet", 
                                                "floatdef": self.ramp_rgb_params['carpaint_roughness_weight'], "rangeflt2": 1.0}).outputSocket("_weight")
                        
                        multiply.setParameter('in1', weight)
                        multiply.setParameter('in2', roughness)
                        return multiply.outputSocket("out")
    
                    elif nodeType == "colorWithPuzzlematte":
                        puzzlematteData = nodePayload["puzzlematteData"]

                        switchNode = createBuilderNode("mtlxswitch")

                        switchNode.setParameter("which", compiledNode(puzzlematteData["enabled"]))

                        def puzzlematteColor(name):
                            return lambda: compiledNode(puzzlematteData[name])

                        baseColor = puzzlematteColor("baseColor")
                        layer1Color = puzzlematteColor("layer1Color")
                        layer2Color = puzzlematteColor("layer2Color")
                        layer3Color = puzzlematteColor("layer3Color")

                        separateColorsNode = createBuilderNode("mtlxseparate3c")

                        # ensure the puzzle mat image gets created as a color.
                        weights = puzzlematteData["weights"]
                        weightsdata = weights["nodePayload"]
                        weightsdata["isSrgb"] = True

                        separateColorsNode.setParameter("in", compiledNode(weights))

                        def layer(maskChannel, getColor1OutputSocket, getColor2OutputSocket):
                            def getOutputSocket():
                                mixNode = createBuilderNode("mtlxmix", None, {"signature": "color3"})
                                mixNode.setParameter("bg", getColor1OutputSocket())
                                mixNode.setParameter("fg", getColor2OutputSocket())
                                mixNode.setParameter("mix", separateColorsNode.outputSocket(maskChannel))
                                return mixNode.outputSocket("out")
                            return getOutputSocket

                        layer1 = layer("outr", baseColor, layer1Color)
                        layer2 = layer("outg", layer1,    layer2Color)
                        layer3 = layer("outb", layer2,    layer3Color)

                        baseColorOutput = compiledNode(nodePayload["colorData"])

                        switchNode.setParameter("in1", baseColorOutput)

                        multiplyNode = createBuilderNode("mtlxmultiply")
                        multiplyNode.setParameter("in1", layer3())
                        multiplyNode.setParameter("in2", baseColorOutput)
                        switchNode.setParameter("in2", multiplyNode.outputSocket("out"))

                        return switchNode.outputSocket("out")

                    else:
                        raise Exception("Unrecognized \"" +
                                        nodeType + "\" node type")

                compileCache[typedNode["nodeId"]] = compileNodeForCache()

            return compileCache[typedNode["nodeId"]]

        def setMaterialInput(inputSocketName, inputData):
            if inputData:
                matNode.setParameter(inputSocketName, compiledNode(inputData))

        def postHooks():
            if (not self.is_triplanar) or (len(self.triplanar_nodes) == 0):
                return
            # Connect a blend parameter to all triplanar nodes
            blendNode = createBuilderNode("parameter", "triplanar_blend", {
                "parmname": "blend", "parmlabel": "Triplanar Blend", "parmtype": 0, "parmscope": "shaderparm", "floatdef": 1.0}).outputSocket("blend")
            for triplanar_node in self.triplanar_nodes:
                triplanar_node.setParameter("blend", blendNode)


        basecolorData = materialData["basecolorWithPuzzlematte"]
        setMaterialInput("base_color", basecolorData)
        setMaterialInput("metalness",  materialData["metallic"])

        setMaterialInput("specular",           materialData["specularlevel"])
        setMaterialInput("specular_roughness", materialData["roughness"])
        setMaterialInput("specular_color",     materialData["specularedgecolor"])

        displacementData = materialData["heightWithScale"]
        if displacementData:
            if majorVersion == 19:
                disp_subnet_input = createBuilderNode("subnetconnector", 'displacement_output', {
                                                      "connectorkind": "output", "parmname": "displacement", "parmlabel": "Displacement", "parmtype": 25, 'useasparmdefiner': 0})
                disp_subnet_input.setParameter("suboutput", compiledNode(displacementData))
            else:
                output_node20.setInput(1, compiledNode(displacementData)["socketNode"])
                output_node20.parm('name2').set('displacement')

        setMaterialInput("sheen_color", materialData["sheencolor"])
        setMaterialInput("sheen",       materialData["sheenopacity"])
        # setMaterialInput(simpleInput("ao"), materialData["ambientocclusion"])

        scatteringWeightData = materialData["scatteringweight"]
        scatteringDistanceScaleData = materialData["scatteringdistancescale"]

        hasScatteringWeightData = scatteringWeightData is not None
        hasScatteringDistancescaleData = scatteringDistanceScaleData is not None
        if materialData["scatteringcolor"] is not None:
            setMaterialInput("subsurface_color", materialData["scatteringcolor"])
        elif (hasScatteringWeightData or  hasScatteringDistancescaleData) and basecolorData is not None:
            setMaterialInput("subsurface_color", basecolorData)

        if scatteringWeightData:
            setMaterialInput("subsurface", scatteringWeightData)
        elif not scatteringWeightData and scatteringDistanceScaleData:
            # subsurface is set to 0 by default, and possibly overridden by the gsgm data
            has_subsurface_from_params = matNode.getParameter("subsurface") != 0
            # subsurface_scale is not set to any default but can have been set by the gsgm data
            has_subsurface_scale_from_params = standardSurfaceParams.Has("subsurface_scale")

            if not has_subsurface_from_params:
                matNode.setParameter("subsurface", 1.0)
            if not has_subsurface_scale_from_params:
                matNode.setParameter("subsurface_scale", 0.001)

        setMaterialInput("subsurface_radius", scatteringDistanceScaleData)

        setMaterialInput("opacity",             materialData["opacity"])
        setMaterialInput("specular_anisotropy", materialData["anisotropylevel"])
        setMaterialInput("specular_rotation",   materialData["anisotropyangle"])

        setMaterialInput("emission_color",      materialData["emissioncolor"])
        setMaterialInput("emission_weight",     materialData["emissionscale"])


        # setMaterialInput("normal", materialData["normalWithWrinkle"])
        setMaterialInput("normal", materialData["normal"])
        if self.normalMapNode is not None:
            normalMapParams.SetAll(self.normalMapNode)

        setMaterialInput("coat_normal",    materialData["coatnormalWithScale"])
        setMaterialInput("coat",           materialData["coatopacity"])
        setMaterialInput("coat_roughness", materialData["coatroughness"])

        # transmission (the missing ones are not implemented, although exposed in the UI, as of Houdini 20.5
        setMaterialInput("transmission_color",      materialData["transcolor"])
        setMaterialInput("transmission",            materialData["transweight"])
        setMaterialInput("transmission_depth",      materialData["transdepth"])
        setMaterialInput("transmission_dispersion", materialData["transdispersion"])

        postHooks()

        subnet_node.setMaterialFlag(True)
        return subnet_node


class RedshiftRenderer():
    available = None
    RopNodeName = "Redshift_ROP"
    color_spaces = {}

    def __init__(self):
        self.is_triplanar = False

        if not ocio:
            self.color_spaces["raw"] = "Raw"
            self.color_spaces["srgb"] = "sRGB"
            return

        ocio_config_file = os.environ.get('OCIO')
        if ocio_config_file is not None and os.path.exists(ocio_config_file):
            config = PyOpenColorIO.Config.CreateFromFile(ocio_config_file)
            spaces = config.getColorSpaces()
            names = [space.getName() for space in spaces]
            names.sort()

            for name in names:
                if name.lower().startswith("raw"):
                    self.color_spaces["raw"] = name
                    break
                
            # first look for sRGB - Texture
            for name in names:
                if name.lower().startswith("srgb - texture"):
                    self.color_spaces["srgb"] = name
                    break
            # then, if not found, for the first occurrence of srgb_texture
            if not "srgb" in self.color_spaces:
                for name in names:
                    if name.lower().startswith("srgb_texture"):
                        self.color_spaces["srgb"] = name
                        break
            # then, if not found, for the first occurrence of sRGB
            if not "srgb" in self.color_spaces:
                for name in names:
                    if name.lower().startswith("srgb"):
                        self.color_spaces["srgb"] = name
                        break

        if not "raw" in self.color_spaces:
            self.color_spaces["raw"] = "Raw"
        if not "srgb" in self.color_spaces:
            self.color_spaces["srgb"] = "sRGB"

    def isAvailable(self):
        if self.available is None:
            try:
                n = hou.node("/mat").createNode("redshift_vopnet",
                                                "redshift_vopnet_test", force_valid_node_name=True)
                self.available = n is not None
                if n:
                    n.destroy()
            except hou.OperationFailed as err:
                self.available = False
        return self.available

    def rendererName(self):
        return "Redshift"

    def setupRenderImage(self, ropNode, cameraNode, size, outExtension, qualityStr):

        ropNode.parm("RS_renderCamera").set(cameraNode.path())

        if size is not None:
            ropNode.parm("RS_overrideCameraRes").set(True)
            ropNode.parm("RS_overrideResScale").set("user")
            ropNode.parm("RS_overrideRes1").set(size[0])
            ropNode.parm("RS_overrideRes2").set(size[1])

        def outFmt(RS_outputFileFormat, otherParms=None):
            fmt = {"RS_outputFileFormat": RS_outputFileFormat}
            if otherParms:
                for k, v in otherParms.items():
                    fmt[k] = v
            return fmt

        # 8-bit 2.2 gamma gives the most consinstent renders for all the formats
        extensions = {
            ".jpg":  outFmt(".jpg"),
            ".jpeg": outFmt(".jpg"),
            ".png":  outFmt(".png", {"RS_outputBitsPNG": "INTEGER8"}),
            ".tif":  outFmt(".tif", {"RS_outputBitsTiff": "INTEGER8"}),
            ".tiff": outFmt(".tif", {"RS_outputBitsTiff": "INTEGER8"})
        }

        if outExtension.lower() in extensions:
            for k, v in extensions[outExtension.lower()].items():
                if v is not None:
                    ropNode.parm(k).set(v)
        else:
            raise Exception("Unsupported extension \"" + outExtension +
                            "\". Use one of: " + ", ". join(extensions.keys()) + ".")

        try:
            quality = None if (qualityStr is None) else int(qualityStr)
        except ValueError:
            raise Exception(
                "The --quality option should be numeric, was \"" + qualityStr + "\".")

        ropNode.parm("RS_renderToMPlay").set(False)

        def renderImage(outputFilePath):
            ropNode.parm("RS_outputFileNamePrefix").set(outputFilePath)
            ropNode.render(quality=(quality if (quality is not None)
                           else 2), verbose=True, output_progress=True)

        return renderImage

    def addMaterial(self, parent, materialDefinition, in_is_triplanar):
        
        self.is_triplanar = in_is_triplanar

        materialData, standardSurfaceParams, normalMapParams, rgbParams = materialDefinition["makeMaterialData"](
            "redshift", 0.01, 0.01)
        
        self.ramp_rgb_params = rgbParams.params
        
        builder = createEmptyBuilder(parent, "redshift_vopnet", materialDefinition["name"])

        createBuilderNode = makeNodeCreator(builder)

        matNode = createBuilderNode("redshift::StandardMaterial")
        #set all the standard_surface parameters
        standardSurfaceParams.SetAll(matNode)

        outputNode = createBuilderNode("redshift_material")
        outputNode.setParameter("Surface", matNode.outputSocket("outColor"))

        # create the texture transforms if at least one map exists
        if materialDefinition['has_maps']:
            # for triplanar, use a single-float scaler
            if self.is_triplanar:
                scaleNode = createBuilderNode("parameter", "triplanar_scale", {
                                            "parmname": "scale", "parmlabel": "Triplanar Scale", "parmtype": 0, "parmscope": "shaderparm", "floatdef": 1.0}).outputSocket("scale")
                blendNode = createBuilderNode("parameter", "triplanar_blend", {
                                            "parmname": "blend", "parmlabel": "Triplanar Blend", "parmtype": 0, "parmscope": "shaderparm", "floatdef": 0.1}).outputSocket("blend")
            else:
                scaleNode = createBuilderNode("parameter", "scale", {
                                            "parmname": "scale", "parmlabel": "Scale", "parmtype": 5, "parmscope": "shaderparm", "float2def1": 1.0, "float2def2": 1.0}).outputSocket("scale")

            offsetNode = createBuilderNode("parameter", "offset", {
                                        "parmname": "offset", "parmlabel": "Offset", "parmtype": 5, "parmscope": "shaderparm", "float2def1": 0.0, "float2def2": 0.0}).outputSocket("offset")
            rotateNode = createBuilderNode("parameter", "rotate", {"parmname": "rotate", "parmlabel": "Rotate", "parmtype": 3,
                                        "parmscope": "shaderparm", "float2def1": 0.0, "rangeflt1": 0.0, "rangeflt2": 360.0}).outputSocket("rotate")

        compileCache = {}
        # quick and dirty placeholder for the normal map node
        self.normalMapNode = None

        def getColorSpace(texNode):
            return self.color_spaces["srgb"] if texNode["isSrgb"] else self.color_spaces["raw"]

        def compiledNode(typedNode):
            if typedNode["nodeId"] not in compileCache:
                def compileNodeForCache():
                    nodeType = typedNode["nodeType"]
                    nodePayload = typedNode["nodePayload"]

                    def compileAsDisplaceTexture(displaceType, displaceInputSocketName, displaceParams):
                        node = createBuilderNode(displaceType, None, displaceParams)

                        node.setParameter(displaceInputSocketName, compiledNode(
                            nodePayload["textureNode"]))

                        if displaceType == "redshift::BumpMap":
                            self.normalMapNode = node

                        textureScale = (
                            "textureScale" in nodePayload) and nodePayload["textureScale"]
                        if textureScale:
                            node.setParameter("scale", compiledNode(textureScale))

                        return node.outputSocket("out")

                    if nodeType == "constant":
                        return nodePayload

                    elif nodeType == "parameter":
                        p = nodePayload
                        name = p["paramName"]
                        label = p["label"]
                        value = p["value"]

                        def createParameter(parmTpl):
                            if p["activeWhen"]:
                                parmTpl.setConditional(hou.parmCondType.HideWhen, p["activeWhen"])

                            return builder.addSpareParmTuple(parmTpl, ("Settings", p["folder"]) if p["folder"] else ("Settings", ), create_missing_folders=True)

                        if isinstance(value, bool):
                            return createParameter(hou.ToggleParmTemplate(name, label, value))

                        elif isinstance(value, (float, int)):
                            return createParameter(hou.FloatParmTemplate(name, label, 1, (value, )))

                        elif isinstance(value, (tuple, list)):
                            return createParameter(hou.FloatParmTemplate(name, label, 3, value, look=hou.parmLook.ColorSquare, naming_scheme=hou.parmNamingScheme.RGBA))

                        else:
                            raise Exception(
                                "Unhandled \"" + name + "\" parameter type " + str(type(value)) + " (" + str(value) + ")")

                    elif nodeType == "texture":
                        tex = createBuilderNode("redshift::TextureSampler", typedNode["nodeName"], {
                            "tex0": nodePayload["path"],
                            "tex0_colorSpace": getColorSpace(nodePayload)
                        })

                        out_node = tex
                        if self.is_triplanar:
                            triplanar = createBuilderNode("redshift::TriPlanar", typedNode["nodeName"] + '_triplanar')
                            triplanar.setParameter("imageX", tex.outputSocket("outColor"))
                            triplanar.setParameter("rotation", rotateNode)
                            triplanar.setParameter("blendAmount", blendNode)
                            out_node = triplanar
                        else:
                            tex.setParameter("rotate", rotateNode)

                        out_node.setParameter("scale", scaleNode)
                        out_node.setParameter("offset", offsetNode)

                        # Special case for anisotropy angle, set the filter type to "None"
                        if typedNode["nodeName"] == "anisotropyangle":
                            tex.setParameter("filter_enable_type", '0')

                        return out_node.outputSocket("outColor")

                    elif nodeType == "normalTexture":
                        # Tangent-Space Normal
                        return compileAsDisplaceTexture("redshift::BumpMap", "input", {"inputType": "1"})

                    elif nodeType == "displacementTexture":
                        # Height Field
                        return compileAsDisplaceTexture("redshift::Displacement", "texMap", {"newrange_min": -1, "map_encoding": "2"})

                    elif nodeType == "wrinkledNormal":
                        bumpBlender = createBuilderNode("redshift::BumpBlender", None, {
                                                        "additive": True, "bumpWeight1": 1})
                        bumpBlender.setParameter("bumpInput0", compiledNode(nodePayload["wrinkleTexture"]))
                        bumpBlender.setParameter("bumpWeight0", compiledNode(nodePayload["wrinkleWeight"]))
                        bumpBlender.setParameter("bumpInput1", compiledNode(nodePayload["baseTexture"]))
                        return bumpBlender.outputSocket("outDisplacementVector")

                    elif nodeType == "multiply":
                        input1 = nodePayload["multiplier"]
                        input2 = nodePayload["multiplicant"]
                        hasVectorInput = input1["isVectorOutput"] or input2["isVectorOutput"]
                        multiplyNode = createBuilderNode("redshift::RSMathMulVector" if hasVectorInput else "redshift::RSMathMul")
                        multiplyNode.setParameter("input1", compiledNode(input1))
                        multiplyNode.setParameter("input2", compiledNode(input2))
                        return multiplyNode.outputSocket("out")
                    
                    elif nodeType in ("carpaint", "neon"):
                        nb_keys = self.ramp_rgb_params['nb_keys'] if 'nb_keys' in self.ramp_rgb_params else 0
                        if nb_keys == 0:
                            return None
                        
                        position = self.ramp_rgb_params['position']
                        color = self.ramp_rgb_params['color']

                        ramp = createBuilderNode("redshift::RSRamp", nodeType + " ramp")
                        # input source from the input plug
                        ramp.setParameter('inputSource', "1")
                        ramp.setParameter('ramp', nb_keys)
                        houdini_node = ramp.getHoudiniNode()
                        parm_instances = houdini_node.parm('ramp').multiParmInstances()
                        for i in range(nb_keys):
                            # position
                            parm_instances[i * 5].set(position[i])
                            # r, g, b
                            parm_instances[i * 5 + 1].set(color[i]['r'])
                            parm_instances[i * 5 + 2].set(color[i]['g'])
                            parm_instances[i * 5 + 3].set(color[i]['b'])
                            # interpolation
                            parm_instances[i * 5 + 4].set(2)

                        fresnel = createBuilderNode("redshift::Fresnel", nodeType + " incidence")
                        fresnel.setParameter('fresnel_useior', False)
                        fresnel.setParameter('user_curve', 1.0)
                        fresnel.setParameter('facing_color', (1,1,1))
                        fresnel.setParameter('perp_color', (0,0,0))

                        # capitalize the first letter of the node name to show as such in the material builder
                        label_prefix = nodeType[0].upper() + nodeType[1:]

                        # falloff parameter
                        falloff = createBuilderNode("parameter", nodeType + " falloff", {
                                                    "parmname": "falloff", "parmlabel": label_prefix + " Falloff", "parmtype": 0, "parmscope": "shaderparm", 
                                                    "floatdef": self.ramp_rgb_params['falloff'], "rangeflt2": 10.0}).outputSocket("falloff")
                        # power the facing ratio by the falloff parameter
                        power = createBuilderNode("redshift::RSMathPow", "power")
                        power.setParameter('base', fresnel.outputSocket("outColor"))
                        power.setParameter('exponent', falloff)
                        # power output to the ramp shader
                        ramp.setParameter('input', power.outputSocket("out"))

                        if nodeType == "neon":
                            # multiply the ramp output by the neon color
                            color = self.ramp_rgb_params['neon_color']
                            multiply = createBuilderNode("redshift::RSMathMulVector", "multiply")
                            neon_color = createBuilderNode("parameter", "neon_color", {
                                                            "parmname": "neon_color", "parmlabel": "Neon Color", "parmtype": 19, "parmscope": "shaderparm", 
                                                            "colordefr": color['r'], "colordefg": color['g'], "colordefb": color['b']}).outputSocket("neon_color")

                            multiply.setParameter("input1", neon_color)
                            multiply.setParameter("input2", ramp.outputSocket("outColor"))
                            # return the multiply node, that will plug into the emission color
                            return multiply.outputSocket("out")
                        else:
                            # return the ramp node, that will plug into the base color
                            return ramp.outputSocket("outColor")
                        
                    elif nodeType == "roughnessweight":
                        if (nodePayload["channel"] is None) or (nodePayload["channel"] == ""):
                            return None
                        # Carpaint roughness weight. Multiply the roughness texture by the weight parameter
                        roughness = compiledNode(nodePayload["channel"])
                        multiply = createBuilderNode("redshift::RSMathMul", "multiply")
                        weight = createBuilderNode("parameter", "roughness_weight", {
                                                "parmname": "weight", "parmlabel": "Roughness Weight", "parmtype": 0, "parmscope": "shaderparm", 
                                                "floatdef": self.ramp_rgb_params['carpaint_roughness_weight'], "rangeflt2": 1.0}).outputSocket("weight")
                        
                        multiply.setParameter('input1', weight)
                        multiply.setParameter('input2', roughness)
                        return multiply.outputSocket("out")
                    
                    elif nodeType == "colorWithPuzzlematte":
                        puzzlematteData = nodePayload["puzzlematteData"]

                        switch = createBuilderNode("redshift::vopSwitch")
                        switch.setParameter("RS_switch", compiledNode(puzzlematteData["enabled"]))

                        def puzzlematteColor(name):
                            return lambda: compiledNode(puzzlematteData[name])

                        baseColor = puzzlematteColor("baseColor")
                        layer1Color = puzzlematteColor("layer1Color")
                        layer2Color = puzzlematteColor("layer2Color")
                        layer3Color = puzzlematteColor("layer3Color")

                        separateColorsNode = createBuilderNode("redshift::RSColorSplitter")
                        separateColorsNode.setParameter("input", compiledNode(puzzlematteData["weights"]))

                        colorLayer = createBuilderNode("redshift::RSColorLayer", None, {
                            "layer1_enable": True,
                            "layer2_enable": True,
                            "layer3_enable": True,
                            "layer4_enable": True,
                            "layer4_blend_mode": "4"  # Multiply
                        })

                        def linkLayer(getColorOutputSocket, inputSocketName):
                            colorLayer.setParameter(inputSocketName, getColorOutputSocket())

                        linkLayer(baseColor, "base_color")
                        colorLayer.setParameter("layer1_mask", separateColorsNode.outputSocket("outR"))
                        linkLayer(layer1Color, "layer1_color")
                        colorLayer.setParameter("layer2_mask", separateColorsNode.outputSocket("outG"))
                        linkLayer(layer2Color, "layer2_color")
                        colorLayer.setParameter("layer3_mask", separateColorsNode.outputSocket("outB"))
                        linkLayer(layer3Color, "layer3_color")

                        baseColorOutput = compiledNode(nodePayload["colorData"])

                        colorLayer.setParameter("layer4_color", baseColorOutput)

                        switch.setParameter("VOP1", baseColorOutput)
                        switch.setParameter("VOP2", colorLayer.outputSocket("outColor"))

                        return switch.outputSocket("VOPOut")

                    else:
                        raise Exception("Unrecognized \"" +
                                        nodeType + "\" node type")

                compileCache[typedNode["nodeId"]] = compileNodeForCache()

            return compileCache[typedNode["nodeId"]]

        def setMaterialInput(inputSpec, inputData):
            if inputData:
                matNode.setParameter(inputSpec["param_inputSocketName"], compiledNode(inputData))

        def simpleInput(inputSocketName):
            return {"param_inputSocketName": inputSocketName}

        basecolorData = materialData["basecolorWithPuzzlematte"]
        setMaterialInput(simpleInput("base_color"), basecolorData)

        setMaterialInput(simpleInput("refl_weight"),    materialData["specularlevel"])
        setMaterialInput(simpleInput("refl_color"),     materialData["specularedgecolor"])
        setMaterialInput(simpleInput("opacity_color"),  materialData["opacity"])
        setMaterialInput(simpleInput("metalness"),      materialData["metallic"])
        setMaterialInput(simpleInput("refl_roughness"), materialData["roughness"])
        setMaterialInput(simpleInput("bump_input"),     materialData["normalWithWrinkle"])
        
        if self.normalMapNode is not None:
            normalMapParams.SetAll(self.normalMapNode)

        displacementData = materialData["heightWithScale"]
        if displacementData:
            outputNode.setParameter("Displacement", compiledNode(displacementData))

        setMaterialInput(simpleInput("refl_aniso"),          materialData["anisotropylevel"])
        setMaterialInput(simpleInput("refl_aniso_rotation"), materialData["anisotropyangle"])
        setMaterialInput(simpleInput("emission_color"),      materialData["emissioncolor"])
        setMaterialInput(simpleInput("emission_weight"),     materialData["emissionscale"])
        setMaterialInput(simpleInput("sheen_roughness"),     materialData["sheenroughness"])
        setMaterialInput(simpleInput("sheen_weight"),        materialData["sheenopacity"])
        setMaterialInput(simpleInput("sheen_color"),         materialData["sheencolor"])
        setMaterialInput(simpleInput("refl_ior"),            materialData["indexofrefraction"])

        scatteringWeightData = materialData["scatteringweight"]
        setMaterialInput(simpleInput("ms_amount"), scatteringWeightData)

        scatterRadiusData = materialData["scatteringdistancescale"]
        scatterScaleData = materialData["scatteringdistance"]
        mulScatterRadiusByScale = scatterScaleData and (
            scatterScaleData["nodeType"] not in ("constant", "parameter"))

        setMaterialInput(simpleInput("ms_radius"), multiply(
            scatterRadiusData, scatterScaleData) if mulScatterRadiusByScale else scatterRadiusData)

        setMaterialInput(simpleInput("ms_radius_scale"),
                         None if mulScatterRadiusByScale else scatterScaleData)

        hasScatteringWeightData = scatteringWeightData is not None
        hasScatteringDistanceScaleData = scatterRadiusData is not None

        if materialData["scatteringcolor"] is not None:
            setMaterialInput(simpleInput("ms_color"), materialData["scatteringcolor"])
        elif (hasScatteringWeightData or  hasScatteringDistanceScaleData) and basecolorData is not None:
            setMaterialInput(simpleInput("ms_color"), basecolorData)

        setMaterialInput(simpleInput("coat_ior"),        materialData["coatindexofrefraction"])
        setMaterialInput(simpleInput("coat_weight"),     materialData["coatopacity"])
        setMaterialInput(simpleInput("coat_roughness"),  materialData["coatroughness"])
        setMaterialInput(simpleInput("coat_bump_input"), materialData["coatnormalWithScale"])
        setMaterialInput(simpleInput("coat_color"),      materialData["coatcolor"])

        # transmission
        setMaterialInput(simpleInput("refr_color"),       materialData["transcolor"])
        setMaterialInput(simpleInput("refr_weight"),      materialData["transweight"])
        setMaterialInput(simpleInput("ss_depth"),         materialData["transdepth"])
        setMaterialInput(simpleInput("ss_scatter_color"), materialData["transscattercolor"])
        setMaterialInput(simpleInput("refr_abbe"),        materialData["transdispersion"])
        setMaterialInput(simpleInput("refr_roughness"),   materialData["transroughness"])
        # the following gives and error, not being linkable (same in C4D)
        # setMaterialInput(simpleInput("ss_phase"),         materialData["transscatteranisotropy"])

        return builder


# Import Models Utils

def load_fbx_model(model_metadata):
    model_name = model_metadata['name']
    fbx_file_path = model_metadata['path']
    imported_node_tuple = hou.hipFile.importFBX(fbx_file_path)
    imported_node, _ = imported_node_tuple
    if imported_node:
        sanitized_name = model_name.replace(' ', '_')
        imported_node.setName(sanitized_name, unique_name=True)
        if imported_node.name() != sanitized_name:
            imported_node.setName(sanitized_name + '_1', unique_name=True)
        imported_node.parm('tx').set(0)
        imported_node.parm('ty').set(0)
        imported_node.parm('tz').set(0)
        imported_node.moveToGoodPosition()
        imported_node.setCurrent(True, clear_all_selected=True)
        #log.info(f"Imported model: {imported_node.name()}")
    else:
        log.error('No models found at path.')
        error_message = "Error loading model"
        model_metadata['error'] = {
            "error_code": error_codes['ModelsNotLoaded'],
            "message": error_message,
            "path_to_asset": model_metadata['path']
        }


task_manager = TaskManager()


def on_message(message, client_socket):
    def on_task_completed(client_socket, succeses):
        log.debug('Replying...')
        response = {
            "type": "completed",
            "version": MESSAGING_VERSION,
            "completed": succeses,
        }
        encoded_message = protocol.encode_message(
            json.dumps(response, indent=4))
        client_socket.sendall(encoded_message)

    def on_task_error(client_socket, errors):
        for error in errors:
            if error.get('path_to_asset'):
                error['path_to_asset'] = error['path_to_asset'].replace(
                    '\\', '/')
            if error.get('message'):
                error['message'] = error['message'].replace(
                    '\\', '/')
        plus_app_response = {
            "type": "error",
            "version": MESSAGING_VERSION,
            "errors": errors,
        }
        encoded_response = protocol.encode_message(
            json.dumps(plus_app_response, indent=4))
        client_socket.sendall(encoded_response)

    log.debug(f'Processing message: {message}')
    task_metadata = json.loads(message)
    assets_path = task_metadata.get('path_to_asset').strip('\"')
    renderer = task_metadata.get('renderer')
    has_triplanar = task_metadata.get('triplanar') is not None
    event_type = task_metadata.get('event_type')
    asset_type = task_metadata.get('asset_type')
    is_surface_imperfections = False
    tags = task_metadata.get('tags')
    if tags and asset_type == 'TEXTURE':
        for tag in tags:
            if 'imperfection' in tag.lower():
                is_surface_imperfections = True
                break

    if task_metadata.get('dcc') != 'HOUDINI':
        return
    if asset_type.lower() == 'material':
        get_materials_metadata(
            assets_path,
            on_metadata_received=lambda materials_metadata: globals()['task_manager'].add('load_materials', {
                "metadata": materials_metadata,
                "renderer": task_metadata.get('renderer'),
                "triplanar": task_metadata.get('triplanar') if has_triplanar else False,
                "on_success": lambda completed: on_task_completed(client_socket, completed),
                "on_error": lambda errored_materials: on_task_error(client_socket, errored_materials)
            })
        )
    elif asset_type.lower() == 'model':
        get_models_metadata(
            assets_path,
            on_metadata_received=lambda models_metadata: globals()['task_manager'].add('load_models', {
                "metadata": models_metadata,
                "on_success": lambda completed: on_task_completed(client_socket, completed),
                "on_error": lambda errored_materials: on_task_error(client_socket, errored_materials)
            })
        )
    elif asset_type.lower() in ('hdri', 'texture') and task_metadata.get('path_to_asset'):
        get_textures_metadata(
            assets_path,
            on_metadata_received=lambda textures_metadata: globals()['task_manager'].add('load_textures', {
                "metadata": textures_metadata,
                "renderer": renderer,
                "user_selected": event_type == 'UserSelected' if event_type else False,
                "surface_imperfections": is_surface_imperfections,
                "on_success": lambda completed: on_task_completed(client_socket, completed),
                "on_error": lambda errored_materials: on_task_error(client_socket, errored_materials)
            })
        )



#### REDSHIFT LIGHTS ####

class RsLight:
    def __init__(self, in_type, in_use_selected=False):
        self._light = None
        self._shop = hou.node("/obj")
        self._gsg_type = in_type
        self._world_position = hou.Vector3(-4, 4, 4)
        allowed_type_starters = ('rslight', 'hlight', 'envlight')

        if in_use_selected:
            all_nodes = hou.selectedNodes()
            if all_nodes == None or len(all_nodes) != 1:
                return
            node = all_nodes[0]
            if node.type().name() == 'subnet':
                children = node.children()
                for child in children:
                    type_name = child.type().name()
                    if type_name.startswith(allowed_type_starters):
                        self._light = child
                        self._shop = node
                        break
            else:
                type_name = node.type().name()
                if type_name.startswith(allowed_type_starters):
                    self._light = node

    def IsValid(self):
        return self._light is not None

    def Translate(self):
        if self._light is not None:
            self._light.parmTuple('t').set(self._world_position)

    def CreateSubnetwork(self):
        # Create the subnet and OVERRIDE _shop !!
        self._shop = self._shop.createNode('subnet', self._gsg_type.capitalize() + '_Light_Subnetwork')
        self._shop.moveToGoodPosition()

    def AddControllerAndTarget(self):
        type_name = self._gsg_type.capitalize()
        # Parent null
        parent_null = self._shop.createNode('null', type_name + '_Light_Controller')
        self._light.setInput(0, parent_null)
        # Target null
        target_null = self._shop.createNode('null', type_name + '_Light_Target')
        target_null.setInput(0, parent_null)
        self._light.parm('lookatpath').set(target_null.path())

        parent_null.moveToGoodPosition()
        self._light.moveToGoodPosition()
        target_null.moveToGoodPosition()

        # Set the subnet as the current selection
        self._shop.setCurrent(True, True)


class RsAreaLight(RsLight):
    def __init__(self, in_use_selected=False):
        super().__init__('area', in_use_selected)

    def Create(self):
        # Add the subnet and override _shop
        self.CreateSubnetwork()
        self._light = self._shop.createNode('rslight', 'Area_Light')
        self._light.parm('light_type').set('area')
        #0 : 'rectangle'
        self._light.parm('RSL_areaShape').set('0')
        self._light.parm('RSL_spread').set('0.3')
        self._light.parm('RSL_intensityMultiplier').set(10)
        self.Translate()
        self.AddControllerAndTarget()

    def IsValid(self):
        if self._light is not None:
            if not self._light.type().name().startswith('rslightdome'):
                if self._light.type().name().startswith('rslight'):
                    return self._light.parm('light_type').eval() == 3
        return False

    def SetTexture(self, in_path, *args):
        self._light.parm('TextureSampler1_tex0').set(in_path)
        self._light.parm('RSColorLayer1_layer1_enable').set(1)


class RsGoboLight(RsLight):
    def __init__(self, in_use_selected=False):
        super().__init__('gobo', in_use_selected)

    def Create(self):
        # Add the subnet and override _shop
        self.CreateSubnetwork()
        self._light = self._shop.createNode('rslight', 'Gobo_Light')
        self._light.parm('light_type').set('spot')
        self._light.parm('RSL_intensityMultiplier').set(10)
        self.Translate()
        self.AddControllerAndTarget()

    def IsValid(self):
        if super().IsValid():
            if not self._light.type().name().startswith('rslightdome'):
                if self._light.type().name().startswith('rslight'):
                    return self._light.parm('light_type').eval() == 2
        return False

    def SetTexture(self, in_path):
        self._light.parm('TextureSampler1_tex0').set(in_path)
        self._light.parm('RSColorLayer1_layer1_enable').set(1)


class RsDomeLight(RsLight):
    def __init__(self, in_use_selected=False):
        super().__init__('dome', in_use_selected)

    def Create(self):
        self._light = self._shop.createNode('rslightdome', 'Dome_Light')
        self._light.moveToGoodPosition()
        self._light.setCurrent(True, True)

    def IsValid(self):
        return super().IsValid() and self._light.type().name().startswith('rslightdome')

    def SetTexture(self, in_path):
        self._light.parm('env_map').set(in_path)


#### KARMA LIGHTS ####

class KaLight:
    def __init__(self, in_type, in_use_selected=False):
        self._light = None
        self._stage = hou.node("/stage")
        self._lightfilterlibrary = None
        self._gobo_filter = None
        self._gsg_type = in_type
        self._merge = None
        self._world_position = hou.Vector3(-4, 4, 4)

        all_nodes = hou.selectedNodes()
        if all_nodes == None or len(all_nodes) != 1:
            return
        current_node = all_nodes[0]
        current_node_type = current_node.type().name()
        if in_use_selected and 'light' in current_node_type:
            self._light = current_node

        if current_node_type == 'merge':
            self._merge = current_node

    def IsValid(self):
        return self._light is not None
    
    def SetCurrent(self):
        if self._light is not None:
            self._light.setCurrent(True, True)
        if self._merge is not None:
            self._merge.setInput(len(self._merge.inputConnections()), self._light)

        self._light.moveToGoodPosition(False, False)
        if self._lightfilterlibrary is not None:
            self._lightfilterlibrary.moveToGoodPosition(False, False)


class KaPositionalLight(KaLight):
    def __init__(self, in_type, in_use_selected=False):
        super().__init__(in_type, in_use_selected)

    def Translate(self):
        if self._light is not None:
            self._light.parmTuple('t').set(self._world_position)
    

class KaAreaLight(KaPositionalLight):
    def __init__(self, in_use_selected=False):
        super().__init__('area', in_use_selected)

    def Create(self):
        self._light = self._stage.createNode('light::2.0', 'Area_Light')
        self._light.parm('lighttype').set(4)
        self._light.parm('xn__inputswidth_zta').set(4)
        self._light.parm('xn__inputsheight_mva').set(4)
        self.Translate()
        self._light.parm('lookatenable').set(True)
        self.SetCurrent()

    def IsValid(self):
        if super().IsValid():
            if self._light.type().name().startswith('light::2.0'):
                return self._light.parm('lighttype').eval() == 4
        return False

    def SetTexture(self, in_path):
        if self.IsValid():
            self._light.parm('xn__inputstexturefile_r3ah').set(in_path)

class KaGoboLight(KaPositionalLight):
    def __init__(self, in_use_selected=False):
        super().__init__('gobo', in_use_selected)

    def Create(self):
        self._light = self._stage.createNode('light::2.0', 'Gobo_Light')
        self._light.parm('lighttype').set(3)
        self._light.parm('spotlightenable').set(True)
        self._light.parm('xn__inputsshapingconeangle_wcbhe').set(30)
        self._light.parm('xn__inputsintensity_i0a').set(10)
        self.Translate()
        self._light.parm('scale').set(10)
        self._light.parm('lookatenable').set(True)
        self.CreateGoboFilter()
        self.SetCurrent()

    def IsValid(self):
        if super().IsValid():
            if self._light.type().name().startswith('light::2.0'):
                self._lightfilterlibrary = self._light.input(0)
                if self._lightfilterlibrary is not None:
                    if self._lightfilterlibrary.type().name() == 'lightfilterlibrary':
                        self._gobo_filter = self._lightfilterlibrary.node('Gobo_Filter')
                        if self._gobo_filter is not None:
                            # Light well set up in stage. Now return if it is a spot light
                            return self._light.parm('lighttype').eval() == 3
        return False

    def CreateGoboFilter(self):
        # Crete the light filter library
        self._lightfilterlibrary = self._stage.createNode('lightfilterlibrary', 'Light_Filter_Library')
        # Create the gobo filter node
        self._gobo_filter = self._lightfilterlibrary.createNode('kma_lfilter_gobo', 'Gobo_Filter')
        # Connect the filter library to the light
        self._light.setInput(0, self._lightfilterlibrary)
        # Join the filter library scene graph path to the gobo filter name
        prefix = self._lightfilterlibrary.parm('filterpathprefix').eval()
        path = os.path.join(prefix, self._gobo_filter.name())
        # And assign it as light's filter
        self._light.parm('xn__lightfilters_lva').set(path)

    def SetTexture(self, in_path):
        if self.IsValid():
            self._gobo_filter.parm('map').set(in_path)

class KaDomeLight(KaLight):
    def __init__(self, in_use_selected=False):
        super().__init__('dome', in_use_selected)

    def Create(self):
        self._light = self._stage.createNode('domelight', 'Dome_Light')
        self.SetCurrent()

    def IsValid(self):
        return super().IsValid() and self._light.type().name().startswith('domelight')

    def SetTexture(self, in_path):
        self._light.parm('xn__inputstexturefile_r3ah').set(in_path)


# Class handling the creation of the surface imperfections nodes
# A texture connected to a default ramp. The texture is optionally driven by the triplanar mapping node.
#
class SurfaceImperfections:
    def __init__(self, in_renderer):
        self._material = None
        self._is_redshift = in_renderer == 'REDSHIFT'

        material_network_name = 'redshift_vopnet' if self._is_redshift else 'subnet'

        all_nodes = hou.selectedNodes()
        # If a material is selected, use it
        if all_nodes is not None and len(all_nodes) == 1:
            current_node = all_nodes[0]
            path = current_node.path()
            if path.startswith('/mat/'):
                current_node_type = current_node.type().name()
                if current_node_type == material_network_name:
                    self._material = current_node

        if self._material is None:
            # If no material is selected, look for the first redshift_vopnet (Redshift)
            # or subnet (Karma) in the current desktop panes.
            # With "first" I mean that there could be several /mat panes open at the same time.
            desktop = hou.ui.curDesktop()
            for panel in desktop.currentPaneTabs():
                if panel.type() == hou.paneTabType.NetworkEditor:
                    path = panel.pwd().path()
                    n = hou.node(path)
                    if n is not None and n.type().name() == material_network_name:
                        if path.startswith('/mat/'):
                            self._material = n
                            break

    def IsValid(self):
        return self._material is not None

    def FindTypedNode(self, in_node_type):
        for child in self._material.children():
            if child.type().name() == in_node_type:
                return child
        return None
    
    def FindConnectedNode(self, in_node, in_input_name):
        for c in in_node.inputConnections():
            input_name = c.inputName()
            # input_name is the name of the output socket of the input node
            output_name = c.outputName()
            # output_name is the name of the input socket of in_node
            if output_name == in_input_name:
                return c.inputNode(), input_name
        return None, ''

    def Create(self, in_texture_path):
        if not self.IsValid():
            return
        
        texture_node = ramp_node = triplanar_projection_node = None
        # Look for any triplanar projection node in the material
        any_triplanar_node = self.FindTypedNode('redshift::TriPlanar') if self._is_redshift else self.FindTypedNode('mtlxtriplanarprojection')

        if self._is_redshift:
            ramp_node = self._material.createNode('redshift::RSScalarRamp', 'imperfections_ramp')
            # Create the texture node
            texture_node = self._material.createNode('redshift::TextureSampler', 'surface_imperfections')
            texture_node.parm("tex0").set(in_texture_path)
            texture_node.parm("tex0_colorSpace").set('Raw')

            any_texture_sampler = self.FindTypedNode('redshift::TextureSampler')

            if any_triplanar_node is not None:
                triplanar_projection_node = self._material.createNode('redshift::TriPlanar', 'imperfections_triplanar')
                triplanar_projection_node.setNamedInput('imageX', texture_node, 'outColor')
                # Copy the links from the existing triplanar node
                for p in ('blendAmount', 'scale', 'offset', 'rotation'):
                    n, output_socket_name = self.FindConnectedNode(any_triplanar_node, p)
                    if n is not None:
                        triplanar_projection_node.setNamedInput(p, n, output_socket_name)
                #blend_input_node, output_socket_name = self.FindConnectedNode(any_triplanar_node, 'blendAmount')
                #if blend_input_node is not None:
                #    triplanar_projection_node.setNamedInput('blendAmount', blend_input_node, output_socket_name)

                # Connect the triplanar projection node to the ramp
                ramp_node.setNamedInput('input', triplanar_projection_node, 'outColor')
            else:
                # Connect the texture to the ramp
                ramp_node.setNamedInput('input', texture_node, 'outColor')
                for p in ('scale', 'offset', 'rotate'):
                    n, output_socket_name = self.FindConnectedNode(any_texture_sampler, p)
                    if n is not None:
                        texture_node.setNamedInput(p, n, output_socket_name)

        else: # Karma material
            ramp_node = self._material.createNode("kma_rampconst", 'imperfections_ramp')
            ramp_node.parm('signature').set('float')

            texture_placer_node = self.FindTypedNode('mtlxplace2d')

            if any_triplanar_node is not None:
                triplanar_projection_node = self._material.createNode('mtlxtriplanarprojection', 'imperfections_triplanar')
                triplanar_projection_node.parm('signature').set('color3')
                for f in ('filex', 'filey', 'filez'):
                    triplanar_projection_node.parm(f).set(in_texture_path)
                color_space = 'Raw' if ocio else 'lin_rec709'
                for f in ('filexcolorspace', 'fileycolorspace', 'filezcolorspace'):
                    triplanar_projection_node.parm(f).set(color_space)
                ramp_node.setNamedInput('t', triplanar_projection_node, 'out')

                if any_triplanar_node is not None:
                    blend_input_node, output_socket_name = self.FindConnectedNode(any_triplanar_node, 'blend')
                    if blend_input_node is not None:
                        triplanar_projection_node.setNamedInput('blend', blend_input_node, output_socket_name)

            else:
                texture_node = self._material.createNode('mtlximage', 'surface_imperfections')
                texture_node.parm('signature').set('float')
                texture_node.parm('file').set(in_texture_path)
                texture_node.parm('filecolorspace').set('Raw')

            if texture_placer_node is not None:
                # Not in triplanar mode, so connect the texture placer node to the texture node
                texture_node.setNamedInput('texcoord', texture_placer_node, 'out')
                ramp_node.setNamedInput('t', texture_node, 'out')

        for node in (texture_node, triplanar_projection_node, ramp_node):
            if node is not None:
                node.setDetailMediumFlag(True)
                node.moveToGoodPosition()


renderers = [
    RedshiftRenderer(),
    KarmaRenderer()
]


def main():
    port = int(os.environ.get('GREYSCALEGORILLA_PORT') or 8912)
    if port < 1024 or port > 49151:
        log.error('Please select a number between 1024 and 49151')
        return

    socket_server = SocketServer('localhost', port)
    socket_server.start(on_message=on_message)
    task_manager.register('load_materials', process_material)
    task_manager.register('load_models', process_models)
    task_manager.register('load_textures', process_textures)
    hou.ui.addEventLoopCallback(task_manager.check_pending)


try:
    import hdefereval
    hdefereval.executeDeferred(main)
except ImportError:
    # hdefereval is only available on GUI environment
    pass
