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

#include <MaterialXTest/External/Catch/catch.hpp>

#include <MaterialXCore/Document.h>
#include <MaterialXFormat/File.h>
#include <MaterialXFormat/Util.h>
#include <MaterialXFormat/XmlIo.h>

namespace mx = MaterialX;

TEST_CASE("Document", "[document]")
{
    // Create a document.
    mx::DocumentPtr doc = mx::createDocument();

    // Test version strings.
    REQUIRE(mx::stringStartsWith(mx::getVersionString(), doc->getVersionString()));

    // Test version integers.
    REQUIRE(doc->getVersionIntegers().first == std::get<0>(mx::getVersionIntegers()));
    REQUIRE(doc->getVersionIntegers().second == std::get<1>(mx::getVersionIntegers()));

    // Create a node graph with a constant color output.
    mx::NodeGraphPtr nodeGraph = doc->addNodeGraph();
    mx::NodePtr constant = nodeGraph->addNode("constant");
    constant->setInputValue("value", mx::Color3(0.5f));
    mx::OutputPtr output = nodeGraph->addOutput();
    output->setConnectedNode(constant);
    REQUIRE(output->isColorType());
    REQUIRE(doc->validate());

    // Create and test a type mismatch in a connection.
    output->setType("float");
    REQUIRE(!doc->validate());
    output->setType("color3");
    REQUIRE(doc->validate());

    // Test hierarchical name paths.
    REQUIRE(constant->getNamePath() == "nodegraph1/node1");
    REQUIRE(constant->getNamePath(nodeGraph) == "node1");

    // Test getting elements by path
    REQUIRE(doc->getDescendant("") == doc);
    REQUIRE(doc->getDescendant("nodegraph1") == nodeGraph);
    REQUIRE(doc->getDescendant("nodegraph1/node1") == constant);
    REQUIRE(doc->getDescendant("missingElement") == mx::ElementPtr());
    REQUIRE(doc->getDescendant("nodegraph1/missingNode") == mx::ElementPtr());
    REQUIRE(nodeGraph->getDescendant("") == nodeGraph);
    REQUIRE(nodeGraph->getDescendant("node1") == constant);
    REQUIRE(nodeGraph->getDescendant("missingNode") == mx::ElementPtr());

    // Create a simple shader interface.
    mx::NodeDefPtr simpleSrf = doc->addNodeDef("", mx::SURFACE_SHADER_TYPE_STRING, "simpleSrf");
    simpleSrf->setInputValue("diffColor", mx::Color3(1.0f));
    simpleSrf->setInputValue("specColor", mx::Color3(0.0f));
    mx::InputPtr roughness = simpleSrf->setInputValue("roughness", 0.25f);
    REQUIRE(!roughness->getIsUniform());
    roughness->setIsUniform(true);
    REQUIRE(roughness->getIsUniform());

    // Instantiate shader and material nodes.
    mx::NodePtr shaderNode = doc->addNodeInstance(simpleSrf);
    mx::NodePtr materialNode = doc->addMaterialNode("", shaderNode);
    REQUIRE(materialNode->getUpstreamElement() == shaderNode);

    // Bind the diffuse color input to the constant color output.
    shaderNode->setConnectedOutput("diffColor", output);
    REQUIRE(shaderNode->getUpstreamElement() == constant);

    // Bind the roughness input to a value.
    mx::InputPtr instanceRoughness = shaderNode->setInputValue("roughness", 0.5f);
    REQUIRE(instanceRoughness->getValue()->asA<float>() == 0.5f);
    REQUIRE(instanceRoughness->getDefaultValue()->asA<float>() == 0.25f);

    // Create a collection 
    mx::CollectionPtr collection = doc->addCollection();
    REQUIRE(doc->getCollections().size() == 1);
    REQUIRE(doc->getCollection(collection->getName()));
    doc->removeCollection(collection->getName());
    REQUIRE(doc->getCollections().size() == 0);

    // Create a property set
    mx::PropertySetPtr propertySet = doc->addPropertySet();
    REQUIRE(doc->getPropertySets().size() == 1);
    REQUIRE(doc->getPropertySet(propertySet->getName()) != nullptr);
    doc->removePropertySet(propertySet->getName());
    REQUIRE(doc->getPropertySets().size() == 0);

    // Validate the document.
    REQUIRE(doc->validate());

    // Create a namespaced custom library.
    mx::DocumentPtr customLibrary = mx::createDocument();
    customLibrary->setNamespace("custom");
    mx::NodeGraphPtr customNodeGraph = customLibrary->addNodeGraph("NG_custom");
    mx::NodeDefPtr customNodeDef = customLibrary->addNodeDef("ND_simpleSrf", mx::SURFACE_SHADER_TYPE_STRING, "simpleSrf");
    mx::ImplementationPtr customImpl = customLibrary->addImplementation("IM_custom");
    customNodeGraph->addNodeInstance(customNodeDef, "custom1");
    customImpl->setNodeDef(customNodeDef);
    REQUIRE(customLibrary->validate());

    // Import the custom library.
    mx::DocumentPtr customDatalibrary = mx::createDocument();
    customDatalibrary->importLibrary(customLibrary);

    // Set data library
    doc->setDataLibrary(customDatalibrary);
    mx::NodeGraphPtr importedNodeGraph = doc->getNodeGraph("custom:NG_custom");
    mx::NodeDefPtr importedNodeDef = doc->getNodeDef("custom:ND_simpleSrf");
    mx::ImplementationPtr importedImpl = doc->getImplementation("custom:IM_custom");
    mx::NodePtr importedNode = importedNodeGraph->getNode("custom1");
    REQUIRE(importedNodeDef != nullptr);
    REQUIRE(importedNode->getNodeDef() == importedNodeDef);
    REQUIRE(importedImpl->getNodeDef() == importedNodeDef);

    // Validate the combined document.
    REQUIRE(doc->validate());
}

TEST_CASE("Document equivalence", "[document]")
{
    mx::DocumentPtr doc = mx::createDocument();
    std::unordered_multimap<std::string, std::string> inputMap;

    inputMap.insert({ "color3", "  1.0,   +2.0,  3.0   " });
    inputMap.insert({ "color4", "1.0,   2.00, 0.3000, -4" });
    inputMap.insert({ "integer", "  12 " });
    inputMap.insert({ "matrix33",
                      "01.0,         2.0,  0000.2310, "
                      "   01.0,         2.0,  0000.2310, "
                      "01.0,         2.0,  0000.2310       " });
    inputMap.insert({ "matrix44",
                      "01.0,         2.0,  0000.2310, 0.100, "
                      "01.0,         2.0,  0000.2310, 0.100, "
                      "01.0,         2.0,  0000.2310, 0.100, "
                      "01.0,         2.0,  0000.2310, 0.100" });
    inputMap.insert({ "vector2", "1.0,   0.012345608" }); // For precision check
    inputMap.insert({ "vector3", "  1.0,   +2.0,  3.0   " });
    inputMap.insert({ "vector4", "1.0,   2.00, 0.3000, -4" });
    inputMap.insert({ "string", "mystring" });
    inputMap.insert({ "boolean", "false" });
    inputMap.insert({ "filename", "filename1" });
    inputMap.insert({ "float", "  1.2e-10  " });
    inputMap.insert({ "float", "  00.1000  " });

    unsigned int index = 0;
    mx::ElementPtr child = doc->addNodeGraph("mygraph");
    mx::NodeGraphPtr graph = child->asA<mx::NodeGraph>();
    // Add comment block at the start of the first doc to check skipping
    mx::ElementPtr comment = doc->addChildOfCategory(mx::CommentElement::CATEGORY);
    comment->setDocString("Comment 1");
    for (auto it = inputMap.begin(); it != inputMap.end(); ++it)
    {
        const std::string inputType = (*it).first;
        mx::InputPtr input = graph->addInput("input_" + std::to_string(index), inputType);
        if (inputType == "float")
        {
            input->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, "  0.0100 ");
            input->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, "  01.0100 ");
            index++;
        }
        else
        {
            input->setName("input_" + inputType); // Set by name for difference in order test
        }
        input->setValueString((*it).second);
    }

    mx::DocumentPtr doc2 = mx::createDocument();
    std::unordered_multimap<std::string, std::string> inputMap2;
    inputMap2.insert({ "color4", "1, 2, 0.3, -4" }); 
    inputMap2.insert({ "integer", "12" });
    inputMap2.insert({ "matrix33", "1, 2, 0.231,  1, 2, 0.231,  1, 2, 0.231,  1, 2, 0.231" });
    inputMap2.insert({ "matrix44", "1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1" });
    inputMap2.insert({ "vector2", "1, 0.012345611" }); // For precision check
    inputMap2.insert({ "string", "mystring" });
    inputMap2.insert({ "boolean", "false" });
    inputMap2.insert({ "color3", "1, 2, 3" });
    inputMap2.insert({ "vector3", "1, 2, 3" });
    inputMap2.insert({ "vector4", "1, 2, 0.3, -4" });
    inputMap2.insert({ "filename", "filename1" });
    inputMap2.insert({ "float", "1.2e-10" });
    inputMap2.insert({ "float", "0.1" });

    index = 0;
    child = doc2->addNodeGraph("mygraph");
    graph = child->asA<mx::NodeGraph>();
    std::vector<mx::InputPtr> floatInputs;
    for (auto it = inputMap2.begin(); it != inputMap2.end(); ++it)
    {
        const std::string inputType = (*it).first;
        mx::InputPtr input = graph->addInput("input_" + std::to_string(index), inputType);
        // Note: order of value and ui attributes is different for ordering comparison
        input->setValueString((*it).second);
        if (inputType == "float")
        {
            input->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, "  0.01");
            input->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, "  1.01");
            floatInputs.push_back(input);
            index++;
        }
        else
        {
            input->setName("input_" + inputType);
        }
    }
    // Add comment blocks at end of second doc to check value and count checks
    comment = doc2->addChildOfCategory(mx::CommentElement::CATEGORY);
    comment->setDocString("Comment 2");
    comment = doc2->addChildOfCategory(mx::CommentElement::CATEGORY);
    comment->setDocString("Comment 3");

    mx::ElementEquivalenceOptions options;
    std::string message;

    // Check that this fails when not performing value comparisons
    options.performValueComparisons = false;
    bool equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(!equivalent);

    // Check attribute values 
    options.performValueComparisons = true;
    equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(equivalent);

    unsigned int currentPrecision = mx::Value::getFloatPrecision();
    // This will compare 0.012345608 versus: 1, 0.012345611 for input10
    options.floatPrecision = 8;
    equivalent = doc->isEquivalent(doc2, options);
    REQUIRE(!equivalent);
    options.floatPrecision = currentPrecision;

    // Check attribute filtering of inputs
    options.attributeExclusionList = { mx::ValueElement::UI_MIN_ATTRIBUTE, mx::ValueElement::UI_MAX_ATTRIBUTE };
    for (mx::InputPtr floatInput : floatInputs)
    {
        floatInput->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, "0.9");
        floatInput->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, "100.0");
    }
    equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(equivalent);
    for (mx::InputPtr floatInput : floatInputs)
    {
        floatInput->setAttribute(mx::ValueElement::UI_MIN_ATTRIBUTE, "  0.01");
        floatInput->setAttribute(mx::ValueElement::UI_MAX_ATTRIBUTE, "  1.01");
    }

    // Check for child name mismatch
    mx::ElementPtr mismatchElement = doc->getDescendant("mygraph/input_color4");
    std::string previousName = mismatchElement->getName();
    mismatchElement->setName("mismatch_color4");
    equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(!equivalent);
    mismatchElement->setName(previousName);
    equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(equivalent);

    // Check for functional nodegraphs
    mx::NodeGraphPtr nodeGraph = doc->getNodeGraph("mygraph");
    REQUIRE(nodeGraph);
    doc->addNodeDef("ND_mygraph");
    nodeGraph->setNodeDefString("ND_mygraph");
    mx::NodeGraphPtr nodeGraph2 = doc2->getNodeGraph("mygraph");
    REQUIRE(nodeGraph2);
    doc2->addNodeDef("ND_mygraph");
    nodeGraph2->setNodeDefString("ND_mygraph");
    equivalent = doc->isEquivalent(doc2, options, &message);
    REQUIRE(!equivalent);
}

TEST_CASE("Document createValidChildName", "[document]")
{
    // Create a custom library.
    mx::DocumentPtr customLibrary = mx::createDocument();
    mx::NodeDefPtr customNodeDef = customLibrary->addNodeDef("ND_simpleSrf", mx::SURFACE_SHADER_TYPE_STRING, "simpleSrf");
    mx::ImplementationPtr customImpl = customLibrary->addImplementation("IM_custom");
    customImpl->setNodeDef(customNodeDef);
    REQUIRE(customLibrary->validate());

    mx::DocumentPtr doc;

    // Create a document and import the data library
    doc = mx::createDocument();
    doc->importLibrary(customLibrary);

    // test createValidChildName with imported data library
    {
        // add a non-clashing name
        std::string nodename_01_orig = "non_clashing_name";
        std::string nodename_01 = doc->createValidChildName(nodename_01_orig);
        REQUIRE(nodename_01_orig == nodename_01);

        mx::NodePtr node_01 = doc->addNodeInstance(customNodeDef, nodename_01);
        REQUIRE(doc->validate());
        REQUIRE(node_01->getName() == nodename_01);

        // add a clashing name - which needs to be uniquified.
        std::string nodename_02_orig = "non_clashing_name";
        std::string nodename_02 = doc->createValidChildName(nodename_02_orig);
        REQUIRE(nodename_02_orig != nodename_02);

        // add the same name this should be uniquified
        mx::NodePtr node_02 = doc->addNodeInstance(customNodeDef, nodename_02);
        REQUIRE(doc->validate());
        REQUIRE(node_02->getName() == nodename_02);

        // add a name that clashes with something that is in the data library
        std::string nodename_03_orig = "ND_simpleSrf";
        std::string nodename_03 = doc->createValidChildName(nodename_03_orig);
        REQUIRE(nodename_03_orig != nodename_03);

        mx::NodePtr node_03 = doc->addNodeInstance(customNodeDef, nodename_03);
        REQUIRE(doc->validate());
        REQUIRE(node_03->getName() == nodename_03);
        
        // an empty name should be replaced with a valid placeholder
        REQUIRE(!doc->createValidChildName("").empty());
    }

    // Create a document and reference the data library
    doc = mx::createDocument();
    doc->setDataLibrary(customLibrary);

    // test createValidChildName with referenced data library
    {
        // add a non-clashing name
        std::string nodename_01_orig = "non_clashing_name";
        std::string nodename_01 = doc->createValidChildName(nodename_01_orig);
        REQUIRE(nodename_01_orig == nodename_01);

        mx::NodePtr node_01 = doc->addNodeInstance(customNodeDef, nodename_01);
        REQUIRE(doc->validate());
        REQUIRE(node_01->getName() == nodename_01);

        // add a clashing name - which needs to be uniquified.
        std::string nodename_02_orig = "non_clashing_name";
        std::string nodename_02 = doc->createValidChildName(nodename_02_orig);
        REQUIRE(nodename_02_orig != nodename_02);

        // add the same name this should be uniquified
        mx::NodePtr node_02 = doc->addNodeInstance(customNodeDef, nodename_02);
        REQUIRE(doc->validate());
        REQUIRE(node_02->getName() == nodename_02);

        // add a name that clashes with something that is in the data library
        std::string nodename_03_orig = "ND_simpleSrf";
        std::string nodename_03 = doc->createValidChildName(nodename_03_orig);
        REQUIRE(nodename_03_orig != nodename_03);

        mx::NodePtr node_03 = doc->addNodeInstance(customNodeDef, nodename_03);
        REQUIRE(doc->validate());
        REQUIRE(node_03->getName() == nodename_03);

        // an empty name should be replaced with a valid placeholder
        REQUIRE(!doc->createValidChildName("").empty());
    }
}
