// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/media/media_internals_audio_focus_helper.h"

#include <algorithm>
#include <string>
#include <string_view>

#include "base/containers/adapters.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/values.h"
#include "content/browser/media/media_internals.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/media_session_service.h"
#include "content/public/browser/web_ui.h"

namespace content {

namespace {

const char kAudioFocusFunction[] = "media.onReceiveAudioFocusState";
const char kAudioFocusIdKey[] = "id";
const char kAudioFocusSessionsKey[] = "sessions";

const char kAudioFocusForceDuck[] = "ForceDuck";
const char kAudioFocusPreferStop[] = "PreferStop";

const char kAudioFocusTypeGain[] = "Gain";
const char kAudioFocusTypeGainTransient[] = "GainTransient";
const char kAudioFocusTypeGainTransientMayDuck[] = "GainTransientMayDuck";
const char kAudioFocusTypeAmbient[] = "Ambient";

const char kMediaSessionStateActive[] = "Active";
const char kMediaSessionStateDucking[] = "Ducking";
const char kMediaSessionStateSuspended[] = "Suspended";
const char kMediaSessionStateInactive[] = "Inactive";

const char kMediaSessionPlaybackStatePaused[] = "Paused";
const char kMediaSessionPlaybackStatePlaying[] = "Playing";

const char kMediaSessionIsControllable[] = "Controllable";

const char kMediaSessionHasAudio[] = "HasAudio";
const char kMediaSessionHasVideo[] = "HasVideo";
const char kMediaSessionHasAudioVideo[] = "HasAudioVideo";

}  // namespace

MediaInternalsAudioFocusHelper::MediaInternalsAudioFocusHelper() = default;

MediaInternalsAudioFocusHelper::~MediaInternalsAudioFocusHelper() = default;

void MediaInternalsAudioFocusHelper::SendAudioFocusState() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!EnsureServiceConnection())
    return;

  // Get the audio focus state from the media session service.
  audio_focus_->GetFocusRequests(base::BindOnce(
      &MediaInternalsAudioFocusHelper::DidGetAudioFocusRequestList,
      base::Unretained(this)));
}

void MediaInternalsAudioFocusHelper::OnFocusGained(
    media_session::mojom::AudioFocusRequestStatePtr session) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&MediaInternalsAudioFocusHelper::SendAudioFocusState,
                     base::Unretained(this)));
}

void MediaInternalsAudioFocusHelper::OnFocusLost(
    media_session::mojom::AudioFocusRequestStatePtr session) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&MediaInternalsAudioFocusHelper::SendAudioFocusState,
                     base::Unretained(this)));
}

void MediaInternalsAudioFocusHelper::SetEnabled(bool enabled) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  enabled_ = enabled;

  EnsureServiceConnection();

  if (!enabled) {
    audio_focus_.reset();
    audio_focus_debug_.reset();
    receiver_.reset();
  }
}

bool MediaInternalsAudioFocusHelper::EnsureServiceConnection() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!enabled_)
    return false;

  // Connect to the media session service.
  if (!audio_focus_.is_bound()) {
    GetMediaSessionService().BindAudioFocusManager(
        audio_focus_.BindNewPipeAndPassReceiver());
    audio_focus_.set_disconnect_handler(base::BindOnce(
        &MediaInternalsAudioFocusHelper::OnMojoError, base::Unretained(this)));
  }

  // Connect to the media session service debug interface.
  if (!audio_focus_debug_.is_bound()) {
    GetMediaSessionService().BindAudioFocusManagerDebug(
        audio_focus_debug_.BindNewPipeAndPassReceiver());
    audio_focus_debug_.set_disconnect_handler(
        base::BindOnce(&MediaInternalsAudioFocusHelper::OnDebugMojoError,
                       base::Unretained(this)));
  }

  // Add the observer to receive audio focus events.
  if (!receiver_.is_bound()) {
    audio_focus_->AddObserver(receiver_.BindNewPipeAndPassRemote());

    receiver_.set_disconnect_handler(base::BindOnce(
        &MediaInternalsAudioFocusHelper::OnMojoError, base::Unretained(this)));
  }

  return true;
}

void MediaInternalsAudioFocusHelper::OnMojoError() {
  audio_focus_.reset();
  receiver_.reset();
}

void MediaInternalsAudioFocusHelper::OnDebugMojoError() {
  audio_focus_debug_.reset();
}

void MediaInternalsAudioFocusHelper::DidGetAudioFocusRequestList(
    std::vector<media_session::mojom::AudioFocusRequestStatePtr> stack) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!EnsureServiceConnection())
    return;

  audio_focus_data_.clear();
  request_state_.clear();

  // We should go backwards through the stack so the top of the stack is
  // always shown first in the list.
  base::ListValue stack_data;
  for (const auto& session : base::Reversed(stack)) {
    if (!session->request_id.has_value())
      continue;

    std::string id_string = session->request_id.value().ToString();
    base::DictValue media_session_data;
    media_session_data.Set(kAudioFocusIdKey, id_string);
    stack_data.Append(std::move(media_session_data));

    request_state_.emplace(id_string, session.Clone());

    audio_focus_debug_->GetDebugInfoForRequest(
        session->request_id.value(),
        base::BindOnce(
            &MediaInternalsAudioFocusHelper::DidGetAudioFocusDebugInfo,
            base::Unretained(this), id_string));
  }

  audio_focus_data_.Set(kAudioFocusSessionsKey, std::move(stack_data));

  // If the stack is empty then we should send an update to the web ui to clear
  // the list.
  if (stack.empty())
    SerializeAndSendUpdate(kAudioFocusFunction, audio_focus_data_);
}

void MediaInternalsAudioFocusHelper::DidGetAudioFocusDebugInfo(
    const std::string& id,
    media_session::mojom::MediaSessionDebugInfoPtr info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!EnsureServiceConnection())
    return;

  base::ListValue* sessions_list =
      audio_focus_data_.FindList(kAudioFocusSessionsKey);
  DCHECK(sessions_list);

  bool updated = false;
  for (auto& value : *sessions_list) {
    base::DictValue& session = value.GetDict();
    if (session.Find(kAudioFocusIdKey)->GetString() != id)
      continue;

    auto state = request_state_.find(id);
    CHECK(state != request_state_.end());

    session.Set("name", BuildNameString(state->second, info->name));
    session.Set("owner", info->owner);
    session.Set("state", BuildStateString(state->second, info->state));
    updated = true;
  }

  if (!updated)
    return;

  SerializeAndSendUpdate(kAudioFocusFunction, audio_focus_data_);
}

void MediaInternalsAudioFocusHelper::SerializeAndSendUpdate(
    std::string_view function,
    const base::DictValue& value) {
  base::ValueView args[] = {value};
  return MediaInternals::GetInstance()->SendUpdate(
      content::WebUI::GetJavascriptCall(function, args));
}

std::string MediaInternalsAudioFocusHelper::BuildNameString(
    const media_session::mojom::AudioFocusRequestStatePtr& state,
    const std::string& provided_name) const {
  std::string result;

  // Add the |source_name| (optional).
  if (state->source_name.has_value())
    base::StrAppend(&result, {state->source_name.value(), ":"});

  // Add the |request_id|.
  result.append(state->request_id.value().ToString());

  if (!provided_name.empty())
    base::StrAppend(&result, {" ", provided_name});

  return result;
}

std::string MediaInternalsAudioFocusHelper::BuildStateString(
    const media_session::mojom::AudioFocusRequestStatePtr& state,
    const std::string& provided_state) const {
  std::string result(" ");

  // Convert the AudioFocusType mojo enum to a string.
  switch (state->audio_focus_type) {
    case media_session::mojom::AudioFocusType::kGain:
      result.append(kAudioFocusTypeGain);
      break;
    case media_session::mojom::AudioFocusType::kGainTransient:
      result.append(kAudioFocusTypeGainTransient);
      break;
    case media_session::mojom::AudioFocusType::kGainTransientMayDuck:
      result.append(kAudioFocusTypeGainTransientMayDuck);
      break;
    case media_session::mojom::AudioFocusType::kAmbient:
      result.append(kAudioFocusTypeAmbient);
      break;
  }

  // Convert the MediaSessionInfo::SessionState mojo enum to a string.
  result.append(" ");
  switch (state->session_info->state) {
    case media_session::mojom::MediaSessionInfo::SessionState::kActive:
      result.append(kMediaSessionStateActive);
      break;
    case media_session::mojom::MediaSessionInfo::SessionState::kDucking:
      result.append(kMediaSessionStateDucking);
      break;
    case media_session::mojom::MediaSessionInfo::SessionState::kSuspended:
      result.append(kMediaSessionStateSuspended);
      break;
    case media_session::mojom::MediaSessionInfo::SessionState::kInactive:
      result.append(kMediaSessionStateInactive);
      break;
  }

  // Convert the MediaPlaybackState mojo enum to a string.
  result.append(" ");
  switch (state->session_info->playback_state) {
    case media_session::mojom::MediaPlaybackState::kPaused:
      result.append(kMediaSessionPlaybackStatePaused);
      break;
    case media_session::mojom::MediaPlaybackState::kPlaying:
      result.append(kMediaSessionPlaybackStatePlaying);
      break;
  }

  // Convert the audio_video_states to a string.
  if (state->session_info->audio_video_states) {
    result.append(" {");
    std::ranges::for_each(
        *state->session_info->audio_video_states, [&result](const auto& state) {
          result.append(" ");
          switch (state) {
            case media_session::mojom::MediaAudioVideoState::kAudioOnly:
              result.append(kMediaSessionHasAudio);
              break;
            case media_session::mojom::MediaAudioVideoState::kVideoOnly:
              result.append(kMediaSessionHasVideo);
              break;
            case media_session::mojom::MediaAudioVideoState::kAudioVideo:
              result.append(kMediaSessionHasAudioVideo);
              break;
            case media_session::mojom::MediaAudioVideoState::kDeprecatedUnknown:
              NOTREACHED();
          }
        });
    result.append(" }");
  }

  // Convert the |force_duck| boolean into a string.
  if (state->session_info->force_duck)
    base::StrAppend(&result, {" ", kAudioFocusForceDuck});

  // Convert the |prefer_stop_for_gain_focus_loss| boolean into a string.
  if (state->session_info->prefer_stop_for_gain_focus_loss)
    base::StrAppend(&result, {" ", kAudioFocusPreferStop});

  // Convert the |is_controllable| boolean into a string.
  if (state->session_info->is_controllable)
    base::StrAppend(&result, {" ", kMediaSessionIsControllable});

  if (!provided_state.empty())
    base::StrAppend(&result, {" ", provided_state});

  return result;
}

}  // namespace content
