#include "core/videoredo_batch.hpp"

#include <algorithm>
#include <charconv>
#include <cctype>
#include <fstream>
#include <sstream>
#include <string_view>

namespace smartcut::core {
namespace {

void set_error(std::string* error, std::string value) {
  if (error) *error = std::move(value);
}

std::string xml_decode(std::string value) {
  const std::pair<std::string_view, std::string_view> entities[] = {
      {"&amp;", "&"}, {"&lt;", "<"}, {"&gt;", ">"},
      {"&quot;", "\""}, {"&apos;", "'"}};
  for (const auto& [encoded, decoded] : entities) {
    std::size_t cursor{};
    while ((cursor = value.find(encoded, cursor)) != std::string::npos) {
      value.replace(cursor, encoded.size(), decoded);
      cursor += decoded.size();
    }
  }
  return value;
}

std::string xml_encode(std::string_view value) {
  std::string output;
  for (const char character : value) {
    switch (character) {
      case '&': output += "&amp;"; break;
      case '<': output += "&lt;"; break;
      case '>': output += "&gt;"; break;
      case '\"': output += "&quot;"; break;
      case '\'': output += "&apos;"; break;
      default: output.push_back(character); break;
    }
  }
  return output;
}

std::optional<std::string> element_text(std::string_view document,
                                        std::string_view tag) {
  const auto open_start = document.find("<" + std::string(tag));
  if (open_start == std::string_view::npos) return std::nullopt;
  const auto open_end = document.find('>', open_start);
  if (open_end == std::string_view::npos) return std::nullopt;
  if (open_end > open_start && document[open_end - 1] == '/') return std::string{};
  const auto close = document.find("</" + std::string(tag) + ">", open_end + 1);
  if (close == std::string_view::npos) return std::nullopt;
  return xml_decode(std::string(document.substr(open_end + 1, close - open_end - 1)));
}

std::optional<std::string_view> element_document(std::string_view document,
                                                 std::string_view tag) {
  const auto open = document.find("<" + std::string(tag));
  if (open == std::string_view::npos) return std::nullopt;
  const auto close_start = document.find("</" + std::string(tag) + ">", open);
  if (close_start == std::string_view::npos) return std::nullopt;
  return document.substr(open, close_start + tag.size() + 3 - open);
}

int integer_element(std::string_view document, std::string_view tag, int fallback = 0) {
  const auto text = element_text(document, tag);
  if (!text) return fallback;
  int value{};
  const auto result = std::from_chars(text->data(), text->data() + text->size(), value);
  return result.ec == std::errc{} && result.ptr == text->data() + text->size() ? value : fallback;
}

bool boolean_element(std::string_view document, std::string_view tag, bool fallback = false) {
  const auto text = element_text(document, tag);
  if (!text) return fallback;
  std::string lower = *text;
  std::transform(lower.begin(), lower.end(), lower.begin(),
                 [](unsigned char value) { return static_cast<char>(std::tolower(value)); });
  return lower == "true" || lower == "1" || lower == "yes";
}

std::string attribute(std::string_view element, std::string_view name) {
  const auto marker = std::string(name) + "=\"";
  const auto start = element.find(marker);
  if (start == std::string_view::npos) return {};
  const auto begin = start + marker.size();
  const auto end = element.find('"', begin);
  return end == std::string_view::npos ? std::string{} : std::string(element.substr(begin, end - begin));
}

bool atomic_write(const std::filesystem::path& path, std::string_view document,
                  std::string* error) {
  const auto temporary = path.string() + ".tmp";
  std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
  output.write(document.data(), static_cast<std::streamsize>(document.size()));
  output.close();
  if (!output) {
    std::filesystem::remove(temporary);
    set_error(error, "could not write VideoReDo batch project");
    return false;
  }
  std::error_code filesystem_error;
  const auto backup = path.string() + ".bak";
  std::filesystem::remove(backup, filesystem_error);
  filesystem_error.clear();
  const bool replacing = std::filesystem::exists(path);
  if (replacing) {
    std::filesystem::rename(path, backup, filesystem_error);
    if (filesystem_error) {
      std::filesystem::remove(temporary);
      set_error(error, "could not preserve existing VideoReDo batch project");
      return false;
    }
  }
  std::filesystem::rename(temporary, path, filesystem_error);
  if (filesystem_error) {
    std::filesystem::remove(temporary);
    if (replacing) {
      std::error_code ignored;
      std::filesystem::rename(backup, path, ignored);
    }
    set_error(error, "could not finalize VideoReDo batch project");
    return false;
  }
  if (replacing) std::filesystem::remove(backup, filesystem_error);
  return true;
}

}  // namespace

std::optional<VideoReDoBatchProject> read_videoredo_batch_project(
    const std::filesystem::path& path, std::string* error) {
  std::ifstream input(path, std::ios::binary);
  if (!input) {
    set_error(error, "could not open VideoReDo batch project");
    return std::nullopt;
  }
  const std::string document((std::istreambuf_iterator<char>(input)), {});
  if (document.find("<VideoReDoBatch") == std::string::npos) {
    set_error(error, "not a VideoReDo batch project document");
    return std::nullopt;
  }
  const auto embedded = element_document(document, "VideoReDoProject");
  if (!embedded) {
    set_error(error, "VideoReDo batch project has no embedded edit project");
    return std::nullopt;
  }
  auto project = parse_videoredo_project(*embedded, error);
  if (!project) return std::nullopt;
  VideoReDoBatchProject batch;
  batch.project = std::move(*project);
  batch.status = element_text(document, "Status").value_or("New");
  batch.type = element_text(document, "Type").value_or("File");
  batch.output_file = element_text(document, "OutputFile").value_or("");
  batch.quick_stream_fix = boolean_element(document, "QuickStreamFix");
  if (const auto dimensions = element_document(document, "DimensionFilter")) {
    batch.dimension_width = integer_element(*dimensions, "Width");
    batch.dimension_height = integer_element(*dimensions, "Height");
  }
  if (const auto profile = element_document(document, "VRDOutputProfile")) {
    const auto id = attribute(*profile, "id");
    if (!id.empty()) {
      const auto parsed = std::from_chars(id.data(), id.data() + id.size(), batch.profile.id);
      if (parsed.ec != std::errc{}) batch.profile.id = 8;
    }
    batch.profile.name = element_text(*profile, "Name").value_or(batch.profile.name);
    batch.profile.output_mode = element_text(*profile, "OutputMode").value_or(batch.profile.output_mode);
    batch.profile.file_type = element_text(*profile, "FileType").value_or(batch.profile.file_type);
    if (const auto video = element_document(*profile, "VideoAttributes")) {
      batch.profile.video_encoder = element_text(*video, "Encoder").value_or(batch.profile.video_encoder);
      batch.profile.width = integer_element(*video, "Width");
      batch.profile.height = integer_element(*video, "Height");
      batch.profile.bitrate_kbps = integer_element(*video, "EncodeBitrate");
      batch.profile.max_bitrate_kbps = integer_element(*video, "MaxBitrate");
      batch.profile.deinterlace = element_text(*video, "DeinterlaceMethod").value_or(batch.profile.deinterlace);
      batch.profile.frame_rate = element_text(*video, "FrameRate").value_or(batch.profile.frame_rate);
    }
    if (const auto audio = element_document(*profile, "AudioAttributes")) {
      batch.profile.audio_encoder = element_text(*audio, "Encoder").value_or(batch.profile.audio_encoder);
      batch.profile.audio_bitrate_kbps = integer_element(*audio, "Bitrate");
      batch.profile.audio_sample_rate = element_text(*audio, "SampleRate").value_or(batch.profile.audio_sample_rate);
      batch.profile.audio_channels = element_text(*audio, "ChannelMode").value_or(batch.profile.audio_channels);
    }
  }
  if (batch.output_file.empty()) {
    set_error(error, "VideoReDo batch project has no output filename");
    return std::nullopt;
  }
  return batch;
}

bool write_videoredo_batch_project(const std::filesystem::path& path,
                                   const VideoReDoBatchProject& batch,
                                   std::string* error) {
  if (batch.output_file.empty() || batch.project.source_path.empty()) {
    set_error(error, "VideoReDo batch project requires source and output filenames");
    return false;
  }
  std::ostringstream output;
  output << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
         << "<VideoReDoBatch version=\"6.000000\">\n"
         << "\t<Status>" << xml_encode(batch.status) << "</Status>\n"
         << "\t<Type>" << xml_encode(batch.type) << "</Type>\n"
         << "\t<OutputFile>" << xml_encode(batch.output_file) << "</OutputFile>\n"
         << "\t<Options>\n\t\t<QuickStreamFix>" << (batch.quick_stream_fix ? "True" : "False")
         << "</QuickStreamFix>\n\t\t<DimensionFilter>\n\t\t\t<Width>" << batch.dimension_width
         << "</Width>\n\t\t\t<Height>" << batch.dimension_height
         << "</Height>\n\t\t</DimensionFilter>\n\t</Options>\n\t<Project>\n"
         << serialize_videoredo_project(batch.project, false)
         << "\t</Project>\n\t<OutputProfile>\n\t\t<VRDOutputProfile id=\"" << batch.profile.id
         << "\"><Name>" << xml_encode(batch.profile.name) << "</Name><OutputMode>"
         << xml_encode(batch.profile.output_mode) << "</OutputMode><RecodeMode>Quality</RecodeMode><FileType>"
         << xml_encode(batch.profile.file_type)
         << "</FileType><FileExt/><OutputLibrary><Program>None</Program><CustomPath/></OutputLibrary><DefaultOutputDir/>"
         << "<VideoAttributes><Encoder>" << xml_encode(batch.profile.video_encoder)
         << "</Encoder><AspectRatio>No Change</AspectRatio><AspectCorrection>None</AspectCorrection><EncodeDimensions><Width>"
         << batch.profile.width << "</Width><Height>" << batch.profile.height
         << "</Height></EncodeDimensions><EncodeBitrate>" << batch.profile.bitrate_kbps
         << "</EncodeBitrate><HeaderBitrate>0</HeaderBitrate><MaxBitrate>" << batch.profile.max_bitrate_kbps
         << "</MaxBitrate><EncodingMode>SP</EncodingMode><DeinterlaceMethod>" << xml_encode(batch.profile.deinterlace)
         << "</DeinterlaceMethod><FrameRate>" << xml_encode(batch.profile.frame_rate)
         << "</FrameRate></VideoAttributes><AudioAttributes><Encoder>" << xml_encode(batch.profile.audio_encoder)
         << "</Encoder><Bitrate>" << batch.profile.audio_bitrate_kbps << "</Bitrate><SampleRate>"
         << xml_encode(batch.profile.audio_sample_rate) << "</SampleRate><ChannelMode>"
         << xml_encode(batch.profile.audio_channels)
         << "</ChannelMode><LPCMEndianess>Little</LPCMEndianess></AudioAttributes></VRDOutputProfile>\n"
         << "\t</OutputProfile>\n</VideoReDoBatch>\n";
  return atomic_write(path, output.str(), error);
}

}  // namespace smartcut::core
