/*
 *  Copyright (c) 2022 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 "modules/video_coding/codecs/test/video_codec_analyzer.h"

#include <memory>

#include "api/task_queue/default_task_queue_factory.h"
#include "api/video/i420_buffer.h"
#include "api/video/video_codec_constants.h"
#include "api/video/video_frame.h"
#include "rtc_base/checks.h"
#include "rtc_base/event.h"
#include "rtc_base/time_utils.h"
#include "third_party/libyuv/include/libyuv/compare.h"

namespace webrtc {
namespace test {

namespace {
using Psnr = VideoCodecStats::Frame::Psnr;

Psnr CalcPsnr(const I420BufferInterface& ref_buffer,
              const I420BufferInterface& dec_buffer) {
  RTC_CHECK_EQ(ref_buffer.width(), dec_buffer.width());
  RTC_CHECK_EQ(ref_buffer.height(), dec_buffer.height());

  uint64_t sse_y = libyuv::ComputeSumSquareErrorPlane(
      dec_buffer.DataY(), dec_buffer.StrideY(), ref_buffer.DataY(),
      ref_buffer.StrideY(), dec_buffer.width(), dec_buffer.height());

  uint64_t sse_u = libyuv::ComputeSumSquareErrorPlane(
      dec_buffer.DataU(), dec_buffer.StrideU(), ref_buffer.DataU(),
      ref_buffer.StrideU(), dec_buffer.width() / 2, dec_buffer.height() / 2);

  uint64_t sse_v = libyuv::ComputeSumSquareErrorPlane(
      dec_buffer.DataV(), dec_buffer.StrideV(), ref_buffer.DataV(),
      ref_buffer.StrideV(), dec_buffer.width() / 2, dec_buffer.height() / 2);

  int num_y_samples = dec_buffer.width() * dec_buffer.height();
  Psnr psnr;
  psnr.y = libyuv::SumSquareErrorToPsnr(sse_y, num_y_samples);
  psnr.u = libyuv::SumSquareErrorToPsnr(sse_u, num_y_samples / 4);
  psnr.v = libyuv::SumSquareErrorToPsnr(sse_v, num_y_samples / 4);

  return psnr;
}

}  // namespace

VideoCodecAnalyzer::VideoCodecAnalyzer(
    ReferenceVideoSource* reference_video_source)
    : reference_video_source_(reference_video_source), num_frames_(0) {
  sequence_checker_.Detach();
}

void VideoCodecAnalyzer::StartEncode(const VideoFrame& input_frame) {
  int64_t encode_start_us = rtc::TimeMicros();
  task_queue_.PostTask(
      [this, timestamp_rtp = input_frame.timestamp(), encode_start_us]() {
        RTC_DCHECK_RUN_ON(&sequence_checker_);

        RTC_CHECK(frame_num_.find(timestamp_rtp) == frame_num_.end());
        frame_num_[timestamp_rtp] = num_frames_++;

        stats_.AddFrame({.frame_num = frame_num_[timestamp_rtp],
                         .timestamp_rtp = timestamp_rtp,
                         .encode_start = Timestamp::Micros(encode_start_us)});
      });
}

void VideoCodecAnalyzer::FinishEncode(const EncodedImage& frame) {
  int64_t encode_finished_us = rtc::TimeMicros();

  task_queue_.PostTask([this, timestamp_rtp = frame.Timestamp(),
                        spatial_idx = frame.SpatialIndex().value_or(0),
                        temporal_idx = frame.TemporalIndex().value_or(0),
                        width = frame._encodedWidth,
                        height = frame._encodedHeight,
                        frame_type = frame._frameType,
                        frame_size_bytes = frame.size(), qp = frame.qp_,
                        encode_finished_us]() {
    RTC_DCHECK_RUN_ON(&sequence_checker_);

    if (spatial_idx > 0) {
      VideoCodecStats::Frame* base_frame =
          stats_.GetFrame(timestamp_rtp, /*spatial_idx=*/0);

      stats_.AddFrame({.frame_num = base_frame->frame_num,
                       .timestamp_rtp = timestamp_rtp,
                       .spatial_idx = spatial_idx,
                       .encode_start = base_frame->encode_start});
    }

    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
    fs->spatial_idx = spatial_idx;
    fs->temporal_idx = temporal_idx;
    fs->width = width;
    fs->height = height;
    fs->frame_size = DataSize::Bytes(frame_size_bytes);
    fs->qp = qp;
    fs->keyframe = frame_type == VideoFrameType::kVideoFrameKey;
    fs->encode_time = Timestamp::Micros(encode_finished_us) - fs->encode_start;
    fs->encoded = true;
  });
}

void VideoCodecAnalyzer::StartDecode(const EncodedImage& frame) {
  int64_t decode_start_us = rtc::TimeMicros();
  task_queue_.PostTask([this, timestamp_rtp = frame.Timestamp(),
                        spatial_idx = frame.SpatialIndex().value_or(0),
                        frame_size_bytes = frame.size(), decode_start_us]() {
    RTC_DCHECK_RUN_ON(&sequence_checker_);

    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
    if (fs == nullptr) {
      if (frame_num_.find(timestamp_rtp) == frame_num_.end()) {
        frame_num_[timestamp_rtp] = num_frames_++;
      }
      stats_.AddFrame({.frame_num = frame_num_[timestamp_rtp],
                       .timestamp_rtp = timestamp_rtp,
                       .spatial_idx = spatial_idx,
                       .frame_size = DataSize::Bytes(frame_size_bytes)});
      fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
    }

    fs->decode_start = Timestamp::Micros(decode_start_us);
  });
}

void VideoCodecAnalyzer::FinishDecode(const VideoFrame& frame,
                                      int spatial_idx) {
  int64_t decode_finished_us = rtc::TimeMicros();
  task_queue_.PostTask([this, timestamp_rtp = frame.timestamp(), spatial_idx,
                        width = frame.width(), height = frame.height(),
                        decode_finished_us]() {
    RTC_DCHECK_RUN_ON(&sequence_checker_);
    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
    fs->decode_time = Timestamp::Micros(decode_finished_us) - fs->decode_start;

    if (!fs->encoded) {
      fs->width = width;
      fs->height = height;
    }

    fs->decoded = true;
  });

  if (reference_video_source_ != nullptr) {
    // Copy hardware-backed frame into main memory to release output buffers
    // which number may be limited in hardware decoders.
    rtc::scoped_refptr<I420BufferInterface> decoded_buffer =
        frame.video_frame_buffer()->ToI420();

    task_queue_.PostTask([this, decoded_buffer,
                          timestamp_rtp = frame.timestamp(), spatial_idx]() {
      RTC_DCHECK_RUN_ON(&sequence_checker_);
      VideoFrame ref_frame = reference_video_source_->GetFrame(
          timestamp_rtp, {.width = decoded_buffer->width(),
                          .height = decoded_buffer->height()});
      rtc::scoped_refptr<I420BufferInterface> ref_buffer =
          ref_frame.video_frame_buffer()->ToI420();

      Psnr psnr = CalcPsnr(*decoded_buffer, *ref_buffer);

      VideoCodecStats::Frame* fs =
          this->stats_.GetFrame(timestamp_rtp, spatial_idx);
      fs->psnr = psnr;
    });
  }
}

std::unique_ptr<VideoCodecStats> VideoCodecAnalyzer::GetStats() {
  std::unique_ptr<VideoCodecStats> stats;
  rtc::Event ready;
  task_queue_.PostTask([this, &stats, &ready]() mutable {
    RTC_DCHECK_RUN_ON(&sequence_checker_);
    stats.reset(new VideoCodecStatsImpl(stats_));
    ready.Set();
  });
  ready.Wait(rtc::Event::kForever);
  return stats;
}

}  // namespace test
}  // namespace webrtc
