// Copyright (c) 2022-2022, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

// clang-format off
#include "UsdPCH.h"
// clang-format on

#include "PrimCommon.h"

#include <OgnReadSettingDatabase.h>

#include <carb/settings/ISettings.h>
#include <carb/settings/SettingsUtils.h>
#include <carb/dictionary/IDictionary.h>

using carb::dictionary::ItemType;

namespace omni
{
namespace graph
{
namespace nodes
{
// unnamed namespace to avoid multiple declaration when linking
namespace
{

template<typename T>
void getSetting(OgnReadSettingDatabase& db)
{
    carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
    std::string strSettingPath = db.inputs.settingPath();
    char const* settingPath = strSettingPath.c_str();

    auto outputValue = db.outputs.value().template get<T>();
    *outputValue = settings->get<T>(settingPath);
}
} // namespace

class OgnReadSetting
{
public:
    // ----------------------------------------------------------------------------
    static void initialize(GraphContextObj const& context, NodeObj const& nodeObj)
    {
        AttributeObj attrObj = nodeObj.iNode->getAttributeByToken(nodeObj, inputs::settingPath.m_token);
        attrObj.iAttribute->registerValueChangedCallback(attrObj, onValueChanged, true);
        onValueChanged(attrObj, nullptr);
    }

    // ----------------------------------------------------------------------------
    // Called by OG to resolve the input type
    static void onConnectionTypeResolve(NodeObj const& nodeObj)
    {
        auto attrObj = nodeObj.iNode->getAttributeByToken(nodeObj, inputs::settingPath.m_token);

        auto graphObj = nodeObj.iNode->getGraph(nodeObj);
        if (graphObj.graphHandle == kInvalidGraphHandle)
            return;

        auto context = graphObj.iGraph->getDefaultGraphContext(graphObj);
        if (context.contextHandle == kInvalidGraphContextHandle)
            return;

        ConstAttributeDataHandle attrDataHandle =
            attrObj.iAttribute->getAttributeDataHandle(attrObj, kAccordingToContextIndex);
        if (!attrDataHandle.isValid())
            return;
        
        Type type(BaseDataType::eUnknown);
        auto stringLen = getElementCount(context, attrDataHandle);
        auto cstrPtrPtr = getDataR<char const*>(context, attrDataHandle);
        if (stringLen && cstrPtrPtr && *cstrPtrPtr) {
            carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
            
            std::string strSettingPath = std::string(*cstrPtrPtr, stringLen);
            char const* settingPath = strSettingPath.c_str();
            
            ItemType pathType;
            if (settings->isAccessibleAsArray(settingPath))
                pathType = settings->getPreferredArrayType(settingPath);
            else
                pathType = settings->getItemType(settingPath);
            if (pathType == ItemType::eBool)
                type.baseType = BaseDataType::eBool;
            else if (pathType == ItemType::eInt)
                type.baseType = BaseDataType::eInt;
            else if (pathType == ItemType::eFloat)
                type.baseType = BaseDataType::eFloat;
            else if (pathType == ItemType::eString)
                type.baseType = BaseDataType::eToken;
            else
                CARB_LOG_ERROR("Invalid Setting Path!");
            
            if (settings->isAccessibleAsArray(settingPath)) {
                size_t arrayLen = (size_t)settings->getArrayLength(settingPath);
                if ((pathType == ItemType::eFloat || pathType == ItemType::eInt) && (arrayLen > 1 && arrayLen <= 4))
                    type.componentCount = (uint8_t)arrayLen;
                else
                    type.arrayDepth = 1;
            }
        }
        
        omni::graph::nodes::tryResolveInputAttribute(nodeObj, outputs::value.m_token, type);
    }

    // ----------------------------------------------------------------------------
    // Called by OG when the value of the settingPath changes 
    static void onValueChanged(AttributeObj const& attrObj, void const* userData)
    {
        auto nodeObj = attrObj.iAttribute->getNode(attrObj);
        if (nodeObj.nodeHandle == kInvalidNodeHandle)
            return;

        onConnectionTypeResolve(nodeObj);
    }
 
    static bool compute(OgnReadSettingDatabase& db)
    {
        carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
        std::string strSettingPath = db.inputs.settingPath();
        char const* settingPath = strSettingPath.c_str();

        auto const valueType = db.outputs.value().type();
        size_t arrayLen = settings->getArrayLength(settingPath);
        switch (valueType.baseType)
        {
        case BaseDataType::eBool:
            switch (valueType.arrayDepth)
            {
            case 0:
                getSetting<bool>(db);
                break;
            case 1:
            {
                auto outputValue = db.outputs.value().template get<bool[]>();
                outputValue->resize(arrayLen);
                settings->getAsBoolArray(settingPath, outputValue->data(), arrayLen);
                break;
            }
            }
            break;
        case BaseDataType::eInt:
            switch (valueType.componentCount)
            {
            case 1:
                switch (valueType.arrayDepth)
                {
                case 0:
                    getSetting<int32_t>(db);
                    break;
                case 1:
                {
                    auto outputValue = db.outputs.value().template get<int32_t[]>();
                    outputValue->resize(arrayLen);
                    settings->getAsIntArray(settingPath, outputValue->data(), arrayLen);
                    break;
                }
                }
                break;
            case 2:
                {
                auto outputValue = db.outputs.value().template get<int32_t[2]>();
                settings->getAsIntArray(settingPath, *outputValue, 2);
                break;
                }
            case 3:
            {
                auto outputValue = db.outputs.value().template get<int32_t[3]>();
                settings->getAsIntArray(settingPath, *outputValue, 3);
                break;
            }
            case 4:
            {
                auto outputValue = db.outputs.value().template get<int32_t[4]>();
                settings->getAsIntArray(settingPath, *outputValue, 4);
                break;
            }
            }
            break;
        case BaseDataType::eInt64:
            switch (valueType.arrayDepth)
            {
            case 0:
                getSetting<int64_t>(db);
                break;
            case 1:
            {
                auto outputValue = db.outputs.value().template get<int64_t[]>();
                outputValue->resize(arrayLen);
                settings->getAsInt64Array(settingPath, outputValue->data(), arrayLen);
                break;
            }
            }
            break;
        case BaseDataType::eFloat:
            switch (valueType.componentCount)
            {
            case 1:
                switch (valueType.arrayDepth)
                {
                case 0:
                    getSetting<float>(db);
                    break;
                case 1:
                {
                    auto outputValue = db.outputs.value().template get<float[]>();
                    outputValue->resize(arrayLen);
                    settings->getAsFloatArray(settingPath, outputValue->data(), arrayLen);
                    break;
                }
                }
                break;
            case 2:
            {
                auto outputValue = db.outputs.value().template get<float[2]>();
                settings->getAsFloatArray(settingPath, *outputValue, 2);
                break;
            }
            case 3:
            {
                auto outputValue = db.outputs.value().template get<float[3]>();
                settings->getAsFloatArray(settingPath, *outputValue, 3);
                break;
            }
            case 4:
            {
                auto outputValue = db.outputs.value().template get<float[4]>();
                settings->getAsFloatArray(settingPath, *outputValue, 4);
                break;
            }
            }
            break;
        case BaseDataType::eDouble:
            switch (valueType.componentCount)
            {
            case 1:
                switch (valueType.arrayDepth)
                {
                case 0:
                    getSetting<double>(db);
                    break;
                case 1:
                {
                    auto outputValue = db.outputs.value().template get<double[]>();
                    outputValue->resize(arrayLen);
                    settings->getAsFloat64Array(settingPath, outputValue->data(), arrayLen);
                    break;
                }
                }
                break;
            case 2:
            {
                auto outputValue = db.outputs.value().template get<double[2]>();
                settings->getAsFloat64Array(settingPath, *outputValue, 2);
                break;
            }
            case 3:
            {
                auto outputValue = db.outputs.value().template get<double[3]>();
                settings->getAsFloat64Array(settingPath, *outputValue, 3);
                break;
            }
            case 4:
            {
                auto outputValue = db.outputs.value().template get<double[4]>();
                settings->getAsFloat64Array(settingPath, *outputValue, 4);
                break;
            }
            }
            break;
        case BaseDataType::eToken:
            switch (valueType.arrayDepth)
            {
            case 0:
            {
                auto outputValue = db.outputs.value().template get<OgnToken>();
                std::string stringSetting = carb::settings::getString(settings, settingPath);
                *outputValue = db.stringToToken(stringSetting.c_str());
                break;
            }
            case 1:
            {
                auto outputValue = db.outputs.value().template get<OgnToken[]>();
                outputValue->resize(arrayLen);
                std::vector<std::string> stringSetting = carb::settings::getStringArray(settings, settingPath);
                for(size_t i = 0; i < arrayLen; i++)
                    (*outputValue)[i] = db.stringToToken(stringSetting[i].c_str());
                break;
            }
            }
            break;
        default:
        {
            db.logError("Type %s not supported", getOgnTypeName(valueType).c_str());
            return false;
        }
        }

        return true;
    }
};

REGISTER_OGN_NODE()
} // namespace nodes
} // namespace graph
} // namespace omni
