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

#ifndef CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_
#define CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_

#include <string>

#include "base/logging.h"
#include "chrome/browser/media/prefs/pref_names.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "media/audio/audio_device_description.h"
#include "media/capture/video/video_capture_device_info.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"

namespace media_prefs::internal {

// Returns a stable device id given `device_info`. This isn't guaranteed
// to be unique, but it is stable across reboots and device plug/unplug.
std::string DeviceInfoToStableId(
    const media::VideoCaptureDeviceInfo& device_info);
std::string DeviceInfoToStableId(
    const media::AudioDeviceDescription& device_info);
std::string DeviceInfoToStableId(const blink::WebMediaDeviceInfo& device_info);
std::string DeviceInfoToStableId(const blink::MediaStreamDevice& device_info);

// Returns a unique id given `device_info`. This is unique for the current
// session, but isn't guaranteed to be stable through reboots or device
// plug/unplug.
std::string DeviceInfoToUniqueId(
    const media::VideoCaptureDeviceInfo& device_info);
std::string DeviceInfoToUniqueId(
    const media::AudioDeviceDescription& device_info);
std::string DeviceInfoToUniqueId(const blink::WebMediaDeviceInfo& device_info);
std::string DeviceInfoToUniqueId(const blink::MediaStreamDevice& device_info);

}  // namespace media_prefs::internal

namespace {

using media_prefs::internal::DeviceInfoToStableId;
using media_prefs::internal::DeviceInfoToUniqueId;

// Returns the rank index for the passed `info`. If `info` doesn't have a
// rank then return size of `id_to_rank` because that will rank lower than
// any of the ranked devices.
template <typename T>
size_t GetRank(const T& info,
               const base::flat_map<std::string, size_t>& id_to_rank) {
  auto found_iter = id_to_rank.find(DeviceInfoToStableId(info));
  if (found_iter != id_to_rank.end()) {
    return found_iter->second;
  }

  return id_to_rank.size();
}

// Map device ids to a numeric ranking. Smaller values are more preferred.
template <typename Iterator>
base::flat_map<std::string, size_t> GetIdToRankMap(Iterator begin,
                                                   Iterator end) {
  base::flat_map<std::string, size_t> id_to_rank;
  for (auto iter = begin; iter < end; ++iter) {
    id_to_rank[iter->GetString()] = iter - begin;
  }

  return id_to_rank;
}

template <typename T>
const base::ListValue& GetAndMaybeMigratePref(PrefService& prefs,
                                              const std::string& pref_name,
                                              std::vector<T>& device_infos);

// Reorders the passed `device_infos`, so that ranked devices are ordered at the
// beginning of the list. Devices don't have a ranking will retain their
// ordering, but will be shifted below ranked devices.
//
// This must be run on the UI thread as it reads from the PrefService.
template <typename T>
void PreferenceRankDeviceInfos(PrefService& prefs,
                               const std::string& pref_name,
                               std::vector<T>& device_infos) {
  if (!prefs.FindPreference(pref_name)) {
    LOG(WARNING) << "Can't rank device infos because " << pref_name
                 << " isn't registered";
    return;
  }
  const base::ListValue& ranking =
      GetAndMaybeMigratePref(prefs, pref_name, device_infos);
  const auto id_to_rank = GetIdToRankMap(ranking.begin(), ranking.end());

  std::stable_sort(
      device_infos.begin(), device_infos.end(),
      [id_to_rank = std::move(id_to_rank)](const T& a, const T& b) {
        return GetRank(a, id_to_rank) < GetRank(b, id_to_rank);
      });
}

// Updates device ranking given a preferred device and the list of competing
// devices.
//
// The algorithm is as follows:
// 1. Identify the best ranked device among the current devices
//   a. If a device isn't found in the ranking list, then it is appended
// 2. If the best ranked device is the preferred device, then exit
// 3. Else, move the preferred device to be just before the best ranked device
//
// The goal of the algorithm is to ensure that given the same set of devices we
// would get the same device ranking.
//
// Example case:
// Update 1 -
// The user selects the USB mic as preferred, and the other current
// devices are at the end of the list in the order enumerated by the OS.
// current ranking: []
// preferred_device_iter: 'USB Camera Mic',
// current_device_infos: 'bluetooth mic', 'builtin mic', 'USB Camera Mic'
//
// updated ranking: 'USB Camera Mic', 'bluetooth mic', 'builtin mic'
//
// Update 2 -
// The user disconnects the USB camera because they left their desk
// and they select the 'builtin mic' as preferred.
// current ranking: 'USB Camera Mic', 'builtin mic', 'bluetooth mic'
// preferred_device_iter: 'builtin mic'
// current_deivce_infos: 'bluetooth mic', 'builtin mic'
//
// updated ranking: 'USB Camera Mic', 'builtin mic', 'bluetooth mic'
//
// Note that the second update leaves the 'USB Camera Mic' as preferred over the
// other two mics because it wasn't present to be considered.
//
// This must be run on the UI thread as it read/writes to the PrefService.
template <typename T>
void UpdateDevicePreferenceRanking(
    PrefService& prefs,
    const std::string& pref_name,
    const typename std::vector<T>::const_iterator preferred_device_iter,
    const std::vector<T>& current_device_infos) {
  CHECK(preferred_device_iter < current_device_infos.end());
  auto preferred_device_stable_id =
      DeviceInfoToStableId(*preferred_device_iter);
  ScopedListPrefUpdate ranking(&prefs, pref_name);
  const auto id_to_rank = GetIdToRankMap(ranking->begin(), ranking->end());

  // Start `best_competing_rank` with a bigger number than is possible.
  auto best_competing_rank = ranking->size();
  for (const auto& device_info : current_device_infos) {
    // Skip the preferred device because we're looking for the best ranked
    // competing device.
    if (DeviceInfoToUniqueId(device_info) ==
        DeviceInfoToUniqueId(*preferred_device_iter)) {
      continue;
    }

    auto rank = id_to_rank.find(DeviceInfoToStableId(device_info));
    if (rank == id_to_rank.end()) {
      // The device doesn't have a rank, so insert it at the end of the list.
      ranking->Append(DeviceInfoToStableId(device_info));
      continue;
    }

    if (rank->second < best_competing_rank) {
      best_competing_rank = rank->second;
    }
  }

  auto preferred_device_rank = id_to_rank.find(preferred_device_stable_id);
  if (preferred_device_rank == id_to_rank.end()) {
    // The preferred device didn't previously have a rank, so insert it before
    // the best ranked competitor.
    ranking->Insert(ranking->begin() + best_competing_rank,
                    base::Value{preferred_device_stable_id});
    return;
  }

  // Only move the preferred device if there is a better ranked competitor.
  if (best_competing_rank < preferred_device_rank->second) {
    // Move the preferred device to be just before the best ranked competitor.
    ranking->erase(ranking->begin() + preferred_device_rank->second);
    ranking->Insert(ranking->begin() + best_competing_rank,
                    base::Value{preferred_device_stable_id});
  }
}

// Get the value of the ranking pref.
//
// If the ranking pref is unset, it will be initialized from the default device
// pref. If the default device pref is unset or the device isn't in
// `device_infos`, then the ranking pref will remain uninitialized.
// `pref_name` is required to be in {kAudioInputUserPreferenceRanking,
// kVideoInputUserPreferenceRanking}.
//
// Example scenarios:
// For all scenarios the user has set dev2 as the default and updated the
// browser.
//
// Scenario A:
//  1) User connects [dev1, dev2]
//  2) User visits a website that calls enumerateDevices or getUserMedia
//  3) Device ranking pref is updated to [dev2, dev1]
//  4) All subsequent usages refer to the device ranking pref and ignore legacy
//     default pref
//
// Scenario B:
//  1) User connects [dev1]
//  2) User visits a website that calls enumerateDevices or getUserMedia
//  3) Device ranking pref remains uninitialized because the default device
//     isn't present
//  4) User connects [dev1, dev2]
//  5) User visits a website that calls enumerateDevices or getUserMedia
//  6) Device ranking pref is updated to [dev2, dev1]
//  7) All subsequent usages refer to the device ranking pref and
//     ignore legacy default pref.
//
// Scenario C:
//  1) User connects [dev1]
//  2) User visits a website that calls enumerateDevices or getUserMedia
//  3) Device ranking pref remains uninitialized because the default device
//     isn't present
//  4) User expresses preference for dev1 in permission bubble or
//     Chrome settings
//  5) Device ranking pref is updated to [dev1]
//  6) All subsequent usages refer to the device ranking pref and ignore legacy
//     default pref.
//
// TODO(crbug.com/311205211): Remove this special initialization logic once the
// default device pref is removed.
template <typename T>
const base::ListValue& GetAndMaybeMigratePref(PrefService& prefs,
                                              const std::string& pref_name,
                                              std::vector<T>& device_infos) {
  const base::ListValue& ranking = prefs.GetList(pref_name);
  if (!ranking.empty()) {
    return ranking;
  }

  // Initialize the ranking pref with the legacy default device because it is
  // unset.
  std::string default_device_pref_name;
  if (pref_name == kAudioInputUserPreferenceRanking) {
    default_device_pref_name = prefs::kDefaultAudioCaptureDeviceDeprecated;
  } else if (pref_name == kVideoInputUserPreferenceRanking) {
    default_device_pref_name = prefs::kDefaultVideoCaptureDeviceDeprecated;
  } else {
    NOTREACHED();
  }

  if (!prefs.HasPrefPath(default_device_pref_name)) {
    LOG(WARNING) << "Can't initialize the value of " << pref_name << " because "
                 << default_device_pref_name << " isn't registered or is empty";
    return ranking;
  }

  const auto& default_id = prefs.GetString(default_device_pref_name);
  for (auto iter = device_infos.begin(); iter < device_infos.end(); ++iter) {
    if (DeviceInfoToUniqueId(*iter) == default_id) {
      UpdateDevicePreferenceRanking(prefs, pref_name, iter, device_infos);
      return prefs.GetList(pref_name);
    }
  }

  return ranking;
}

}  // namespace

namespace media_prefs {

void RegisterUserPrefs(PrefRegistrySimple* registry);

template <typename T>
void PreferenceRankAudioDeviceInfos(PrefService& prefs,
                                    std::vector<T>& device_infos) {
  static_assert(std::is_same_v<media::AudioDeviceDescription, T> ||
                std::is_same_v<blink::WebMediaDeviceInfo, T> ||
                std::is_same_v<blink::MediaStreamDevice, T>);
  PreferenceRankDeviceInfos(prefs, kAudioInputUserPreferenceRanking,
                            device_infos);
}

template <typename T>
void PreferenceRankVideoDeviceInfos(PrefService& prefs,
                                    std::vector<T>& device_infos) {
  static_assert(std::is_same_v<media::VideoCaptureDeviceInfo, T> ||
                std::is_same_v<blink::WebMediaDeviceInfo, T> ||
                std::is_same_v<blink::MediaStreamDevice, T>);
  PreferenceRankDeviceInfos(prefs, kVideoInputUserPreferenceRanking,
                            device_infos);
}

template <typename T>
void UpdateAudioDevicePreferenceRanking(
    PrefService& prefs,
    const typename std::vector<T>::const_iterator preferred_device_iter,
    const std::vector<T>& current_device_infos) {
  static_assert(std::is_same_v<media::AudioDeviceDescription, T> ||
                std::is_same_v<blink::WebMediaDeviceInfo, T> ||
                std::is_same_v<blink::MediaStreamDevice, T>);
  UpdateDevicePreferenceRanking(prefs, kAudioInputUserPreferenceRanking,
                                preferred_device_iter, current_device_infos);
}

template <typename T>
void UpdateVideoDevicePreferenceRanking(
    PrefService& prefs,
    const typename std::vector<T>::const_iterator preferred_device_iter,
    const std::vector<T>& current_device_infos) {
  static_assert(std::is_same_v<media::VideoCaptureDeviceInfo, T> ||
                std::is_same_v<blink::WebMediaDeviceInfo, T> ||
                std::is_same_v<blink::MediaStreamDevice, T>);
  UpdateDevicePreferenceRanking(prefs, kVideoInputUserPreferenceRanking,
                                preferred_device_iter, current_device_infos);
}

}  // namespace media_prefs

#endif  // CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_
