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

#include "components/media_router/common/media_source.h"

#include <algorithm>
#include <array>
#include <cstdio>
#include <ostream>
#include <string>
#include <string_view>

#include "base/compiler_specific.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "media/audio/audio_features.h"
#include "media/base/audio_codecs.h"
#include "media/base/media_switches.h"
#include "media/base/video_codecs.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/platform/modules/remoteplayback/remote_playback_source.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace media_router {

namespace {

// Prefixes used to format and detect various protocols' media source URNs.
// See: https://www.ietf.org/rfc/rfc3406.txt
constexpr char kAnyTabMediaUrn[] = "urn:x-org.chromium.media:source:tab:*";
constexpr char kTabMediaUrnFormat[] = "urn:x-org.chromium.media:source:tab:%d";
constexpr std::string_view kDesktopMediaUrnPrefix =
    "urn:x-org.chromium.media:source:desktop:";
// WARNING: If more desktop URN parameters are added in the future, the parsing
// code will have to be smarter!
constexpr std::string_view kDesktopMediaUrnAudioParam = "?with_audio=true";
constexpr std::string_view kUnchosenDesktopMediaUrn =
    "urn:x-org.chromium.media:source:desktop";
constexpr std::string_view kUnchosenDesktopWithAudioMediaUrn =
    "urn:x-org.chromium.media:source:desktop:?with_audio=true";

// List of non-http(s) schemes that are allowed in a Presentation URL.
constexpr std::array<const char* const, 5> kAllowedSchemes{
    {kCastPresentationUrlScheme, kCastDialPresentationUrlScheme,
     kDialPresentationUrlScheme, blink::kRemotePlaybackPresentationUrlScheme,
     "test"}};

bool IsSchemeAllowed(const GURL& url) {
  if (url.SchemeIs(url::kHttpsScheme)) {
    return true;
  } else if (url.SchemeIs(url::kHttpScheme)) {
    return net::IsLocalhost(url);
  } else {
    return std::ranges::any_of(
        kAllowedSchemes,
        [&url](const char* const scheme) { return url.SchemeIs(scheme); });
  }
}

bool IsSystemAudioCaptureSupported() {
  if (!media::IsSystemLoopbackCaptureSupported()) {
    return false;
  }
#if BUILDFLAG(IS_MAC)
  return media::IsMacSckSystemLoopbackCaptureSupported() ||
         base::FeatureList::IsEnabled(media::kMacCatapLoopbackAudioForCast);
#elif BUILDFLAG(IS_LINUX)
  return base::FeatureList::IsEnabled(media::kPulseaudioLoopbackForCast);
#else
  return true;
#endif  // BUILDFLAG(IS_LINUX)
}

}  // namespace

bool IsDialAppName(std::string_view app_name) {
  if (app_name.empty() || app_name == "." || app_name == "..") {
    return false;
  }
  // Reject the RFC 3986 dot-segment tokens so GetDialAppUrl()'s Resolve() call
  // cannot traverse outside the device's Application-URL path namespace.
  if (app_name == "." || app_name == "..") {
    return false;
  }
  return std::ranges::all_of(app_name, [](char c) {
    return base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '-' ||
           c == '.' || c == '_' || c == '~';
  });
}

GURL GetDialAppUrl(const GURL& app_url, const std::string& app_name) {
  if (!IsDialAppName(app_name) || !app_url.is_valid()) {
    return GURL();
  }

  // The DIAL spec (Section 5.4) implies that the app URL must not have a
  // trailing slash.
  std::string spec = app_url.spec();
  if (!spec.empty() && spec.back() != '/') {
    spec += "/";
  }
  return GURL(spec).Resolve(app_name);
}

bool IsLegacyCastPresentationUrl(const GURL& url) {
  return base::StartsWith(url.spec(), kLegacyCastPresentationUrlPrefix,
                          base::CompareCase::INSENSITIVE_ASCII);
}

bool IsValidPresentationUrl(const GURL& url) {
  return url.is_valid() && IsSchemeAllowed(url);
}

bool IsValidStandardPresentationSource(const std::string& media_source) {
  const GURL source_url(media_source);
  if (!source_url.is_valid()) {
    return false;
  } else if (source_url.SchemeIs(url::kHttpsScheme)) {
    return !base::StartsWith(source_url.spec(),
                             kLegacyCastPresentationUrlPrefix,
                             base::CompareCase::INSENSITIVE_ASCII);
  } else if (source_url.SchemeIs(url::kHttpScheme)) {
    return net::IsLocalhost(source_url);
  } else {
    return false;
  }
}

bool IsAutoJoinPresentationId(const std::string& presentation_id) {
  return presentation_id == kAutoJoinPresentationId;
}

MediaSource::MediaSource(const MediaSource::Id& source_id) : id_(source_id) {
  GURL url(source_id);
  if (IsValidPresentationUrl(url)) {
    url_ = url;
  }
}

MediaSource::MediaSource(const GURL& presentation_url)
    : id_(presentation_url.spec()), url_(presentation_url) {}

MediaSource::~MediaSource() = default;

// static
MediaSource MediaSource::ForAnyTab() {
  return MediaSource(std::string(kAnyTabMediaUrn));
}

// static
MediaSource MediaSource::ForTab(int tab_id) {
  // Ideally we shouldn't allow -1 as a tab ID, but in unit tests, a tab ID of
  // -1 can show up when this function is called from
  // CastHandler::StartObservingForSinks() because SessionTabHelper::IdForTab
  // can return -1.
  DCHECK_GE(tab_id, -1);
  return MediaSource(base::StringPrintf(kTabMediaUrnFormat, tab_id));
}

// static
MediaSource MediaSource::ForPresentationUrl(const GURL& presentation_url) {
  return MediaSource(presentation_url);
}

// static
MediaSource MediaSource::ForRemotePlayback(int tab_id,
                                           media::VideoCodec video_codec,
                                           media::AudioCodec audio_codec) {
  return MediaSource(
      base::StringPrintf(blink::kRemotePlaybackDesktopUrlFormat,
                         media::GetCodecName(video_codec).c_str(),
                         media::GetCodecName(audio_codec).c_str(), tab_id));
}

// static
MediaSource MediaSource::ForDesktop(const std::string& desktop_media_id,
                                    bool with_audio) {
  DCHECK(!desktop_media_id.empty());
  std::string id = std::string(kDesktopMediaUrnPrefix) + desktop_media_id;
  if (with_audio) {
    id += std::string(kDesktopMediaUrnAudioParam);
  }
  return MediaSource(id);
}

// static
MediaSource MediaSource::ForUnchosenDesktop() {
  return IsSystemAudioCaptureSupported()
             ? MediaSource(std::string(kUnchosenDesktopWithAudioMediaUrn))
             : MediaSource(std::string(kUnchosenDesktopMediaUrn));
}

bool MediaSource::IsTabMirroringSource() const {
  return id() == kAnyTabMediaUrn || TabId().has_value();
}

bool MediaSource::IsDesktopMirroringSource() const {
  return id() == kUnchosenDesktopMediaUrn ||
         id() == kUnchosenDesktopWithAudioMediaUrn ||
         base::StartsWith(id(), kDesktopMediaUrnPrefix,
                          base::CompareCase::SENSITIVE);
}

bool MediaSource::IsCastPresentationUrl() const {
  return url_.SchemeIs(kCastPresentationUrlScheme) ||
         IsLegacyCastPresentationUrl(url_);
}

bool MediaSource::IsRemotePlaybackSource() const {
  return url_.SchemeIs(kRemotePlaybackPresentationUrlScheme);
}

std::optional<int> MediaSource::TabId() const {
  int tab_id;
  if (UNSAFE_TODO(sscanf(id_.c_str(), kTabMediaUrnFormat, &tab_id)) != 1) {
    return std::nullopt;
  }
  return tab_id;
}

std::optional<int> MediaSource::TabIdFromRemotePlaybackSource() const {
  if (!IsRemotePlaybackSource()) {
    return std::nullopt;
  }

  std::string tab_id_str;
  if (!net::GetValueForKeyInQuery(url(), "tab_id", &tab_id_str)) {
    return std::nullopt;
  }

  int tab_id;
  if (!base::StringToInt(tab_id_str, &tab_id)) {
    return std::nullopt;
  }
  return tab_id;
}

std::optional<std::string> MediaSource::DesktopStreamId() const {
  if (base::StartsWith(id_, kDesktopMediaUrnPrefix,
                       base::CompareCase::SENSITIVE)) {
    const auto begin = id_.begin() + kDesktopMediaUrnPrefix.size();
    auto end = id_.end();
    if (base::EndsWith(id_, kDesktopMediaUrnAudioParam,
                       base::CompareCase::SENSITIVE)) {
      end -= kDesktopMediaUrnAudioParam.size();
    }
    return std::string(begin, end);
  }
  return std::nullopt;
}

bool MediaSource::IsDesktopSourceWithAudio() const {
  return base::StartsWith(id_, kDesktopMediaUrnPrefix,
                          base::CompareCase::SENSITIVE) &&
         base::EndsWith(id_, kDesktopMediaUrnAudioParam,
                        base::CompareCase::SENSITIVE);
}

bool MediaSource::IsDialSource() const {
  return url_.SchemeIs(kCastDialPresentationUrlScheme);
}

std::string MediaSource::AppNameFromDialSource() const {
  if (!IsDialSource()) {
    return "";
  }
  std::string app_name = url_.GetPath();
  return IsDialAppName(app_name) ? app_name : "";
}

std::string MediaSource::TruncateForLogging(size_t max_length) const {
  const std::string origin = url_.DeprecatedGetOriginAsURL().spec();
  if (!origin.empty()) {
    return origin.substr(0, max_length);
  }
  // TODO(takumif): Keep the query string by redacting PII. The query string
  // may contain info useful for debugging such as the required capabilities.
  const size_t query_start_index = id_.find("?");
  const size_t length =
      query_start_index == std::string::npos ? max_length : query_start_index;
  return id_.substr(0, length);
}

void MediaSource::AppendTabIdToRemotePlaybackUrlQuery(int tab_id) {
  if (url_.is_empty()) {
    return;
  }

  GURL::Replacements replacements;
  std::string tab_id_query = base::StringPrintf("tab_id=%d", tab_id);
  std::string new_query =
      (url_.has_query() ? url_.GetQuery() + "&" : "") + tab_id_query;
  replacements.SetQueryStr(new_query);
  url_ = url_.ReplaceComponents(replacements);
  id_ = url_.spec();
}

}  // namespace media_router
