/* -*- 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 <windows.h>
#include <threadpoolapiset.h>

#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h"
#include "mozilla/Logging.h"
#include "mozilla/Mutex.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ThreadSafety.h"
#include "mozilla/WinHandleWatcher.h"

#include "nsCOMPtr.h"
#include "nsIRunnable.h"
#include "nsISerialEventTarget.h"
#include "nsISupportsImpl.h"
#include "nsITargetShutdownTask.h"
#include "nsIWeakReferenceUtils.h"
#include "nsThreadUtils.h"

mozilla::LazyLogModule sHWLog("HandleWatcher");

namespace mozilla {
namespace details {
struct WaitHandleDeleter {
  void operator()(PTP_WAIT waitHandle) {
    MOZ_LOG(sHWLog, LogLevel::Debug, ("Closing PTP_WAIT %p", waitHandle));
    ::CloseThreadpoolWait(waitHandle);
  }
};
}  // namespace details
using WaitHandlePtr = UniquePtr<TP_WAIT, details::WaitHandleDeleter>;

// HandleWatcher::Impl
//
// The backing implementation of HandleWatcher is a PTP_WAIT, an OS-threadpool
// wait-object. Windows doesn't actually create a new thread per wait-object;
// OS-threadpool threads are assigned to wait-objects only when their associated
// handle become signaled -- although explicit documentation of this fact is
// somewhat obscurely placed. [1]
//
// Throughout this class, we use manual locking and unlocking guarded by Clang's
// thread-safety warnings, rather than scope-based lock-guards. See `Replace()`
// for an explanation and justification.
//
// [1]https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects#remarks
class HandleWatcher::Impl final : public nsITargetShutdownTask {
  NS_DECL_THREADSAFE_ISUPPORTS

 public:
  Impl() = default;

 private:
  ~Impl() { MOZ_ASSERT(IsStopped()); }

  struct Data {
    // The watched handle and its callback.
    HANDLE handle;
    RefPtr<nsIEventTarget> target;
    nsCOMPtr<nsIRunnable> runnable;

    // Handle to the threadpool wait-object.
    WaitHandlePtr waitHandle;
    // A pointer to ourselves, notionally owned by the wait-object.
    RefPtr<Impl> self;

    // (We can't actually do this because a) it has annoying consequences in
    // C++20 thanks to P1008R1, and b) Clang just ignores it anyway.)
    //
    // ~Data() MOZ_EXCLUDES(mMutex) = default;
  };

  mozilla::Mutex mMutex{"HandleWatcher::Impl"};
  Data mData MOZ_GUARDED_BY(mMutex) = {};

  // Callback from OS threadpool wait-object.
  static void CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE, void* ctx,
                                    PTP_WAIT aWaitHandle,
                                    TP_WAIT_RESULT aResult) {
    static_cast<Impl*>(ctx)->OnWaitCompleted(aWaitHandle, aResult);
  }

  void OnWaitCompleted(PTP_WAIT aWaitHandle, TP_WAIT_RESULT aResult)
      MOZ_EXCLUDES(mMutex) {
    MOZ_ASSERT(aResult == WAIT_OBJECT_0);

    mMutex.Lock();
    // If this callback is no longer the active callback, skip out.
    // All cleanup is someone else's problem.
    if (aWaitHandle != mData.waitHandle.get()) {
      MOZ_LOG(sHWLog, LogLevel::Debug,
              ("Recv'd already-stopped callback: HW %p | PTP_WAIT %p", this,
               aWaitHandle));
      mMutex.Unlock();
      return;
    }

    // Take our self-pointer so that we release it on exit.
    RefPtr<Impl> self = std::move(mData.self);

    MOZ_LOG(sHWLog, LogLevel::Info,
            ("Recv'd callback: HW %p | handle %p | target %p | PTP_WAIT %p",
             this, mData.handle, mData.target.get(), aWaitHandle));

    // This may fail if (for example) `mData.target` is being shut down, but we
    // have not yet received the shutdown callback.
    mData.target->Dispatch(mData.runnable.forget());
    Replace(Data{});
  }

 public:
  static RefPtr<Impl> Create(HANDLE aHandle, nsIEventTarget* aTarget,
                             already_AddRefed<nsIRunnable> aRunnable) {
    auto impl = MakeRefPtr<Impl>();
    bool const ok [[maybe_unused]] =
        impl->Watch(aHandle, aTarget, std::move(aRunnable));
    MOZ_ASSERT(ok);
    return impl;
  }

 private:
  bool Watch(HANDLE aHandle, nsIEventTarget* aTarget,
             already_AddRefed<nsIRunnable> aRunnable) MOZ_EXCLUDES(mMutex) {
    MOZ_ASSERT(aHandle);
    MOZ_ASSERT(aTarget);

    RefPtr<nsIEventTarget> target(aTarget);

    WaitHandlePtr waitHandle{
        ::CreateThreadpoolWait(&WaitCallback, this, nullptr)};
    if (!waitHandle) {
      return false;
    }

    {
      mMutex.Lock();

      nsresult const ret = aTarget->RegisterShutdownTask(this);
      if (NS_FAILED(ret)) {
        mMutex.Unlock();
        return false;
      }

      MOZ_LOG(sHWLog, LogLevel::Info,
              ("Setting callback: HW %p | handle %p | target %p | PTP_WAIT %p",
               this, aHandle, aTarget, waitHandle.get()));

      // returns `void`; presumably always succeeds given a successful
      // `::CreateThreadpoolWait()`
      ::SetThreadpoolWait(waitHandle.get(), aHandle, nullptr);
      // After this point, you must call `FlushWaitHandle(waitHandle.get())`
      // before destroying the wait handle. (Note that this must be done while
      // *not* holding `mMutex`!)

      Replace(Data{.handle = aHandle,
                   .target = std::move(target),
                   .runnable = aRunnable,
                   .waitHandle = std::move(waitHandle),
                   .self = this});
    }

    return true;
  }

  void TargetShutdown() MOZ_EXCLUDES(mMutex) override final {
    mMutex.Lock();

    MOZ_LOG(sHWLog, LogLevel::Debug,
            ("Target shutdown: HW %p | handle %p | target %p | PTP_WAIT %p",
             this, mData.handle, mData.target.get(), mData.waitHandle.get()));

    // Clear mData.target, since there's no need to unregister the shutdown task
    // anymore. Hold onto it until we release the mutex, though, to avoid any
    // reentrancy issues.
    //
    // This is more for internal consistency than safety: someone has to be
    // shutting `target` down, and that someone isn't us, so there's necessarily
    // another reference out there. (Although decrementing the refcount might
    // still have arbitrary effects if someone's been excessively clever with
    // nsISupports::Release...)
    auto const oldTarget = std::move(mData.target);
    Replace(Data{});
    // (Static-assert that the mutex has indeed been released.)
    ([&]() MOZ_EXCLUDES(mMutex) {})();
  }

 public:
  void Stop() MOZ_EXCLUDES(mMutex) {
    mMutex.Lock();
    Replace(Data{});
  }

  bool IsStopped() MOZ_EXCLUDES(mMutex) {
    mozilla::MutexAutoLock lock(mMutex);
    return !mData.handle;
  }

 private:
  // Throughout this class, we use manual locking and unlocking guarded by
  // Clang's thread-safety warnings, rather than scope-based lock-guards. This
  // is largely driven by `Replace()`, below, which performs both operations
  // which require the mutex to be held and operations which require it to not
  // be held, and therefore must explicitly sequence the mutex release.
  //
  // These explicit locks, unlocks, and annotations are both alien to C++ and
  // offensively tedious; but they _are_ still checked for state consistency at
  // scope boundaries. (The concerned reader is invited to test this by
  // deliberately removing an `mMutex.Unlock()` call from anywhere in the class
  // and viewing the resultant compiler diagnostics.)
  //
  // A more principled, or at least differently-principled, implementation might
  // create a scope-based lock-guard and pass it to `Replace()` to dispose of at
  // the proper time. Alas, it cannot be communicated to Clang's thread-safety
  // checker that such a guard is associated with `mMutex`.
  //
  void Replace(Data&& aData) MOZ_CAPABILITY_RELEASE(mMutex) {
    // either both handles are NULL, or neither is
    MOZ_ASSERT(!!aData.handle == !!aData.waitHandle);

    if (mData.handle) {
      MOZ_LOG(sHWLog, LogLevel::Info,
              ("Stop callback: HW %p | handle %p | target %p | PTP_WAIT %p",
               this, mData.handle, mData.target.get(), mData.waitHandle.get()));
    }

    if (mData.target) {
      mData.target->UnregisterShutdownTask(this);
    }

    // Extract the old data and insert the new -- but hold onto the old data for
    // now. (See [1] and [2], below.)
    Data oldData = std::exchange(mData, std::move(aData));

    ////////////////////////////////////////////////////////////////////////////
    // Release the mutex.
    mMutex.Unlock();
    ////////////////////////////////////////////////////////////////////////////

    // [1] `oldData.self` will be unset if the old callback already ran (or if
    // there was no old callback in the first place). If it's set, though, we
    // need to explicitly clear out the wait-object first.
    if (oldData.self) {
      MOZ_ASSERT(oldData.waitHandle);
      FlushWaitHandle(oldData.waitHandle.get());
    }

    // [2] oldData also includes several other reference-counted pointers. It's
    // possible that these may be the last pointer to something, so releasing
    // them may have arbitrary side-effects -- like calling this->Stop(), which
    // will try to reacquire the mutex.
    //
    // Now that we've released the mutex, we can (implicitly) release them all
    // here.
  }

  // Either confirm as complete or cancel any callbacks on aWaitHandle. Block
  // until this is done. (See documentation for ::CloseThreadpoolWait().)
  void FlushWaitHandle(PTP_WAIT aWaitHandle) MOZ_EXCLUDES(mMutex) {
    ::SetThreadpoolWait(aWaitHandle, nullptr, nullptr);
    // This might block on `OnWaitCompleted()`, so we can't hold `mMutex` here.
    ::WaitForThreadpoolWaitCallbacks(aWaitHandle, TRUE);
    // ::CloseThreadpoolWait() itself is the caller's responsibility.
  }
};

NS_IMPL_ISUPPORTS(HandleWatcher::Impl, nsITargetShutdownTask)

//////
// HandleWatcher member function implementations

HandleWatcher::HandleWatcher() : mImpl{} {}
HandleWatcher::~HandleWatcher() {
  if (mImpl) {
    MOZ_ASSERT(mImpl->IsStopped());
    mImpl->Stop();  // just in case, in release
  }
}

void HandleWatcher::Watch(HANDLE aHandle, nsIEventTarget* aTarget,
                          already_AddRefed<nsIRunnable> aRunnable) {
  auto impl = Impl::Create(aHandle, aTarget, std::move(aRunnable));
  MOZ_ASSERT(impl);

  if (mImpl) {
    mImpl->Stop();
  }
  mImpl = std::move(impl);
}

void HandleWatcher::Stop() {
  if (mImpl) {
    mImpl->Stop();
  }
}

bool HandleWatcher::IsStopped() { return !mImpl || mImpl->IsStopped(); }

}  // namespace mozilla
