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

#ifndef mozilla_dom_audiochannelservice_h__
#define mozilla_dom_audiochannelservice_h__

#include "nsIObserver.h"
#include "nsTObserverArray.h"
#include "nsTArray.h"

#include "AudioChannelAgent.h"
#include "nsAttrValue.h"
#include "mozilla/Logging.h"
#include "mozilla/UniquePtr.h"

#include <functional>

class nsPIDOMWindowOuter;
struct PRLogModuleInfo;

namespace mozilla::dom {

class AudioPlaybackConfig {
 public:
  AudioPlaybackConfig()
      : mVolume(1.0),
        mMuted(false),
        mSuspend(nsISuspendedTypes::NONE_SUSPENDED),
        mNumberOfAgents(0) {}

  AudioPlaybackConfig(float aVolume, bool aMuted, uint32_t aSuspended)
      : mVolume(aVolume),
        mMuted(aMuted),
        mSuspend(aSuspended),
        mNumberOfAgents(0) {}

  float mVolume;
  bool mMuted;
  uint32_t mSuspend;
  bool mCapturedAudio = false;
  uint32_t mNumberOfAgents;
};

class AudioChannelService final : public nsIObserver {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER

  /**
   * We use `AudibleState` to represent the audible state of an owner of audio
   * channel agent. Those information in AudioChannelWindow could help us to
   * determine if a tab is being audible or not, in order to tell Chrome JS to
   * show the sound indicator or delayed autoplay icon on the tab bar.
   *
   * - Sound indicator
   * When a tab is playing sound, we would show the sound indicator on tab bar
   * to tell users that this tab is producing sound now. In addition, the sound
   * indicator also give users an ablility to mute or unmute tab.
   *
   * When an AudioChannelWindow first contains an agent with state `eAudible`,
   * or an AudioChannelWindow losts its last agent with state `eAudible`, we
   * would notify Chrome JS about those changes, to tell them that a tab has
   * been being audible or not, in order to display or remove the indicator for
   * a corresponding tab.
   *
   * - Delayed autoplay icon (Play Tab icon)
   * When we enable delaying autoplay, which is to postpone the autoplay media
   * for unvisited tab until it first goes to foreground, or user click the
   * play tab icon to resume the delayed media.
   *
   * When an AudioChannelWindow first contains an agent with state `eAudible` or
   * `eMaybeAudible`, we would notify Chrome JS about this change, in order to
   * show the delayed autoplay tab icon to user, which is used to notice user
   * there is a media being delayed starting, and then user can click the play
   * tab icon to resume the start of media, or visit that tab to resume delayed
   * media automatically.
   *
   * According to our UX design, we don't show this icon for inaudible media.
   * The reason of showing the icon for a tab, where the agent starts with state
   * `eMaybeAudible`, is because some video might be silent in the beginning
   * but would soon become audible later.
   *
   * ---------------------------------------------------------------------------
   *
   * eNotAudible : agent is not audible
   * eMaybeAudible : agent is not audible now, but it might be audible later
   * eAudible : agent is audible now
   */
  enum AudibleState : uint8_t {
    eNotAudible = 0,
    eMaybeAudible = 1,
    eAudible = 2
  };

  enum AudioCaptureState : bool { eCapturing = true, eNotCapturing = false };

  enum AudibleChangedReasons : uint32_t {
    eVolumeChanged = 0,
    eDataAudibleChanged = 1,
    ePauseStateChanged = 2
  };

  /**
   * Returns the AudioChannelServce singleton.
   * If AudioChannelService doesn't exist, create and return new one.
   * Only to be called from main thread.
   */
  static already_AddRefed<AudioChannelService> GetOrCreate();

  /**
   * Returns the AudioChannelService singleton if one exists.
   * If AudioChannelService doesn't exist, returns null.
   */
  static already_AddRefed<AudioChannelService> Get();

  static LogModule* GetAudioChannelLog();

  static bool IsEnableAudioCompeting();

  /**
   * Any audio channel agent that starts playing should register itself to
   * this service, sharing the AudioChannel.
   */
  void RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
                                 AudibleState aAudible);

  /**
   * Any audio channel agent that stops playing should unregister itself to
   * this service.
   */
  void UnregisterAudioChannelAgent(AudioChannelAgent* aAgent);

  /**
   * Return the state to indicate this audioChannel for his window should keep
   * playing/muted/suspended.
   */
  AudioPlaybackConfig GetMediaConfig(nsPIDOMWindowOuter* aWindow) const;

  /**
   * Called this method when the audible state of the audio playback changed,
   * it would dispatch the playback event to observers which want to know the
   * actual audible state of the window.
   */
  void AudioAudibleChanged(AudioChannelAgent* aAgent, AudibleState aAudible,
                           AudibleChangedReasons aReason);

  bool IsWindowActive(nsPIDOMWindowOuter* aWindow);

  void RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow, float aVolume,
                           bool aMuted);

  // This method needs to know the inner window that wants to capture audio. We
  // group agents per top outer window, but we can have multiple innerWindow per
  // top outerWindow (subiframes, etc.) and we have to identify all the agents
  // just for a particular innerWindow.
  void SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow,
                              uint64_t aInnerWindowID, bool aCapture);

  void NotifyResumingDelayedMedia(nsPIDOMWindowOuter* aWindow);

 private:
  AudioChannelService();
  ~AudioChannelService();

  void RefreshAgents(nsPIDOMWindowOuter* aWindow,
                     const std::function<void(AudioChannelAgent*)>& aFunc);

  void RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow,
                            nsSuspendedTypes aSuspend);

  static void CreateServiceIfNeeded();

  /**
   * Shutdown the singleton.
   */
  static void Shutdown();

  void RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent);

  class AudioChannelWindow final {
   public:
    explicit AudioChannelWindow(uint64_t aWindowID)
        : mWindowID(aWindowID),
          mIsAudioCaptured(false),
          mShouldSendActiveMediaBlockStopEvent(false) {}

    void AudioAudibleChanged(AudioChannelAgent* aAgent, AudibleState aAudible,
                             AudibleChangedReasons aReason);

    void AppendAgent(AudioChannelAgent* aAgent, AudibleState aAudible);
    void RemoveAgent(AudioChannelAgent* aAgent);

    void NotifyMediaBlockStop(nsPIDOMWindowOuter* aWindow);

    uint64_t mWindowID;
    bool mIsAudioCaptured;
    AudioPlaybackConfig mConfig;

    // Raw pointer because the AudioChannelAgent must unregister itself.
    nsTObserverArray<AudioChannelAgent*> mAgents;
    nsTObserverArray<AudioChannelAgent*> mAudibleAgents;

    // If we've dispatched "activeMediaBlockStart" event, we must dispatch
    // another event "activeMediablockStop" when the window is resumed from
    // suspend-block.
    bool mShouldSendActiveMediaBlockStopEvent;

   private:
    void AppendAudibleAgentIfNotContained(AudioChannelAgent* aAgent,
                                          AudibleChangedReasons aReason);
    void RemoveAudibleAgentIfContained(AudioChannelAgent* aAgent,
                                       AudibleChangedReasons aReason);

    void AppendAgentAndIncreaseAgentsNum(AudioChannelAgent* aAgent);
    void RemoveAgentAndReduceAgentsNum(AudioChannelAgent* aAgent);

    bool IsFirstAudibleAgent() const;
    bool IsLastAudibleAgent() const;

    void NotifyAudioAudibleChanged(nsPIDOMWindowOuter* aWindow,
                                   AudibleState aAudible,
                                   AudibleChangedReasons aReason);

    void MaybeNotifyMediaBlockStart(AudioChannelAgent* aAgent);
  };

  AudioChannelWindow* GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow);

  AudioChannelWindow* GetWindowData(uint64_t aWindowID) const;

  nsTObserverArray<UniquePtr<AudioChannelWindow>> mWindows;
};

const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend);
const char* AudibleStateToStr(
    const AudioChannelService::AudibleState& aAudible);
const char* AudibleChangedReasonToStr(
    const AudioChannelService::AudibleChangedReasons& aReason);

}  // namespace mozilla::dom

#endif
