/*
 *  Copyright 2024 The WebRTC Project Authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */
#include "media/base/codec_comparators.h"

#include <algorithm>
#include <cstddef>
#include <optional>
#include <string>
#include <vector>

#include "absl/algorithm/container.h"
#include "absl/functional/any_invocable.h"
#include "absl/strings/match.h"
#include "absl/strings/string_view.h"
#include "api/media_types.h"
#include "api/payload_type.h"
#include "api/rtp_parameters.h"
#include "api/video_codecs/av1_profile.h"
#include "api/video_codecs/h264_profile_level_id.h"
#ifdef RTC_ENABLE_H265
#include "api/video_codecs/h265_profile_tier_level.h"
#endif
#include "api/video_codecs/vp9_profile.h"
#include "media/base/codec.h"
#include "media/base/media_constants.h"
#include "rtc_base/checks.h"
#include "rtc_base/string_encode.h"

namespace webrtc {

namespace {

// TODO(bugs.webrtc.org/15847): remove code duplication of IsSameCodecSpecific
// in api/video_codecs/sdp_video_format.cc
std::string GetFmtpParameterOrDefault(const CodecParameterMap& params,
                                      const std::string& name,
                                      const std::string& default_value) {
  const auto it = params.find(name);
  if (it != params.end()) {
    return it->second;
  }
  return default_value;
}

bool HasParameter(const CodecParameterMap& params, const std::string& name) {
  return params.find(name) != params.end();
}

std::string H264GetPacketizationModeOrDefault(const CodecParameterMap& params) {
  // If packetization-mode is not present, default to "0".
  // https://tools.ietf.org/html/rfc6184#section-6.2
  return GetFmtpParameterOrDefault(params, kH264FmtpPacketizationMode, "0");
}

std::string AV1GetTierOrDefault(const CodecParameterMap& params) {
  // If the parameter is not present, the tier MUST be inferred to be 0.
  // https://aomediacodec.github.io/av1-rtp-spec/#72-sdp-parameters
  return GetFmtpParameterOrDefault(params, kAv1FmtpTier, "0");
}

std::string AV1GetLevelIdxOrDefault(const CodecParameterMap& params) {
  // If the parameter is not present, it MUST be inferred to be 5 (level 3.1).
  // https://aomediacodec.github.io/av1-rtp-spec/#72-sdp-parameters
  return GetFmtpParameterOrDefault(params, kAv1FmtpLevelIdx, "5");
}

#ifdef RTC_ENABLE_H265
std::string GetH265TxModeOrDefault(const CodecParameterMap& params) {
  // If TxMode is not present, a value of "SRST" must be inferred.
  // https://tools.ietf.org/html/rfc7798@section-7.1
  return GetFmtpParameterOrDefault(params, kH265FmtpTxMode, "SRST");
}

bool IsSameH265TxMode(const CodecParameterMap& left,
                      const CodecParameterMap& right) {
  return absl::EqualsIgnoreCase(GetH265TxModeOrDefault(left),
                                GetH265TxModeOrDefault(right));
}
#endif

// Some (video) codecs are actually families of codecs and rely on parameters
// to distinguish different incompatible family members.
bool IsSameCodecSpecific(const std::string& name1,
                         const CodecParameterMap& params1,
                         const std::string& name2,
                         const CodecParameterMap& params2) {
  // The names might not necessarily match, so check both.
  auto either_name_matches = [&](const std::string name) {
    return absl::EqualsIgnoreCase(name, name1) ||
           absl::EqualsIgnoreCase(name, name2);
  };
  if (either_name_matches(kH264CodecName))
    return H264IsSameProfile(params1, params2) &&
           IsSameH264PacketizationMode(params1, params2);
  if (either_name_matches(kVp9CodecName))
    return VP9IsSameProfile(params1, params2);
  // https://aomediacodec.github.io/av1-rtp-spec/#723-usage-with-the-sdp-offeranswer-model
  //   These media configuration parameters are asymmetrical and the answerer
  //   MAY declare its own media configuration
  // TODO(bugs.webrtc.org/396434695): for backward compability we currently
  // compare profile.
  if (either_name_matches(kAv1CodecName))
    return AV1IsSameProfile(params1, params2);
#ifdef RTC_ENABLE_H265
  if (either_name_matches(kH265CodecName)) {
    return H265IsSameProfile(params1, params2) &&
           H265IsSameTier(params1, params2) &&
           IsSameH265TxMode(params1, params2);
  }
#endif
  return true;
}

bool ReferencedCodecsMatch(const std::vector<Codec>& codecs1,
                           const PayloadType codec1_id,
                           const std::vector<Codec>& codecs2,
                           const PayloadType codec2_id) {
  const Codec* codec1 = FindCodecById(codecs1, codec1_id);
  const Codec* codec2 = FindCodecById(codecs2, codec2_id);
  return codec1 != nullptr && codec2 != nullptr && codec1->Matches(*codec2);
}

CodecParameterMap InsertDefaultParams(const std::string& name,
                                      const CodecParameterMap& params) {
  CodecParameterMap updated_params = params;
  if (absl::EqualsIgnoreCase(name, kVp9CodecName)) {
    if (!HasParameter(params, kVP9FmtpProfileId)) {
      if (std::optional<VP9Profile> default_profile =
              ParseSdpForVP9Profile({})) {
        updated_params.insert(
            {kVP9FmtpProfileId, VP9ProfileToString(*default_profile)});
      }
    }
  }
  if (absl::EqualsIgnoreCase(name, kAv1CodecName)) {
    if (!HasParameter(params, kAv1FmtpProfile)) {
      if (std::optional<AV1Profile> default_profile =
              ParseSdpForAV1Profile({})) {
        updated_params.insert(
            {kAv1FmtpProfile, AV1ProfileToString(*default_profile).data()});
      }
    }
    if (!HasParameter(params, kAv1FmtpTier)) {
      updated_params.insert({kAv1FmtpTier, AV1GetTierOrDefault({})});
    }
    if (!HasParameter(params, kAv1FmtpLevelIdx)) {
      updated_params.insert({kAv1FmtpLevelIdx, AV1GetLevelIdxOrDefault({})});
    }
  }
  if (absl::EqualsIgnoreCase(name, kH264CodecName)) {
    if (!HasParameter(params, kH264FmtpPacketizationMode)) {
      updated_params.insert(
          {kH264FmtpPacketizationMode, H264GetPacketizationModeOrDefault({})});
    }
  }
#ifdef RTC_ENABLE_H265
  if (absl::EqualsIgnoreCase(name, kH265CodecName)) {
    if (std::optional<H265ProfileTierLevel> default_params =
            ParseSdpForH265ProfileTierLevel({})) {
      if (!HasParameter(params, kH265FmtpProfileId)) {
        updated_params.insert(
            {kH265FmtpProfileId, H265ProfileToString(default_params->profile)});
      }
      if (!HasParameter(params, kH265FmtpLevelId)) {
        updated_params.insert(
            {kH265FmtpLevelId, H265LevelToString(default_params->level)});
      }
      if (!HasParameter(params, kH265FmtpTierFlag)) {
        updated_params.insert(
            {kH265FmtpTierFlag, H265TierToString(default_params->tier)});
      }
    }
    if (!HasParameter(params, kH265FmtpTxMode)) {
      updated_params.insert({kH265FmtpTxMode, GetH265TxModeOrDefault({})});
    }
  }
#endif
  return updated_params;
}

}  // namespace

bool MatchesWithReferenceAttributesAndComparator(
    const Codec& codec_to_match,
    const Codec& potential_match,
    absl::AnyInvocable<bool(PayloadType, PayloadType)> reference_comparator) {
  if (!MatchesWithCodecRules(codec_to_match, potential_match)) {
    return false;
  }
  Codec::ResiliencyType resiliency_type = codec_to_match.GetResiliencyType();
  if (resiliency_type == Codec::ResiliencyType::kRtx) {
    int apt_value_1_int = 0;
    int apt_value_2_int = 0;
    bool has_apt_1 = codec_to_match.GetParam(kCodecParamAssociatedPayloadType,
                                             &apt_value_1_int);
    bool has_apt_2 = potential_match.GetParam(kCodecParamAssociatedPayloadType,
                                              &apt_value_2_int);
    if (!has_apt_1 && !has_apt_2) {
      return true;
    }
    if (!has_apt_1 || !has_apt_2) {
      return false;
    }
    return reference_comparator(PayloadType(apt_value_1_int),
                                PayloadType(apt_value_2_int));
  }
  if (resiliency_type == Codec::ResiliencyType::kRed) {
    auto red_parameters_1 =
        codec_to_match.params.find(kCodecParamNotInNameValueFormat);
    auto red_parameters_2 =
        potential_match.params.find(kCodecParamNotInNameValueFormat);
    bool has_parameters_1 = red_parameters_1 != codec_to_match.params.end();
    bool has_parameters_2 = red_parameters_2 != potential_match.params.end();

    if (has_parameters_1 && has_parameters_2) {
      // Different levels of redundancy between offer and answer are OK
      // since RED is considered to be declarative.
      std::vector<absl::string_view> redundant_payloads_1 =
          split(red_parameters_1->second, '/');
      std::vector<absl::string_view> redundant_payloads_2 =
          split(red_parameters_2->second, '/');
      // note: split returns at least 1 string even on empty strings.
      size_t smallest_size =
          std::min(redundant_payloads_1.size(), redundant_payloads_2.size());
      // If the smaller list is equivalent to the longer list, we consider them
      // equivalent even if size differs.
      for (size_t i = 0; i < smallest_size; i++) {
        int red_value_1;
        int red_value_2;
        if (FromString(redundant_payloads_1[i], &red_value_1) &&
            FromString(redundant_payloads_2[i], &red_value_2)) {
          if (!reference_comparator(red_value_1, red_value_2)) {
            return false;
          }
        } else {
          // At least one parameter was not an integer.
          // This is a syntax error, but we allow it here if the whole parameter
          // equals the other parameter, in order to not generate more errors
          // by duplicating the bad parameter.
          return red_parameters_1->second == red_parameters_2->second;
        }
      }
      return true;
    }
    if (!has_parameters_1 && !has_parameters_2) {
      return true;
    }
    // Exactly one lacks parameters.
    // Allow match if it is an audio RED codec and at least one of the
    // codecs has an unassigned payload type or they have the same ID.
    if (codec_to_match.type == Codec::Type::kAudio &&
        codec_to_match.name == kRedCodecName &&
        (codec_to_match.id == PayloadType::NotSet() ||
         potential_match.id == PayloadType::NotSet() ||
         codec_to_match.id == potential_match.id)) {
      return true;
    }
    return false;
  }
  return true;  // Not a codec with a PT-valued reference.
}

bool MatchesWithCodecRules(const Codec& left_codec, const Codec& right_codec) {
  // Match the codec id/name based on the typical static/dynamic name rules.
  // Matching is case-insensitive.

  // We support the ranges [96, 127] and more recently [35, 63].
  // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1
  // Within those ranges we match by codec name, outside by codec id.
  // We also match by name if either ID is unassigned.
  // Since no codecs are assigned an id in the range [66, 95] by us, these will
  // never match.
  const bool is_id_in_dynamic_range =
      (left_codec.id >= PayloadType::kLowerDynamicRangeMin &&
       left_codec.id <= PayloadType::kLowerDynamicRangeMax) ||
      (left_codec.id >= PayloadType::kUpperDynamicRangeMin &&
       left_codec.id <= PayloadType::kUpperDynamicRangeMax);
  const bool is_codec_id_in_dynamic_range =
      (right_codec.id >= PayloadType::kLowerDynamicRangeMin &&
       right_codec.id <= PayloadType::kLowerDynamicRangeMax) ||
      (right_codec.id >= PayloadType::kUpperDynamicRangeMin &&
       right_codec.id <= PayloadType::kUpperDynamicRangeMax);

  if (left_codec.type != right_codec.type) {
    return false;
  }

  bool matches_id;
  if ((is_id_in_dynamic_range && is_codec_id_in_dynamic_range) ||
      left_codec.id == PayloadType::NotSet() ||
      right_codec.id == PayloadType::NotSet()) {
    matches_id = absl::EqualsIgnoreCase(left_codec.name, right_codec.name);
  } else {
    matches_id = (left_codec.id == right_codec.id);
  }

  auto matches_type_specific = [&]() {
    switch (left_codec.type) {
      case Codec::Type::kAudio:
        // If a nonzero bitrate is specified, it must match the
        // actual bitrate, unless the codec is VBR (0), where we just force the
        // supplied value. The number of channels must match exactly, with the
        // exception that channels=0 is treated synonymously as channels=1, per
        // RFC 4566 section 6: " [The channels] parameter is OPTIONAL and may be
        // omitted if the number of channels is one."
        // Preference is ignored.
        return ((left_codec.clockrate == right_codec.clockrate) &&
                (right_codec.bitrate == 0 || left_codec.bitrate <= 0 ||
                 left_codec.bitrate == right_codec.bitrate) &&
                ((right_codec.channels < 2 && left_codec.channels < 2) ||
                 left_codec.channels == right_codec.channels));

      case Codec::Type::kVideo:
        return IsSameCodecSpecific(left_codec.name, left_codec.params,
                                   right_codec.name, right_codec.params);
    }
  };

  return matches_id && matches_type_specific();
}

bool MatchesWithReferenceAttributes(const Codec& codec1, const Codec& codec2) {
  return MatchesWithReferenceAttributesAndComparator(
      codec1, codec2, [](PayloadType a, PayloadType b) { return a == b; });
}

// Finds a codec in `codecs2` that matches `codec_to_match`, which is
// a member of `codecs1`. If `codec_to_match` is an RED or RTX codec, both
// the codecs themselves and their associated codecs must match.
std::optional<Codec> FindMatchingCodec(const std::vector<Codec>& codecs1,
                                       const std::vector<Codec>& codecs2,
                                       const Codec& codec_to_match) {
  // `codec_to_match` should be a member of `codecs1`, in order to look up
  // RED/RTX codecs' associated codecs correctly. If not, that's a programming
  // error.
  RTC_DCHECK(absl::c_any_of(codecs1, [&codec_to_match](const Codec& codec) {
    return &codec == &codec_to_match;
  }));
  for (const Codec& potential_match : codecs2) {
    if (MatchesWithReferenceAttributesAndComparator(
            codec_to_match, potential_match,
            [&codecs1, &codecs2](PayloadType a, PayloadType b) {
              return ReferencedCodecsMatch(codecs1, a, codecs2, b);
            })) {
      return potential_match;
    }
  }
  return std::nullopt;
}

bool IsSameRtpCodec(const Codec& codec, const RtpCodec& rtp_codec) {
  RtpCodecParameters rtp_codec2 = codec.ToCodecParameters();

  if (!absl::EqualsIgnoreCase(rtp_codec.name, rtp_codec2.name) ||
      rtp_codec.kind != rtp_codec2.kind ||
      rtp_codec.num_channels != rtp_codec2.num_channels ||
      rtp_codec.clock_rate != rtp_codec2.clock_rate) {
    return false;
  }

  // audio/RED should ignore the parameters which specify payload types so
  // can not be compared.
  if (rtp_codec.kind == MediaType::AUDIO &&
      absl::EqualsIgnoreCase(rtp_codec.name, kRedCodecName)) {
    return true;
  }

  return InsertDefaultParams(rtp_codec.name, rtp_codec.parameters) ==
         InsertDefaultParams(rtp_codec2.name, rtp_codec2.parameters);
}

bool IsSameRtpCodecIgnoringLevel(const Codec& codec,
                                 const RtpCodec& rtp_codec) {
  RtpCodecParameters rtp_codec2 = codec.ToCodecParameters();

  if (!absl::EqualsIgnoreCase(rtp_codec.name, rtp_codec2.name) ||
      rtp_codec.kind != rtp_codec2.kind ||
      rtp_codec.num_channels != rtp_codec2.num_channels ||
      rtp_codec.clock_rate != rtp_codec2.clock_rate) {
    return false;
  }

  CodecParameterMap params1 =
      InsertDefaultParams(rtp_codec.name, rtp_codec.parameters);
  CodecParameterMap params2 =
      InsertDefaultParams(rtp_codec2.name, rtp_codec2.parameters);

  // Some video codecs are compatible with others (e.g. same profile but
  // different level). This comparison looks at the relevant parameters,
  // ignoring ones that are either irrelevant or unrecognized.
  if (rtp_codec.kind == MediaType::VIDEO && rtp_codec.IsMediaCodec()) {
    return IsSameCodecSpecific(rtp_codec.name, params1, rtp_codec2.name,
                               params2);
  }
  // audio/RED should ignore the parameters which specify payload types so
  // can not be compared.
  if (rtp_codec.kind == MediaType::AUDIO &&
      absl::EqualsIgnoreCase(rtp_codec.name, kRedCodecName)) {
    return true;
  }

  return params1 == params2;
}

bool IsSameH264PacketizationMode(const CodecParameterMap& left,
                                 const CodecParameterMap& right) {
  return H264GetPacketizationModeOrDefault(left) ==
         H264GetPacketizationModeOrDefault(right);
}

}  // namespace webrtc
