#include "media/multitrack_fixture.hpp"

#include <cstring>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <sstream>
#include <vector>

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavformat/avformat.h>
}
#endif

namespace smartcut::media {
std::string make_dolby_e_s337m_fixture(const std::string& output, int frames) {
  if (frames < 4) return "{\"schema\":\"dolby-e-fixture-v1\",\"status\":\"FAILED\",\"reason\":\"at least four frames are required\"}";
  std::ofstream file(output, std::ios::binary | std::ios::trunc);
  if (!file) return "{\"schema\":\"dolby-e-fixture-v1\",\"status\":\"FAILED\",\"reason\":\"output open failed\"}";
  constexpr std::uint8_t preamble[]{0x72, 0xF8, 0x1F, 0x4E, 0x1C, 0x00, 0x00, 0xE4};
  std::vector<std::uint8_t> decoded_payload(3832, 0);
  for (int frame = 0; frame < frames; ++frame) {
    decoded_payload[0] = 0x07;
    decoded_payload[1] = 0x8E;
    decoded_payload[2] = 0x00;
    for (std::size_t index = 3; index < decoded_payload.size(); ++index)
      decoded_payload[index] = static_cast<std::uint8_t>((index * 17U + static_cast<unsigned>(frame) * 29U) & 0xffU);
    file.write(reinterpret_cast<const char*>(preamble), sizeof(preamble));
    for (std::size_t index = 0; index + 1 < decoded_payload.size(); index += 2) {
      file.put(static_cast<char>(decoded_payload[index + 1]));
      file.put(static_cast<char>(decoded_payload[index]));
    }
  }
  file.close();
  std::ostringstream report;
  report << "{\"schema\":\"dolby-e-fixture-v1\",\"status\":\"PASSED\",\"carrier\":\"smpte-337m-16le\",\"frames\":"
         << frames << ",\"bytes\":" << std::filesystem::file_size(output) << '}';
  return report.str();
}

std::string make_mkv_multitrack_fixture(const std::string& input, const std::string& output) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input;
  (void)output;
  return "{\"error\":\"FFmpeg support is not enabled\"}";
#else
  if (std::filesystem::weakly_canonical(input) == std::filesystem::weakly_canonical(output)) {
    return "{\"error\":\"source and output paths must differ\"}";
  }
  AVFormatContext* source = nullptr;
  AVFormatContext* destination = nullptr;
  AVPacket* packet = av_packet_alloc();
  bool completed = false;
  auto cleanup = [&] {
    av_packet_free(&packet);
    avformat_close_input(&source);
    if (destination && destination->pb) avio_closep(&destination->pb);
    avformat_free_context(destination);
    if (!completed) {
      std::error_code ignored;
      std::filesystem::remove(output, ignored);
    }
  };
  if (!packet || avformat_open_input(&source, input.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(source, nullptr) < 0) {
    cleanup();
    return "{\"error\":\"fixture input open failed\"}";
  }
  int audio_stream = -1;
  int video_stream = -1;
  for (unsigned i = 0; i < source->nb_streams; ++i) {
    if (source->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_stream < 0) video_stream = static_cast<int>(i);
    if (source->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream < 0) {
      audio_stream = static_cast<int>(i);
    }
  }
  if (video_stream < 0 || audio_stream < 0 || avformat_alloc_output_context2(&destination, nullptr, "matroska", output.c_str()) < 0 || !destination) {
    cleanup();
    return "{\"error\":\"fixture requires audio and Matroska output\"}";
  }
  av_dict_set(&destination->metadata, "title", "SmartCut64 M11 multitrack fixture", 0);
  for (unsigned i = 0; i < source->nb_streams; ++i) {
    AVStream* copied = avformat_new_stream(destination, nullptr);
    if (!copied || avcodec_parameters_copy(copied->codecpar, source->streams[i]->codecpar) < 0) {
      cleanup();
      return "{\"error\":\"fixture stream copy failed\"}";
    }
    copied->time_base = source->streams[i]->time_base;
    copied->disposition = source->streams[i]->disposition;
    av_dict_copy(&copied->metadata, source->streams[i]->metadata, 0);
    if (static_cast<int>(i) == audio_stream) {
      av_dict_set(&copied->metadata, "title", "Main Audio", 0);
      av_dict_set(&copied->metadata, "language", "eng", 0);
      copied->disposition |= AV_DISPOSITION_DEFAULT;
    }
  }
  AVStream* commentary = avformat_new_stream(destination, nullptr);
  if (!commentary || avcodec_parameters_copy(commentary->codecpar, source->streams[audio_stream]->codecpar) < 0) {
    cleanup();
    return "{\"error\":\"fixture commentary stream failed\"}";
  }
  commentary->time_base = source->streams[audio_stream]->time_base;
  commentary->disposition = 0;
  av_dict_set(&commentary->metadata, "title", "Commentary", 0);
  av_dict_set(&commentary->metadata, "language", "jpn", 0);

  AVStream* subtitles = avformat_new_stream(destination, nullptr);
  if (!subtitles) {
    cleanup();
    return "{\"error\":\"fixture subtitle stream failed\"}";
  }
  subtitles->time_base = AVRational{1, 1000};
  subtitles->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
  subtitles->codecpar->codec_id = AV_CODEC_ID_SUBRIP;
  subtitles->disposition = AV_DISPOSITION_DEFAULT;
  av_dict_set(&subtitles->metadata, "title", "English Subtitles", 0);
  av_dict_set(&subtitles->metadata, "language", "eng", 0);

  AVStream* attachment = avformat_new_stream(destination, nullptr);
  if (!attachment) {
    cleanup();
    return "{\"error\":\"fixture attachment stream failed\"}";
  }
  attachment->codecpar->codec_type = AVMEDIA_TYPE_ATTACHMENT;
  attachment->codecpar->codec_id = AV_CODEC_ID_TTF;
  constexpr std::uint8_t attachment_bytes[]{0x00, 0x01, 0x00, 0x00};
  attachment->codecpar->extradata = static_cast<std::uint8_t*>(av_mallocz(sizeof(attachment_bytes) + AV_INPUT_BUFFER_PADDING_SIZE));
  if (!attachment->codecpar->extradata) {
    cleanup();
    return "{\"error\":\"fixture attachment allocation failed\"}";
  }
  std::memcpy(attachment->codecpar->extradata, attachment_bytes, sizeof(attachment_bytes));
  attachment->codecpar->extradata_size = sizeof(attachment_bytes);
  av_dict_set(&attachment->metadata, "filename", "fixture-font.ttf", 0);
  av_dict_set(&attachment->metadata, "mimetype", "application/x-truetype-font", 0);

  destination->chapters = static_cast<AVChapter**>(av_mallocz(sizeof(AVChapter*)));
  AVChapter* chapter = static_cast<AVChapter*>(av_mallocz(sizeof(AVChapter)));
  if (!destination->chapters || !chapter) {
    av_free(chapter);
    cleanup();
    return "{\"error\":\"fixture chapter allocation failed\"}";
  }
  destination->chapters[0] = chapter;
  destination->nb_chapters = 1;
  chapter->id = 1;
  chapter->time_base = AVRational{1, 1000};
  chapter->start = 0;
  chapter->end = 220;
  av_dict_set(&chapter->metadata, "title", "Fixture chapter", 0);

  if ((!(destination->oformat->flags & AVFMT_NOFILE) && avio_open(&destination->pb, output.c_str(), AVIO_FLAG_WRITE) < 0) ||
      avformat_write_header(destination, nullptr) < 0) {
    cleanup();
    return "{\"error\":\"fixture output header failed\"}";
  }
  auto write_subtitle = [&](std::int64_t pts, std::int64_t duration, const char* text) {
    AVPacket* subtitle = av_packet_alloc();
    const auto bytes = std::strlen(text);
    if (!subtitle || av_new_packet(subtitle, static_cast<int>(bytes)) < 0) {
      av_packet_free(&subtitle);
      return -1;
    }
    std::memcpy(subtitle->data, text, bytes);
    subtitle->stream_index = subtitles->index;
    subtitle->pts = pts;
    subtitle->dts = pts;
    subtitle->duration = duration;
    const int write_result = av_interleaved_write_frame(destination, subtitle);
    av_packet_free(&subtitle);
    return write_result;
  };
  int result = write_subtitle(20, 140, "crosses the edit boundary");
  if (result >= 0) result = write_subtitle(170, 35, "after the edit boundary");
  std::size_t source_packets = 0;
  std::size_t commentary_packets = 0;
  std::size_t video_packet_index = 0;
  constexpr std::int64_t vfr_pts_ms[]{0, 33, 80, 123, 200};
  AVPacket* drift_video = nullptr;
  AVPacket* drift_audio = nullptr;
  while (result >= 0 && av_read_frame(source, packet) >= 0) {
    if (!drift_video && source->streams[packet->stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) drift_video = av_packet_clone(packet);
    if (!drift_audio && packet->stream_index == audio_stream && packet->pts >= 0) drift_audio = av_packet_clone(packet);
    AVPacket* duplicate = packet->stream_index == audio_stream ? av_packet_clone(packet) : nullptr;
    const int source_index = packet->stream_index;
    if (source->streams[source_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_packet_index < std::size(vfr_pts_ms)) {
      packet->pts = av_rescale_q(vfr_pts_ms[video_packet_index], AVRational{1, 1000}, source->streams[source_index]->time_base);
      packet->dts = packet->pts;
      if (video_packet_index + 1 < std::size(vfr_pts_ms)) {
        packet->duration = av_rescale_q(vfr_pts_ms[video_packet_index + 1] - vfr_pts_ms[video_packet_index], AVRational{1, 1000}, source->streams[source_index]->time_base);
      }
      ++video_packet_index;
    }
    av_packet_rescale_ts(packet, source->streams[source_index]->time_base,
                         destination->streams[source_index]->time_base);
    packet->stream_index = source_index;
    packet->pos = -1;
    result = av_interleaved_write_frame(destination, packet);
    if (result >= 0) ++source_packets;
    if (result >= 0 && duplicate) {
      if (duplicate->pts != AV_NOPTS_VALUE) duplicate->pts += av_rescale_q(7, AVRational{1, 1000}, source->streams[audio_stream]->time_base);
      if (duplicate->dts != AV_NOPTS_VALUE) duplicate->dts += av_rescale_q(7, AVRational{1, 1000}, source->streams[audio_stream]->time_base);
      av_packet_rescale_ts(duplicate, source->streams[audio_stream]->time_base, commentary->time_base);
      duplicate->stream_index = commentary->index;
      duplicate->pos = -1;
      result = av_interleaved_write_frame(destination, duplicate);
      if (result >= 0) ++commentary_packets;
    }
    av_packet_free(&duplicate);
    av_packet_unref(packet);
  }
  auto write_checkpoint = [&](const AVPacket* prototype, int stream_index, std::int64_t target_ms) {
    AVPacket* checkpoint = av_packet_clone(prototype);
    if (!checkpoint) return -1;
    checkpoint->pts = av_rescale_q(target_ms, AVRational{1, 1000}, destination->streams[stream_index]->time_base);
    checkpoint->dts = checkpoint->pts;
    checkpoint->stream_index = stream_index;
    checkpoint->pos = -1;
    const int write_result = av_interleaved_write_frame(destination, checkpoint);
    av_packet_free(&checkpoint);
    return write_result;
  };
  for (const std::int64_t checkpoint_ms : {600000LL, 1200000LL}) {
    if (result >= 0 && drift_video) result = write_checkpoint(drift_video, video_stream, checkpoint_ms);
    if (result >= 0 && drift_audio) result = write_checkpoint(drift_audio, audio_stream, checkpoint_ms);
    if (result >= 0 && drift_audio) result = write_checkpoint(drift_audio, commentary->index, checkpoint_ms + 7);
  }
  av_packet_free(&drift_video);
  av_packet_free(&drift_audio);
  if (result >= 0) result = av_write_trailer(destination);
  if (result >= 0) completed = true;
  cleanup();
  if (result < 0) return "{\"error\":\"fixture packet mux failed\"}";
  std::ostringstream report;
  report << "{\"schema\":\"m11-fixture-v1\",\"streams\":5,\"source_packets\":" << source_packets
         << ",\"commentary_packets\":" << commentary_packets
         << ",\"subtitle_events\":2,\"chapters\":1,\"attachments\":1,\"drift_checkpoints\":3}";
  return report.str();
#endif
}

std::string make_mkv_dvd_subtitle_fixture(const std::string& input, const std::string& output) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input;
  (void)output;
  return "{\"error\":\"FFmpeg support is not enabled\"}";
#else
  AVFormatContext* source = nullptr;
  AVFormatContext* destination = nullptr;
  AVPacket* packet = av_packet_alloc();
  bool completed = false;
  const auto cleanup = [&] {
    av_packet_free(&packet);
    avformat_close_input(&source);
    if (destination && destination->pb) avio_closep(&destination->pb);
    avformat_free_context(destination);
    if (!completed) { std::error_code ignored; std::filesystem::remove(output, ignored); }
  };
  if (!packet || avformat_open_input(&source, input.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(source, nullptr) < 0 ||
      avformat_alloc_output_context2(&destination, nullptr, "matroska", output.c_str()) < 0 ||
      !destination) {
    cleanup();
    return "{\"error\":\"DVD subtitle fixture setup failed\"}";
  }
  bool has_video = false;
  bool has_audio = false;
  for (unsigned index = 0; index < source->nb_streams; ++index) {
    const auto* input_stream = source->streams[index];
    has_video = has_video || input_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO;
    has_audio = has_audio || input_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
    auto* output_stream = avformat_new_stream(destination, nullptr);
    if (!output_stream || avcodec_parameters_copy(output_stream->codecpar, input_stream->codecpar) < 0) {
      cleanup();
      return "{\"error\":\"DVD subtitle fixture stream copy failed\"}";
    }
    output_stream->time_base = input_stream->time_base;
    output_stream->disposition = input_stream->disposition;
    av_dict_copy(&output_stream->metadata, input_stream->metadata, 0);
  }
  if (!has_video || !has_audio) {
    cleanup();
    return "{\"error\":\"DVD subtitle fixture requires video and audio\"}";
  }
  auto* subtitles = avformat_new_stream(destination, nullptr);
  if (!subtitles) {
    cleanup();
    return "{\"error\":\"DVD subtitle fixture subtitle setup failed\"}";
  }
  subtitles->time_base = AVRational{1, 1000};
  subtitles->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
  subtitles->codecpar->codec_id = AV_CODEC_ID_DVD_SUBTITLE;
  subtitles->codecpar->width = 720;
  subtitles->codecpar->height = 480;
  constexpr char codec_private[] =
      "size: 720x480\n"
      "palette: 000000, ffffff, 808080, c0c0c0, 202020, 404040, 606060, a0a0a0, "
      "101010, 303030, 505050, 707070, 909090, b0b0b0, d0d0d0, e0e0e0\n";
  subtitles->codecpar->extradata = static_cast<std::uint8_t*>(
      av_mallocz(sizeof(codec_private) - 1 + AV_INPUT_BUFFER_PADDING_SIZE));
  if (!subtitles->codecpar->extradata) {
    cleanup();
    return "{\"error\":\"DVD subtitle fixture private-data allocation failed\"}";
  }
  std::memcpy(subtitles->codecpar->extradata, codec_private, sizeof(codec_private) - 1);
  subtitles->codecpar->extradata_size = sizeof(codec_private) - 1;
  av_dict_set(&subtitles->metadata, "title", "DVD VobSub", 0);
  av_dict_set(&subtitles->metadata, "language", "eng", 0);
  if ((!(destination->oformat->flags & AVFMT_NOFILE) &&
       avio_open(&destination->pb, output.c_str(), AVIO_FLAG_WRITE) < 0) ||
      avformat_write_header(destination, nullptr) < 0) {
    cleanup();
    return "{\"error\":\"DVD subtitle fixture header failed\"}";
  }
  constexpr std::uint8_t spu[]{0x00, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x01, 0xff,
                               0x00, 0x00, 0x00, 0x00};
  AVPacket* subtitle = av_packet_alloc();
  int result = subtitle && av_new_packet(subtitle, static_cast<int>(sizeof(spu))) >= 0 ? 0 : -1;
  if (result >= 0) {
    std::memcpy(subtitle->data, spu, sizeof(spu));
    subtitle->stream_index = subtitles->index;
    subtitle->pts = 20;
    subtitle->dts = 20;
    subtitle->duration = 120;
    result = av_interleaved_write_frame(destination, subtitle);
  }
  av_packet_free(&subtitle);
  std::size_t packets = 0;
  while (result >= 0 && av_read_frame(source, packet) >= 0) {
    const int index = packet->stream_index;
    av_packet_rescale_ts(packet, source->streams[index]->time_base,
                         destination->streams[index]->time_base);
    packet->stream_index = index;
    packet->pos = -1;
    result = av_interleaved_write_frame(destination, packet);
    if (result >= 0) ++packets;
    av_packet_unref(packet);
  }
  if (result >= 0) result = av_write_trailer(destination);
  const auto stream_count = destination->nb_streams;
  completed = result >= 0;
  cleanup();
  if (!completed) return "{\"error\":\"DVD subtitle fixture packet mux failed\"}";
  std::ostringstream report;
  report << "{\"schema\":\"dvd-subtitle-fixture-v1\",\"status\":\"PASSED\",\"streams\":"
         << stream_count << ",\"source_packets\":" << packets
         << ",\"subtitle_events\":1}";
  return report.str();
#endif
}
}
