/* 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 mozilla_SyncRunnable_h
#define mozilla_SyncRunnable_h

#include <utility>

#include "mozilla/AbstractThread.h"
#include "mozilla/Monitor.h"
#include "mozilla/dom/JSExecutionManager.h"
#include "nsThreadUtils.h"

namespace mozilla {

/**
 * This class will wrap a nsIRunnable and dispatch it to the target thread
 * synchronously. This is different from
 * NS_DispatchAndSpinEventLoopUntilComplete: this class does not spin the event
 * loop waiting for the event to be dispatched. This means that you don't risk
 * reentrance from pending messages, but you must be sure that the target thread
 * does not ever block on this thread, or else you will deadlock.
 *
 * Typical usage:
 *   RefPtr sr = MakeRefPtr<SyncRunnable>(new myrunnable...());
 *   sr->DispatchToThread(t);
 *
 * We also provide convenience wrappers:
 *   SyncRunnable::DispatchToThread(pThread, new myrunnable...());
 *   SyncRunnable::DispatchToThread(pThread, NS_NewRunnableFunction(...));
 *
 */
class SyncRunnable : public Runnable {
 public:
  explicit SyncRunnable(nsIRunnable* aRunnable)
      : Runnable("SyncRunnable"),
        mRunnable(aRunnable),
        mMonitor("SyncRunnable"),
        mDone(false) {}

  explicit SyncRunnable(already_AddRefed<nsIRunnable> aRunnable)
      : Runnable("SyncRunnable"),
        mRunnable(std::move(aRunnable)),
        mMonitor("SyncRunnable"),
        mDone(false) {}

  nsresult DispatchToThread(nsIEventTarget* aThread,
                            bool aForceDispatch = false) {
    nsresult rv;
    bool on;

    if (!aForceDispatch) {
      rv = aThread->IsOnCurrentThread(&on);
      MOZ_ASSERT(NS_SUCCEEDED(rv));
      if (NS_SUCCEEDED(rv) && on) {
        mRunnable->Run();
        return NS_OK;
      }
    }

    rv = aThread->Dispatch(this, NS_DISPATCH_NORMAL);
    if (NS_SUCCEEDED(rv)) {
      mozilla::MonitorAutoLock lock(mMonitor);
      // This could be synchronously dispatching to a thread currently waiting
      // for JS execution clearance. Yield JS execution.
      dom::AutoYieldJSThreadExecution yield;

      while (!mDone) {
        lock.Wait();
      }
    }
    return rv;
  }

  nsresult DispatchToThread(AbstractThread* aThread,
                            bool aForceDispatch = false) {
    if (!aForceDispatch && aThread->IsCurrentThreadIn()) {
      mRunnable->Run();
      return NS_OK;
    }

    // Check we don't have tail dispatching here. Otherwise we will deadlock
    // ourself when spinning the loop below.
    MOZ_ASSERT(!aThread->RequiresTailDispatchFromCurrentThread());

    nsresult rv = aThread->Dispatch(RefPtr<nsIRunnable>(this).forget());
    if (NS_SUCCEEDED(rv)) {
      mozilla::MonitorAutoLock lock(mMonitor);
      while (!mDone) {
        lock.Wait();
      }
    }
    return rv;
  }

  static nsresult DispatchToThread(nsIEventTarget* aThread,
                                   nsIRunnable* aRunnable,
                                   bool aForceDispatch = false) {
    RefPtr<SyncRunnable> s(new SyncRunnable(aRunnable));
    return s->DispatchToThread(aThread, aForceDispatch);
  }

  static nsresult DispatchToThread(AbstractThread* aThread,
                                   nsIRunnable* aRunnable,
                                   bool aForceDispatch = false) {
    RefPtr<SyncRunnable> s(new SyncRunnable(aRunnable));
    return s->DispatchToThread(aThread, aForceDispatch);
  }

  static nsresult DispatchToThread(nsIEventTarget* aThread,
                                   already_AddRefed<nsIRunnable> aRunnable,
                                   bool aForceDispatch = false) {
    RefPtr<SyncRunnable> s(new SyncRunnable(std::move(aRunnable)));
    return s->DispatchToThread(aThread, aForceDispatch);
  }

  static nsresult DispatchToThread(AbstractThread* aThread,
                                   already_AddRefed<nsIRunnable> aRunnable,
                                   bool aForceDispatch = false) {
    RefPtr<SyncRunnable> s(new SyncRunnable(std::move(aRunnable)));
    return s->DispatchToThread(aThread, aForceDispatch);
  }

  // These deleted overloads prevent accidentally (if harmlessly) double-
  // wrapping SyncRunnable, which was previously a common anti-pattern.
  static nsresult DispatchToThread(nsIEventTarget* aThread,
                                   SyncRunnable* aRunnable,
                                   bool aForceDispatch = false) = delete;
  static nsresult DispatchToThread(AbstractThread* aThread,
                                   SyncRunnable* aRunnable,
                                   bool aForceDispatch = false) = delete;

 protected:
  NS_IMETHOD Run() override {
    mRunnable->Run();

    mozilla::MonitorAutoLock lock(mMonitor);
    MOZ_ASSERT(!mDone);

    mDone = true;
    mMonitor.Notify();

    return NS_OK;
  }

 private:
  nsCOMPtr<nsIRunnable> mRunnable;
  mozilla::Monitor mMonitor;
  bool mDone MOZ_GUARDED_BY(mMonitor);
};

}  // namespace mozilla

#endif  // mozilla_SyncRunnable_h
