/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=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 "DisplayDeviceProvider.h"

#include "DeviceProviderHelpers.h"
#include "mozilla/Logging.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include "nsIObserverService.h"
#include "nsIServiceManager.h"
#include "nsIWindowWatcher.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsSimpleURI.h"
#include "nsTCPDeviceInfo.h"
#include "nsThreadUtils.h"

static mozilla::LazyLogModule gDisplayDeviceProviderLog("DisplayDeviceProvider");

#define LOG(format) MOZ_LOG(gDisplayDeviceProviderLog, mozilla::LogLevel::Debug, format)

#define DISPLAY_CHANGED_NOTIFICATION "display-changed"
#define DEFAULT_CHROME_FEATURES_PREF "toolkit.defaultChromeFeatures"
#define CHROME_REMOTE_URL_PREF       "b2g.multiscreen.chrome_remote_url"
#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS "dom.presentation.discoverable.retry_ms"

namespace mozilla {
namespace dom {
namespace presentation {

/**
 * This wrapper is used to break circular-reference problem.
 */
class DisplayDeviceProviderWrappedListener final
  : public nsIPresentationControlServerListener
{
public:
  NS_DECL_ISUPPORTS
  NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener)

  explicit DisplayDeviceProviderWrappedListener() = default;

  nsresult SetListener(DisplayDeviceProvider* aListener)
  {
    mListener = aListener;
    return NS_OK;
  }

private:
  virtual ~DisplayDeviceProviderWrappedListener() = default;

  DisplayDeviceProvider* mListener = nullptr;
};

NS_IMPL_ISUPPORTS(DisplayDeviceProviderWrappedListener,
                  nsIPresentationControlServerListener)

NS_IMPL_ISUPPORTS(DisplayDeviceProvider::HDMIDisplayDevice,
                  nsIPresentationDevice,
                  nsIPresentationLocalDevice)

// nsIPresentationDevice
NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::GetId(nsACString& aId)
{
  aId = mWindowId;
  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::GetName(nsACString& aName)
{
  aName = mName;
  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::GetType(nsACString& aType)
{
  aType = mType;
  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::GetWindowId(nsACString& aWindowId)
{
  aWindowId = mWindowId;
  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice
                     ::EstablishControlChannel(nsIPresentationControlChannel** aControlChannel)
{
  nsresult rv = OpenTopLevelWindow();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  RefPtr<DisplayDeviceProvider> provider = mProvider.get();
  if (NS_WARN_IF(!provider)) {
    return NS_ERROR_FAILURE;
  }
  return provider->Connect(this, aControlChannel);
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::Disconnect()
{
  nsresult rv = CloseTopLevelWindow();
  if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
  }
  return NS_OK;;
}

NS_IMETHODIMP
DisplayDeviceProvider::HDMIDisplayDevice::IsRequestedUrlSupported(
                                                 const nsAString& aRequestedUrl,
                                                 bool* aRetVal)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (!aRetVal) {
    return NS_ERROR_INVALID_POINTER;
  }

  // 1-UA device only supports HTTP/HTTPS hosted receiver page.
  *aRetVal = DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl);

  return NS_OK;
}

nsresult
DisplayDeviceProvider::HDMIDisplayDevice::OpenTopLevelWindow()
{
  MOZ_ASSERT(!mWindow);

  nsresult rv;
  nsAutoCString flags(Preferences::GetCString(DEFAULT_CHROME_FEATURES_PREF));
  if (flags.IsEmpty()) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  flags.AppendLiteral(",mozDisplayId=");
  flags.AppendInt(mScreenId);

  nsAutoCString remoteShellURLString(Preferences::GetCString(CHROME_REMOTE_URL_PREF));
  remoteShellURLString.AppendLiteral("#");
  remoteShellURLString.Append(mWindowId);

  // URI validation
  nsCOMPtr<nsIURI> remoteShellURL;
  rv = NS_NewURI(getter_AddRefs(remoteShellURL), remoteShellURLString);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = remoteShellURL->GetSpec(remoteShellURLString);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID);
  MOZ_ASSERT(ww);

  rv = ww->OpenWindow(nullptr,
                      remoteShellURLString.get(),
                      "_blank",
                      flags.get(),
                      nullptr,
                      getter_AddRefs(mWindow));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult
DisplayDeviceProvider::HDMIDisplayDevice::CloseTopLevelWindow()
{
  MOZ_ASSERT(mWindow);

  nsCOMPtr<nsPIDOMWindowOuter> piWindow = nsPIDOMWindowOuter::From(mWindow);
  nsresult rv = piWindow->Close();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

NS_IMPL_ISUPPORTS(DisplayDeviceProvider,
                  nsIObserver,
                  nsIPresentationDeviceProvider,
                  nsIPresentationControlServerListener)

DisplayDeviceProvider::~DisplayDeviceProvider()
{
  Uninit();
}

nsresult
DisplayDeviceProvider::Init()
{
  // Provider must be initialized only once.
  if (mInitialized) {
    return NS_OK;
  }

  nsresult rv;

  mServerRetryMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS);
  mServerRetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  MOZ_ASSERT(obs);

  obs->AddObserver(this, DISPLAY_CHANGED_NOTIFICATION, false);

  mDevice = new HDMIDisplayDevice(this);

  mWrappedListener = new DisplayDeviceProviderWrappedListener();
  rv = mWrappedListener->SetListener(this);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mPresentationService = do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID,
                                           &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = StartTCPService();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mInitialized = true;
  return NS_OK;
}

nsresult
DisplayDeviceProvider::Uninit()
{
  // Provider must be deleted only once.
  if (!mInitialized) {
    return NS_OK;
  }

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->RemoveObserver(this, DISPLAY_CHANGED_NOTIFICATION);
  }

  // Remove device from device manager when the provider is uninit
  RemoveExternalScreen();

  AbortServerRetry();

  mInitialized = false;
  mWrappedListener->SetListener(nullptr);
  return NS_OK;
}

nsresult
DisplayDeviceProvider::StartTCPService()
{
  MOZ_ASSERT(NS_IsMainThread());

  nsresult rv;
  rv =  mPresentationService->SetId(NS_LITERAL_CSTRING("DisplayDeviceProvider"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  uint16_t servicePort;
  rv = mPresentationService->GetPort(&servicePort);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  /*
   * If |servicePort| is non-zero, it means PresentationServer is running.
   * Otherwise, we should make it start serving.
   */
  if (servicePort) {
    mPort = servicePort;
    return NS_OK;
  }

  rv = mPresentationService->SetListener(mWrappedListener);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  AbortServerRetry();

  // 1-UA doesn't need encryption.
  rv = mPresentationService->StartServer(/* aEncrypted = */ false,
                                         /* aPort = */ 0);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

void
DisplayDeviceProvider::AbortServerRetry()
{
  if (mIsServerRetrying) {
    mIsServerRetrying = false;
    mServerRetryTimer->Cancel();
  }
}

nsresult
DisplayDeviceProvider::AddExternalScreen()
{
  MOZ_ASSERT(mDeviceListener);

  nsresult rv;
  nsCOMPtr<nsIPresentationDeviceListener> listener;
  rv = GetListener(getter_AddRefs(listener));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = listener->AddDevice(mDevice);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult
DisplayDeviceProvider::RemoveExternalScreen()
{
  MOZ_ASSERT(mDeviceListener);

  nsresult rv;
  nsCOMPtr<nsIPresentationDeviceListener> listener;
  rv = GetListener(getter_AddRefs(listener));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = listener->RemoveDevice(mDevice);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mDevice->Disconnect();
  return NS_OK;
}

// nsIPresentationDeviceProvider
NS_IMETHODIMP
DisplayDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener)
{
  if (NS_WARN_IF(!aListener)) {
    return NS_ERROR_INVALID_POINTER;
  }

  nsresult rv;
  nsCOMPtr<nsIPresentationDeviceListener> listener =
    do_QueryReferent(mDeviceListener, &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
  }

  listener.forget(aListener);

  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener)
{
  mDeviceListener = do_GetWeakReference(aListener);
  nsresult rv = mDeviceListener ? Init() : Uninit();
  if(NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::ForceDiscovery()
{
  return NS_OK;
}

// nsIPresentationControlServerListener
NS_IMETHODIMP
DisplayDeviceProvider::OnServerReady(uint16_t aPort,
                                     const nsACString& aCertFingerprint)
{
  MOZ_ASSERT(NS_IsMainThread());
  mPort = aPort;

  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::OnServerStopped(nsresult aResult)
{
  MOZ_ASSERT(NS_IsMainThread());

  // Try restart server if it is stopped abnormally.
  if (NS_FAILED(aResult)) {
    mIsServerRetrying = true;
    mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT);
  }

  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::OnSessionRequest(nsITCPDeviceInfo* aDeviceInfo,
                                      const nsAString& aUrl,
                                      const nsAString& aPresentationId,
                                      nsIPresentationControlChannel* aControlChannel)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aDeviceInfo);
  MOZ_ASSERT(aControlChannel);

  nsresult rv;

  nsCOMPtr<nsIPresentationDeviceListener> listener;
  rv = GetListener(getter_AddRefs(listener));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  MOZ_ASSERT(!listener);

  rv = listener->OnSessionRequest(mDevice,
                                  aUrl,
                                  aPresentationId,
                                  aControlChannel);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
                                          const nsAString& aPresentationId,
                                          nsIPresentationControlChannel* aControlChannel,
                                          bool aIsFromReceiver)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aDeviceInfo);
  MOZ_ASSERT(aControlChannel);

  nsresult rv;

  nsCOMPtr<nsIPresentationDeviceListener> listener;
  rv = GetListener(getter_AddRefs(listener));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  MOZ_ASSERT(!listener);

  rv = listener->OnTerminateRequest(mDevice,
                                    aPresentationId,
                                    aControlChannel,
                                    aIsFromReceiver);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

NS_IMETHODIMP
DisplayDeviceProvider::OnReconnectRequest(nsITCPDeviceInfo* aDeviceInfo,
                                          const nsAString& aUrl,
                                          const nsAString& aPresentationId,
                                          nsIPresentationControlChannel* aControlChannel)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aDeviceInfo);
  MOZ_ASSERT(aControlChannel);

  nsresult rv;

  nsCOMPtr<nsIPresentationDeviceListener> listener;
  rv = GetListener(getter_AddRefs(listener));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  MOZ_ASSERT(!listener);

  rv = listener->OnReconnectRequest(mDevice,
                                    aUrl,
                                    aPresentationId,
                                    aControlChannel);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

// nsIObserver
NS_IMETHODIMP
DisplayDeviceProvider::Observe(nsISupports* aSubject,
                               const char* aTopic,
                               const char16_t* aData)
{
  if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) {
    nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
    MOZ_ASSERT(displayInfo);

    int32_t type;
    bool isConnected;
    displayInfo->GetConnected(&isConnected);
    // XXX The ID is as same as the type of display.
    // See Bug 1138287 and nsScreenManagerGonk::AddScreen() for more detail.
    displayInfo->GetId(&type);

    if (type == DisplayType::DISPLAY_EXTERNAL) {
      nsresult rv = isConnected ? AddExternalScreen() : RemoveExternalScreen();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
  } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
    nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject);
    if (!timer) {
      return NS_ERROR_UNEXPECTED;
    }

    if (timer == mServerRetryTimer) {
      mIsServerRetrying = false;
      StartTCPService();
    }
  }

  return NS_OK;
}

nsresult
DisplayDeviceProvider::Connect(HDMIDisplayDevice* aDevice,
                               nsIPresentationControlChannel** aControlChannel)
{
  MOZ_ASSERT(aDevice);
  MOZ_ASSERT(mPresentationService);
  NS_ENSURE_ARG_POINTER(aControlChannel);
  *aControlChannel = nullptr;

  nsCOMPtr<nsITCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(),
                                                            aDevice->Address(),
                                                            mPort,
                                                            EmptyCString());

  return mPresentationService->Connect(deviceInfo, aControlChannel);
}

} // namespace presentation
} // namespace dom
} // namespace mozilla