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

#include "MediaControlKeySource.h"
#include "MediaEventSource.h"
#include "MediaPlaybackStatus.h"
#include "mozilla/dom/MediaMetadata.h"
#include "mozilla/dom/MediaSessionBinding.h"
#include "mozilla/Maybe.h"
#include "nsTHashMap.h"
#include "nsISupportsImpl.h"

namespace mozilla::dom {

class MediaSessionInfo {
 public:
  MediaSessionInfo() = default;

  explicit MediaSessionInfo(MediaMetadataBase& aMetadata) {
    mMetadata.emplace(aMetadata);
  }

  MediaSessionInfo(MediaMetadataBase& aMetadata,
                   MediaSessionPlaybackState& aState) {
    mMetadata.emplace(aMetadata);
    mDeclaredPlaybackState = aState;
  }

  static MediaSessionInfo EmptyInfo() { return MediaSessionInfo(); }

  static uint32_t GetActionBitMask(MediaSessionAction aAction) {
    return 1 << static_cast<uint8_t>(aAction);
  }

  void EnableAction(MediaSessionAction aAction) {
    mSupportedActions |= GetActionBitMask(aAction);
  }

  void DisableAction(MediaSessionAction aAction) {
    mSupportedActions &= ~GetActionBitMask(aAction);
  }

  bool IsActionSupported(MediaSessionAction aAction) const {
    return mSupportedActions & GetActionBitMask(aAction);
  }

  // These attributes are all propagated from the media session in the content
  // process.
  Maybe<MediaMetadataBase> mMetadata;
  MediaSessionPlaybackState mDeclaredPlaybackState =
      MediaSessionPlaybackState::None;
  // Use bitwise to store the supported actions.
  uint32_t mSupportedActions = 0;
};

/**
 * IMediaInfoUpdater is an interface which provides methods to update the media
 * related information that happens in the content process.
 */
class IMediaInfoUpdater {
  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING

  // Use this method to update controlled media's playback state and the
  // browsing context where controlled media exists. When notifying the state
  // change, we MUST follow the following rules.
  // (1) `eStart` MUST be the first state and `eStop` MUST be the last state
  // (2) Do not notify same state again
  // (3) `ePaused` can only be notified after notifying `ePlayed`.
  virtual void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
                                          MediaPlaybackState aState) = 0;

  // Use this method to update the audible state of controlled media, and MUST
  // follow the following rules in which `audible` and `inaudible` should be a
  // pair. `inaudible` should always be notified after `audible`. When audible
  // media paused, `inaudible` should be notified
  // Eg. (O) `audible` -> `inaudible` -> `audible` -> `inaudible`
  //     (X) `inaudible` -> `audible`    [notify `inaudible` before `audible`]
  //     (X) `audible` -> `audible`      [notify `audible` twice]
  //     (X) `audible` -> (media pauses) [forgot to notify `inaudible`]
  virtual void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
                                         MediaAudibleState aState) = 0;

  // Use this method to update media session's declared playback state for the
  // specific media session.
  virtual void SetDeclaredPlaybackState(uint64_t aBrowsingContextId,
                                        MediaSessionPlaybackState aState) = 0;

  // Use these methods to update controller's media session list. We'd use it
  // when media session is created/destroyed in the content process.
  virtual void NotifySessionCreated(uint64_t aBrowsingContextId) = 0;
  virtual void NotifySessionDestroyed(uint64_t aBrowsingContextId) = 0;

  // Use this method to update the metadata for the specific media session.
  virtual void UpdateMetadata(uint64_t aBrowsingContextId,
                              const Maybe<MediaMetadataBase>& aMetadata) = 0;

  // Use this method to update the picture in picture mode state of controlled
  // media, and it's safe to notify same state again.
  virtual void SetIsInPictureInPictureMode(uint64_t aBrowsingContextId,
                                           bool aIsInPictureInPictureMode) = 0;

  // Use these methods to update the supported media session action for the
  // specific media session. For a media session from a given browsing context,
  // do not re-enable the same action, or disable the action without enabling it
  // before.
  virtual void EnableAction(uint64_t aBrowsingContextId,
                            MediaSessionAction aAction) = 0;
  virtual void DisableAction(uint64_t aBrowsingContextId,
                             MediaSessionAction aAction) = 0;

  // Use this method when media enters or leaves the fullscreen.
  virtual void NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
                                          bool aIsInFullScreen) = 0;

  // Use this method when media session update its position state.
  virtual void UpdatePositionState(uint64_t aBrowsingContextId,
                                   const PositionState& aState) = 0;
};

/**
 * MediaStatusManager would decide the media related status which can represents
 * the whole tab. The status includes the playback status, tab's metadata and
 * the active media session ID if it exists.
 *
 * We would use `IMediaInfoUpdater` methods to update the media playback related
 * information and then use `MediaPlaybackStatus` to determine the final
 * playback state.
 *
 * The metadata would be the one from the active media session, or the default
 * one. This class would determine which media session is an active media
 * session [1] whithin a tab. It tracks all alive media sessions within a tab
 * and store their metadata which could be used to show on the virtual media
 * control interface. In addition, we can use it to get the current media
 * metadata even if there is no media session existing. However, the meaning of
 * active media session here is not equal to the definition from the spec [1].
 * We just choose the session which is the active one inside the tab, the global
 * active media session among different tabs would be the one inside the main
 * controller which is determined by MediaControlService.
 *
 * [1] https://w3c.github.io/mediasession/#active-media-session
 */
class MediaStatusManager : public IMediaInfoUpdater {
 public:
  explicit MediaStatusManager(uint64_t aBrowsingContextId);

  // IMediaInfoUpdater's methods
  void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
                                  MediaPlaybackState aState) override;
  void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
                                 MediaAudibleState aState) override;
  void SetDeclaredPlaybackState(uint64_t aSessionContextId,
                                MediaSessionPlaybackState aState) override;
  void NotifySessionCreated(uint64_t aSessionContextId) override;
  void NotifySessionDestroyed(uint64_t aSessionContextId) override;
  void UpdateMetadata(uint64_t aSessionContextId,
                      const Maybe<MediaMetadataBase>& aMetadata) override;
  void EnableAction(uint64_t aBrowsingContextId,
                    MediaSessionAction aAction) override;
  void DisableAction(uint64_t aBrowsingContextId,
                     MediaSessionAction aAction) override;
  void UpdatePositionState(uint64_t aBrowsingContextId,
                           const PositionState& aState) override;

  // Return active media session's metadata if active media session exists and
  // it has already set its metadata. Otherwise, return default media metadata
  // which is based on website's title and favicon.
  MediaMetadataBase GetCurrentMediaMetadata() const;

  bool IsMediaAudible() const;
  bool IsMediaPlaying() const;
  bool IsAnyMediaBeingControlled() const;

  // These events would be notified when the active media session's certain
  // property changes.
  MediaEventSource<MediaMetadataBase>& MetadataChangedEvent() {
    return mMetadataChangedEvent;
  }

  MediaEventSource<PositionState>& PositionChangedEvent() {
    return mPositionStateChangedEvent;
  }

  MediaEventSource<MediaSessionPlaybackState>& PlaybackChangedEvent() {
    return mPlaybackStateChangedEvent;
  }

  // Return the actual playback state.
  MediaSessionPlaybackState PlaybackState() const;

  // When page title changes, we might need to update it on the default
  // metadata as well.
  void NotifyPageTitleChanged();

 protected:
  ~MediaStatusManager() = default;

  // This event would be notified when the active media session changes its
  // supported actions.
  MediaEventSource<nsTArray<MediaSessionAction>>&
  SupportedActionsChangedEvent() {
    return mSupportedActionsChangedEvent;
  }

  uint64_t mTopLevelBrowsingContextId;

  // Within a tab, the Id of the browsing context which has already created a
  // media session and owns the audio focus within a tab.
  Maybe<uint64_t> mActiveMediaSessionContextId;

  void ClearActiveMediaSessionContextIdIfNeeded();

 private:
  nsString GetDefaultFaviconURL() const;
  nsString GetDefaultTitle() const;
  MediaMetadataBase CreateDefaultMetadata() const;
  bool IsInPrivateBrowsing() const;
  void FillMissingTitleAndArtworkIfNeeded(MediaMetadataBase& aMetadata) const;

  bool IsSessionOwningAudioFocus(uint64_t aBrowsingContextId) const;
  void SetActiveMediaSessionContextId(uint64_t aBrowsingContextId);
  void HandleAudioFocusOwnerChanged(Maybe<uint64_t>& aBrowsingContextId);

  void NotifySupportedKeysChangedIfNeeded(uint64_t aBrowsingContextId);

  // Return a copyable array filled with the supported media session actions.
  // Use copyable array so that we can use the result as a parameter for the
  // media event.
  CopyableTArray<MediaSessionAction> GetSupportedActions() const;

  void StoreMediaSessionContextIdOnWindowContext();

  // When the amount of playing media changes, we would use this function to
  // update the guessed playback state.
  void SetGuessedPlayState(MediaSessionPlaybackState aState);

  // Whenever the declared playback state or the guessed playback state changes,
  // we should recompute actual playback state to know if we need to update the
  // virtual control interface.
  void UpdateActualPlaybackState();

  // Return the active media session's declared playback state. If the active
  // media session doesn't exist, return  'None' instead.
  MediaSessionPlaybackState GetCurrentDeclaredPlaybackState() const;

  // This state can match to the `guessed playback state` in the spec [1], it
  // indicates if we have any media element playing within the tab which this
  // controller belongs to. But currently we only take media elements into
  // account, which is different from the way the spec recommends. In addition,
  // We don't support web audio and plugin and not consider audible state of
  // media.
  // [1] https://w3c.github.io/mediasession/#guessed-playback-state
  MediaSessionPlaybackState mGuessedPlaybackState =
      MediaSessionPlaybackState::None;

  // This playback state would be the final playback which can be used to know
  // if the controller is playing or not.
  // https://w3c.github.io/mediasession/#actual-playback-state
  MediaSessionPlaybackState mActualPlaybackState =
      MediaSessionPlaybackState::None;

  nsTHashMap<nsUint64HashKey, MediaSessionInfo> mMediaSessionInfoMap;
  MediaEventProducer<MediaMetadataBase> mMetadataChangedEvent;
  MediaEventProducer<nsTArray<MediaSessionAction>>
      mSupportedActionsChangedEvent;
  MediaEventProducer<PositionState> mPositionStateChangedEvent;
  MediaEventProducer<MediaSessionPlaybackState> mPlaybackStateChangedEvent;
  MediaPlaybackStatus mPlaybackStatusDelegate;
};

}  // namespace mozilla::dom

#endif  // DOM_MEDIA_MEDIACONTROL_MEDIASTATUSMANAGER_H_