/* 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_MEDIAMANAGER_H
#define MOZILLA_MEDIAMANAGER_H

#include "MediaEnginePrefs.h"
#include "MediaEventSource.h"
#include "mozilla/dom/GetUserMediaRequest.h"
#include "mozilla/Unused.h"
#include "nsIMediaDevice.h"
#include "nsIMediaManager.h"

#include "nsHashKeys.h"
#include "nsClassHashtable.h"
#include "nsRefPtrHashtable.h"
#include "nsIMemoryReporter.h"
#include "nsIObserver.h"

#include "nsXULAppAPI.h"
#include "mozilla/Attributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/MediaStreamBinding.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/MediaStreamError.h"
#include "mozilla/dom/NavigatorBinding.h"
#include "mozilla/media/MediaChild.h"
#include "mozilla/media/MediaParent.h"
#include "mozilla/Logging.h"
#include "mozilla/UniquePtr.h"
#include "DOMMediaStream.h"
#include "PerformanceRecorder.h"

#ifdef MOZ_WEBRTC
#  include "transport/runnable_utils.h"
#endif

class AudioDeviceInfo;
class nsIPrefBranch;

namespace mozilla {
class MediaEngine;
class MediaEngineSource;
class TaskQueue;
class MediaTimer;
class MediaTrack;
namespace dom {
struct AudioOutputOptions;
struct MediaStreamConstraints;
struct MediaTrackConstraints;
struct MediaTrackConstraintSet;
struct MediaTrackSettings;
enum class CallerType : uint32_t;
enum class MediaDeviceKind : uint8_t;
}  // namespace dom

namespace ipc {
class PrincipalInfo;
}

class GetUserMediaTask;
class GetUserMediaWindowListener;
class MediaManager;
class DeviceListener;

/**
 * Device info that is independent of any Window.
 * MediaDevices can be shared, unlike LocalMediaDevices.
 */
class MediaDevice final {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDevice)

  /**
   * Whether source device does end-run around cross origin restrictions.
   */
  enum class IsScary { No, Yes };

  /**
   * Whether source device can use OS level selection prompt
   */
  enum class OsPromptable { No, Yes };

  MediaDevice(MediaEngine* aEngine, dom::MediaSourceEnum aMediaSource,
              const nsString& aRawName, const nsString& aRawID,
              const nsString& aRawGroupID, IsScary aIsScary,
              const OsPromptable canRequestOsLevelPrompt);

  MediaDevice(MediaEngine* aEngine,
              const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
              const nsString& aRawID);

  static RefPtr<MediaDevice> CopyWithNewRawGroupId(
      const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID);

  dom::MediaSourceEnum GetMediaSource() const;

 protected:
  ~MediaDevice();

 public:
  const RefPtr<MediaEngine> mEngine;
  const RefPtr<AudioDeviceInfo> mAudioDeviceInfo;
  const dom::MediaSourceEnum mMediaSource;
  const dom::MediaDeviceKind mKind;
  const bool mScary;
  const bool mCanRequestOsLevelPrompt;
  const bool mIsFake;
  const nsString mType;
  const nsString mRawID;
  const nsString mRawGroupID;
  const nsString mRawName;
};

/**
 * Device info that is specific to a particular Window.  If the device is a
 * source device, then a single corresponding MediaEngineSource is provided,
 * which can provide a maximum of one capture stream.  LocalMediaDevices are
 * not shared, but APIs returning LocalMediaDevices return a new object each
 * call.
 */
class LocalMediaDevice final : public nsIMediaDevice {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIMEDIADEVICE

  LocalMediaDevice(RefPtr<const MediaDevice> aRawDevice, const nsString& aID,
                   const nsString& aGroupID, const nsString& aName);

  uint32_t GetBestFitnessDistance(
      const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
      dom::CallerType aCallerType);

  nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs& aPrefs, uint64_t aWindowId,
                    const char** aOutBadConstraint);
  void SetTrack(const RefPtr<MediaTrack>& aTrack,
                const nsMainThreadPtrHandle<nsIPrincipal>& aPrincipal);
  nsresult Start();
  nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
                       const MediaEnginePrefs& aPrefs,
                       const char** aOutBadConstraint);
  nsresult FocusOnSelectedSource();
  nsresult Stop();
  nsresult Deallocate();

  void GetSettings(dom::MediaTrackSettings& aOutSettings);
  MediaEngineSource* Source();
  const TrackingId& GetTrackingId() const;
  // Returns null if not a physical audio device.
  AudioDeviceInfo* GetAudioDeviceInfo() const {
    return mRawDevice->mAudioDeviceInfo;
  }
  dom::MediaSourceEnum GetMediaSource() const {
    return mRawDevice->GetMediaSource();
  }
  dom::MediaDeviceKind Kind() const { return mRawDevice->mKind; }
  bool IsFake() const { return mRawDevice->mIsFake; }
  const nsString& RawID() { return mRawDevice->mRawID; }

 private:
  virtual ~LocalMediaDevice() = default;

  static uint32_t FitnessDistance(
      nsString aN,
      const dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters&
          aConstraint);

  static bool StringsContain(const dom::OwningStringOrStringSequence& aStrings,
                             nsString aN);
  static uint32_t FitnessDistance(
      nsString aN, const dom::ConstrainDOMStringParameters& aParams);

 public:
  const RefPtr<const MediaDevice> mRawDevice;
  const nsString mName;
  const nsString mID;
  const nsString mGroupID;

 private:
  RefPtr<MediaEngineSource> mSource;
};

typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener>
    WindowTable;

class MediaManager final : public nsIMediaManagerService,
                           public nsIMemoryReporter,
                           public nsIObserver {
  friend DeviceListener;

 public:
  static already_AddRefed<MediaManager> GetInstance();

  // NOTE: never NS_DispatchAndSpinEventLoopUntilComplete to the MediaManager
  // thread from the MainThread, as we NS_DispatchAndSpinEventLoopUntilComplete
  // to MainThread from MediaManager thread.
  static MediaManager* Get();
  static MediaManager* GetIfExists();
  static void StartupInit();
  static void Dispatch(already_AddRefed<Runnable> task);

  /**
   * Posts an async operation to the media manager thread.
   * FunctionType must be a function that takes a `MozPromiseHolder&`.
   *
   * The returned promise is resolved or rejected by aFunction on the media
   * manager thread.
   */
  template <typename MozPromiseType, typename FunctionType>
  static RefPtr<MozPromiseType> Dispatch(const char* aName,
                                         FunctionType&& aFunction);

#ifdef DEBUG
  static bool IsInMediaThread();
#endif

  static bool Exists() { return !!GetIfExists(); }

  static nsresult NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow);

  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIOBSERVER
  NS_DECL_NSIMEMORYREPORTER
  NS_DECL_NSIMEDIAMANAGERSERVICE

  media::Parent<media::NonE10s>* GetNonE10sParent();

  // If the window has not been destroyed, then return the
  // GetUserMediaWindowListener for this window.
  // If the window has been destroyed, then return null.
  RefPtr<GetUserMediaWindowListener> GetOrMakeWindowListener(
      nsPIDOMWindowInner* aWindow);
  WindowTable* GetActiveWindows() {
    MOZ_ASSERT(NS_IsMainThread());
    return &mActiveWindows;
  }
  GetUserMediaWindowListener* GetWindowListener(uint64_t aWindowId) {
    MOZ_ASSERT(NS_IsMainThread());
    return mActiveWindows.GetWeak(aWindowId);
  }
  void AddWindowID(uint64_t aWindowId,
                   RefPtr<GetUserMediaWindowListener> aListener);
  void RemoveWindowID(uint64_t aWindowId);
  void SendPendingGUMRequest();
  bool IsWindowStillActive(uint64_t aWindowId) {
    return !!GetWindowListener(aWindowId);
  }
  bool IsWindowListenerStillActive(
      const RefPtr<GetUserMediaWindowListener>& aListener);

  static bool IsOn(const dom::OwningBooleanOrMediaTrackConstraints& aUnion) {
    return !aUnion.IsBoolean() || aUnion.GetAsBoolean();
  }
  using GetUserMediaSuccessCallback = dom::NavigatorUserMediaSuccessCallback;
  using GetUserMediaErrorCallback = dom::NavigatorUserMediaErrorCallback;

  MOZ_CAN_RUN_SCRIPT
  static void CallOnError(GetUserMediaErrorCallback& aCallback,
                          dom::MediaStreamError& aError);
  MOZ_CAN_RUN_SCRIPT
  static void CallOnSuccess(GetUserMediaSuccessCallback& aCallback,
                            DOMMediaStream& aTrack);

  using MediaDeviceSet = nsTArray<RefPtr<MediaDevice>>;
  using MediaDeviceSetRefCnt = media::Refcountable<MediaDeviceSet>;
  using LocalMediaDeviceSet = nsTArray<RefPtr<LocalMediaDevice>>;
  using LocalMediaDeviceSetRefCnt = media::Refcountable<LocalMediaDeviceSet>;

  using StreamPromise =
      MozPromise<RefPtr<DOMMediaStream>, RefPtr<MediaMgrError>, true>;
  using DeviceSetPromise =
      MozPromise<RefPtr<MediaDeviceSetRefCnt>, RefPtr<MediaMgrError>, true>;
  using ConstDeviceSetPromise = MozPromise<RefPtr<const MediaDeviceSetRefCnt>,
                                           RefPtr<MediaMgrError>, true>;
  using LocalDevicePromise =
      MozPromise<RefPtr<LocalMediaDevice>, RefPtr<MediaMgrError>, true>;
  using LocalDeviceSetPromise = MozPromise<RefPtr<LocalMediaDeviceSetRefCnt>,
                                           RefPtr<MediaMgrError>, true>;
  using MgrPromise = MozPromise<bool, RefPtr<MediaMgrError>, true>;

  RefPtr<StreamPromise> GetUserMedia(
      nsPIDOMWindowInner* aWindow,
      const dom::MediaStreamConstraints& aConstraints,
      dom::CallerType aCallerType);

  RefPtr<LocalDevicePromise> SelectAudioOutput(
      nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
      dom::CallerType aCallerType);

  // Return the list of microphone, camera, and speaker devices.
  // MediaDeviceSets provided on promise resolution are shared between
  // callers and so cannot be modified.
  RefPtr<ConstDeviceSetPromise> GetPhysicalDevices();

  void OnNavigation(uint64_t aWindowID);
  void OnCameraMute(bool aMute);
  void OnMicrophoneMute(bool aMute);
  bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);

  MediaEventSource<void>& DeviceListChangeEvent() {
    return mDeviceListChangeEvent;
  }
  RefPtr<LocalDeviceSetPromise> AnonymizeDevices(
      nsPIDOMWindowInner* aWindow, RefPtr<const MediaDeviceSetRefCnt> aDevices);

  MediaEnginePrefs mPrefs;

 private:
  static nsresult GenerateUUID(nsAString& aResult);

 public:
  /**
   * This function tries to guess the group id for a video device in aDevices
   * based on the device name. If the name of only one audio device in aAudios
   * contains the name of the video device, then, this video device will take
   * the group id of the audio device. Since this is a guess we try to minimize
   * the probability of false positive. If we fail to find a correlation we
   * leave the video group id untouched. In that case the group id will be the
   * video device name.
   */
  static void GuessVideoDeviceGroupIDs(MediaDeviceSet& aDevices,
                                       const MediaDeviceSet& aAudios);

 private:
  enum class EnumerationFlag {
    AllowPermissionRequest,
    EnumerateAudioOutputs,
    ForceFakes,
  };
  using EnumerationFlags = EnumSet<EnumerationFlag>;
  RefPtr<LocalDeviceSetPromise> EnumerateDevicesImpl(
      nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType,
      dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);

  RefPtr<DeviceSetPromise> EnumerateRawDevices(
      dom::MediaSourceEnum aVideoInputType,
      dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);

  RefPtr<LocalDeviceSetPromise> SelectSettings(
      const dom::MediaStreamConstraints& aConstraints,
      dom::CallerType aCallerType, RefPtr<LocalMediaDeviceSetRefCnt> aDevices);

  void GetPref(nsIPrefBranch* aBranch, const char* aPref, const char* aData,
               int32_t* aVal);
  void GetPrefBool(nsIPrefBranch* aBranch, const char* aPref, const char* aData,
                   bool* aVal);
  void GetPrefs(nsIPrefBranch* aBranch, const char* aData);

  // Make private because we want only one instance of this class
  explicit MediaManager(already_AddRefed<TaskQueue> aMediaThread);

  ~MediaManager() = default;
  void Shutdown();

  void StopScreensharing(uint64_t aWindowID);

  void RemoveMediaDevicesCallback(uint64_t aWindowID);
  void DeviceListChanged();
  void InvalidateDeviceCache();
  void HandleDeviceListChanged();

  // Returns the number of incomplete tasks associated with this window,
  // including the newly added task.
  size_t AddTaskAndGetCount(uint64_t aWindowID, const nsAString& aCallID,
                            RefPtr<GetUserMediaTask> aTask);
  // Finds the task corresponding to aCallID and removes it from tracking.
  RefPtr<GetUserMediaTask> TakeGetUserMediaTask(const nsAString& aCallID);
  // Intended for use with "media.navigator.permission.disabled" to bypass the
  // permission prompt and use the first appropriate device.
  void NotifyAllowed(const nsString& aCallID,
                     const LocalMediaDeviceSet& aDevices);

  // Media thread only
  MediaEngine* GetBackend();

  MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);

  // ONLY access from MainThread so we don't need to lock
  WindowTable mActiveWindows;
  nsRefPtrHashtable<nsStringHashKey, GetUserMediaTask> mActiveCallbacks;
  nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mCallIds;
  nsTArray<RefPtr<dom::GetUserMediaRequest>> mPendingGUMRequest;
  // non-null if a device enumeration is in progress and was started after the
  // last device-change invalidation
  RefPtr<media::Refcountable<nsTArray<MozPromiseHolder<ConstDeviceSetPromise>>>>
      mPendingDevicesPromises;
  RefPtr<MediaDeviceSetRefCnt> mPhysicalDevices;
  TimeStamp mUnhandledDeviceChangeTime;
  RefPtr<MediaTimer> mDeviceChangeTimer;
  bool mCamerasMuted = false;
  bool mMicrophonesMuted = false;

 public:
  // Always exists
  const RefPtr<TaskQueue> mMediaThread;

 private:
  nsCOMPtr<nsIAsyncShutdownBlocker> mShutdownBlocker;

  // ONLY accessed from MediaManagerThread
  RefPtr<MediaEngine> mBackend;

  // Accessed only on main thread and mMediaThread.
  // Set before mMediaThread is created, and cleared on main thread after last
  // mMediaThread task is run.
  static StaticRefPtr<MediaManager> sSingleton;

  // Connect/Disconnect on media thread only
  MediaEventListener mDeviceListChangeListener;

  MediaEventProducer<void> mDeviceListChangeEvent;

 public:
  RefPtr<media::Parent<media::NonE10s>> mNonE10sParent;
};

}  // namespace mozilla

#endif  // MOZILLA_MEDIAMANAGER_H
