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

#include <MaterialXGenShader/ShaderNode.h>

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

#include <MaterialXTrace/Tracing.h>

MATERIALX_NAMESPACE_BEGIN

const string ShaderMetadataRegistry::USER_DATA_NAME = "ShaderMetadataRegistry";

//
// ShaderPort methods
//

ShaderPort::ShaderPort(ShaderNode* node, TypeDesc type, const string& name, ValuePtr value) :
    _node(node),
    _type(type),
    _name(name),
    _variable(name),
    _value(value),
    _flags(0)
{
}

string ShaderPort::getFullName() const
{
    return (_node->getName() + "_" + _name);
}

string ShaderPort::getValueString() const
{
    return getValue() ? getValue()->getValueString() : EMPTY_STRING;
}

//
// ShaderInput methods
//

ShaderInput::ShaderInput(ShaderNode* node, TypeDesc type, const string& name) :
    ShaderPort(node, type, name),
    _connection(nullptr)
{
}

void ShaderInput::makeConnection(ShaderOutput* src)
{
    // Make sure this is a new connection.
    if (src != _connection)
    {
        // Break the old connection.
        breakConnection();
        if (src)
        {
            // Make the new connection.
            if (src->getNode() == getNode() && !getNode()->isAGraph())
            {
                throw ExceptionShaderGenError(
                    "Tried to create looping connection on node " + getNode()->getName()
                        + " from output: " + src->getFullName() + " to input: " + getFullName());
            }

            _connection = src;
            src->_connections.push_back(this);
        }
    }
}

void ShaderInput::breakConnection()
{
    if (_connection)
    {
        // Find and erase this input from the connected output's connection vector.
        ShaderInputVec& connected = _connection->_connections;
        ShaderInputVec::iterator it = std::find(connected.begin(), connected.end(), this);
        if (it != connected.end())
        {
            connected.erase(it);
        }
        // Clear the connection.
        _connection = nullptr;
    }
}

ShaderNode* ShaderInput::getConnectedSibling() const
{
    if (_connection && _connection->getNode()->getParent() == _node->getParent())
    {
        return _connection->getNode();
    }
    return nullptr;
}

//
// ShaderOutput methods
//

ShaderOutput::ShaderOutput(ShaderNode* node, TypeDesc type, const string& name) :
    ShaderPort(node, type, name)
{
}

void ShaderOutput::makeConnection(ShaderInput* dst)
{
    dst->makeConnection(this);
}

void ShaderOutput::breakConnection(ShaderInput* dst)
{
    if (std::find(_connections.begin(), _connections.end(), dst) == _connections.end())
    {
        throw ExceptionShaderGenError(
            "Cannot break non-existent connection from output: " + getFullName() + " to input: " + dst->getFullName());
    }
    dst->breakConnection();
}

void ShaderOutput::breakConnections()
{
    ShaderInputVec inputVec(_connections);
    for (ShaderInput* input : inputVec)
    {
        input->breakConnection();
    }

    if (!_connections.empty())
    {
        throw ExceptionShaderGenError("Number of output connections not broken properly'" + std::to_string(_connections.size()) +
                                      " for output: " + getFullName());
    }
}

namespace
{
ShaderNodePtr createEmptyNode()
{
    return std::make_shared<ShaderNode>(nullptr, "");
}
} // namespace

const ShaderNodePtr ShaderNode::NONE = createEmptyNode();

const string ShaderNode::CONSTANT = "constant";
const string ShaderNode::DOT = "dot";
const string ShaderNode::IMAGE = "image";
const string ShaderNode::SURFACESHADER = "surfaceshader";
const string ShaderNode::BACKSURFACESHADER = "backsurfaceshader";
const string ShaderNode::BSDF_R = "R";
const string ShaderNode::BSDF_T = "T";
const string ShaderNode::TEXTURE2D_GROUPNAME = "texture2d";
const string ShaderNode::TEXTURE3D_GROUPNAME = "texture3d";
const string ShaderNode::PROCEDURAL2D_GROUPNAME = "procedural2d";
const string ShaderNode::PROCEDURAL3D_GROUPNAME = "procedural3d";
const string ShaderNode::GEOMETRIC_GROUPNAME = "geometric";

//
// ShaderNode methods
//

ShaderNode::ShaderNode(const ShaderGraph* parent, const string& name) :
    _parent(parent),
    _name(name),
    _uniqueId(name),
    _classification(0),
    _impl(nullptr)
{
}

ShaderNodePtr ShaderNode::create(const ShaderGraph* parent, const string& name, const NodeDef& nodeDef, GenContext& context)
{
    MX_TRACE_FUNCTION(Tracing::Category::ShaderGen);
    MX_TRACE_SCOPE(Tracing::Category::ShaderGen, name.c_str());

    ShaderNodePtr newNode = std::make_shared<ShaderNode>(parent, name);

    const ShaderGenerator& shadergen = context.getShaderGenerator();

    // Find the implementation for this nodedef
    newNode->_impl = shadergen.getImplementation(nodeDef, context);
    if (!newNode->_impl)
    {
        throw ExceptionShaderGenError("Could not find a matching implementation for node '" + nodeDef.getNodeString() +
                                      "' matching target '" + shadergen.getTarget() + "'");
    }

    // Create interface from nodedef
    for (const ValueElementPtr& port : nodeDef.getActiveValueElements())
    {
        const TypeDesc portType = context.getTypeDesc(port->getType());
        if (port->isA<Output>())
        {
            newNode->addOutput(port->getName(), portType);
        }
        else if (port->isA<Input>())
        {
            ShaderInput* input;
            const string& portValue = port->getResolvedValueString();
            std::pair<TypeDesc, ValuePtr> enumResult;
            const string& enumNames = port->getAttribute(ValueElement::ENUM_ATTRIBUTE);
            if (context.getShaderGenerator().getSyntax().remapEnumeration(portValue, portType, enumNames, enumResult))
            {
                input = newNode->addInput(port->getName(), enumResult.first);
                input->setValue(enumResult.second, false);
            }
            else
            {
                input = newNode->addInput(port->getName(), portType);
                if (!portValue.empty())
                {
                    input->setValue(port->getResolvedValue(), false);
                }
            }
            if (port->getIsUniform())
            {
                input->setUniform();
            }
        }
    }

    // Add any additional inputs required by the implementation
    newNode->getImplementation().addInputs(*newNode, context);

    // Add a default output if needed
    if (newNode->numOutputs() == 0)
    {
        newNode->addOutput("out", context.getTypeDesc(nodeDef.getType()));
    }

    const string& nodeDefName = nodeDef.getName();
    const string& groupName = nodeDef.getNodeGroup();

    //
    // Set node classification, defaulting to texture node
    //
    newNode->_classification = Classification::TEXTURE;

    // First, check for specific output types
    const ShaderOutput* primaryOutput = newNode->getOutput();
    if (primaryOutput->getType() == Type::MATERIAL)
    {
        newNode->_classification = Classification::MATERIAL;
    }
    else if (primaryOutput->getType() == Type::SURFACESHADER)
    {
        if (nodeDefName == "ND_surface_unlit")
        {
            newNode->_classification = Classification::SHADER | Classification::SURFACE | Classification::UNLIT;
        }
        else
        {
            newNode->_classification = Classification::SHADER | Classification::SURFACE | Classification::CLOSURE;
        }
    }
    else if (primaryOutput->getType() == Type::VOLUMESHADER)
    {
        newNode->_classification = Classification::SHADER | Classification::VOLUME | Classification::CLOSURE;
    }
    else if (primaryOutput->getType() == Type::LIGHTSHADER)
    {
        newNode->_classification = Classification::LIGHT | Classification::SHADER | Classification::CLOSURE;
    }
    else if (primaryOutput->getType() == Type::BSDF)
    {
        newNode->_classification = Classification::BSDF | Classification::CLOSURE;

        // Add additional classifications for BSDF reflection and/or transmission.
        const string& bsdfType = nodeDef.getAttribute("bsdf");
        if (bsdfType == BSDF_R)
        {
            newNode->_classification |= Classification::BSDF_R;
        }
        else if (bsdfType == BSDF_T)
        {
            newNode->_classification |= Classification::BSDF_T;
        }
        else
        {
            newNode->_classification |= (Classification::BSDF_R | Classification::BSDF_T);
        }

        // Check specifically for the vertical layering node
        if (nodeDefName == "ND_layer_bsdf" || nodeDefName == "ND_layer_vdf")
        {
            newNode->_classification |= Classification::LAYER;
        }

        // Check specifically for the mix node
        if (nodeDefName == "ND_mix_bsdf")
        {
            newNode->_classification |= Classification::MIX;
        }
    }
    else if (primaryOutput->getType() == Type::EDF)
    {
        newNode->_classification = Classification::EDF | Classification::CLOSURE;
        if (nodeDefName == "ND_mix_edf")
        {
            newNode->_classification |= Classification::MIX;
        }
    }
    else if (primaryOutput->getType() == Type::VDF)
    {
        newNode->_classification = Classification::VDF | Classification::CLOSURE;
        if (nodeDefName == "ND_mix_vdf")
        {
            newNode->_classification |= Classification::MIX;
        }
    }
    // Second, check for specific nodes types
    else if (nodeDef.getNodeString() == CONSTANT)
    {
        newNode->_classification = Classification::TEXTURE | Classification::CONSTANT;
    }
    else if (nodeDef.getNodeString() == DOT)
    {
        newNode->_classification = Classification::TEXTURE | Classification::DOT;
    }
    // Third, check for file texture classification by group name
    else if (groupName == TEXTURE2D_GROUPNAME || groupName == TEXTURE3D_GROUPNAME)
    {
        newNode->_classification = Classification::TEXTURE | Classification::FILETEXTURE;
    }

    // Add in classification based on group name
    if (groupName == TEXTURE2D_GROUPNAME || groupName == PROCEDURAL2D_GROUPNAME)
    {
        newNode->_classification |= Classification::SAMPLE2D;
    }
    else if (groupName == TEXTURE3D_GROUPNAME || groupName == PROCEDURAL3D_GROUPNAME)
    {
        newNode->_classification |= Classification::SAMPLE3D;
    }
    else if (groupName == GEOMETRIC_GROUPNAME)
    {
        newNode->_classification |= Classification::GEOMETRIC;
    }

    // Create any metadata.
    newNode->createMetadata(nodeDef, context);

    return newNode;
}

ShaderNodePtr ShaderNode::create(const ShaderGraph* parent, const string& name, ShaderNodeImplPtr impl, unsigned int classification)
{
    ShaderNodePtr newNode = std::make_shared<ShaderNode>(parent, name);
    newNode->_impl = impl;
    newNode->_classification = classification;
    return newNode;
}

void ShaderNode::initialize(const Node& node, const NodeDef& nodeDef, GenContext& context)
{
    // Copy input values from the given node
    for (InputPtr nodeInput : node.getActiveInputs())
    {
        ShaderInput* input = getInput(nodeInput->getName());
        ValueElementPtr nodeDefInput = nodeDef.getActiveValueElement(nodeInput->getName());
        if (input && nodeDefInput)
        {
            ValuePtr portValue = nodeInput->getResolvedValue();
            if (!portValue)
            {
                InputPtr interfaceInput = nodeInput->getInterfaceInput();
                if (interfaceInput)
                {
                    portValue = interfaceInput->getResolvedValue();
                }
            }
            const string& valueString = portValue ? portValue->getValueString() : EMPTY_STRING;
            if (!valueString.empty())
            {
                // We explicitly check the valueString is not empty before checking the enumeration,
                // because otherwise the enumeration value would always return nullptr
                std::pair<TypeDesc, ValuePtr> enumResult;
                const string& enumNames = nodeDefInput->getAttribute(ValueElement::ENUM_ATTRIBUTE);
                const TypeDesc type = context.getTypeDesc(nodeDefInput->getType());
                if (context.getShaderGenerator().getSyntax().remapEnumeration(valueString, type, enumNames, enumResult))
                {
                    input->setValue(enumResult.second);
                }
                else
                {
                    input->setValue(portValue);
                }
            }
        }
    }

    // Set implementation specific values.
    if (_impl)
    {
        _impl->setValues(node, *this, context);
    }

    // Set element paths for children on the node
    for (const ValueElementPtr& nodeValue : node.getActiveValueElements())
    {
        ShaderInput* input = getInput(nodeValue->getName());
        if (input)
        {
            string path = nodeValue->getNamePath();
            InputPtr nodeInput = nodeValue->asA<Input>();
            if (nodeInput)
            {
                InputPtr interfaceInput = nodeInput->getInterfaceInput();
                if (interfaceInput)
                {
                    path = interfaceInput->getNamePath();
                }
            }
            input->setPath(path);
        }
    }

    // Set element paths based on the node definition. Note that these
    // paths don't actually exist at time of shader generation since there
    // are no inputs specified on the node itself
    //
    const string& nodePath = node.getNamePath();
    for (auto nodeInput : nodeDef.getActiveInputs())
    {
        ShaderInput* input = getInput(nodeInput->getName());
        if (input && input->getPath().empty())
        {
            input->setPath(nodePath + NAME_PATH_SEPARATOR + nodeInput->getName());
        }
    }
}

void ShaderNode::createMetadata(const NodeDef& nodeDef, GenContext& context)
{
    ShaderMetadataRegistryPtr registry = context.getUserData<ShaderMetadataRegistry>(ShaderMetadataRegistry::USER_DATA_NAME);
    if (!(registry && registry->getAllMetadata().size()))
    {
        // Early out if no metadata is registered.
        return;
    }

    // Set metadata on the node according to the nodedef attributes.
    ShaderMetadataVecPtr nodeMetadataStorage = getMetadata();
    for (const string& nodedefAttr : nodeDef.getAttributeNames())
    {
        const ShaderMetadata* metadataEntry = registry->findMetadata(nodedefAttr);
        if (metadataEntry)
        {
            const string& attrValue = nodeDef.getAttribute(nodedefAttr);
            if (!attrValue.empty())
            {
                ValuePtr value = metadataEntry->type.createValueFromStrings(attrValue);
                if (!value)
                {
                    value = metadataEntry->value;
                }
                if (value)
                {
                    if (!nodeMetadataStorage)
                    {
                        nodeMetadataStorage = std::make_shared<ShaderMetadataVec>();
                        setMetadata(nodeMetadataStorage);
                    }
                    nodeMetadataStorage->push_back(ShaderMetadata(metadataEntry->name, metadataEntry->type, value));
                }
            }
        }
    }

    // Set metadata on inputs according to attributes on the nodedef's inputs
    for (const ValueElementPtr& nodedefPort : nodeDef.getActiveValueElements())
    {
        ShaderInput* input = getInput(nodedefPort->getName());
        if (input)
        {
            ShaderMetadataVecPtr inputMetadataStorage = input->getMetadata();

            for (const string& nodedefPortAttr : nodedefPort->getAttributeNames())
            {
                const ShaderMetadata* metadataEntry = registry->findMetadata(nodedefPortAttr);
                if (metadataEntry)
                {
                    const string& attrValue = nodedefPort->getAttribute(nodedefPortAttr);
                    if (!attrValue.empty())
                    {
                        const TypeDesc type = metadataEntry->type != Type::NONE ? metadataEntry->type : input->getType();
                        ValuePtr value = type.createValueFromStrings(attrValue);
                        if (!value)
                        {
                            value = metadataEntry->value;
                        }
                        if (value)
                        {
                            if (!inputMetadataStorage)
                            {
                                inputMetadataStorage = std::make_shared<ShaderMetadataVec>();
                                input->setMetadata(inputMetadataStorage);
                            }
                            inputMetadataStorage->push_back(ShaderMetadata(metadataEntry->name, type, value));
                        }
                    }
                }
            }
        }
    }
}

ShaderInput* ShaderNode::getInput(const string& name)
{
    auto it = _inputMap.find(name);
    return it != _inputMap.end() ? it->second.get() : nullptr;
}

ShaderOutput* ShaderNode::getOutput(const string& name)
{
    auto it = _outputMap.find(name);
    return it != _outputMap.end() ? it->second.get() : nullptr;
}

const ShaderInput* ShaderNode::getInput(const string& name) const
{
    auto it = _inputMap.find(name);
    return it != _inputMap.end() ? it->second.get() : nullptr;
}

const ShaderOutput* ShaderNode::getOutput(const string& name) const
{
    auto it = _outputMap.find(name);
    return it != _outputMap.end() ? it->second.get() : nullptr;
}

ShaderInput* ShaderNode::addInput(const string& name, TypeDesc type)
{
    if (getInput(name))
    {
        throw ExceptionShaderGenError("An input named '" + name + "' already exists on node '" + _name + "'");
    }

    ShaderInputPtr input = std::make_shared<ShaderInput>(this, type, name);
    _inputMap[name] = input;
    _inputOrder.push_back(input.get());

    return input.get();
}

ShaderOutput* ShaderNode::addOutput(const string& name, TypeDesc type)
{
    if (getOutput(name))
    {
        throw ExceptionShaderGenError("An output named '" + name + "' already exists on node '" + _name + "'");
    }

    ShaderOutputPtr output = std::make_shared<ShaderOutput>(this, type, name);
    _outputMap[name] = output;
    _outputOrder.push_back(output.get());

    return output.get();
}

MATERIALX_NAMESPACE_END
