//===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "ProgressEvent.h"

#include "JSONUtils.h"
#include <optional>

using namespace lldb_vscode;
using namespace llvm;

// The minimum duration of an event for it to be reported
const std::chrono::duration<double> kStartProgressEventReportDelay =
    std::chrono::seconds(1);
// The minimum time interval between update events for reporting. If multiple
// updates fall within the same time interval, only the latest is reported.
const std::chrono::duration<double> kUpdateProgressEventReportDelay =
    std::chrono::milliseconds(250);

ProgressEvent::ProgressEvent(uint64_t progress_id,
                             std::optional<StringRef> message,
                             uint64_t completed, uint64_t total,
                             const ProgressEvent *prev_event)
    : m_progress_id(progress_id) {
  if (message)
    m_message = message->str();

  const bool calculate_percentage = total != UINT64_MAX;
  if (completed == 0) {
    // Start event
    m_event_type = progressStart;
    // Wait a bit before reporting the start event in case in completes really
    // quickly.
    m_minimum_allowed_report_time =
        m_creation_time + kStartProgressEventReportDelay;
    if (calculate_percentage)
      m_percentage = 0;
  } else if (completed == total) {
    // End event
    m_event_type = progressEnd;
    // We should report the end event right away.
    m_minimum_allowed_report_time = std::chrono::seconds::zero();
    if (calculate_percentage)
      m_percentage = 100;
  } else {
    // Update event
    m_event_type = progressUpdate;
    m_percentage = std::min(
        (uint32_t)((double)completed / (double)total * 100.0), (uint32_t)99);
    if (prev_event->Reported()) {
      // Add a small delay between reports
      m_minimum_allowed_report_time =
          prev_event->m_minimum_allowed_report_time +
          kUpdateProgressEventReportDelay;
    } else {
      // We should use the previous timestamp, as it's still pending
      m_minimum_allowed_report_time = prev_event->m_minimum_allowed_report_time;
    }
  }
}

std::optional<ProgressEvent>
ProgressEvent::Create(uint64_t progress_id, std::optional<StringRef> message,
                      uint64_t completed, uint64_t total,
                      const ProgressEvent *prev_event) {
  // If it's an update without a previous event, we abort
  if (completed > 0 && completed < total && !prev_event)
    return std::nullopt;
  ProgressEvent event(progress_id, message, completed, total, prev_event);
  // We shouldn't show unnamed start events in the IDE
  if (event.GetEventType() == progressStart && event.GetEventName().empty())
    return std::nullopt;

  if (prev_event && prev_event->EqualsForIDE(event))
    return std::nullopt;

  return event;
}

bool ProgressEvent::EqualsForIDE(const ProgressEvent &other) const {
  return m_progress_id == other.m_progress_id &&
         m_event_type == other.m_event_type &&
         m_percentage == other.m_percentage;
}

ProgressEventType ProgressEvent::GetEventType() const { return m_event_type; }

StringRef ProgressEvent::GetEventName() const {
  if (m_event_type == progressStart)
    return "progressStart";
  else if (m_event_type == progressEnd)
    return "progressEnd";
  else
    return "progressUpdate";
}

json::Value ProgressEvent::ToJSON() const {
  llvm::json::Object event(CreateEventObject(GetEventName()));
  llvm::json::Object body;

  std::string progress_id_str;
  llvm::raw_string_ostream progress_id_strm(progress_id_str);
  progress_id_strm << m_progress_id;
  progress_id_strm.flush();
  body.try_emplace("progressId", progress_id_str);

  if (m_event_type == progressStart) {
    EmplaceSafeString(body, "title", m_message);
    body.try_emplace("cancellable", false);
  }

  std::string timestamp(llvm::formatv("{0:f9}", m_creation_time.count()));
  EmplaceSafeString(body, "timestamp", timestamp);

  if (m_percentage)
    body.try_emplace("percentage", *m_percentage);

  event.try_emplace("body", std::move(body));
  return json::Value(std::move(event));
}

bool ProgressEvent::Report(ProgressEventReportCallback callback) {
  if (Reported())
    return true;
  if (std::chrono::system_clock::now().time_since_epoch() <
      m_minimum_allowed_report_time)
    return false;

  m_reported = true;
  callback(*this);
  return true;
}

bool ProgressEvent::Reported() const { return m_reported; }

ProgressEventManager::ProgressEventManager(
    const ProgressEvent &start_event,
    ProgressEventReportCallback report_callback)
    : m_start_event(start_event), m_finished(false),
      m_report_callback(report_callback) {}

bool ProgressEventManager::ReportIfNeeded() {
  // The event finished before we were able to report it.
  if (!m_start_event.Reported() && Finished())
    return true;

  if (!m_start_event.Report(m_report_callback))
    return false;

  if (m_last_update_event)
    m_last_update_event->Report(m_report_callback);
  return true;
}

const ProgressEvent &ProgressEventManager::GetMostRecentEvent() const {
  return m_last_update_event ? *m_last_update_event : m_start_event;
}

void ProgressEventManager::Update(uint64_t progress_id, uint64_t completed,
                                  uint64_t total) {
  if (std::optional<ProgressEvent> event = ProgressEvent::Create(
          progress_id, std::nullopt, completed, total, &GetMostRecentEvent())) {
    if (event->GetEventType() == progressEnd)
      m_finished = true;

    m_last_update_event = *event;
    ReportIfNeeded();
  }
}

bool ProgressEventManager::Finished() const { return m_finished; }

ProgressEventReporter::ProgressEventReporter(
    ProgressEventReportCallback report_callback)
    : m_report_callback(report_callback) {
  m_thread_should_exit = false;
  m_thread = std::thread([&] {
    while (!m_thread_should_exit) {
      std::this_thread::sleep_for(kUpdateProgressEventReportDelay);
      ReportStartEvents();
    }
  });
}

ProgressEventReporter::~ProgressEventReporter() {
  m_thread_should_exit = true;
  m_thread.join();
}

void ProgressEventReporter::ReportStartEvents() {
  std::lock_guard<std::mutex> locker(m_mutex);

  while (!m_unreported_start_events.empty()) {
    ProgressEventManagerSP event_manager = m_unreported_start_events.front();
    if (event_manager->Finished())
      m_unreported_start_events.pop();
    else if (event_manager->ReportIfNeeded())
      m_unreported_start_events
          .pop(); // we remove it from the queue as it started reporting
                  // already, the Push method will be able to continue its
                  // reports.
    else
      break; // If we couldn't report it, then the next event in the queue won't
             // be able as well, as it came later.
  }
}

void ProgressEventReporter::Push(uint64_t progress_id, const char *message,
                                 uint64_t completed, uint64_t total) {
  std::lock_guard<std::mutex> locker(m_mutex);

  auto it = m_event_managers.find(progress_id);
  if (it == m_event_managers.end()) {
    if (std::optional<ProgressEvent> event = ProgressEvent::Create(
            progress_id, StringRef(message), completed, total)) {
      ProgressEventManagerSP event_manager =
          std::make_shared<ProgressEventManager>(*event, m_report_callback);
      m_event_managers.insert({progress_id, event_manager});
      m_unreported_start_events.push(event_manager);
    }
  } else {
    it->second->Update(progress_id, completed, total);
    if (it->second->Finished())
      m_event_managers.erase(it);
  }
}
