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

#include <cstddef>
#include <cstring>
#include <map>
#include <memory>

#include "VideoEngine.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include "modules/desktop_capture/desktop_capturer.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/SyncRunnable.h"
#include "nsIBrowserWindowTracker.h"
#include "nsImportModule.h"
#include "nsPrintfCString.h"

using mozilla::camera::CaptureDeviceType;

namespace webrtc {

void DesktopSource::setScreenId(ScreenId aId) { mScreenId = aId; }
void DesktopSource::setName(nsCString&& aName) { mName = std::move(aName); }
void DesktopSource::setUniqueId(nsCString&& aId) { mUniqueId = std::move(aId); }
void DesktopSource::setPid(const int aPid) { mPid = aPid; }

ScreenId DesktopSource::getScreenId() const { return mScreenId; }
const nsCString& DesktopSource::getName() const { return mName; }
const nsCString& DesktopSource::getUniqueId() const { return mUniqueId; }
pid_t DesktopSource::getPid() const { return mPid; }

void TabSource::setBrowserId(uint64_t aId) { mBrowserId = aId; }
void TabSource::setUniqueId(nsCString&& aId) { mUniqueId = std::move(aId); }
void TabSource::setName(nsCString&& aName) { mName = std::move(aName); }

uint64_t TabSource::getBrowserId() const { return mBrowserId; }
const nsCString& TabSource::getName() const { return mName; }
const nsCString& TabSource::getUniqueId() const { return mUniqueId; }

template <CaptureDeviceType Type, typename Device>
class DesktopDeviceInfoImpl : public CaptureInfo<Device> {
 public:
  explicit DesktopDeviceInfoImpl(const DesktopCaptureOptions& aOptions);

  void Refresh() override;
  size_t getSourceCount() const override;
  const Device* getSource(size_t aIndex) const override;

 protected:
  const DesktopCaptureOptions mOptions;
  std::map<intptr_t, Device> mDeviceList;
};

template <CaptureDeviceType Type, typename Device>
DesktopDeviceInfoImpl<Type, Device>::DesktopDeviceInfoImpl(
    const DesktopCaptureOptions& aOptions)
    : mOptions(aOptions) {}

template <CaptureDeviceType Type, typename Device>
size_t DesktopDeviceInfoImpl<Type, Device>::getSourceCount() const {
  return mDeviceList.size();
}

template <CaptureDeviceType Type, typename Device>
const Device* DesktopDeviceInfoImpl<Type, Device>::getSource(
    size_t aIndex) const {
  if (aIndex >= mDeviceList.size()) {
    return nullptr;
  }
  auto it = mDeviceList.begin();
  std::advance(it, aIndex);
  return &std::get<Device>(*it);
}

static std::map<intptr_t, TabSource> InitializeTabList() {
  std::map<intptr_t, TabSource> tabList;
  // This is a sync dispatch to main thread, which is unfortunate. To
  // call JavaScript we have to be on main thread, but the remaining
  // DesktopCapturer very much wants to be off main thread. This might
  // be solvable by calling this method earlier on while we're still on
  // main thread and plumbing the information down to here.
  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(__func__, [&] {
    nsresult rv;
    nsCOMPtr<nsIBrowserWindowTracker> bwt =
        do_ImportESModule("resource:///modules/BrowserWindowTracker.sys.mjs",
                          "BrowserWindowTracker", &rv);
    if (NS_FAILED(rv)) {
      return;
    }

    nsTArray<RefPtr<nsIVisibleTab>> tabArray;
    rv = bwt->GetAllVisibleTabs(tabArray);
    if (NS_FAILED(rv)) {
      return;
    }

    for (const auto& browserTab : tabArray) {
      nsString contentTitle;
      browserTab->GetContentTitle(contentTitle);
      int64_t browserId;
      browserTab->GetBrowserId(&browserId);

      auto result =
          tabList.try_emplace(mozilla::AssertedCast<intptr_t>(browserId));
      auto& [iter, inserted] = result;
      if (!inserted) {
        MOZ_ASSERT_UNREACHABLE("Duplicate browser ids");
        continue;
      }
      auto& [key, desktopTab] = *iter;
      desktopTab.setBrowserId(browserId);
      desktopTab.setName(NS_ConvertUTF16toUTF8(contentTitle));
      desktopTab.setUniqueId(nsPrintfCString("%" PRId64, browserId));
    }
  });
  mozilla::SyncRunnable::DispatchToThread(
      mozilla::GetMainThreadSerialEventTarget(), runnable);
  return tabList;
}

template <CaptureDeviceType Type, typename Device>
void DesktopDeviceInfoImpl<Type, Device>::Refresh() {
  if constexpr (Type == CaptureDeviceType::Browser) {
    mDeviceList = InitializeTabList();
    return;
  }

  mDeviceList.clear();

  std::unique_ptr<DesktopCapturer> cap;
  if constexpr (Type == CaptureDeviceType::Screen ||
                Type == CaptureDeviceType::Window) {
    cap = DesktopCapturer::CreateGenericCapturer(mOptions);
    if constexpr (Type == CaptureDeviceType::Screen) {
      if (!cap) {
        cap = DesktopCapturer::CreateScreenCapturer(mOptions);
      }
    } else if constexpr (Type == CaptureDeviceType::Window) {
      if (cap) {
        // We only use the screen side of a generic capturer for enumeration.
        return;
      }
      cap = DesktopCapturer::CreateWindowCapturer(mOptions);
    }

    if (!cap) {
      return;
    }

    DesktopCapturer::SourceList list;
    if (!cap->GetSourceList(&list)) {
      return;
    }

    for (const auto& elem : list) {
      auto result = mDeviceList.try_emplace(elem.id);
      auto& [iter, inserted] = result;
      if (!inserted) {
        MOZ_ASSERT_UNREACHABLE("Duplicate screen id");
        continue;
      }
      auto& [key, device] = *iter;
      device.setScreenId(elem.id);
      device.setUniqueId(nsPrintfCString("%" PRIdPTR, elem.id));
      if (Type == CaptureDeviceType::Screen && list.size() == 1) {
        device.setName(nsCString("Primary Monitor"));
      } else {
        device.setName(nsCString(elem.title.c_str()));
      }
      device.setPid(elem.pid);
    }
  }
}

std::unique_ptr<DesktopCaptureInfo> CreateScreenCaptureInfo(
    const DesktopCaptureOptions& aOptions) {
  std::unique_ptr<DesktopCaptureInfo> info(
      new DesktopDeviceInfoImpl<CaptureDeviceType::Screen, DesktopSource>(
          aOptions));
  info->Refresh();
  return info;
}

std::unique_ptr<DesktopCaptureInfo> CreateWindowCaptureInfo(
    const DesktopCaptureOptions& aOptions) {
  std::unique_ptr<DesktopCaptureInfo> info(
      new DesktopDeviceInfoImpl<CaptureDeviceType::Window, DesktopSource>(
          aOptions));
  info->Refresh();
  return info;
}

std::unique_ptr<TabCaptureInfo> CreateTabCaptureInfo() {
  std::unique_ptr<TabCaptureInfo> info(
      new DesktopDeviceInfoImpl<CaptureDeviceType::Browser, TabSource>(
          DesktopCaptureOptions()));
  info->Refresh();
  return info;
}

// simulate deviceInfo interface for video engine, bridge screen/application and
// real screen/application device info
template <typename Source>
class DesktopCaptureDeviceInfo final : public VideoCaptureModule::DeviceInfo {
 public:
  explicit DesktopCaptureDeviceInfo(
      std::unique_ptr<CaptureInfo<Source>>&& aSourceInfo);

  int32_t Refresh() override;

  uint32_t NumberOfDevices() override;
  int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
                        uint32_t aDeviceNameUTF8Size, char* aDeviceUniqueIdUTF8,
                        uint32_t aDeviceUniqueIdUTF8Size,
                        char* aProductUniqueIdUTF8,
                        uint32_t aProductUniqueIdUTF8Size, pid_t* aPid,
                        bool* aDeviceIsPlaceholder = nullptr) override;

  int32_t DisplayCaptureSettingsDialogBox(const char* aDeviceUniqueIdUTF8,
                                          const char* aDialogTitleUTF8,
                                          void* aParentWindow,
                                          uint32_t aPositionX,
                                          uint32_t aPositionY) override;
  int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8) override;
  int32_t GetCapability(const char* aDeviceUniqueIdUTF8,
                        uint32_t aDeviceCapabilityNumber,
                        VideoCaptureCapability& aCapability) override;

  int32_t GetBestMatchedCapability(const char* aDeviceUniqueIdUTF8,
                                   const VideoCaptureCapability& aRequested,
                                   VideoCaptureCapability& aResulting) override;
  int32_t GetOrientation(const char* aDeviceUniqueIdUTF8,
                         VideoRotation& aOrientation) override;

 protected:
  std::unique_ptr<CaptureInfo<Source>> mDeviceInfo;
};

using DesktopDeviceInfo = DesktopCaptureDeviceInfo<DesktopSource>;
using TabDeviceInfo = DesktopCaptureDeviceInfo<TabSource>;

template <typename Source>
DesktopCaptureDeviceInfo<Source>::DesktopCaptureDeviceInfo(
    std::unique_ptr<CaptureInfo<Source>>&& aSourceInfo)
    : mDeviceInfo(std::move(aSourceInfo)) {}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::Refresh() {
  mDeviceInfo->Refresh();
  return 0;
}

template <typename Source>
uint32_t DesktopCaptureDeviceInfo<Source>::NumberOfDevices() {
  return mDeviceInfo->getSourceCount();
}

template <>
int32_t DesktopCaptureDeviceInfo<DesktopSource>::GetDeviceName(
    uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size,
    char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size,
    char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size, pid_t* aPid,
    bool* aDeviceIsPlaceholder) {
  // always initialize output
  if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) {
    memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size);
  }
  if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) {
    memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size);
  }
  if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) {
    memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size);
  }

  const DesktopSource* source = mDeviceInfo->getSource(aDeviceNumber);
  if (!source) {
    return 0;
  }

  const nsCString& deviceName = source->getName();
  size_t len = deviceName.Length();
  if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) {
    memcpy(aDeviceNameUTF8, deviceName.Data(), len);
  }

  const nsCString& deviceUniqueId = source->getUniqueId();
  len = deviceUniqueId.Length();
  if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) {
    memcpy(aDeviceUniqueIdUTF8, deviceUniqueId.Data(), len);
  }

  if (aPid) {
    *aPid = source->getPid();
  }

  return 0;
}

template <>
int32_t DesktopCaptureDeviceInfo<TabSource>::GetDeviceName(
    uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size,
    char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size,
    char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size, pid_t* aPid,
    bool* aDeviceIsPlaceholder) {
  // always initialize output
  if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) {
    memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size);
  }
  if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) {
    memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size);
  }
  if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) {
    memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size);
  }

  const TabSource* source = mDeviceInfo->getSource(aDeviceNumber);
  if (!source) {
    return 0;
  }

  const nsCString& deviceName = source->getName();
  size_t len = deviceName.Length();
  if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) {
    memcpy(aDeviceNameUTF8, deviceName.Data(), len);
  }

  const nsCString& deviceUniqueId = source->getUniqueId();
  len = deviceUniqueId.Length();
  if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) {
    memcpy(aDeviceUniqueIdUTF8, deviceUniqueId.Data(), len);
  }

  return 0;
}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::DisplayCaptureSettingsDialogBox(
    const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
    void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) {
  // no device properties to change
  return 0;
}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::NumberOfCapabilities(
    const char* aDeviceUniqueIdUTF8) {
  return 0;
}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::GetCapability(
    const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber,
    VideoCaptureCapability& aCapability) {
  return 0;
}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::GetBestMatchedCapability(
    const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
    VideoCaptureCapability& aResulting) {
  return 0;
}

template <typename Source>
int32_t DesktopCaptureDeviceInfo<Source>::GetOrientation(
    const char* aDeviceUniqueIdUTF8, VideoRotation& aOrientation) {
  return 0;
}

std::shared_ptr<VideoCaptureModule::DeviceInfo> CreateDesktopDeviceInfo(
    std::unique_ptr<DesktopCaptureInfo>&& aInfo) {
  return std::make_shared<DesktopDeviceInfo>(std::move(aInfo));
}

std::shared_ptr<VideoCaptureModule::DeviceInfo> CreateTabDeviceInfo(
    std::unique_ptr<TabCaptureInfo>&& aInfo) {
  return std::make_shared<TabDeviceInfo>(std::move(aInfo));
}
}  // namespace webrtc
