SettingsUtils.h#

Fully qualified name: carb/settings/SettingsUtils.h

File members: carb/settings/SettingsUtils.h

// SPDX-FileCopyrightText: Copyright (c) 2019-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: LicenseRef-NvidiaProprietary
//
// NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
// property and proprietary rights in and to this material, related
// documentation and any modifications thereto. Any use, reproduction,
// disclosure or distribution of this material and related documentation
// without an express license agreement from NVIDIA CORPORATION or
// its affiliates is strictly prohibited.

#pragma once

#include "../../omni/Vector.h"

#include "../dictionary/DictionaryUtils.h"
#include "../dictionary/ISerializer.h"
#include "../cpp/Optional.h"
#include "../extras/StringUtils.h"
#include "../cpp/Span.h"
#include "../cpp/StringViewLike.h"
#include "ISettings.h"

#include <atomic>
#include <mutex>
#include <string>

namespace carb::settings
{

namespace detail
{
#if CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
inline dictionary::ItemType getItemType(const ISettings* settings, cpp::string_view path)
{
    return settings->getItemType(path);
}
inline std::string getString(const ISettings* settings, cpp::string_view path, cpp::string_view defaultValue = {})
{
    ScopedRead lock(settings);
    auto val = settings->getStringView(path);
    return val ? std::string(*val) : std::string(defaultValue);
}
inline cpp::optional<std::string> getStringOpt(const ISettings* settings, cpp::string_view path)
{
    ScopedRead lock(settings);
    if (auto val = settings->getStringView(path))
        return std::string(*val);
    return cpp::nullopt;
}
inline cpp::optional<omni::string> createStringFromItemValue(const ISettings* settings, cpp::string_view path)
{
    return settings->createStringFromItemValue(path);
}
inline cpp::optional<omni::string> createStringFromItemValueAt(const ISettings* settings,
                                                               cpp::string_view path,
                                                               size_t index)
{
    return settings->createStringFromItemValueAt(path, index);
}
inline cpp::optional<cpp::zstring_view> getStringView(const ISettings* settings, cpp::string_view path)
{
    return settings->getStringView(path);
}
inline cpp::optional<cpp::zstring_view> getStringViewAt(const ISettings* settings, cpp::string_view path, size_t index)
{
    return settings->getStringViewAt(path, index);
}
inline bool isAccessibleAs(const ISettings* settings, dictionary::ItemType type, cpp::string_view path)
{
    return settings->isAccessibleAs(type, path);
}
inline size_t getArrayLength(const ISettings* settings, cpp::string_view path)
{
    return settings->getArrayLength(path);
}
inline void setStringArray(const ISettings* settings, cpp::string_view path, const cpp::span<const cpp::zstring_view> array)
{
    settings->setArray(path, array);
}
template <class Container, std::enable_if_t<!std::is_convertible_v<Container, cpp::span<const cpp::zstring_view>>, bool> = false>
void setStringArray(const ISettings* settings, cpp::string_view path, const Container& c)
{
    auto views = CARB_STACK_ALLOC(cpp::zstring_view, c.size());
    for (size_t i = 0; i != c.size(); ++i)
        views[i] = cpp::zstring_view(c[i]);
    setStringArray(settings, path, cpp::span<cpp::zstring_view>(views, c.size()));
}
inline const dictionary::Item* getSettingsDictionary(const ISettings* settings, cpp::string_view path)
{
    return settings->getSettingsDictionary(path);
}
template <class T>
cpp::optional<T> get(const ISettings* settings, cpp::string_view path)
{
    return settings->getOpt<T>(path);
}
template <class T, std::enable_if_t<!cpp::is_std_string_view_like_v<T>, bool> = false>
void set(const ISettings* settings, cpp::string_view path, T value)
{
    settings->set(path, value);
}
inline void set(const ISettings* settings, cpp::string_view path, cpp::string_view value)
{
    settings->set(path, value);
}
template <class T, std::enable_if_t<!cpp::is_std_string_view_like_v<T>, bool> = false>
void setDefault(const ISettings* settings, cpp::string_view path, T value)
{
    settings->setDefault(path, value);
}
inline void setDefault(const ISettings* settings, cpp::string_view path, cpp::string_view value)
{
    settings->setDefault(path, value);
}
template <class T, std::enable_if_t<!cpp::is_carb_zstring_view_like_v<T>, bool> = false>
void setDefaultArray(const ISettings* settings, cpp::string_view path, const cpp::span<const T> array)
{
    settings->setDefaultArray(path, array);
}
inline void setDefaultArray(const ISettings* settings, cpp::string_view path, const cpp::span<const cpp::zstring_view> array)
{
    settings->setDefaultArray(path, array);
}
inline void update(const ISettings* settings,
                   cpp::string_view path,
                   const dictionary::Item* dictionary,
                   cpp::string_view dictionaryPath,
                   dictionary::OnUpdateItemFn onUpdateItemFn,
                   void* userData)
{
    settings->update(path, dictionary, dictionaryPath, onUpdateItemFn, userData);
}
inline dictionary::SubscriptionId* subscribeToNodeChangeEvents(const ISettings* settings,
                                                               cpp::string_view path,
                                                               dictionary::OnNodeChangeEventFn onChangeEventFn,
                                                               void* userData)
{
    return settings->subscribeToNodeChangeEvents(path, onChangeEventFn, userData);
}
inline void unsubscribeFromChangeEvents(const ISettings* settings, dictionary::SubscriptionId* sub)
{
    settings->unsubscribeFromChangeEvents(sub);
}
inline void destroyItem(const ISettings* settings, cpp::string_view path)
{
    settings->destroyItem(path);
}
#else
inline dictionary::ItemType getItemType(const ISettings* settings, cpp::string_view path)
{
    return settings->getItemTypeS(path);
}
inline std::string getString(const ISettings* settings, cpp::string_view path, cpp::string_view defaultValue)
{
    ScopedRead lock(settings);
    auto val = settings->getStringBufferS(path);
    return val ? std::string(*val) : std::string(defaultValue);
}
inline cpp::optional<std::string> getStringOpt(const ISettings* settings, cpp::string_view path)
{
    ScopedRead lock(settings);
    if (auto val = settings->getStringBufferS(path))
        return std::string(*val);
    return cpp::nullopt;
}
inline cpp::optional<omni::string> createStringFromItemValue(const ISettings* settings, cpp::string_view path)
{
    cpp::optional<omni::string> ret;
    if (auto buf = settings->createStringBufferFromItemValueS(path))
    {
        ret.emplace(buf);
        settings->destroyStringBuffer(buf);
    }
    return ret;
}
inline cpp::optional<omni::string> createStringFromItemValueAt(const ISettings* settings,
                                                               cpp::string_view path,
                                                               size_t index)
{
    cpp::optional<omni::string> ret;
    if (auto buf = settings->createStringBufferFromItemValueAtS(path, index))
    {
        ret.emplace(buf);
        settings->destroyStringBuffer(buf);
    }
    return ret;
}
inline cpp::optional<cpp::zstring_view> getStringView(const ISettings* settings, cpp::string_view path)
{
    return settings->getStringBufferS(path);
}
inline cpp::optional<cpp::zstring_view> getStringViewAt(const ISettings* settings, cpp::string_view path, size_t index)
{
    return settings->getStringBufferAtS(path, index);
}
inline bool isAccessibleAs(const ISettings* settings, dictionary::ItemType type, cpp::string_view path)
{
    return settings->isAccessibleAsS(type, path);
}
inline size_t getArrayLength(const ISettings* settings, cpp::string_view path)
{
    return settings->getArrayLengthS(path);
}
inline void setStringArray(const ISettings* settings, cpp::string_view path, const cpp::span<const cpp::zstring_view> array)
{
    auto mem = CARB_STACK_ALLOC(const char*, array.size());
    for (size_t i = 0; i != array.size(); ++i)
        mem[i] = array[i].c_str();
    settings->setStringArrayS(path, mem, array.size());
}
template <class Container, std::enable_if_t<!std::is_convertible_v<Container, cpp::span<const cpp::zstring_view>>, bool> = false>
void setStringArray(const ISettings* settings, cpp::string_view path, const Container& c)
{
    auto views = CARB_STACK_ALLOC(const char*, c.size());
    for (size_t i = 0; i != c.size(); ++i)
        views[i] = cpp::zstring_view(c[i]).c_str();
    settings->setStringArrayS(path, views, c.size());
}
inline const dictionary::Item* getSettingsDictionary(const ISettings* settings, cpp::string_view path)
{
    return settings->getSettingsDictionaryS(path);
}
template <class T>
cpp::optional<T> get(const ISettings* settings, cpp::string_view path)
{
    if constexpr (std::is_same_v<T, cpp::zstring_view>)
        return settings->getS<cpp::optional<T>>(path);
    else if constexpr (cpp::is_unbounded_character_sequence_v<T, char>)
    {
        auto opt = settings->getS<cpp::optional<cpp::zstring_view>>(path);
        return opt ? opt->c_str() : nullptr;
    }
    else
    {
        ScopedRead lock(settings);
        if (settings->getItemTypeS(path) == dictionary::ItemType::eCount)
            return cpp::nullopt;
        return settings->getS<T>(path);
    }
}
template <class T, std::enable_if_t<!cpp::is_std_string_view_like_v<T>, bool> = false>
void set(const ISettings* settings, cpp::string_view path, T value)
{
    settings->setS(path, value);
}
inline void set(const ISettings* settings, cpp::string_view path, cpp::string_view value)
{
    settings->setS(path, value);
}
template <class T, std::enable_if_t<!cpp::is_std_string_view_like_v<T>, bool> = false>
void setDefault(const ISettings* settings, cpp::string_view path, T value)
{
    settings->setDefaultS(path, value);
}
inline void setDefault(const ISettings* settings, cpp::string_view path, cpp::string_view value)
{
    settings->setDefaultS(path, value);
}
template <class T, std::enable_if_t<!cpp::is_carb_zstring_view_like_v<T>, bool> = false>
void setDefaultArray(const ISettings* settings, cpp::string_view path, const cpp::span<const T> array)
{
    settings->setDefaultArrayS(path, array.data(), array.size());
}
inline void setDefaultArray(const ISettings* settings, cpp::string_view path, const cpp::span<const cpp::zstring_view> array)
{
    auto vals = CARB_STACK_ALLOC(const char*, array.size());
    for (size_t i = 0; i != array.size(); ++i)
        vals[i] = array[i].c_str();
    settings->setDefaultArrayS(path, vals, array.size());
}
inline void update(const ISettings* settings,
                   cpp::string_view path,
                   const dictionary::Item* dictionary,
                   cpp::string_view dictionaryPath,
                   dictionary::OnUpdateItemFn onUpdateItemFn,
                   void* userData)
{
    settings->updateS(path, dictionary, dictionaryPath, onUpdateItemFn, userData);
}
inline dictionary::SubscriptionId* subscribeToNodeChangeEvents(const ISettings* settings,
                                                               cpp::string_view path,
                                                               dictionary::OnNodeChangeEventFn onChangeEventFn,
                                                               void* userData)
{
    return settings->subscribeToNodeChangeEventsS(path, onChangeEventFn, userData);
}
inline void unsubscribeFromChangeEvents(const ISettings* settings, dictionary::SubscriptionId* sub)
{
    settings->unsubscribeToChangeEvents(sub);
}
inline void destroyItem(const ISettings* settings, cpp::string_view path)
{
    settings->destroyItemS(path);
}
#endif

template <class Container>
Container getStringArray(const ISettings* settings, cpp::string_view path, cpp::string_view defaultValue = {})
{
    ScopedRead readLock(settings);
    Container array(getArrayLength(settings, path));
    using value_type = typename Container::value_type;
    const size_t arraySize = array.size();
    for (size_t i = 0; i != arraySize; ++i)
    {
        auto val = getStringViewAt(settings, path, i);
        array[i] = val ? value_type(*val) : value_type(defaultValue);
    }
    return array;
}
template <typename ElementData, typename OnItemFnTypeS>
inline void walkSettings(dictionary::IDictionary* idict,
                         settings::ISettings* settings,
                         dictionary::WalkerMode walkerMode,
                         cpp::string_view rootPath,
                         ElementData rootElementData,
                         OnItemFnTypeS onItemFnS)
{
    using namespace carb;

    if (rootPath.empty())
        rootPath = "/";

    struct ValueToParse
    {
        std::string srcPath;
        ElementData elementData;
    };

    std::vector<ValueToParse> valuesToParse;
    valuesToParse.reserve(10);

    auto enqueueChildren = [&idict, &settings, &valuesToParse](
                               cpp::string_view parentPath, ElementData parentElementData) {
        const dictionary::Item* parentItem = getSettingsDictionary(settings, parentPath);
        size_t numChildren = idict->getItemChildCount(parentItem);
        for (size_t chIdx = 0; chIdx < numChildren; ++chIdx)
        {
            const dictionary::Item* childItem = idict->getItemChildByIndex(parentItem, numChildren - chIdx - 1);
            auto childItemName = dictionary::detail::getItemName(idict, childItem).value_or("");
            std::string childPath;
            bool isRootParent = (idict->getItemParent(parentItem) == nullptr);
            if (isRootParent)
            {
                childPath = extras::join(parentPath, childItemName);
            }
            else
            {
                childPath = extras::join(parentPath, "/", childItemName);
            }
            valuesToParse.push_back({ childPath, parentElementData });
        }
    };

    // lock settings to ensure consistency and allow us to safely access the settings dictionary
    ScopedRead readLock(settings, idict);

    if (walkerMode == dictionary::WalkerMode::eSkipRoot)
    {
        enqueueChildren(rootPath, rootElementData);
    }
    else
    {
        valuesToParse.push_back({ std::string{ rootPath }, rootElementData });
    }

    while (valuesToParse.size())
    {
        ValueToParse& valueToParse = valuesToParse.back(); // this is not a const reference because we move the value
                                                           // out of the vector
        std::string curItemPath = std::move(valueToParse.srcPath);
        ElementData elementData = std::move(valueToParse.elementData);
        valuesToParse.pop_back();

        dictionary::ItemType curItemType = getItemType(settings, curItemPath);
        [[maybe_unused]] ElementData parentElementData = onItemFnS(curItemPath, elementData);

        if (curItemType == dictionary::ItemType::eDictionary)
            enqueueChildren(curItemPath, parentElementData);
    }
}
} // namespace detail

#if CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
inline omni::string getStringFromItemValue(const ISettings* settings,
                                           cpp::string_view path,
                                           cpp::string_view defaultValue = {})
{
    auto val = settings->createStringFromItemValue(path);
    return val ? *val : omni::string(defaultValue);
}

inline omni::string getStringFromItemValueAt(const ISettings* settings,
                                             cpp::string_view path,
                                             size_t index,
                                             cpp::string_view defaultValue = {})
{
    auto val = settings->createStringFromItemValueAt(path, index);
    return val ? *val : omni::string(defaultValue);
}

inline omni::string getString(const ISettings* settings, cpp::string_view path, cpp::string_view defaultValue = {})
{
    ScopedRead lock(settings);
    auto val = settings->getStringView(path);
    return val ? omni::string(*val) : omni::string(defaultValue);
}

inline cpp::optional<omni::string> getStringOpt(const ISettings* settings, cpp::string_view path)
{
    ScopedRead lock(settings);
    if (auto val = settings->getStringView(path))
        return omni::string(*val);
    return cpp::nullopt;
}

inline omni::string getStringAt(const ISettings* settings,
                                cpp::string_view path,
                                size_t index,
                                cpp::string_view defaultValue = {})
{
    ScopedRead lock(settings);
    auto value = settings->getStringViewAt(path, index);
    return value ? omni::string(*value) : omni::string(defaultValue);
}

inline cpp::optional<omni::string> getStringAtOpt(const ISettings* settings, cpp::string_view path, size_t index)
{
    ScopedRead lock(settings);
    if (auto value = settings->getStringViewAt(path, index))
        return omni::string(*value);
    return cpp::nullopt;
}

inline std::vector<std::string> getStringArray(ISettings* settings,
                                               cpp::string_view path,
                                               cpp::string_view defaultValue = {})
{
    return detail::getStringArray<std::vector<std::string>>(settings, path, defaultValue);
}

inline std::vector<std::string> getStringArrayFromItemValues(const ISettings* settings,
                                                             cpp::string_view path,
                                                             cpp::string_view defaultValue = {})
{
    ScopedRead readLock(settings);
    std::vector<std::string> array(settings->getArrayLength(path));
    const size_t arraySize = array.size();
    for (size_t i = 0; i < arraySize; ++i)
    {
        auto val = settings->createStringFromItemValueAt(path, i);
        array[i] = val ? std::string(*val) : std::string(defaultValue);
    }
    return array;
}

inline void setStringArray(ISettings* settings, cpp::string_view path, const std::vector<std::string>& array)
{
    const size_t arraySize = array.size();
    auto mem = CARB_STACK_ALLOC(cpp::string_view, arraySize);
    for (size_t i = 0; i != arraySize; ++i)
        mem[i] = array[i];
    settings->setStringArray(path, { mem, arraySize });
}

inline void loadSettingsFromFile(ISettings* settings,
                                 cpp::string_view path,
                                 dictionary::IDictionary* dictionary,
                                 dictionary::ISerializer* serializer,
                                 cpp::zstring_view filename)
{
    dictionary::Item* settingsFromFile = dictionary::createDictionaryFromFileS(serializer, filename);
    if (settingsFromFile)
    {
        CARB_SCOPE_EXIT
        {
            dictionary->destroyItem(settingsFromFile);
        };
        settings->update(path, settingsFromFile, {}, dictionary::overwriteOriginalWithArrayHandling, dictionary);
    }
}

inline void saveFileFromSettings(const ISettings* settings,
                                 dictionary::ISerializer* serializer,
                                 cpp::string_view path,
                                 cpp::zstring_view filename,
                                 dictionary::SerializerOptions serializerOptions)
{
    ScopedRead lock;
    auto settingsDictionaryAtPath = detail::getSettingsDictionary(settings, path);
    dictionary::saveFileFromDictionaryS(serializer, settingsDictionaryAtPath, filename, serializerOptions);
}

template <typename ElementData, typename OnItemFnTypeS>
inline void walkSettings(dictionary::IDictionary* idict,
                         settings::ISettings* settings,
                         dictionary::WalkerMode walkerMode,
                         cpp::string_view rootPath,
                         ElementData rootElementData,
                         OnItemFnTypeS onItemFnS)
{
    detail::walkSettings(idict, settings, walkerMode, rootPath, std::move(rootElementData), std::move(onItemFnS));
}
#else
inline std::string getStringFromItemValueS(const ISettings* settings,
                                           cpp::string_view path,
                                           cpp::string_view defaultValue = "")
{
    if (auto val = detail::createStringFromItemValue(settings, path))
        return std::string((cpp::string_view)*val);
    return std::string(defaultValue);
}
inline std::string getStringFromItemValue(const ISettings* settings, const char* path, const std::string& defaultValue = "")
{
    if (auto val = detail::createStringFromItemValue(settings, cpp::string_view(cpp::unsafe_length, path)))
        return std::string((cpp::string_view)*val);
    return std::string(defaultValue);
}
inline std::string getStringFromItemValueAtS(const ISettings* settings,
                                             cpp::string_view path,
                                             size_t index,
                                             cpp::string_view defaultValue = "")
{
    if (auto val = detail::createStringFromItemValueAt(settings, path, index))
        return std::string((cpp::string_view)*val);
    return std::string(defaultValue);
}
inline std::string getStringFromItemValueAt(const ISettings* settings,
                                            const char* path,
                                            size_t index,
                                            const std::string& defaultValue = "")
{
    if (auto val = detail::createStringFromItemValueAt(settings, cpp::string_view(cpp::unsafe_length, path), index))
        return std::string((cpp::string_view)*val);
    return std::string(defaultValue);
}
inline std::string getStringS(const ISettings* settings, cpp::string_view path, cpp::string_view defaultValue = "")
{
    return detail::getString(settings, path, defaultValue);
}
inline std::string getString(const ISettings* settings, const char* path, const std::string& defaultValue = "")
{
    return detail::getString(settings, cpp::string_view(cpp::unsafe_length, path), defaultValue);
}
inline cpp::optional<std::string> getStringOptS(const ISettings* settings, cpp::string_view path)
{
    return detail::getStringOpt(settings, path);
}
inline cpp::optional<std::string> getStringOpt(const ISettings* settings, const char* path)
{
    return detail::getStringOpt(settings, cpp::string_view(cpp::unsafe_length, path));
}
inline std::string getStringAtS(const ISettings* settings,
                                cpp::string_view path,
                                size_t index,
                                cpp::string_view defaultValue = "")
{
    ScopedRead lock(settings);
    if (auto value = detail::getStringViewAt(settings, path, index))
    {
        return std::string{ *value };
    }
    return std::string{ defaultValue };
}
inline std::string getStringAt(const ISettings* settings,
                               const char* path,
                               size_t index,
                               const std::string& defaultValue = "")
{
    ScopedRead lock(settings);
    if (auto value = detail::getStringViewAt(settings, cpp::string_view(cpp::unsafe_length, path), index))
    {
        return std::string{ *value };
    }
    return std::string{ defaultValue };
}
inline cpp::optional<std::string> getStringAtOptS(const ISettings* settings, cpp::string_view path, size_t index)
{
    ScopedRead lock(settings);
    if (auto value = detail::getStringViewAt(settings, path, index))
    {
        return std::string{ *value };
    }
    return cpp::nullopt;
}
inline cpp::optional<std::string> getStringAtOpt(const ISettings* settings, const char* path, size_t index)
{
    ScopedRead lock(settings);
    if (auto value = detail::getStringViewAt(settings, cpp::string_view(cpp::unsafe_length, path), index))
    {
        return std::string{ *value };
    }
    return cpp::nullopt;
}
inline void setIntArrayS(ISettings* settings, cpp::string_view path, const std::vector<int>& array)
{
    settings->setIntArrayS(path, array.data(), array.size());
}
inline void setIntArray(ISettings* settings, const char* path, const std::vector<int>& array)
{
    settings->setIntArray(path, array.data(), array.size());
}
inline void setIntArrayS(ISettings* settings, cpp::string_view path, const std::vector<int64_t>& array)
{
    settings->setInt64ArrayS(path, array.data(), array.size());
}
inline void setIntArray(ISettings* settings, const char* path, const std::vector<int64_t>& array)
{
    settings->setInt64Array(path, array.data(), array.size());
}
inline void setFloatArrayS(ISettings* settings, cpp::string_view path, const std::vector<float>& array)
{
    settings->setFloatArrayS(path, array.data(), array.size());
}
inline void setFloatArray(ISettings* settings, const char* path, const std::vector<float>& array)
{
    settings->setFloatArray(path, array.data(), array.size());
}
inline void setFloatArrayS(ISettings* settings, cpp::string_view path, const std::vector<double>& array)
{
    settings->setFloat64ArrayS(path, array.data(), array.size());
}
inline void setFloatArray(ISettings* settings, const char* path, const std::vector<double>& array)
{
    settings->setFloat64Array(path, array.data(), array.size());
}
inline void setBoolArrayS(ISettings* settings, cpp::string_view path, const std::vector<bool>& array)
{
    const size_t arraySize = array.size();
    // Since std::vector<bool> is typically specialized and doesn't function like normal vector (i.e. no data()), first
    // convert to an array of bools on the stack.
    bool* pbools = CARB_STACK_ALLOC(bool, arraySize);
    for (size_t i = 0; i != arraySize; ++i)
        pbools[i] = array[i];
    settings->setBoolArrayS(path, pbools, arraySize);
}
inline void setBoolArray(ISettings* settings, const char* path, const std::vector<bool>& array)
{
    const size_t arraySize = array.size();
    // Since std::vector<bool> is typically specialized and doesn't function like normal vector (i.e. no data()), first
    // convert to an array of bools on the stack.
    bool* pbools = CARB_STACK_ALLOC(bool, arraySize);
    for (size_t i = 0; i != arraySize; ++i)
        pbools[i] = array[i];
    settings->setBoolArray(path, pbools, arraySize);
}
inline std::vector<std::string> getStringArrayS(ISettings* settings,
                                                cpp::string_view path,
                                                cpp::string_view defaultValue = {})
{
    return detail::getStringArray<std::vector<std::string>>(settings, path, defaultValue);
}
inline std::vector<std::string> getStringArray(ISettings* settings,
                                               const char* path,
                                               const cpp::string_view defaultValue = {})
{
    return detail::getStringArray<std::vector<std::string>>(
        settings, cpp::string_view(cpp::unsafe_length, path), defaultValue);
}
inline std::vector<std::string> getStringArrayFromItemValuesS(ISettings* settings,
                                                              cpp::string_view path,
                                                              cpp::string_view defaultValue = "")
{
    ScopedRead readLock(settings);
    std::vector<std::string> array(detail::getArrayLength(settings, path));
    for (size_t i = 0, arraySize = array.size(); i < arraySize; ++i)
    {
        array[i] = getStringFromItemValueAtS(settings, path, i, defaultValue);
    }
    return array;
}
inline std::vector<std::string> getStringArrayFromItemValues(ISettings* settings,
                                                             const char* path,
                                                             const std::string& defaultValue = "")
{
    dictionary::ScopedRead readLock(*getCachedInterface<dictionary::IDictionary>(), settings->getSettingsDictionary(""));
    std::vector<std::string> array(detail::getArrayLength(settings, cpp::string_view(cpp::unsafe_length, path)));
    for (size_t i = 0, arraySize = array.size(); i < arraySize; ++i)
    {
        array[i] = getStringFromItemValueAt(settings, path, i, defaultValue);
    }
    return array;
}
inline void setStringArrayS(ISettings* settings, cpp::string_view path, const std::vector<std::string>& array)
{
    const size_t arraySize = array.size();
    auto mem = CARB_STACK_ALLOC(cpp::zstring_view, arraySize);
    for (size_t i = 0; i != arraySize; ++i)
        mem[i] = array[i];
    detail::setStringArray(settings, path, cpp::span<cpp::zstring_view>(mem, arraySize));
}
inline void setStringArray(ISettings* settings, const char* path, const std::vector<std::string>& array)
{
    const size_t arraySize = array.size();
    auto mem = CARB_STACK_ALLOC(cpp::zstring_view, arraySize);
    for (size_t i = 0; i != arraySize; ++i)
        mem[i] = array[i];
    detail::setStringArray(
        settings, cpp::string_view(cpp::unsafe_length, path), cpp::span<cpp::zstring_view>(mem, arraySize));
}
inline void loadSettingsFromFileS(ISettings* settings,
                                  cpp::string_view path,
                                  dictionary::IDictionary* dictionary,
                                  dictionary::ISerializer* serializer,
                                  cpp::zstring_view filename)
{
    dictionary::Item* settingsFromFile = dictionary::createDictionaryFromFileS(serializer, filename);
    if (settingsFromFile)
    {
        settings->updateS(path, settingsFromFile, {}, dictionary::overwriteOriginalWithArrayHandling, dictionary);
        dictionary->destroyItem(settingsFromFile);
    }
}
inline void loadSettingsFromFile(ISettings* settings,
                                 const char* path,
                                 dictionary::IDictionary* dictionary,
                                 dictionary::ISerializer* serializer,
                                 const char* filename)
{
    dictionary::Item* settingsFromFile = dictionary::createDictionaryFromFile(serializer, filename);
    if (settingsFromFile)
    {
        settings->update(path, settingsFromFile, {}, dictionary::overwriteOriginalWithArrayHandling, dictionary);
        dictionary->destroyItem(settingsFromFile);
    }
}
inline void saveFileFromSettingsS(const ISettings* settings,
                                  dictionary::ISerializer* serializer,
                                  cpp::string_view path,
                                  cpp::zstring_view filename,
                                  dictionary::SerializerOptions serializerOptions)
{
    ScopedRead lock;
    auto settingsDictionaryAtPath = detail::getSettingsDictionary(settings, path);
    dictionary::saveFileFromDictionaryS(serializer, settingsDictionaryAtPath, filename, serializerOptions);
}
inline void saveFileFromSettings(const ISettings* settings,
                                 dictionary::ISerializer* serializer,
                                 const char* path,
                                 const char* filename,
                                 dictionary::SerializerOptions serializerOptions)
{
    ScopedRead lock;
    auto settingsDictionaryAtPath = detail::getSettingsDictionary(settings, cpp::string_view(cpp::unsafe_length, path));
    dictionary::saveFileFromDictionary(serializer, settingsDictionaryAtPath, filename, serializerOptions);
}
template <typename ElementData, typename OnItemFnTypeS>
inline void walkSettingsS(dictionary::IDictionary* idict,
                          settings::ISettings* settings,
                          dictionary::WalkerMode walkerMode,
                          cpp::string_view rootPath,
                          ElementData rootElementData,
                          OnItemFnTypeS onItemFnS,
                          void* userData)
{
    detail::walkSettings(idict, settings, walkerMode, rootPath, std::move(rootElementData),
                         [&](const std::string& str, const ElementData& ed) { return onItemFnS(str, ed, userData); });
}
template <typename ElementData, typename OnItemFnType>
inline void walkSettings(dictionary::IDictionary* idict,
                         settings::ISettings* settings,
                         dictionary::WalkerMode walkerMode,
                         const char* rootPath,
                         ElementData rootElementData,
                         OnItemFnType onItemFn,
                         void* userData)
{
    if (!rootPath)
        return;
    detail::walkSettings(
        idict, settings, walkerMode, cpp::string_view(cpp::unsafe_length, rootPath), std::move(rootElementData),
        [&](cpp::zstring_view str, const ElementData& ed) { return onItemFn(str.c_str(), ed, userData); });
}
#endif

template <typename SettingType>
class ThreadSafeLocalCache
{
public:
    ThreadSafeLocalCache(SettingType initState = SettingType{}) : m_value(initState), m_valueDirty(false)
    {
    }

    ~ThreadSafeLocalCache()
    {
        stopTracking();
    }

    void startTracking(cpp::string_view settingPath)
    {
        CARB_ASSERT(m_subscription == nullptr,
                    "Already tracking this value, do not track again without calling stopTracking first.");

        Framework* f = getFramework();
        m_settings = f->tryAcquireInterface<settings::ISettings>();
        m_dictionary = f->tryAcquireInterface<dictionary::IDictionary>();
        if (!m_settings || !m_dictionary)
            return;

        m_valueSettingsPath = settingPath;
        m_value.store(
            detail::get<SettingType>(m_settings, settingPath).value_or(SettingType{}), std::memory_order_relaxed);
        m_valueDirty.store(false, std::memory_order_release);

        m_subscription = detail::subscribeToNodeChangeEvents(
            m_settings, settingPath,
            [](const dictionary::Item* changedItem, dictionary::ChangeEventType changeEventType, void* userData) {
                if (changeEventType == dictionary::ChangeEventType::eChanged)
                {
                    ThreadSafeLocalCache* thisClassInstance = reinterpret_cast<ThreadSafeLocalCache*>(userData);
                    thisClassInstance->m_value.store(
                        thisClassInstance->getDictionaryInterface()->template get<SettingType>(changedItem),
                        std::memory_order_relaxed);
                    thisClassInstance->m_valueDirty.store(true, std::memory_order_release);
                }
            },
            this);
        if (m_subscription != nullptr)
        {
            f->addReleaseHook(m_settings, sOnRelease, this);
        }
    }

#if !CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
    void startTracking(const char* settingPath)
    {
        CARB_ASSERT(settingPath, "Must specify a valid setting name.");
        startTracking(cpp::string_view(cpp::unsafe_length, settingPath));
    }
#endif

    void stopTracking()
    {
        if (m_subscription)
        {
            getFramework()->removeReleaseHook(m_settings, sOnRelease, this);
            detail::unsubscribeFromChangeEvents(m_settings, std::exchange(m_subscription, nullptr));
        }
    }

    SettingType get() const
    {
        CARB_ASSERT(m_subscription, "Call startTracking before reading this variable.");
        return m_value.load(std::memory_order_relaxed);
    }

    operator SettingType() const
    {
        return get();
    }

    void set(SettingType value)
    {
        CARB_ASSERT(m_subscription);
        if (!m_valueSettingsPath.empty())
            m_settings->set<SettingType>(m_valueSettingsPath.c_str(), value);
    }

    bool isValueDirty() const
    {
        return m_valueDirty.load(std::memory_order_relaxed);
    }

    void clearValueDirty()
    {
        m_valueDirty.store(false, std::memory_order_release);
    }

    const char* getSettingsPath() const
    {
        return m_valueSettingsPath.c_str();
    }

    cpp::zstring_view getSettingsPathS() const
    {
        return m_valueSettingsPath;
    }

    inline dictionary::IDictionary* getDictionaryInterface() const
    {
        return m_dictionary;
    }

private:
    static void sOnRelease(void* iface, void* user)
    {
        // Settings has gone away, so our subscription is defunct
        static_cast<ThreadSafeLocalCache*>(user)->m_subscription = nullptr;
        getFramework()->removeReleaseHook(iface, sOnRelease, user);
    }

    // NOTE: The callback may come in on another thread so wrap it in an atomic to prevent a race.
    std::atomic<SettingType> m_value;
    std::atomic<bool> m_valueDirty;
    std::string m_valueSettingsPath;
    dictionary::SubscriptionId* m_subscription = nullptr;
    dictionary::IDictionary* m_dictionary = nullptr;
    settings::ISettings* m_settings = nullptr;
};

#ifndef DOXYGEN_BUILD
#    if CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
template <>
class ThreadSafeLocalCache<std::string>
{
public:
    ThreadSafeLocalCache(cpp::string_view initState = {}) : m_valueDirty(false)
    {
        m_value = initState;
    }
    ~ThreadSafeLocalCache()
    {
        stopTracking();
    }

    void startTracking(cpp::string_view settingPath)
    {
        CARB_ASSERT(m_subscription == nullptr,
                    "Already tracking this value, do not track again without calling stopTracking first.");

        m_settings = getCachedInterface<settings::ISettings>();
        m_dictionary = getCachedInterface<dictionary::IDictionary>();

        m_valueSettingsPath = settingPath;
        m_value = detail::get<cpp::zstring_view>(m_settings, settingPath).value_or("");
        m_valueDirty.store(false, std::memory_order_release);

        m_subscription = m_settings->subscribeToNodeChangeEvents(
            settingPath,
            [](const dictionary::Item* changedItem, dictionary::ChangeEventType changeEventType, void* userData) {
                if (changeEventType == dictionary::ChangeEventType::eChanged)
                {
                    ThreadSafeLocalCache* thisClassInstance = reinterpret_cast<ThreadSafeLocalCache*>(userData);
                    {
                        auto valueStringBuffer =
                            thisClassInstance->getDictionaryInterface()->template get<std::string>(changedItem);
                        std::lock_guard<std::mutex> guard(thisClassInstance->m_valueMutex);
                        thisClassInstance->m_value = std::move(valueStringBuffer);
                    }
                    thisClassInstance->m_valueDirty.store(true, std::memory_order_release);
                }
            },
            this);
        if (m_subscription)
        {
            getFramework()->addReleaseHook(m_settings, sOnRelease, this);
        }
    }

    void stopTracking()
    {
        if (m_subscription)
        {
            detail::unsubscribeFromChangeEvents(m_settings, std::exchange(m_subscription, nullptr));
            getFramework()->removeReleaseHook(m_settings, sOnRelease, this);
        }
    }

    std::string get() const
    {
        std::lock_guard guard(m_valueMutex);
        return m_value;
    }

    operator std::string() const
    {
        return get();
    }

    std::string getStringSafe() const
    {
        // Not a safe operation
        CARB_ASSERT(m_subscription, "Call startTracking before reading this variable.");
        std::lock_guard guard(m_valueMutex);
        return m_value;
    }

    void set(cpp::string_view value)
    {
        m_settings->set(m_valueSettingsPath, value);
    }

    bool isValueDirty() const
    {
        return m_valueDirty.load(std::memory_order_relaxed);
    }
    void clearValueDirty()
    {
        m_valueDirty.store(false, std::memory_order_release);
    }

    cpp::zstring_view getSettingsPath() const
    {
        return m_valueSettingsPath;
    }

    inline dictionary::IDictionary* getDictionaryInterface() const
    {
        return m_dictionary;
    }

private:
    static void sOnRelease(void* iface, void* user)
    {
        // Settings has gone away, so our subscription is defunct
        static_cast<ThreadSafeLocalCache*>(user)->m_subscription = nullptr;
        getFramework()->removeReleaseHook(iface, sOnRelease, user);
    }

    // NOTE: The callback may come in on another thread so wrap it in a mutex to prevent a race.
    std::string m_value;
    mutable std::mutex m_valueMutex;
    std::atomic<bool> m_valueDirty;
    std::string m_valueSettingsPath;
    dictionary::SubscriptionId* m_subscription = nullptr;
    dictionary::IDictionary* m_dictionary = nullptr;
    settings::ISettings* m_settings = nullptr;
};
#    else
template <>
class ThreadSafeLocalCache<const char*>
{
public:
    ThreadSafeLocalCache(const char* initState = "") : m_valueDirty(false)
    {
        std::lock_guard guard(m_valueMutex);
        m_value = initState;
    }
    ~ThreadSafeLocalCache()
    {
        stopTracking();
    }

    void startTracking(cpp::string_view settingPath)
    {
        CARB_ASSERT(m_subscription == nullptr,
                    "Already tracking this value, do not track again without calling stopTracking first.");

        m_settings = getCachedInterface<settings::ISettings>();
        m_dictionary = getCachedInterface<dictionary::IDictionary>();

        m_valueSettingsPath = settingPath;
        m_value = detail::get<cpp::zstring_view>(m_settings, settingPath).value_or("");
        m_valueDirty.store(false, std::memory_order_release);

        m_subscription = detail::subscribeToNodeChangeEvents(
            m_settings, settingPath,
            [](const dictionary::Item* changedItem, dictionary::ChangeEventType changeEventType, void* userData) {
                if (changeEventType == dictionary::ChangeEventType::eChanged)
                {
                    ThreadSafeLocalCache* thisClassInstance = reinterpret_cast<ThreadSafeLocalCache*>(userData);
                    {
                        auto valueStringBuffer =
                            thisClassInstance->getDictionaryInterface()->template get<std::string>(changedItem);
                        std::lock_guard guard(thisClassInstance->m_valueMutex);
                        thisClassInstance->m_value = std::move(valueStringBuffer);
                    }
                    thisClassInstance->m_valueDirty.store(true, std::memory_order_release);
                }
            },
            this);
        if (m_subscription)
        {
            getFramework()->addReleaseHook(m_settings, sOnRelease, this);
        }
    }

    void startTracking(const char* settingPath)
    {
        CARB_ASSERT(settingPath, "Must specify a valid setting name.");
        startTracking(cpp::string_view(cpp::unsafe_length, settingPath));
    }

    void stopTracking()
    {
        if (m_subscription)
        {
            detail::unsubscribeFromChangeEvents(m_settings, std::exchange(m_subscription, nullptr));
            getFramework()->removeReleaseHook(m_settings, sOnRelease, this);
        }
    }

    const char* get() const
    {
        // Not a safe operation
        CARB_ASSERT(false);
        CARB_LOG_ERROR("Shouldn't use unsafe get on a ThreadSafeLocalCache<const char*>");
        return "";
    }

    operator const char*() const
    {
        // Not a safe operation
        return get();
    }

    std::string getStringSafe() const
    {
        // Not a safe operation
        CARB_ASSERT(m_subscription, "Call startTracking before reading this variable.");
        std::lock_guard guard(m_valueMutex);
        return m_value;
    }

    void set(const char* value)
    {
#        if CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
        m_settings->set(m_valueSettingsPath, cpp::string_view(cpp::unsafe_length, value));
#        else
        m_settings->setStringS(m_valueSettingsPath, cpp::string_view(cpp::unsafe_length, value));
#        endif
    }

    bool isValueDirty() const
    {
        return m_valueDirty.load(std::memory_order_relaxed);
    }
    void clearValueDirty()
    {
        m_valueDirty.store(false, std::memory_order_release);
    }

    const char* getSettingsPath() const
    {
        return m_valueSettingsPath.c_str();
    }

    cpp::zstring_view getSettingsPathS() const
    {
        return m_valueSettingsPath;
    }

    inline dictionary::IDictionary* getDictionaryInterface() const
    {
        return m_dictionary;
    }

private:
    static void sOnRelease(void* iface, void* user)
    {
        // Settings has gone away, so our subscription is defunct
        static_cast<ThreadSafeLocalCache*>(user)->m_subscription = nullptr;
        getFramework()->removeReleaseHook(iface, sOnRelease, user);
    }

    // NOTE: The callback may come in on another thread so wrap it in a mutex to prevent a race.
    std::string m_value;
    mutable std::mutex m_valueMutex;
    std::atomic<bool> m_valueDirty;
    std::string m_valueSettingsPath;
    dictionary::SubscriptionId* m_subscription = nullptr;
    dictionary::IDictionary* m_dictionary = nullptr;
    settings::ISettings* m_settings = nullptr;
};
#    endif
#endif

class ScopedSubscription
{
public:
    ScopedSubscription() = default;

    ScopedSubscription(cpp::string_view path, dictionary::OnNodeChangeEventFn fn, void* userData)
    {
        _getSettingsInterface();

        if (m_settings != nullptr)
        {
            m_sub = detail::subscribeToNodeChangeEvents(m_settings, path, fn, userData);
        }
    }

    ScopedSubscription(cpp::string_view path, dictionary::OnTreeChangeEventFn fn, void* userData)
    {
        _getSettingsInterface();

        if (m_settings != nullptr)
        {
#if CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
            m_sub = m_settings->subscribeToTreeChangeEvents(path, fn, userData);
#else
            m_sub = m_settings->subscribeToTreeChangeEventsS(path, fn, userData);
#endif
        }
    }

#if !CARB_VERSION_ATLEAST(carb_settings_ISettings, 2, 0)
    ScopedSubscription(const char* path, dictionary::OnNodeChangeEventFn fn, void* userData)
        : ScopedSubscription(cpp::string_view(cpp::unsafe_length, path), fn, userData)
    {
    }

    ScopedSubscription(const char* path, dictionary::OnTreeChangeEventFn fn, void* userData)
        : ScopedSubscription(cpp::string_view(cpp::unsafe_length, path), fn, userData)
    {
    }
#endif

    ScopedSubscription(ScopedSubscription&& other)
    {
        *this = std::move(other);
    }

    ~ScopedSubscription()
    {
        // remove the release hook since we no longer need it.  Note that if the release hook
        // had fired, it will already have been removed and this will be a no-op.
        getFramework()->removeReleaseHook(m_settings, sOnReleaseSettings, this);
        _clearSubscription();
    }

    CARB_PREVENT_COPY(ScopedSubscription);

    ScopedSubscription& operator=(ScopedSubscription&& other)
    {
        if (&other == this)
            return *this;

        _clearSubscription();
        _getSettingsInterface();
        m_sub = std::exchange(other.m_sub, nullptr);
        return *this;
    }

    explicit operator bool() const noexcept
    {
        return m_sub != nullptr;
    }

private:
    static void sOnReleaseSettings([[maybe_unused]] void* iface, void* userData)
    {
        ScopedSubscription* self = reinterpret_cast<ScopedSubscription*>(userData);

        if (self == nullptr)
            return;

        getFramework()->removeReleaseHook(self->m_settings, sOnReleaseSettings, self);
        self->m_settings = nullptr;
        self->m_sub = nullptr;
    }

    void _getSettingsInterface()
    {
        if (m_settings != nullptr)
            return;

        m_settings = getCachedInterface<settings::ISettings>();
        getFramework()->addReleaseHook(m_settings, sOnReleaseSettings, this);
    }

    void _clearSubscription()
    {
        if (m_sub != nullptr && m_settings != nullptr)
        {
            detail::unsubscribeFromChangeEvents(m_settings, std::exchange(m_sub, nullptr));
        }
    }

    settings::ISettings* m_settings = nullptr;

    dictionary::SubscriptionId* m_sub = nullptr;
};

} // namespace carb::settings