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

#include "ExtensionAPIAddRemoveListener.h"

#include "mozilla/dom/ExtensionEventManagerBinding.h"
#include "nsIGlobalObject.h"
#include "ExtensionEventListener.h"

namespace mozilla {
namespace extensions {

NS_IMPL_CYCLE_COLLECTION_CLASS(ExtensionEventManager);
NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionEventManager);
NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionEventManager)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ExtensionEventManager)
  tmp->mListeners.clear();
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionBrowser)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ExtensionEventManager)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionBrowser)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ExtensionEventManager)
  for (auto iter = tmp->mListeners.iter(); !iter.done(); iter.next()) {
    aCallbacks.Trace(&iter.get().mutableKey(), "mListeners key", aClosure);
  }
  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionEventManager)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

ExtensionEventManager::ExtensionEventManager(
    nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
    const nsAString& aNamespace, const nsAString& aEventName,
    const nsAString& aObjectType, const nsAString& aObjectId)
    : mGlobal(aGlobal),
      mExtensionBrowser(aExtensionBrowser),
      mAPINamespace(aNamespace),
      mEventName(aEventName),
      mAPIObjectType(aObjectType),
      mAPIObjectId(aObjectId) {
  MOZ_DIAGNOSTIC_ASSERT(mGlobal);
  MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);

  mozilla::HoldJSObjects(this);
}

ExtensionEventManager::~ExtensionEventManager() {
  ReleaseListeners();
  mozilla::DropJSObjects(this);
};

void ExtensionEventManager::ReleaseListeners() {
  if (mListeners.empty()) {
    return;
  }

  for (auto iter = mListeners.iter(); !iter.done(); iter.next()) {
    iter.get().value()->Cleanup();
  }

  mListeners.clear();
}

JSObject* ExtensionEventManager::WrapObject(JSContext* aCx,
                                            JS::Handle<JSObject*> aGivenProto) {
  return dom::ExtensionEventManager_Binding::Wrap(aCx, this, aGivenProto);
}

nsIGlobalObject* ExtensionEventManager::GetParentObject() const {
  return mGlobal;
}

void ExtensionEventManager::AddListener(
    JSContext* aCx, dom::Function& aCallback,
    const dom::Optional<JS::Handle<JSObject*>>& aOptions, ErrorResult& aRv) {
  JS::Rooted<JSObject*> cb(aCx, aCallback.CallbackOrNull());
  if (cb == nullptr) {
    ThrowUnexpectedError(aCx, aRv);
    return;
  }

  RefPtr<ExtensionEventManager> self = this;

  IgnoredErrorResult rv;
  RefPtr<ExtensionEventListener> wrappedCb = ExtensionEventListener::Create(
      mGlobal, mExtensionBrowser, &aCallback,
      [self = std::move(self)]() { self->ReleaseListeners(); }, rv);

  if (NS_WARN_IF(rv.Failed())) {
    ThrowUnexpectedError(aCx, aRv);
    return;
  }

  RefPtr<ExtensionEventListener> storedWrapper = wrappedCb;
  if (!mListeners.put(cb, std::move(storedWrapper))) {
    ThrowUnexpectedError(aCx, aRv);
    return;
  }

  auto request = SendAddListener(mEventName);
  request->Run(mGlobal, aCx, {}, wrappedCb, aRv);

  if (!aRv.Failed() && mAPIObjectType.IsEmpty()) {
    mExtensionBrowser->TrackWakeupEventListener(aCx, mAPINamespace, mEventName);
  }
}

void ExtensionEventManager::RemoveListener(dom::Function& aCallback,
                                           ErrorResult& aRv) {
  dom::AutoJSAPI jsapi;
  if (NS_WARN_IF(!jsapi.Init(mGlobal))) {
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }

  JSContext* cx = jsapi.cx();
  JS::Rooted<JSObject*> cb(cx, aCallback.CallbackOrNull());
  const auto& ptr = mListeners.lookup(cb);

  // Return earlier if the listener wasn't found
  if (!ptr) {
    return;
  }

  RefPtr<ExtensionEventListener> wrappedCb = ptr->value();
  auto request = SendRemoveListener(mEventName);
  request->Run(mGlobal, cx, {}, wrappedCb, aRv);

  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  if (mAPIObjectType.IsEmpty()) {
    mExtensionBrowser->UntrackWakeupEventListener(cx, mAPINamespace,
                                                  mEventName);
  }

  mListeners.remove(cb);

  wrappedCb->Cleanup();
}

bool ExtensionEventManager::HasListener(dom::Function& aCallback,
                                        ErrorResult& aRv) const {
  return mListeners.has(aCallback.CallbackOrNull());
}

bool ExtensionEventManager::HasListeners(ErrorResult& aRv) const {
  return !mListeners.empty();
}

}  // namespace extensions
}  // namespace mozilla
