#include "analytics.h"
#include "marshalvector.h"
#include <catch2/catch.hpp>
#include <emscripten/bind.h>
#include <iomanip>
#include <regex>
#include <sstream>

constexpr size_t ANALYTICS_RESERVED_SPACE = 256;

Uuid::Uuid(const std::string &str) {
  if (str.empty()) {
    lower = upper = 0;
    return;
  }
  std::regex uuid_pattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-"
                          "fA-F]{4}-[0-9a-fA-F]{12}$");
  if (!std::regex_match(str, uuid_pattern)) {
    throw std::invalid_argument("Invalid UUID format");
  }
  std::string uuid = str;
  uuid.erase(std::remove(uuid.begin(), uuid.end(), '-'), uuid.end());
  lower = std::stoull(uuid.substr(0, 16), nullptr, 16);
  upper = std::stoull(uuid.substr(16, 16), nullptr, 16);
}

std::string Uuid::toString() const {
  std::stringstream ss;
  ss << std::hex << std::setw(8) << std::setfill('0') << (lower >> 32);
  ss << "-";
  ss << std::hex << std::setw(4) << std::setfill('0')
     << ((lower >> 16) & 0xFFFF);
  ss << "-";
  ss << std::hex << std::setw(4) << std::setfill('0') << (lower & 0xFFFF);
  ss << "-";
  ss << std::hex << std::setw(4) << std::setfill('0') << (upper >> 48);
  ss << "-";
  ss << std::hex << std::setw(12) << std::setfill('0')
     << (upper & 0xFFFFFFFFFFFFULL);

  return ss.str();
}

AnalyticsObserver::AnalyticsObserver()
    : currentRenderCycleIndex(0),
      inactiveSpans(new std::vector<PlaybackSpan>()) {
  auto inactiveSpansGuard = WaitFreeGuard(inactiveSpans);
  inactiveSpansGuard->reserve(ANALYTICS_RESERVED_SPACE);
}

AnalyticsObserver::~AnalyticsObserver() {
  const auto swapRes = inactiveSpans.swap(nullptr);
  assert(swapRes.second);
  delete swapRes.first;
}

void AnalyticsObserver::trackPlayback(const Uuid &arrangementId,
                                      time_units::Seconds<double> position) {
  auto it = activeSpans.find(arrangementId);
  if (it == activeSpans.end()) {
    activeSpans.emplace(arrangementId, PlaybackSpan(arrangementId, position,
                                                    currentRenderCycleIndex));
    return;
  }
  if (position > it->second.begin) {
    it->second.end = position;
  }
  it->second.renderCycleIndex = currentRenderCycleIndex;
}

void AnalyticsObserver::trackLooped(const Uuid &arrangementId) {
  auto it = activeSpans.find(arrangementId);
  if (it == activeSpans.end()) {
    return;
  }
  auto inactiveSpansGuard = WaitFreeGuard(inactiveSpans);
  if (inactiveSpansGuard->size() < ANALYTICS_RESERVED_SPACE) {
    inactiveSpansGuard->push_back(it->second);
  }
  activeSpans.erase(it);
}

void AnalyticsObserver::trackRenderCycleEnd() {
  auto inactiveSpansGuard = WaitFreeGuard(inactiveSpans);
  auto it = activeSpans.begin();
  while (it != activeSpans.end()) {
    if (it->second.renderCycleIndex != currentRenderCycleIndex) {
      if (inactiveSpansGuard->size() < ANALYTICS_RESERVED_SPACE) {
        inactiveSpansGuard->push_back(it->second);
      }
      it = activeSpans.erase(it);
    } else {
      ++it;
    }
  }
  currentRenderCycleIndex++;
}

std::vector<PlaybackSpan> AnalyticsObserver::flushSpans() {
  const auto newInactiveSpans = new std::vector<PlaybackSpan>();
  newInactiveSpans->reserve(ANALYTICS_RESERVED_SPACE);
  const auto swapRes = inactiveSpans.swap(newInactiveSpans);
  if (swapRes.second) {
    std::vector<PlaybackSpan> result(std::move(*swapRes.first));
    delete swapRes.first;
    return result;
  }
  delete newInactiveSpans;
  return {};
}

PlaybackSpanList AnalyticsObserver::flushSpansJs() {
  const auto spans = flushSpans();
  emscripten::val result = emscripten::val::array();
  for (const auto &span : spans) {
    result.call<void>("push", span);
  }
  return result.as<PlaybackSpanList>();
}

EMSCRIPTEN_BINDINGS(analytics) {
  using namespace emscripten;

  value_object<PlaybackSpan>("PlaybackSpan")
      .field("arrangementId", &PlaybackSpan::getArrangementIdString,
             &PlaybackSpan::setArrangementIdString)
      .field("begin", &PlaybackSpan::getBeginSeconds,
             &PlaybackSpan::setBeginSeconds)
      .field("end", &PlaybackSpan::getEndSeconds, &PlaybackSpan::setEndSeconds);

  register_type<PlaybackSpanList>("PlaybackSpan[]");

  class_<AnalyticsObserver>("AnalyticsObserver")
      .smart_ptr_constructor<std::shared_ptr<AnalyticsObserver>>(
          "AnalyticsObserver", &std::make_shared<AnalyticsObserver>)
      .function("flushSpans", &AnalyticsObserver::flushSpansJs);
}

TEST_CASE("uuid", "[analytics]") {
  REQUIRE(Uuid("") == Uuid());
  REQUIRE(Uuid("00000000-0000-0000-0000-000000000000") == Uuid());
  const std::string uuid = "cb1aa6e5-ee7b-4611-9d8b-8031eb3913a0";
  REQUIRE(Uuid(uuid).toString() == uuid);
}

TEST_CASE("analytics", "[analytics]") {
  AnalyticsObserver observer;
  observer.trackPlayback(Uuid("cb1aa6e5-ee7b-4611-9d8b-8031eb3913a0"),
                         time_units::Seconds<double>(1.0));
  observer.trackRenderCycleEnd();
  observer.trackRenderCycleEnd();
  const auto spans = observer.flushSpans();
  REQUIRE(spans.size() == 1);
  REQUIRE(spans[0].getArrangementIdString() ==
          "cb1aa6e5-ee7b-4611-9d8b-8031eb3913a0");
  REQUIRE(spans[0].getBeginSeconds() == 1.0);
  REQUIRE(spans[0].getEndSeconds() == 1.0);
}