// Copyright (c) 2019-2021, 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.
//
#pragma once

#include <carb/Framework.h>
#include <carb/Interface.h>
#include <carb/flatcache/IToken.h>
#include <carb/flatcache/Intrinsics.h>

#include <functional>

// Set to empty macro when IPath::iPath static member is removed
#define FLATCACHE_IPATH_INIT \
const carb::flatcache::IPath* carb::flatcache::Path::iPath = nullptr;

namespace carb
{
namespace flatcache
{
// PathC are integer keys that identify paths to C-ABI interfaces
struct PathC 
{
    uint64_t path;

    constexpr bool operator<(const PathC& other) const
    {
        return path < other.path;
    }
    constexpr bool operator==(const PathC& other) const
    {
        return path == other.path;
    }
    constexpr bool operator!=(const PathC& other) const
    {
        return path != other.path;
    }
};

static_assert(std::is_standard_layout<PathC>::value, "Struct must be standard layout as it is used in C-ABI interfaces");

// We don't reference count the uninitialized (or empty) path, and we use
// this fact to avoid unnecessary dll calls to addRef()/removeRef(), for
// example during std::vector resize. To do this we need to check whether a
// path is uninitialized without the dll call getEmptyPath(), so we store
// its value here in a constant.
// We run automated test "IPath::getEmptyPath() dll call can be replaced with
// constant, Path::kUninitializedPath" to ensure that this constant never
// changes.
static constexpr PathC kUninitializedPath{0};

// C-ABI interface to pxr::SdfPath
struct IPath
{
    CARB_PLUGIN_INTERFACE("carb::flatcache::IPath", 0, 1);
    PathC (*getHandle)(const char* name);
    const char* (*getText)(PathC handle);
    PathC (*getParent)(PathC handle);
    PathC (*appendChild)(PathC handle, TokenC childName);
    void (*addRef)(PathC handle);
    void (*removeRef)(PathC handle);
    PathC (*getEmptyPath)();

    // Creates a path by appending a given relative path to this path.
    PathC (*appendPath)(PathC handle, PathC path);

    // Returns the number of path elements in this path.
    uint32_t (*getPathElementCount)(PathC handle);
};

// C++ wrapper for IPath
class Path
{
    static carb::flatcache::IPath& sIPath();
public:
    // DEPRECATED: keeping for binary compatibility
    // Will be removed in October 2021 - @TODO set FLATCACHE_IPATH_INIT to empty macro when removed!
    // Still safe to use if initialized in a given dll
    static const carb::flatcache::IPath* iPath;

    Path() : mHandle(kUninitializedPath)
    {
    }

    Path(const char* path)
    {
        mHandle = sIPath().getHandle(path);
    }

    // Needs to be noexcept for std::vector::resize() to move instead of copy
    ~Path() noexcept
    {
        // We see the compiler construct and destruct many uninitialized
        // temporaries, for example when resizing std::vector.
        // We don't want to do an IPath dll call for these, so skip if handle
        // is uninitialized.
        if (mHandle != kUninitializedPath)
        {
            sIPath().removeRef(mHandle);
        }
    }

    // Copy constructor
    Path(const Path& other) : mHandle(other.mHandle)
    {
        if (mHandle != kUninitializedPath)
        {
            sIPath().addRef(mHandle);
        }
    }

    // Copy construct from integer
    Path(PathC handle) : mHandle(handle)
    {
        if (mHandle != kUninitializedPath)
        {
            sIPath().addRef(mHandle);
        }
    }

    // Move constructor
    // Needs to be noexcept for std::vector::resize() to move instead of copy
    Path(Path&& other) noexcept
    {
        // We are moving the src handle so don't need to change its refcount
        mHandle = other.mHandle;

        // Make source invalid
        other.mHandle = kUninitializedPath;
    }

    // Copy assignment
    Path& operator=(const Path& other)
    {
        if (this != &other)
        {
            if (mHandle != kUninitializedPath)
            {
                sIPath().removeRef(mHandle);
            }

            if (other.mHandle != kUninitializedPath)
            {
                sIPath().addRef(other.mHandle);
            }
        }
        mHandle = other.mHandle;

        return *this;
    }

    // Move assignment
    Path& operator=(Path&& other) noexcept
    {
        if (&other == this)
            return *this;

        // We are about to overwrite the dest handle, so decrease its refcount
        if (mHandle != kUninitializedPath)
        {
            sIPath().removeRef(mHandle);
        }
        // We are moving the src handle so don't need to change its refcount

        mHandle = other.mHandle;
        other.mHandle = kUninitializedPath;

        return *this;
    }

    const char* getText() const
    {
        return sIPath().getText(mHandle);
    }

    constexpr bool operator<(const Path& other) const
    {
        return mHandle < other.mHandle;
    }
    constexpr bool operator!=(const Path& other) const
    {
        return mHandle != other.mHandle;
    }
    constexpr bool operator==(const Path& other) const
    {
        return mHandle == other.mHandle;
    }

    constexpr operator PathC() const
    {
        return mHandle;
    }

private:
    PathC mHandle;
};

static_assert(std::is_standard_layout<Path>::value, "Path must be standard layout as it is used in C-ABI interfaces");

#ifndef __CUDACC__
inline carb::flatcache::IPath& Path::sIPath()
{
    // Acquire carbonite interface on first use
    carb::flatcache::IPath* iPath = carb::getCachedInterface<carb::flatcache::IPath>();
    CARB_ASSERT(iPath);
    return *iPath;
}
#endif // __CUDACC__

}
}
namespace std
{
template <>
class hash<carb::flatcache::PathC>
{
public:
    inline size_t operator()(const carb::flatcache::PathC& key) const
    {
        // lower 8 bits have no entropy, so just remove the useless bits
        return key.path >> 8;
    }
};
template <>
class hash<carb::flatcache::Path>
{
public:
    inline size_t operator()(const carb::flatcache::Path& key) const
    {
        return std::hash<carb::flatcache::PathC>()(carb::flatcache::PathC(key));
    }
};
}
