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

#include "media/formats/webm/webm_video_client.h"

#include "media/base/video_decoder_config.h"
#include "media/base/video_spatial_format.h"
#include "media/formats/mp4/box_definitions.h"
#include "media/formats/webm/webm_constants.h"
#include "media/media_buildflags.h"

namespace media {

namespace {

// Tries to parse |data| to extract the VP9 Profile ID, or returns Profile 0.
media::VideoCodecProfile GetVP9CodecProfile(const std::vector<uint8_t>& data,
                                            bool is_probably_10bit) {
  // VP9 CodecPrivate (http://wiki.webmproject.org/vp9-codecprivate) might have
  // Profile information in the first field, if present.
  constexpr uint8_t kVP9ProfileFieldId = 0x01;
  constexpr uint8_t kVP9ProfileFieldLength = 1;
  if (data.size() < 3 || data[0] != kVP9ProfileFieldId ||
      data[1] != kVP9ProfileFieldLength || data[2] > 3) {
    return is_probably_10bit ? VP9PROFILE_PROFILE2 : VP9PROFILE_PROFILE0;
  }

  return static_cast<VideoCodecProfile>(
      static_cast<size_t>(VP9PROFILE_PROFILE0) + data[2]);
}

#if BUILDFLAG(ENABLE_AV1_DECODER)
media::VideoCodecProfile GetAV1CodecProfile(const std::vector<uint8_t>& data) {
  if (data.empty()) {
    return AV1PROFILE_PROFILE_MAIN;
  }

  mp4::AV1CodecConfigurationRecord av1_config;
  if (av1_config.Parse(data)) {
    return av1_config.profile;
  }

  DLOG(WARNING) << "Failed to parser AV1 extra data for profile.";
  return AV1PROFILE_PROFILE_MAIN;
}
#endif  // BUILDFLAG(ENABLE_AV1_DECODER)

// Values for "StereoMode" are spec'd here:
// https://www.matroska.org/technical/elements.html#StereoMode
bool IsValidStereoMode(int64_t stereo_mode_code) {
  const int64_t stereo_mode_min = kWebMStereoModeMono;
  const int64_t stereo_mode_max = kWebMStereoModeBothEyesBlockRL;
  return stereo_mode_code >= stereo_mode_min &&
         stereo_mode_code <= stereo_mode_max;
}

}  // namespace

WebMVideoClient::WebMVideoClient(MediaLog* media_log)
    : media_log_(MediaLog::CloneSafely(media_log)),
      projection_parser_(media_log) {
  Reset();
}

WebMVideoClient::~WebMVideoClient() = default;

void WebMVideoClient::Reset() {
  pixel_width_ = -1;
  pixel_height_ = -1;
  crop_bottom_ = -1;
  crop_top_ = -1;
  crop_left_ = -1;
  crop_right_ = -1;
  display_width_ = -1;
  display_height_ = -1;
  display_unit_ = -1;
  alpha_mode_ = -1;
  colour_parsed_ = false;
  colour_parser_.Reset();
  stereo_mode_ = -1;
  projection_parsed_ = false;
  projection_parser_.Reset();
}

bool WebMVideoClient::InitializeConfig(
    const std::string& codec_id,
    const std::vector<uint8_t>& codec_private,
    EncryptionScheme encryption_scheme,
    VideoDecoderConfig* config) {
  DCHECK(config);

  bool is_8bit = true;
  VideoColorSpace color_space = VideoColorSpace::REC709();
  if (colour_parsed_) {
    WebMColorMetadata color_metadata = colour_parser_.GetWebMColorMetadata();
    color_space = color_metadata.color_space;
    if (!color_metadata.hdr_metadata.IsEmpty()) {
      config->set_hdr_metadata(color_metadata.hdr_metadata);
    }
    is_8bit = color_metadata.BitsPerChannel <= 8;
  }

  VideoTransformation transformation =
      projection_parsed_ ? projection_parser_.GetVideoTransformation()
                         : kNoTransformation;
  VideoCodec video_codec = VideoCodec::kUnknown;
  VideoCodecProfile profile = VIDEO_CODEC_PROFILE_UNKNOWN;
  if (codec_id == "V_VP8") {
    video_codec = VideoCodec::kVP8;
    profile = VP8PROFILE_ANY;
  } else if (codec_id == "V_VP9") {
    video_codec = VideoCodec::kVP9;
    profile = GetVP9CodecProfile(
        codec_private, color_space.GuessGfxColorSpace().IsHDR() ||
                           !config->hdr_metadata().IsEmpty() || !is_8bit);
#if BUILDFLAG(ENABLE_AV1_DECODER)
  } else if (codec_id == "V_AV1") {
    video_codec = VideoCodec::kAV1;
    profile = GetAV1CodecProfile(codec_private);
#endif
  } else {
    MEDIA_LOG(ERROR, media_log_) << "Unsupported video codec_id " << codec_id;
    return false;
  }

  if (pixel_width_ <= 0 || pixel_height_ <= 0)
    return false;

  // Set crop and display unit defaults if these elements are not present.
  if (crop_bottom_ == -1)
    crop_bottom_ = 0;

  if (crop_top_ == -1)
    crop_top_ = 0;

  if (crop_left_ == -1)
    crop_left_ = 0;

  if (crop_right_ == -1)
    crop_right_ = 0;

  if (display_unit_ == -1)
    display_unit_ = 0;

  gfx::Size coded_size(pixel_width_, pixel_height_);
  gfx::Rect visible_rect(crop_top_, crop_left_,
                         pixel_width_ - (crop_left_ + crop_right_),
                         pixel_height_ - (crop_top_ + crop_bottom_));
  // TODO(dalecurtis): This is not correct, but it's what's muxed in webm
  // containers with AV1 right now. So accept it. We won't get here unless the
  // build and runtime flags are enabled for AV1.
  if (display_unit_ == 0 ||
      (video_codec == VideoCodec::kAV1 && display_unit_ == 4)) {
    if (display_width_ <= 0)
      display_width_ = visible_rect.width();
    if (display_height_ <= 0)
      display_height_ = visible_rect.height();
  } else if (display_unit_ == 3) {
    if (display_width_ <= 0 || display_height_ <= 0)
      return false;
  } else {
    MEDIA_LOG(ERROR, media_log_)
        << "Unsupported display unit type " << display_unit_;
    return false;
  }
  gfx::Size natural_size = gfx::Size(display_width_, display_height_);

  config->Initialize(video_codec, profile,
                     alpha_mode_ == 1
                         ? VideoDecoderConfig::AlphaMode::kHasAlpha
                         : VideoDecoderConfig::AlphaMode::kIsOpaque,
                     color_space, transformation, coded_size, visible_rect,
                     natural_size, codec_private, encryption_scheme);

  VideoSpatialFormat spatial_format;
  if (stereo_mode_ != -1) {
    if (stereo_mode_ == kWebMStereoModeLeftRight) {
      spatial_format.stereo_mode = VideoStereoMode::kSideBySideLeftFirst;
    } else if (stereo_mode_ == kWebMStereoModeTopBottom) {
      spatial_format.stereo_mode = VideoStereoMode::kTopBottomLeftFirst;
    }
  }

  if (projection_parsed_) {
    spatial_format.projection_type = projection_parser_.GetProjectionType();
  }
  config->set_spatial_format(spatial_format);

  return config->IsValidConfig();
}

WebMParserClient* WebMVideoClient::OnListStart(int id) {
  if (id == kWebMIdColour) {
    colour_parsed_ = false;
    return &colour_parser_;
  }

  if (id == kWebMIdProjection) {
    if (projection_parsed_ == true) {
      MEDIA_LOG(ERROR, media_log_)
          << "Unexpected multiple Projection elements.";
      return nullptr;
    }
    return &projection_parser_;
  }

  return this;
}

bool WebMVideoClient::OnListEnd(int id) {
  if (id == kWebMIdColour) {
    colour_parsed_ = true;
  } else if (id == kWebMIdProjection) {
    if (!projection_parser_.OnListEnd(id)) {
      return false;
    }
    projection_parsed_ = true;
  }
  return true;
}

bool WebMVideoClient::OnUInt(int id, int64_t val) {
  int64_t* dst = nullptr;

  switch (id) {
    case kWebMIdPixelWidth:
      dst = &pixel_width_;
      break;
    case kWebMIdPixelHeight:
      dst = &pixel_height_;
      break;
    case kWebMIdPixelCropTop:
      dst = &crop_top_;
      break;
    case kWebMIdPixelCropBottom:
      dst = &crop_bottom_;
      break;
    case kWebMIdPixelCropLeft:
      dst = &crop_left_;
      break;
    case kWebMIdPixelCropRight:
      dst = &crop_right_;
      break;
    case kWebMIdDisplayWidth:
      dst = &display_width_;
      break;
    case kWebMIdDisplayHeight:
      dst = &display_height_;
      break;
    case kWebMIdDisplayUnit:
      dst = &display_unit_;
      break;
    case kWebMIdAlphaMode:
      dst = &alpha_mode_;
      break;
    case kWebMIdStereoMode:
      dst = &stereo_mode_;
      break;
    default:
      return true;
  }

  if (*dst != -1) {
    MEDIA_LOG(ERROR, media_log_) << "Multiple values for id " << std::hex << id
                                 << " specified (" << *dst << " and " << val
                                 << ")";
    return false;
  }

  if (id == kWebMIdStereoMode && !IsValidStereoMode(val)) {
    MEDIA_LOG(ERROR, media_log_)
        << "Unexpected value for StereoMode: 0x" << std::hex << val;
    return false;
  }

  *dst = val;
  return true;
}

bool WebMVideoClient::OnBinary(int id, base::span<const uint8_t> data) {
  // Accept binary fields we don't care about for now.
  return true;
}

bool WebMVideoClient::OnFloat(int id, double val) {
  // Accept float fields we don't care about for now.
  return true;
}

}  // namespace media
