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

#include "chromecast/starboard/media/renderer/chromium_starboard_conversions.h"

#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "chromecast/starboard/media/media/mime_utils.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "media/base/audio_codecs.h"
#include "media/base/channel_layout.h"
#include "media/base/video_codecs.h"
#include "media/base/video_color_space.h"
#include "third_party/abseil-cpp/absl/container/node_hash_set.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/hdr_metadata.h"

namespace chromecast {
namespace media {

namespace {

using ::media::VideoColorSpace;

// Registers the given MIME type, if it has not already been registered. Returns
// a c string of the MIME type, which is guaranteed to point to valid data for
// the duration of the program.
const char* RegisterMimeType(std::string mime) {
  if (mime.empty()) {
    return "";
  }

  static base::NoDestructor<base::Lock> lock;
  // A node_hash_set is used here because we require key ptr stability (so that
  // c strings remain valid).
  static base::NoDestructor<absl::node_hash_set<std::string>> registry
      GUARDED_BY(*lock);

  base::AutoLock autlock(*lock);
  auto it_and_inserted = registry->insert(std::move(mime));
  return it_and_inserted.first->c_str();
}

// Converts a chromium codec to a starboard codec, returning nullopt if the
// codec does not exist in starboard.
//
// `profile` is necessary for differentiating Dolby Vision codecs.
std::optional<StarboardVideoCodec> ToSbVideoCodec(
    ::media::VideoCodec codec,
    ::media::VideoCodecProfile profile) {
  switch (codec) {
    case ::media::VideoCodec::kH264:
      return StarboardVideoCodec::kStarboardVideoCodecH264;
    case ::media::VideoCodec::kMPEG2:
      return StarboardVideoCodec::kStarboardVideoCodecMpeg2;
    case ::media::VideoCodec::kTheora:
      return StarboardVideoCodec::kStarboardVideoCodecTheora;
    case ::media::VideoCodec::kVP8:
      return StarboardVideoCodec::kStarboardVideoCodecVp8;
    case ::media::VideoCodec::kVP9:
      return StarboardVideoCodec::kStarboardVideoCodecVp9;
    case ::media::VideoCodec::kHEVC:
      return StarboardVideoCodec::kStarboardVideoCodecH265;
    case ::media::VideoCodec::kAV1:
      return StarboardVideoCodec::kStarboardVideoCodecAv1;
    case ::media::VideoCodec::kVC1:
      return StarboardVideoCodec::kStarboardVideoCodecVc1;
    case ::media::VideoCodec::kDolbyVision:
      switch (profile) {
        // This logic was copied from
        // https://source.chromium.org/chromium/chromium/src/+/main:chromecast/media/base/media_codec_support.cc;l=63;drc=586d9e059d27bfbe85c8df737882821e7b68929d
        case ::media::VideoCodecProfile::DOLBYVISION_PROFILE5:
        case ::media::VideoCodecProfile::DOLBYVISION_PROFILE7:
        case ::media::VideoCodecProfile::DOLBYVISION_PROFILE8:
          return StarboardVideoCodec::kStarboardVideoCodecH265;
        default:
          // We only support Dolby Vision for HEVC currently. The H264 profiles
          // (DOLBYVISION_PROFILE0 and DOLBYVISION_PROFILE9) are considered
          // unsupported.
          LOG(INFO) << "Unsupported Dolby Vision profile=" << profile;
          return std::nullopt;
      }
    default:
      LOG(ERROR) << "Unsupported video codec: " << codec;
      return std::nullopt;
  }
}

// Populates HDR fields of `out_color_metadata` based on `hdr_metadata`.
void PopulateHdrMetadata(const gfx::HDRMetadata& hdr_metadata,
                         StarboardColorMetadata& out_color_metadata) {
  if (hdr_metadata.HasCLLI()) {
    const auto& cta_861_3 = hdr_metadata.GetCLLI();
    out_color_metadata.max_cll = cta_861_3.fMaxCLL;
    out_color_metadata.max_fall = cta_861_3.fMaxFALL;
  } else {
    LOG(INFO) << "HDR metadata is missing cta_861_3 info.";
  }

  if (hdr_metadata.HasMDCV()) {
    const auto& smpte_st_2086 = hdr_metadata.GetMDCV();
    const auto& color_volume_metadata = smpte_st_2086.fDisplayPrimaries;

    StarboardMediaMasteringMetadata& mastering_metadata =
        out_color_metadata.mastering_metadata;
    mastering_metadata.primary_r_chromaticity_x = color_volume_metadata.fRX;
    mastering_metadata.primary_r_chromaticity_y = color_volume_metadata.fRY;
    mastering_metadata.primary_g_chromaticity_x = color_volume_metadata.fGX;
    mastering_metadata.primary_g_chromaticity_y = color_volume_metadata.fGY;
    mastering_metadata.primary_b_chromaticity_x = color_volume_metadata.fBX;
    mastering_metadata.primary_b_chromaticity_y = color_volume_metadata.fBY;
    mastering_metadata.white_point_chromaticity_x = color_volume_metadata.fWX;
    mastering_metadata.white_point_chromaticity_y = color_volume_metadata.fWY;
    mastering_metadata.luminance_max =
        smpte_st_2086.fMaximumDisplayMasteringLuminance;
    mastering_metadata.luminance_min =
        smpte_st_2086.fMinimumDisplayMasteringLuminance;
  } else {
    LOG(INFO) << "HDR metadata is missing smpte_st_2086 info.";
  }
}

// Converts chromium color metadata to starboard color metadata, returning
// nullopt if the chromium color metadata cannot be converted.
std::optional<StarboardColorMetadata> ToSbColorMetadata(
    const gfx::HDRMetadata& hdr_metadata,
    const ::media::VideoColorSpace& color_space) {
  StarboardColorMetadata color_metadata = {};
  // bits_per_channel and the chroma_*/cb_* fields below need to be derived from
  // the MIME string. See crbug.com/230915942 for more info.
  // Unfortunately, it doesn't look like MIME type is exposed to cast. Note that
  // in Cobalt, these fields are all currently hard-coded to zero (in
  // third_party/chromium/media/base/starboard_utils.cc). I don't think they're
  // necessary for cast either, since cast doesn't seem to populate this info
  // anywhere.

  // 0 translates to "unknown".
  color_metadata.bits_per_channel = 0;
  color_metadata.chroma_subsampling_horizontal = 0;
  color_metadata.chroma_subsampling_vertical = 0;
  color_metadata.cb_subsampling_horizontal = 0;
  color_metadata.cb_subsampling_vertical = 0;
  color_metadata.chroma_siting_horizontal = 0;
  color_metadata.chroma_siting_vertical = 0;

  if (!hdr_metadata.IsEmpty()) {
    LOG(INFO) << "Video config has HDR metadata.";
    PopulateHdrMetadata(hdr_metadata, color_metadata);
  } else {
    LOG(INFO) << "Video config has no HDR metadata.";
    color_metadata.max_cll = 0;
    color_metadata.max_fall = 0;
  }

  switch (color_space.primaries()) {
    case VideoColorSpace::PrimaryID::INVALID:
    case VideoColorSpace::PrimaryID::BT709:
    case VideoColorSpace::PrimaryID::UNSPECIFIED:
    case VideoColorSpace::PrimaryID::BT470M:
    case VideoColorSpace::PrimaryID::BT470BG:
    case VideoColorSpace::PrimaryID::SMPTE170M:
    case VideoColorSpace::PrimaryID::SMPTE240M:
    case VideoColorSpace::PrimaryID::FILM:
    case VideoColorSpace::PrimaryID::BT2020:
    case VideoColorSpace::PrimaryID::SMPTEST428_1:
    case VideoColorSpace::PrimaryID::SMPTEST431_2:
    case VideoColorSpace::PrimaryID::SMPTEST432_1:
      color_metadata.primaries = static_cast<int>(color_space.primaries());
      break;
    default:
      LOG(ERROR) << "Unsupported color space primaries: "
                 << static_cast<int>(color_space.primaries());
      return std::nullopt;
  }

  switch (color_space.transfer()) {
    case VideoColorSpace::TransferID::INVALID:
    case VideoColorSpace::TransferID::BT709:
    case VideoColorSpace::TransferID::UNSPECIFIED:
    case VideoColorSpace::TransferID::GAMMA22:
    case VideoColorSpace::TransferID::GAMMA28:
    case VideoColorSpace::TransferID::SMPTE170M:
    case VideoColorSpace::TransferID::SMPTE240M:
    case VideoColorSpace::TransferID::LINEAR:
    case VideoColorSpace::TransferID::LOG:
    case VideoColorSpace::TransferID::LOG_SQRT:
    case VideoColorSpace::TransferID::IEC61966_2_4:
    case VideoColorSpace::TransferID::BT1361_ECG:
    case VideoColorSpace::TransferID::IEC61966_2_1:
    case VideoColorSpace::TransferID::BT2020_10:
    case VideoColorSpace::TransferID::BT2020_12:
    case VideoColorSpace::TransferID::SMPTEST2084:
    case VideoColorSpace::TransferID::SMPTEST428_1:
      color_metadata.transfer = static_cast<int>(color_space.transfer());
      break;
    default:
      LOG(ERROR) << "Unsupported color space transfer: "
                 << static_cast<int>(color_space.transfer());
      return std::nullopt;
  }

  switch (color_space.matrix()) {
    case VideoColorSpace::MatrixID::RGB:
    case VideoColorSpace::MatrixID::BT709:
    case VideoColorSpace::MatrixID::UNSPECIFIED:
    case VideoColorSpace::MatrixID::FCC:
    case VideoColorSpace::MatrixID::BT470BG:
    case VideoColorSpace::MatrixID::SMPTE170M:
    case VideoColorSpace::MatrixID::SMPTE240M:
    case VideoColorSpace::MatrixID::YCOCG:
    case VideoColorSpace::MatrixID::BT2020_NCL:
    case VideoColorSpace::MatrixID::BT2020_CL:
    case VideoColorSpace::MatrixID::YDZDX:
    case VideoColorSpace::MatrixID::INVALID:
      color_metadata.matrix = static_cast<int>(color_space.matrix());
      break;
    default:
      LOG(ERROR) << "Unsupported color space matrix: "
                 << static_cast<int>(color_space.matrix());
      return std::nullopt;
  }

  switch (color_space.range()) {
    case gfx::ColorSpace::RangeID::INVALID:
    case gfx::ColorSpace::RangeID::LIMITED:
    case gfx::ColorSpace::RangeID::FULL:
    case gfx::ColorSpace::RangeID::DERIVED:
      color_metadata.range = static_cast<int>(color_space.range());
      break;
    default:
      LOG(ERROR) << "Unsupported color space range: "
                 << static_cast<int>(color_space.range());
      return std::nullopt;
  }

  // color_space.primaries() (::media::VideoColorSpace::PrimaryID)
  // does not support any value equivalent to Starboard's
  // kSbMediaPrimaryIdCustom. Thus, we don't need to populate
  // custom_primary_matrix. Just zero it, in case something reads from it.
  return color_metadata;
}

}  // namespace

std::optional<StarboardAudioSampleInfo> ToStarboardAudioSampleInfo(
    const ::media::AudioDecoderConfig& in_config) {
  StarboardAudioSampleInfo out_config = {};

  switch (in_config.codec()) {
    case ::media::AudioCodec::kAAC:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecAac;
      break;
    case ::media::AudioCodec::kMP3:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecMp3;
      break;
    case ::media::AudioCodec::kVorbis:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecVorbis;
      break;
    case ::media::AudioCodec::kFLAC:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecFlac;
      break;
    case ::media::AudioCodec::kPCM:
    case ::media::AudioCodec::kPCM_S16BE:
    case ::media::AudioCodec::kPCM_S24BE:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecPcm;
      break;
    case ::media::AudioCodec::kOpus:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecOpus;
      break;
    case ::media::AudioCodec::kEAC3:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecEac3;
      break;
    case ::media::AudioCodec::kAC3:
      out_config.codec = StarboardAudioCodec::kStarboardAudioCodecAc3;
      break;
    default:
      LOG(ERROR) << "Unsupported audio codec: " << in_config.codec();
      return std::nullopt;
  }

  out_config.mime = RegisterMimeType(GetMimeType(in_config.codec()));
  out_config.format_tag = 0;
  out_config.number_of_channels = in_config.channels();
  out_config.samples_per_second = in_config.samples_per_second();

  // Based on starboard_utils.cc (MediaAudioConfigToSbMediaAudioSampleInfo) in
  // the cobalt codebase, bits_per_sample  does not take into account the number
  // of channels.
  if (out_config.codec == StarboardAudioCodec::kStarboardAudioCodecPcm) {
    // TODO(antoniori): handle resampling to other formats. Currently we only
    // support S16.
    out_config.bits_per_sample = 16;
  } else {
    // For starboard, "bits per sample" does not factor in channel count.
    out_config.bits_per_sample = in_config.bytes_per_channel() * 8;
  }
  out_config.average_bytes_per_second = out_config.number_of_channels *
                                        out_config.samples_per_second *
                                        out_config.bits_per_sample / 8;
  out_config.block_alignment = 4;
  out_config.audio_specific_config_size = in_config.extra_data().size();
  out_config.audio_specific_config = in_config.extra_data().data();

  return out_config;
}

std::optional<StarboardVideoSampleInfo> ToStarboardVideoSampleInfo(
    const ::media::VideoDecoderConfig& in_config) {
  StarboardVideoSampleInfo out_config = {};

  std::optional<StarboardVideoCodec> sb_codec =
      ToSbVideoCodec(in_config.codec(), in_config.profile());
  if (!sb_codec.has_value()) {
    return std::nullopt;
  }

  out_config.codec = *sb_codec;
  out_config.mime = RegisterMimeType(
      GetMimeType(in_config.codec(), in_config.profile(), in_config.level()));
  // Specify that the max capabilities are not known.
  out_config.max_video_capabilities = "";
  // This needs to be set on a per-sample basis later.
  out_config.is_key_frame = false;

  const gfx::Size aspect_ratio = in_config.coded_size();
  out_config.frame_width = aspect_ratio.width();
  out_config.frame_height = aspect_ratio.height();

  std::optional<StarboardColorMetadata> sb_color_metadata =
      ToSbColorMetadata(in_config.hdr_metadata(), in_config.color_space_info());
  if (!sb_color_metadata.has_value()) {
    return std::nullopt;
  }
  out_config.color_metadata = *std::move(sb_color_metadata);

  return out_config;
}

}  // namespace media
}  // namespace chromecast
