/*=========================================================================

  Program:   ParaView
  Module:    vtkSMViewProxy.cxx

  Copyright (c) Kitware, Inc.
  All rights reserved.
  See Copyright.txt or http://www.paraview.org/HTML/Copyright.html for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkSMViewProxy.h"

#include "vtkCallbackCommand.h"
#include "vtkCamera.h"
#include "vtkChannelSubscription.h"
#include "vtkClientSession.h"
#include "vtkFFmpegSoftwareDecoder.h"
#include "vtkGenericOpenGLRenderWindow.h"
#include "vtkImageData.h"
#include "vtkJPEGVideoDecoder.h"
#include "vtkLogger.h"
#include "vtkObjectFactory.h"
#include "vtkOpenGLState.h"
#include "vtkPVCoreApplication.h"
#include "vtkPVCoreApplicationOptions.h"
#include "vtkPVRenderingCapabilitiesInformation.h"
#include "vtkPVXMLElement.h"
#include "vtkPacket.h"
#include "vtkPointData.h"
#include "vtkRemoteObjectProviderViews.h"
#include "vtkRemotingCoreUtilities.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkSMIntVectorProperty.h"
#include "vtkSMParaViewPipelineControllerWithRendering.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMProxyIterator.h"
#include "vtkSMProxyProperty.h"
#include "vtkSMSessionProxyManager.h"
#include "vtkSMSourceProxy.h"
#include "vtkSMUncheckedPropertyHelper.h"
#include "vtkSMViewProxyInternals.h"
#include "vtkServiceEndpoint.h"
#include "vtkSmartPointer.h"
#include "vtkSynchronizedRenderers.h"
#include "vtkTextActor.h"
#include "vtkTextProperty.h"
#include "vtkTextureObject.h"
#include "vtkTimerLog.h"
#include "vtkVariant.h"
#include "vtkVideoCodecTypes.h"

#include "vtk_glew.h"

#include <string>
#include <vtk_fmt.h> // needed for `fmt`
// clang-format off
#include VTK_FMT(fmt/core.h)
// clang-format on

#include <cstdint>
#include <sstream>

namespace vtkSMViewProxyNS
{
const char* GetRepresentationNameFromHints(const char* viewType, vtkPVXMLElement* hints, int port)
{
  if (!hints)
  {
    return nullptr;
  }

  for (unsigned int cc = 0, max = hints->GetNumberOfNestedElements(); cc < max; ++cc)
  {
    vtkPVXMLElement* child = hints->GetNestedElement(cc);
    if (child == nullptr || child->GetName() == nullptr)
    {
      continue;
    }

    // LEGACY: support DefaultRepresentations hint.
    // <Hints>
    //    <DefaultRepresentations representation="Foo" />
    // </Hints>
    if (strcmp(child->GetName(), "DefaultRepresentations") == 0)
    {
      return child->GetAttribute("representation");
    }

    // <Hints>
    //    <Representation port="outputPort" view="ViewName" type="ReprName" />
    // </Hints>
    else if (strcmp(child->GetName(), "Representation") == 0 &&
      // has an attribute "view" that matches the viewType.
      child->GetAttribute("view") && strcmp(child->GetAttribute("view"), viewType) == 0 &&
      child->GetAttribute("type") != nullptr)
    {
      // if port is present, it must match "port".
      int xmlPort;
      if (child->GetScalarAttribute("port", &xmlPort) == 0 || xmlPort == port)
      {
        return child->GetAttribute("type");
      }
    }
  }
  return nullptr;
}

} // namespace vtkSMViewProxyNS

namespace
{
inline std::string getNiceTime(vtkIdType time_ns)
{
  if (time_ns > 1e9)
  {
    return fmt::format("{:>3}s", vtkIdType(time_ns / 1e9));
  }
  else if (time_ns > 1e6)
  {
    return fmt::format("{:>3}ms", vtkIdType(time_ns / 1e6));
  }
  else if (time_ns > 1e3)
  {
    return fmt::format("{:>3}us", vtkIdType(time_ns / 1e3));
  }
  else
  {
    return fmt::format("{:>3}ns", time_ns);
  }
}

inline std::string getNiceBytes(int num_bytes)
{
  if (num_bytes > 1024 * 1024 * 1024)
  {
    return fmt::format("{:>3}GB", int(num_bytes / (1024 * 1024 * 1024)));
  }
  else if (num_bytes > 1024 * 1024)
  {
    return fmt::format("{:>3}MB", int(num_bytes / (1024 * 1024)));
  }
  else if (num_bytes > 1024)
  {
    return fmt::format("{:>3}KB", int(num_bytes / 1024));
  }
  else
  {
    return fmt::format("{:>3}By", num_bytes);
  }
}
}

vtkStandardNewMacro(vtkSMViewProxy);

bool vtkSMViewProxy::UseGenericOpenGLRenderWindow = false;

//----------------------------------------------------------------------------
vtkSMViewProxy::vtkSMViewProxy()
  : Internals(new vtkSMViewProxyInternals())
{
  auto& internals = (*this->Internals);

  auto* options = vtkPVCoreApplication::GetInstance()->GetOptions();
  if (vtkSMViewProxy::GetUseGenericOpenGLRenderWindow())
  {
    vtkLogF(TRACE, "Using generic opengl render window.");
    internals.RenderWindow = vtk::TakeSmartPointer(vtkGenericOpenGLRenderWindow::New());
  }
  else if (options->GetForceOffscreenRendering())
  {
    vtkLogF(TRACE, "Using offscreen opengl render window.");
    internals.RenderWindow = vtkPVRenderingCapabilitiesInformation::NewOffscreenRenderWindow();
  }
  else
  {
    vtkLogF(TRACE, "Using opengl render window factory override.");
    internals.RenderWindow = vtk::TakeSmartPointer(vtkRenderWindow::New());
  }

  internals.Renderer = vtk::TakeSmartPointer(vtkRenderer::New());
  internals.Renderer->SetBackground(0, 0, 0);
  internals.Renderer->EraseOff(); // avoids frame blanking.
  internals.RenderWindow->AddRenderer(internals.Renderer);
  internals.RenderWindow->SetNumberOfLayers(2);

  internals.Renderer2DForKeys = vtk::TakeSmartPointer(vtkRenderer::New());
  internals.Renderer2DForKeys->SetLayer(1);
  internals.Renderer2DForKeys->InteractiveOff();
  internals.RenderWindow->AddRenderer(internals.Renderer2DForKeys);

  internals.Renderer2DForValues = vtk::TakeSmartPointer(vtkRenderer::New());
  internals.Renderer2DForValues->SetLayer(1);
  internals.Renderer2DForValues->InteractiveOff();
  internals.RenderWindow->AddRenderer(internals.Renderer2DForValues);

  // Setup Corner annotation for rendering/encoding status.
  internals.AnnotationKeys = vtk::TakeSmartPointer(vtkTextRepresentation::New());
  internals.Renderer2DForKeys->AddActor(internals.AnnotationKeys);
  internals.AnnotationKeys->SetText("Key");
  internals.AnnotationKeys->SetRenderer(internals.Renderer2DForKeys);
  internals.AnnotationKeys->BuildRepresentation();
  internals.AnnotationKeys->SetWindowLocation(vtkTextRepresentation::UpperLeftCorner);
  internals.AnnotationKeys->GetTextActor()->SetTextScaleModeToNone();
  internals.AnnotationKeys->GetTextActor()->GetTextProperty()->SetJustificationToLeft();
  internals.AnnotationKeys->SetVisibility(false);

  internals.AnnotationValues = vtk::TakeSmartPointer(vtkTextRepresentation::New());
  internals.Renderer2DForValues->AddActor(internals.AnnotationValues);
  internals.AnnotationValues->SetText("Value");
  internals.AnnotationValues->SetRenderer(internals.Renderer2DForValues);
  internals.AnnotationValues->BuildRepresentation();
  internals.AnnotationValues->SetWindowLocation(vtkTextRepresentation::AnyLocation);
  internals.AnnotationValues->GetTextActor()->SetTextScaleModeToNone();
  internals.AnnotationValues->GetTextActor()->GetTextProperty()->SetJustificationToRight();
  internals.AnnotationValues->SetVisibility(false);

  internals.Decoder = vtk::TakeSmartPointer(vtkFFmpegSoftwareDecoder::New());

  // monitor render window so we can setup observers on interactor
  // for events that send render requests to the server.
  internals.RenderWindowObserverId = internals.RenderWindow->AddObserver(
    vtkCommand::ModifiedEvent, this, &vtkSMViewProxy::SetupInteractor);
  // this gives us a chance to render the RS (render-service) rendering result on top of our window.
  internals.Renderer->AddObserver(
    vtkCommand::EndEvent, this, &vtkSMViewProxy::HandleRendererEndEvent);
}

//----------------------------------------------------------------------------
vtkSMViewProxy::~vtkSMViewProxy()
{
  const auto& internals = (*this->Internals);
  // clean up observers.
  if (auto* iren = internals.RenderWindow->GetInteractor())
  {
    for (const auto& observerId : internals.InteractorObserverIds)
    {
      iren->RemoveObserver(observerId);
    }
  }
  // unsub the output stream observable.
  this->UnsubscribeOutputStreams();
}

//----------------------------------------------------------------------------
vtkRenderWindow* vtkSMViewProxy::GetRenderWindow() const
{
  const auto& internals = (*this->Internals);
  return internals.RenderWindow;
}

//----------------------------------------------------------------------------
vtkRenderWindowInteractor* vtkSMViewProxy::GetRenderWindowInteractor() const
{
  const auto& internals = (*this->Internals);
  return internals.RenderWindow ? internals.RenderWindow->GetInteractor() : nullptr;
}

//----------------------------------------------------------------------------
vtkCamera* vtkSMViewProxy::GetCamera() const
{
  const auto& internals = (*this->Internals);
  return internals.Renderer->GetActiveCamera();
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RequestProgressiveRenderingPassIfNeeded()
{
  if (this->GetContinueRendering())
  {
    this->RequestProgressiveRenderingPass();
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::PostRenderingResult(const vtkPacket& packet)
{
  vtkLogScopeF(TRACE, "%s got packet", __func__);
  auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);

  this->ParsePayload(packet);
  this->ParseMetadata(packet);

  const bool streamOutput = vtkSMPropertyHelper(this, "StreamOutput").GetAsInt(0) == 1;
  const auto& json = packet.GetJSON();
  if (streamOutput && json.contains("metadata"))
  {
    auto metadata = json.at("metadata");
    if (metadata.contains("mimetype") && internals.CompressedFrame != nullptr)
    {
      internals.CompressedFrame->SetMimeType(metadata.at("mimetype").get<std::string>().c_str());
    }
    this->ContainerizePackage();
  }

  int codec = json.at("codec").get<int>();
  bool display = vtkSMPropertyHelper(this, "Display").GetAsInt(0) == 1;

  // detect video codec changes.
  if (codec > -1 && internals.Decoder->GetCodec() != static_cast<VTKVideoCodecType>(codec))
  {
    internals.RawFrame->SetWidth(0);
    internals.RawFrame->SetHeight(0);
  }

  internals.NeedsVideoDecoder = codec > -1;
  if (display && internals.NeedsVideoDecoder)
  {
    // need to decode.
    this->RefreshVideoDecoder(packet);
  }
  else if (display && codec == -1)
  {
    // no need to decode.
    internals.RawFrame->SetSliceOrderType(vtkRawVideoFrame::SliceOrderType::BottomUp);
  }
  else
  {
    internals.CompressedFrame = nullptr;
  }

  // this will fire of end event on the renderer that eventually
  // 1. allocate opengl video frame storage for internals.RawFrame
  // 2. do decoding if necessary and store result in internals.RawFrame
  // 3. overlay internals.RawFrame on top of render window.
  internals.RenderWindow->Render();
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::SetUseGenericOpenGLRenderWindow(bool val)
{
  vtkSMViewProxy::UseGenericOpenGLRenderWindow = val;
}

//----------------------------------------------------------------------------
bool vtkSMViewProxy::GetUseGenericOpenGLRenderWindow()
{
  return vtkSMViewProxy::UseGenericOpenGLRenderWindow;
}

//----------------------------------------------------------------------------
rxcpp::observable<bool> vtkSMViewProxy::Update()
{
  vtkLogScopeF(TRACE, "%s", __func__);
  auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);

  using BehaviorT = rxcpp::subjects::behavior<vtkVariant>;
  auto behavior = std::make_shared<BehaviorT>(vtkVariant());
  this->ExecuteCommand("Update", vtkClientSession::DATA_SERVER).subscribe([behavior, this](bool) {
    this->ExecuteCommand("UpdateForRendering", vtkClientSession::RENDER_SERVER)
      .subscribe([=](bool status) {
        behavior->get_subscriber().on_next(vtkVariant(status ? 1 : 0));
        this->StillRender();
      });
  });

  return behavior->get_observable()
    .filter([](const vtkVariant& variant) { return variant.IsValid(); })
    .take(1)
    .map([behavior](const vtkVariant& variant) {
      // capture behavior to ensure it hangs around until the returned value hangs around.
      (void)behavior;
      return variant.ToInt() != 0;
    });
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ResetCameraUsingVisiblePropBounds()
{
  vtkLogScopeF(TRACE, "%s", __func__);
  auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);
  if (internals.VisiblePropBounds.IsValid())
  {
    double bds[6];
    internals.VisiblePropBounds.GetBounds(bds);
    internals.Renderer->ResetCamera(bds);

    // If 'CenterOfRotation' is present, set it to the center of bounds as well.
    if (auto* centerProperty = this->GetProperty("CenterOfRotation"))
    {
      double center[3];
      internals.VisiblePropBounds.GetCenter(center);
      vtkSMPropertyHelper(centerProperty).Set(center, 3);
    }
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::StillRender()
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);
  this->SetupInteractor();
  // update bounds.
  this->ResetCameraUsingVisiblePropBounds();
  this->RequestRender();
}

//----------------------------------------------------------------------------
const vtkBoundingBox& vtkSMViewProxy::GetVisiblePropBounds() const
{
  return this->Internals->VisiblePropBounds;
}

//----------------------------------------------------------------------------
bool vtkSMViewProxy::HideOtherRepresentationsIfNeeded(vtkSMProxy* repr)
{
  if (repr == nullptr || this->GetHints() == nullptr ||
    this->GetHints()->FindNestedElementByName("ShowOneRepresentationAtATime") == nullptr)
  {
    return false;
  }

  vtkPVXMLElement* oneRepr =
    this->GetHints()->FindNestedElementByName("ShowOneRepresentationAtATime");
  const char* reprType = oneRepr->GetAttribute("type");

  if (reprType && strcmp(repr->GetXMLName(), reprType) != 0)
  {
    return false;
  }

  vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;

  bool modified = false;
  vtkSMPropertyHelper helper(this, "Representations");
  for (unsigned int cc = 0, max = helper.GetNumberOfElements(); cc < max; ++cc)
  {
    auto* arepr = helper.GetAsProxy(cc);
    if (arepr && arepr != repr)
    {
      if (vtkSMPropertyHelper(arepr, "Visibility", /*quiet*/ true).GetAsInt() == 1 &&
        (!reprType || (reprType && !strcmp(arepr->GetXMLName(), reprType))))
      {
        controller->Hide(arepr, this);
        modified = true;
      }
    }
  }
  return modified;
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RepresentationVisibilityChanged(vtkSMProxy*, bool) {}

//----------------------------------------------------------------------------
int vtkSMViewProxy::ReadXMLAttributes(vtkSMSessionProxyManager* pm, vtkPVXMLElement* element)
{
  if (!this->Superclass::ReadXMLAttributes(pm, element))
  {
    return 0;
  }

  auto& internals = (*this->Internals);
  const char* repr_name = element->GetAttribute("representation_name");
  if (repr_name)
  {
    internals.DefaultRepresentationName = repr_name;
  }
  return 1;
}

//----------------------------------------------------------------------------
vtkSMProxy* vtkSMViewProxy::CreateDefaultRepresentation(vtkSMProxy* proxy, int outputPort)
{
  vtkSMSourceProxy* producer = vtkSMSourceProxy::SafeDownCast(proxy);
  if ((producer == nullptr) || (outputPort < 0) ||
    (static_cast<int>(producer->GetNumberOfOutputPorts()) <= outputPort) ||
    (producer->GetSession() != this->GetSession()))
  {
    return nullptr;
  }

  // Update with time from the view to ensure we have up-to-date data.
#if 0
  double view_time = vtkSMPropertyHelper(this, "ViewTime").GetAsDouble();
#else
  // FIXME ASYNC: Until and if we support animation, let's just request the first time step.
  // Otherwise, output messages window will annoyingly pop up with an error complaining
  // the property "ViewTime" does not exist on the view proxy, rightly so, it doesn't exist.
  double view_time = 0;
#endif
  producer->UpdatePipeline(view_time);

  const char* representationType = this->GetRepresentationType(producer, outputPort);
  if (!representationType)
  {
    return nullptr;
  }

  vtkSMSessionProxyManager* pxm = this->GetSessionProxyManager();
  if (auto* repr = pxm->NewProxy("representations", representationType))
  {
    return repr;
  }
  vtkWarningMacro(
    "Failed to create representation (representations," << representationType << ").");
  return nullptr;
}

//----------------------------------------------------------------------------
const char* vtkSMViewProxy::GetRepresentationType(vtkSMSourceProxy* producer, int outputPort)
{
  assert(producer && static_cast<int>(producer->GetNumberOfOutputPorts()) > outputPort);

  // Process producer hints to see if indicates what type of representation
  // to create for this view.
  if (const char* reprName = vtkSMViewProxyNS::GetRepresentationNameFromHints(
        this->GetXMLName(), producer->GetHints(), outputPort))
  {
    return reprName;
  }

  const auto& internals = (*this->Internals);
  // check if we have default representation name specified in XML.
  if (!internals.DefaultRepresentationName.empty())
  {
    vtkSMSessionProxyManager* pxm = this->GetSessionProxyManager();
    vtkSMProxy* prototype =
      pxm->GetPrototypeProxy("representations", internals.DefaultRepresentationName.c_str());
    if (prototype)
    {
      vtkSMProperty* inputProp = prototype->GetProperty("Input");
      vtkSMUncheckedPropertyHelper helper(inputProp);
      helper.Set(producer, outputPort);
      bool acceptable = (inputProp->IsInDomains() > 0);
      helper.SetNumberOfElements(0);

      if (acceptable)
      {
        return internals.DefaultRepresentationName.c_str();
      }
    }
  }

  return nullptr;
}

//----------------------------------------------------------------------------
bool vtkSMViewProxy::CanDisplayData(vtkSMSourceProxy* producer, int outputPort)
{
  if (producer == nullptr || outputPort < 0 ||
    static_cast<int>(producer->GetNumberOfOutputPorts()) <= outputPort ||
    producer->GetSession() != this->GetSession())
  {
    return false;
  }

  const char* type = this->GetRepresentationType(producer, outputPort);
  if (type != nullptr)
  {
    vtkSMSessionProxyManager* pxm = this->GetSessionProxyManager();
    return (pxm->GetPrototypeProxy("representations", type) != nullptr);
  }

  return false;
}

//----------------------------------------------------------------------------
vtkSMProxy* vtkSMViewProxy::FindRepresentation(vtkSMSourceProxy* producer, int outputPort)
{
  vtkSMPropertyHelper helper(this, "Representations");
  for (unsigned int cc = 0, max = helper.GetNumberOfElements(); cc < max; ++cc)
  {
    auto* repr = helper.GetAsProxy(cc);
    if (repr && repr->GetProperty("Input"))
    {
      vtkSMPropertyHelper helper2(repr, "Input");
      if (helper2.GetAsProxy() == producer &&
        static_cast<int>(helper2.GetOutputPort()) == outputPort)
      {
        return repr;
      }
    }
  }

  return nullptr;
}

//----------------------------------------------------------------------------
vtkSMViewProxy* vtkSMViewProxy::FindView(vtkSMProxy* repr, const char* reggroup /*=views*/)
{
  if (!repr)
  {
    return nullptr;
  }

  vtkSMSessionProxyManager* pxm = repr->GetSessionProxyManager();
  vtkNew<vtkSMProxyIterator> iter;
  iter->SetSessionProxyManager(pxm);
  iter->SetModeToOneGroup();
  for (iter->Begin(reggroup); !iter->IsAtEnd(); iter->Next())
  {
    if (vtkSMViewProxy* view = vtkSMViewProxy::SafeDownCast(iter->GetProxy()))
    {
      auto reprs = vtkSMProxyProperty::SafeDownCast(view->GetProperty("Representations"));
      auto hreprs = vtkSMProxyProperty::SafeDownCast(view->GetProperty("HiddenRepresentations"));
      if ((reprs != nullptr && reprs->IsProxyAdded(repr)) ||
        (hreprs != nullptr && hreprs->IsProxyAdded(repr)))
      {
        return view;
      }
    }
  }
  return nullptr;
}

//----------------------------------------------------------------------------
rxcpp::observable<vtkSmartPointer<vtkCompressedVideoPacket>>
vtkSMViewProxy::GetViewOutputObservable() const
{
  return this->Internals->OutputStream.get_observable().tap(
    [](auto package) { vtkLogF(INFO, "push package to output observable"); });
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::UnsubscribeOutputStreams()
{
  vtkLogScopeFunction(TRACE);
  this->Internals->OutputStream.get_subscriber().on_completed();
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ProcessPiggybackedInformation(const vtkNJson& json)
{
  this->Superclass::ProcessPiggybackedInformation(json);

  auto iter = json.find("VisiblePropBounds");
  if (iter != json.end())
  {
    std::vector<double> bounds = iter.value().get<std::vector<double>>();
    this->Internals->VisiblePropBounds.SetBounds(bounds.data());
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::InitializeInteractor(vtkRenderWindowInteractor* iren)
{
  vtkLogScopeF(TRACE, "%s iren=%s", __func__, vtkLogIdentifier(iren));
  auto& internals = (*this->Internals);
  if (iren != nullptr && !iren->GetInitialized())
  {
    vtkLogF(TRACE, "Initializing interactor because it's unininitialized.");
    iren->Initialize();
    // refresh the size here if interactor did not already initialize default size 300,300.
    if (0 == internals.RenderWindow->GetSize()[0] && 0 == internals.RenderWindow->GetSize()[1])
    {
      internals.RenderWindow->SetSize(300, 300);
    }
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RefreshVideoDecoder(const vtkPacket& packet)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  const auto& json = packet.GetJSON();
  const auto codec = static_cast<VTKVideoCodecType>(json.at("codec").get<int>());

  if (internals.Decoder->GetCodec() != codec)
  {
    internals.Decoder->Flush();
    internals.Decoder->Shutdown();
    internals.Decoder = nullptr;
    if (codec == VTKVideoCodecType::VTKVC_JPEG)
    {
      internals.Decoder = vtk::TakeSmartPointer(vtkJPEGVideoDecoder::New());
    }
    else
    {
      internals.Decoder = vtk::TakeSmartPointer(vtkFFmpegSoftwareDecoder::New());
    }
    internals.Decoder->SetCodec(codec);
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ParseMetadata(const vtkPacket& packet)
{
  // parse annotations from the metadata.
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  const auto& json = packet.GetJSON();
  if (json.contains("metadata"))
  {
    auto& metadata = json["metadata"];
    this->UpdateRayTracingState(metadata);
    this->AddMetadataAsAnnotations(metadata);
  }
  else
  {
    internals.AnnotationKeys->SetVisibility(false);
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ParsePayload(const vtkPacket& packet)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  const auto& json = packet.GetJSON();
  if (!json.at("size").get<int>())
  {
    vtkLog(ERROR, << "payload is empty");
    return;
  }

  auto array = packet.GetPayload<vtkUnsignedCharArray>("package");
  if (array == nullptr)
  {
    vtkLogF(ERROR, "invalid package (nullptr)");
    return;
  }

  internals.CompressedFrame = vtk::TakeSmartPointer(vtkCompressedVideoPacket::New());
  internals.CompressedFrame->SetWidth(json["width"]);
  internals.CompressedFrame->SetHeight(json["height"]);
  internals.CompressedFrame->SetIsKeyFrame(json["keyframe"]);
  internals.CompressedFrame->SetPresentationTS(json["pts"]);
  internals.CompressedFrame->SetSize(array->GetNumberOfValues());
  internals.CompressedFrame->CopyData(array);
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ContainerizePackage()
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  vtkSmartPointer<vtkCompressedVideoPacket> chunk = nullptr;
  if (internals.CompressedFrame == nullptr)
  {
    // nothing to containerize.
    return;
  }
  const int& width = internals.CompressedFrame->GetWidth();
  const int& height = internals.CompressedFrame->GetHeight();
  if (internals.CompressedFrame->GetMimeType() == std::string("video/webm; codecs=vp09.00.10.08"))
  {
    vtkLogScopeF(TRACE, "write video/webm; codecs=vp09.00.10.08");
    // containerize decoded packet.
    // for every key frame, write a new header.
    // if there were any previous chunks, we need to signal
    // end-of-stream for previous chunks.
    const bool isFirstFrame = internals.CompressedFrame->GetPresentationTS() <= 1;
    if (internals.CompressedFrame->GetIsKeyFrame() && !isFirstFrame)
    {
      internals.WebmWriter->WriteFileTrailer();
    }

    if (!internals.WebmWriter->GetIsHeaderWritten())
    {
      internals.WebmWriter->SetWidth(width);
      internals.WebmWriter->SetHeight(height);
      internals.WebmWriter->SetFramerate(30);
      internals.WebmWriter->SetWriteToMemory(true);
      internals.WebmWriter->WriteVP9FileHeader();
    }

    internals.WebmWriter->WriteWebmBlock(internals.CompressedFrame);
    chunk = vtk::TakeSmartPointer(vtkCompressedVideoPacket::New());
    chunk->CopyMetadata(internals.CompressedFrame);
    chunk->CopyData(internals.WebmWriter->GetDynamicBuffer());
    internals.WebmWriter->Flush();
  }
  else if (internals.CompressedFrame->GetMimeType() == std::string("image/bmp"))
  {
    // write bmp.
    vtkNew<vtkUnsignedCharArray> pixels;
    unsigned char* dataPtr = nullptr;
    const auto size = internals.CompressedFrame->GetData(dataPtr);
    const vtkIdType numTuples = size >> 2;
    pixels->SetNumberOfComponents(4);
    pixels->SetNumberOfTuples(numTuples);
    pixels->SetArray(dataPtr, size, 1);

    vtkNew<vtkImageData> img;
    img->SetDimensions(width, height, 1);
    img->AllocateScalars(VTK_UNSIGNED_CHAR, 4);
    img->GetPointData()->SetScalars(pixels);

    internals.BitmapWriter->SetInputData(img);
    internals.BitmapWriter->SetWriteToMemory(true);
    internals.BitmapWriter->Write();

    auto result = internals.BitmapWriter->GetResult();
    chunk = vtk::TakeSmartPointer(vtkCompressedVideoPacket::New());
    chunk->CopyMetadata(internals.CompressedFrame);
    chunk->CopyData(result);
  }
  else
  {
    // put raw bitstream in the observable..
    chunk = internals.CompressedFrame;
  }

  internals.OutputStream.get_subscriber().on_next(chunk);
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::ParseDecoderOutput(const VTKVideoDecoderResultType& output)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  auto& frames = output.second;
  if (frames.empty())
  {
    return;
  }
  if (frames[0] == nullptr)
  {
    return;
  }

  const auto& srcFrame = frames[0];
  const auto& dstFrame = internals.RawFrame.Get();
  const int& width = srcFrame->GetWidth();
  const int& height = srcFrame->GetHeight();
  const auto& pixFmt = srcFrame->GetPixelFormat();

  // when switching codecs, the output frame may differ.
  // handle that here and reallocate storage if needed.
  this->RefreshOpenGLRawFrame(width, height, pixFmt);
  dstFrame->DeepCopy(srcFrame);
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RefreshOpenGLRawFrame(int width, int height, VTKPixelFormatType pixelFormat)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  bool reInit = internals.RawFrame->GetWidth() != width;
  reInit |= internals.RawFrame->GetHeight() != height;
  reInit |= internals.RawFrame->GetPixelFormat() != pixelFormat;

  if (reInit)
  {
    internals.RawFrame->ReleaseGraphicsResources();
    internals.RawFrame->SetContext(vtkOpenGLRenderWindow::SafeDownCast(internals.RenderWindow));
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::AddMetadataAsAnnotations(const vtkNJson& metadata)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  if (metadata.empty())
  {
    internals.AnnotationKeys->SetVisibility(false);
    internals.AnnotationValues->SetVisibility(false);
  }

  std::ostringstream keys;
  std::ostringstream values;
  if (metadata.contains("using-raytracing") && metadata.at("using-raytracing").get<bool>())
  {
    keys << "Rendering Pass: \n";
    values << fmt::format("{:3d}/{:3d}\n", internals.RenderingPass, internals.TotalRenderingPasses);
  }

  if (metadata.contains("timestamp"))
  {
    const double delta = vtkTimerLog::GetUniversalTime() - metadata.at("timestamp").get<double>();
    keys << "Frame rate: \n";
    values << fmt::format("{:0.3}\n", 1.0 / delta);
  }

  if (metadata.contains("mimetype"))
  {
    keys << "Mime type: \n";
    values << metadata.at("mimetype").get<std::string>() << '\n';
  }
  keys << "Video codec: \n";
  values << (internals.NeedsVideoDecoder
                ? vtkVideoCodecTypeUtilities::ToString(internals.Decoder->GetCodec())
                : "none (lossless)")
         << '\n';

  if (metadata.contains("hwaccel"))
  {
    keys << "HW-accel: \n";
    values << metadata.at("hwaccel").get<std::string>() << '\n';
  }
  if (metadata.contains("capturetime"))
  {
    keys << "Capture: \n";
    values << ::getNiceTime(metadata.at("capturetime").get<double>()) << '\n';
  }
  if (metadata.contains("gpu2cpuxfertime"))
  {
    keys << "Pixel xfer: \n";
    values << ::getNiceTime(metadata.at("gpu2cpuxfertime").get<double>()) << '\n';
  }
  if (metadata.contains("encodetime"))
  {
    keys << "Encode: \n";
    values << ::getNiceTime(metadata.at("encodetime").get<double>()) << '\n';
  }
  if (internals.CompressedFrame != nullptr)
  {
    keys << "Packet size: \n";
    values << ::getNiceBytes(internals.CompressedFrame->GetSize()) << '\n';
  }

  internals.AnnotationKeys->SetVisibility(true);
  internals.AnnotationValues->SetVisibility(true);
  internals.AnnotationKeys->SetText(keys.str().c_str());
  internals.AnnotationValues->SetText(values.str().c_str());
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::DrawOverlay()
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  auto texture = reinterpret_cast<vtkTextureObject*>(internals.RawFrame->GetResourceHandle());
  if (texture->GetContext() != internals.RenderWindow)
  {
    return;
  }

  internals.InDrawOverlay = true;
  internals.RawFrame->Render(internals.RenderWindow);
  internals.InDrawOverlay = false;
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RequestRender()
{
  vtkLogScopeFunction(TRACE);
  this->UpdateSize();
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::RequestProgressiveRenderingPass()
{
  vtkLogScopeFunction(TRACE);
  this->UpdateCameraIfNeeded(/*force*/ true, /* progressiveRendering*/ true);
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::SetupInteractor()
{
  auto& internals = (*this->Internals);
  vtkLogScopeF(TRACE, "%s, numObservers=%zud", __func__, internals.InteractorObserverIds.size());

  auto* iren = internals.RenderWindow->GetInteractor();
  if (!iren)
  {
    vtkLogF(TRACE, "Interactor null. Yet to be setup..");
    return;
  }
  if (!internals.InteractorObserverIds.empty())
  {
    vtkLogF(TRACE, "Interactor already setup.");
    return;
  }

  this->InitializeInteractor(iren);

  internals.InteractorObserverIds.push_back(iren->AddObserver(
    vtkCommand::StartInteractionEvent, this, &vtkSMViewProxy::HandleInteractorEvents));
  internals.InteractorObserverIds.push_back(iren->AddObserver(
    vtkCommand::EndInteractionEvent, this, &vtkSMViewProxy::HandleInteractorEvents));
  internals.InteractorObserverIds.push_back(
    iren->AddObserver(vtkCommand::RenderEvent, this, &vtkSMViewProxy::HandleInteractorEvents));
  internals.InteractorObserverIds.push_back(iren->AddObserver(
    vtkCommand::WindowResizeEvent, this, &vtkSMViewProxy::HandleInteractorEvents));

  // XOpenGLRenderWindow fires ConfigureEvent when window resizes.
  internals.InteractorObserverIds.push_back(
    iren->AddObserver(vtkCommand::ConfigureEvent, this, &vtkSMViewProxy::HandleInteractorEvents));

  // Remove the observer so we don't end up calling this method again.
  internals.RenderWindow->RemoveObserver(internals.RenderWindowObserverId);
  internals.RenderWindowObserverId = 0;
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::HandleRendererEndEvent(
  vtkObject* /*caller*/, unsigned long eventId, void* /*calldata*/)
{
  auto& internals = (*this->Internals);
  vtkLogScopeF(TRACE, "%s, needs_video_decoder=%d", __func__, internals.NeedsVideoDecoder);

  if (eventId != vtkCommand::EndEvent)
  {
    return;
  }

  // 1. adjust positioning of value annotations to appear next to keys.
  auto pos = internals.AnnotationKeys->GetPositionCoordinate();
  auto pos2 = internals.AnnotationKeys->GetPosition2Coordinate();
  internals.AnnotationValues->SetPosition(pos2->GetValue()[0] + 0.01, 0.99 - pos2->GetValue()[1]);

  // 2. Handle muted display, null render service result.
  bool display = vtkSMPropertyHelper(this, "Display").GetAsInt(0) == 1;
  if (!display)
  {
    // we're not supposed to decode and render the frame. display stats only.
    internals.Renderer2DForKeys->DrawOn();
    internals.Renderer2DForValues->DrawOn();
    internals.Renderer->Clear();
    return;
  }
  else if (internals.CompressedFrame == nullptr)
  {
    // draw NOTHING new.
    // we turned off erase so that `internals.Renderer` retains previous pixels
    // so as to avoid displaying a black screen.
    // if the annotations are drawn now, they will appear on top of the previous
    // annotations which looks bad.
    internals.Renderer2DForKeys->DrawOff();
    internals.Renderer2DForValues->DrawOff();
    return;
  }

  // 3. initialize our opengl frame.
  // need to initialize here and only here for reasons to do with qt's opengl context.
  auto texture = reinterpret_cast<vtkTextureObject*>(internals.RawFrame->GetResourceHandle());
  if (texture->GetContext() != internals.RenderWindow)
  {
    internals.RawFrame->ReleaseGraphicsResources();
    internals.RawFrame->SetContext(vtkOpenGLRenderWindow::SafeDownCast(internals.RenderWindow));
  }

  // 4. decode if necessary.
  if (internals.NeedsVideoDecoder)
  {
    internals.Decoder->Push(internals.CompressedFrame);
    const auto result = internals.Decoder->GetResult();
    internals.CompressedFrame = nullptr;

    this->ParseDecoderOutput(result);
  }
  else
  {
    const int& width = internals.CompressedFrame->GetWidth();
    const int& height = internals.CompressedFrame->GetHeight();
    const auto& pixFmt = VTKPixelFormatType::VTKPF_RGBA32;

    this->RefreshOpenGLRawFrame(width, height, pixFmt);
    internals.RawFrame->SetWidth(width);
    internals.RawFrame->SetHeight(height);
    internals.RawFrame->SetPixelFormat(pixFmt);
    internals.RawFrame->ComputeDefaultStrides();
    internals.RawFrame->AllocateDataStore();
    internals.RawFrame->CopyData(internals.CompressedFrame->GetData());
  }

  // 5. Draw the picture as an overlay on top of our render window.
  this->DrawOverlay();
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::HandleInteractorEvents(
  vtkObject* caller, unsigned long eventId, void* /*calldata*/)
{
  vtkLogScopeF(TRACE, "%s, eventId=%ld", __func__, eventId);

  auto& internals = (*this->Internals);
  auto* interactor = vtkRenderWindowInteractor::SafeDownCast(caller);
  switch (eventId)
  {
    case vtkCommand::StartInteractionEvent:
      break;
    case vtkCommand::EndInteractionEvent:
      break;
    case vtkCommand::RenderEvent:
      if (!internals.InDrawOverlay && interactor->GetEnableRender())
      {
        internals.Renderer2DForKeys->DrawOn();
        internals.Renderer2DForValues->DrawOn();
        this->UpdateCameraIfNeeded();
      }
      break;
    case vtkCommand::WindowResizeEvent:
    case vtkCommand::ConfigureEvent:
      this->UpdateSize();
      break;
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::UpdateCameraIfNeeded(
  bool force /*= false*/, bool progressiveRendering /*= false*/)
{
  vtkLogScopeF(
    TRACE, "%s, force=%d, progressiveRendering=%d", __func__, force, progressiveRendering);

  auto& internals = (*this->Internals);
  auto* camera = internals.Renderer->GetActiveCamera();
  if (force || progressiveRendering || internals.LastCameraMTime != camera->GetMTime())
  {
    internals.RenderingPass = progressiveRendering ? internals.RenderingPass + 1 : 0;
    vtkLogF(TRACE, "push camera");
    internals.LastCameraMTime = camera->GetMTime();
    auto packet = vtkRemoteObjectProviderViews::Render(this->GetGlobalID(), camera);
    this->GetSession()->SendMessage(vtkClientSession::RENDER_SERVER, std::move(packet));
  }
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::UpdateSize()
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  auto& window = internals.RenderWindow;
  vtkSMPropertyHelper(this, "ViewSize").Set(window->GetSize(), 2);
  vtkLogF(TRACE, "UpdateSize(%d,%d)", window->GetSize()[0], window->GetSize()[1]);
  this->UpdateVTKObjects();
  this->UpdateCameraIfNeeded(true);
}

//----------------------------------------------------------------------------
void vtkSMViewProxy::UpdateRayTracingState(const vtkNJson& metadata)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  if (metadata.empty())
  {
    return;
  }
  if (metadata.contains("using-raytracing"))
  {
    internals.UsingRayTracing = metadata.at("using-raytracing").get<bool>();
  }

  if (metadata.contains("total-rendering-passes"))
  {
    internals.TotalRenderingPasses = metadata.at("total-rendering-passes").get<int>();
  }
}

//----------------------------------------------------------------------------
bool vtkSMViewProxy::GetContinueRendering() const
{
  auto& internals = (*this->Internals);
  if (!internals.UsingRayTracing)
  {
    return false;
  }
  if (internals.RenderingPass < internals.TotalRenderingPasses)
  {
    return true;
  }
  return false;
}

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