/* -*- 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/. */

#ifndef mozilla_dom_PresentationServiceBase_h
#define mozilla_dom_PresentationServiceBase_h

#include "mozilla/Unused.h"
#include "nsClassHashtable.h"
#include "nsCOMArray.h"
#include "nsIPresentationListener.h"
#include "nsIPresentationService.h"
#include "nsRefPtrHashtable.h"
#include "nsString.h"
#include "nsTArray.h"

namespace mozilla {
namespace dom {

template<class T>
class PresentationServiceBase
{
public:
  PresentationServiceBase() = default;

  already_AddRefed<T>
  GetSessionInfo(const nsAString& aSessionId, const uint8_t aRole)
  {
    MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
               aRole == nsIPresentationService::ROLE_RECEIVER);

    RefPtr<T> info;
    if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
      return mSessionInfoAtController.Get(aSessionId, getter_AddRefs(info)) ?
             info.forget() : nullptr;
    } else {
      return mSessionInfoAtReceiver.Get(aSessionId, getter_AddRefs(info)) ?
             info.forget() : nullptr;
    }
  }

protected:
  class SessionIdManager final
  {
  public:
    explicit SessionIdManager()
    {
      MOZ_COUNT_CTOR(SessionIdManager);
    }

    ~SessionIdManager()
    {
      MOZ_COUNT_DTOR(SessionIdManager);
    }

    nsresult GetWindowId(const nsAString& aSessionId, uint64_t* aWindowId)
    {
      MOZ_ASSERT(NS_IsMainThread());

      if (mRespondingWindowIds.Get(aSessionId, aWindowId)) {
        return NS_OK;
      }
      return NS_ERROR_NOT_AVAILABLE;
    }

    nsresult GetSessionIds(uint64_t aWindowId, nsTArray<nsString>& aSessionIds)
    {
      MOZ_ASSERT(NS_IsMainThread());

      nsTArray<nsString>* sessionIdArray;
      if (!mRespondingSessionIds.Get(aWindowId, &sessionIdArray)) {
        return NS_ERROR_INVALID_ARG;
      }

      aSessionIds.Assign(*sessionIdArray);
      return NS_OK;
    }

    void AddSessionId(uint64_t aWindowId, const nsAString& aSessionId)
    {
      MOZ_ASSERT(NS_IsMainThread());

      if (NS_WARN_IF(aWindowId == 0)) {
        return;
      }

      nsTArray<nsString>* sessionIdArray;
      if (!mRespondingSessionIds.Get(aWindowId, &sessionIdArray)) {
        sessionIdArray = new nsTArray<nsString>();
        mRespondingSessionIds.Put(aWindowId, sessionIdArray);
      }

      sessionIdArray->AppendElement(nsString(aSessionId));
      mRespondingWindowIds.Put(aSessionId, aWindowId);
    }

    void RemoveSessionId(const nsAString& aSessionId)
    {
      MOZ_ASSERT(NS_IsMainThread());

      uint64_t windowId = 0;
      if (mRespondingWindowIds.Get(aSessionId, &windowId)) {
        mRespondingWindowIds.Remove(aSessionId);
        nsTArray<nsString>* sessionIdArray;
        if (mRespondingSessionIds.Get(windowId, &sessionIdArray)) {
          sessionIdArray->RemoveElement(nsString(aSessionId));
          if (sessionIdArray->IsEmpty()) {
            mRespondingSessionIds.Remove(windowId);
          }
        }
      }
    }

    nsresult UpdateWindowId(const nsAString& aSessionId, const uint64_t aWindowId)
    {
      MOZ_ASSERT(NS_IsMainThread());

      RemoveSessionId(aSessionId);
      AddSessionId(aWindowId, aSessionId);
      return NS_OK;
    }

    void Clear()
    {
      mRespondingSessionIds.Clear();
      mRespondingWindowIds.Clear();
    }

  private:
    nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mRespondingSessionIds;
    nsDataHashtable<nsStringHashKey, uint64_t> mRespondingWindowIds;
  };

  class AvailabilityManager final
  {
  public:
    explicit AvailabilityManager()
    {
      MOZ_COUNT_CTOR(AvailabilityManager);
    }

    ~AvailabilityManager()
    {
      MOZ_COUNT_DTOR(AvailabilityManager);
    }

    void AddAvailabilityListener(
                               const nsTArray<nsString>& aAvailabilityUrls,
                               nsIPresentationAvailabilityListener* aListener)
    {
      nsTArray<nsString> dummy;
      AddAvailabilityListener(aAvailabilityUrls, aListener, dummy);
    }

    void AddAvailabilityListener(
                               const nsTArray<nsString>& aAvailabilityUrls,
                               nsIPresentationAvailabilityListener* aListener,
                               nsTArray<nsString>& aAddedUrls)
    {
      if (!aListener) {
        MOZ_ASSERT(false, "aListener should not be null.");
        return;
      }

      if (aAvailabilityUrls.IsEmpty()) {
        MOZ_ASSERT(false, "aAvailabilityUrls should not be empty.");
        return;
      }

      aAddedUrls.Clear();
      nsTArray<nsString> knownAvailableUrls;
      for (const auto& url : aAvailabilityUrls) {
        AvailabilityEntry* entry;
        if (!mAvailabilityUrlTable.Get(url, &entry)) {
          entry = new AvailabilityEntry();
          mAvailabilityUrlTable.Put(url, entry);
          aAddedUrls.AppendElement(url);
        }
        if (!entry->mListeners.Contains(aListener)) {
          entry->mListeners.AppendElement(aListener);
        }
        if (entry->mAvailable) {
          knownAvailableUrls.AppendElement(url);
        }
      }

      if (!knownAvailableUrls.IsEmpty()) {
        Unused <<
          NS_WARN_IF(
            NS_FAILED(aListener->NotifyAvailableChange(knownAvailableUrls,
                                                       true)));
      } else {
        // If we can't find any known available url and there is no newly
        // added url, we still need to notify the listener of the result.
        // So, the promise returned by |getAvailability| can be resolved.
        if (aAddedUrls.IsEmpty()) {
          Unused <<
            NS_WARN_IF(
              NS_FAILED(aListener->NotifyAvailableChange(aAvailabilityUrls,
                                                         false)));
        }
      }
    }

    void RemoveAvailabilityListener(
                               const nsTArray<nsString>& aAvailabilityUrls,
                               nsIPresentationAvailabilityListener* aListener)
    {
      nsTArray<nsString> dummy;
      RemoveAvailabilityListener(aAvailabilityUrls, aListener, dummy);
    }

    void RemoveAvailabilityListener(
                               const nsTArray<nsString>& aAvailabilityUrls,
                               nsIPresentationAvailabilityListener* aListener,
                               nsTArray<nsString>& aRemovedUrls)
    {
      if (!aListener) {
        MOZ_ASSERT(false, "aListener should not be null.");
        return;
      }

      if (aAvailabilityUrls.IsEmpty()) {
        MOZ_ASSERT(false, "aAvailabilityUrls should not be empty.");
        return;
      }

      aRemovedUrls.Clear();
      for (const auto& url : aAvailabilityUrls) {
        AvailabilityEntry* entry;
        if (mAvailabilityUrlTable.Get(url, &entry)) {
          entry->mListeners.RemoveElement(aListener);
          if (entry->mListeners.IsEmpty()) {
            mAvailabilityUrlTable.Remove(url);
            aRemovedUrls.AppendElement(url);
          }
        }
      }
    }

    nsresult DoNotifyAvailableChange(const nsTArray<nsString>& aAvailabilityUrls,
                                     bool aAvailable)
    {
      typedef nsClassHashtable<nsISupportsHashKey,
                               nsTArray<nsString>> ListenerToUrlsMap;
      ListenerToUrlsMap availabilityListenerTable;
      // Create a mapping from nsIPresentationAvailabilityListener to
      // availabilityUrls.
      for (auto it = mAvailabilityUrlTable.ConstIter(); !it.Done(); it.Next()) {
        if (aAvailabilityUrls.Contains(it.Key())) {
          AvailabilityEntry* entry = it.UserData();
          entry->mAvailable = aAvailable;

          for (uint32_t i = 0; i < entry->mListeners.Length(); ++i) {
            nsIPresentationAvailabilityListener* listener =
              entry->mListeners.ObjectAt(i);
            nsTArray<nsString>* urlArray;
            if (!availabilityListenerTable.Get(listener, &urlArray)) {
              urlArray = new nsTArray<nsString>();
              availabilityListenerTable.Put(listener, urlArray);
            }
            urlArray->AppendElement(it.Key());
          }
        }
      }

      for (auto it = availabilityListenerTable.Iter(); !it.Done(); it.Next()) {
        auto listener =
          static_cast<nsIPresentationAvailabilityListener*>(it.Key());

        Unused <<
          NS_WARN_IF(NS_FAILED(listener->NotifyAvailableChange(*it.UserData(),
                                                               aAvailable)));
      }
      return NS_OK;
    }

    void GetAvailbilityUrlByAvailability(nsTArray<nsString>& aOutArray,
                                         bool aAvailable)
    {
      aOutArray.Clear();

      for (auto it = mAvailabilityUrlTable.ConstIter(); !it.Done(); it.Next()) {
        if (it.UserData()->mAvailable == aAvailable) {
          aOutArray.AppendElement(it.Key());
        }
      }
    }

    void Clear()
    {
      mAvailabilityUrlTable.Clear();
    }

  private:
    struct AvailabilityEntry
    {
      explicit AvailabilityEntry()
        : mAvailable(false)
      {}

      bool mAvailable;
      nsCOMArray<nsIPresentationAvailabilityListener> mListeners;
    };

    nsClassHashtable<nsStringHashKey, AvailabilityEntry> mAvailabilityUrlTable;
  };

  virtual ~PresentationServiceBase() = default;

  void Shutdown()
  {
    mRespondingListeners.Clear();
    mControllerSessionIdManager.Clear();
    mReceiverSessionIdManager.Clear();
  }

  nsresult GetWindowIdBySessionIdInternal(const nsAString& aSessionId,
                                          uint8_t aRole,
                                          uint64_t* aWindowId)
  {
    MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
               aRole == nsIPresentationService::ROLE_RECEIVER);

    if (NS_WARN_IF(!aWindowId)) {
      return NS_ERROR_INVALID_POINTER;
    }

    if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
      return mControllerSessionIdManager.GetWindowId(aSessionId, aWindowId);
    }

    return mReceiverSessionIdManager.GetWindowId(aSessionId, aWindowId);
  }

  void AddRespondingSessionId(uint64_t aWindowId,
                              const nsAString& aSessionId,
                              uint8_t aRole)
  {
    MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
               aRole == nsIPresentationService::ROLE_RECEIVER);

    if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
      mControllerSessionIdManager.AddSessionId(aWindowId, aSessionId);
    } else {
      mReceiverSessionIdManager.AddSessionId(aWindowId, aSessionId);
    }
  }

  void RemoveRespondingSessionId(const nsAString& aSessionId,
                                 uint8_t aRole)
  {
    MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
               aRole == nsIPresentationService::ROLE_RECEIVER);

    if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
      mControllerSessionIdManager.RemoveSessionId(aSessionId);
    } else {
      mReceiverSessionIdManager.RemoveSessionId(aSessionId);
    }
  }

  nsresult UpdateWindowIdBySessionIdInternal(const nsAString& aSessionId,
                                             uint8_t aRole,
                                             const uint64_t aWindowId)
  {
    MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
               aRole == nsIPresentationService::ROLE_RECEIVER);

    if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
      return mControllerSessionIdManager.UpdateWindowId(aSessionId, aWindowId);
    }

    return mReceiverSessionIdManager.UpdateWindowId(aSessionId, aWindowId);
  }

  // Store the responding listener based on the window ID of the (in-process or
  // OOP) receiver page.
  nsRefPtrHashtable<nsUint64HashKey, nsIPresentationRespondingListener>
  mRespondingListeners;

  // Store the mapping between the window ID of the in-process and OOP page and the ID
  // of the responding session. It's used for both controller and receiver page
  // to retrieve the correspondent session ID. Besides, also keep the mapping
  // between the responding session ID and the window ID to help look up the
  // window ID.
  SessionIdManager mControllerSessionIdManager;
  SessionIdManager mReceiverSessionIdManager;

  nsRefPtrHashtable<nsStringHashKey, T> mSessionInfoAtController;
  nsRefPtrHashtable<nsStringHashKey, T> mSessionInfoAtReceiver;

  AvailabilityManager mAvailabilityManager;
};

} // namespace dom
} // namespace mozilla

#endif // mozilla_dom_PresentationServiceBase_h
