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

  Program:   ParaView
  Module:    vtkPVView.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 "vtkPVView.h"

#include "vtkBMPWriter.h"
#include "vtkCPUVideoFrame.h"
#include "vtkCallbackCommand.h"
#include "vtkCompressedVideoPacket.h"
#include "vtkDataObject.h"
#include "vtkGenericOpenGLRenderWindow.h"
#include "vtkImageData.h"
#include "vtkInformation.h"
#include "vtkInformationDoubleKey.h"
#include "vtkInformationObjectBaseKey.h"
#include "vtkInformationRequestKey.h"
#include "vtkLogger.h"
#include "vtkMultiProcessController.h"
#include "vtkObjectFactory.h"
#include "vtkOpenGLRenderWindow.h"
#include "vtkOpenGLState.h"
#include "vtkOpenGLVideoFrame.h"
#include "vtkPVCoreApplication.h"
#include "vtkPVCoreApplicationOptions.h"
#include "vtkPVDataDeliveryManager.h"
#include "vtkPVDataRepresentation.h"
#include "vtkPVLogger.h"
#include "vtkPVProcessWindow.h"
#include "vtkPVRenderingCapabilitiesInformation.h"
#include "vtkPacket.h"
#include "vtkPixelFormatTypes.h"
#include "vtkPointData.h"
#include "vtkRawVideoFrame.h"
#include "vtkRenderWindow.h"
#include "vtkRendererCollection.h"
#include "vtkSmartPointer.h"
#include "vtkTimerLog.h"
#include "vtkVideoCodecTypes.h"
#include "vtkVideoEncoder.h"
#include "vtkVideoEncoderFactory.h"
#include "vtkVideoProcessingStatusTypes.h"
#include "vtkViewLayout.h"

#include <cassert>
#include <chrono>
#include <map>
#include <ostream>
#include <sstream>

namespace
{
class vtkOffscreenOpenGLRenderWindow : public vtkGenericOpenGLRenderWindow
{
public:
  static vtkOffscreenOpenGLRenderWindow* New();
  vtkTypeMacro(vtkOffscreenOpenGLRenderWindow, vtkGenericOpenGLRenderWindow);

  void SetContext(vtkOpenGLRenderWindow* cntxt)
  {
    if (cntxt == this->Context)
    {
      return;
    }

    if (this->Context)
    {
      this->MakeCurrent();
      this->Finalize();
    }

    this->SetReadyForRendering(false);
    this->Context = cntxt;
    if (cntxt)
    {
      this->SetForceMaximumHardwareLineWidth(1);
      this->SetReadyForRendering(true);
      this->SetOwnContext(0);
    }
  }
  void MakeCurrent() override
  {
    if (this->Context)
    {
      this->Context->MakeCurrent();
      this->Superclass::MakeCurrent();
    }
  }

  bool IsCurrent() override { return this->Context ? this->Context->IsCurrent() : false; }

  void SetUseOffScreenBuffers(bool) override {}
  void SetShowWindow(bool) override {}

  void Render() override
  {
    if (this->Context && this->GetReadyForRendering())
    {
      this->InvokeEvent(vtkViewLayout::RequestUpdateLayoutEvent);
      if (!this->Initialized)
      {
        // ensures that context is created.
        vtkPVProcessWindow::PrepareForRendering();
        this->Context->MakeCurrent();
        this->GetState()->PushFramebufferBindings();
        this->OpenGLInit();
      }
      else
      {
        this->GetState()->PushFramebufferBindings();
      }
      this->Superclass::Render();
      if (auto state = this->GetState())
      {
        state->PopFramebufferBindings();
      }
      this->InvokeEvent(vtkViewLayout::RequestUpdateDisplayEvent);
    }
  }

  vtkOpenGLState* GetState() override
  {
    return this->Context ? this->Context->GetState() : this->Superclass::GetState();
  }

protected:
  vtkOffscreenOpenGLRenderWindow()
  {
    this->SetReadyForRendering(false);
    this->Superclass::SetUseOffScreenBuffers(true);
    this->Superclass::SetShowWindow(false);
  }
  ~vtkOffscreenOpenGLRenderWindow() override
  {
    // have to finalize here while GetState() will use this classes
    // vtable. In parent destuctors GetState will return a different
    // value causing resource/state issues.
    this->Finalize();
  }

private:
  vtkOffscreenOpenGLRenderWindow(const vtkOffscreenOpenGLRenderWindow&) = delete;
  void operator=(const vtkOffscreenOpenGLRenderWindow&) = delete;

  vtkSmartPointer<vtkOpenGLRenderWindow> Context;
};

vtkStandardNewMacro(vtkOffscreenOpenGLRenderWindow);

struct EncOptions
{
  int Quality{ 1 };
  VTKVideoCodecType CodecType{ VTKVideoCodecType::VTKVC_VP9 };
  bool UseAsynchronousEncoding{ true };
  bool UseHardwareAcceleration{ false };
  bool UseCompression{ false };
};

}

//============================================================================
class vtkPVView::vtkInternals
{
public:
  vtkSmartPointer<vtkMultiProcessController> Controller;
  vtkSmartPointer<vtkVideoEncoder> Encoder;
  vtkSmartPointer<vtkVideoEncoderFactory> EncoderFactory;
  vtkNew<vtkOpenGLVideoFrame> CapturedFrame;
  EncOptions EncoderOptions;
  vtkTypeUInt32 GlobalID{ 0 };
  vtkVector2i Position{ 0, 0 };
  vtkVector2i Size{ 300, 300 };
  int PPI{ 72 };
  double ViewTime{ 0.0 };
  std::string LogName;
  bool Initialized{ false };
  bool InitializedForDataProcessing{ false };
  std::chrono::high_resolution_clock::duration CaptureTimeElapsed, GPUCPUXferTimeElapsed;

  vtkSmartPointer<vtkPVDataDeliveryManager> DeliveryManager;
  std::vector<vtkSmartPointer<vtkPVDataRepresentation>> Representations;

  mutable vtkTimeStamp UpdateTimeStamp;

  /**
   * Keeps track of the active rendering pass.
   */
  mutable vtkInformation* ActiveRequest{ nullptr };

  mutable int LastRenderingPass{ 0 };
  mutable int LastRenderingMode{ vtkPVView::STILL_RENDER };

  std::atomic<std::uint64_t> LocalPreviewRenderCounter{ 0 };
  std::uint64_t GlobalPreviewRenderCounter{ 0 };
  std::uint64_t RenderCounter{ 0 };
};

vtkInformationKeyMacro(vtkPVView, REQUEST_UPDATE_DATA, Request);
vtkInformationKeyMacro(vtkPVView, REQUEST_UPDATE_DATA_LOD, Request);
vtkInformationKeyMacro(vtkPVView, REQUEST_UPDATE_RENDERING_INFORMATION, Request);
vtkInformationKeyMacro(vtkPVView, REQUEST_PREPARE_FOR_RENDER, Request);
vtkInformationKeyMacro(vtkPVView, REQUEST_PREPARE_FOR_RENDER_LOD, Request);
vtkInformationKeyMacro(vtkPVView, REQUEST_RENDER, Request);
vtkInformationKeyMacro(vtkPVView, UPDATE_TIME_STEP, Double);
//============================================================================
bool vtkPVView::UseGenericOpenGLRenderWindow = false;
//----------------------------------------------------------------------------
void vtkPVView::SetUseGenericOpenGLRenderWindow(bool val)
{
  vtkPVView::UseGenericOpenGLRenderWindow = val;
}

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

//============================================================================
vtkPVView::vtkPVView()
  : Internals(new vtkPVView::vtkInternals())
{
  auto& internals = (*this->Internals);
  internals.DeliveryManager.TakeReference(vtkPVDataDeliveryManager::New());
  internals.DeliveryManager->SetView(this);
}

//----------------------------------------------------------------------------
vtkPVView::~vtkPVView() = default;

//----------------------------------------------------------------------------
void vtkPVView::SetGlobalID(vtkTypeUInt32 gid)
{
  auto& internals = (*this->Internals);
  internals.GlobalID = gid;
}

//----------------------------------------------------------------------------
vtkTypeUInt32 vtkPVView::GetGlobalID() const
{
  const auto& internals = (*this->Internals);
  return internals.GlobalID;
}

//----------------------------------------------------------------------------
void vtkPVView::SetController(vtkMultiProcessController* controller)
{
  auto& internals = (*this->Internals);
  internals.Controller = controller;
}

//----------------------------------------------------------------------------
vtkMultiProcessController* vtkPVView::GetController() const
{
  const auto& internals = (*this->Internals);
  return internals.Controller;
}

//----------------------------------------------------------------------------
void vtkPVView::SetPosition(int xpos, int ypos)
{
  auto& internals = (*this->Internals);
  internals.Position = vtkVector2i(xpos, ypos);
}

//----------------------------------------------------------------------------
const int* vtkPVView::GetPosition() const
{
  const auto& internals = (*this->Internals);
  return internals.Position.GetData();
}

//----------------------------------------------------------------------------
void vtkPVView::SetSize(int width, int height)
{
  vtkLogScopeF(TRACE, "%s %dx%d", __func__, width, height);
  auto& internals = (*this->Internals);
  if (this->InTileDisplayMode() || this->InCaveDisplayMode())
  {
    vtkLogF(TRACE, "Ignore request to set size. Reason: In tile/cave display mode");
    // the size request is ignored.
    return;
  }
  bool isInvalidSize = width <= 0 || height <= 0;
  if (isInvalidSize)
  {
    vtkLogF(WARNING, "Ignore request to set size. Reason. Invalid size=%dx%d", width, height);
    return;
  }
  else if (auto* window = this->GetRenderWindow())
  {
    const auto cur_size = window->GetActualSize();
    if (cur_size[0] != width || cur_size[1] != height)
    {
      window->SetSize(width, height);
    }
    else
    {
      vtkLogF(TRACE, "Ignore request to set size. Reason: current_size=%dx%d=size", width, height);
    }
  }
  internals.Size = vtkVector2i(width, height);
}

//----------------------------------------------------------------------------
const int* vtkPVView::GetSize() const
{
  const auto& internals = (*this->Internals);
  return internals.Size.GetData();
}

//----------------------------------------------------------------------------
void vtkPVView::SetPPI(int ppi)
{
  auto& internals = (*this->Internals);
  internals.PPI = ppi;
  if (auto window = this->GetRenderWindow())
  {
    window->SetDPI(ppi);
  }
}

//----------------------------------------------------------------------------
int vtkPVView::GetPPI() const
{
  const auto& internals = (*this->Internals);
  return internals.PPI;
}

//----------------------------------------------------------------------------
void vtkPVView::SetViewTime(double value)
{
  auto& internals = (*this->Internals);
  internals.ViewTime = value;
}

//----------------------------------------------------------------------------
double vtkPVView::GetViewTime() const
{
  const auto& internals = (*this->Internals);
  return internals.ViewTime;
}

//----------------------------------------------------------------------------
void vtkPVView::SetLogName(const std::string& name)
{
  auto& internals = (*this->Internals);
  internals.LogName = name;
}

//----------------------------------------------------------------------------
const std::string& vtkPVView::GetLogName() const
{
  const auto& internals = (*this->Internals);
  return internals.LogName;
}

//----------------------------------------------------------------------------
void vtkPVView::InitializeForDataProcessing()
{
  auto& internals = (*this->Internals);
  assert("Not already initialized" && !internals.Initialized);
  internals.Initialized = true;
  internals.InitializedForDataProcessing = true;
}

//----------------------------------------------------------------------------
void vtkPVView::InitializeForRendering()
{
  auto& internals = (*this->Internals);
  assert("Not already initialized" && !internals.Initialized);
  internals.Initialized = true;
  internals.InitializedForDataProcessing = false;
  internals.EncoderFactory = vtk::TakeSmartPointer(vtkVideoEncoderFactory::New());
}

//----------------------------------------------------------------------------
vtkRenderWindow* vtkPVView::NewRenderWindow()
{
  auto& internals = (*this->Internals);
  assert("View has been initialized." && internals.Initialized);
  assert("Called on rendering service." && !internals.InitializedForDataProcessing);

  auto* pvapp = vtkPVCoreApplication::GetInstance();
  // FIXME:
  // auto config = vtkRemotingCoreConfiguration::GetInstance();
  // // A little bit of a hack. I am not what's a good place to setup the display
  // // environment for this rank. So doing it here.
  // config->HandleDisplayEnvironment();

  vtkSmartPointer<vtkRenderWindow> window;
  // FIXME: ASYNC
  // For now, just create offscreen window always...we need to rethink this
  // quite a bit.
  window = vtkPVRenderingCapabilitiesInformation::NewOffscreenRenderWindow();
  if (window)
  {
    vtkVLogF(PARAVIEW_LOG_RENDERING_VERBOSITY(),
      "created a `%s` as a new render window for view %s", vtkLogIdentifier(window),
      vtkLogIdentifier(this));

    window->AlphaBitPlanesOn();

    std::ostringstream str;
    switch (pvapp->GetApplicationType())
    {
      case vtkPVCoreApplication::SERVER:
        str << "ParaView Server";
        break;

      default:
        str << "ParaView";
        break;
    }
    if (pvapp->GetNumberOfRanks() > 1)
    {
      str << pvapp->GetRank();
    }
    window->SetWindowName(str.str().c_str());
    window->Register(this);
    return window;
  }

  vtkVLogF(PARAVIEW_LOG_RENDERING_VERBOSITY(),
    "created `nullptr` as a new render window for view %s", vtkLogIdentifier(this));
  return nullptr;
}

//----------------------------------------------------------------------------
bool vtkPVView::InTileDisplayMode()
{
  // auto serverInfo = this->Session->GetServerInformation();
  // return serverInfo->GetIsInTileDisplay();
  // FIXME: ASYNC
  return false;
}

//----------------------------------------------------------------------------
bool vtkPVView::InCaveDisplayMode()
{
  // auto serverInfo = this->Session->GetServerInformation();
  // return serverInfo->GetIsInCave();
  // FIXME: ASYNC
  return false;
}

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

//----------------------------------------------------------------------------
void vtkPVView::Update()
{
  vtkVLogScopeF(PARAVIEW_LOG_RENDERING_VERBOSITY(), "%s: update view", this->GetLogName().c_str());
  this->InvokeEvent(vtkCommand::StartEvent);

  const auto& internals = (*this->Internals);
  if (!internals.InitializedForDataProcessing)
  {
    vtkLogF(WARNING, "Update called on non-data processing service. Ignored.");
    return;
  }

  vtkNew<vtkInformation> request;
  request->Set(vtkPVView::REQUEST_UPDATE_DATA());
  request->Set(vtkPVView::UPDATE_TIME_STEP(), internals.ViewTime);
  this->CallProcessViewRequest(request);
  internals.UpdateTimeStamp.Modified();

  // TODO:
  // reduce visible data size across all ranks.
  // this will then be used for other decision making.
  // This decision making will include which service to render on, perhaps i.e. render-server
  // or render-server-lite.
  this->InvokeEvent(vtkCommand::UpdateEvent);
  this->InvokeEvent(vtkCommand::EndEvent);
}

//----------------------------------------------------------------------------
void vtkPVView::UpdateForRendering()
{
  vtkVLogScopeF(PARAVIEW_LOG_RENDERING_VERBOSITY(), "%s: update view", this->GetLogName().c_str());
  this->InvokeEvent(vtkCommand::StartEvent);
  const auto& internals = (*this->Internals);
  if (internals.InitializedForDataProcessing)
  {
    vtkLogF(WARNING, "UpdateForRendering called on data processing service. Ignored.");
    return;
  }

  // RequestPrepareForRender is done twice. This ensures that
  // RequestRenderingInformation has as up-to-date data as possible to make
  // rendering decisions.
  this->RequestPrepareForRender();
  this->RequestRenderingInformation();
  this->RequestPrepareForRender();

  this->InvokeEvent(vtkCommand::UpdateEvent);
  this->InvokeEvent(vtkCommand::EndEvent);
}

//----------------------------------------------------------------------------
void vtkPVView::RequestPrepareForRender()
{
  vtkNew<vtkInformation> request;
  request->Set(vtkPVView::REQUEST_PREPARE_FOR_RENDER());
  this->CallProcessViewRequest(request);
}

//----------------------------------------------------------------------------
void vtkPVView::RequestRenderingInformation()
{
  vtkNew<vtkInformation> request;
  request->Set(vtkPVView::REQUEST_UPDATE_RENDERING_INFORMATION());
  this->CallProcessViewRequest(request);
}

//----------------------------------------------------------------------------
void vtkPVView::StillRender(int pass /*=0*/)
{
  vtkVLogScopeF(PARAVIEW_LOG_RENDERING_VERBOSITY(), "%s: still-render", this->GetLogName().c_str());
  const auto& internals = (*this->Internals);
  if (internals.InitializedForDataProcessing)
  {
    vtkLogF(WARNING, "StillRender called on data processing service. Ignored.");
    return;
  }

  internals.LastRenderingPass = pass;
  internals.LastRenderingMode = vtkPVView::STILL_RENDER;

  vtkNew<vtkInformation> request;
  request->Set(vtkPVView::REQUEST_RENDER());
  this->CallProcessViewRequest(request);

  this->PrepareForRendering();
  if (auto* window = this->GetRenderWindow())
  {
    window->Render();
  }
  this->InvokeEvent(vtkCommand::RenderEvent, &internals.LastRenderingMode);
}

//----------------------------------------------------------------------------
void vtkPVView::InteractiveRender(int pass /*=0*/)
{
  vtkVLogScopeF(
    PARAVIEW_LOG_RENDERING_VERBOSITY(), "%s: interative-render", this->GetLogName().c_str());
  const auto& internals = (*this->Internals);
  if (internals.InitializedForDataProcessing)
  {
    vtkLogF(WARNING, "InteractiveRender called on data processing service. Ignored.");
    return;
  }

  internals.LastRenderingPass = pass;
  internals.LastRenderingMode = vtkPVView::INTERACTIVE_RENDER;
  this->InvokeEvent(vtkCommand::RenderEvent, &internals.LastRenderingMode);
}

//----------------------------------------------------------------------------
void vtkPVView::DoPreviewRender()
{
  // this is thread safe!
  auto& internals = (*this->Internals);
  ++internals.LocalPreviewRenderCounter;
}

//----------------------------------------------------------------------------
void vtkPVView::DoRender()
{
  auto& internals = (*this->Internals);
  auto& controller = internals.Controller;

  ++internals.RenderCounter;
  if (internals.RenderCounter < internals.GlobalPreviewRenderCounter)
  {
    // skip this render; globally we've seen newer render requests.
    return;
  }

  // we track local and global preview counters separately to avoid
  // communication between ranks for render requests that are known to be
  // obsolete since the most recent exchange among ranks.
  const uint64_t lcounter = internals.LocalPreviewRenderCounter;
  if (controller->GetNumberOfProcesses() > 1)
  {
    controller->AllReduce(
      &lcounter, &internals.GlobalPreviewRenderCounter, 1, vtkCommunicator::MAX_OP);
  }
  else
  {
    internals.GlobalPreviewRenderCounter = lcounter;
  }

  if (internals.RenderCounter < internals.GlobalPreviewRenderCounter)
  {
    return;
  }

  assert(internals.RenderCounter == internals.GlobalPreviewRenderCounter);
  this->StillRender();
}

//----------------------------------------------------------------------------
int vtkPVView::GetLastRenderingPass() const
{
  const auto& internals = (*this->Internals);
  return internals.LastRenderingPass;
}

//----------------------------------------------------------------------------
int vtkPVView::GetLastRenderingMode() const
{
  const auto& internals = (*this->Internals);
  return internals.LastRenderingMode;
}

//----------------------------------------------------------------------------
void vtkPVView::SetCodecType(VTKVideoCodecType value)
{
  vtkLogScopeF(TRACE, "%s, %s", __func__, vtkVideoCodecTypeUtilities::ToString(value));
  auto& internals = (*this->Internals);
  internals.EncoderOptions.CodecType = value;
}

//----------------------------------------------------------------------------
void vtkPVView::SetCodecType(int value)
{
  vtkLogScopeF(TRACE, "%s, %d", __func__, value);
  auto& internals = (*this->Internals);
  if (value >= 0 && value < static_cast<int>(VTKVideoCodecType::VTKVC_MaxNumberOfSupportedCodecs))
  {
    internals.EncoderOptions.CodecType = static_cast<VTKVideoCodecType>(value);
    internals.EncoderOptions.UseCompression = true;
    vtkLogF(TRACE, "Using video codec %s",
      vtkVideoCodecTypeUtilities::ToString(internals.EncoderOptions.CodecType));
  }
  else
  {
    vtkLogF(TRACE, "Using lossless image stream");
    internals.EncoderOptions.UseCompression = false;
  }
}

//----------------------------------------------------------------------------
VTKVideoCodecType vtkPVView::GetCodecType() const
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  return internals.EncoderOptions.CodecType;
}

//----------------------------------------------------------------------------
void vtkPVView::SetUseAsynchronousEncoding(bool value)
{
  vtkLogScopeF(TRACE, "%s, %d", __func__, value);
  auto& internals = (*this->Internals);
  internals.EncoderOptions.UseAsynchronousEncoding = value;
}

//----------------------------------------------------------------------------
bool vtkPVView::GetUseAsynchronousEncoding() const
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  return internals.EncoderOptions.UseAsynchronousEncoding;
}

//----------------------------------------------------------------------------
void vtkPVView::UseAsynchronousEncodingOn()
{
  vtkLogScopeFunction(TRACE);
  this->SetUseAsynchronousEncoding(true);
}

//----------------------------------------------------------------------------
void vtkPVView::UseAsynchronousEncodingOff()
{
  vtkLogScopeFunction(TRACE);
  this->SetUseAsynchronousEncoding(false);
}

//----------------------------------------------------------------------------
void vtkPVView::SetUseHardwareAcceleration(bool value)
{
  vtkLogScopeF(TRACE, "%s, %d", __func__, value);
  auto& internals = (*this->Internals);
  internals.EncoderOptions.UseHardwareAcceleration = value;
}

//----------------------------------------------------------------------------
bool vtkPVView::GetUseHardwareAcceleration() const
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  return internals.EncoderOptions.UseHardwareAcceleration;
}

//----------------------------------------------------------------------------
void vtkPVView::UseHardwareAccelerationOn()
{
  vtkLogScopeFunction(TRACE);
  this->SetUseHardwareAcceleration(true);
}

//----------------------------------------------------------------------------
void vtkPVView::UseHardwareAccelerationOff()
{
  vtkLogScopeFunction(TRACE);
  this->SetUseHardwareAcceleration(false);
}

//----------------------------------------------------------------------------
void vtkPVView::SetForceKeyFrame(bool value)
{
  vtkLogScopeF(TRACE, "%s, %d", __func__, value);
  const auto& internals = (*this->Internals);
  if (internals.Encoder == nullptr)
  {
    return;
  }
  internals.Encoder->SetForceIFrame(value);
}

//----------------------------------------------------------------------------
bool vtkPVView::GetForceKeyFrame() const
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  if (internals.Encoder == nullptr)
  {
    return false;
  }
  return internals.Encoder->GetForceIFrame();
}

//----------------------------------------------------------------------------
void vtkPVView::ForceKeyFrameOn()
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  if (internals.Encoder == nullptr)
  {
    return;
  }
  internals.Encoder->SetForceIFrame(true);
}

//----------------------------------------------------------------------------
void vtkPVView::ForceKeyFrameOff()
{
  vtkLogScopeFunction(TRACE);
  const auto& internals = (*this->Internals);
  if (internals.Encoder == nullptr)
  {
    return;
  }
  internals.Encoder->SetForceIFrame(false);
}

//----------------------------------------------------------------------------
void vtkPVView::SetQuality(int value)
{
  vtkLogScopeF(TRACE, "%s, %d", __func__, value);
  auto& internals = (*this->Internals);
  internals.EncoderOptions.Quality = value;
}

//----------------------------------------------------------------------------
int vtkPVView::GetQuality() const
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  return internals.EncoderOptions.Quality;
}

vtkVideoEncoder* vtkPVView::CreateNewEncoder(VTKVideoCodecType codec, bool hardwareAccel)
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  // create new encoder.
  vtkVideoEncoder* encoder = nullptr;
  if (hardwareAccel)
  {
    encoder = internals.EncoderFactory->NewEncoder(codec);
  }
  if (!hardwareAccel || encoder == nullptr)
  {
    encoder = internals.EncoderFactory->NewSoftwareEncoder(codec);
  }
  return encoder;
}

//----------------------------------------------------------------------------
void vtkPVView::GetDefaultEncoder()
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);
  const auto& options = internals.EncoderOptions;
  if (internals.InitializedForDataProcessing)
  {
    // silently ignore.
    return;
  }
  if (internals.Encoder != nullptr)
  {
    // 1. Facilitate runtime switch b/w codecs and hardware/software.
    if (options.UseHardwareAcceleration != internals.Encoder->IsHardwareAccelerated() ||
      options.CodecType != internals.Encoder->GetCodec())
    {
      vtkLogScopeF(TRACE, "Setup encoder with hardware acceleration %s for codec %s",
        options.UseHardwareAcceleration ? "on" : "off",
        vtkVideoCodecTypeUtilities::ToString(options.CodecType));
      // drain and shutdown previous encoder.
      this->ShutdownEncoder();
      internals.Encoder = vtk::TakeSmartPointer(
        this->CreateNewEncoder(options.CodecType, options.UseHardwareAcceleration));
    }

    // 2. for runtime switch b/w async-sync encoding.
    internals.Encoder->RemoveAllObservers();
    internals.Encoder->SetAsyncMode(options.UseAsynchronousEncoding);
    // add observer if encoder really uses an async delegate.
    if (internals.Encoder->GetAsyncMode())
    {
      vtkNew<vtkCallbackCommand> cmd;
      cmd->SetClientData(this);
      cmd->SetCallback([](vtkObject*, unsigned long, void* selfPtr, void*) {
        auto self = reinterpret_cast<vtkPVView*>(selfPtr);
        vtkLogScopeF(TRACE, "encoder-progress");
        self->InvokeEvent(vtkCommand::ViewProgressEvent);
      });
      internals.Encoder->AddObserver(vtkCommand::ProgressEvent, cmd);
    }
  }
  else
  {
    internals.Encoder = vtk::TakeSmartPointer(
      this->CreateNewEncoder(options.CodecType, options.UseHardwareAcceleration));
  }
  internals.Encoder->SetGraphicsContext(this->GetRenderWindow());
}

//----------------------------------------------------------------------------
void vtkPVView::InitializeEncoder()
{
  auto& internals = (*this->Internals);
  if (internals.InitializedForDataProcessing)
  {
    // silently ignore.
    return;
  }
  vtkLogScopeFunction(TRACE);
  auto& options = internals.EncoderOptions;

  // 1. if it doesn't exist.
  this->GetDefaultEncoder();

  if (internals.Encoder == nullptr)
  {
    vtkLogF(ERROR, "An encoder for codec %s is unavailable",
      vtkVideoCodecTypeUtilities::ToString(options.CodecType));
    return;
  }

  // 2. Initialize the encoder parameters.
  vtkLog(TRACE, << "Quality: " << options.Quality);
  internals.Encoder->SetQuality(options.Quality);
  auto* factory = internals.EncoderFactory.GetPointer();
  // low quality -> low bitrate.
  const int bit_rate = factory->GetFactoryBitrate() * options.Quality;
  // low quality -> bigger gop size.
  const int gop_size = factory->GetFactoryGroupOfPicturesSize() * 10 / options.Quality;
  // low quality -> bigger time base.  (doesn't really affect quality that much?)

  // 3.determine if encoder needs to be restarted with the above parameters.
  bool needsRestart = internals.Encoder->GetCodec() != options.CodecType;
  needsRestart |= (bit_rate != internals.Encoder->GetBitRate());
  needsRestart |= (gop_size != internals.Encoder->GetGroupOfPicturesSize());

  if (!needsRestart)
  {
    return;
  }
  // shut it down so that new parameters are applied the next time encoder is initialized.
  internals.Encoder->Shutdown();
  internals.Encoder->SetBitRate(bit_rate);
  internals.Encoder->SetGroupOfPicturesSize(gop_size);
  internals.Encoder->SetMaximumBFrames(0);
  internals.Encoder->SetTimeBaseStart(1);
  internals.Encoder->SetTimeBaseEnd(internals.EncoderFactory->GetFactoryTimeBaseEnd());
  internals.Encoder->Initialize();
}

//----------------------------------------------------------------------------
void vtkPVView::ShutdownEncoder()
{
  auto& internals = (*this->Internals);
  if (internals.InitializedForDataProcessing)
  {
    // ignored.
    return;
  }
  vtkLogScopeFunction(TRACE);
  internals.Encoder->RemoveAllObservers();
  internals.Encoder->Shutdown();
  internals.Encoder = nullptr;
}

//----------------------------------------------------------------------------
int vtkPVView::CallProcessViewRequest(vtkInformation* request)
{
  const auto& internals = (*this->Internals);
  internals.ActiveRequest = request;

  int count = 0;
  for (auto& repr : internals.Representations)
  {
    if (repr->GetVisibility())
    {
      count += repr->ProcessViewRequest(request) ? 1 : 0;
    }
  }

  internals.ActiveRequest = nullptr;
  return count;
}

//-----------------------------------------------------------------------------
void vtkPVView::SetRepresentations(const std::vector<vtkPVDataRepresentation*>& representations)
{
  const auto& internals = (*this->Internals);

  std::vector<vtkPVDataRepresentation*> to_add;
  std::vector<vtkPVDataRepresentation*> to_remove;

  const std::set<vtkPVDataRepresentation*> old_value(
    internals.Representations.begin(), internals.Representations.end());
  const std::set<vtkPVDataRepresentation*> new_value(
    representations.begin(), representations.end());

  std::set_difference(old_value.begin(), old_value.end(), new_value.begin(), new_value.end(),
    std::back_inserter(to_remove));
  std::set_difference(new_value.begin(), new_value.end(), old_value.begin(), old_value.end(),
    std::back_inserter(to_add));

  for (auto& repr : to_remove)
  {
    if (repr)
    {
      this->RemoveRepresentation(repr);
    }
  }

  for (auto& repr : to_add)
  {
    if (repr)
    {
      this->AddRepresentation(repr);
    }
  }
}

//----------------------------------------------------------------------------
void vtkPVView::AddRepresentation(vtkPVDataRepresentation* representation)
{
  auto& internals = (*this->Internals);
  auto iter =
    std::find(internals.Representations.begin(), internals.Representations.end(), representation);
  if (iter == internals.Representations.end())
  {
    if (representation->AddToView(this))
    {
      this->AddRepresentationInternal(representation);
      internals.DeliveryManager->RegisterRepresentation(representation);
      internals.Representations.push_back(representation);
    }
    this->Modified();
  }
}

//----------------------------------------------------------------------------
void vtkPVView::RemoveRepresentation(vtkPVDataRepresentation* representation)
{
  auto& internals = (*this->Internals);
  auto iter =
    std::find(internals.Representations.begin(), internals.Representations.end(), representation);
  if (iter != internals.Representations.end())
  {
    this->RemoveRepresentationInternal(representation);
    representation->RemoveFromView(this);
    internals.DeliveryManager->UnRegisterRepresentation(representation);
    internals.Representations.erase(iter);
    this->Modified();
  }
}

//-----------------------------------------------------------------------------
void vtkPVView::ScaleRendererViewports(const double viewport[4])
{
  auto window = this->GetRenderWindow();
  if (!window)
  {
    return;
  }

  assert(viewport[0] >= 0 && viewport[0] <= 1.0);
  assert(viewport[1] >= 0 && viewport[1] <= 1.0);
  assert(viewport[2] >= 0 && viewport[2] <= 1.0);
  assert(viewport[3] >= 0 && viewport[3] <= 1.0);

  auto collection = window->GetRenderers();
  collection->InitTraversal();
  while (auto renderer = collection->GetNextItem())
  {
    renderer->SetViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
  }
}

//-----------------------------------------------------------------------------
void vtkPVView::SetTileScale(int x, int y)
{
  auto window = this->GetRenderWindow();
  if (window && !this->InTileDisplayMode() && !this->InCaveDisplayMode())
  {
    window->SetTileScale(x, y);
  }
}

//-----------------------------------------------------------------------------
void vtkPVView::SetTileViewport(double x0, double y0, double x1, double y1)
{
  auto window = this->GetRenderWindow();
  if (window && !this->InTileDisplayMode() && !this->InCaveDisplayMode())
  {
    window->SetTileViewport(x0, y0, x1, y1);
  }
}

//----------------------------------------------------------------------------
std::string vtkPVView::GetChannelName(vtkTypeUInt32 gid)
{
  return "view-image-" + std::to_string(gid);
}

//----------------------------------------------------------------------------
void vtkPVView::SetPiece(
  vtkPVDataRepresentation* repr, int index, vtkDataObject* data, int lodLevel)
{
  if (auto* self = repr ? repr->GetView() : nullptr)
  {
    auto& dManager = self->Internals->DeliveryManager;
    dManager->SetPiece(repr, index, data, lodLevel);
  }
}

//----------------------------------------------------------------------------
vtkSmartPointer<vtkDataObject> vtkPVView::GetPiece(
  vtkPVDataRepresentation* repr, int index, int lodLevel)
{
  if (auto* self = repr ? repr->GetView() : nullptr)
  {
    auto& dManager = self->Internals->DeliveryManager;
    return dManager->GetPiece(repr, index, lodLevel);
  }

  return nullptr;
}

//----------------------------------------------------------------------------
vtkPVDataDeliveryManager* vtkPVView::GetDataDeliveryManager() const
{
  const auto& internals = (*this->Internals);
  return internals.DeliveryManager;
}

//----------------------------------------------------------------------------'
void vtkPVView::PostRender()
{
  auto& internals = (*this->Internals);
  auto window = this->GetRenderWindow();
  if (window == nullptr)
  {
    return;
  }

  if (internals.EncoderOptions.UseCompression)
  {
    this->InitializeEncoder();
  }
  else if (internals.Encoder != nullptr)
  {
    // switched from video codec to lossless.
    this->ShutdownEncoder();
  }

  vtkLogStartScope(TRACE, "capture");
  const auto& width = window->GetSize()[0];
  const auto& height = window->GetSize()[1];
  if (width != internals.CapturedFrame->GetWidth() ||
    height != internals.CapturedFrame->GetHeight())
  {
    // initialize graphics resources for our frame.
    internals.CapturedFrame->ReleaseGraphicsResources();
    internals.CapturedFrame->SetContext(vtkOpenGLRenderWindow::SafeDownCast(window));
    // initialize the dimensions and storage.
    internals.CapturedFrame->SetWidth(width);
    internals.CapturedFrame->SetHeight(height);
    internals.CapturedFrame->SetPixelFormat(VTKPixelFormatType::VTKPF_RGBA32);
    internals.CapturedFrame->SetSliceOrderType(vtkRawVideoFrame::SliceOrderType::BottomUp);
    internals.CapturedFrame->ComputeDefaultStrides();
    internals.CapturedFrame->AllocateDataStore();
  }
  auto tStart = std::chrono::high_resolution_clock::now();
  internals.CapturedFrame->Capture(window);
  auto tStop = std::chrono::high_resolution_clock::now();
  internals.CaptureTimeElapsed = tStop - tStart;
  vtkLogEndScope("capture");

  const auto& options = internals.EncoderOptions;
  const bool doVideoEncode = options.UseCompression;

  if (doVideoEncode && (internals.Encoder != nullptr))
  {
    // prediction-based video compression
    vtkLogScopeF(TRACE, "video-encode-push");
    internals.Encoder->Push(internals.CapturedFrame);
    if (!internals.Encoder->GetAsyncMode())
    {
      // emit event so that provider can get the encoded result right away.
      this->InvokeEvent(vtkCommand::ViewProgressEvent, &internals.LastRenderingMode);
    }
  }
  else
  {
    // we have the raw image and that is progress, let's proceed.
    this->InvokeEvent(vtkCommand::ViewProgressEvent, &internals.LastRenderingMode);
  }
}

//----------------------------------------------------------------------------
vtkPacket vtkPVView::GetRenderedResult(vtkNJson& metadata) const
{
  vtkLogScopeFunction(TRACE);
  auto& internals = (*this->Internals);

  const auto& options = internals.EncoderOptions;
  const bool doVideoEncode = options.UseCompression;
  const int codecType = doVideoEncode ? static_cast<int>(options.CodecType) : -1;
  std::string mimetype;
  vtkNJson result;

  // 1. Distinct view ID.
  result["gid"] = this->GetGlobalID();

  // 2. metadata ..
  if (!metadata.empty())
  {
    result["metadata"] = metadata;
  }
  else
  {
    result["metadata"] = {};
  }
  result["codec"] = codecType;
  result["metadata"]["capturetime"] = static_cast<double>(internals.CaptureTimeElapsed.count());

  // set the mimetype here, so that client can stream into an appropriate multimedia container.
  if (codecType == -1)
  {
    mimetype = "image/bmp";
  }
  else
  {
    switch (options.CodecType)
    {
      case VTKVideoCodecType::VTKVC_VP9:
        mimetype = "video/webm; codecs=vp09.00.10.08";
        break;
      case VTKVideoCodecType::VTKVC_JPEG:
        mimetype = "image/jpeg";
        break;
      default:
        // we don't have muxers setup for the rest of the codecs.
        mimetype = "application/octet-stream";
        break;
    }
  }

  // 3. payload.
  using VTKVPStatus = VTKVideoProcessingStatusType;
  std::map<std::string, vtkSmartPointer<vtkObject>> payload;
  if (doVideoEncode && (internals.Encoder == nullptr))
  {
    vtkLog(WARNING, << "Video encoder uavailable!");
    result["size"] = 0;
    payload["package"] = vtk::TakeSmartPointer(vtkUnsignedCharArray::New());
    return vtkPacket(result, payload);
  }
  else if (doVideoEncode)
  {
    // prediction-based video compression
    vtkLogScopeF(TRACE, "video-encode");
    const auto output = internals.Encoder->GetResult();
    const auto recvStatus = output.first;
    const auto& packets = output.second;

    bool received = (recvStatus == VTKVPStatus::VTKVPStatus_Success);
    received &= !packets.empty();
    vtkLogF(TRACE, "Recv status=%s", vtkVideoProcessingStatusTypeUtilities::ToString(recvStatus));
    if (received && packets[0] != nullptr)
    {
      auto& package = packets[0];
      result["width"] = package->GetWidth();
      result["height"] = package->GetHeight();
      result["keyframe"] = package->GetIsKeyFrame();
      result["pts"] = package->GetPresentationTS();
      result["size"] = package->GetSize();
      result["metadata"]["mimetype"] = mimetype;
      result["metadata"]["encodetime"] =
        static_cast<double>(internals.Encoder->GetLastEncodeTimeNS());
      result["metadata"]["hwaccel"] = internals.Encoder->IsHardwareAccelerated() ? "yes" : "no";
      result["metadata"]["gpu2cpuxfertime"] =
        static_cast<double>(internals.Encoder->GetLastScaleTimeNS());
      payload["package"] = package->GetData();
      return vtkPacket(result, payload);
    }
    vtkLog(WARNING, << "Could not receive compressed packet from encoder!");
    result["size"] = 0;
    payload["package"] = vtk::TakeSmartPointer(vtkUnsignedCharArray::New());
    return vtkPacket(result, payload);
  }
  // write the frame as it is.
  unsigned char* dataPtr = nullptr;
  auto tStart = std::chrono::high_resolution_clock::now();
  const auto size = internals.CapturedFrame->GetData(dataPtr);
  auto tStop = std::chrono::high_resolution_clock::now();
  internals.GPUCPUXferTimeElapsed = tStop - tStart;

  auto package = vtk::TakeSmartPointer(vtkUnsignedCharArray::New());
  package->SetArray(dataPtr, size, 1);
  // wrap up.
  result["width"] = internals.CapturedFrame->GetWidth();
  result["height"] = internals.CapturedFrame->GetHeight();
  result["keyframe"] = true;
  result["pts"] = internals.RenderCounter;
  result["size"] = size;
  result["metadata"]["mimetype"] = mimetype;
  result["metadata"]["encodetime"] = 0;
  result["metadata"]["hwaccel"] = "no";
  result["metadata"]["gpu2cpuxfertime"] =
    static_cast<double>(internals.GPUCPUXferTimeElapsed.count());
  payload["package"] = package;
  return vtkPacket(result, payload);
}

//----------------------------------------------------------------------------
vtkPacket vtkPVView::GetRenderedResult() const
{
  vtkNJson metadata;
  return this->GetRenderedResult(metadata);
}
