//
// Copyright 2023 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// SharedContextMutex.h: Classes for protecting Shared Context access and EGLImage siblings.

#ifndef LIBANGLE_SHARED_CONTEXT_MUTEX_H_
#define LIBANGLE_SHARED_CONTEXT_MUTEX_H_

#include <atomic>

#include "common/debug.h"

namespace gl
{
class Context;
}

namespace egl
{
#if defined(ANGLE_ENABLE_SHARED_CONTEXT_MUTEX)
constexpr bool kIsSharedContextMutexEnabled = true;
#else
constexpr bool kIsSharedContextMutexEnabled = false;
#endif

class ContextMutex : angle::NonCopyable
{
  public:
    // Below group of methods are not thread safe and requires external synchronization.
    // Note: since release*() may call onDestroy(), additional synchronization requirements may be
    // enforced by concrete implementations.
    ANGLE_INLINE void addRef() { ++mRefCount; }
    ANGLE_INLINE void release() { release(UnlockBehaviour::kNotUnlock); }
    ANGLE_INLINE void releaseAndUnlock() { release(UnlockBehaviour::kUnlock); }
    ANGLE_INLINE bool isReferenced() const { return mRefCount > 0; }

    virtual bool try_lock() = 0;
    virtual void lock()     = 0;
    virtual void unlock()   = 0;

  protected:
    virtual ~ContextMutex();

    enum class UnlockBehaviour
    {
        kNotUnlock,
        kUnlock
    };

    void release(UnlockBehaviour unlockBehaviour);

    virtual void onDestroy(UnlockBehaviour unlockBehaviour);

  protected:
    size_t mRefCount = 0;
};

struct ContextMutexMayBeNullTag final
{};
constexpr ContextMutexMayBeNullTag kContextMutexMayBeNull;

// Prevents destruction while locked, uses mMutex to protect addRef()/releaseAndUnlock() calls.
class [[nodiscard]] ScopedContextMutexAddRefLock final : angle::NonCopyable
{
  public:
    ANGLE_INLINE ScopedContextMutexAddRefLock() = default;
    ANGLE_INLINE explicit ScopedContextMutexAddRefLock(ContextMutex *mutex) { lock(mutex); }
    ANGLE_INLINE ScopedContextMutexAddRefLock(ContextMutex *mutex, ContextMutexMayBeNullTag)
    {
        if (mutex != nullptr)
        {
            lock(mutex);
        }
    }
    ANGLE_INLINE ~ScopedContextMutexAddRefLock()
    {
        if (mMutex != nullptr)
        {
            mMutex->releaseAndUnlock();
        }
    }

  private:
    void lock(ContextMutex *mutex);

  private:
    ContextMutex *mMutex = nullptr;
};

class [[nodiscard]] ScopedContextMutexLock final
{
  public:
    ANGLE_INLINE ScopedContextMutexLock() = default;
    ANGLE_INLINE ScopedContextMutexLock(ContextMutex *mutex, gl::Context *context)
        : mMutex(mutex), mContext(context)
    {
        ASSERT(mutex != nullptr);
        ASSERT(context != nullptr);
        mutex->lock();
    }
    ANGLE_INLINE ScopedContextMutexLock(ContextMutex *mutex,
                                        gl::Context *context,
                                        ContextMutexMayBeNullTag)
        : mMutex(mutex), mContext(context)
    {
        if (ANGLE_LIKELY(mutex != nullptr))
        {
            ASSERT(context != nullptr);
            mutex->lock();
        }
    }
    ANGLE_INLINE ~ScopedContextMutexLock()
    {
        if (ANGLE_LIKELY(mMutex != nullptr))
        {
            ASSERT(IsContextMutexStateConsistent(mContext));
            mMutex->unlock();
        }
    }

    ANGLE_INLINE ScopedContextMutexLock(ScopedContextMutexLock &&other)
        : mMutex(other.mMutex), mContext(other.mContext)
    {
        other.mMutex = nullptr;
    }
    ANGLE_INLINE ScopedContextMutexLock &operator=(ScopedContextMutexLock &&other)
    {
        std::swap(mMutex, other.mMutex);
        mContext = other.mContext;
        return *this;
    }

  private:
    static bool IsContextMutexStateConsistent(gl::Context *context);

  private:
    ContextMutex *mMutex  = nullptr;
    gl::Context *mContext = nullptr;
};

// Mutex may be locked only by a single thread. Other threads may only check the status.
class SingleContextMutex final : public ContextMutex
{
  public:
    ANGLE_INLINE bool isLocked(std::memory_order order) const { return mState.load(order) != 0; }

    // ContextMutex
    bool try_lock() override;
    void lock() override;
    void unlock() override;

  private:
    std::atomic_int mState{0};
};

// Note: onDestroy() method must be protected by "this" mutex, since onDestroy() is called from
// release*() methods, these methods must also be protected by "this" mutex.
template <class Mutex>
class SharedContextMutex final : public ContextMutex
{
  public:
    SharedContextMutex();
    ~SharedContextMutex() override;

    // Merges mutexes so they work as one.
    // At the end, only single "root" mutex will be locked.
    // Does nothing if two mutexes are the same or already merged (have same "root" mutex).
    // Note: synchronization requirements for addRef()/release*() calls for merged mutexes are
    // the same as for the single unmerged mutex. For example: can't call at the same time
    // mutexA.addRef() and mutexB.release() if they are merged.
    static void Merge(SharedContextMutex *lockedMutex, SharedContextMutex *otherMutex);

    // Returns current "root" mutex.
    // Warning! Result is only stable if mutex is locked, while may change any time if unlocked.
    // May be used to compare against already locked "root" mutex.
    ANGLE_INLINE SharedContextMutex *getRoot() { return mRoot.load(std::memory_order_relaxed); }

    // ContextMutex
    bool try_lock() override;
    void lock() override;
    void unlock() override;

  private:
    SharedContextMutex *doTryLock();
    SharedContextMutex *doLock();
    void doUnlock();

    // All methods below must be protected by "this" mutex ("stable root" in "this" instance).

    void setNewRoot(SharedContextMutex *newRoot);
    void addLeaf(SharedContextMutex *leaf);
    void removeLeaf(SharedContextMutex *leaf);

    // ContextMutex
    void onDestroy(UnlockBehaviour unlockBehaviour) override;

  private:
    Mutex mMutex;

    // mRoot and mLeaves tree structure details:
    // - used to implement primary functionality of this class;
    // - initially, all mutexes are "root"s;
    // - "root" mutex has "mRoot == this";
    // - "root" mutex stores unreferenced pointers to all its leaves (used in merging);
    // - "leaf" mutex holds reference (addRef) to the current "root" mutex in the mRoot;
    // - "leaf" mutex has empty mLeaves;
    // - "leaf" mutex can't become a "root" mutex;
    // - before locking the mMutex, "this" is an "unstable root" or a "leaf";
    // - the implementation always locks mRoot's mMutex ("unstable root");
    // - if after locking the mMutex "mRoot != this", then "this" is/become a "leaf";
    // - otherwise, "this" is a locked "stable root" - lock is successful.
    std::atomic<SharedContextMutex *> mRoot;
    std::set<SharedContextMutex *> mLeaves;

    // mOldRoots is used to solve a particular problem (below example does not use mRank):
    // - have "leaf" mutex_2 with a reference to mutex_1 "root";
    // - the mutex_1 has no other references (only in the mutex_2);
    // - have other mutex_3 "root";
    // - mutex_1 pointer is cached on the stack during locking of mutex_2 (thread A);
    // - marge mutex_2 and mutex_3 (thread B):
    //     * now "leaf" mutex_2 stores reference to mutex_3 "root";
    //     * old "root" mutex_1 becomes a "leaf" of mutex_3;
    //     * old "root" mutex_1 has no references and gets destroyed.
    // - invalid pointer to destroyed mutex_1 stored on the stack and in the mLeaves of mutex_3;
    // - to fix this problem, references to old "root"s are kept in the mOldRoots vector.
    std::vector<SharedContextMutex *> mOldRoots;

    // mRank is used to fix a problem of indefinite grows of mOldRoots:
    // - merge mutex_1 and mutex_2 -> mutex_2 is "root" of mutex_1 (mOldRoots == 0);
    // - destroy mutex_2;
    // - merge mutex_1 and mutex_3 -> mutex_3 is "root" of mutex_1 (mOldRoots == 1);
    // - destroy mutex_3;
    // - merge mutex_1 and mutex_4 -> mutex_4 is "root" of mutex_1 (mOldRoots == 2);
    // - destroy mutex_4;
    // - continuing this pattern can lead to indefinite grows of mOldRoots, while pick number of
    //   mutexes is only 2.
    // Fix details using mRank:
    // - initially "mRank == 0" and only relevant for "root" mutexes;
    // - merging mutexes with equal mRank of their "root"s, will use second (otherMutex) "root"
    //   mutex as a new "root" and increase its mRank by 1;
    // - otherwise, "root" mutex with a highest rank will be used without changing the mRank;
    // - this way, "stronger" (with a higher mRank) "root" mutex will "protect" its "leaves" from
    //   "mRoot" replacement and therefore - mOldRoots grows.
    // Lets look at the problematic pattern with the mRank:
    // - merge mutex_1 and mutex_2 -> mutex_2 is "root" (mRank == 1) of mutex_1 (mOldRoots == 0);
    // - destroy mutex_2;
    // - merge mutex_1 and mutex_3 -> mutex_2 is "root" (mRank == 1) of mutex_3 (mOldRoots == 0);
    // - destroy mutex_3;
    // - merge mutex_1 and mutex_4 -> mutex_2 is "root" (mRank == 1) of mutex_4 (mOldRoots == 0);
    // - destroy mutex_4;
    // - no mOldRoots grows at all;
    // - minumum number of mutexes to reach mOldRoots size of N => 2^(N+1).
    uint32_t mRank;

    // Only used when ASSERT() is enabled.
    std::atomic<angle::ThreadId> mOwnerThreadId;
};

class ContextMutexManager
{
  public:
    virtual ~ContextMutexManager() = default;

    virtual ContextMutex *create()                                          = 0;
    virtual void merge(ContextMutex *lockedMutex, ContextMutex *otherMutex) = 0;
    virtual ContextMutex *getRootMutex(ContextMutex *mutex)                 = 0;
};

template <class Mutex>
class SharedContextMutexManager final : public ContextMutexManager
{
  public:
    ContextMutex *create() override;
    void merge(ContextMutex *lockedMutex, ContextMutex *otherMutex) override;
    ContextMutex *getRootMutex(ContextMutex *mutex) override;
};

}  // namespace egl

#endif  // LIBANGLE_SHARED_CONTEXT_MUTEX_H_
