/*
 *  Copyright (c) 2018 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 "rtc_tools/video_file_reader.h"

#include <cstdio>
#include <optional>
#include <string>
#include <vector>

#include "absl/strings/match.h"
#include "api/make_ref_counted.h"
#include "api/video/i420_buffer.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/string_encode.h"
#include "rtc_base/string_to_number.h"

namespace webrtc {
namespace test {

namespace {

bool ReadBytes(uint8_t* dst, size_t n, FILE* file) {
  return fread(reinterpret_cast<char*>(dst), /* size= */ 1, n, file) == n;
}

// Common base class for .yuv and .y4m files.
class VideoFile : public Video {
 public:
  VideoFile(int width,
            int height,
            const std::vector<fpos_t>& frame_positions,
            FILE* file)
      : width_(width),
        height_(height),
        frame_positions_(frame_positions),
        file_(file) {}

  ~VideoFile() override { fclose(file_); }

  size_t number_of_frames() const override { return frame_positions_.size(); }
  int width() const override { return width_; }
  int height() const override { return height_; }

  rtc::scoped_refptr<I420BufferInterface> GetFrame(
      size_t frame_index) const override {
    RTC_CHECK_LT(frame_index, frame_positions_.size());

    fsetpos(file_, &frame_positions_[frame_index]);
    rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(width_, height_);

    if (!ReadBytes(buffer->MutableDataY(), width_ * height_, file_) ||
        !ReadBytes(buffer->MutableDataU(),
                   buffer->ChromaWidth() * buffer->ChromaHeight(), file_) ||
        !ReadBytes(buffer->MutableDataV(),
                   buffer->ChromaWidth() * buffer->ChromaHeight(), file_)) {
      RTC_LOG(LS_ERROR) << "Could not read YUV data for frame " << frame_index;
      return nullptr;
    }

    return buffer;
  }

 private:
  const int width_;
  const int height_;
  const std::vector<fpos_t> frame_positions_;
  FILE* const file_;
};

}  // namespace

Video::Iterator::Iterator(const rtc::scoped_refptr<const Video>& video,
                          size_t index)
    : video_(video), index_(index) {}

Video::Iterator::Iterator(const Video::Iterator& other) = default;
Video::Iterator::Iterator(Video::Iterator&& other) = default;
Video::Iterator& Video::Iterator::operator=(Video::Iterator&&) = default;
Video::Iterator& Video::Iterator::operator=(const Video::Iterator&) = default;
Video::Iterator::~Iterator() = default;

rtc::scoped_refptr<I420BufferInterface> Video::Iterator::operator*() const {
  return video_->GetFrame(index_);
}
bool Video::Iterator::operator==(const Video::Iterator& other) const {
  return index_ == other.index_;
}
bool Video::Iterator::operator!=(const Video::Iterator& other) const {
  return !(*this == other);
}

Video::Iterator Video::Iterator::operator++(int) {
  const Iterator copy = *this;
  ++*this;
  return copy;
}

Video::Iterator& Video::Iterator::operator++() {
  ++index_;
  return *this;
}

Video::Iterator Video::begin() const {
  return Iterator(rtc::scoped_refptr<const Video>(this), 0);
}

Video::Iterator Video::end() const {
  return Iterator(rtc::scoped_refptr<const Video>(this), number_of_frames());
}

rtc::scoped_refptr<Video> OpenY4mFile(const std::string& file_name) {
  FILE* file = fopen(file_name.c_str(), "rb");
  if (file == nullptr) {
    RTC_LOG(LS_ERROR) << "Could not open input file for reading: " << file_name;
    return nullptr;
  }

  int parse_file_header_result = -1;
  if (fscanf(file, "YUV4MPEG2 %n", &parse_file_header_result) != 0 ||
      parse_file_header_result == -1) {
    RTC_LOG(LS_ERROR) << "File " << file_name
                      << " does not start with YUV4MPEG2 header";
    return nullptr;
  }

  std::string header_line;
  while (true) {
    const int c = fgetc(file);
    if (c == EOF) {
      RTC_LOG(LS_ERROR) << "Could not read header line";
      return nullptr;
    }
    if (c == '\n')
      break;
    header_line.push_back(static_cast<char>(c));
  }

  std::optional<int> width;
  std::optional<int> height;
  std::optional<float> fps;

  std::vector<std::string> fields;
  rtc::tokenize(header_line, ' ', &fields);
  for (const std::string& field : fields) {
    const char prefix = field.front();
    const std::string suffix = field.substr(1);
    switch (prefix) {
      case 'W':
        width = rtc::StringToNumber<int>(suffix);
        break;
      case 'H':
        height = rtc::StringToNumber<int>(suffix);
        break;
      case 'C':
        if (suffix != "420" && suffix != "420mpeg2") {
          RTC_LOG(LS_ERROR)
              << "Does not support any other color space than I420 or "
                 "420mpeg2, but was: "
              << suffix;
          return nullptr;
        }
        break;
      case 'F': {
        std::vector<std::string> fraction;
        rtc::tokenize(suffix, ':', &fraction);
        if (fraction.size() == 2) {
          const std::optional<int> numerator =
              rtc::StringToNumber<int>(fraction[0]);
          const std::optional<int> denominator =
              rtc::StringToNumber<int>(fraction[1]);
          if (numerator && denominator && *denominator != 0)
            fps = *numerator / static_cast<float>(*denominator);
          break;
        }
      }
    }
  }
  if (!width || !height) {
    RTC_LOG(LS_ERROR) << "Could not find width and height in file header";
    return nullptr;
  }
  if (!fps) {
    RTC_LOG(LS_ERROR) << "Could not find fps in file header";
    return nullptr;
  }
  RTC_LOG(LS_INFO) << "Video has resolution: " << *width << "x" << *height
                   << " " << *fps << " fps";
  if (*width % 2 != 0 || *height % 2 != 0) {
    RTC_LOG(LS_ERROR)
        << "Only supports even width/height so that chroma size is a "
           "whole number.";
    return nullptr;
  }

  const int i420_frame_size = 3 * *width * *height / 2;
  std::vector<fpos_t> frame_positions;
  while (true) {
    std::array<char, 6> read_buffer;
    if (fread(read_buffer.data(), 1, read_buffer.size(), file) <
            read_buffer.size() ||
        memcmp(read_buffer.data(), "FRAME\n", read_buffer.size()) != 0) {
      if (!feof(file)) {
        RTC_LOG(LS_ERROR) << "Did not find FRAME header, ignoring rest of file";
      }
      break;
    }
    fpos_t pos;
    fgetpos(file, &pos);
    frame_positions.push_back(pos);
    // Skip over YUV pixel data.
    fseek(file, i420_frame_size, SEEK_CUR);
  }
  if (frame_positions.empty()) {
    RTC_LOG(LS_ERROR) << "Could not find any frames in the file";
    return nullptr;
  }
  RTC_LOG(LS_INFO) << "Video has " << frame_positions.size() << " frames";

  return rtc::make_ref_counted<VideoFile>(*width, *height, frame_positions,
                                          file);
}

rtc::scoped_refptr<Video> OpenYuvFile(const std::string& file_name,
                                      int width,
                                      int height) {
  FILE* file = fopen(file_name.c_str(), "rb");
  if (file == nullptr) {
    RTC_LOG(LS_ERROR) << "Could not open input file for reading: " << file_name;
    return nullptr;
  }

  if (width % 2 != 0 || height % 2 != 0) {
    RTC_LOG(LS_ERROR)
        << "Only supports even width/height so that chroma size is a "
           "whole number.";
    return nullptr;
  }

  // Seek to end of file.
  fseek(file, 0, SEEK_END);
  const size_t file_size = ftell(file);
  // Seek back to beginning of file.
  fseek(file, 0, SEEK_SET);

  const int i420_frame_size = 3 * width * height / 2;
  const size_t number_of_frames = file_size / i420_frame_size;

  std::vector<fpos_t> frame_positions;
  for (size_t i = 0; i < number_of_frames; ++i) {
    fpos_t pos;
    fgetpos(file, &pos);
    frame_positions.push_back(pos);
    fseek(file, i420_frame_size, SEEK_CUR);
  }
  if (frame_positions.empty()) {
    RTC_LOG(LS_ERROR) << "Could not find any frames in the file";
    return nullptr;
  }
  RTC_LOG(LS_INFO) << "Video has " << frame_positions.size() << " frames";

  return rtc::make_ref_counted<VideoFile>(width, height, frame_positions, file);
}

rtc::scoped_refptr<Video> OpenYuvOrY4mFile(const std::string& file_name,
                                           int width,
                                           int height) {
  if (absl::EndsWith(file_name, ".yuv"))
    return OpenYuvFile(file_name, width, height);
  if (absl::EndsWith(file_name, ".y4m"))
    return OpenY4mFile(file_name);

  RTC_LOG(LS_ERROR) << "Video file does not end in either .yuv or .y4m: "
                    << file_name;

  return nullptr;
}

}  // namespace test
}  // namespace webrtc