#include "media/s337m_bridge.hpp"

#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iterator>

namespace smartcut::media {
namespace {

constexpr std::uint8_t kPreamble16Le[]{0x72, 0xf8, 0x1f, 0x4e};
constexpr std::size_t kHeaderBytes = 8;

bool preamble_at(const std::vector<std::uint8_t>& bytes, std::size_t offset) {
  return offset + sizeof(kPreamble16Le) <= bytes.size() &&
         std::equal(std::begin(kPreamble16Le), std::end(kPreamble16Le), bytes.begin() + offset);
}

bool dolby_e_payload_at(const std::vector<std::uint8_t>& bytes, std::size_t offset) {
  if (offset + kHeaderBytes + 4 > bytes.size() || !preamble_at(bytes, offset)) return false;
  if ((bytes[offset + 4] & 0x1fU) != 0x1cU) return false;
  const auto first = static_cast<unsigned>(bytes[offset + 9]) << 16U |
                     static_cast<unsigned>(bytes[offset + 8]) << 8U |
                     static_cast<unsigned>(bytes[offset + 11]);
  return (first & 0xfffe00U) == 0x078e00U;
}

void set_reason(std::string* reason, const std::string& value) {
  if (reason) *reason = value;
}

}  // namespace

bool is_s337m_path(const std::string& path) {
  auto extension = std::filesystem::path(path).extension().string();
  std::transform(extension.begin(), extension.end(), extension.begin(),
                 [](unsigned char value) { return static_cast<char>(std::tolower(value)); });
  return extension == ".s337m" || extension == ".dolbye";
}

bool parse_s337m_16le(const std::string& path, std::vector<S337mFrame>* frames,
                      std::string* reason) {
  if (!frames) {
    set_reason(reason, "frame destination is missing");
    return false;
  }
  frames->clear();
  std::ifstream input(path, std::ios::binary);
  if (!input) {
    set_reason(reason, "carrier input open failed");
    return false;
  }
  const std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(input)),
                                        std::istreambuf_iterator<char>());
  if (bytes.size() < kHeaderBytes || !dolby_e_payload_at(bytes, 0)) {
    set_reason(reason, "only 16-bit little-endian SMPTE 337M Dolby E carriers are supported");
    return false;
  }
  std::vector<std::size_t> starts{0};
  for (std::size_t offset = kHeaderBytes; offset + kHeaderBytes <= bytes.size(); ++offset) {
    if (dolby_e_payload_at(bytes, offset)) {
      starts.push_back(offset);
      offset += kHeaderBytes - 1;
    }
  }
  starts.push_back(bytes.size());
  for (std::size_t index = 0; index + 1 < starts.size(); ++index) {
    const auto begin = starts[index];
    const auto end = starts[index + 1];
    if (end <= begin + kHeaderBytes || ((end - begin - kHeaderBytes) & 1U) != 0) {
      frames->clear();
      set_reason(reason, "invalid or truncated SMPTE 337M burst");
      return false;
    }
    S337mFrame frame;
    frame.carrier_header.assign(bytes.begin() + begin, bytes.begin() + begin + kHeaderBytes);
    frame.decoded_payload.reserve(end - begin - kHeaderBytes);
    for (auto offset = begin + kHeaderBytes; offset + 1 < end; offset += 2) {
      frame.decoded_payload.push_back(bytes[offset + 1]);
      frame.decoded_payload.push_back(bytes[offset]);
    }
    frames->push_back(std::move(frame));
  }
  if (frames->empty()) {
    set_reason(reason, "no Dolby E bursts found");
    return false;
  }
  return true;
}

bool write_s337m_16le(const std::vector<S337mFrame>& frames, const std::string& path,
                      std::string* reason) {
  if (frames.empty()) {
    set_reason(reason, "no Dolby E bursts to write");
    return false;
  }
  std::ofstream output(path, std::ios::binary | std::ios::trunc);
  if (!output) {
    set_reason(reason, "carrier output open failed");
    return false;
  }
  for (const auto& frame : frames) {
    if (frame.carrier_header.size() != kHeaderBytes ||
        (frame.decoded_payload.size() & 1U) != 0) {
      set_reason(reason, "invalid Dolby E bridge frame");
      return false;
    }
    output.write(reinterpret_cast<const char*>(frame.carrier_header.data()),
                 static_cast<std::streamsize>(frame.carrier_header.size()));
    for (std::size_t offset = 0; offset < frame.decoded_payload.size(); offset += 2) {
      output.put(static_cast<char>(frame.decoded_payload[offset + 1]));
      output.put(static_cast<char>(frame.decoded_payload[offset]));
    }
  }
  output.close();
  if (!output) {
    set_reason(reason, "carrier output write failed");
    return false;
  }
  return true;
}

std::uint64_t s337m_payload_hash(const std::vector<S337mFrame>& frames) {
  std::uint64_t hash = 14695981039346656037ULL;
  for (const auto& frame : frames) {
    for (const auto byte : frame.decoded_payload) {
      hash ^= byte;
      hash *= 1099511628211ULL;
    }
  }
  return hash;
}

std::size_t s337m_payload_bytes(const std::vector<S337mFrame>& frames) {
  std::size_t bytes = 0;
  for (const auto& frame : frames) bytes += frame.decoded_payload.size();
  return bytes;
}

}  // namespace smartcut::media
