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

#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "nsIObserverService.h"
#include "nsISupportsPrimitives.h"
#include "nsSocketTransportService2.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"

namespace mozilla::dom {

namespace {

StaticRefPtr<EventSourceEventService> gEventSourceEventService;

}  // anonymous namespace

class EventSourceBaseRunnable : public Runnable {
 public:
  EventSourceBaseRunnable(uint64_t aHttpChannelId, uint64_t aInnerWindowID)
      : Runnable("dom::EventSourceBaseRunnable"),
        mHttpChannelId(aHttpChannelId),
        mInnerWindowID(aInnerWindowID) {}

  NS_IMETHOD Run() override {
    MOZ_ASSERT(NS_IsMainThread());
    RefPtr<EventSourceEventService> service =
        EventSourceEventService::GetOrCreate();
    MOZ_ASSERT(service);

    EventSourceEventService::EventSourceListeners listeners;

    service->GetListeners(mInnerWindowID, listeners);

    for (uint32_t i = 0; i < listeners.Length(); ++i) {
      DoWork(listeners[i]);
    }

    return NS_OK;
  }

 protected:
  ~EventSourceBaseRunnable() = default;

  virtual void DoWork(nsIEventSourceEventListener* aListener) = 0;

  uint64_t mHttpChannelId;
  uint64_t mInnerWindowID;
};

class EventSourceConnectionOpenedRunnable final
    : public EventSourceBaseRunnable {
 public:
  EventSourceConnectionOpenedRunnable(uint64_t aHttpChannelId,
                                      uint64_t aInnerWindowID)
      : EventSourceBaseRunnable(aHttpChannelId, aInnerWindowID) {}

 private:
  virtual void DoWork(nsIEventSourceEventListener* aListener) override {
    DebugOnly<nsresult> rv =
        aListener->EventSourceConnectionOpened(mHttpChannelId);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EventSourceConnectionOpened failed");
  }
};

class EventSourceConnectionClosedRunnable final
    : public EventSourceBaseRunnable {
 public:
  EventSourceConnectionClosedRunnable(uint64_t aHttpChannelId,
                                      uint64_t aInnerWindowID)
      : EventSourceBaseRunnable(aHttpChannelId, aInnerWindowID) {}

 private:
  virtual void DoWork(nsIEventSourceEventListener* aListener) override {
    DebugOnly<nsresult> rv =
        aListener->EventSourceConnectionClosed(mHttpChannelId);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EventSourceConnectionClosed failed");
  }
};

class EventSourceEventRunnable final : public EventSourceBaseRunnable {
 public:
  EventSourceEventRunnable(uint64_t aHttpChannelId, uint64_t aInnerWindowID,
                           const nsAString& aEventName,
                           const nsAString& aLastEventID,
                           const nsAString& aData, uint32_t aRetry,
                           DOMHighResTimeStamp aTimeStamp)
      : EventSourceBaseRunnable(aHttpChannelId, aInnerWindowID),
        mEventName(aEventName),
        mLastEventID(aLastEventID),
        mData(aData),
        mRetry(aRetry),
        mTimeStamp(aTimeStamp) {}

 private:
  virtual void DoWork(nsIEventSourceEventListener* aListener) override {
    DebugOnly<nsresult> rv = aListener->EventReceived(
        mHttpChannelId, mEventName, mLastEventID, mData, mRetry, mTimeStamp);

    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Event op failed");
  }

  nsString mEventName;
  nsString mLastEventID;
  nsString mData;
  uint32_t mRetry;
  DOMHighResTimeStamp mTimeStamp;
};

/* static */
already_AddRefed<EventSourceEventService>
EventSourceEventService::GetOrCreate() {
  MOZ_ASSERT(NS_IsMainThread());

  if (!gEventSourceEventService) {
    gEventSourceEventService = new EventSourceEventService();
  }

  RefPtr<EventSourceEventService> service = gEventSourceEventService.get();
  return service.forget();
}

NS_INTERFACE_MAP_BEGIN(EventSourceEventService)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEventSourceEventService)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsIEventSourceEventService)
NS_INTERFACE_MAP_END

NS_IMPL_ADDREF(EventSourceEventService)
NS_IMPL_RELEASE(EventSourceEventService)

EventSourceEventService::EventSourceEventService() : mCountListeners(0) {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->AddObserver(this, "xpcom-shutdown", false);
    obs->AddObserver(this, "inner-window-destroyed", false);
  }
}

EventSourceEventService::~EventSourceEventService() {
  MOZ_ASSERT(NS_IsMainThread());
}

void EventSourceEventService::EventSourceConnectionOpened(
    uint64_t aHttpChannelId, uint64_t aInnerWindowID) {
  // Let's continue only if we have some listeners.
  if (!HasListeners()) {
    return;
  }

  RefPtr<EventSourceConnectionOpenedRunnable> runnable =
      new EventSourceConnectionOpenedRunnable(aHttpChannelId, aInnerWindowID);
  DebugOnly<nsresult> rv = NS_DispatchToMainThread(runnable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
}

void EventSourceEventService::EventSourceConnectionClosed(
    uint64_t aHttpChannelId, uint64_t aInnerWindowID) {
  // Let's continue only if we have some listeners.
  if (!HasListeners()) {
    return;
  }
  RefPtr<EventSourceConnectionClosedRunnable> runnable =
      new EventSourceConnectionClosedRunnable(aHttpChannelId, aInnerWindowID);
  DebugOnly<nsresult> rv = NS_DispatchToMainThread(runnable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
}

void EventSourceEventService::EventReceived(
    uint64_t aHttpChannelId, uint64_t aInnerWindowID,
    const nsAString& aEventName, const nsAString& aLastEventID,
    const nsAString& aData, uint32_t aRetry, DOMHighResTimeStamp aTimeStamp) {
  // Let's continue only if we have some listeners.
  if (!HasListeners()) {
    return;
  }

  RefPtr<EventSourceEventRunnable> runnable =
      new EventSourceEventRunnable(aHttpChannelId, aInnerWindowID, aEventName,
                                   aLastEventID, aData, aRetry, aTimeStamp);
  DebugOnly<nsresult> rv = NS_DispatchToMainThread(runnable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
}

NS_IMETHODIMP
EventSourceEventService::AddListener(uint64_t aInnerWindowID,
                                     nsIEventSourceEventListener* aListener) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!aListener) {
    return NS_ERROR_FAILURE;
  }
  ++mCountListeners;

  WindowListener* listener = mWindows.GetOrInsertNew(aInnerWindowID);

  listener->mListeners.AppendElement(aListener);

  return NS_OK;
}

NS_IMETHODIMP
EventSourceEventService::RemoveListener(
    uint64_t aInnerWindowID, nsIEventSourceEventListener* aListener) {
  MOZ_ASSERT(NS_IsMainThread());

  if (!aListener) {
    return NS_ERROR_FAILURE;
  }

  WindowListener* listener = mWindows.Get(aInnerWindowID);
  if (!listener) {
    return NS_ERROR_FAILURE;
  }

  if (!listener->mListeners.RemoveElement(aListener)) {
    return NS_ERROR_FAILURE;
  }

  // The last listener for this window.
  if (listener->mListeners.IsEmpty()) {
    mWindows.Remove(aInnerWindowID);
  }

  MOZ_ASSERT(mCountListeners);
  --mCountListeners;

  return NS_OK;
}

NS_IMETHODIMP
EventSourceEventService::HasListenerFor(uint64_t aInnerWindowID,
                                        bool* aResult) {
  MOZ_ASSERT(NS_IsMainThread());

  *aResult = mWindows.Get(aInnerWindowID);

  return NS_OK;
}

NS_IMETHODIMP
EventSourceEventService::Observe(nsISupports* aSubject, const char* aTopic,
                                 const char16_t* aData) {
  MOZ_ASSERT(NS_IsMainThread());

  if (!strcmp(aTopic, "xpcom-shutdown")) {
    Shutdown();
    return NS_OK;
  }

  if (!strcmp(aTopic, "inner-window-destroyed") && HasListeners()) {
    nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
    NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);

    uint64_t innerID;
    nsresult rv = wrapper->GetData(&innerID);
    NS_ENSURE_SUCCESS(rv, rv);

    WindowListener* listener = mWindows.Get(innerID);
    if (!listener) {
      return NS_OK;
    }

    MOZ_ASSERT(mCountListeners >= listener->mListeners.Length());
    mCountListeners -= listener->mListeners.Length();
    mWindows.Remove(innerID);
  }

  // This should not happen.
  return NS_ERROR_FAILURE;
}

void EventSourceEventService::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());

  if (gEventSourceEventService) {
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->RemoveObserver(gEventSourceEventService, "xpcom-shutdown");
      obs->RemoveObserver(gEventSourceEventService, "inner-window-destroyed");
    }

    mWindows.Clear();
    gEventSourceEventService = nullptr;
  }
}

bool EventSourceEventService::HasListeners() const { return !!mCountListeners; }

void EventSourceEventService::GetListeners(
    uint64_t aInnerWindowID,
    EventSourceEventService::EventSourceListeners& aListeners) const {
  aListeners.Clear();

  WindowListener* listener = mWindows.Get(aInnerWindowID);
  if (!listener) {
    return;
  }

  aListeners.AppendElements(listener->mListeners);
}

}  // namespace mozilla::dom
