/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */

#include "mozilla/dom/cache/Context.h"

#include "CacheCommon.h"

#include "mozilla/AutoRestore.h"
#include "mozilla/dom/SafeRefPtr.h"
#include "mozilla/dom/cache/Action.h"
#include "mozilla/dom/cache/FileUtils.h"
#include "mozilla/dom/cache/Manager.h"
#include "mozilla/dom/cache/ManagerId.h"
#include "mozilla/dom/quota/Assertions.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozIStorageConnection.h"
#include "nsIPrincipal.h"
#include "nsIRunnable.h"
#include "nsIThread.h"
#include "nsThreadUtils.h"

namespace {

using mozilla::dom::cache::Action;
using mozilla::dom::cache::CacheDirectoryMetadata;

class NullAction final : public Action {
 public:
  NullAction() = default;

  virtual void RunOnTarget(mozilla::SafeRefPtr<Resolver> aResolver,
                           const mozilla::Maybe<CacheDirectoryMetadata>&,
                           Data*) override {
    // Resolve success immediately.  This Action does no actual work.
    MOZ_DIAGNOSTIC_ASSERT(aResolver);
    aResolver->Resolve(NS_OK);
  }
};

}  // namespace

namespace mozilla::dom::cache {

using mozilla::dom::quota::AssertIsOnIOThread;
using mozilla::dom::quota::DirectoryLock;
using mozilla::dom::quota::OpenDirectoryListener;
using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
using mozilla::dom::quota::PersistenceType;
using mozilla::dom::quota::QuotaManager;

class Context::Data final : public Action::Data {
 public:
  explicit Data(nsISerialEventTarget* aTarget) : mTarget(aTarget) {
    MOZ_DIAGNOSTIC_ASSERT(mTarget);
  }

  virtual mozIStorageConnection* GetConnection() const override {
    MOZ_ASSERT(mTarget->IsOnCurrentThread());
    return mConnection;
  }

  virtual void SetConnection(mozIStorageConnection* aConn) override {
    MOZ_ASSERT(mTarget->IsOnCurrentThread());
    MOZ_DIAGNOSTIC_ASSERT(!mConnection);
    mConnection = aConn;
    MOZ_DIAGNOSTIC_ASSERT(mConnection);
  }

 private:
  ~Data() {
    // We could proxy release our data here, but instead just assert.  The
    // Context code should guarantee that we are destroyed on the target
    // thread once the connection is initialized.  If we're not, then
    // QuotaManager might race and try to clear the origin out from under us.
    MOZ_ASSERT_IF(mConnection, mTarget->IsOnCurrentThread());
  }

  nsCOMPtr<nsISerialEventTarget> mTarget;
  nsCOMPtr<mozIStorageConnection> mConnection;

  // Threadsafe counting because we're created on the PBackground thread
  // and destroyed on the target IO thread.
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Context::Data)
};

// Executed to perform the complicated dance of steps necessary to initialize
// the QuotaManager.  This must be performed for each origin before any disk
// IO occurrs.
class Context::QuotaInitRunnable final : public nsIRunnable,
                                         public OpenDirectoryListener {
 public:
  QuotaInitRunnable(SafeRefPtr<Context> aContext, SafeRefPtr<Manager> aManager,
                    Data* aData, nsISerialEventTarget* aTarget,
                    SafeRefPtr<Action> aInitAction)
      : mContext(std::move(aContext)),
        mThreadsafeHandle(mContext->CreateThreadsafeHandle()),
        mManager(std::move(aManager)),
        mData(aData),
        mTarget(aTarget),
        mInitAction(std::move(aInitAction)),
        mInitiatingEventTarget(GetCurrentSerialEventTarget()),
        mResult(NS_OK),
        mState(STATE_INIT),
        mCanceled(false) {
    MOZ_DIAGNOSTIC_ASSERT(mContext);
    MOZ_DIAGNOSTIC_ASSERT(mManager);
    MOZ_DIAGNOSTIC_ASSERT(mData);
    MOZ_DIAGNOSTIC_ASSERT(mTarget);
    MOZ_DIAGNOSTIC_ASSERT(mInitiatingEventTarget);
    MOZ_DIAGNOSTIC_ASSERT(mInitAction);
  }

  Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
    NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);

    return ToMaybeRef(mDirectoryLock.get());
  }

  nsresult Dispatch() {
    NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_INIT);

    mState = STATE_GET_INFO;
    nsresult rv = NS_DispatchToMainThread(this, nsIThread::DISPATCH_NORMAL);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      mState = STATE_COMPLETE;
      Clear();
    }
    return rv;
  }

  void Cancel() {
    NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
    MOZ_DIAGNOSTIC_ASSERT(!mCanceled);
    mCanceled = true;
    mInitAction->CancelOnInitiatingThread();
  }

  // OpenDirectoryListener methods
  virtual void DirectoryLockAcquired(DirectoryLock* aLock) override;

  virtual void DirectoryLockFailed() override;

 private:
  class SyncResolver final : public Action::Resolver {
   public:
    SyncResolver() : mResolved(false), mResult(NS_OK) {}

    virtual void Resolve(nsresult aRv) override {
      MOZ_DIAGNOSTIC_ASSERT(!mResolved);
      mResolved = true;
      mResult = aRv;
    };

    bool Resolved() const { return mResolved; }
    nsresult Result() const { return mResult; }

   private:
    ~SyncResolver() = default;

    bool mResolved;
    nsresult mResult;

    NS_INLINE_DECL_REFCOUNTING(Context::QuotaInitRunnable::SyncResolver,
                               override)
  };

  ~QuotaInitRunnable() {
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_COMPLETE);
    MOZ_DIAGNOSTIC_ASSERT(!mContext);
    MOZ_DIAGNOSTIC_ASSERT(!mInitAction);
  }

  enum State {
    STATE_INIT,
    STATE_GET_INFO,
    STATE_CREATE_QUOTA_MANAGER,
    STATE_WAIT_FOR_DIRECTORY_LOCK,
    STATE_ENSURE_ORIGIN_INITIALIZED,
    STATE_RUN_ON_TARGET,
    STATE_RUNNING,
    STATE_COMPLETING,
    STATE_COMPLETE
  };

  void Complete(nsresult aResult) {
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_RUNNING || NS_FAILED(aResult));

    MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(mResult));
    mResult = aResult;

    mState = STATE_COMPLETING;
    MOZ_ALWAYS_SUCCEEDS(
        mInitiatingEventTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));
  }

  void Clear() {
    NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
    MOZ_DIAGNOSTIC_ASSERT(mContext);
    mContext = nullptr;
    mManager = nullptr;
    mInitAction = nullptr;
  }

  SafeRefPtr<Context> mContext;
  SafeRefPtr<ThreadsafeHandle> mThreadsafeHandle;
  SafeRefPtr<Manager> mManager;
  RefPtr<Data> mData;
  nsCOMPtr<nsISerialEventTarget> mTarget;
  SafeRefPtr<Action> mInitAction;
  nsCOMPtr<nsIEventTarget> mInitiatingEventTarget;
  nsresult mResult;
  Maybe<mozilla::ipc::PrincipalInfo> mPrincipalInfo;
  Maybe<CacheDirectoryMetadata> mDirectoryMetadata;
  RefPtr<DirectoryLock> mDirectoryLock;
  State mState;
  Atomic<bool> mCanceled;

 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIRUNNABLE
};

void Context::QuotaInitRunnable::DirectoryLockAcquired(DirectoryLock* aLock) {
  NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
  MOZ_DIAGNOSTIC_ASSERT(aLock);
  MOZ_DIAGNOSTIC_ASSERT(mState == STATE_WAIT_FOR_DIRECTORY_LOCK);
  MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);

  mDirectoryLock = aLock;

  MOZ_DIAGNOSTIC_ASSERT(mDirectoryLock->Id() >= 0);
  mDirectoryMetadata->mDirectoryLockId = mDirectoryLock->Id();

  if (mCanceled) {
    Complete(NS_ERROR_ABORT);
    return;
  }

  QuotaManager* qm = QuotaManager::Get();
  MOZ_DIAGNOSTIC_ASSERT(qm);

  mState = STATE_ENSURE_ORIGIN_INITIALIZED;
  nsresult rv = qm->IOThread()->Dispatch(this, nsIThread::DISPATCH_NORMAL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Complete(rv);
    return;
  }
}

void Context::QuotaInitRunnable::DirectoryLockFailed() {
  NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
  MOZ_DIAGNOSTIC_ASSERT(mState == STATE_WAIT_FOR_DIRECTORY_LOCK);
  MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);

  NS_WARNING("Failed to acquire a directory lock!");

  Complete(NS_ERROR_FAILURE);
}

NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::QuotaInitRunnable, nsIRunnable);

// The QuotaManager init state machine is represented in the following diagram:
//
//    +---------------+
//    |     Start     |      Resolve(error)
//    | (Orig Thread) +---------------------+
//    +-------+-------+                     |
//            |                             |
// +----------v-----------+                 |
// |       GetInfo        |  Resolve(error) |
// |    (Main Thread)     +-----------------+
// +----------+-----------+                 |
//            |                             |
// +----------v-----------+                 |
// |  CreateQuotaManager  |  Resolve(error) |
// |    (Orig Thread)     +-----------------+
// +----------+-----------+                 |
//            |                             |
// +----------v-----------+                 |
// | WaitForDirectoryLock |  Resolve(error) |
// |    (Orig Thread)     +-----------------+
// +----------+-----------+                 |
//            |                             |
// +----------v------------+                |
// |EnsureOriginInitialized| Resolve(error) |
// |   (Quota IO Thread)   +----------------+
// +----------+------------+                |
//            |                             |
// +----------v------------+                |
// |     RunOnTarget       | Resolve(error) |
// |   (Target Thread)     +----------------+
// +----------+------------+                |
//            |                             |
//  +---------v---------+            +------v------+
//  |      Running      |            |  Completing |
//  | (Target Thread)   +------------>(Orig Thread)|
//  +-------------------+            +------+------+
//                                          |
//                                    +-----v----+
//                                    | Complete |
//                                    +----------+
//
// The initialization process proceeds through the main states.  If an error
// occurs, then we transition to Completing state back on the original thread.
NS_IMETHODIMP
Context::QuotaInitRunnable::Run() {
  // May run on different threads depending on the state.  See individual
  // state cases for thread assertions.

  SafeRefPtr<SyncResolver> resolver = MakeSafeRefPtr<SyncResolver>();

  switch (mState) {
    // -----------------------------------
    case STATE_GET_INFO: {
      MOZ_ASSERT(NS_IsMainThread());

      auto res = [this]() -> Result<Ok, nsresult> {
        if (mCanceled) {
          return Err(NS_ERROR_ABORT);
        }

        nsCOMPtr<nsIPrincipal> principal = mManager->GetManagerId().Principal();

        mozilla::ipc::PrincipalInfo principalInfo;
        QM_TRY(
            MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));

        mPrincipalInfo.emplace(std::move(principalInfo));

        mState = STATE_CREATE_QUOTA_MANAGER;

        MOZ_ALWAYS_SUCCEEDS(
            mInitiatingEventTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));

        return Ok{};
      }();

      if (res.isErr()) {
        resolver->Resolve(res.inspectErr());
      }

      break;
    }
    // ----------------------------------
    case STATE_CREATE_QUOTA_MANAGER: {
      NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);

      if (mCanceled || QuotaManager::IsShuttingDown()) {
        resolver->Resolve(NS_ERROR_ABORT);
        break;
      }

      QM_TRY(QuotaManager::EnsureCreated(), QM_PROPAGATE,
             [&resolver](const auto rv) { resolver->Resolve(rv); });

      auto* const quotaManager = QuotaManager::Get();
      MOZ_DIAGNOSTIC_ASSERT(quotaManager);

      QM_TRY_UNWRAP(
          auto principalMetadata,
          quotaManager->GetInfoFromValidatedPrincipalInfo(*mPrincipalInfo));

      mDirectoryMetadata.emplace(std::move(principalMetadata));

      // Open directory
      RefPtr<DirectoryLock> directoryLock = quotaManager->CreateDirectoryLock(
          PERSISTENCE_TYPE_DEFAULT, *mDirectoryMetadata,
          quota::Client::DOMCACHE,
          /* aExclusive */ false);

      // DirectoryLock::Acquire() will hold a reference to us as a listener. We
      // will then get DirectoryLockAcquired() on the owning thread when it is
      // safe to access our storage directory.
      mState = STATE_WAIT_FOR_DIRECTORY_LOCK;
      directoryLock->Acquire(this);

      break;
    }
    // ----------------------------------
    case STATE_ENSURE_ORIGIN_INITIALIZED: {
      AssertIsOnIOThread();

      auto res = [this]() -> Result<Ok, nsresult> {
        if (mCanceled) {
          return Err(NS_ERROR_ABORT);
        }

        QuotaManager* quotaManager = QuotaManager::Get();
        MOZ_DIAGNOSTIC_ASSERT(quotaManager);

        QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized()));

        QM_TRY(
            MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized()));

        QM_TRY_UNWRAP(mDirectoryMetadata->mDir,
                      quotaManager
                          ->EnsureTemporaryOriginIsInitialized(
                              PERSISTENCE_TYPE_DEFAULT, *mDirectoryMetadata)
                          .map([](const auto& res) { return res.first; }));

        mState = STATE_RUN_ON_TARGET;

        MOZ_ALWAYS_SUCCEEDS(
            mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));

        return Ok{};
      }();

      if (res.isErr()) {
        resolver->Resolve(res.inspectErr());
      }

      break;
    }
    // -------------------
    case STATE_RUN_ON_TARGET: {
      MOZ_ASSERT(mTarget->IsOnCurrentThread());

      mState = STATE_RUNNING;

      // Execute the provided initialization Action.  The Action must Resolve()
      // before returning.
      mInitAction->RunOnTarget(resolver.clonePtr(), mDirectoryMetadata, mData);
      MOZ_DIAGNOSTIC_ASSERT(resolver->Resolved());

      mData = nullptr;

      // If the database was opened, then we should always succeed when creating
      // the marker file.  If it wasn't opened successfully, then no need to
      // create a marker file anyway.
      if (NS_SUCCEEDED(resolver->Result())) {
        MOZ_ALWAYS_SUCCEEDS(CreateMarkerFile(*mDirectoryMetadata));
      }

      break;
    }
    // -------------------
    case STATE_COMPLETING: {
      NS_ASSERT_OWNINGTHREAD(QuotaInitRunnable);
      mInitAction->CompleteOnInitiatingThread(mResult);
      mContext->OnQuotaInit(mResult, mDirectoryMetadata,
                            mDirectoryLock.forget());
      mState = STATE_COMPLETE;

      // Explicitly cleanup here as the destructor could fire on any of
      // the threads we have bounced through.
      Clear();
      break;
    }
    // -----
    case STATE_WAIT_FOR_DIRECTORY_LOCK:
    default: {
      MOZ_CRASH("unexpected state in QuotaInitRunnable");
    }
  }

  if (resolver->Resolved()) {
    Complete(resolver->Result());
  }

  return NS_OK;
}

// Runnable wrapper around Action objects dispatched on the Context.  This
// runnable executes the Action on the appropriate threads while the Context
// is initialized.
class Context::ActionRunnable final : public nsIRunnable,
                                      public Action::Resolver,
                                      public Context::Activity {
 public:
  ActionRunnable(SafeRefPtr<Context> aContext, Data* aData,
                 nsISerialEventTarget* aTarget, SafeRefPtr<Action> aAction,
                 const Maybe<CacheDirectoryMetadata>& aDirectoryMetadata)
      : mContext(std::move(aContext)),
        mData(aData),
        mTarget(aTarget),
        mAction(std::move(aAction)),
        mDirectoryMetadata(aDirectoryMetadata),
        mInitiatingThread(GetCurrentSerialEventTarget()),
        mState(STATE_INIT),
        mResult(NS_OK),
        mExecutingRunOnTarget(false) {
    MOZ_DIAGNOSTIC_ASSERT(mContext);
    // mData may be nullptr
    MOZ_DIAGNOSTIC_ASSERT(mTarget);
    MOZ_DIAGNOSTIC_ASSERT(mAction);
    // mDirectoryMetadata.mDir may be nullptr if QuotaInitRunnable failed
    MOZ_DIAGNOSTIC_ASSERT(mInitiatingThread);
  }

  nsresult Dispatch() {
    NS_ASSERT_OWNINGTHREAD(ActionRunnable);
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_INIT);

    mState = STATE_RUN_ON_TARGET;
    nsresult rv = mTarget->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      mState = STATE_COMPLETE;
      Clear();
    }
    return rv;
  }

  virtual bool MatchesCacheId(CacheId aCacheId) const override {
    NS_ASSERT_OWNINGTHREAD(ActionRunnable);
    return mAction->MatchesCacheId(aCacheId);
  }

  virtual void Cancel() override {
    NS_ASSERT_OWNINGTHREAD(ActionRunnable);
    mAction->CancelOnInitiatingThread();
  }

  virtual void Resolve(nsresult aRv) override {
    MOZ_ASSERT(mTarget->IsOnCurrentThread());
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_RUNNING);

    mResult = aRv;

    // We ultimately must complete on the initiating thread, but bounce through
    // the current thread again to ensure that we don't destroy objects and
    // state out from under the currently running action's stack.
    mState = STATE_RESOLVING;

    // If we were resolved synchronously within Action::RunOnTarget() then we
    // can avoid a thread bounce and just resolve once RunOnTarget() returns.
    // The Run() method will handle this by looking at mState after
    // RunOnTarget() returns.
    if (mExecutingRunOnTarget) {
      return;
    }

    // Otherwise we are in an asynchronous resolve.  And must perform a thread
    // bounce to run on the target thread again.
    MOZ_ALWAYS_SUCCEEDS(mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL));
  }

 private:
  ~ActionRunnable() {
    MOZ_DIAGNOSTIC_ASSERT(mState == STATE_COMPLETE);
    MOZ_DIAGNOSTIC_ASSERT(!mContext);
    MOZ_DIAGNOSTIC_ASSERT(!mAction);
  }

  void Clear() {
    NS_ASSERT_OWNINGTHREAD(ActionRunnable);
    MOZ_DIAGNOSTIC_ASSERT(mContext);
    MOZ_DIAGNOSTIC_ASSERT(mAction);
    mContext->RemoveActivity(*this);
    mContext = nullptr;
    mAction = nullptr;
  }

  enum State {
    STATE_INIT,
    STATE_RUN_ON_TARGET,
    STATE_RUNNING,
    STATE_RESOLVING,
    STATE_COMPLETING,
    STATE_COMPLETE
  };

  SafeRefPtr<Context> mContext;
  RefPtr<Data> mData;
  nsCOMPtr<nsISerialEventTarget> mTarget;
  SafeRefPtr<Action> mAction;
  const Maybe<CacheDirectoryMetadata> mDirectoryMetadata;
  nsCOMPtr<nsIEventTarget> mInitiatingThread;
  State mState;
  nsresult mResult;

  // Only accessible on target thread;
  bool mExecutingRunOnTarget;

 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIRUNNABLE
};

NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::ActionRunnable, nsIRunnable);

// The ActionRunnable has a simpler state machine.  It basically needs to run
// the action on the target thread and then complete on the original thread.
//
//   +-------------+
//   |    Start    |
//   |(Orig Thread)|
//   +-----+-------+
//         |
// +-------v---------+
// |  RunOnTarget    |
// |Target IO Thread)+---+ Resolve()
// +-------+---------+   |
//         |             |
// +-------v----------+  |
// |     Running      |  |
// |(Target IO Thread)|  |
// +------------------+  |
//         | Resolve()   |
// +-------v----------+  |
// |     Resolving    <--+                   +-------------+
// |                  |                      |  Completing |
// |(Target IO Thread)+---------------------->(Orig Thread)|
// +------------------+                      +-------+-----+
//                                                   |
//                                                   |
//                                              +----v---+
//                                              |Complete|
//                                              +--------+
//
// Its important to note that synchronous actions will effectively Resolve()
// out of the Running state immediately.  Asynchronous Actions may remain
// in the Running state for some time, but normally the ActionRunnable itself
// does not see any execution there.  Its all handled internal to the Action.
NS_IMETHODIMP
Context::ActionRunnable::Run() {
  switch (mState) {
    // ----------------------
    case STATE_RUN_ON_TARGET: {
      MOZ_ASSERT(mTarget->IsOnCurrentThread());
      MOZ_DIAGNOSTIC_ASSERT(!mExecutingRunOnTarget);

      // Note that we are calling RunOnTarget().  This lets us detect
      // if Resolve() is called synchronously.
      AutoRestore<bool> executingRunOnTarget(mExecutingRunOnTarget);
      mExecutingRunOnTarget = true;

      mState = STATE_RUNNING;
      mAction->RunOnTarget(SafeRefPtrFromThis(), mDirectoryMetadata, mData);

      mData = nullptr;

      // Resolve was called synchronously from RunOnTarget().  We can
      // immediately move to completing now since we are sure RunOnTarget()
      // completed.
      if (mState == STATE_RESOLVING) {
        // Use recursion instead of switch case fall-through...  Seems slightly
        // easier to understand.
        Run();
      }

      break;
    }
    // -----------------
    case STATE_RESOLVING: {
      MOZ_ASSERT(mTarget->IsOnCurrentThread());
      // The call to Action::RunOnTarget() must have returned now if we
      // are running on the target thread again.  We may now proceed
      // with completion.
      mState = STATE_COMPLETING;
      // Shutdown must be delayed until all Contexts are destroyed.  Crash
      // for this invariant violation.
      MOZ_ALWAYS_SUCCEEDS(
          mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL));
      break;
    }
    // -------------------
    case STATE_COMPLETING: {
      NS_ASSERT_OWNINGTHREAD(ActionRunnable);
      mAction->CompleteOnInitiatingThread(mResult);
      mState = STATE_COMPLETE;
      // Explicitly cleanup here as the destructor could fire on any of
      // the threads we have bounced through.
      Clear();
      break;
    }
    // -----------------
    default: {
      MOZ_CRASH("unexpected state in ActionRunnable");
      break;
    }
  }
  return NS_OK;
}

void Context::ThreadsafeHandle::AllowToClose() {
  if (mOwningEventTarget->IsOnCurrentThread()) {
    AllowToCloseOnOwningThread();
    return;
  }

  // Dispatch is guaranteed to succeed here because we block shutdown until
  // all Contexts have been destroyed.
  nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
      "dom::cache::Context::ThreadsafeHandle::AllowToCloseOnOwningThread", this,
      &ThreadsafeHandle::AllowToCloseOnOwningThread);
  MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(runnable.forget(),
                                                   nsIThread::DISPATCH_NORMAL));
}

void Context::ThreadsafeHandle::InvalidateAndAllowToClose() {
  if (mOwningEventTarget->IsOnCurrentThread()) {
    InvalidateAndAllowToCloseOnOwningThread();
    return;
  }

  // Dispatch is guaranteed to succeed here because we block shutdown until
  // all Contexts have been destroyed.
  nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
      "dom::cache::Context::ThreadsafeHandle::"
      "InvalidateAndAllowToCloseOnOwningThread",
      this, &ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread);
  MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(runnable.forget(),
                                                   nsIThread::DISPATCH_NORMAL));
}

Context::ThreadsafeHandle::ThreadsafeHandle(SafeRefPtr<Context> aContext)
    : mStrongRef(std::move(aContext)),
      mWeakRef(mStrongRef.unsafeGetRawPtr()),
      mOwningEventTarget(GetCurrentSerialEventTarget()) {}

Context::ThreadsafeHandle::~ThreadsafeHandle() {
  // Normally we only touch mStrongRef on the owning thread.  This is safe,
  // however, because when we do use mStrongRef on the owning thread we are
  // always holding a strong ref to the ThreadsafeHandle via the owning
  // runnable.  So we cannot run the ThreadsafeHandle destructor simultaneously.
  if (!mStrongRef || mOwningEventTarget->IsOnCurrentThread()) {
    return;
  }

  // Dispatch in NS_ProxyRelease is guaranteed to succeed here because we block
  // shutdown until all Contexts have been destroyed. Therefore it is ok to have
  // MOZ_ALWAYS_SUCCEED here.
  MOZ_ALWAYS_SUCCEEDS(NS_ProxyRelease("Context::ThreadsafeHandle::mStrongRef",
                                      mOwningEventTarget, mStrongRef.forget()));
}

void Context::ThreadsafeHandle::AllowToCloseOnOwningThread() {
  MOZ_ASSERT(mOwningEventTarget->IsOnCurrentThread());

  // A Context "closes" when its ref count drops to zero.  Dropping this
  // strong ref is necessary, but not sufficient for the close to occur.
  // Any outstanding IO will continue and keep the Context alive.  Once
  // the Context is idle, it will be destroyed.

  // First, tell the context to flush any target thread shared data.  This
  // data must be released on the target thread prior to running the Context
  // destructor.  This will schedule an Action which ensures that the
  // ~Context() is not immediately executed when we drop the strong ref.
  if (mStrongRef) {
    mStrongRef->DoomTargetData();
  }

  // Now drop our strong ref and let Context finish running any outstanding
  // Actions.
  mStrongRef = nullptr;
}

void Context::ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread() {
  MOZ_ASSERT(mOwningEventTarget->IsOnCurrentThread());
  // Cancel the Context through the weak reference.  This means we can
  // allow the Context to close by dropping the strong ref, but then
  // still cancel ongoing IO if necessary.
  if (mWeakRef) {
    mWeakRef->Invalidate();
  }
  // We should synchronously have AllowToCloseOnOwningThread called when
  // the Context is canceled.
  MOZ_DIAGNOSTIC_ASSERT(!mStrongRef);
}

void Context::ThreadsafeHandle::ContextDestroyed(Context& aContext) {
  MOZ_ASSERT(mOwningEventTarget->IsOnCurrentThread());
  MOZ_DIAGNOSTIC_ASSERT(!mStrongRef);
  MOZ_DIAGNOSTIC_ASSERT(mWeakRef);
  MOZ_DIAGNOSTIC_ASSERT(mWeakRef == &aContext);
  mWeakRef = nullptr;
}

// static
SafeRefPtr<Context> Context::Create(SafeRefPtr<Manager> aManager,
                                    nsISerialEventTarget* aTarget,
                                    SafeRefPtr<Action> aInitAction,
                                    Maybe<Context&> aOldContext) {
  auto context = MakeSafeRefPtr<Context>(std::move(aManager), aTarget,
                                         std::move(aInitAction));
  context->Init(aOldContext);
  return context;
}

Context::Context(SafeRefPtr<Manager> aManager, nsISerialEventTarget* aTarget,
                 SafeRefPtr<Action> aInitAction)
    : mManager(std::move(aManager)),
      mTarget(aTarget),
      mData(new Data(aTarget)),
      mState(STATE_CONTEXT_PREINIT),
      mOrphanedData(false),
      mInitAction(std::move(aInitAction)) {
  MOZ_DIAGNOSTIC_ASSERT(mManager);
  MOZ_DIAGNOSTIC_ASSERT(mTarget);
}

void Context::Dispatch(SafeRefPtr<Action> aAction) {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_DIAGNOSTIC_ASSERT(aAction);
  MOZ_DIAGNOSTIC_ASSERT(mState != STATE_CONTEXT_CANCELED);

  if (mState == STATE_CONTEXT_CANCELED) {
    return;
  }

  if (mState == STATE_CONTEXT_INIT || mState == STATE_CONTEXT_PREINIT) {
    PendingAction* pending = mPendingActions.AppendElement();
    pending->mAction = std::move(aAction);
    return;
  }

  MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_READY);
  DispatchAction(std::move(aAction));
}

Maybe<DirectoryLock&> Context::MaybeDirectoryLockRef() const {
  NS_ASSERT_OWNINGTHREAD(Context);

  if (mState == STATE_CONTEXT_PREINIT) {
    MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);
    MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);

    return Nothing();
  }

  if (mState == STATE_CONTEXT_INIT) {
    MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);

    return mInitRunnable->MaybeDirectoryLockRef();
  }

  return ToMaybeRef(mDirectoryLock.get());
}

void Context::CancelAll() {
  NS_ASSERT_OWNINGTHREAD(Context);

  // In PREINIT state we have not dispatch the init action yet.  Just
  // forget it.
  if (mState == STATE_CONTEXT_PREINIT) {
    MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);
    mInitAction = nullptr;

    // In INIT state we have dispatched the runnable, but not received the
    // async completion yet.  Cancel the runnable, but don't forget about it
    // until we get OnQuotaInit() callback.
  } else if (mState == STATE_CONTEXT_INIT) {
    mInitRunnable->Cancel();
  }

  mState = STATE_CONTEXT_CANCELED;
  mPendingActions.Clear();
  for (const auto& activity : mActivityList.ForwardRange()) {
    activity->Cancel();
  }
  AllowToClose();
}

bool Context::IsCanceled() const {
  NS_ASSERT_OWNINGTHREAD(Context);
  return mState == STATE_CONTEXT_CANCELED;
}

void Context::Invalidate() {
  NS_ASSERT_OWNINGTHREAD(Context);
  mManager->NoteClosing();
  CancelAll();
}

void Context::AllowToClose() {
  NS_ASSERT_OWNINGTHREAD(Context);
  if (mThreadsafeHandle) {
    mThreadsafeHandle->AllowToClose();
  }
}

void Context::CancelForCacheId(CacheId aCacheId) {
  NS_ASSERT_OWNINGTHREAD(Context);

  // Remove matching pending actions
  mPendingActions.RemoveElementsBy([aCacheId](const auto& pendingAction) {
    return pendingAction.mAction->MatchesCacheId(aCacheId);
  });

  // Cancel activities and let them remove themselves
  for (const auto& activity : mActivityList.ForwardRange()) {
    if (activity->MatchesCacheId(aCacheId)) {
      activity->Cancel();
    }
  }
}

Context::~Context() {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_DIAGNOSTIC_ASSERT(mManager);
  MOZ_DIAGNOSTIC_ASSERT(!mData);

  if (mThreadsafeHandle) {
    mThreadsafeHandle->ContextDestroyed(*this);
  }

  // Note, this may set the mOrphanedData flag.
  mManager->RemoveContext(*this);

  if (mDirectoryMetadata && mDirectoryMetadata->mDir && !mOrphanedData) {
    MOZ_ALWAYS_SUCCEEDS(DeleteMarkerFile(*mDirectoryMetadata));
  }

  if (mNextContext) {
    mNextContext->Start();
  }
}

void Context::Init(Maybe<Context&> aOldContext) {
  NS_ASSERT_OWNINGTHREAD(Context);

  if (aOldContext) {
    aOldContext->SetNextContext(SafeRefPtrFromThis());
    return;
  }

  Start();
}

void Context::Start() {
  NS_ASSERT_OWNINGTHREAD(Context);

  // Previous context closing delayed our start, but then we were canceled.
  // In this case, just do nothing here.
  if (mState == STATE_CONTEXT_CANCELED) {
    MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);
    MOZ_DIAGNOSTIC_ASSERT(!mInitAction);
    // If we can't initialize the quota subsystem we will never be able to
    // clear our shared data object via the target IO thread.  Instead just
    // clear it here to maintain the invariant that the shared data is
    // cleared before Context destruction.
    mData = nullptr;
    return;
  }

  MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_PREINIT);
  MOZ_DIAGNOSTIC_ASSERT(!mInitRunnable);

  mInitRunnable =
      new QuotaInitRunnable(SafeRefPtrFromThis(), mManager.clonePtr(), mData,
                            mTarget, std::move(mInitAction));
  mState = STATE_CONTEXT_INIT;

  nsresult rv = mInitRunnable->Dispatch();
  if (NS_FAILED(rv)) {
    // Shutdown must be delayed until all Contexts are destroyed.  Shutdown
    // must also prevent any new Contexts from being constructed.  Crash
    // for this invariant violation.
    MOZ_CRASH("Failed to dispatch QuotaInitRunnable.");
  }
}

void Context::DispatchAction(SafeRefPtr<Action> aAction, bool aDoomData) {
  NS_ASSERT_OWNINGTHREAD(Context);

  auto runnable =
      MakeSafeRefPtr<ActionRunnable>(SafeRefPtrFromThis(), mData, mTarget,
                                     std::move(aAction), mDirectoryMetadata);

  if (aDoomData) {
    mData = nullptr;
  }

  nsresult rv = runnable->Dispatch();
  if (NS_FAILED(rv)) {
    // Shutdown must be delayed until all Contexts are destroyed.  Crash
    // for this invariant violation.
    MOZ_CRASH("Failed to dispatch ActionRunnable to target thread.");
  }
  AddActivity(*runnable);
}

void Context::OnQuotaInit(
    nsresult aRv, const Maybe<CacheDirectoryMetadata>& aDirectoryMetadata,
    already_AddRefed<DirectoryLock> aDirectoryLock) {
  NS_ASSERT_OWNINGTHREAD(Context);

  MOZ_DIAGNOSTIC_ASSERT(mInitRunnable);
  mInitRunnable = nullptr;

  if (aDirectoryMetadata) {
    mDirectoryMetadata.emplace(*aDirectoryMetadata);
  }

  // Always save the directory lock to ensure QuotaManager does not shutdown
  // before the Context has gone away.
  MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLock);
  mDirectoryLock = aDirectoryLock;

  // If we opening the context failed, but we were not explicitly canceled,
  // still treat the entire context as canceled.  We don't want to allow
  // new actions to be dispatched.  We also cannot leave the context in
  // the INIT state after failing to open.
  if (NS_FAILED(aRv)) {
    mState = STATE_CONTEXT_CANCELED;
  }

  if (mState == STATE_CONTEXT_CANCELED) {
    for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
      mPendingActions[i].mAction->CompleteOnInitiatingThread(aRv);
    }
    mPendingActions.Clear();
    mThreadsafeHandle->AllowToClose();
    // Context will destruct after return here and last ref is released.
    return;
  }

  MOZ_DIAGNOSTIC_ASSERT(mState == STATE_CONTEXT_INIT);
  mState = STATE_CONTEXT_READY;

  for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
    DispatchAction(std::move(mPendingActions[i].mAction));
  }
  mPendingActions.Clear();
}

void Context::AddActivity(Activity& aActivity) {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_ASSERT(!mActivityList.Contains(&aActivity));
  mActivityList.AppendElement(WrapNotNullUnchecked(&aActivity));
}

void Context::RemoveActivity(Activity& aActivity) {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_ALWAYS_TRUE(mActivityList.RemoveElement(&aActivity));
  MOZ_ASSERT(!mActivityList.Contains(&aActivity));
}

void Context::NoteOrphanedData() {
  NS_ASSERT_OWNINGTHREAD(Context);
  // This may be called more than once
  mOrphanedData = true;
}

SafeRefPtr<Context::ThreadsafeHandle> Context::CreateThreadsafeHandle() {
  NS_ASSERT_OWNINGTHREAD(Context);
  if (!mThreadsafeHandle) {
    mThreadsafeHandle = MakeSafeRefPtr<ThreadsafeHandle>(SafeRefPtrFromThis());
  }
  return mThreadsafeHandle.clonePtr();
}

void Context::SetNextContext(SafeRefPtr<Context> aNextContext) {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_DIAGNOSTIC_ASSERT(aNextContext);
  MOZ_DIAGNOSTIC_ASSERT(!mNextContext);
  mNextContext = std::move(aNextContext);
}

void Context::DoomTargetData() {
  NS_ASSERT_OWNINGTHREAD(Context);
  MOZ_DIAGNOSTIC_ASSERT(mData);

  // We are about to drop our reference to the Data.  We need to ensure that
  // the ~Context() destructor does not run until contents of Data have been
  // released on the Target thread.

  // Dispatch a no-op Action.  This will hold the Context alive through a
  // roundtrip to the target thread and back to the owning thread.  The
  // ref to the Data object is cleared on the owning thread after creating
  // the ActionRunnable, but before dispatching it.
  DispatchAction(MakeSafeRefPtr<NullAction>(), true /* doomed data */);

  MOZ_DIAGNOSTIC_ASSERT(!mData);
}

}  // namespace mozilla::dom::cache
