/*
 * 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 "video/corruption_detection/halton_frame_sampler.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <memory>
#include <vector>

#include "api/scoped_refptr.h"
#include "api/video/i420_buffer.h"
#include "api/video/nv12_buffer.h"
#include "api/video/video_frame.h"
#include "api/video/video_frame_buffer.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/safe_minmax.h"
#include "video/corruption_detection/halton_sequence.h"
#include "video/corruption_detection/video_frame_sampler.h"

namespace webrtc {
namespace {

const double kCutoff = 0.2;
const int kLowerBoundKernelSize = 3;
constexpr int kMaxFramesBetweenSamples = 33;

// Corresponds to 1 second for RTP timestamps (which are 90kHz).
constexpr uint32_t kMaxDurationBetweenSamples = 90'000;

// The second *time* is always later than the first. If the second *timestamp*
// is smaller than the first, we interpret that as if one wraparound has
// occurred.
uint32_t EnoughTimeHasPassed(uint32_t from, uint32_t to) {
  return (to - from) >= kMaxDurationBetweenSamples;
}

}  // namespace

HaltonFrameSampler::HaltonFrameSampler()
    : coordinate_sampler_prng_(HaltonSequence(2)) {}

std::vector<HaltonFrameSampler::Coordinates>
HaltonFrameSampler::GetSampleCoordinatesForFrameIfFrameShouldBeSampled(
    bool is_key_frame,
    uint32_t rtp_timestamp,
    int num_samples) {
  if (num_samples < 1) {
    return {};
  }
  if (rtp_timestamp_last_frame_sampled_.has_value()) {
    RTC_CHECK_NE(*rtp_timestamp_last_frame_sampled_, rtp_timestamp);
  }
  if (is_key_frame || frames_until_next_sample_ <= 0 ||
      !rtp_timestamp_last_frame_sampled_.has_value() ||
      EnoughTimeHasPassed(*rtp_timestamp_last_frame_sampled_, rtp_timestamp)) {
    frames_until_next_sample_ =
        (kMaxFramesBetweenSamples - 1) - (frames_sampled_ % 8);
    ++frames_sampled_;
    rtp_timestamp_last_frame_sampled_ = rtp_timestamp;
    return GetSampleCoordinatesForFrame(num_samples);
  }
  --frames_until_next_sample_;
  return {};
}

std::vector<HaltonFrameSampler::Coordinates>
HaltonFrameSampler::GetSampleCoordinatesForFrame(int num_samples) {
  RTC_CHECK_GE(num_samples, 1);
  std::vector<Coordinates> coordinates;
  coordinates.reserve(num_samples);
  for (int i = 0; i < num_samples; ++i) {
    coordinates.push_back(GetNextSampleCoordinates());
  }
  return coordinates;
}

HaltonFrameSampler::Coordinates HaltonFrameSampler::GetNextSampleCoordinates() {
  std::vector<double> point = coordinate_sampler_prng_.GetNext();
  return {.row = point[0], .column = point[1]};
}

void HaltonFrameSampler::Restart() {
  coordinate_sampler_prng_.Reset();
}

int HaltonFrameSampler::GetCurrentIndex() const {
  return coordinate_sampler_prng_.GetCurrentIndex();
}

void HaltonFrameSampler::SetCurrentIndex(int index) {
  coordinate_sampler_prng_.SetCurrentIndex(index);
}

// Apply Gaussian filtering to the data.
double GetFilteredElement(const VideoFrameSampler& frame_sampler,
                          VideoFrameSampler::ChannelType channel,
                          int row,
                          int column,
                          double std_dev) {
  RTC_CHECK_GE(row, 0);
  RTC_CHECK_LT(row, frame_sampler.height(channel));
  RTC_CHECK_GE(column, 0);
  RTC_CHECK_LT(column, frame_sampler.width(channel));
  RTC_CHECK_GE(std_dev, 0.0);

  // `std_dev` being zero should ideally correspond to a very low QP value. In
  // this case even a noisy pixel should be able to be encoded and transmitted
  // correctly. Hence, the pixel value can be used as is.
  if (std_dev == 0.0) {
    return frame_sampler.GetSampleValue(channel, column, row);
  }

  int max_distance =
      std::ceil(std::sqrt(-2.0 * std::log(kCutoff) * std::pow(std_dev, 2.0))) -
      1;
  // In order to counteract unexpected distortions (such as noise), a lower
  // bound for blurring is introduced. This is done to reduce false positives
  // caused by these distortions.
  // False positives are decreased since for small `std_dev`s the quantization
  // is strong and would cut of many of the small continuous weights used for
  // robust comparision.
  max_distance = std::max(kLowerBoundKernelSize, max_distance);

  double element_sum = 0.0;
  double total_weight = 0.0;
  for (int r = std::max(row - max_distance, 0);
       r < std::min(row + max_distance + 1, frame_sampler.height(channel));
       ++r) {
    for (int c = std::max(column - max_distance, 0);
         c < std::min(column + max_distance + 1, frame_sampler.width(channel));
         ++c) {
      double weight =
          std::exp(-1.0 * (std::pow(row - r, 2) + std::pow(column - c, 2)) /
                   (2.0 * std::pow(std_dev, 2)));
      element_sum += frame_sampler.GetSampleValue(channel, c, r) * weight;
      total_weight += weight;
    }
  }

  // Take the rounding errors into consideration.
  return SafeClamp(element_sum / total_weight, 0.0, 255.0);
}

std::vector<FilteredSample> GetSampleValuesForFrame(
    const VideoFrame& frame,
    std::vector<HaltonFrameSampler::Coordinates> sample_coordinates,
    int scaled_width,
    int scaled_height,
    double std_dev_gaussian_blur) {
  // Validate input.
  if (frame.video_frame_buffer() == nullptr) {
    RTC_LOG(LS_WARNING) << "The framebuffer must not be nullptr";
    return {};
  }
  if (sample_coordinates.empty()) {
    RTC_LOG(LS_WARNING) << "There must be at least one coordinate provided";
    return {};
  }
  for (HaltonFrameSampler::Coordinates coordinate : sample_coordinates) {
    if (coordinate.column < 0.0 || coordinate.column >= 1.0 ||
        coordinate.row < 0.0 || coordinate.row >= 1.0) {
      RTC_LOG(LS_WARNING) << "The coordinates must be in [0,1): column="
                          << coordinate.column << ", row=" << coordinate.row
                          << ".\n";
      return {};
    }
  }
  if (scaled_width <= 0 || scaled_height <= 0) {
    RTC_LOG(LS_WARNING)
        << "The width and height to scale to must be positive: width="
        << scaled_width << ", height=" << scaled_height << ".\n";
    return {};
  }
  if (std_dev_gaussian_blur < 0.0) {
    RTC_LOG(LS_WARNING)
        << "The standard deviation for the Gaussian blur must not be negative: "
        << std_dev_gaussian_blur << ".\n";
    return {};
  }
  if (scaled_width > frame.width() || scaled_height > frame.height()) {
    RTC_LOG(LS_WARNING)
        << "Upscaling causes corruption. Therefore, only down-scaling is "
           "permissible.";
    return {};
  }

  VideoFrame scaled_frame = frame;
  std::unique_ptr<VideoFrameSampler> frame_sampler;
  if (scaled_width == frame.width() && scaled_height == frame.height()) {
    frame_sampler = VideoFrameSampler::Create(frame);
  } else {
    // Scale the frame to the desired resolution:
    // 1. Create a new buffer with the desired resolution.
    // 2. Scale the old buffer to the size of the new buffer.
    if (frame.video_frame_buffer()->type() == VideoFrameBuffer::Type::kNV12) {
      scoped_refptr<NV12Buffer> scaled_buffer =
          NV12Buffer::Create(scaled_width, scaled_height);
      // Set crop width/height to full width/height so this is only a scaling
      // operation, no cropping happening.
      scaled_buffer->CropAndScaleFrom(
          *frame.video_frame_buffer()->GetNV12(), /*offset_x=*/0,
          /*offset_y=*/0, /*crop_width=*/frame.width(),
          /*crop_height=*/frame.height());
      scaled_frame.set_video_frame_buffer(scaled_buffer);
    } else {
      scoped_refptr<I420Buffer> scaled_buffer =
          I420Buffer::Create(scaled_width, scaled_height);
      scoped_refptr<I420BufferInterface> buffer =
          frame.video_frame_buffer()->ToI420();
      if (buffer == nullptr) {
        RTC_LOG(LS_WARNING) << "Unable to convert frame to I420 format.";
        return {};
      }
      scaled_buffer->ScaleFrom(*buffer);
      scaled_frame.set_video_frame_buffer(scaled_buffer);
    }
    frame_sampler = VideoFrameSampler::Create(scaled_frame);
  }
  if (frame_sampler == nullptr) {
    RTC_LOG(LS_WARNING) << "Unable to create frame sampler for buffer type "
                        << VideoFrameBufferTypeToString(
                               frame.video_frame_buffer()->type());
    return {};
  }

  // Treat the planes as if they would have the following 2-dimensional layout:
  // +------+---+
  // |      | U |
  // |  Y   +---+
  // |      | V |
  // +------+---+
  // where width:=(Y.width+U.width) and height:=Y.height.
  // When interpreting the 2D sample coordinates, we simply treat them
  // as if they were taken from the above layout. We then need to translate the
  // coordinates back to the corresponding plane's corresponding 2D coordinates.
  // Then we find the filtered value that corresponds to those coordinates.
  RTC_DCHECK_EQ(frame_sampler->width(VideoFrameSampler::ChannelType::U),
                frame_sampler->width(VideoFrameSampler::ChannelType::V))
      << "Chroma channels are expected to be equal in resolution.";
  int width_merged_planes =
      frame_sampler->width(VideoFrameSampler::ChannelType::Y) +
      frame_sampler->width(VideoFrameSampler::ChannelType::U);
  int height_merged_planes =
      frame_sampler->height(VideoFrameSampler::ChannelType::Y);
  // Fetch the sample value for all of the requested coordinates.
  std::vector<FilteredSample> filtered_samples;
  filtered_samples.reserve(sample_coordinates.size());
  for (HaltonFrameSampler::Coordinates coordinate : sample_coordinates) {
    // Scale the coordinates from [0,1) to [0,`width_merged_planes`) and
    // [0,`height_merged_planes`). Truncation is intentional.
    int column = coordinate.column * width_merged_planes;
    int row = coordinate.row * height_merged_planes;

    // Map to plane coordinates and fetch the value.
    double value_for_coordinate;
    if (column < frame_sampler->width(VideoFrameSampler::ChannelType::Y)) {
      // Y plane.
      value_for_coordinate =
          GetFilteredElement(*frame_sampler, VideoFrameSampler::ChannelType::Y,
                             row, column, std_dev_gaussian_blur);
      filtered_samples.push_back(
          {.value = value_for_coordinate, .plane = ImagePlane::kLuma});
    } else if (row < frame_sampler->height(VideoFrameSampler::ChannelType::U)) {
      // U plane.
      column -= frame_sampler->width(VideoFrameSampler::ChannelType::Y);
      value_for_coordinate =
          GetFilteredElement(*frame_sampler, VideoFrameSampler::ChannelType::U,
                             row, column, std_dev_gaussian_blur);
      filtered_samples.push_back(
          {.value = value_for_coordinate, .plane = ImagePlane::kChroma});
    } else {
      // V plane.
      column -= frame_sampler->width(VideoFrameSampler::ChannelType::Y);
      row -= frame_sampler->height(VideoFrameSampler::ChannelType::U);
      value_for_coordinate =
          GetFilteredElement(*frame_sampler, VideoFrameSampler::ChannelType::V,
                             row, column, std_dev_gaussian_blur);
      filtered_samples.push_back(
          {.value = value_for_coordinate, .plane = ImagePlane::kChroma});
    }
  }
  return filtered_samples;
}

[[deprecated]] std::vector<FilteredSample> GetSampleValuesForFrame(
    scoped_refptr<I420BufferInterface> i420_frame_buffer,
    std::vector<HaltonFrameSampler::Coordinates> sample_coordinates,
    int scaled_width,
    int scaled_height,
    double std_dev_gaussian_blur) {
  return GetSampleValuesForFrame(
      VideoFrame::Builder().set_video_frame_buffer(i420_frame_buffer).build(),
      sample_coordinates, scaled_width, scaled_height, std_dev_gaussian_blur);
}

}  // namespace webrtc
