//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXGenMdl/MdlShaderGenerator.h>

#include <MaterialXGenMdl/MdlSyntax.h>
#include <MaterialXGenMdl/Nodes/CompoundNodeMdl.h>
#include <MaterialXGenMdl/Nodes/SourceCodeNodeMdl.h>
#include <MaterialXGenMdl/Nodes/MaterialNodeMdl.h>
#include <MaterialXGenMdl/Nodes/SurfaceNodeMdl.h>
#include <MaterialXGenMdl/Nodes/HeightToNormalNodeMdl.h>
#include <MaterialXGenMdl/Nodes/ClosureLayerNodeMdl.h>
#include <MaterialXGenMdl/Nodes/ClosureCompoundNodeMdl.h>
#include <MaterialXGenMdl/Nodes/CustomNodeMdl.h>
#include <MaterialXGenMdl/Nodes/ImageNodeMdl.h>

#include <MaterialXGenShader/Exception.h>
#include <MaterialXGenShader/GenContext.h>
#include <MaterialXGenShader/Shader.h>
#include <MaterialXGenShader/ShaderStage.h>
#include <MaterialXGenShader/Util.h>

MATERIALX_NAMESPACE_BEGIN

namespace
{

const vector<string> DEFAULT_IMPORTS =
{
    "import ::df::*",
    "import ::base::*",
    "import ::math::*",
    "import ::state::*",
    "import ::anno::*",
    "import ::tex::*",
    "using ::materialx::core import *",
    "using ::materialx::sampling import *",
};

const vector<string> DEFAULT_VERSIONED_IMPORTS =
{
    "using ::materialx::stdlib_",
    "using ::materialx::pbrlib_",
};

const string IMPORT_ALL = " import *";

const string MDL_VERSION_1_6 = "1.6";
const string MDL_VERSION_1_7 = "1.7";
const string MDL_VERSION_1_8 = "1.8";
const string MDL_VERSION_1_9 = "1.9";
const string MDL_VERSION_1_10 = "1.10";
const string MDL_VERSION_SUFFIX_1_6 = "1_6";
const string MDL_VERSION_SUFFIX_1_7 = "1_7";
const string MDL_VERSION_SUFFIX_1_8 = "1_8";
const string MDL_VERSION_SUFFIX_1_9 = "1_9";
const string MDL_VERSION_SUFFIX_1_10 = "1_10";

} // anonymous namespace

const string MdlShaderGenerator::TARGET = "genmdl";
const string GenMdlOptions::GEN_CONTEXT_USER_DATA_KEY = "genmdloptions";

const std::unordered_map<string, string> MdlShaderGenerator::GEOMPROP_DEFINITIONS =
{
    { "Pobject", "state::transform_point(state::coordinate_internal, state::coordinate_object, state::position())" },
    { "Pworld", "state::transform_point(state::coordinate_internal, state::coordinate_world, state::position())" },
    { "Nobject", "state::transform_normal(state::coordinate_internal, state::coordinate_object, state::normal())" },
    { "Nworld", "state::transform_normal(state::coordinate_internal, state::coordinate_world, state::normal())" },
    { "Tobject", "state::transform_vector(state::coordinate_internal, state::coordinate_object, state::texture_tangent_u(0))" },
    { "Tworld", "state::transform_vector(state::coordinate_internal, state::coordinate_world, state::texture_tangent_u(0))" },
    { "Bobject", "state::transform_vector(state::coordinate_internal, state::coordinate_object, state::texture_tangent_v(0))" },
    { "Bworld", "state::transform_vector(state::coordinate_internal, state::coordinate_world, state::texture_tangent_v(0))" },
    { "UV0", "float2(state::texture_coordinate(0).x, state::texture_coordinate(0).y)" },
    { "Vworld", "state::direction()" }
};

//
// MdlShaderGenerator methods
//

MdlShaderGenerator::MdlShaderGenerator(TypeSystemPtr typeSystem) :
    ShaderGenerator(typeSystem, MdlSyntax::create(typeSystem))
{
    // Register custom types to handle enumeration output
    _typeSystem->registerType(Type::MDL_COORDINATESPACE);
    _typeSystem->registerType(Type::MDL_ADDRESSMODE);
    _typeSystem->registerType(Type::MDL_FILTERLOOKUPMODE);
    _typeSystem->registerType(Type::MDL_FILTERTYPE);
    _typeSystem->registerType(Type::MDL_DISTRIBUTIONTYPE);
    _typeSystem->registerType(Type::MDL_SCATTER_MODE);

    // Register build-in implementations

    // <!-- <surfacematerial> -->
    registerImplementation("IM_surfacematerial_" + MdlShaderGenerator::TARGET, MaterialNodeMdl::create);

    // <!-- <surface> -->
    registerImplementation("IM_surface_" + MdlShaderGenerator::TARGET, SurfaceNodeMdl::create);

    // <!-- <heighttonormal> -->
    registerImplementation("IM_heighttonormal_vector3_" + MdlShaderGenerator::TARGET, HeightToNormalNodeMdl::create);

    // <!-- <layer> -->
    registerImplementation("IM_layer_bsdf_" + MdlShaderGenerator::TARGET, ClosureLayerNodeMdl::create);
    registerImplementation("IM_layer_vdf_" + MdlShaderGenerator::TARGET, ClosureLayerNodeMdl::create);

    // <!-- <dielectric_bsdf> -->
    registerImplementation("IM_dielectric_bsdf_" + MdlShaderGenerator::TARGET, LayerableNodeMdl::create);

    // <!-- <generalized_schlick_bsdf> -->
    registerImplementation("IM_generalized_schlick_bsdf_" + MdlShaderGenerator::TARGET, LayerableNodeMdl::create);

    // <!-- <sheen_bsdf> -->
    registerImplementation("IM_sheen_bsdf_" + MdlShaderGenerator::TARGET, LayerableNodeMdl::create);

    // <!-- <image> -->
    registerImplementation("IM_image_float_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
    registerImplementation("IM_image_color3_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
    registerImplementation("IM_image_color4_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
    registerImplementation("IM_image_vector2_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
    registerImplementation("IM_image_vector3_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
    registerImplementation("IM_image_vector4_" + MdlShaderGenerator::TARGET, ImageNodeMdl::create);
}

void MdlShaderGenerator::applyDefaultOptions(GenOptions& options) const
{
    ShaderGenerator::applyDefaultOptions(options);
    options.distributeLayerOverBsdfMix = true;
}

ShaderPtr MdlShaderGenerator::generate(const string& name, ElementPtr element, GenContext& context) const
{
    // For MDL we cannot cache node implementations between generation calls,
    // because this generator needs to do edits to subgraphs implementations
    // depending on the context in which a node is used.
    context.clearNodeImplementations();

    ShaderPtr shader = createShader(name, element, context);

    // Request fixed floating-point notation for consistency across targets.
    ScopedFloatFormatting fmt(Value::FloatFormatFixed);

    ShaderGraph& graph = shader->getGraph();
    ShaderStage& stage = shader->getStage(Stage::PIXEL);

    // Emit version
    emitMdlVersionNumber(context, stage);
    emitLineBreak(stage);

    // Emit module imports
    for (const string& module : DEFAULT_IMPORTS)
    {
        emitLine(module, stage);
    }
    for (const string& module : DEFAULT_VERSIONED_IMPORTS)
    {
        emitString(module, stage);
        emitMdlVersionFilenameSuffix(context, stage);
        emitString(IMPORT_ALL, stage);
        emitLineEnd(stage, true);
    }

    // Emit custom node imports for nodes in the graph
    for (ShaderNode* node : graph.getNodes())
    {
        const ShaderNodeImpl& impl = node->getImplementation();
        const CustomCodeNodeMdl* customNode = dynamic_cast<const CustomCodeNodeMdl*>(&impl);
        if (customNode)
        {
            const string& importName = customNode->getQualifiedModuleName();
            if (!importName.empty())
            {
                emitString("import ", stage);
                emitString(importName, stage);
                emitString("::*", stage);
                emitLineEnd(stage, true);
            }
        }
    }

    // Add global constants and type definitions
    emitTypeDefinitions(context, stage);

    // Emit function definitions for all nodes
    emitFunctionDefinitions(graph, context, stage);

    // Emit shader type, determined from the first
    // output if there are multiple outputs.
    const ShaderGraphOutputSocket* outputSocket = graph.getOutputSocket(0);
    emitString("export material ", stage);

    // Begin shader signature. Note that the function name is already sanitized.
    string functionName = shader->getName();
    setFunctionName(functionName, stage);
    emitLine(functionName, stage, false);
    emitScopeBegin(stage, Syntax::PARENTHESES);

    // Emit shader inputs
    emitShaderInputs(stage.getInputBlock(MDL::INPUTS), stage);

    // End shader signature
    emitScopeEnd(stage);

    // Begin shader body
    emitLine("= let", stage, false);
    emitScopeBegin(stage);

    // Emit constants
    const VariableBlock& constants = stage.getConstantBlock();
    if (constants.size())
    {
        emitVariableDeclarations(constants, _syntax->getConstantQualifier(), Syntax::SEMICOLON, context, stage);
        emitLineBreak(stage);
    }

    // Emit shader inputs that have been filtered during printing of the public interface
    const string uniformPrefix = _syntax->getUniformQualifier() + " ";
    for (ShaderGraphInputSocket* inputSocket : graph.getInputSockets())
    {
        if (inputSocket->getConnections().size() &&
            (inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_SHADER ||
             inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_CLOSURE ||
             inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_MATERIAL))
        {
            const string& qualifier = inputSocket->isUniform() || inputSocket->getType() == Type::FILENAME
                ? uniformPrefix
                : EMPTY_STRING;
            const string& type = _syntax->getTypeName(inputSocket->getType());

            emitLineBegin(stage);
            emitString(qualifier + type + " " + inputSocket->getVariable() + " = ", stage);
            emitString(_syntax->getDefaultValue(inputSocket->getType(), true), stage);
            emitLineEnd(stage, true);
        }
    }

    // Emit all texturing nodes. These are inputs to any
    // closure/shader nodes and need to be emitted first.
    emitFunctionCalls(graph, context, stage, ShaderNode::Classification::TEXTURE);

    // Emit function calls for "root" closure/shader nodes.
    // These will internally emit function calls for any dependent closure nodes upstream.
    bool rootFunctionCallEmitted = false;
    for (ShaderGraphOutputSocket* socket : graph.getOutputSockets())
    {
        if (socket->getConnection())
        {
            const ShaderNode* upstream = socket->getConnection()->getNode();
            if (upstream->getParent() == &graph &&
                (upstream->hasClassification(ShaderNode::Classification::CLOSURE) ||
                 upstream->hasClassification(ShaderNode::Classification::SHADER)))
            {
                emitFunctionCall(*upstream, context, stage);
                rootFunctionCallEmitted = true;
            }
        }
    }

    // Get final result
    const string result = getUpstreamResult(outputSocket, context);

    const TypeDesc outputType = outputSocket->getType();
    // Try to return some meaningful color in case the output is not a material
    if (graph.hasClassification(ShaderNode::Classification::TEXTURE))
    {
        if (outputType == Type::DISPLACEMENTSHADER)
        {
            emitLine("float3 displacement__ = " + result + ".geometry.displacement", stage);
            emitLine("color finalOutput__ = mk_color3("
                     "r: math::dot(displacement__, state::texture_tangent_u(0)),"
                     "g: math::dot(displacement__, state::texture_tangent_v(0)),"
                     "b: math::dot(displacement__, state::normal()))",
                     stage);
        }
        else
        {
            emitLine("float3 displacement__ = float3(0.0)", stage);
            std::string finalOutput = "mk_color3(0.0)";
            if (outputType == Type::BOOLEAN)
                finalOutput = result + " ? mk_color3(1.0, 1.0, 1.0) : mk_color3(0.0, 0.0, 0.0)";
            else if (outputType == Type::INTEGER)
                finalOutput = "mk_color3(" + result + ")";
            else if (outputType == Type::FLOAT)
                finalOutput = "mk_color3(" + result + ")";
            else if (outputType == Type::VECTOR2)
                finalOutput = "mk_color3(" + result + ".x, " + result + ".y, 0.0)";
            else if (outputType == Type::VECTOR3)
                finalOutput = "mk_color3(" + result + ")";
            else if (outputType == Type::VECTOR4)
                finalOutput = "mk_color3(" + result + ".x, " + result + ".y, " + result + ".z)";
            else if (outputType == Type::COLOR3)
                finalOutput = result;
            else if (outputType == Type::COLOR4)
                finalOutput = result + ".rgb";
            else if (outputType == Type::MATRIX33 || outputType == Type::MATRIX44)
                finalOutput = "mk_color3(" + result + "[0][0], " + result + "[1][1], " + result + "[2][2])";

            emitLine("color finalOutput__ = " + finalOutput, stage);
        }

        // End shader body
        emitScopeEnd(stage);

        static const string textureMaterial =
            "in material\n"
            "(\n"
            "    surface: material_surface(\n"
            "        emission : material_emission(\n"
            "            emission : df::diffuse_edf(),\n"
            "            intensity : finalOutput__ * math::PI,\n"
            "            mode : intensity_radiant_exitance\n"
            "        )\n"
            "    ),\n"
            "    geometry: material_geometry(\n"
            "       displacement : displacement__\n"
            "    )\n"
            ");";
        emitBlock(textureMaterial, FilePath(), context, stage);
    }
    else
    {
        if (rootFunctionCallEmitted)
        {
            emitLine(_syntax->getTypeSyntax(outputType).getName() + " finalOutput__ = " + result, stage);
        }
        else
        {
            // No code has been emitted for "result". Use default material as fallback.
            emitLine(_syntax->getTypeSyntax(outputType).getName() + " finalOutput__ = material()", stage);
        }

        // End shader body
        emitScopeEnd(stage);

        static const string shaderMaterial = "in material(finalOutput__);";
        emitBlock(shaderMaterial, FilePath(), context, stage);
    }

    // Perform token substitution
    replaceTokens(_tokenSubstitutions, stage);

    return shader;
}

ShaderNodeImplPtr MdlShaderGenerator::createShaderNodeImplForNodeGraph(const NodeGraph& nodegraph) const
{
    vector<OutputPtr> outputs = nodegraph.getActiveOutputs();
    if (outputs.empty())
    {
        throw ExceptionShaderGenError("NodeGraph '" + nodegraph.getName() + "' has no outputs defined");
    }

    const TypeDesc outputType = _typeSystem->getType(outputs[0]->getType());

    ShaderNodeImplPtr impl;
    // Use a compound implementation.
    if (outputType.isClosure())
    {
        return ClosureCompoundNodeMdl::create();
    }
    return CompoundNodeMdl::create();
}

ShaderNodeImplPtr MdlShaderGenerator::createShaderNodeImplForImplementation(const Implementation& implElement) const
{
    // When `file` and `function` are provided we consider this node a user node
    const string file = implElement.getTypedAttribute<string>("file");
    const string function = implElement.getTypedAttribute<string>("function");
    // Or, if `sourcecode` is provided we consider this node a user node with inline implementation
    // inline implementations are not supposed to have replacement markers
    const string sourcecode = implElement.getTypedAttribute<string>("sourcecode");
    if ((!file.empty() && !function.empty()) || (!sourcecode.empty() && sourcecode.find("{{") == string::npos))
    {
        return CustomCodeNodeMdl::create();
    }
    if (file.empty() && sourcecode.empty())
    {
        throw ExceptionShaderGenError("No valid MDL implementation found for '" + implElement.getName() + "'");
    }
    return SourceCodeNodeMdl::create();
}

string MdlShaderGenerator::getUpstreamResult(const ShaderInput* input, GenContext& context) const
{
    const ShaderOutput* upstreamOutput = input->getConnection();

    if (!upstreamOutput || upstreamOutput->getNode()->isAGraph())
    {
        return ShaderGenerator::getUpstreamResult(input, context);
    }

    const MdlSyntax& mdlSyntax = static_cast<const MdlSyntax&>(getSyntax());
    string variable;
    const ShaderNode* upstreamNode = upstreamOutput->getNode();
    if (!upstreamNode->isAGraph() && upstreamNode->numOutputs() > 1)
    {
        const CompoundNodeMdl* upstreamNodeMdl = dynamic_cast<const CompoundNodeMdl*>(&upstreamNode->getImplementation());
        if (upstreamNodeMdl && upstreamNodeMdl->unrollReturnStructMembers())
        {
            variable = upstreamNode->getName() + "__" + upstreamOutput->getName();
        }
        else
        {
            const string& fieldName = upstreamOutput->getName();
            const CustomCodeNodeMdl* upstreamCustomNodeMdl = dynamic_cast<const CustomCodeNodeMdl*>(&upstreamNode->getImplementation());
            if (upstreamCustomNodeMdl)
            {
                // Prefix the port name depending on the CustomCodeNode
                variable = upstreamNode->getName() + "_result." + upstreamCustomNodeMdl->modifyPortName(fieldName, mdlSyntax);
            }
            else
            {
                // Existing implementations and none user defined structs will keep the prefix always to not break existing content
                variable = upstreamNode->getName() + "_result." + mdlSyntax.modifyPortName(upstreamOutput->getName());
            }
        }
    }
    else
    {
        variable = upstreamOutput->getVariable();
    }

    // Look for any additional suffix to append
    string suffix;
    context.getInputSuffix(input, suffix);
    if (!suffix.empty())
    {
        variable += suffix;
    }

    return variable;
}

namespace
{

// [TODO]
// Here we assume this bit of the port flags is unused.
// Change this to a more general and safe solution.
class ShaderPortFlagMdl
{
  public:
    static const uint32_t TRANSMISSION_IOR_DEPENDENCY = 1u << 31;
};

// Check if a graph has inputs with dependencies on transmission IOR on the inside.
// Track all subgraphs found that has such a dependency, as well as subgraphs that are
// found to have a varying connection to transmission IOR.
// Returns true if uniform ior dependencies are found.
bool checkTransmissionIorDependencies(ShaderGraph* g, std::set<ShaderGraph*>& graphsWithIorDependency, std::set<ShaderGraph*>& graphsWithIorVarying)
{
    bool result = false;
    for (ShaderNode* node : g->getNodes())
    {
        ShaderGraph* subgraph = node->getImplementation().getGraph();
        if (subgraph)
        {
            // Check recursively if this subgraph has IOR dependencies.
            if (checkTransmissionIorDependencies(subgraph, graphsWithIorDependency, graphsWithIorVarying))
            {
                for (ShaderOutput* socket : subgraph->getInputSockets())
                {
                    if (socket->getFlag(ShaderPortFlagMdl::TRANSMISSION_IOR_DEPENDENCY))
                    {
                        ShaderInput* input = node->getInput(socket->getName());
                        ShaderOutput* source = input ? input->getConnection() : nullptr;
                        if (source)
                        {
                            // Check if this is a graph interface connection.
                            if (source->getNode() == g)
                            {
                                graphsWithIorDependency.insert(g);
                                source->setFlag(ShaderPortFlagMdl::TRANSMISSION_IOR_DEPENDENCY, true);
                                result = true;
                            }
                            else if (source->getNode()->hasClassification(ShaderNode::Classification::CONSTANT))
                            {
                                // If the connection is to a constant node we can
                                // handled that here since it's just a uniform value.
                                ShaderInput* value = source->getNode()->getInput(ValueElement::VALUE_ATTRIBUTE);
                                if (value && value->getValue())
                                {
                                    input->setValue(value->getValue());
                                }
                                input->breakConnection();
                            }
                            else
                            {
                                // If we get here we have to assume this is a varying connection.
                                // Save the graph as a varying graph so we later can break its
                                // internal connections to transmission IOR.
                                graphsWithIorVarying.insert(subgraph);
                                return false; // no need to continue with this subgraph
                            }
                        }
                    }
                }
            }
        }
        else
        {
            // Check for transmission BSDF node.
            if (node->hasClassification(ShaderNode::Classification::BSDF_T))
            {
                // Check if IOR is connected.
                ShaderInput* ior = node->getInput("ior");
                ShaderOutput* source = ior ? ior->getConnection() : nullptr;
                if (source)
                {
                    // Check if this is a graph interface connection.
                    if (source->getNode() == g)
                    {
                        graphsWithIorDependency.insert(g);
                        source->setFlag(ShaderPortFlagMdl::TRANSMISSION_IOR_DEPENDENCY, true);
                        result = true;
                    }
                    else if (source->getNode()->hasClassification(ShaderNode::Classification::CONSTANT))
                    {
                        // If the connection is to a constant node we can
                        // handled that here since it's just a uniform value.
                        ShaderInput* value = source->getNode()->getInput(ValueElement::VALUE_ATTRIBUTE);
                        if (value && value->getValue())
                        {
                            ior->setValue(value->getValue());
                        }
                        ior->breakConnection();
                    }
                    else
                    {
                        // If we get here we have to assume this is a varying connection
                        // and we can break it immediately here.
                        ior->breakConnection();
                    }
                }
            }
        }
    }
    return result;
}

// Disconnect any incoming connections to transmission IOR
// inside a graph.
void disconnectTransmissionIor(ShaderGraph* g)
{
    for (ShaderNode* node : g->getNodes())
    {
        ShaderGraph* subgraph = node->getImplementation().getGraph();
        if (subgraph && (subgraph->hasClassification(ShaderNode::Classification::SHADER) ||
                         subgraph->hasClassification(ShaderNode::Classification::CLOSURE)))
        {
            disconnectTransmissionIor(subgraph);
        }
        else if (node->hasClassification(ShaderNode::Classification::BSDF_T))
        {
            ShaderInput* ior = node->getInput("ior");
            if (ior)
            {
                ior->breakConnection();
            }
        }
    }
}

} // anonymous namespace

ShaderPtr MdlShaderGenerator::createShader(const string& name, ElementPtr element, GenContext& context) const
{
    // Create the root shader graph
    ShaderGraphPtr graph = ShaderGraph::create(nullptr, name, element, context);
    ShaderPtr shader = std::make_shared<Shader>(name, graph);

    // Create our stage.
    ShaderStagePtr stage = createStage(Stage::PIXEL, *shader);
    VariableBlockPtr inputs = stage->createInputBlock(MDL::INPUTS);
    VariableBlockPtr outputs = stage->createOutputBlock(MDL::OUTPUTS);

    // Create shader variables for all nodes that need this.
    createVariables(graph, context, *shader);

    // Create inputs for the published graph interface.
    for (ShaderGraphInputSocket* inputSocket : graph->getInputSockets())
    {
        // Only for inputs that are connected/used internally,
        // and are editable by users.
        if (inputSocket->getConnections().size() && graph->isEditable(*inputSocket))
        {
            if (inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_SHADER ||
                inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_CLOSURE ||
                inputSocket->getType().getSemantic() == TypeDesc::SEMANTIC_MATERIAL)
            {
                continue;
            }

            inputs->add(inputSocket->getSelf());
        }
    }

    // Create outputs from the graph interface.
    for (ShaderGraphOutputSocket* outputSocket : graph->getOutputSockets())
    {
        outputs->add(outputSocket->getSelf());
    }

    // MDL does not allow varying data connected to transmission IOR until MDL 1.9.
    // We must find all uses of transmission IOR and make sure we don't
    // have a varying connection to it. If a varying connection is found
    // we break that connection and revert to using default value on that
    // instance of IOR, so that other uses of the same varying input still
    // works in other places.
    // As a result if a varying connections is set on transmission IOR
    // it just reverts to default value. Varying data on transmission IOR
    // is very rare so this is normally not a problem in practice.
    // One use-case where this fix is important is for shading models with
    // a single IOR input, that gets connected to both reflection and
    // transmission IOR inside the shading model graph. For such cases
    // this fix will disconnect the transmission IOR on the inside, but
    // still support the connection to reflection IOR.
    //
    GenMdlOptions::MdlVersion version = getMdlVersion(context);
    bool uniformIorRequired =
        version == GenMdlOptions::MdlVersion::MDL_1_6 ||
        version == GenMdlOptions::MdlVersion::MDL_1_7 ||
        version == GenMdlOptions::MdlVersion::MDL_1_8;
    if (uniformIorRequired && (
        graph->hasClassification(ShaderNode::Classification::SHADER) ||
        graph->hasClassification(ShaderNode::Classification::CLOSURE)))
    {
        // Find dependencies on transmission IOR.
        std::set<ShaderGraph*> graphsWithIorDependency;
        std::set<ShaderGraph*> graphsWithIorVarying;
        checkTransmissionIorDependencies(graph.get(), graphsWithIorDependency, graphsWithIorVarying);

        // For any graphs found that has a varying connection
        // to transmission IOR we need to break that connection.
        for (ShaderGraph* g : graphsWithIorVarying)
        {
            disconnectTransmissionIor(g);
            graphsWithIorDependency.erase(g);
        }

        // For graphs that has a dependency with transmission IOR on the inside,
        // we can declare the corresponding inputs as being uniform and preserve
        // the internal connection to transmission IOR.
        for (ShaderGraph* g : graphsWithIorDependency)
        {
            for (ShaderOutput* socket : g->getInputSockets())
            {
                if (socket->getFlag(ShaderPortFlagMdl::TRANSMISSION_IOR_DEPENDENCY))
                {
                    socket->setUniform();
                }
            }
        }
    }

    return shader;
}

namespace
{

void emitInputAnnotations(const MdlShaderGenerator& _this, const ShaderPort* variable, ShaderStage& stage)
{
    // allows to relate between MaterialX and MDL parameters when looking at the MDL code.
    const std::string mtlxParameterPathAnno = "materialx::core::origin(\"" + variable->getPath() + "\")";

    _this.emitLineEnd(stage, false);
    _this.emitLine("[[", stage, false);
    _this.emitLineBegin(stage);
    _this.emitString(_this.getSyntax().getIndentation() + mtlxParameterPathAnno, stage);
    const ShaderGraphInputSocket* input = static_cast<const ShaderGraphInputSocket*>(variable);

    if (input->getConnections().empty())
    {
        _this.emitString(",", stage);
        _this.emitLineEnd(stage, false);
        _this.emitLineBegin(stage);
        _this.emitString(_this.getSyntax().getIndentation() + "anno::unused()", stage);
    }

    _this.emitLineEnd(stage, false);
    _this.emitLineBegin(stage);
    _this.emitString("]]", stage); // line ending follows by caller
}

} // anonymous namespace

void MdlShaderGenerator::emitShaderInputs(const VariableBlock& inputs, ShaderStage& stage) const
{
    const string uniformPrefix = _syntax->getUniformQualifier() + " ";
    for (size_t i = 0; i < inputs.size(); ++i)
    {
        const ShaderPort* input = inputs[i];

        const string& qualifier = input->isUniform() || input->getType() == Type::FILENAME ? uniformPrefix : EMPTY_STRING;
        const string& type = _syntax->getTypeName(input->getType());

        string value = input->getValue() ? _syntax->getValue(input->getType(), *input->getValue(), true) : EMPTY_STRING;
        const string& geomprop = input->getGeomProp();
        if (!geomprop.empty())
        {
            auto it = GEOMPROP_DEFINITIONS.find(geomprop);
            if (it != GEOMPROP_DEFINITIONS.end())
            {
                value = it->second;
            }
        }
        if (value.empty())
        {
            value = _syntax->getDefaultValue(input->getType(), true);
        }

        emitLineBegin(stage);
        emitString(qualifier + type + " " + input->getVariable() + " = " + value, stage);
        emitInputAnnotations(*this, input, stage);

        if (i < inputs.size() - 1)
        {
            emitString(",", stage);
        }

        emitLineEnd(stage, false);
    }
}

GenMdlOptions::MdlVersion MdlShaderGenerator::getMdlVersion(GenContext& context) const
{
    GenMdlOptionsPtr options = context.getUserData<GenMdlOptions>(GenMdlOptions::GEN_CONTEXT_USER_DATA_KEY);
    return options ? options->targetVersion : GenMdlOptions::MdlVersion::MDL_LATEST;
}

void MdlShaderGenerator::emitMdlVersionNumber(GenContext& context, ShaderStage& stage) const
{
    GenMdlOptions::MdlVersion version = getMdlVersion(context);

    emitLineBegin(stage);
    emitString("mdl ", stage);
    switch (version)
    {
        case GenMdlOptions::MdlVersion::MDL_1_6:
            emitString(MDL_VERSION_1_6, stage);
            break;
        case GenMdlOptions::MdlVersion::MDL_1_7:
            emitString(MDL_VERSION_1_7, stage);
            break;
        case GenMdlOptions::MdlVersion::MDL_1_8:
            emitString(MDL_VERSION_1_8, stage);
            break;
        case GenMdlOptions::MdlVersion::MDL_1_9:
            emitString(MDL_VERSION_1_9, stage);
            break;
        default:
            // GenMdlOptions::MdlVersion::MDL_1_10
            // GenMdlOptions::MdlVersion::MDL_LATEST
            emitString(MDL_VERSION_1_10, stage);
            break;
    }
    emitLineEnd(stage, true);
}


const string& MdlShaderGenerator::getMdlVersionFilenameSuffix(GenContext& context) const
{
    GenMdlOptions::MdlVersion version = getMdlVersion(context);

    switch (version)
    {
        case GenMdlOptions::MdlVersion::MDL_1_6:
            return MDL_VERSION_SUFFIX_1_6;
        case GenMdlOptions::MdlVersion::MDL_1_7:
            return MDL_VERSION_SUFFIX_1_7;
        case GenMdlOptions::MdlVersion::MDL_1_8:
            return MDL_VERSION_SUFFIX_1_8;
        case GenMdlOptions::MdlVersion::MDL_1_9:
            return MDL_VERSION_SUFFIX_1_9;
        default:
            // GenMdlOptions::MdlVersion::MDL_1_10
            // GenMdlOptions::MdlVersion::MDL_LATEST
            return MDL_VERSION_SUFFIX_1_10;
    }
}

void MdlShaderGenerator::emitMdlVersionFilenameSuffix(GenContext& context, ShaderStage& stage) const
{
    emitString(getMdlVersionFilenameSuffix(context), stage);
}

void MdlShaderGenerator::emitTypeDefinitions(GenContext&, ShaderStage& stage) const
{
    // Emit typedef statements for all data types that have an alias
    for (const auto& syntax : _syntax->getTypeSyntaxes())
    {
        if (!syntax->getTypeDefinition().empty())
        {
            stage.addLine("export " + syntax->getTypeDefinition(), false);
        }
    }
    stage.newLine();
}

namespace MDL
{
// Identifiers for MDL variable blocks
const string INPUTS = "i";
const string OUTPUTS = "o";
} // namespace MDL

MATERIALX_NAMESPACE_END
