/* -*- 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 "WorkerDebuggerManager.h"

#include "nsSimpleEnumerator.h"

#include "mozilla/dom/JSExecutionManager.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"

#include "WorkerDebugger.h"
#include "WorkerPrivate.h"
#include "nsIObserverService.h"

namespace mozilla::dom {

namespace {

class RegisterDebuggerMainThreadRunnable final : public mozilla::Runnable {
  WorkerPrivate* mWorkerPrivate;
  bool mNotifyListeners;

 public:
  RegisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate,
                                     bool aNotifyListeners)
      : mozilla::Runnable("RegisterDebuggerMainThreadRunnable"),
        mWorkerPrivate(aWorkerPrivate),
        mNotifyListeners(aNotifyListeners) {}

 private:
  ~RegisterDebuggerMainThreadRunnable() = default;

  NS_IMETHOD
  Run() override {
    WorkerDebuggerManager* manager = WorkerDebuggerManager::Get();
    MOZ_ASSERT(manager);

    manager->RegisterDebuggerMainThread(mWorkerPrivate, mNotifyListeners);
    return NS_OK;
  }
};

class UnregisterDebuggerMainThreadRunnable final : public mozilla::Runnable {
  WorkerPrivate* mWorkerPrivate;

 public:
  explicit UnregisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate)
      : mozilla::Runnable("UnregisterDebuggerMainThreadRunnable"),
        mWorkerPrivate(aWorkerPrivate) {}

 private:
  ~UnregisterDebuggerMainThreadRunnable() = default;

  NS_IMETHOD
  Run() override {
    WorkerDebuggerManager* manager = WorkerDebuggerManager::Get();
    MOZ_ASSERT(manager);

    manager->UnregisterDebuggerMainThread(mWorkerPrivate);
    return NS_OK;
  }
};

static StaticRefPtr<WorkerDebuggerManager> gWorkerDebuggerManager;

} /* anonymous namespace */

class WorkerDebuggerEnumerator final : public nsSimpleEnumerator {
  nsTArray<RefPtr<WorkerDebugger>> mDebuggers;
  uint32_t mIndex;

 public:
  explicit WorkerDebuggerEnumerator(
      const nsTArray<RefPtr<WorkerDebugger>>& aDebuggers)
      : mDebuggers(aDebuggers.Clone()), mIndex(0) {}

  NS_DECL_NSISIMPLEENUMERATOR

  const nsID& DefaultInterface() override {
    return NS_GET_IID(nsIWorkerDebugger);
  }

 private:
  ~WorkerDebuggerEnumerator() override = default;
};

NS_IMETHODIMP
WorkerDebuggerEnumerator::HasMoreElements(bool* aResult) {
  *aResult = mIndex < mDebuggers.Length();
  return NS_OK;
};

NS_IMETHODIMP
WorkerDebuggerEnumerator::GetNext(nsISupports** aResult) {
  if (mIndex == mDebuggers.Length()) {
    return NS_ERROR_FAILURE;
  }

  mDebuggers.ElementAt(mIndex++).forget(aResult);
  return NS_OK;
};

WorkerDebuggerManager::WorkerDebuggerManager()
    : mMutex("WorkerDebuggerManager::mMutex") {
  AssertIsOnMainThread();
}

WorkerDebuggerManager::~WorkerDebuggerManager() { AssertIsOnMainThread(); }

// static
already_AddRefed<WorkerDebuggerManager> WorkerDebuggerManager::GetInstance() {
  RefPtr<WorkerDebuggerManager> manager = WorkerDebuggerManager::GetOrCreate();
  return manager.forget();
}

// static
WorkerDebuggerManager* WorkerDebuggerManager::GetOrCreate() {
  AssertIsOnMainThread();

  if (!gWorkerDebuggerManager) {
    // The observer service now owns us until shutdown.
    gWorkerDebuggerManager = new WorkerDebuggerManager();
    if (NS_SUCCEEDED(gWorkerDebuggerManager->Init())) {
      ClearOnShutdown(&gWorkerDebuggerManager);
    } else {
      NS_WARNING("Failed to initialize worker debugger manager!");
      gWorkerDebuggerManager = nullptr;
    }
  }

  return gWorkerDebuggerManager;
}

WorkerDebuggerManager* WorkerDebuggerManager::Get() {
  MOZ_ASSERT(gWorkerDebuggerManager);
  return gWorkerDebuggerManager;
}

NS_IMPL_ISUPPORTS(WorkerDebuggerManager, nsIObserver, nsIWorkerDebuggerManager);

NS_IMETHODIMP
WorkerDebuggerManager::Observe(nsISupports* aSubject, const char* aTopic,
                               const char16_t* aData) {
  if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
    Shutdown();
    return NS_OK;
  }

  MOZ_ASSERT_UNREACHABLE("Unknown observer topic!");
  return NS_OK;
}

NS_IMETHODIMP
WorkerDebuggerManager::GetWorkerDebuggerEnumerator(
    nsISimpleEnumerator** aResult) {
  AssertIsOnMainThread();

  RefPtr<WorkerDebuggerEnumerator> enumerator =
      new WorkerDebuggerEnumerator(mDebuggers);
  enumerator.forget(aResult);
  return NS_OK;
}

NS_IMETHODIMP
WorkerDebuggerManager::AddListener(
    nsIWorkerDebuggerManagerListener* aListener) {
  AssertIsOnMainThread();

  MutexAutoLock lock(mMutex);

  if (mListeners.Contains(aListener)) {
    return NS_ERROR_INVALID_ARG;
  }

  mListeners.AppendElement(aListener);
  return NS_OK;
}

NS_IMETHODIMP
WorkerDebuggerManager::RemoveListener(
    nsIWorkerDebuggerManagerListener* aListener) {
  AssertIsOnMainThread();

  MutexAutoLock lock(mMutex);

  if (!mListeners.Contains(aListener)) {
    return NS_OK;
  }

  mListeners.RemoveElement(aListener);
  return NS_OK;
}

nsresult WorkerDebuggerManager::Init() {
  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);

  nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

void WorkerDebuggerManager::Shutdown() {
  AssertIsOnMainThread();

  MutexAutoLock lock(mMutex);

  mListeners.Clear();
}

void WorkerDebuggerManager::RegisterDebugger(WorkerPrivate* aWorkerPrivate) {
  aWorkerPrivate->AssertIsOnParentThread();

  if (NS_IsMainThread()) {
    // When the parent thread is the main thread, it will always block until all
    // register liseners have been called, since it cannot continue until the
    // call to RegisterDebuggerMainThread returns.
    //
    // In this case, it is always safe to notify all listeners on the main
    // thread, even if there were no listeners at the time this method was
    // called, so we can always pass true for the value of aNotifyListeners.
    // This avoids having to lock mMutex to check whether mListeners is empty.
    RegisterDebuggerMainThread(aWorkerPrivate, true);
  } else {
    // We guarantee that if any register listeners are called, the worker does
    // not start running until all register listeners have been called. To
    // guarantee this, the parent thread should block until all register
    // listeners have been called.
    //
    // However, to avoid overhead when the debugger is not being used, the
    // parent thread will only block if there were any listeners at the time
    // this method was called. As a result, we should not notify any listeners
    // on the main thread if there were no listeners at the time this method was
    // called, because the parent will not be blocking in that case.
    bool hasListeners = false;
    {
      MutexAutoLock lock(mMutex);

      hasListeners = !mListeners.IsEmpty();
    }

    nsCOMPtr<nsIRunnable> runnable =
        new RegisterDebuggerMainThreadRunnable(aWorkerPrivate, hasListeners);
    MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL));

    if (hasListeners) {
      aWorkerPrivate->WaitForIsDebuggerRegistered(true);
    }
  }
}

void WorkerDebuggerManager::UnregisterDebugger(WorkerPrivate* aWorkerPrivate) {
  aWorkerPrivate->AssertIsOnParentThread();

  if (NS_IsMainThread()) {
    UnregisterDebuggerMainThread(aWorkerPrivate);
  } else {
    nsCOMPtr<nsIRunnable> runnable =
        new UnregisterDebuggerMainThreadRunnable(aWorkerPrivate);
    MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL));

    aWorkerPrivate->WaitForIsDebuggerRegistered(false);
  }
}

void WorkerDebuggerManager::RegisterDebuggerMainThread(
    WorkerPrivate* aWorkerPrivate, bool aNotifyListeners) {
  AssertIsOnMainThread();

  RefPtr<WorkerDebugger> debugger = new WorkerDebugger(aWorkerPrivate);
  mDebuggers.AppendElement(debugger);

  aWorkerPrivate->SetDebugger(debugger);

  if (aNotifyListeners) {
    for (const auto& listener : CloneListeners()) {
      listener->OnRegister(debugger);
    }
  }

  aWorkerPrivate->SetIsDebuggerRegistered(true);
}

void WorkerDebuggerManager::UnregisterDebuggerMainThread(
    WorkerPrivate* aWorkerPrivate) {
  AssertIsOnMainThread();

  // There is nothing to do here if the debugger was never succesfully
  // registered. We need to check this on the main thread because the worker
  // does not wait for the registration to complete if there were no listeners
  // installed when it started.
  if (!aWorkerPrivate->IsDebuggerRegistered()) {
    return;
  }

  RefPtr<WorkerDebugger> debugger = aWorkerPrivate->Debugger();
  mDebuggers.RemoveElement(debugger);

  aWorkerPrivate->SetDebugger(nullptr);

  for (const auto& listener : CloneListeners()) {
    listener->OnUnregister(debugger);
  }

  debugger->Close();
  aWorkerPrivate->SetIsDebuggerRegistered(false);
}

uint32_t WorkerDebuggerManager::GetDebuggersLength() const {
  return mDebuggers.Length();
}

WorkerDebugger* WorkerDebuggerManager::GetDebuggerAt(uint32_t aIndex) const {
  return mDebuggers.SafeElementAt(aIndex, nullptr);
}

nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>>
WorkerDebuggerManager::CloneListeners() {
  MutexAutoLock lock(mMutex);

  return mListeners.Clone();
}

}  // namespace mozilla::dom
