#include "media/frame_decode.hpp"
#include "media/ffmpeg_raii.hpp"
#include "media/hardware_decode.hpp"
#include "media/video_frame_index.hpp"

#include <algorithm>
#include <limits>
#include <iomanip>
#include <random>
#include <sstream>
#include <vector>

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/imgutils.h>
}
#endif

namespace smartcut::media {

#ifdef SMARTCUT_HAS_FFMPEG
namespace {

std::uint64_t hash_frame(const AVFrame* frame) {
  const auto format = static_cast<AVPixelFormat>(frame->format);
  const int size = av_image_get_buffer_size(format, frame->width, frame->height, 1);
  if (size <= 0) return 0;
  std::vector<std::uint8_t> packed(static_cast<std::size_t>(size));
  if (av_image_copy_to_buffer(packed.data(), size, frame->data, frame->linesize,
                              format, frame->width, frame->height, 1) < 0) return 0;
  std::uint64_t hash = 14695981039346656037ULL;
  for (const auto value : packed) {
    hash ^= value;
    hash *= 1099511628211ULL;
  }
  return hash;
}

std::string hash_text(std::uint64_t hash) {
  std::ostringstream output;
  output << std::hex << std::setfill('0') << std::setw(16) << hash;
  return output.str();
}

std::int64_t frame_timestamp(const AVFrame* frame) {
  return frame->best_effort_timestamp != AV_NOPTS_VALUE ? frame->best_effort_timestamp : frame->pts;
}

struct FrameSignature {
  std::int64_t pts = AV_NOPTS_VALUE;
  std::uint64_t hash = 0;
};

bool open_video(const std::string& path, AVFormatContext*& format, AVCodecContext*& context, int& video_stream) {
  if (avformat_open_input(&format, path.c_str(), nullptr, nullptr) < 0) return false;
  if (avformat_find_stream_info(format, nullptr) < 0) return false;
  video_stream = -1;
  for (unsigned i = 0; i < format->nb_streams; ++i) {
    if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      video_stream = static_cast<int>(i);
      break;
    }
  }
  const AVCodec* decoder = video_stream >= 0
      ? ffmpeg::preferred_video_decoder(format->streams[video_stream]->codecpar->codec_id) : nullptr;
  context = decoder ? avcodec_alloc_context3(decoder) : nullptr;
  return context && avcodec_parameters_to_context(context, format->streams[video_stream]->codecpar) >= 0
      && avcodec_open2(context, decoder, nullptr) >= 0;
}

void close_video(AVFormatContext*& format, AVCodecContext*& context) {
  avcodec_free_context(&context);
  avformat_close_input(&format);
}

std::vector<FrameSignature> linear_signatures(AVFormatContext* format, AVCodecContext* context, int video_stream) {
  std::vector<FrameSignature> signatures;
  AVFrame* frame = av_frame_alloc();
  AVPacket* packet = av_packet_alloc();
  auto receive = [&] {
    while (avcodec_receive_frame(context, frame) >= 0) {
      signatures.push_back({frame_timestamp(frame), hash_frame(frame)});
      av_frame_unref(frame);
    }
  };
  while (av_read_frame(format, packet) >= 0) {
    if (packet->stream_index == video_stream && avcodec_send_packet(context, packet) >= 0) receive();
    av_packet_unref(packet);
  }
  avcodec_send_packet(context, nullptr);
  receive();
  av_packet_free(&packet);
  av_frame_free(&frame);
  return signatures;
}

bool seek_signature(AVFormatContext* format, AVCodecContext* context, int video_stream,
                    std::int64_t target_pts, FrameSignature& signature) {
  if (av_seek_frame(format, video_stream, target_pts, AVSEEK_FLAG_BACKWARD) < 0) return false;
  avcodec_flush_buffers(context);
  AVFrame* frame = av_frame_alloc();
  AVPacket* packet = av_packet_alloc();
  bool found = false;
  auto receive = [&] {
    while (!found && avcodec_receive_frame(context, frame) >= 0) {
      const auto pts = frame_timestamp(frame);
      if (pts != AV_NOPTS_VALUE && pts >= target_pts) {
        signature = {pts, hash_frame(frame)};
        found = true;
      }
      av_frame_unref(frame);
    }
  };
  while (!found && av_read_frame(format, packet) >= 0) {
    if (packet->stream_index == video_stream && avcodec_send_packet(context, packet) >= 0) receive();
    av_packet_unref(packet);
  }
  if (!found) {
    avcodec_send_packet(context, nullptr);
    receive();
  }
  av_packet_free(&packet);
  av_frame_free(&frame);
  return found;
}

}  // namespace
#endif

std::string decode_frame(const std::string& path, std::size_t frame_index, std::size_t max_decode_frames) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)frame_index; (void)max_decode_frames;
  return "{\"error\":\"FFmpeg support is not enabled; configure with SMARTCUT_MEDIA_ROOT\"}";
#else
  AVFormatContext* format = nullptr;
  if (avformat_open_input(&format, path.c_str(), nullptr, nullptr) < 0) return "{\"error\":\"avformat_open_input failed\"}";
  if (avformat_find_stream_info(format, nullptr) < 0) { avformat_close_input(&format); return "{\"error\":\"avformat_find_stream_info failed\"}"; }
  int video_stream = -1;
  for (unsigned i = 0; i < format->nb_streams; ++i) if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream = static_cast<int>(i); break; }
  const AVCodec* decoder = video_stream >= 0 ? ffmpeg::preferred_video_decoder(format->streams[video_stream]->codecpar->codec_id) : nullptr;
  AVCodecContext* context = decoder ? avcodec_alloc_context3(decoder) : nullptr;
  if (!context || avcodec_parameters_to_context(context, format->streams[video_stream]->codecpar) < 0 || avcodec_open2(context, decoder, nullptr) < 0) {
    avcodec_free_context(&context); avformat_close_input(&format); return "{\"error\":\"video decoder could not be opened\"}";
  }
  AVFrame* frame = av_frame_alloc(); AVPacket* packet = av_packet_alloc();
  std::size_t decoded = 0; bool found = false; std::ostringstream result;
  const auto stream_time_base = format->streams[video_stream]->time_base;
  const auto frame_rate = av_guess_frame_rate(format, format->streams[video_stream], nullptr);
  const auto indexed_pts = indexed_frame_pts(path, frame_index);
  const bool rate_seek = frame_rate.num > 0 && frame_rate.den > 0;
  const bool seek_assisted = indexed_pts.has_value() || rate_seek;
  const auto target_pts = indexed_pts.has_value() ? *indexed_pts
      : rate_seek ? av_rescale_q(static_cast<std::int64_t>(frame_index), AVRational{frame_rate.den, frame_rate.num}, stream_time_base) : 0;
  if (seek_assisted) { av_seek_frame(format, video_stream, target_pts, AVSEEK_FLAG_BACKWARD); avcodec_flush_buffers(context); }
  while (packet && av_read_frame(format, packet) >= 0 && !found && (max_decode_frames == 0 || decoded < max_decode_frames)) {
    if (packet->stream_index == video_stream && avcodec_send_packet(context, packet) >= 0) {
      while (avcodec_receive_frame(context, frame) >= 0) {
        const bool target_frame = seek_assisted ? (frame->pts != AV_NOPTS_VALUE && frame->pts >= target_pts) : decoded == frame_index;
        if (target_frame) {
          result << "{\"schema\":\"decoded-frame-v1\",\"frame_index\":" << frame_index << ",\"pts\":" << frame->pts
                 << ",\"width\":" << frame->width << ",\"height\":" << frame->height << ",\"pixel_format\":" << frame->format
                 << ",\"color_range\":" << frame->color_range << ",\"color_primaries\":" << frame->color_primaries
                 << ",\"color_transfer\":" << frame->color_trc << ",\"color_matrix\":" << frame->colorspace
                 << ",\"pixel_hash\":\"" << hash_text(hash_frame(frame)) << "\"}";
          found = true; break;
        }
        ++decoded; av_frame_unref(frame);
      }
    }
    av_packet_unref(packet);
  }
  if (!found && context && packet) {
    avcodec_send_packet(context, nullptr);
    while (avcodec_receive_frame(context, frame) >= 0) { const bool target_frame = seek_assisted ? (frame->pts != AV_NOPTS_VALUE && frame->pts >= target_pts) : decoded == frame_index; if (target_frame) { result << "{\"schema\":\"decoded-frame-v1\",\"frame_index\":" << frame_index << ",\"pts\":" << frame->pts << ",\"width\":" << frame->width << ",\"height\":" << frame->height << ",\"pixel_format\":" << frame->format << ",\"color_range\":" << frame->color_range << ",\"color_primaries\":" << frame->color_primaries << ",\"color_transfer\":" << frame->color_trc << ",\"color_matrix\":" << frame->colorspace << ",\"pixel_hash\":\"" << hash_text(hash_frame(frame)) << "\"}"; found = true; break; } ++decoded; av_frame_unref(frame); }
  }
  av_frame_free(&frame); av_packet_free(&packet); avcodec_free_context(&context); avformat_close_input(&format);
  if (!found) { std::ostringstream error; error << "{\"error\":\"requested frame was not decoded\",\"decoded_frames\":" << decoded << "}"; return error.str(); }
  return result.str();
#endif
}

std::string validate_random_seeks(const std::string& path, std::size_t iterations, std::uint64_t seed) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)iterations; (void)seed;
  return "{\"schema\":\"seek-validation-report-v1\",\"status\":\"FAILED\",\"error\":\"FFmpeg support is not enabled\"}";
#else
  AVFormatContext* format = nullptr;
  AVCodecContext* context = nullptr;
  int video_stream = -1;
  if (!open_video(path, format, context, video_stream)) {
    close_video(format, context);
    return "{\"schema\":\"seek-validation-report-v1\",\"status\":\"FAILED\",\"error\":\"video decoder could not be opened\"}";
  }
  const auto references = linear_signatures(format, context, video_stream);
  if (references.empty()) {
    close_video(format, context);
    return "{\"schema\":\"seek-validation-report-v1\",\"status\":\"FAILED\",\"error\":\"linear decode produced no frames\"}";
  }
  std::mt19937_64 generator(seed);
  std::uniform_int_distribution<std::size_t> distribution(0, references.size() - 1);
  std::size_t passed = 0;
  std::size_t failed_index = std::numeric_limits<std::size_t>::max();
  FrameSignature actual;
  for (std::size_t iteration = 0; iteration < iterations; ++iteration) {
    const auto index = distribution(generator);
    actual = {};
    if (!seek_signature(format, context, video_stream, references[index].pts, actual)
        || actual.pts != references[index].pts || actual.hash != references[index].hash) {
      failed_index = index;
      break;
    }
    ++passed;
  }
  close_video(format, context);
  std::ostringstream report;
  report << "{\"schema\":\"seek-validation-report-v1\",\"status\":\""
         << (passed == iterations ? "PASSED" : "FAILED") << "\",\"iterations\":" << iterations
         << ",\"passed\":" << passed << ",\"reference_frames\":" << references.size()
         << ",\"seed\":" << seed;
  if (failed_index != std::numeric_limits<std::size_t>::max()) {
    report << ",\"failed_frame_index\":" << failed_index
           << ",\"expected_pts\":" << references[failed_index].pts << ",\"actual_pts\":" << actual.pts
           << ",\"expected_hash\":\"" << hash_text(references[failed_index].hash)
           << "\",\"actual_hash\":\"" << hash_text(actual.hash) << "\"";
  }
  report << "}";
  return report.str();
#endif
}

std::shared_ptr<core::PreviewFrame> decode_preview_frame(const std::string& path, std::size_t frame_index,
                                                         std::optional<std::int64_t> target_pts_override) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)frame_index; (void)target_pts_override; return {};
#else
  const auto decode = [&](bool allow_hardware) {
    AVFormatContext* format = nullptr;
    if (avformat_open_input(&format, path.c_str(), nullptr, nullptr) < 0 ||
        avformat_find_stream_info(format, nullptr) < 0) {
      avformat_close_input(&format);
      return std::shared_ptr<core::PreviewFrame>{};
    }
    int video_stream = -1;
    for (unsigned index = 0; index < format->nb_streams; ++index)
      if (format->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream = static_cast<int>(index);
        break;
      }
    auto* stream = video_stream >= 0 ? format->streams[video_stream] : nullptr;
    const AVCodec* software_decoder = stream ? ffmpeg::preferred_video_decoder(stream->codecpar->codec_id) : nullptr;
    HardwareVideoDecoder decoder;
    if (!stream || !decoder.open(stream->codecpar, stream->codecpar->codec_id,
                                 software_decoder, allow_hardware)) {
      avformat_close_input(&format);
      return std::shared_ptr<core::PreviewFrame>{};
    }
    AVFrame* frame = av_frame_alloc();
    AVFrame* transferred = av_frame_alloc();
    AVPacket* packet = av_packet_alloc();
    std::size_t decoded = 0;
    bool transfer_failed = false;
    std::shared_ptr<core::PreviewFrame> result;
    const auto target_pts = target_pts_override ? target_pts_override :
        (frame_index == 0 ? std::optional<std::int64_t>{} : indexed_frame_pts(path, frame_index));
    if (target_pts.has_value()) {
      av_seek_frame(format, video_stream, *target_pts, AVSEEK_FLAG_BACKWARD);
      avcodec_flush_buffers(decoder.context());
    }
    auto receive = [&] {
      while (!result && !transfer_failed &&
             avcodec_receive_frame(decoder.context(), frame) >= 0) {
        const auto pts = frame_timestamp(frame);
        const bool requested = target_pts.has_value()
            ? pts != AV_NOPTS_VALUE && pts >= *target_pts : decoded == frame_index;
        ++decoded;
        if (requested) {
          const auto* cpu = decoder.cpu_frame(frame, transferred);
          if (!cpu) {
            transfer_failed = true;
          } else {
            auto preview = std::make_shared<core::PreviewFrame>();
            preview->sequence = frame_index; preview->frame_index = frame_index;
            preview->width = cpu->width; preview->height = cpu->height;
            preview->stride = cpu->linesize[0]; preview->pixel_format = cpu->format;
            preview->color_range = cpu->color_range; preview->color_primaries = cpu->color_primaries;
            preview->color_transfer = cpu->color_trc; preview->color_matrix = cpu->colorspace;
            preview->decode_backend = decoder.hardware_active()
                ? decoder.backend_name() : "software";
            const int size = av_image_get_buffer_size(static_cast<AVPixelFormat>(cpu->format),
                                                      cpu->width, cpu->height, 1);
            if (size > 0) {
              preview->pixels.resize(static_cast<std::size_t>(size));
              if (av_image_copy_to_buffer(preview->pixels.data(), size, cpu->data, cpu->linesize,
                                          static_cast<AVPixelFormat>(cpu->format), cpu->width,
                                          cpu->height, 1) >= 0)
                result = std::move(preview);
            }
          }
        }
        av_frame_unref(frame);
      }
    };
    while (packet && !result && !transfer_failed && av_read_frame(format, packet) >= 0) {
      if (packet->stream_index == video_stream &&
          avcodec_send_packet(decoder.context(), packet) >= 0) receive();
      av_packet_unref(packet);
    }
    if (!result && !transfer_failed) {
      avcodec_send_packet(decoder.context(), nullptr);
      receive();
    }
    av_frame_free(&frame);
    av_frame_free(&transferred);
    av_packet_free(&packet);
    avformat_close_input(&format);
    return result;
  };
  auto result = decode(true);
  return result ? result : decode(false);
#endif
}

std::vector<std::shared_ptr<core::PreviewFrame>> decode_preview_frames(
    const std::string& path,
    const std::vector<std::pair<std::size_t, std::int64_t>>& requests,
    int output_width) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)requests; (void)output_width; return {};
#else
  std::vector<std::shared_ptr<core::PreviewFrame>> results(requests.size());
  if (requests.empty()) return results;
  const auto decode = [&](bool allow_hardware) {
    std::vector<std::shared_ptr<core::PreviewFrame>> decoded_results(requests.size());
    AVFormatContext* format = nullptr;
    if (avformat_open_input(&format, path.c_str(), nullptr, nullptr) < 0 ||
        avformat_find_stream_info(format, nullptr) < 0) {
      avformat_close_input(&format);
      return decoded_results;
    }
    int video_stream = -1;
    for (unsigned index = 0; index < format->nb_streams; ++index)
      if (format->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream = static_cast<int>(index);
        break;
      }
    auto* stream = video_stream >= 0 ? format->streams[video_stream] : nullptr;
    const auto* software_decoder = stream ? ffmpeg::preferred_video_decoder(stream->codecpar->codec_id) : nullptr;
    HardwareVideoDecoder decoder;
    if (!stream || !decoder.open(stream->codecpar, stream->codecpar->codec_id,
                                 software_decoder, allow_hardware)) {
      avformat_close_input(&format);
      return decoded_results;
    }
    AVFrame* frame = av_frame_alloc();
    AVFrame* gpu_scaled = av_frame_alloc();
    AVFrame* transferred = av_frame_alloc();
    AVPacket* packet = av_packet_alloc();
    HardwareFrameScaler hardware_scaler;
    bool transfer_failed = false;
    bool hardware_scaling_used = false;
    auto cleanup = [&] {
      av_frame_free(&frame); av_frame_free(&gpu_scaled); av_frame_free(&transferred); av_packet_free(&packet);
      avformat_close_input(&format);
    };
    if (!frame || !gpu_scaled || !transferred || !packet ||
        av_seek_frame(format, video_stream, requests.front().second, AVSEEK_FLAG_BACKWARD) < 0) {
      cleanup();
      return decoded_results;
    }
    avformat_flush(format);
    avcodec_flush_buffers(decoder.context());
    std::size_t request = 0;
    const auto capture = [&](const AVFrame* source, std::size_t requested_frame) {
      const AVFrame* conversion_source = source;
      if (output_width > 0 && decoder.is_hardware_frame(source)) {
        int output_height = std::max(2, static_cast<int>(
            static_cast<std::int64_t>(source->height) * output_width / source->width));
        output_height -= output_height % 2;
        if (const auto* scaled = hardware_scaler.scale(source, gpu_scaled,
                                                       output_width, output_height)) {
          conversion_source = scaled;
          hardware_scaling_used = true;
        }
      }
      const auto* cpu = decoder.cpu_frame(conversion_source, transferred);
      if (!cpu) {
        transfer_failed = true;
        return std::shared_ptr<core::PreviewFrame>{};
      }
      auto preview = std::make_shared<core::PreviewFrame>();
      preview->sequence = requested_frame; preview->frame_index = requested_frame;
      preview->width = cpu->width; preview->height = cpu->height;
      preview->stride = cpu->linesize[0]; preview->pixel_format = cpu->format;
      preview->color_range = cpu->color_range; preview->color_primaries = cpu->color_primaries;
      preview->color_transfer = cpu->color_trc; preview->color_matrix = cpu->colorspace;
      preview->decode_backend = decoder.hardware_active()
          ? (hardware_scaling_used
                 ? decoder.backend_name() + "+" + hardware_scaler.backend_name()
                 : decoder.backend_name())
          : "software";
      const int size = av_image_get_buffer_size(static_cast<AVPixelFormat>(cpu->format),
                                                cpu->width, cpu->height, 1);
      if (size <= 0) return std::shared_ptr<core::PreviewFrame>{};
      preview->pixels.resize(static_cast<std::size_t>(size));
      if (av_image_copy_to_buffer(preview->pixels.data(), size, cpu->data, cpu->linesize,
                                  static_cast<AVPixelFormat>(cpu->format), cpu->width,
                                  cpu->height, 1) < 0)
        return std::shared_ptr<core::PreviewFrame>{};
      return preview;
    };
    const auto receive = [&] {
      while (request < requests.size() && !transfer_failed &&
             avcodec_receive_frame(decoder.context(), frame) >= 0) {
        const auto pts = frame_timestamp(frame);
        while (request < requests.size() && pts != AV_NOPTS_VALUE &&
               pts >= requests[request].second) {
          decoded_results[request] = capture(frame, requests[request].first);
          ++request;
        }
        av_frame_unref(frame);
      }
    };
    while (request < requests.size() && !transfer_failed &&
           av_read_frame(format, packet) >= 0) {
      if (packet->stream_index == video_stream &&
          avcodec_send_packet(decoder.context(), packet) >= 0) receive();
      av_packet_unref(packet);
    }
    if (request < requests.size() && !transfer_failed) {
      avcodec_send_packet(decoder.context(), nullptr);
      receive();
    }
    cleanup();
    if (transfer_failed) std::fill(decoded_results.begin(), decoded_results.end(), nullptr);
    return decoded_results;
  };
  results = decode(true);
  if (std::any_of(results.cbegin(), results.cend(), [](const auto& frame) { return !frame; }))
    results = decode(false);
  return results;
#endif
}

PreviewStreamResult decode_preview_stream(
    const std::string& path, std::size_t start_frame, std::int64_t start_pts,
    const std::function<bool(std::shared_ptr<core::PreviewFrame>, std::uint64_t)>& present,
    bool allow_hardware) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)start_frame; (void)start_pts; (void)present; (void)allow_hardware;
  return {};
#else
  PreviewStreamResult report;
  AVFormatContext* format = nullptr;
  if (avformat_open_input(&format, path.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(format, nullptr) < 0) {
    avformat_close_input(&format);
    return report;
  }
  int video_stream = -1;
  for (unsigned index = 0; index < format->nb_streams; ++index) {
    if (format->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      video_stream = static_cast<int>(index);
      break;
    }
  }
  auto* stream = video_stream >= 0 ? format->streams[video_stream] : nullptr;
  const auto* software_decoder = stream
      ? ffmpeg::preferred_video_decoder(stream->codecpar->codec_id) : nullptr;
  HardwareVideoDecoder decoder;
  if (!stream || !decoder.open(stream->codecpar, stream->codecpar->codec_id,
                               software_decoder, allow_hardware)) {
    avformat_close_input(&format);
    return report;
  }
  AVFrame* decoded = av_frame_alloc();
  AVFrame* transferred = av_frame_alloc();
  AVPacket* packet = av_packet_alloc();
  if (!decoded || !transferred || !packet ||
      av_seek_frame(format, video_stream, start_pts, AVSEEK_FLAG_BACKWARD) < 0) {
    av_frame_free(&decoded); av_frame_free(&transferred); av_packet_free(&packet);
    avformat_close_input(&format);
    return report;
  }
  avformat_flush(format);
  avcodec_flush_buffers(decoder.context());
  bool keep_decoding = true;
  bool failed = false;
  std::size_t frame_index = start_frame;
  const auto receive = [&] {
    while (keep_decoding && !failed &&
           avcodec_receive_frame(decoder.context(), decoded) >= 0) {
      const auto pts = frame_timestamp(decoded);
      if (pts == AV_NOPTS_VALUE || pts < start_pts) {
        av_frame_unref(decoded);
        continue;
      }
      const auto* cpu = decoder.cpu_frame(decoded, transferred);
      if (!cpu) {
        failed = true;
        av_frame_unref(decoded);
        break;
      }
      auto frame = std::make_shared<core::PreviewFrame>();
      frame->sequence = frame_index + 1;
      frame->frame_index = frame_index++;
      frame->width = cpu->width; frame->height = cpu->height;
      frame->stride = cpu->linesize[0]; frame->pixel_format = cpu->format;
      frame->color_range = cpu->color_range;
      frame->color_primaries = cpu->color_primaries;
      frame->color_transfer = cpu->color_trc;
      frame->color_matrix = cpu->colorspace;
      frame->decode_backend = decoder.hardware_active()
          ? decoder.backend_name() : "software";
      const int size = av_image_get_buffer_size(static_cast<AVPixelFormat>(cpu->format),
                                                cpu->width, cpu->height, 1);
      if (size <= 0) {
        failed = true;
      } else {
        frame->pixels.resize(static_cast<std::size_t>(size));
        if (av_image_copy_to_buffer(frame->pixels.data(), size, cpu->data, cpu->linesize,
                                    static_cast<AVPixelFormat>(cpu->format), cpu->width,
                                    cpu->height, 1) < 0) {
          failed = true;
        }
      }
      const auto source_us = pts == AV_NOPTS_VALUE ? std::uint64_t{0}
          : static_cast<std::uint64_t>(std::max<std::int64_t>(0, av_rescale_q(
                pts, stream->time_base, AVRational{1, 1000000})));
      av_frame_unref(decoded);
      av_frame_unref(transferred);
      if (failed) break;
      ++report.decoded_frames;
      report.hardware_accelerated = report.hardware_accelerated || decoder.hardware_active();
      report.backend = decoder.hardware_active() ? decoder.backend_name() : "software";
      if (present && !present(std::move(frame), source_us)) keep_decoding = false;
    }
  };
  while (keep_decoding && !failed && av_read_frame(format, packet) >= 0) {
    if (packet->stream_index == video_stream &&
        avcodec_send_packet(decoder.context(), packet) >= 0) receive();
    av_packet_unref(packet);
  }
  if (keep_decoding && !failed) {
    avcodec_send_packet(decoder.context(), nullptr);
    receive();
  }
  report.completed = !failed;
  av_frame_free(&decoded); av_frame_free(&transferred); av_packet_free(&packet);
  avformat_close_input(&format);
  return report;
#endif
}

}  // namespace smartcut::media
