#include "core/videoredo_joiner.hpp"

#include <charconv>
#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::optional<std::string_view> element_document(std::string_view document,
                                                 std::string_view tag,
                                                 std::size_t start = 0) {
  const auto open = document.find("<" + std::string(tag), start);
  if (open == std::string_view::npos) return std::nullopt;
  const auto open_end = document.find('>', open);
  if (open_end == std::string_view::npos) return std::nullopt;
  const auto close_start = document.find("</" + std::string(tag) + ">", open_end + 1);
  if (close_start == std::string_view::npos) return std::nullopt;
  return document.substr(open, close_start + tag.size() + 3 - open);
}

std::optional<std::string_view> element_content(std::string_view document,
                                                std::string_view tag) {
  const auto element = element_document(document, tag);
  if (!element) return std::nullopt;
  const auto begin = element->find('>');
  const auto end = element->rfind("</");
  if (begin == std::string_view::npos || end == std::string_view::npos || begin >= end)
    return std::nullopt;
  return element->substr(begin + 1, end - begin - 1);
}

std::uint64_t unsigned_element(std::string_view document, std::string_view tag,
                               std::uint64_t fallback = 0) {
  const auto content = element_content(document, tag);
  if (!content) return fallback;
  std::uint64_t value{};
  const auto parsed = std::from_chars(content->data(), content->data() + content->size(), value);
  return parsed.ec == std::errc{} && parsed.ptr == content->data() + content->size()
             ? value
             : fallback;
}

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 Joiner 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 Joiner 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 Joiner project");
    return false;
  }
  if (replacing) std::filesystem::remove(backup, filesystem_error);
  return true;
}

}  // namespace

std::optional<VideoReDoJoinerProject> read_videoredo_joiner_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 Joiner project");
    return std::nullopt;
  }
  const std::string document((std::istreambuf_iterator<char>(input)), {});
  if (document.find("<VideoReDoJoiner") == std::string::npos) {
    set_error(error, "not a VideoReDo Joiner project document");
    return std::nullopt;
  }
  VideoReDoJoinerProject project;
  project.frame_duration_100ns = unsigned_element(document, "FrameRateDuration", 400'000);
  std::size_t cursor{};
  while (const auto item_document = element_document(document, "JoinerData", cursor)) {
    const auto item_offset = static_cast<std::size_t>(item_document->data() - document.data());
    const auto embedded = element_document(*item_document, "VideoReDoProject");
    if (!embedded) {
      set_error(error, "VideoReDo Joiner item has no embedded edit project");
      return std::nullopt;
    }
    auto edit_project = parse_videoredo_project(*embedded, error);
    if (!edit_project) return std::nullopt;
    VideoReDoJoinerItem item;
    item.project = std::move(*edit_project);
    item.type = static_cast<int>(unsigned_element(*item_document, "JoinerType", 1));
    item.original_duration_us = unsigned_element(*item_document, "OriginalDuration") / 10ULL;
    item.cut_duration_us = unsigned_element(*item_document, "CutDuration") / 10ULL;
    item.output_bytes = unsigned_element(*item_document, "OutputBytes");
    if (!item.original_duration_us) item.original_duration_us = item.project.duration_us;
    if (!item.cut_duration_us) item.cut_duration_us = item.project.duration_us;
    project.items.push_back(std::move(item));
    cursor = item_offset + item_document->size();
  }
  if (project.items.empty()) {
    set_error(error, "VideoReDo Joiner project has no items");
    return std::nullopt;
  }
  return project;
}

bool write_videoredo_joiner_project(const std::filesystem::path& path,
                                    const VideoReDoJoinerProject& project,
                                    std::string* error) {
  if (project.items.empty()) {
    set_error(error, "VideoReDo Joiner project has no items");
    return false;
  }
  std::ostringstream output;
  output << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
         << "<VideoReDoJoiner Version=\"6.000000\">\n"
         << "\t<DVDProject Type=\"bool\">0</DVDProject>\n"
         << "\t<FrameRateDuration>" << project.frame_duration_100ns << "</FrameRateDuration>\n"
         << "\t<JoinerProjects>\n";
  for (std::size_t index = 0; index < project.items.size(); ++index) {
    const auto& item = project.items[index];
    if (item.project.source_path.empty()) {
      set_error(error, "VideoReDo Joiner item has no source filename");
      return false;
    }
    const auto original_duration = item.original_duration_us ? item.original_duration_us : item.project.duration_us;
    const auto cut_duration = item.cut_duration_us ? item.cut_duration_us : item.project.duration_us;
    output << "\t\t<JoinerData Sequence=\"" << index + 1 << "\">\n"
           << serialize_videoredo_project(item.project, false)
           << "\t\t\t<JoinerType>" << item.type << "</JoinerType>\n"
           << "\t\t\t<OriginalDuration>" << original_duration * 10ULL << "</OriginalDuration>\n"
           << "\t\t\t<CutDuration>" << cut_duration * 10ULL << "</CutDuration>\n"
           << "\t\t\t<OutputBytes>" << item.output_bytes << "</OutputBytes>\n"
           << "\t\t\t<CroppingRect><top>0</top><left>0</left><bottom>0</bottom><right>0</right></CroppingRect>\n"
           << "\t\t\t<BorderRect><top>0</top><left>0</left><bottom>0</bottom><right>0</right></BorderRect>\n"
           << "\t\t</JoinerData>\n";
  }
  output << "\t</JoinerProjects>\n</VideoReDoJoiner>\n";
  return atomic_write(path, output.str(), error);
}

}  // namespace smartcut::core
