////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

#ifndef REALM_ATOMIC_SHARED_PTR_HPP
#define REALM_ATOMIC_SHARED_PTR_HPP

#include <atomic>
#include <memory>
#include <mutex>
#include <type_traits>

namespace realm {
namespace _impl {

// Check if std::atomic_load has an overload taking a std::shared_ptr, and set
// HasAtomicPtrOps to either true_type or false_type

template <typename, typename = std::void_t<>>
struct HasAtomicPtrOps : std::false_type {};

template <class T>
struct HasAtomicPtrOps<T, std::void_t<decltype(std::atomic_load(std::declval<T*>()))>> : std::true_type {};
} // namespace _impl

namespace util {
// A wrapper for std::shared_ptr that enables sharing a shared_ptr instance
// (and not just a thing *pointed to* by a shared_ptr) between threads. Is
// lock-free iff the underlying shared_ptr implementation supports atomic
// operations. Currently the only implemented operation other than copy/move
// construction/assignment is exchange().
template <typename T, bool = realm::_impl::HasAtomicPtrOps<std::shared_ptr<T>>::value>
class AtomicSharedPtr;

template <typename T>
class AtomicSharedPtr<T, true> {
public:
    AtomicSharedPtr() = default;
    AtomicSharedPtr(std::shared_ptr<T> ptr)
        : m_ptr(std::move(ptr))
    {
    }

    AtomicSharedPtr(AtomicSharedPtr const& ptr)
        : m_ptr(std::atomic_load(&ptr.m_ptr))
    {
    }
    AtomicSharedPtr(AtomicSharedPtr&& ptr)
        : m_ptr(std::atomic_exchange(&ptr.m_ptr, {}))
    {
    }

    AtomicSharedPtr& operator=(AtomicSharedPtr const& ptr)
    {
        if (&ptr != this) {
            std::atomic_store(&m_ptr, std::atomic_load(&ptr.m_ptr));
        }
        return *this;
    }

    AtomicSharedPtr& operator=(AtomicSharedPtr&& ptr)
    {
        std::atomic_store(&m_ptr, std::atomic_exchange(&ptr.m_ptr, {}));
        return *this;
    }

    std::shared_ptr<T> exchange(std::shared_ptr<T> ptr)
    {
        return std::atomic_exchange(&m_ptr, std::move(ptr));
    }

    std::shared_ptr<T> load() const noexcept
    {
        return std::atomic_load(&m_ptr);
    }

private:
    std::shared_ptr<T> m_ptr = nullptr;
};

template <typename T>
class AtomicSharedPtr<T, false> {
public:
    AtomicSharedPtr() = default;
    AtomicSharedPtr(std::shared_ptr<T> ptr)
        : m_ptr(std::move(ptr))
    {
    }

    AtomicSharedPtr(AtomicSharedPtr const& ptr)
    {
        std::lock_guard<std::mutex> lock(ptr.m_mutex);
        m_ptr = ptr.m_ptr;
    }
    AtomicSharedPtr(AtomicSharedPtr&& ptr)
    {
        std::lock_guard<std::mutex> lock(ptr.m_mutex);
        m_ptr = std::move(ptr.m_ptr);
    }

    AtomicSharedPtr& operator=(AtomicSharedPtr const& ptr)
    {
        if (&ptr != this) {
            // std::lock() ensures that these are locked in a consistent order
            // to avoid deadlock
            std::lock(m_mutex, ptr.m_mutex);
            m_ptr = ptr.m_ptr;
            m_mutex.unlock();
            ptr.m_mutex.unlock();
        }
        return *this;
    }

    AtomicSharedPtr& operator=(AtomicSharedPtr&& ptr)
    {
        std::lock(m_mutex, ptr.m_mutex);
        m_ptr = std::move(ptr.m_ptr);
        m_mutex.unlock();
        ptr.m_mutex.unlock();
        return *this;
    }

    std::shared_ptr<T> exchange(std::shared_ptr<T> ptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_ptr.swap(ptr);
        return ptr;
    }

    std::shared_ptr<T> load() const noexcept
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_ptr;
    }

private:
    mutable std::mutex m_mutex;
    std::shared_ptr<T> m_ptr = nullptr;
};

} // namespace util
} // namespace realm

#endif // REALM_ATOMIC_SHARED_PTR_HPP
