// Copyright 2013 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/webrtc/webrtc_internals.h"

#include <algorithm>
#include <cstddef>
#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/media/peer_connection_tracker_host.h"
#include "content/browser/web_contents/web_contents_view.h"
#include "content/browser/webrtc/webrtc_internals_connections_observer.h"
#include "content/browser/webrtc/webrtc_internals_ui_observer.h"
#include "content/public/browser/audio_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/webrtc_event_logger.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "media/audio/audio_debug_recording_session.h"
#include "media/audio/audio_manager.h"
#include "media/media_buildflags.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "services/audio/public/cpp/debug_recording_session_factory.h"
#include "services/device/public/mojom/wake_lock_provider.mojom.h"
#include "ui/shell_dialogs/select_file_policy.h"
#include "ui/shell_dialogs/selected_file_info.h"

using base::ProcessId;
using std::string;

namespace content {

namespace {

const base::FilePath::CharType kEventLogFilename[] =
    FILE_PATH_LITERAL("event_log");

constexpr char kGetUserMedia[] = "getUserMedia";
constexpr char kGetDisplayMedia[] = "getDisplayMedia";

// This is intended to limit DoS attacks against the browser process consisting
// of many getUserMedia()/getDisplayMedia() calls. See https://crbug.com/804440.
const size_t kMaxMediaEntries = 1000;

// getUserMedia()/getDisplayMedia() entries older than this are pruned so that
// long-lived browser sessions do not accumulate stale requests indefinitely.
constexpr base::TimeDelta kMaxMediaEntryAge = base::Hours(24);

// Controls the polling interval used to refresh getStats() data for
// chrome://webrtc-internals. The "interval" param is clamped to [200ms, 60s].
BASE_FEATURE(kWebRtcInternalsStatsPollingInterval,
             base::FEATURE_ENABLED_BY_DEFAULT);
constexpr base::TimeDelta kDefaultStatsPollingInterval = base::Seconds(1);
constexpr base::TimeDelta kMinStatsPollingInterval = base::Milliseconds(200);
constexpr base::TimeDelta kMaxStatsPollingInterval = base::Seconds(60);
const base::FeatureParam<base::TimeDelta> kStatsPollingIntervalDuration{
    &kWebRtcInternalsStatsPollingInterval, "interval",
    kDefaultStatsPollingInterval};

base::TimeDelta GetStatsPollingInterval() {
  return std::clamp(kStatsPollingIntervalDuration.Get(),
                    kMinStatsPollingInterval, kMaxStatsPollingInterval);
}

// Makes sure that |dict| has a List under path "log".
base::ListValue& EnsureLogList(base::DictValue& dict) {
  base::ListValue* log = dict.FindList("log");
  if (log)
    return *log;
  return dict.Set("log", base::ListValue())->GetList();
}

// Removes the log entry associated with a given record.
void FreeLogList(base::Value* value) {
  DCHECK(value->is_dict());
  value->GetDict().Remove("log");
}

}  // namespace

WebRTCInternals* WebRTCInternals::g_webrtc_internals = nullptr;

WebRTCInternals::PendingUpdate::PendingUpdate(const std::string& event_name,
                                              base::Value event_data)
    : event_name_(event_name), event_data_(std::move(event_data)) {}

WebRTCInternals::PendingUpdate::PendingUpdate(PendingUpdate&& other)
    : event_name_(other.event_name_),
      event_data_(std::move(other.event_data_)) {}

WebRTCInternals::PendingUpdate::~PendingUpdate() {
  DCHECK(thread_checker_.CalledOnValidThread());
}

const std::string& WebRTCInternals::PendingUpdate::event_name() const {
  DCHECK(thread_checker_.CalledOnValidThread());
  return event_name_;
}

const base::Value* WebRTCInternals::PendingUpdate::event_data() const {
  DCHECK(thread_checker_.CalledOnValidThread());
  return event_data_.is_none() ? nullptr : &event_data_;
}

WebRTCInternals::WebRTCInternals() : WebRTCInternals(500, true) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

WebRTCInternals::WebRTCInternals(int aggregate_updates_ms,
                                 bool should_block_power_saving)
    : peer_connection_data_(base::ListValue()),
      selection_type_(SelectionType::kAudioDebugRecordings),
      command_line_derived_logging_path_(
          base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
              switches::kWebRtcLocalEventLogging)),
      event_log_recordings_(false),
      num_connected_connections_(0),
      should_block_power_saving_(should_block_power_saving),
      aggregate_updates_ms_(aggregate_updates_ms) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!g_webrtc_internals);

  const base::FilePath default_path =
      GetContentClient()->browser()->GetDefaultDownloadDirectory();
  audio_debug_recordings_file_path_ = default_path;
  event_log_recordings_file_path_ = default_path;
  data_channel_recordings_file_path_ = default_path;

  if (!default_path.empty()) {
    audio_debug_recordings_file_path_ =
        audio_debug_recordings_file_path_.Append(
            FILE_PATH_LITERAL("audio_debug"));
    event_log_recordings_file_path_ =
        event_log_recordings_file_path_.Append(kEventLogFilename);
    data_channel_recordings_file_path_ =
        data_channel_recordings_file_path_.Append(
            FILE_PATH_LITERAL("data_channel"));
  }

  // Allow command-line based setting of (local) WebRTC event logging.
  if (!command_line_derived_logging_path_.empty()) {
    const base::FilePath local_logs_path =
        command_line_derived_logging_path_.Append(kEventLogFilename);
    WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
    if (logger) {
      logger->EnableLocalLogging(local_logs_path);
    }
    // For clarity's sake, though these aren't supposed to be regarded now:
    event_log_recordings_ = true;
    event_log_recordings_file_path_.clear();
  }

  g_webrtc_internals = this;
}

WebRTCInternals::~WebRTCInternals() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(g_webrtc_internals);
  g_webrtc_internals = nullptr;

  if (select_file_dialog_) {
    select_file_dialog_->ListenerDestroyed();
  }
}

WebRTCInternals* WebRTCInternals::CreateSingletonInstance() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!g_webrtc_internals);
  g_webrtc_internals = new WebRTCInternals;
  return g_webrtc_internals;
}

WebRTCInternals* WebRTCInternals::GetInstance() {
  // TODO(crbug.com/40837773): DCHECK calling from UI thread.
  // Currently, some unit tests call this from outside of the UI thread,
  // but that's not a real issue as these tests neglect setting
  // `g_webrtc_internals` to begin with, and therefore just ignore it.
  DCHECK(!g_webrtc_internals || BrowserThread::CurrentlyOn(BrowserThread::UI));
  return g_webrtc_internals;
}

void WebRTCInternals::OnPeerConnectionAdded(GlobalRenderFrameHostId frame_id,
                                            int lid,
                                            ProcessId pid,
                                            const string& url,
                                            const string& rtc_configuration) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // TODO(tommi): Consider changing this design so that webrtc-internals has
  // minimal impact if chrome://webrtc-internals isn't open.

  base::DictValue dict;
  dict.Set("rid", frame_id.child_id.value());
  dict.Set("lid", lid);
  dict.Set("pid", static_cast<int>(pid));
  dict.Set("rtcConfiguration", rtc_configuration);
  dict.Set("url", url);
  dict.Set("isOpen", true);
  dict.Set("connected", false);
  dict.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());

  if (!observers_.empty())
    SendUpdate("add-peer-connection", dict.Clone());

  peer_connection_data().Append(std::move(dict));

  if (render_process_id_set_.insert(frame_id.child_id).second) {
    RenderProcessHost* host = RenderProcessHost::FromID(frame_id.child_id);
    if (host)
      host->AddObserver(this);
  }
}

void WebRTCInternals::OnPeerConnectionRemoved(GlobalRenderFrameHostId frame_id,
                                              int lid) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto it = FindRecord(frame_id, lid);
  if (it != peer_connection_data().end()) {
    MaybeClosePeerConnection(*it);
    peer_connection_data().erase(it);
  }

  if (!observers_.empty()) {
    base::DictValue id;
    id.Set("rid", frame_id.child_id.value());
    id.Set("lid", lid);
    SendUpdate("remove-peer-connection", std::move(id));
  }
}

void WebRTCInternals::OnPeerConnectionUpdated(GlobalRenderFrameHostId frame_id,
                                              int lid,
                                              const string& type,
                                              const string& value) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto it = FindRecord(frame_id, lid);
  if (it == peer_connection_data().end())
    return;

  if (type == "oniceconnectionstatechange") {
    if (value == "\"connected\"" || value == "\"checking\"" ||
        value == "\"completed\"") {
      MaybeMarkPeerConnectionAsConnected(*it);
    } else if (value == "\"failed\"" || value == "\"disconnected\"" ||
               value == "\"closed\"" || value == "\"new\"") {
      MaybeMarkPeerConnectionAsNotConnected(*it);
    }
  } else if (type == "close") {
    MaybeClosePeerConnection(*it);
  } else if (type == "setConfiguration") {
    // Update the configuration we have for this connection.
    it->GetDict().Set("rtcConfiguration", value);
  }

  // Don't update entries if there aren't any observers.
  if (observers_.empty())
    return;

  base::DictValue log_entry;

  log_entry.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());
  log_entry.Set("type", type);
  log_entry.Set("value", value);

  base::DictValue update;
  update.Set("rid", frame_id.child_id.value());
  update.Set("lid", lid);
  update.Merge(log_entry.Clone());

  SendUpdate("update-peer-connection", std::move(update));

  // Append the update to the end of the log.
  EnsureLogList(it->GetDict()).Append(std::move(log_entry));
}

void WebRTCInternals::OnAddStandardStats(GlobalRenderFrameHostId frame_id,
                                         int lid,
                                         base::ListValue value) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (observers_.empty())
    return;

  base::DictValue dict;
  dict.Set("rid", frame_id.child_id.value());
  dict.Set("lid", lid);

  dict.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());
  dict.Set("reports", std::move(value));

  SendUpdate("add-standard-stats", std::move(dict));
}

void WebRTCInternals::PruneOldGetUserMediaRequests() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  const double min_timestamp =
      (base::Time::Now() - kMaxMediaEntryAge).InMillisecondsFSinceUnixEpoch();
  // Iterating backwards so that .erase() is safe.
  for (int i = get_user_media_requests_.size() - 1; i >= 0; --i) {
    std::optional<double> timestamp =
        get_user_media_requests_[i].GetDict().FindDouble("timestamp");
    if (timestamp && *timestamp < min_timestamp) {
      get_user_media_requests_.erase(get_user_media_requests_.begin() + i);
    }
  }
}

void WebRTCInternals::OnGetMedia(std::string_view request_type,
                                 GlobalRenderFrameHostId frame_id,
                                 base::ProcessId pid,
                                 int request_id,
                                 bool audio,
                                 bool video,
                                 const std::string& audio_constraints,
                                 const std::string& video_constraints) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  PruneOldGetUserMediaRequests();
  if (get_user_media_requests_.size() >= kMaxMediaEntries) {
    LOG(WARNING) << "Maximum number of tracked getUserMedia/getDisplayMedia "
                    "requests reached in webrtc-internals.";
    return;
  }

  RenderFrameHost* rfh = RenderFrameHost::FromID(frame_id);
  // Frame may be gone (and does not exist in tests).
  std::string origin = rfh ? rfh->GetLastCommittedOrigin().Serialize() : "";
  std::string url = rfh ? rfh->GetLastCommittedURL().spec() : "";

  base::DictValue dict;
  dict.Set("rid", frame_id.child_id.value());
  dict.Set("pid", static_cast<int>(pid));
  dict.Set("request_id", request_id);
  dict.Set("request_type", request_type);
  dict.Set("origin", origin);
  dict.Set("url", url);
  dict.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());
  if (audio)
    dict.Set("audio", audio_constraints);
  if (video)
    dict.Set("video", video_constraints);

  if (!observers_.empty())
    SendUpdate("add-media", dict.Clone());

  get_user_media_requests_.Append(std::move(dict));

  if (render_process_id_set_.insert(frame_id.child_id).second) {
    RenderProcessHost* rph = RenderProcessHost::FromID(frame_id.child_id);
    if (rph)
      rph->AddObserver(this);
  }
}

void WebRTCInternals::OnGetMediaSuccess(std::string_view request_type,
                                        GlobalRenderFrameHostId frame_id,
                                        base::ProcessId pid,
                                        int request_id,
                                        const std::string& stream_id,
                                        const std::string& audio_track_info,
                                        const std::string& video_track_info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  PruneOldGetUserMediaRequests();
  if (get_user_media_requests_.size() >= kMaxMediaEntries) {
    LOG(WARNING) << "Maximum number of tracked getUserMedia/getDisplayMedia "
                    "requests reached in webrtc-internals.";
    return;
  }

  base::DictValue dict;
  dict.Set("rid", frame_id.child_id.value());
  dict.Set("pid", static_cast<int>(pid));
  dict.Set("request_id", request_id);
  dict.Set("request_type", request_type);
  dict.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());
  dict.Set("stream_id", stream_id);
  if (!audio_track_info.empty())
    dict.Set("audio_track_info", audio_track_info);
  if (!video_track_info.empty())
    dict.Set("video_track_info", video_track_info);

  if (!observers_.empty())
    SendUpdate("update-media", dict.Clone());

  get_user_media_requests_.Append(std::move(dict));

  if (render_process_id_set_.insert(frame_id.child_id).second) {
    RenderProcessHost* host = RenderProcessHost::FromID(frame_id.child_id);
    if (host)
      host->AddObserver(this);
  }
}

void WebRTCInternals::OnGetMediaFailure(std::string_view request_type,
                                        GlobalRenderFrameHostId frame_id,
                                        base::ProcessId pid,
                                        int request_id,
                                        const std::string& error,
                                        const std::string& error_message) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  PruneOldGetUserMediaRequests();
  if (get_user_media_requests_.size() >= kMaxMediaEntries) {
    LOG(WARNING) << "Maximum number of tracked /getDisplayMedia "
                    "requests reached in webrtc-internals.";
    return;
  }

  base::DictValue dict;
  dict.Set("rid", frame_id.child_id.value());
  dict.Set("pid", static_cast<int>(pid));
  dict.Set("request_id", request_id);
  dict.Set("request_type", request_type);
  dict.Set("timestamp", base::Time::Now().InMillisecondsFSinceUnixEpoch());
  dict.Set("error", error);
  dict.Set("error_message", error_message);

  if (!observers_.empty())
    SendUpdate("update-media", dict.Clone());

  get_user_media_requests_.Append(std::move(dict));

  if (render_process_id_set_.insert(frame_id.child_id).second) {
    RenderProcessHost* host = RenderProcessHost::FromID(frame_id.child_id);
    if (host)
      host->AddObserver(this);
  }
}

void WebRTCInternals::OnGetUserMedia(GlobalRenderFrameHostId frame_id,
                                     base::ProcessId pid,
                                     int request_id,
                                     bool audio,
                                     bool video,
                                     const std::string& audio_constraints,
                                     const std::string& video_constraints) {
  OnGetMedia(kGetUserMedia, frame_id, pid, request_id, audio, video,
             audio_constraints, video_constraints);
}

void WebRTCInternals::OnGetUserMediaSuccess(
    GlobalRenderFrameHostId frame_id,
    base::ProcessId pid,
    int request_id,
    const std::string& stream_id,
    const std::string& audio_track_info,
    const std::string& video_track_info) {
  OnGetMediaSuccess(kGetUserMedia, frame_id, pid, request_id, stream_id,
                    audio_track_info, video_track_info);
}

void WebRTCInternals::OnGetUserMediaFailure(GlobalRenderFrameHostId frame_id,
                                            base::ProcessId pid,
                                            int request_id,
                                            const std::string& error,
                                            const std::string& error_message) {
  OnGetMediaFailure(kGetUserMedia, frame_id, pid, request_id, error,
                    error_message);
}

void WebRTCInternals::OnGetDisplayMedia(GlobalRenderFrameHostId frame_id,
                                        base::ProcessId pid,
                                        int request_id,
                                        bool audio,
                                        bool video,
                                        const std::string& audio_constraints,
                                        const std::string& video_constraints) {
  OnGetMedia(kGetDisplayMedia, frame_id, pid, request_id, audio, video,
             audio_constraints, video_constraints);
}

void WebRTCInternals::OnGetDisplayMediaSuccess(
    GlobalRenderFrameHostId frame_id,
    base::ProcessId pid,
    int request_id,
    const std::string& stream_id,
    const std::string& audio_track_info,
    const std::string& video_track_info) {
  OnGetMediaSuccess(kGetDisplayMedia, frame_id, pid, request_id, stream_id,
                    audio_track_info, video_track_info);
}

void WebRTCInternals::OnGetDisplayMediaFailure(
    GlobalRenderFrameHostId frame_id,
    base::ProcessId pid,
    int request_id,
    const std::string& error,
    const std::string& error_message) {
  OnGetMediaFailure(kGetDisplayMedia, frame_id, pid, request_id, error,
                    error_message);
}

void WebRTCInternals::AddObserver(WebRTCInternalsUIObserver* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  observers_.AddObserver(observer);
}

void WebRTCInternals::RemoveObserver(WebRTCInternalsUIObserver* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  observers_.RemoveObserver(observer);
  if (!observers_.empty())
    return;

  // The last webrtc-internals page is going away. Stop polling stats since
  // there is no longer anyone to display them, even if PeerConnections remain
  // active.
  UpdateStatsTimer();

  // Disables event log and audio debug recordings if enabled and the last
  // webrtc-internals page is going away.
  DisableAudioDebugRecordings();
  if (CanToggleEventLogRecordings()) {
    // Do not disable event log recording when the browser was started
    // with the flag to enable the recordings on the command line.
    DisableLocalEventLogRecordings();
  }

  // TODO(tommi): Consider removing all the peer_connection_data().
  for (auto& dictionary : peer_connection_data())
    FreeLogList(&dictionary);
}

void WebRTCInternals::AddConnectionsObserver(
    WebRtcInternalsConnectionsObserver* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  connections_observers_.AddObserver(observer);
}

void WebRTCInternals::RemoveConnectionsObserver(
    WebRtcInternalsConnectionsObserver* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  connections_observers_.RemoveObserver(observer);
}

void WebRTCInternals::UpdateObserver(WebRTCInternalsUIObserver* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (peer_connection_data().size() > 0)
    observer->OnUpdate("update-all-peer-connections", &peer_connection_data_);

  for (const auto& request : get_user_media_requests_) {
    // If there is a stream_id key or an error key this is an update.
    if (request.GetDict().FindString("stream_id") ||
        request.GetDict().FindString("error")) {
      observer->OnUpdate("update-media", &request);
    } else {
      observer->OnUpdate("add-media", &request);
    }
  }
  UpdateWakeLock();
  UpdateStatsTimer();
}

void WebRTCInternals::EnableAudioDebugRecordings(
    content::WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
#if BUILDFLAG(IS_ANDROID)
  EnableAudioDebugRecordingsOnAllRenderProcessHosts();
#else
  MaybeShowSelectFileDialog(web_contents, SelectionType::kAudioDebugRecordings);
#endif
}

void WebRTCInternals::DisableAudioDebugRecordings() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!audio_debug_recording_session_)
    return;
  audio_debug_recording_session_.reset();

  for (RenderProcessHost::iterator i(
           content::RenderProcessHost::AllHostsIterator());
       !i.IsAtEnd(); i.Advance()) {
    i.GetCurrentValue()->DisableAudioDebugRecordings();
  }
}

bool WebRTCInternals::IsAudioDebugRecordingsEnabled() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return !!audio_debug_recording_session_;
}

const base::FilePath& WebRTCInternals::GetAudioDebugRecordingsFilePath() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return audio_debug_recordings_file_path_;
}

void WebRTCInternals::EnableLocalEventLogRecordings(
    content::WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(web_contents);
  DCHECK(CanToggleEventLogRecordings());

#if BUILDFLAG(IS_ANDROID)
  WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
  if (logger) {
    logger->EnableLocalLogging(event_log_recordings_file_path_);
  }
#else
  MaybeShowSelectFileDialog(web_contents, SelectionType::kRtcEventLogs);
#endif
}

void WebRTCInternals::DisableLocalEventLogRecordings() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  event_log_recordings_ = false;
  DCHECK(CanToggleEventLogRecordings());
  WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
  if (logger) {
    logger->DisableLocalLogging();
  }
}

void WebRTCInternals::EnableDataChannelRecordings(
    content::WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
#if BUILDFLAG(IS_ANDROID)
  WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
  if (logger) {
    logger->EnableDataChannelLogging(data_channel_recordings_file_path_);
  }
#else
  MaybeShowSelectFileDialog(web_contents,
                            SelectionType::kDataChannelRecordings);
#endif
}

void WebRTCInternals::DisableDataChannelRecordings() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  data_channel_recording_active_ = false;
  WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
  if (logger) {
    logger->DisableDataChannelLogging();
  }
}

bool WebRTCInternals::IsEventLogRecordingsEnabled() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return event_log_recordings_;
}

bool WebRTCInternals::IsDataChannelRecordingsEnabled() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return data_channel_recording_active_;
}

bool WebRTCInternals::CanToggleEventLogRecordings() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return command_line_derived_logging_path_.empty();
}

void WebRTCInternals::SendUpdate(const std::string& event_name,
                                 base::Value event_data) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!observers_.empty());

  bool queue_was_empty = pending_updates_.empty();
  pending_updates_.push(PendingUpdate(event_name, std::move(event_data)));

  if (queue_was_empty) {
    GetUIThreadTaskRunner({})->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&WebRTCInternals::ProcessPendingUpdates,
                       weak_factory_.GetWeakPtr()),
        base::Milliseconds(aggregate_updates_ms_));
  }
}

void WebRTCInternals::SendUpdate(const std::string& event_name,
                                 base::DictValue event_data) {
  SendUpdate(event_name, base::Value(std::move(event_data)));
}

void WebRTCInternals::RenderProcessExited(
    RenderProcessHost* host,
    const ChildProcessTerminationInfo& info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  OnRendererExit(host->GetDeprecatedID());
  render_process_id_set_.erase(host->GetID());
  host->RemoveObserver(this);
}

void WebRTCInternals::MaybeShowSelectFileDialog(
    content::WebContents* web_contents,
    SelectionType log_type) {
  if (select_file_dialog_) {
    return;
  }

  base::FilePath* file_path = nullptr;
  switch (log_type) {
    case (SelectionType::kRtcEventLogs): {
      file_path = &event_log_recordings_file_path_;
      break;
    }
    case (SelectionType::kAudioDebugRecordings): {
      file_path = &audio_debug_recordings_file_path_;
      break;
    }
    case (SelectionType::kDataChannelRecordings): {
      file_path = &data_channel_recordings_file_path_;
      break;
    }
  }
  CHECK(file_path);

  selection_type_ = log_type;
  select_file_dialog_ = ui::SelectFileDialog::Create(
      this,
      GetContentClient()->browser()->CreateSelectFilePolicy(web_contents));
  select_file_dialog_->SelectFile(
      ui::SelectFileDialog::SELECT_SAVEAS_FILE,
      /*title=*/std::u16string(), *file_path, /*file_types=*/nullptr,
      /*file_type_index=*/0,
      /*default_extension=*/base::FilePath::StringType(),
      web_contents->GetTopLevelNativeWindow());
}

void WebRTCInternals::FileSelected(const ui::SelectedFileInfo& file,
                                   int /* unused_index */) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  switch (selection_type_) {
    case SelectionType::kRtcEventLogs: {
      event_log_recordings_file_path_ = file.path();
      event_log_recordings_ = true;
      WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
      if (logger) {
        logger->EnableLocalLogging(file.path());
      }
      break;
    }
    case SelectionType::kAudioDebugRecordings: {
      audio_debug_recordings_file_path_ = file.path();
      EnableAudioDebugRecordingsOnAllRenderProcessHosts();
      break;
    }
    case SelectionType::kDataChannelRecordings: {
      data_channel_recordings_file_path_ = file.path();
      data_channel_recording_active_ = true;
      WebRtcEventLogger* const logger = WebRtcEventLogger::Get();
      if (logger) {
        logger->EnableDataChannelLogging(file.path());
      }
      break;
    }
    default: {
      NOTREACHED();
    }
  }
  select_file_dialog_.reset();
}

void WebRTCInternals::FileSelectionCanceled() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  switch (selection_type_) {
    case SelectionType::kRtcEventLogs:
      SendUpdate("event-log-recordings-file-selection-cancelled",
                 base::Value());
      break;
    case SelectionType::kAudioDebugRecordings:
      SendUpdate("audio-debug-recordings-file-selection-cancelled",
                 base::Value());
      break;
    case SelectionType::kDataChannelRecordings:
      SendUpdate("data-channel-recordings-file-selection-cancelled",
                 base::Value());
      break;
    default:
      NOTREACHED();
  }
  select_file_dialog_.reset();
}

void WebRTCInternals::OnRendererExit(int render_process_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Iterates from the end of the list to remove the PeerConnections created
  // by the exiting renderer.
  for (int i = peer_connection_data().size() - 1; i >= 0; --i) {
    DCHECK(peer_connection_data()[i].is_dict());

    std::optional<int> this_rid, this_lid;
    this_rid = peer_connection_data()[i].GetDict().FindInt("rid");
    this_lid = peer_connection_data()[i].GetDict().FindInt("lid");

    if (this_rid.value_or(0) == render_process_id) {
      if (!observers_.empty()) {
        base::DictValue update;
        update.Set("rid", this_rid.value_or(0));
        update.Set("lid", this_lid.value_or(0));
        SendUpdate("remove-peer-connection", std::move(update));
      }
      MaybeClosePeerConnection(peer_connection_data()[i]);
      peer_connection_data().erase(peer_connection_data().begin() + i);
    }
  }
  UpdateWakeLock();
  UpdateStatsTimer();

  bool found_any = false;
  // Iterates from the end of the list to remove the getUserMedia requests
  // created by the exiting renderer.
  for (int i = get_user_media_requests_.size() - 1; i >= 0; --i) {
    DCHECK(get_user_media_requests_[i].is_dict());

    std::optional<int> this_rid =
        get_user_media_requests_[i].GetDict().FindInt("rid");

    if (this_rid.value_or(0) == render_process_id) {
      get_user_media_requests_.erase(get_user_media_requests_.begin() + i);
      found_any = true;
    }
  }

  if (found_any && !observers_.empty()) {
    base::DictValue update;
    update.Set("rid", render_process_id);
    SendUpdate("remove-media-for-renderer", std::move(update));
  }
}

void WebRTCInternals::EnableAudioDebugRecordingsOnAllRenderProcessHosts() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!audio_debug_recording_session_);
  mojo::PendingRemote<audio::mojom::DebugRecording> debug_recording;
  content::GetAudioService().BindDebugRecording(
      debug_recording.InitWithNewPipeAndPassReceiver());
  audio_debug_recording_session_ = audio::CreateAudioDebugRecordingSession(
      audio_debug_recordings_file_path_, std::move(debug_recording));

  for (RenderProcessHost::iterator i(
           content::RenderProcessHost::AllHostsIterator());
       !i.IsAtEnd(); i.Advance()) {
    i.GetCurrentValue()->EnableAudioDebugRecordings(
        audio_debug_recordings_file_path_);
  }
}

void WebRTCInternals::MaybeClosePeerConnection(base::Value& record) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  std::optional<bool> is_open = record.GetDict().FindBool("isOpen");
  DCHECK(is_open.has_value());
  if (!*is_open)
    return;

  record.GetDict().Set("isOpen", false);
  MaybeMarkPeerConnectionAsNotConnected(record);
}

void WebRTCInternals::MaybeMarkPeerConnectionAsConnected(base::Value& record) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  bool was_connected = record.GetDict().FindBool("connected").value_or(true);
  if (!was_connected) {
    ++num_connected_connections_;
    record.GetDict().Set("connected", true);
    UpdateWakeLock();
    UpdateStatsTimer();
    for (auto& observer : connections_observers_)
      observer.OnConnectionsCountChange(num_connected_connections_);
  }
}

void WebRTCInternals::MaybeMarkPeerConnectionAsNotConnected(
    base::Value& record) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  bool was_connected = record.GetDict().FindBool("connected").value_or(false);
  if (was_connected) {
    record.GetDict().Set("connected", false);
    --num_connected_connections_;
    DCHECK_GE(num_connected_connections_, 0);
    UpdateWakeLock();
    UpdateStatsTimer();
    for (auto& observer : connections_observers_)
      observer.OnConnectionsCountChange(num_connected_connections_);
  }
}

void WebRTCInternals::UpdateWakeLock() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!should_block_power_saving_)
    return;

  if (num_connected_connections_ == 0) {
    DVLOG(1)
        << ("Cancel the wake lock on application suspension since no "
            "PeerConnections are active anymore.");
    GetWakeLock()->CancelWakeLock();
  } else {
    DCHECK_GT(num_connected_connections_, 0);
    DVLOG(1) << ("Preventing the application from being suspended while one or "
                 "more PeerConnections are active.");
    GetWakeLock()->RequestWakeLock();
  }
}

device::mojom::WakeLock* WebRTCInternals::GetWakeLock() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Here is a lazy binding, and will not reconnect after connection error.
  if (!wake_lock_) {
    mojo::Remote<device::mojom::WakeLockProvider> wake_lock_provider;
    GetDeviceService().BindWakeLockProvider(
        wake_lock_provider.BindNewPipeAndPassReceiver());
    wake_lock_provider->GetWakeLockWithoutContext(
        device::mojom::WakeLockType::kPreventAppSuspension,
        device::mojom::WakeLockReason::kOther,
        "WebRTC has active PeerConnections",
        wake_lock_.BindNewPipeAndPassReceiver());
  }
  return wake_lock_.get();
}

void WebRTCInternals::RequestStandardStats() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  for (auto* host : PeerConnectionTrackerHost::GetAllHosts()) {
    host->GetStandardStats();
  }
}

void WebRTCInternals::UpdateStatsTimer() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (num_connected_connections_ > 0 && !observers_.empty()) {
    if (!stats_timer_.IsRunning()) {
      stats_timer_.Start(FROM_HERE, GetStatsPollingInterval(), this,
                         &WebRTCInternals::RequestStandardStats);
    }
  } else {
    stats_timer_.Stop();
  }
}

void WebRTCInternals::ProcessPendingUpdates() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  while (!pending_updates_.empty()) {
    const auto& update = pending_updates_.front();
    for (auto& observer : observers_)
      observer.OnUpdate(update.event_name(), update.event_data());
    pending_updates_.pop();
  }
}

base::ListValue::iterator WebRTCInternals::FindRecord(
    GlobalRenderFrameHostId frame_id,
    int lid) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  for (auto it = peer_connection_data().begin();
       it != peer_connection_data().end(); ++it) {
    DCHECK(it->is_dict());

    int this_rid = it->GetDict().FindInt("rid").value_or(0);
    int this_lid = it->GetDict().FindInt("lid").value_or(0);

    // TODO(crbug.com/379869738) Remove GetUnsafeValue.
    if (this_rid == frame_id.child_id.GetUnsafeValue() && this_lid == lid) {
      return it;
    }
  }
  return peer_connection_data().end();
}
}  // namespace content
