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

#include "CamerasChild.h"
#include "MediaEngineRemoteVideoSource.h"
#include "MediaEngineWebRTCAudio.h"
#include "MediaManager.h"
#include "mozilla/Logging.h"

// Pipewire detection support
#if defined(WEBRTC_USE_PIPEWIRE)
#  include "modules/desktop_capture/desktop_capturer.h"
#  include "mozilla/StaticPrefs_media.h"
#endif

#if defined(WEBRTC_MAC)
#  include "modules/desktop_capture/mac/screen_capturer_sck.h"
#  include "mozilla/StaticPrefs_media.h"
#endif

#define FAKE_ONDEVICECHANGE_EVENT_PERIOD_IN_MS 500

static mozilla::LazyLogModule sGetUserMediaLog("GetUserMedia");
#define LOG(...) \
  MOZ_LOG_FMT(sGetUserMediaLog, mozilla::LogLevel::Debug, __VA_ARGS__)

namespace mozilla {

using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet;
using camera::CamerasChild;
using camera::GetChildAndCall;
using dom::MediaSourceEnum;

already_AddRefed<CubebDeviceEnumerator> GetEnumerator() {
  return CubebDeviceEnumerator::GetInstance();
}

MediaEngineWebRTC::MediaEngineWebRTC() {
  AssertIsOnOwningThread();

  GetChildAndCall(
      &CamerasChild::ConnectDeviceListChangeListener<MediaEngineWebRTC>,
      &mCameraListChangeListener, AbstractThread::MainThread(), this,
      &MediaEngineWebRTC::DeviceListChanged);
  RefPtr<CubebDeviceEnumerator> enumerator = GetEnumerator();
  mMicrophoneListChangeListener =
      enumerator->OnAudioInputDeviceListChange().Connect(
          AbstractThread::MainThread(), this,
          &MediaEngineWebRTC::DeviceListChanged);
  mSpeakerListChangeListener =
      enumerator->OnAudioOutputDeviceListChange().Connect(
          AbstractThread::MainThread(), this,
          &MediaEngineWebRTC::DeviceListChanged);
}

void MediaEngineWebRTC::EnumerateVideoDevices(
    MediaSourceEnum aMediaSource, nsTArray<RefPtr<MediaDevice>>* aDevices) {
  AssertIsOnOwningThread();
  // flag sources with cross-origin exploit potential
  bool scaryKind = (aMediaSource == MediaSourceEnum::Screen ||
                    aMediaSource == MediaSourceEnum::Browser);
  bool desktopKind = aMediaSource == MediaSourceEnum::Application ||
                     aMediaSource == MediaSourceEnum::Screen ||
                     aMediaSource == MediaSourceEnum::Window;
  (void)desktopKind;  // Suppress "unused variable" on Windows and Android.
#if defined(WEBRTC_USE_PIPEWIRE)
  bool canRequestOsLevelPrompt =
      mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() &&
      webrtc::DesktopCapturer::IsRunningUnderWayland() && desktopKind;
#elif defined(WEBRTC_MAC)
  bool canRequestOsLevelPrompt =
      mozilla::StaticPrefs::
          media_getdisplaymedia_screencapturekit_enabled_AtStartup() &&
      mozilla::StaticPrefs::
          media_getdisplaymedia_screencapturekit_picker_enabled_AtStartup() &&
      webrtc::GenericCapturerSckWithPickerAvailable() && desktopKind;
#else
  bool canRequestOsLevelPrompt = false;
#endif
  /*
   * We still enumerate every time, in case a new device was plugged in since
   * the last call. TODO: Verify that WebRTC actually does deal with hotplugging
   * new devices (with or without new engine creation) and accordingly adjust.
   * Enumeration is not neccessary if GIPS reports the same set of devices
   * for a given instance of the engine.
   */
  int num;
#if defined(_ARM64_) && defined(XP_WIN)
  // There are problems with using DirectShow on versions of Windows before
  // 19H1 on arm64. This disables the camera on older versions of Windows.
  if (aMediaSource == MediaSourceEnum::Camera) {
    typedef ULONG (*RtlGetVersionFn)(LPOSVERSIONINFOEXW);
    RtlGetVersionFn RtlGetVersion;
    RtlGetVersion = (RtlGetVersionFn)GetProcAddress(GetModuleHandleA("ntdll"),
                                                    "RtlGetVersion");
    if (RtlGetVersion) {
      OSVERSIONINFOEXW info;
      info.dwOSVersionInfoSize = sizeof(info);
      RtlGetVersion(&info);
      // 19H1 is 18346
      if (info.dwBuildNumber < 18346) {
        return;
      }
    }
  }
#endif
  camera::CaptureEngine capEngine =
      MediaEngineRemoteVideoSource::CaptureEngine(aMediaSource);
  num = GetChildAndCall(&CamerasChild::NumberOfCaptureDevices, capEngine);

  for (int i = 0; i < num; i++) {
    char deviceName[MediaEngineSource::kMaxDeviceNameLength];
    char uniqueId[MediaEngineSource::kMaxUniqueIdLength];
    bool scarySource = false;

    // paranoia
    deviceName[0] = '\0';
    uniqueId[0] = '\0';
    int error;

    error = GetChildAndCall(&CamerasChild::GetCaptureDevice, capEngine, i,
                            deviceName, sizeof(deviceName), uniqueId,
                            sizeof(uniqueId), &scarySource);
    if (error) {
      LOG("camera:GetCaptureDevice: Failed {}", error);
      continue;
    }
#ifdef DEBUG
    LOG("  Capture Device Index {}, Name {}", i, deviceName);

    webrtc::CaptureCapability cap;
    int numCaps = GetChildAndCall(&CamerasChild::NumberOfCapabilities,
                                  capEngine, uniqueId);
    LOG("Number of Capabilities {}", numCaps);
    for (int j = 0; j < numCaps; j++) {
      if (GetChildAndCall(&CamerasChild::GetCaptureCapability, capEngine,
                          uniqueId, j, &cap) != 0) {
        break;
      }
      LOG("type={} width={} height={} maxFPS={}",
          static_cast<int>(cap.videoType), cap.width, cap.height, cap.maxFPS);
    }
#endif

    NS_ConvertUTF8toUTF16 name(deviceName);
    NS_ConvertUTF8toUTF16 uuid(uniqueId);
    // The remote video backend doesn't implement group id. We return the
    // device name and higher layers will correlate this with the name of
    // audio devices.

    aDevices->EmplaceBack(new MediaDevice(
        this, aMediaSource, name, uuid, uuid,
        MediaDevice::IsScary(scaryKind || scarySource),
        canRequestOsLevelPrompt ? MediaDevice::OsPromptable::Yes
                                : MediaDevice::OsPromptable::No));
  }
}

void MediaEngineWebRTC::EnumerateMicrophoneDevices(
    nsTArray<RefPtr<MediaDevice>>* aDevices) {
  AssertIsOnOwningThread();

  RefPtr<CubebDeviceEnumerator> enumerator = GetEnumerator();
  RefPtr<const AudioDeviceSet> devices =
      enumerator->EnumerateAudioInputDevices();

  DebugOnly<bool> foundPreferredDevice = false;

  for (const auto& deviceInfo : *devices) {
#ifndef ANDROID
    MOZ_ASSERT(deviceInfo->DeviceID());
#endif
    LOG("Cubeb device: type 0x{:x}, state 0x{:x}, name {}, id {}",
        deviceInfo->Type(), deviceInfo->State(),
        NS_ConvertUTF16toUTF8(deviceInfo->Name()).get(),
        fmt::ptr(deviceInfo->DeviceID()));

    if (deviceInfo->State() == CUBEB_DEVICE_STATE_ENABLED) {
      MOZ_ASSERT(deviceInfo->Type() == CUBEB_DEVICE_TYPE_INPUT);
      // Lie and provide the name as UUID
      RefPtr device = new MediaDevice(this, deviceInfo, deviceInfo->Name());
      if (deviceInfo->Preferred()) {
#ifdef DEBUG
        if (!foundPreferredDevice) {
          foundPreferredDevice = true;
        } else {
          // This is possible on windows, there is a default communication
          // device, and a default device:
          // See https://bugzilla.mozilla.org/show_bug.cgi?id=1542739
#  ifndef XP_WIN
          MOZ_ASSERT(!foundPreferredDevice,
                     "Found more than one preferred audio input device"
                     "while enumerating");
#  endif
        }
#endif
        aDevices->InsertElementAt(0, std::move(device));
      } else {
        aDevices->AppendElement(std::move(device));
      }
    }
  }
}

void MediaEngineWebRTC::EnumerateSpeakerDevices(
    nsTArray<RefPtr<MediaDevice>>* aDevices) {
  AssertIsOnOwningThread();

  RefPtr<CubebDeviceEnumerator> enumerator = GetEnumerator();
  RefPtr<const AudioDeviceSet> devices =
      enumerator->EnumerateAudioOutputDevices();

#ifndef XP_WIN
  DebugOnly<bool> preferredDeviceFound = false;
#endif
  for (const auto& deviceInfo : *devices) {
    LOG("Cubeb device: type 0x{:x}, state 0x{:x}, name {}, id {}",
        deviceInfo->Type(), deviceInfo->State(),
        NS_ConvertUTF16toUTF8(deviceInfo->Name()).get(),
        fmt::ptr(deviceInfo->DeviceID()));
    if (deviceInfo->State() == CUBEB_DEVICE_STATE_ENABLED) {
      MOZ_ASSERT(deviceInfo->Type() == CUBEB_DEVICE_TYPE_OUTPUT);
      nsString uuid(deviceInfo->Name());
      // If, for example, input and output are in the same device, uuid
      // would be the same for both which ends up to create the same
      // deviceIDs (in JS).
      uuid.Append(u"_Speaker"_ns);
      RefPtr device = new MediaDevice(this, deviceInfo, uuid);
      if (deviceInfo->Preferred()) {
        // In windows is possible to have more than one preferred device
#if defined(DEBUG) && !defined(XP_WIN)
        MOZ_ASSERT(!preferredDeviceFound, "More than one preferred device");
        preferredDeviceFound = true;
#endif
        aDevices->InsertElementAt(0, std::move(device));
      } else {
        aDevices->AppendElement(std::move(device));
      }
    }
  }
}

void MediaEngineWebRTC::EnumerateDevices(
    MediaSourceEnum aMediaSource, MediaSinkEnum aMediaSink,
    nsTArray<RefPtr<MediaDevice>>* aDevices) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aMediaSource != MediaSourceEnum::Other ||
             aMediaSink != MediaSinkEnum::Other);
  if (MediaEngineSource::IsVideo(aMediaSource)) {
    switch (aMediaSource) {
      case MediaSourceEnum::Window:
        // Since the mediaSource constraint is deprecated, treat the Window
        // value as a request for getDisplayMedia-equivalent sharing: Combine
        // window and fullscreen into a single list of choices. The other values
        // are still useful for testing.
        EnumerateVideoDevices(MediaSourceEnum::Window, aDevices);
        EnumerateVideoDevices(MediaSourceEnum::Browser, aDevices);
        EnumerateVideoDevices(MediaSourceEnum::Screen, aDevices);
        break;
      case MediaSourceEnum::Screen:
      case MediaSourceEnum::Browser:
      case MediaSourceEnum::Camera:
        EnumerateVideoDevices(aMediaSource, aDevices);
        break;
      default:
        MOZ_CRASH("No valid video source");
        break;
    }
  } else if (aMediaSource == MediaSourceEnum::AudioCapture) {
    aDevices->EmplaceBack(new MediaDevice(
        this, aMediaSource, u"AudioCapture"_ns,
        MediaEngineWebRTCAudioCaptureSource::GetUUID(),
        MediaEngineWebRTCAudioCaptureSource::GetGroupId(),
        MediaDevice::IsScary::No, MediaDevice::OsPromptable::No));
  } else if (aMediaSource == MediaSourceEnum::Microphone) {
    EnumerateMicrophoneDevices(aDevices);
  }

  if (aMediaSink == MediaSinkEnum::Speaker) {
    EnumerateSpeakerDevices(aDevices);
  }
}

void MediaEngineWebRTC::InvalidateDesktopCaptureDeviceCache(
    MediaSourceEnum aMediaSource) {
  AssertIsOnOwningThread();
  switch (aMediaSource) {
    case MediaSourceEnum::Browser:
    case MediaSourceEnum::Screen:
    case MediaSourceEnum::Window:
      GetChildAndCall(
          &CamerasChild::InvalidateDesktopCaptureDeviceCache,
          MediaEngineRemoteVideoSource::CaptureEngine(aMediaSource));
      break;
    default:
      break;
  }
}

RefPtr<MediaEngineSource> MediaEngineWebRTC::CreateSource(
    const MediaDevice* aMediaDevice) {
  MOZ_ASSERT(aMediaDevice->mEngine == this);
  if (MediaEngineSource::IsVideo(aMediaDevice->mMediaSource)) {
    return MakeRefPtr<MediaEngineRemoteVideoSource>(aMediaDevice);
  }
  switch (aMediaDevice->mMediaSource) {
    case MediaSourceEnum::AudioCapture:
      return MakeRefPtr<MediaEngineWebRTCAudioCaptureSource>(aMediaDevice);
    case MediaSourceEnum::Microphone:
      return MakeRefPtr<MediaEngineWebRTCMicrophoneSource>(aMediaDevice);
    default:
      MOZ_CRASH("Unsupported source type");
      return nullptr;
  }
}

RefPtr<MediaEngineSource> MediaEngineWebRTC::CreateSourceFrom(
    const MediaEngineSource* aSource, const MediaDevice* aMediaDevice) {
  MOZ_ASSERT(aMediaDevice->mEngine == this);
  if (MediaEngineSource::IsVideo(aMediaDevice->mMediaSource)) {
    return MediaEngineRemoteVideoSource::CreateFrom(
        static_cast<const MediaEngineRemoteVideoSource*>(aSource),
        aMediaDevice);
  }
  switch (aMediaDevice->mMediaSource) {
    case MediaSourceEnum::Microphone:
      return MediaEngineWebRTCMicrophoneSource::CreateFrom(
          static_cast<const MediaEngineWebRTCMicrophoneSource*>(aSource),
          aMediaDevice);
    default:
      MOZ_CRASH("Unsupported source type");
      return nullptr;
  }
}

void MediaEngineWebRTC::Shutdown() {
  AssertIsOnOwningThread();
  mCameraListChangeListener.DisconnectIfExists();
  mMicrophoneListChangeListener.DisconnectIfExists();
  mSpeakerListChangeListener.DisconnectIfExists();

  LOG("{}", __FUNCTION__);
  mozilla::camera::Shutdown();
}

}  // namespace mozilla

#undef LOG
