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

#include "mozilla/BasicEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/JSEventHandler.h"
#include "mozilla/Maybe.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/EventListenerBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "nsArray.h"
#include "nsArrayUtils.h"
#include "nsCOMArray.h"
#include "nsINode.h"
#include "nsJSUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsThreadUtils.h"

namespace mozilla {

using namespace dom;

/******************************************************************************
 * mozilla::EventListenerChange
 ******************************************************************************/

NS_IMPL_ISUPPORTS(EventListenerChange, nsIEventListenerChange)

EventListenerChange::~EventListenerChange() = default;

EventListenerChange::EventListenerChange(EventTarget* aTarget)
    : mTarget(aTarget) {}

void EventListenerChange::AddChangedListenerName(nsAtom* aEventName) {
  mChangedListenerNames.AppendElement(aEventName);
}

NS_IMETHODIMP
EventListenerChange::GetTarget(EventTarget** aTarget) {
  NS_ENSURE_ARG_POINTER(aTarget);
  NS_ADDREF(*aTarget = mTarget);
  return NS_OK;
}

NS_IMETHODIMP
EventListenerChange::GetCountOfEventListenerChangesAffectingAccessibility(
    uint32_t* aCount) {
  *aCount = 0;

  size_t length = mChangedListenerNames.Length();
  for (size_t i = 0; i < length; i++) {
    RefPtr<nsAtom> listenerName = mChangedListenerNames[i];

    // These are the event listener changes which may make an element
    // accessible or inaccessible.
    if (listenerName == nsGkAtoms::onclick ||
        listenerName == nsGkAtoms::onmousedown ||
        listenerName == nsGkAtoms::onmouseup) {
      *aCount += 1;
    }
  }

  return NS_OK;
}

/******************************************************************************
 * mozilla::EventListenerInfo
 ******************************************************************************/

EventListenerInfo::EventListenerInfo(
    EventListenerManager* aListenerManager, const nsAString& aType,
    JS::Handle<JSObject*> aScriptedListener,
    JS::Handle<JSObject*> aScriptedListenerGlobal, bool aCapturing,
    bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler)
    : mListenerManager(aListenerManager),
      mType(aType),
      mScriptedListener(aScriptedListener),
      mScriptedListenerGlobal(aScriptedListenerGlobal),
      mCapturing(aCapturing),
      mAllowsUntrusted(aAllowsUntrusted),
      mInSystemEventGroup(aInSystemEventGroup),
      mIsHandler(aIsHandler) {
  if (aScriptedListener) {
    MOZ_ASSERT(JS_IsGlobalObject(aScriptedListenerGlobal));
    js::AssertSameCompartment(aScriptedListener, aScriptedListenerGlobal);
  }

  HoldJSObjects(this);
}

EventListenerInfo::~EventListenerInfo() { DropJSObjects(this); }

NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(EventListenerInfo, (mListenerManager),
                                         (mScriptedListener,
                                          mScriptedListenerGlobal))

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventListenerInfo)
  NS_INTERFACE_MAP_ENTRY(nsIEventListenerInfo)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(EventListenerInfo)
NS_IMPL_CYCLE_COLLECTING_RELEASE(EventListenerInfo)

NS_IMETHODIMP
EventListenerInfo::GetType(nsAString& aType) {
  aType = mType;
  return NS_OK;
}

NS_IMETHODIMP
EventListenerInfo::GetCapturing(bool* aCapturing) {
  *aCapturing = mCapturing;
  return NS_OK;
}

NS_IMETHODIMP
EventListenerInfo::GetAllowsUntrusted(bool* aAllowsUntrusted) {
  *aAllowsUntrusted = mAllowsUntrusted;
  return NS_OK;
}

NS_IMETHODIMP
EventListenerInfo::GetInSystemEventGroup(bool* aInSystemEventGroup) {
  *aInSystemEventGroup = mInSystemEventGroup;
  return NS_OK;
}

NS_IMETHODIMP
EventListenerInfo::GetEnabled(bool* aEnabled) {
  NS_ENSURE_STATE(mListenerManager);
  return mListenerManager->IsListenerEnabled(
      mType, mScriptedListener, mCapturing, mAllowsUntrusted,
      mInSystemEventGroup, mIsHandler, aEnabled);
}

NS_IMETHODIMP
EventListenerInfo::SetEnabled(bool aEnabled) {
  NS_ENSURE_STATE(mListenerManager);
  return mListenerManager->SetListenerEnabled(
      mType, mScriptedListener, mCapturing, mAllowsUntrusted,
      mInSystemEventGroup, mIsHandler, aEnabled);
}

NS_IMETHODIMP
EventListenerInfo::GetListenerObject(JSContext* aCx,
                                     JS::MutableHandle<JS::Value> aObject) {
  Maybe<JSAutoRealm> ar;
  GetJSVal(aCx, ar, aObject);
  return NS_OK;
}

/******************************************************************************
 * mozilla::EventListenerService
 ******************************************************************************/

NS_IMPL_ISUPPORTS(EventListenerService, nsIEventListenerService)

bool EventListenerInfo::GetJSVal(JSContext* aCx, Maybe<JSAutoRealm>& aAr,
                                 JS::MutableHandle<JS::Value> aJSVal) {
  if (mScriptedListener) {
    aJSVal.setObject(*mScriptedListener);
    aAr.emplace(aCx, mScriptedListenerGlobal);
    return true;
  }

  aJSVal.setNull();
  return false;
}

NS_IMETHODIMP
EventListenerInfo::ToSource(nsAString& aResult) {
  aResult.SetIsVoid(true);

  AutoSafeJSContext cx;
  Maybe<JSAutoRealm> ar;
  JS::Rooted<JS::Value> v(cx);
  if (GetJSVal(cx, ar, &v)) {
    JSString* str = JS_ValueToSource(cx, v);
    if (str) {
      nsAutoJSString autoStr;
      if (autoStr.init(cx, str)) {
        aResult.Assign(autoStr);
      }
    }
  }
  return NS_OK;
}

EventListenerService* EventListenerService::sInstance = nullptr;

EventListenerService::EventListenerService() {
  MOZ_ASSERT(!sInstance);
  sInstance = this;
}

EventListenerService::~EventListenerService() {
  MOZ_ASSERT(sInstance == this);
  sInstance = nullptr;
}

NS_IMETHODIMP
EventListenerService::GetListenerInfoFor(
    EventTarget* aEventTarget,
    nsTArray<RefPtr<nsIEventListenerInfo>>& aOutArray) {
  NS_ENSURE_ARG_POINTER(aEventTarget);

  EventListenerManager* elm = aEventTarget->GetExistingListenerManager();
  if (elm) {
    elm->GetListenerInfo(aOutArray);
  }

  return NS_OK;
}

NS_IMETHODIMP
EventListenerService::HasListenersFor(EventTarget* aEventTarget,
                                      const nsAString& aType, bool* aRetVal) {
  NS_ENSURE_TRUE(aEventTarget, NS_ERROR_UNEXPECTED);

  EventListenerManager* elm = aEventTarget->GetExistingListenerManager();
  *aRetVal = elm && elm->HasListenersFor(aType);
  return NS_OK;
}

static already_AddRefed<EventListener> ToEventListener(
    JSContext* aCx, JS::Handle<JS::Value> aValue) {
  if (NS_WARN_IF(!aValue.isObject())) {
    return nullptr;
  }

  JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
  JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
  RefPtr<EventListener> listener =
      new EventListener(aCx, obj, global, GetIncumbentGlobal());
  return listener.forget();
}

NS_IMETHODIMP
EventListenerService::AddListenerForAllEvents(
    EventTarget* aTarget, JS::Handle<JS::Value> aListener, bool aUseCapture,
    bool aWantsUntrusted, bool aSystemEventGroup, JSContext* aCx) {
  NS_ENSURE_STATE(aTarget);

  RefPtr<EventListener> listener = ToEventListener(aCx, aListener);
  if (!listener) {
    return NS_ERROR_UNEXPECTED;
  }

  EventListenerManager* manager = aTarget->GetOrCreateListenerManager();
  NS_ENSURE_STATE(manager);
  manager->AddListenerForAllEvents(listener, aUseCapture, aWantsUntrusted,
                                   aSystemEventGroup);
  return NS_OK;
}

NS_IMETHODIMP
EventListenerService::RemoveListenerForAllEvents(
    EventTarget* aTarget, JS::Handle<JS::Value> aListener, bool aUseCapture,
    bool aSystemEventGroup, JSContext* aCx) {
  NS_ENSURE_STATE(aTarget);

  RefPtr<EventListener> listener = ToEventListener(aCx, aListener);
  if (!listener) {
    return NS_ERROR_UNEXPECTED;
  }

  EventListenerManager* manager = aTarget->GetExistingListenerManager();
  if (manager) {
    manager->RemoveListenerForAllEvents(listener, aUseCapture,
                                        aSystemEventGroup);
  }
  return NS_OK;
}

NS_IMETHODIMP
EventListenerService::AddListenerChangeListener(
    nsIListenerChangeListener* aListener) {
  if (!mChangeListeners.Contains(aListener)) {
    mChangeListeners.AppendElement(aListener);
  }
  return NS_OK;
};

NS_IMETHODIMP
EventListenerService::RemoveListenerChangeListener(
    nsIListenerChangeListener* aListener) {
  mChangeListeners.RemoveElement(aListener);
  return NS_OK;
};

void EventListenerService::NotifyAboutMainThreadListenerChangeInternal(
    dom::EventTarget* aTarget, nsAtom* aName) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aTarget);
  if (mChangeListeners.IsEmpty()) {
    return;
  }

  if (!mPendingListenerChanges) {
    mPendingListenerChanges = nsArrayBase::Create();
    nsCOMPtr<nsIRunnable> runnable =
        NewRunnableMethod("EventListenerService::NotifyPendingChanges", this,
                          &EventListenerService::NotifyPendingChanges);
    NS_DispatchToCurrentThread(runnable.forget());
  }

  RefPtr<EventListenerChange> changes =
      mPendingListenerChangesSet.LookupOrInsertWith(aTarget, [&] {
        auto c = MakeRefPtr<EventListenerChange>(aTarget);
        mPendingListenerChanges->AppendElement(c);
        return c;
      });
  changes->AddChangedListenerName(aName);
}

void EventListenerService::NotifyPendingChanges() {
  nsCOMPtr<nsIMutableArray> changes;
  mPendingListenerChanges.swap(changes);
  mPendingListenerChangesSet.Clear();

  for (auto const& listener : mChangeListeners.EndLimitedRange()) {
    nsCOMPtr<nsIListenerChangeListener>{listener}->ListenersChanged(changes);
  }
}

}  // namespace mozilla

nsresult NS_NewEventListenerService(nsIEventListenerService** aResult) {
  *aResult = new mozilla::EventListenerService();
  NS_ADDREF(*aResult);
  return NS_OK;
}
