/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "AudioChannelService.h"

#include "base/basictypes.h"

#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/Unused.h"

#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/TabParent.h"

#include "nsContentUtils.h"
#include "nsIScriptSecurityManager.h"
#include "nsISupportsPrimitives.h"
#include "nsThreadUtils.h"
#include "nsHashPropertyBag.h"
#include "nsComponentManagerUtils.h"
#include "nsGlobalWindow.h"
#include "nsPIDOMWindow.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/dom/SettingChangeNotificationBinding.h"

#include "mozilla/Preferences.h"

using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::hal;

namespace {

// If true, any new AudioChannelAgent will be muted when created.
bool sAudioChannelMutedByDefault = false;
bool sAudioChannelCompeting = false;
bool sAudioChannelCompetingAllAgents = false;
bool sXPCOMShuttingDown = false;

class NotifyChannelActiveRunnable final : public Runnable
{
public:
  NotifyChannelActiveRunnable(uint64_t aWindowID, AudioChannel aAudioChannel,
                              bool aActive)
    : mWindowID(aWindowID)
    , mAudioChannel(aAudioChannel)
    , mActive(aActive)
  {}

  NS_IMETHOD Run() override
  {
    nsCOMPtr<nsIObserverService> observerService =
      services::GetObserverService();
    if (NS_WARN_IF(!observerService)) {
      return NS_ERROR_FAILURE;
    }

    nsCOMPtr<nsISupportsPRUint64> wrapper =
      do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID);
    if (NS_WARN_IF(!wrapper)) {
       return NS_ERROR_FAILURE;
    }

    wrapper->SetData(mWindowID);

    nsAutoString name;
    AudioChannelService::GetAudioChannelString(mAudioChannel, name);

    nsAutoCString topic;
    topic.Assign("audiochannel-activity-");
    topic.Append(NS_ConvertUTF16toUTF8(name));

    observerService->NotifyObservers(wrapper, topic.get(),
                                     mActive
                                       ? u"active"
                                       : u"inactive");

    // TODO : remove b2g related event in bug1299390.
    observerService->NotifyObservers(wrapper,
                                     "media-playback",
                                     mActive
                                       ? u"active"
                                       : u"inactive");

    MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
           ("NotifyChannelActiveRunnable, type = %d, active = %d\n",
            mAudioChannel, mActive));

    return NS_OK;
  }

private:
  const uint64_t mWindowID;
  const AudioChannel mAudioChannel;
  const bool mActive;
};

bool
IsParentProcess()
{
  return XRE_GetProcessType() == GeckoProcessType_Default;
}

class AudioPlaybackRunnable final : public Runnable
{
public:
  AudioPlaybackRunnable(nsPIDOMWindowOuter* aWindow, bool aActive,
                        AudioChannelService::AudibleChangedReasons aReason)
    : mWindow(aWindow)
    , mActive(aActive)
    , mReason(aReason)
  {}

 NS_IMETHOD Run() override
 {
    nsCOMPtr<nsIObserverService> observerService =
      services::GetObserverService();
    if (NS_WARN_IF(!observerService)) {
      return NS_ERROR_FAILURE;
    }

    nsAutoString state;
    GetActiveState(state);

    observerService->NotifyObservers(ToSupports(mWindow),
                                     "audio-playback",
                                     state.get());

    MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
           ("AudioPlaybackRunnable, active = %d, reason = %d\n",
            mActive, mReason));

    return NS_OK;
  }

private:
  void GetActiveState(nsAString& astate)
  {
    if (mActive) {
      CopyASCIItoUTF16("active", astate);
    } else {
      if(mReason == AudioChannelService::AudibleChangedReasons::ePauseStateChanged) {
        CopyASCIItoUTF16("inactive-pause", astate);
      } else {
        CopyASCIItoUTF16("inactive-nonaudible", astate);
      }
    }
  }

  nsCOMPtr<nsPIDOMWindowOuter> mWindow;
  bool mActive;
  AudioChannelService::AudibleChangedReasons mReason;
};

bool
IsEnableAudioCompetingForAllAgents()
{
  // In general, the audio competing should only be for audible media and it
  // helps user can focus on one media at the same time.
  return sAudioChannelCompetingAllAgents;
}

} // anonymous namespace

StaticRefPtr<AudioChannelService> gAudioChannelService;

// Mappings from 'mozaudiochannel' attribute strings to an enumeration.
static const nsAttrValue::EnumTable kMozAudioChannelAttributeTable[] = {
  { "normal",             (int16_t)AudioChannel::Normal },
  { "content",            (int16_t)AudioChannel::Content },
  { "notification",       (int16_t)AudioChannel::Notification },
  { "alarm",              (int16_t)AudioChannel::Alarm },
  { "telephony",          (int16_t)AudioChannel::Telephony },
  { "ringer",             (int16_t)AudioChannel::Ringer },
  { "publicnotification", (int16_t)AudioChannel::Publicnotification },
  { "system",             (int16_t)AudioChannel::System },
  { nullptr,              0 }
};

/* static */ void
AudioChannelService::CreateServiceIfNeeded()
{
  MOZ_ASSERT(NS_IsMainThread());

  if (!gAudioChannelService) {
    gAudioChannelService = new AudioChannelService();
  }
}

/* static */ already_AddRefed<AudioChannelService>
AudioChannelService::GetOrCreate()
{
  if (sXPCOMShuttingDown) {
    return nullptr;
  }

  CreateServiceIfNeeded();
  RefPtr<AudioChannelService> service = gAudioChannelService.get();
  return service.forget();
}

/* static */ PRLogModuleInfo*
AudioChannelService::GetAudioChannelLog()
{
  static PRLogModuleInfo *gAudioChannelLog;
  if (!gAudioChannelLog) {
    gAudioChannelLog = PR_NewLogModule("AudioChannel");
  }
  return gAudioChannelLog;
}

/* static */ void
AudioChannelService::Shutdown()
{
  if (gAudioChannelService) {
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->RemoveObserver(gAudioChannelService, "xpcom-shutdown");
      obs->RemoveObserver(gAudioChannelService, "outer-window-destroyed");

      if (IsParentProcess()) {
        obs->RemoveObserver(gAudioChannelService, "ipc:content-shutdown");
      }
    }

    gAudioChannelService->mWindows.Clear();
    gAudioChannelService->mPlayingChildren.Clear();
    gAudioChannelService->mTabParents.Clear();

    gAudioChannelService = nullptr;
  }
}

/* static */ bool
AudioChannelService::IsEnableAudioCompeting()
{
  CreateServiceIfNeeded();
  return sAudioChannelCompeting;
}

NS_INTERFACE_MAP_BEGIN(AudioChannelService)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAudioChannelService)
  NS_INTERFACE_MAP_ENTRY(nsIAudioChannelService)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_END

NS_IMPL_ADDREF(AudioChannelService)
NS_IMPL_RELEASE(AudioChannelService)

AudioChannelService::AudioChannelService()
  : mDefChannelChildID(CONTENT_PROCESS_ID_UNKNOWN)
  , mTelephonyChannel(false)
  , mContentOrNormalChannel(false)
  , mAnyChannel(false)
{
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->AddObserver(this, "xpcom-shutdown", false);
    obs->AddObserver(this, "outer-window-destroyed", false);
    if (IsParentProcess()) {
      obs->AddObserver(this, "ipc:content-shutdown", false);
    }
  }

  Preferences::AddBoolVarCache(&sAudioChannelMutedByDefault,
                               "dom.audiochannel.mutedByDefault");
  Preferences::AddBoolVarCache(&sAudioChannelCompeting,
                               "dom.audiochannel.audioCompeting");
  Preferences::AddBoolVarCache(&sAudioChannelCompetingAllAgents,
                               "dom.audiochannel.audioCompeting.allAgents");
}

AudioChannelService::~AudioChannelService()
{
}

void
AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
                                               AudibleState aAudible)
{
  MOZ_ASSERT(aAgent);

  uint64_t windowID = aAgent->WindowID();
  AudioChannelWindow* winData = GetWindowData(windowID);
  if (!winData) {
    winData = new AudioChannelWindow(windowID);
    mWindows.AppendElement(winData);
  }

  // To make sure agent would be alive because AppendAgent() would trigger the
  // callback function of AudioChannelAgentOwner that means the agent might be
  // released in their callback.
  RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent);
  winData->AppendAgent(aAgent, aAudible);

  MaybeSendStatusUpdate();
}

void
AudioChannelService::UnregisterAudioChannelAgent(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);

  AudioChannelWindow* winData = GetWindowData(aAgent->WindowID());
  if (!winData) {
    return;
  }

  // To make sure agent would be alive because AppendAgent() would trigger the
  // callback function of AudioChannelAgentOwner that means the agent might be
  // released in their callback.
  RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent);
  winData->RemoveAgent(aAgent);

  MaybeSendStatusUpdate();
}

void
AudioChannelService::RegisterTabParent(TabParent* aTabParent)
{
  MOZ_ASSERT(aTabParent);
  MOZ_ASSERT(!mTabParents.Contains(aTabParent));
  mTabParents.AppendElement(aTabParent);
}

void
AudioChannelService::UnregisterTabParent(TabParent* aTabParent)
{
  MOZ_ASSERT(aTabParent);
  mTabParents.RemoveElement(aTabParent);
}

AudioPlaybackConfig
AudioChannelService::GetMediaConfig(nsPIDOMWindowOuter* aWindow,
                                    uint32_t aAudioChannel) const
{
  MOZ_ASSERT(!aWindow || aWindow->IsOuterWindow());
  MOZ_ASSERT(aAudioChannel < NUMBER_OF_AUDIO_CHANNELS);

  AudioPlaybackConfig config(1.0, false,
                             nsISuspendedTypes::NONE_SUSPENDED);

  if (!aWindow || !aWindow->IsOuterWindow()) {
    config.SetConfig(0.0, true,
                     nsISuspendedTypes::SUSPENDED_BLOCK);
    return config;
  }

  AudioChannelWindow* winData = nullptr;
  nsCOMPtr<nsPIDOMWindowOuter> window = aWindow;

  // The volume must be calculated based on the window hierarchy. Here we go up
  // to the top window and we calculate the volume and the muted flag.
  do {
    winData = GetWindowData(window->WindowID());
    if (winData) {
      config.mVolume *= winData->mChannels[aAudioChannel].mVolume;
      config.mMuted = config.mMuted || winData->mChannels[aAudioChannel].mMuted;
      config.mSuspend = winData->mOwningAudioFocus ?
        config.mSuspend : nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE;
    }

    config.mVolume *= window->GetAudioVolume();
    config.mMuted = config.mMuted || window->GetAudioMuted();
    if (window->GetMediaSuspend() != nsISuspendedTypes::NONE_SUSPENDED) {
      config.mSuspend = window->GetMediaSuspend();
    }

    nsCOMPtr<nsPIDOMWindowOuter> win = window->GetScriptableParentOrNull();
    if (!win) {
      break;
    }

    window = do_QueryInterface(win);

    // If there is no parent, or we are the toplevel we don't continue.
  } while (window && window != aWindow);

  return config;
}

void
AudioChannelService::AudioAudibleChanged(AudioChannelAgent* aAgent,
                                         AudibleState aAudible,
                                         AudibleChangedReasons aReason)
{
  MOZ_ASSERT(aAgent);

  uint64_t windowID = aAgent->WindowID();
  AudioChannelWindow* winData = GetWindowData(windowID);
  if (winData) {
    winData->AudioAudibleChanged(aAgent, aAudible, aReason);
  }
}

bool
AudioChannelService::TelephonyChannelIsActive()
{
  nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator windowsIter(mWindows);
  while (windowsIter.HasMore()) {
    AudioChannelWindow* next = windowsIter.GetNext();
    if (next->mChannels[(uint32_t)AudioChannel::Telephony].mNumberOfAgents != 0 &&
        !next->mChannels[(uint32_t)AudioChannel::Telephony].mMuted) {
      return true;
    }
  }

  if (IsParentProcess()) {
    nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator
      childrenIter(mPlayingChildren);
    while (childrenIter.HasMore()) {
      AudioChannelChildStatus* child = childrenIter.GetNext();
      if (child->mActiveTelephonyChannel) {
        return true;
      }
    }
  }

  return false;
}

bool
AudioChannelService::ContentOrNormalChannelIsActive()
{
  // This method is meant to be used just by the child to send status update.
  MOZ_ASSERT(!IsParentProcess());

  nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(mWindows);
  while (iter.HasMore()) {
    AudioChannelWindow* next = iter.GetNext();
    if (next->mChannels[(uint32_t)AudioChannel::Content].mNumberOfAgents > 0 ||
        next->mChannels[(uint32_t)AudioChannel::Normal].mNumberOfAgents > 0) {
      return true;
    }
  }
  return false;
}

AudioChannelService::AudioChannelChildStatus*
AudioChannelService::GetChildStatus(uint64_t aChildID) const
{
  nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator
    iter(mPlayingChildren);
  while (iter.HasMore()) {
    AudioChannelChildStatus* child = iter.GetNext();
    if (child->mChildID == aChildID) {
      return child;
    }
  }

  return nullptr;
}

void
AudioChannelService::RemoveChildStatus(uint64_t aChildID)
{
  nsTObserverArray<nsAutoPtr<AudioChannelChildStatus>>::ForwardIterator
    iter(mPlayingChildren);
  while (iter.HasMore()) {
    nsAutoPtr<AudioChannelChildStatus>& child = iter.GetNext();
    if (child->mChildID == aChildID) {
      mPlayingChildren.RemoveElement(child);
      break;
    }
  }
}

bool
AudioChannelService::ProcessContentOrNormalChannelIsActive(uint64_t aChildID)
{
  AudioChannelChildStatus* child = GetChildStatus(aChildID);
  if (!child) {
    return false;
  }

  return child->mActiveContentOrNormalChannel;
}

bool
AudioChannelService::AnyAudioChannelIsActive()
{
  nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(mWindows);
  while (iter.HasMore()) {
    AudioChannelWindow* next = iter.GetNext();
    for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) {
      if (next->mChannels[kMozAudioChannelAttributeTable[i].value].mNumberOfAgents
          != 0) {
        return true;
      }
    }
  }

  if (IsParentProcess()) {
    return !mPlayingChildren.IsEmpty();
  }

  return false;
}

NS_IMETHODIMP
AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic,
                             const char16_t* aData)
{
  if (!strcmp(aTopic, "xpcom-shutdown")) {
    sXPCOMShuttingDown = true;
    Shutdown();
  } else if (!strcmp(aTopic, "outer-window-destroyed")) {
    nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
    NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);

    uint64_t outerID;
    nsresult rv = wrapper->GetData(&outerID);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    nsAutoPtr<AudioChannelWindow> winData;
    {
      nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator
        iter(mWindows);
      while (iter.HasMore()) {
        nsAutoPtr<AudioChannelWindow>& next = iter.GetNext();
        if (next->mWindowID == outerID) {
          uint32_t pos = mWindows.IndexOf(next);
          winData = next.forget();
          mWindows.RemoveElementAt(pos);
          break;
        }
      }
    }

    if (winData) {
      nsTObserverArray<AudioChannelAgent*>::ForwardIterator
        iter(winData->mAgents);
      while (iter.HasMore()) {
        iter.GetNext()->WindowVolumeChanged();
      }
    }
  } else if (!strcmp(aTopic, "ipc:content-shutdown")) {
    nsCOMPtr<nsIPropertyBag2> props = do_QueryInterface(aSubject);
    if (!props) {
      NS_WARNING("ipc:content-shutdown message without property bag as subject");
      return NS_OK;
    }

    uint64_t childID = 0;
    nsresult rv = props->GetPropertyAsUint64(NS_LITERAL_STRING("childID"),
                                             &childID);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    if (mDefChannelChildID == childID) {
      SetDefaultVolumeControlChannelInternal(-1, false, childID);
      mDefChannelChildID = CONTENT_PROCESS_ID_UNKNOWN;
    }

    RemoveChildStatus(childID);
  }

  return NS_OK;
}

void
AudioChannelService::RefreshAgentsVolumeAndPropagate(AudioChannel aAudioChannel,
                                                     nsPIDOMWindowOuter* aWindow)
{
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
  if (!topWindow) {
    return;
  }

  AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());
  if (!winData) {
    return;
  }

  for (uint32_t i = 0; i < mTabParents.Length(); ++i) {
    mTabParents[i]->AudioChannelChangeNotification(aWindow, aAudioChannel,
                                                   winData->mChannels[(uint32_t)aAudioChannel].mVolume,
                                                   winData->mChannels[(uint32_t)aAudioChannel].mMuted);
  }

  RefreshAgentsVolume(aWindow);
}

void
AudioChannelService::RefreshAgents(nsPIDOMWindowOuter* aWindow,
                                   mozilla::function<void(AudioChannelAgent*)> aFunc)
{
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
  if (!topWindow) {
    return;
  }

  AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());
  if (!winData) {
    return;
  }

  nsTObserverArray<AudioChannelAgent*>::ForwardIterator
    iter(winData->mAgents);
  while (iter.HasMore()) {
    aFunc(iter.GetNext());
  }
}

void
AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow)
{
  RefreshAgents(aWindow, [] (AudioChannelAgent* agent) {
    agent->WindowVolumeChanged();
  });
}

void
AudioChannelService::RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow,
                                          nsSuspendedTypes aSuspend)
{
  RefreshAgents(aWindow, [aSuspend] (AudioChannelAgent* agent) {
    agent->WindowSuspendChanged(aSuspend);
  });
}

void
AudioChannelService::SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow,
                                            uint64_t aInnerWindowID,
                                            bool aCapture)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelService, SetWindowAudioCaptured, window = %p, "
          "aCapture = %d\n", aWindow, aCapture));

  nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
  if (!topWindow) {
    return;
  }

  AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());

  // This can happen, but only during shutdown, because the the outer window
  // changes ScriptableTop, so that its ID is different.
  // In this case either we are capturing, and it's too late because the window
  // has been closed anyways, or we are un-capturing, and everything has already
  // been cleaned up by the HTMLMediaElements or the AudioContexts.
  if (!winData) {
    return;
  }

  if (aCapture != winData->mIsAudioCaptured) {
    winData->mIsAudioCaptured = aCapture;
    nsTObserverArray<AudioChannelAgent*>::ForwardIterator
      iter(winData->mAgents);
    while (iter.HasMore()) {
      iter.GetNext()->WindowAudioCaptureChanged(aInnerWindowID, aCapture);
    }
  }
}

/* static */ const nsAttrValue::EnumTable*
AudioChannelService::GetAudioChannelTable()
{
  return kMozAudioChannelAttributeTable;
}

/* static */ AudioChannel
AudioChannelService::GetAudioChannel(const nsAString& aChannel)
{
  for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) {
    if (aChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) {
      return static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value);
    }
  }

  return AudioChannel::Normal;
}

/* static */ AudioChannel
AudioChannelService::GetDefaultAudioChannel()
{
  nsAutoString audioChannel(Preferences::GetString("media.defaultAudioChannel"));
  if (audioChannel.IsEmpty()) {
    return AudioChannel::Normal;
  }

  for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) {
    if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) {
      return static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value);
    }
  }

  return AudioChannel::Normal;
}

/* static */ void
AudioChannelService::GetAudioChannelString(AudioChannel aChannel,
                                           nsAString& aString)
{
  aString.AssignASCII("normal");

  for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) {
    if (aChannel ==
        static_cast<AudioChannel>(kMozAudioChannelAttributeTable[i].value)) {
      aString.AssignASCII(kMozAudioChannelAttributeTable[i].tag);
      break;
    }
  }
}

/* static */ void
AudioChannelService::GetDefaultAudioChannelString(nsAString& aString)
{
  aString.AssignASCII("normal");

  nsAutoString audioChannel(Preferences::GetString("media.defaultAudioChannel"));
  if (!audioChannel.IsEmpty()) {
    for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) {
      if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) {
        aString = audioChannel;
        break;
      }
    }
  }
}

AudioChannelService::AudioChannelWindow*
AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  AudioChannelWindow* winData = GetWindowData(aWindow->WindowID());
  if (!winData) {
    winData = new AudioChannelWindow(aWindow->WindowID());
    mWindows.AppendElement(winData);
  }

  return winData;
}

AudioChannelService::AudioChannelWindow*
AudioChannelService::GetWindowData(uint64_t aWindowID) const
{
  nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator
    iter(mWindows);
  while (iter.HasMore()) {
    AudioChannelWindow* next = iter.GetNext();
    if (next->mWindowID == aWindowID) {
      return next;
    }
  }

  return nullptr;
}

float
AudioChannelService::GetAudioChannelVolume(nsPIDOMWindowOuter* aWindow,
                                           AudioChannel aAudioChannel)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
  return winData->mChannels[(uint32_t)aAudioChannel].mVolume;
}

NS_IMETHODIMP
AudioChannelService::GetAudioChannelVolume(mozIDOMWindowProxy* aWindow,
                                           unsigned short aAudioChannel,
                                           float* aVolume)
{
  MOZ_ASSERT(NS_IsMainThread());

  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
  *aVolume = GetAudioChannelVolume(window, (AudioChannel)aAudioChannel);
  return NS_OK;
}

void
AudioChannelService::SetAudioChannelVolume(nsPIDOMWindowOuter* aWindow,
                                           AudioChannel aAudioChannel,
                                           float aVolume)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelService, SetAudioChannelVolume, window = %p, type = %d, "
          "volume = %f\n", aWindow, aAudioChannel, aVolume));

  AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
  winData->mChannels[(uint32_t)aAudioChannel].mVolume = aVolume;
  RefreshAgentsVolumeAndPropagate(aAudioChannel, aWindow);
}

NS_IMETHODIMP
AudioChannelService::SetAudioChannelVolume(mozIDOMWindowProxy* aWindow,
                                           unsigned short aAudioChannel,
                                           float aVolume)
{
  MOZ_ASSERT(NS_IsMainThread());

  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
  SetAudioChannelVolume(window, (AudioChannel)aAudioChannel, aVolume);
  return NS_OK;
}

bool
AudioChannelService::GetAudioChannelMuted(nsPIDOMWindowOuter* aWindow,
                                          AudioChannel aAudioChannel)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
  return winData->mChannels[(uint32_t)aAudioChannel].mMuted;
}

NS_IMETHODIMP
AudioChannelService::GetAudioChannelMuted(mozIDOMWindowProxy* aWindow,
                                          unsigned short aAudioChannel,
                                          bool* aMuted)
{
  MOZ_ASSERT(NS_IsMainThread());

  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
  *aMuted = GetAudioChannelMuted(window, (AudioChannel)aAudioChannel);
  return NS_OK;
}

void
AudioChannelService::SetAudioChannelMuted(nsPIDOMWindowOuter* aWindow,
                                          AudioChannel aAudioChannel,
                                          bool aMuted)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelService, SetAudioChannelMuted, window = %p, type = %d, "
          "mute = %d\n", aWindow, aAudioChannel, aMuted));

  if (aAudioChannel == AudioChannel::System) {
    // Workaround for bug1183033, system channel type can always playback.
    return;
  }

  AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
  winData->mChannels[(uint32_t)aAudioChannel].mMuted = aMuted;
  RefreshAgentsVolumeAndPropagate(aAudioChannel, aWindow);
}

NS_IMETHODIMP
AudioChannelService::SetAudioChannelMuted(mozIDOMWindowProxy* aWindow,
                                          unsigned short aAudioChannel,
                                          bool aMuted)
{
  MOZ_ASSERT(NS_IsMainThread());

  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
  SetAudioChannelMuted(window, (AudioChannel)aAudioChannel, aMuted);
  return NS_OK;
}

bool
AudioChannelService::IsAudioChannelActive(nsPIDOMWindowOuter* aWindow,
                                          AudioChannel aAudioChannel)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsOuterWindow());

  AudioChannelWindow* winData = GetOrCreateWindowData(aWindow);
  return !!winData->mChannels[(uint32_t)aAudioChannel].mNumberOfAgents;
}

NS_IMETHODIMP
AudioChannelService::IsAudioChannelActive(mozIDOMWindowProxy* aWindow,
                                          unsigned short aAudioChannel,
                                          bool* aActive)
{
  MOZ_ASSERT(NS_IsMainThread());

  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
  *aActive = IsAudioChannelActive(window, (AudioChannel)aAudioChannel);
  return NS_OK;
}
void
AudioChannelService::SetDefaultVolumeControlChannel(int32_t aChannel,
                                                    bool aVisible)
{
  SetDefaultVolumeControlChannelInternal(aChannel, aVisible,
                                         CONTENT_PROCESS_ID_MAIN);
}

void
AudioChannelService::SetDefaultVolumeControlChannelInternal(int32_t aChannel,
                                                            bool aVisible,
                                                            uint64_t aChildID)
{
  if (!IsParentProcess()) {
    ContentChild* cc = ContentChild::GetSingleton();
    if (cc) {
      cc->SendAudioChannelChangeDefVolChannel(aChannel, aVisible);
    }

    return;
  }

  // If this child is in the background and mDefChannelChildID is set to
  // others then it means other child in the foreground already set it's
  // own default channel.
  if (!aVisible && mDefChannelChildID != aChildID) {
    return;
  }

  // Workaround for the call screen app. The call screen app is running on the
  // main process, that will results in wrong visible state. Because we use the
  // docshell's active state as visible state, the main process is always
  // active. Therefore, we will see the strange situation that the visible
  // state of the call screen is always true. If the mDefChannelChildID is set
  // to others then it means other child in the foreground already set it's
  // own default channel already.
  // Summary :
  //   Child process : foreground app always can set type.
  //   Parent process : check the mDefChannelChildID.
  else if (aChildID == CONTENT_PROCESS_ID_MAIN &&
           mDefChannelChildID != CONTENT_PROCESS_ID_UNKNOWN) {
    return;
  }

  mDefChannelChildID = aVisible ? aChildID : CONTENT_PROCESS_ID_UNKNOWN;
  nsAutoString channelName;

  if (aChannel == -1) {
    channelName.AssignASCII("unknown");
  } else {
    GetAudioChannelString(static_cast<AudioChannel>(aChannel), channelName);
  }

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, "default-volume-channel-changed",
                         channelName.get());
  }
}

void
AudioChannelService::MaybeSendStatusUpdate()
{
  if (IsParentProcess()) {
    return;
  }

  bool telephonyChannel = TelephonyChannelIsActive();
  bool contentOrNormalChannel = ContentOrNormalChannelIsActive();
  bool anyChannel = AnyAudioChannelIsActive();

  if (telephonyChannel == mTelephonyChannel &&
      contentOrNormalChannel == mContentOrNormalChannel &&
      anyChannel == mAnyChannel) {
    return;
  }

  mTelephonyChannel = telephonyChannel;
  mContentOrNormalChannel = contentOrNormalChannel;
  mAnyChannel = anyChannel;

  ContentChild* cc = ContentChild::GetSingleton();
  if (cc) {
    cc->SendAudioChannelServiceStatus(telephonyChannel, contentOrNormalChannel,
                                      anyChannel);
  }
}

void
AudioChannelService::ChildStatusReceived(uint64_t aChildID,
                                         bool aTelephonyChannel,
                                         bool aContentOrNormalChannel,
                                         bool aAnyChannel)
{
  if (!aAnyChannel) {
    RemoveChildStatus(aChildID);
    return;
  }

  AudioChannelChildStatus* data = GetChildStatus(aChildID);
  if (!data) {
    data = new AudioChannelChildStatus(aChildID);
    mPlayingChildren.AppendElement(data);
  }

  data->mActiveTelephonyChannel = aTelephonyChannel;
  data->mActiveContentOrNormalChannel = aContentOrNormalChannel;
}

void
AudioChannelService::RefreshAgentsAudioFocusChanged(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);

  nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator
    iter(mWindows);
  while (iter.HasMore()) {
    AudioChannelWindow* winData = iter.GetNext();
    if (winData->mOwningAudioFocus) {
      winData->AudioFocusChanged(aAgent);
    }
  }
}

void
AudioChannelService::AudioChannelWindow::RequestAudioFocus(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);

  // Don't need to check audio focus for window-less agent.
  if (!aAgent->Window()) {
    return;
  }

  // We already have the audio focus. No operation is needed.
  if (mOwningAudioFocus) {
    return;
  }

  // Only foreground window can request audio focus, but it would still own the
  // audio focus even it goes to background. Audio focus would be abandoned
  // only when other foreground window starts audio competing.
  // One exception is if the pref "media.block-autoplay-until-in-foreground"
  // is on and the background page is the non-visited before. Because the media
  // in that page would be blocked until the page is going to foreground.
  mOwningAudioFocus = (!(aAgent->Window()->IsBackground()) ||
                       aAgent->Window()->GetMediaSuspend() == nsISuspendedTypes::SUSPENDED_BLOCK) ;

  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelWindow, RequestAudioFocus, this = %p, "
          "agent = %p, owning audio focus = %d\n",
          this, aAgent, mOwningAudioFocus));
}

void
AudioChannelService::AudioChannelWindow::NotifyAudioCompetingChanged(AudioChannelAgent* aAgent)
{
  // This function may be called after RemoveAgentAndReduceAgentsNum(), so the
  // agent may be not contained in mAgent. In addition, the agent would still
  // be alive because we have kungFuDeathGrip in UnregisterAudioChannelAgent().
  MOZ_ASSERT(aAgent);

  RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
  MOZ_ASSERT(service);

  if (!service->IsEnableAudioCompeting()) {
    return;
  }

  if (!IsAgentInvolvingInAudioCompeting(aAgent)) {
    return;
  }

  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelWindow, NotifyAudioCompetingChanged, this = %p, "
          "agent = %p\n",
          this, aAgent));

  service->RefreshAgentsAudioFocusChanged(aAgent);
}

bool
AudioChannelService::AudioChannelWindow::IsAgentInvolvingInAudioCompeting(AudioChannelAgent* aAgent) const
{
  MOZ_ASSERT(aAgent);

  if(!mOwningAudioFocus) {
    return false;
  }

  if (IsAudioCompetingInSameTab()) {
    return false;
  }

  // TODO : add MediaSession::ambient kind, because it doens't interact with
  // other kinds.
  return true;
}

bool
AudioChannelService::AudioChannelWindow::IsAudioCompetingInSameTab() const
{
  bool hasMultipleActiveAgents = IsEnableAudioCompetingForAllAgents() ?
    mAgents.Length() > 1 : mAudibleAgents.Length() > 1;
  return mOwningAudioFocus && hasMultipleActiveAgents;
}

void
AudioChannelService::AudioChannelWindow::AudioFocusChanged(AudioChannelAgent* aNewPlayingAgent)
{
  // This agent isn't always known for the current window, because it can comes
  // from other window.
  MOZ_ASSERT(aNewPlayingAgent);

  if (IsInactiveWindow()) {
    // These would happen in two situations,
    // (1) Audio in page A was ended, and another page B want to play audio.
    //     Page A should abandon its focus.
    // (2) Audio was paused by remote-control, page should still own the focus.
    mOwningAudioFocus = IsContainingPlayingAgent(aNewPlayingAgent);
  } else {
    nsTObserverArray<AudioChannelAgent*>::ForwardIterator
      iter(IsEnableAudioCompetingForAllAgents() ? mAgents : mAudibleAgents);
    while (iter.HasMore()) {
      AudioChannelAgent* agent = iter.GetNext();
      MOZ_ASSERT(agent);

      // Don't need to update the playing state of new playing agent.
      if (agent == aNewPlayingAgent) {
        continue;
      }

      uint32_t type = GetCompetingBehavior(agent,
                                           aNewPlayingAgent->AudioChannelType());

      // If window will be suspended, it needs to abandon the audio focus
      // because only one window can own audio focus at a time. However, we
      // would support multiple audio focus at the same time in the future.
      mOwningAudioFocus = (type == nsISuspendedTypes::NONE_SUSPENDED);

      // TODO : support other behaviors which are definded in MediaSession API.
      switch (type) {
        case nsISuspendedTypes::NONE_SUSPENDED:
        case nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE:
          agent->WindowSuspendChanged(type);
          break;
      }
    }
  }

  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelWindow, AudioFocusChanged, this = %p, "
          "OwningAudioFocus = %d\n", this, mOwningAudioFocus));
}

bool
AudioChannelService::AudioChannelWindow::IsContainingPlayingAgent(AudioChannelAgent* aAgent) const
{
  return (aAgent->WindowID() == mWindowID);
}

uint32_t
AudioChannelService::AudioChannelWindow::GetCompetingBehavior(AudioChannelAgent* aAgent,
                                                              int32_t aIncomingChannelType) const
{
  MOZ_ASSERT(aAgent);
  MOZ_ASSERT(IsEnableAudioCompetingForAllAgents() ?
    mAgents.Contains(aAgent) : mAudibleAgents.Contains(aAgent));

  uint32_t competingBehavior = nsISuspendedTypes::NONE_SUSPENDED;
  int32_t presentChannelType = aAgent->AudioChannelType();

  // TODO : add other competing cases for MediaSession API
  if (presentChannelType == int32_t(AudioChannel::Normal) &&
      aIncomingChannelType == int32_t(AudioChannel::Normal)) {
    competingBehavior = nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE;
  }

  MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
         ("AudioChannelWindow, GetCompetingBehavior, this = %p, "
          "present type = %d, incoming channel = %d, behavior = %d\n",
          this, presentChannelType, aIncomingChannelType, competingBehavior));

  return competingBehavior;
}

/* static */ bool
AudioChannelService::IsAudioChannelMutedByDefault()
{
  CreateServiceIfNeeded();
  return sAudioChannelMutedByDefault;
}

void
AudioChannelService::AudioChannelWindow::AppendAgent(AudioChannelAgent* aAgent,
                                                     AudibleState aAudible)
{
  MOZ_ASSERT(aAgent);

  RequestAudioFocus(aAgent);
  AppendAgentAndIncreaseAgentsNum(aAgent);
  AudioCapturedChanged(aAgent, AudioCaptureState::eCapturing);
  if (aAudible == AudibleState::eAudible) {
    AudioAudibleChanged(aAgent,
                        AudibleState::eAudible,
                        AudibleChangedReasons::eDataAudibleChanged);
  } else if (IsEnableAudioCompetingForAllAgents() &&
             aAudible != AudibleState::eAudible) {
    NotifyAudioCompetingChanged(aAgent);
  }
}

void
AudioChannelService::AudioChannelWindow::RemoveAgent(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);

  RemoveAgentAndReduceAgentsNum(aAgent);
  AudioCapturedChanged(aAgent, AudioCaptureState::eNotCapturing);
  AudioAudibleChanged(aAgent,
                      AudibleState::eNotAudible,
                      AudibleChangedReasons::ePauseStateChanged);
}

void
AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);
  MOZ_ASSERT(!mAgents.Contains(aAgent));

  int32_t channel = aAgent->AudioChannelType();
  mAgents.AppendElement(aAgent);

  ++mChannels[channel].mNumberOfAgents;

  // The first one, we must inform the BrowserElementAudioChannel.
  if (mChannels[channel].mNumberOfAgents == 1) {
    NotifyChannelActive(aAgent->WindowID(),
                        static_cast<AudioChannel>(channel),
                        true);
  }
}

void
AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum(AudioChannelAgent* aAgent)
{
  MOZ_ASSERT(aAgent);
  MOZ_ASSERT(mAgents.Contains(aAgent));

  int32_t channel = aAgent->AudioChannelType();
  mAgents.RemoveElement(aAgent);

  MOZ_ASSERT(mChannels[channel].mNumberOfAgents > 0);
  --mChannels[channel].mNumberOfAgents;

  if (mChannels[channel].mNumberOfAgents == 0) {
    NotifyChannelActive(aAgent->WindowID(),
                        static_cast<AudioChannel>(channel),
                        false);
  }
}

void
AudioChannelService::AudioChannelWindow::AudioCapturedChanged(AudioChannelAgent* aAgent,
                                                              AudioCaptureState aCapture)
{
  MOZ_ASSERT(aAgent);

  if (mIsAudioCaptured) {
    aAgent->WindowAudioCaptureChanged(aAgent->InnerWindowID(), aCapture);
  }
}

void
AudioChannelService::AudioChannelWindow::AudioAudibleChanged(AudioChannelAgent* aAgent,
                                                             AudibleState aAudible,
                                                             AudibleChangedReasons aReason)
{
  MOZ_ASSERT(aAgent);

  if (aAudible == AudibleState::eAudible) {
    AppendAudibleAgentIfNotContained(aAgent, aReason);
  } else {
    RemoveAudibleAgentIfContained(aAgent, aReason);
  }

  if (aAudible == AudibleState::eAudible) {
    NotifyAudioCompetingChanged(aAgent);
  } else if (aAudible != AudibleState::eNotAudible) {
    MaybeNotifyMediaBlocked(aAgent);
  }
}

void
AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained(AudioChannelAgent* aAgent,
                                                                          AudibleChangedReasons aReason)
{
  MOZ_ASSERT(aAgent);
  MOZ_ASSERT(mAgents.Contains(aAgent));

  if (!mAudibleAgents.Contains(aAgent)) {
    mAudibleAgents.AppendElement(aAgent);
    if (IsFirstAudibleAgent()) {
      NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eAudible, aReason);
    }
  }
}

void
AudioChannelService::AudioChannelWindow::RemoveAudibleAgentIfContained(AudioChannelAgent* aAgent,
                                                                       AudibleChangedReasons aReason)
{
  MOZ_ASSERT(aAgent);

  if (mAudibleAgents.Contains(aAgent)) {
    mAudibleAgents.RemoveElement(aAgent);
    if (IsLastAudibleAgent()) {
      NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eNotAudible, aReason);
    }
  }
}

bool
AudioChannelService::AudioChannelWindow::IsFirstAudibleAgent() const
{
  return (mAudibleAgents.Length() == 1);
}

bool
AudioChannelService::AudioChannelWindow::IsLastAudibleAgent() const
{
  return mAudibleAgents.IsEmpty();
}

bool
AudioChannelService::AudioChannelWindow::IsInactiveWindow() const
{
  return IsEnableAudioCompetingForAllAgents() ?
    mAudibleAgents.IsEmpty() && mAgents.IsEmpty() : mAudibleAgents.IsEmpty();
}

void
AudioChannelService::AudioChannelWindow::NotifyAudioAudibleChanged(nsPIDOMWindowOuter* aWindow,
                                                                   AudibleState aAudible,
                                                                   AudibleChangedReasons aReason)
{
  RefPtr<AudioPlaybackRunnable> runnable =
    new AudioPlaybackRunnable(aWindow,
                              aAudible == AudibleState::eAudible,
                              aReason);
  DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed");
}

void
AudioChannelService::AudioChannelWindow::NotifyChannelActive(uint64_t aWindowID,
                                                             AudioChannel aChannel,
                                                             bool aActive)
{
  RefPtr<NotifyChannelActiveRunnable> runnable =
    new NotifyChannelActiveRunnable(aWindowID, aChannel, aActive);
  DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed");
}

void
AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlocked(AudioChannelAgent* aAgent)
{
  nsCOMPtr<nsPIDOMWindowOuter> window = aAgent->Window();
  if (!window) {
    return;
  }

  MOZ_ASSERT(window->IsOuterWindow());
  if (window->GetMediaSuspend() != nsISuspendedTypes::SUSPENDED_BLOCK) {
    return;
  }

  NS_DispatchToCurrentThread(NS_NewRunnableFunction([window] () -> void {
      nsCOMPtr<nsIObserverService> observerService =
        services::GetObserverService();
      if (NS_WARN_IF(!observerService)) {
        return;
      }

      observerService->NotifyObservers(ToSupports(window),
                                       "audio-playback",
                                       u"block");
    })
  );
}
