/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef DOM_QUOTA_DIRECTORYLOCKIMPL_H_
#define DOM_QUOTA_DIRECTORYLOCKIMPL_H_

#include <cstdint>
#include <functional>

#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/EnumSet.h"
#include "mozilla/MozPromise.h"
#include "mozilla/NotNull.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/ClientStorageScope.h"
#include "mozilla/dom/quota/CommonMetadata.h"
#include "mozilla/dom/quota/DirectoryLockCategory.h"
#include "mozilla/dom/quota/ForwardDecls.h"
#include "mozilla/dom/quota/OriginScope.h"
#include "mozilla/dom/quota/PersistenceScope.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "nsCOMPtr.h"
#include "nsISupportsImpl.h"
#include "nsTArray.h"

class nsITimer;

namespace mozilla::dom::quota {

class ClientDirectoryLockHandle;
struct OriginMetadata;
class QuotaManager;

enum class ShouldUpdateLockIdTableFlag { No, Yes };

// XXX Rename to DirectoryLockBase.
class DirectoryLockImpl {
 public:
  class PrepareInfo;

 private:
  friend class ClientDirectoryLock;
  friend class ClientDirectoryLockHandle;
  friend class OriginDirectoryLock;
  friend class QuotaManager;
  friend class UniversalDirectoryLock;

  const NotNull<RefPtr<QuotaManager>> mQuotaManager;

  const PersistenceScope mPersistenceScope;
  const OriginScope mOriginScope;
  const ClientStorageScope mClientStorageScope;

  MozPromiseHolder<BoolPromise> mAcquirePromiseHolder;
  nsCOMPtr<nsITimer> mAcquireTimer;

  nsTArray<NotNull<DirectoryLockImpl*>> mBlocking;
  nsTArray<NotNull<DirectoryLockImpl*>> mBlockedOn;

  std::function<void()> mInvalidateCallback;

  const int64_t mId;

  const bool mExclusive;

  // Internal quota manager operations use this flag to prevent directory lock
  // registraction/unregistration from updating origin access time, etc.
  const bool mInternal;

  const bool mShouldUpdateLockIdTable;

  const DirectoryLockCategory mCategory;

  bool mRegistered;
  FlippedOnce<true> mPending;
  FlippedOnce<false> mAcquired;
  FlippedOnce<false> mInvalidated;
  FlippedOnce<false> mDropped;

 public:
  DirectoryLockImpl(MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
                    const PersistenceScope& aPersistenceScope,
                    const OriginScope& aOriginScope,
                    const ClientStorageScope& aClientStorageScope,
                    bool aExclusive, bool aInternal,
                    ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag,
                    DirectoryLockCategory aCategory);

  NS_INLINE_DECL_REFCOUNTING(DirectoryLockImpl)

  QuotaManager& MutableManagerRef() const { return *mQuotaManager; }

  int64_t Id() const { return mId; }

  const PersistenceScope& PersistenceScopeRef() const {
    return mPersistenceScope;
  }

  const OriginScope& GetOriginScope() const { return mOriginScope; }

  const ClientStorageScope& ClientStorageScopeRef() const {
    return mClientStorageScope;
  }

  DirectoryLockCategory Category() const { return mCategory; }

  bool Acquired() const { return mAcquired; }

  bool MustWait() const;

  nsTArray<RefPtr<DirectoryLockImpl>> LocksMustWaitFor() const;

  bool Invalidated() const { return mInvalidated; }

  bool Dropped() const { return mDropped; }

  PrepareInfo Prepare() const;

  RefPtr<BoolPromise> Acquire();

  RefPtr<BoolPromise> Acquire(PrepareInfo&& aPrepareInfo);

  void AcquireImmediately();

  void AssertIsAcquiredExclusively()
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  RefPtr<BoolPromise> Drop();

  void OnInvalidate(std::function<void()>&& aCallback);

  void Log() const;

 private:
  virtual ~DirectoryLockImpl();

  void AssertIsOnOwningThread() const
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  PersistenceType GetPersistenceType() const {
    MOZ_DIAGNOSTIC_ASSERT(mPersistenceScope.IsValue());

    return mPersistenceScope.GetValue();
  }

  quota::OriginMetadata OriginMetadata() const {
    MOZ_DIAGNOSTIC_ASSERT(mOriginScope.IsOrigin());

    return quota::OriginMetadata{mOriginScope.GetPrincipalMetadata(),
                                 GetPersistenceType()};
  }

  const nsACString& Origin() const {
    MOZ_DIAGNOSTIC_ASSERT(mOriginScope.IsOrigin());
    MOZ_DIAGNOSTIC_ASSERT(!mOriginScope.GetOrigin().IsEmpty());

    return mOriginScope.GetOrigin();
  }

  Client::Type ClientType() const {
    MOZ_DIAGNOSTIC_ASSERT(mClientStorageScope.IsClient());
    MOZ_DIAGNOSTIC_ASSERT(mClientStorageScope.GetClientType() <
                          Client::TypeMax());

    return mClientStorageScope.GetClientType();
  }

  bool IsInternal() const { return mInternal; }

  void SetRegistered(bool aRegistered) { mRegistered = aRegistered; }

  bool IsPending() const { return mPending; }

  // Ideally, we would have just one table (instead of these two:
  // QuotaManager::mDirectoryLocks and QuotaManager::mDirectoryLockIdTable) for
  // all registered locks. However, some directory locks need to be accessed off
  // the PBackground thread, so the access must be protected by the quota mutex.
  // The problem is that directory locks for eviction must be currently created
  // while the mutex lock is already acquired. So we decided to have two tables
  // for now and to not register directory locks for eviction in
  // QuotaManager::mDirectoryLockIdTable. This can be improved in future after
  // some refactoring of the mutex locking.
  bool ShouldUpdateLockIdTable() const { return mShouldUpdateLockIdTable; }

  bool Overlaps(const DirectoryLockImpl& aLock) const;

  // Test whether this DirectoryLock needs to wait for the given lock.
  bool MustWaitFor(const DirectoryLockImpl& aLock) const;

  void AddBlockingLock(DirectoryLockImpl& aLock) {
    AssertIsOnOwningThread();

    mBlocking.AppendElement(WrapNotNull(&aLock));
  }

  const nsTArray<NotNull<DirectoryLockImpl*>>& GetBlockedOnLocks() {
    return mBlockedOn;
  }

  void AddBlockedOnLock(DirectoryLockImpl& aLock) {
    AssertIsOnOwningThread();

    mBlockedOn.AppendElement(WrapNotNull(&aLock));
  }

  void MaybeUnblock(DirectoryLockImpl& aLock) {
    AssertIsOnOwningThread();

    mBlockedOn.RemoveElement(&aLock);
    if (mBlockedOn.IsEmpty()) {
      NotifyOpenListener();
    }
  }

  void NotifyOpenListener();

  template <typename T>
  nsTArray<T> LocksMustWaitForInternal() const;

  void AcquireInternal(PrepareInfo&& aPrepareInfo);

  void Invalidate();

  void Unregister();
};

class MOZ_RAII DirectoryLockImpl::PrepareInfo {
  friend class DirectoryLockImpl;

  nsTArray<NotNull<DirectoryLockImpl*>> mBlockedOn;

 public:
  // Disable copy constructor and assignment operator
  PrepareInfo(const PrepareInfo&) = delete;
  PrepareInfo& operator=(const PrepareInfo&) = delete;

  // Move constructor and move assignment operator
  PrepareInfo(PrepareInfo&&) noexcept = default;
  PrepareInfo& operator=(PrepareInfo&&) noexcept = default;

  const nsTArray<NotNull<DirectoryLockImpl*>>& BlockedOnRef() const {
    return mBlockedOn;
  }

  /**
   * Returns true if this directory lock would be blocked by any other lock
   * whose category is included in the given set.
   *
   * Used to detect whether an initialization operation should still run, even
   * if the cached state indicates it has already been performed, because an
   * in-progress or pending uninitialization operation will eventually
   * invalidate that state.
   */
  bool IsBlockedBy(const EnumSet<DirectoryLockCategory>& aCategories) const {
    return std::any_of(mBlockedOn.cbegin(), mBlockedOn.cend(),
                       [&aCategories](const auto& lock) {
                         return aCategories.contains(lock->Category());
                       });
  }

 private:
  explicit PrepareInfo(const DirectoryLockImpl& aDirectoryLock)
      : mBlockedOn(
            aDirectoryLock
                .LocksMustWaitForInternal<NotNull<DirectoryLockImpl*>>()) {}
};

}  // namespace mozilla::dom::quota

#endif  // DOM_QUOTA_DIRECTORYLOCKIMPL_H_
