// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause

#include "vtkOpenGLRenderTimerLog.h"

#include "vtkObjectFactory.h"
#include "vtkOpenGLRenderTimer.h"

#include <algorithm>
#include <cassert>
#include <utility>

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkOpenGLRenderTimerLog);

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::PrintSelf(std::ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);

  os << indent << "CurrentFrame: " << this->CurrentFrame.ChildCount << " events logged\n"
     << indent << "PendingFrames: " << this->PendingFrames.size() << " frames\n"
     << indent << "ReadyFrames: " << this->ReadyFrames.size() << " frames\n"
     << indent << "TimerPool: " << this->TimerPool.size() << " free timers\n";
}

//------------------------------------------------------------------------------
bool vtkOpenGLRenderTimerLog::IsSupported() VTK_FUTURE_CONST
{
  return vtkOpenGLRenderTimer::IsSupported();
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::MarkFrame()
{
  if (!this->DoLogging())
  {
    return;
  }

  // Do nothing if no events on current frame:
  if (this->CurrentFrame.ChildCount == 0)
  {
    return;
  }

  // Stop any running timers (otherwise the PendingQueue will get clogged, since
  // such timers would never be ready)
  this->ForceCloseFrame(this->CurrentFrame);

  this->PendingFrames.push_back(this->CurrentFrame);
  this->CurrentFrame.ChildCount = 0;
  this->CurrentFrame.Events.clear();
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::MarkStartEvent(const std::string& name)
{
  if (!this->DoLogging())
  {
    return;
  }

  OGLEvent& event = this->NewEvent();
  event.Name = name;
  event.Timer = this->NewTimer();
  event.Timer->Start();
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::MarkEndEvent()
{
  if (!this->DoLogging())
  {
    return;
  }

  OGLEvent* event = this->DeepestOpenEvent();
  if (event)
  {
    event->Timer->Stop();
  }
  else
  {
    vtkWarningMacro("No open events to end.");
  }
}

//------------------------------------------------------------------------------
bool vtkOpenGLRenderTimerLog::FrameReady()
{
  if (!this->DoLogging())
  {
    return false;
  }

  this->CheckPendingFrames();
  return !this->ReadyFrames.empty();
}

//------------------------------------------------------------------------------
vtkRenderTimerLog::Frame vtkOpenGLRenderTimerLog::PopFirstReadyFrame()
{
  if (!this->DoLogging() || this->ReadyFrames.empty())
  {
    return Frame();
  }

  Frame frame = this->ReadyFrames.front();
  this->ReadyFrames.pop();
  return frame;
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ReleaseGraphicsResources()
{
  this->ReleaseOGLFrame(this->CurrentFrame);
  this->CurrentFrame.ChildCount = 0;
  this->CurrentFrame.Events.clear();
  while (!this->PendingFrames.empty())
  {
    this->ReleaseOGLFrame(this->PendingFrames.front());
    this->PendingFrames.pop_front();
  }
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimerLog::vtkOpenGLRenderTimerLog()
  : MinTimerPoolSize(32)
{
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimerLog::~vtkOpenGLRenderTimerLog()
{
  this->ReleaseGraphicsResources();
  while (!this->TimerPool.empty())
  {
    delete this->TimerPool.front();
    this->TimerPool.pop();
  }
}

//------------------------------------------------------------------------------
bool vtkOpenGLRenderTimerLog::DoLogging() VTK_FUTURE_CONST
{
  if (!this->LoggingEnabled)
  {
    return false;
  }

  if (this->IsSupported())
  {
    return true;
  }

  // Disable logging if not supported.
  this->LoggingEnabled = false;
  return false;
}

//------------------------------------------------------------------------------
vtkRenderTimerLog::Frame vtkOpenGLRenderTimerLog::Convert(const OGLFrame& oglFrame)
{
  Frame frame;
  frame.Events.reserve(oglFrame.Events.size());
  for (const auto& event : oglFrame.Events)
  {
    frame.Events.push_back(this->Convert(event));
  }

  return frame;
}

//------------------------------------------------------------------------------
vtkRenderTimerLog::Event vtkOpenGLRenderTimerLog::Convert(const OGLEvent& oglEvent)
{
  Event event;
  event.Name = oglEvent.Name;
  event.StartTime = oglEvent.Timer->GetStartTime();
  event.EndTime = oglEvent.Timer->GetStopTime();

  event.Events.reserve(oglEvent.Events.size());
  for (const auto& subOGLEvent : oglEvent.Events)
  {
    event.Events.push_back(this->Convert(subOGLEvent));
  }

  return event;
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimerLog::OGLEvent& vtkOpenGLRenderTimerLog::NewEvent()
{
  ++this->CurrentFrame.ChildCount;
  OGLEvent* openEvent = this->DeepestOpenEvent();

  if (openEvent)
  {
    openEvent->Events.emplace_back();
    return openEvent->Events.back();
  }
  else
  {
    this->CurrentFrame.Events.emplace_back();
    return this->CurrentFrame.Events.back();
  }
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimerLog::OGLEvent* vtkOpenGLRenderTimerLog::DeepestOpenEvent()
{
  OGLEvent* openEvent = nullptr;
  if (!this->CurrentFrame.Events.empty())
  {
    OGLEvent& lastTopEvent = this->CurrentFrame.Events.back();
    if (!lastTopEvent.Timer->Stopped())
    {
      openEvent = &this->WalkOpenEvents(lastTopEvent);
    }
  }
  return openEvent;
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimerLog::OGLEvent& vtkOpenGLRenderTimerLog::WalkOpenEvents(OGLEvent& event)
{
  assert(event.Timer->Started() && !event.Timer->Stopped());
  if (event.Events.empty())
  {
    return event;
  }

  OGLEvent& lastChild = event.Events.back();
  if (lastChild.Timer->Stopped())
  {
    return event;
  }

  return this->WalkOpenEvents(lastChild);
}

//------------------------------------------------------------------------------
vtkOpenGLRenderTimer* vtkOpenGLRenderTimerLog::NewTimer()
{
  if (this->TimerPool.empty())
  {
    return new vtkOpenGLRenderTimer();
  }
  else
  {
    vtkOpenGLRenderTimer* result = this->TimerPool.front();
    this->TimerPool.pop();
    return result;
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ReleaseTimer(vtkOpenGLRenderTimer* timer)
{
  timer->Reset();
  this->TimerPool.push(timer);
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ReleaseOGLFrame(OGLFrame& frame)
{
  for (auto event : frame.Events)
  {
    this->ReleaseOGLEvent(event);
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ReleaseOGLEvent(OGLEvent& event)
{
  this->ReleaseTimer(event.Timer);
  event.Timer = nullptr;
  for (auto subEvent : event.Events)
  {
    this->ReleaseOGLEvent(subEvent);
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::TrimTimerPool()
{
  // Try not to keep too many timers around in the pool. If there are 2x as
  // many as we currently need, free the extras.
  size_t needed = this->CurrentFrame.ChildCount;
  for (const auto& frame : this->PendingFrames)
  {
    needed += frame.ChildCount;
  }

  needed = std::max(needed * 2, this->MinTimerPoolSize);

  while (this->TimerPool.size() > needed)
  {
    delete this->TimerPool.front();
    this->TimerPool.pop();
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::CheckPendingFrames()
{
  while (!this->PendingFrames.empty())
  {
    OGLFrame& frame = this->PendingFrames.front();
    if (this->IsFrameReady(frame))
    {
      this->ReadyFrames.push(this->Convert(frame));
      this->ReleaseOGLFrame(frame);
      this->PendingFrames.pop_front();
    }
    else
    { // If this frame isn't done, later frames won't be either.
      break;
    }
  }

  while (this->FrameLimit > 0 &&
    this->PendingFrames.size() + this->ReadyFrames.size() > this->FrameLimit)
  {
    if (!this->ReadyFrames.empty())
    {
      this->ReadyFrames.pop();
    }
    else if (!this->PendingFrames.empty())
    {
      this->ReleaseOGLFrame(this->PendingFrames.front());
      this->PendingFrames.pop_front();
    }
    else
    { // Shouldn't happen, but eh. Cheap insurance.
      break;
    }
  }
}

//------------------------------------------------------------------------------
bool vtkOpenGLRenderTimerLog::IsFrameReady(OGLFrame& frame)
{
  for (auto event : frame.Events)
  {
    if (!this->IsEventReady(event))
    {
      return false;
    }
  }
  return true;
}

//------------------------------------------------------------------------------
bool vtkOpenGLRenderTimerLog::IsEventReady(OGLEvent& event)
{
  if (!event.Timer->Ready())
  {
    return false;
  }

  for (auto subEvent : event.Events)
  {
    if (!this->IsEventReady(subEvent))
    {
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ForceCloseFrame(OGLFrame& frame)
{
  for (auto event : frame.Events)
  {
    this->ForceCloseEvent(event);
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLRenderTimerLog::ForceCloseEvent(OGLEvent& event)
{
  if (!event.Timer->Started())
  {
    vtkWarningMacro("Timer for event '" << event.Name
                                        << "' was never started? "
                                           "This is an internal error. Timing results will be "
                                           "unreliable.");

    // If this somehow happens, start the timer so it will not clog the pending
    // queue:
    event.Timer->Start();
  }

  if (!event.Timer->Stopped())
  {
    vtkWarningMacro("Timer for event '" << event.Name
                                        << "' was never stopped. "
                                           "Ensure that all events have an end mark (the issue may "
                                           "be with a different event). Timing results will be "
                                           "unreliable.");
    event.Timer->Stop();
  }

  for (auto subEvent : event.Events)
  {
    this->ForceCloseEvent(subEvent);
  }
}
VTK_ABI_NAMESPACE_END
