// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkRenderLargeImage.h"

#include "vtkActor2D.h"
#include "vtkActor2DCollection.h"
#include "vtkCamera.h"
#include "vtkImageData.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkProp.h"
#include "vtkRenderWindow.h"
#include "vtkRenderer.h"
#include "vtkRendererCollection.h"
#include "vtkStreamingDemandDrivenPipeline.h"

#include <vector>
//------------------------------------------------------------------------------
VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkRenderLargeImage);

vtkCxxSetObjectMacro(vtkRenderLargeImage, Input, vtkRenderer);
//------------------------------------------------------------------------------
// 2D Actors need to be rescaled and shifted about for each tile
// use this helper class to make life easier.
class vtkRenderLargeImage2DHelperClass
{
public:
  // maintain a list of 2D actors
  vtkActor2DCollection* StoredActors;
  // maintain lists of their vtkCoordinate objects
  vtkCollection* Coord1s;
  vtkCollection* Coord2s;
  // Store the display coords for adjustment during tiling
  std::vector<std::pair<int, int>> Coords1;
  std::vector<std::pair<int, int>> Coords2;
  //
  vtkRenderLargeImage2DHelperClass()
  {
    this->StoredActors = vtkActor2DCollection::New();
    this->Coord1s = vtkCollection::New();
    this->Coord2s = vtkCollection::New();
  }
  ~vtkRenderLargeImage2DHelperClass()
  {
    this->Coord1s->RemoveAllItems();
    this->Coord2s->RemoveAllItems();
    this->StoredActors->RemoveAllItems();
    this->Coord1s->Delete();
    this->Coord2s->Delete();
    this->StoredActors->Delete();
  }
};
//------------------------------------------------------------------------------
vtkRenderLargeImage::vtkRenderLargeImage()
{
  this->Input = nullptr;
  this->Magnification = 3;
  this->SetNumberOfInputPorts(0);
  this->SetNumberOfOutputPorts(1);
  this->StoredData = new vtkRenderLargeImage2DHelperClass();
}
//------------------------------------------------------------------------------
vtkRenderLargeImage::~vtkRenderLargeImage()
{
  if (this->Input)
  {
    this->Input->UnRegister(this);
    this->Input = nullptr;
  }
  delete this->StoredData;
}

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

  if (this->Input)
  {
    os << indent << "Input:\n";
    this->Input->PrintSelf(os, indent.GetNextIndent());
  }
  else
  {
    os << indent << "Input: (none)\n";
  }

  os << indent << "Magnification: " << this->Magnification << "\n";
}

//------------------------------------------------------------------------------
vtkImageData* vtkRenderLargeImage::GetOutput()
{
  return vtkImageData::SafeDownCast(this->GetOutputDataObject(0));
}

//------------------------------------------------------------------------------
vtkTypeBool vtkRenderLargeImage::ProcessRequest(
  vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
{
  // generate the data
  if (request->Has(vtkDemandDrivenPipeline::REQUEST_DATA()))
  {
    this->RequestData(request, inputVector, outputVector);
    return 1;
  }

  // execute information
  if (request->Has(vtkDemandDrivenPipeline::REQUEST_INFORMATION()))
  {
    this->RequestInformation(request, inputVector, outputVector);
    return 1;
  }

  return this->Superclass::ProcessRequest(request, inputVector, outputVector);
}

//------------------------------------------------------------------------------
// Description:
// This method returns the largest region that can be generated.
void vtkRenderLargeImage::RequestInformation(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  // get the info objects
  vtkInformation* outInfo = outputVector->GetInformationObject(0);

  if (this->Input == nullptr)
  {
    vtkErrorMacro(<< "Please specify a renderer as input!");
    return;
  }

  // set the extent, if the VOI has not been set then default to
  int wExt[6];
  wExt[0] = 0;
  wExt[2] = 0;
  wExt[4] = 0;
  wExt[5] = 0;
  wExt[1] = this->Magnification * this->Input->GetRenderWindow()->GetSize()[0] - 1;
  wExt[3] = this->Magnification * this->Input->GetRenderWindow()->GetSize()[1] - 1;

  outInfo->Set(vtkStreamingDemandDrivenPipeline::WHOLE_EXTENT(), wExt, 6);

  // set the spacing
  outInfo->Set(vtkDataObject::SPACING(), 1.0, 1.0, 1.0);

  // set the origin.
  outInfo->Set(vtkDataObject::ORIGIN(), 0.0, 0.0, 0.0);

  // set the scalar components
  vtkDataObject::SetPointDataActiveScalarInfo(outInfo, VTK_UNSIGNED_CHAR, 3);
}

//------------------------------------------------------------------------------
// Description:
// This function reads a region from a file.  The regions extent/axes
// are assumed to be the same as the file extent/order.
void vtkRenderLargeImage::RequestData(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  vtkInformation* outInfo = outputVector->GetInformationObject(0);
  vtkImageData* data = vtkImageData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT()));
  data->SetExtent(outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_EXTENT()));
  data->AllocateScalars(outInfo);
  int inExtent[6];
  vtkIdType inIncr[3];
  int* size;
  int inWindowExtent[4];
  double viewAngle, parallelScale, windowCenter[2];
  vtkCamera* cam;
  unsigned char *pixels, *outPtr;
  int x, y, row;
  int rowSize, rowStart, rowEnd, colStart, colEnd;
  int doublebuffer;
  int swapbuffers = 0;
  bool gradientBackground;
  double background1[3];
  double background2[3];

  if (this->GetOutput()->GetScalarType() != VTK_UNSIGNED_CHAR)
  {
    vtkErrorMacro("mismatch in scalar types!");
    return;
  }

  // Get the requested extents.
  outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_EXTENT(), inExtent);

  // get and transform the increments
  data->GetIncrements(inIncr);

  // get the size of the render window
  size = this->Input->GetRenderWindow()->GetSize();

  // convert the request into windows
  inWindowExtent[0] = inExtent[0] / size[0];
  inWindowExtent[1] = inExtent[1] / size[0];
  inWindowExtent[2] = inExtent[2] / size[1];
  inWindowExtent[3] = inExtent[3] / size[1];

  this->Rescale2DActors();

  // save original background colors
  gradientBackground = this->Input->GetGradientBackground();
  if (gradientBackground)
  {
    background1[0] = this->Input->GetBackground()[0];
    background1[1] = this->Input->GetBackground()[1];
    background1[2] = this->Input->GetBackground()[2];
    background2[0] = this->Input->GetBackground2()[0];
    background2[1] = this->Input->GetBackground2()[1];
    background2[2] = this->Input->GetBackground2()[2];
  }

  // store the old view angle & set the new
  cam = this->Input->GetActiveCamera();
  cam->GetWindowCenter(windowCenter);
  viewAngle = cam->GetViewAngle();
  parallelScale = cam->GetParallelScale();

  cam->SetViewAngle(2.0 * atan(tan(0.5 * viewAngle * vtkMath::Pi() / 360.0) / this->Magnification) *
    360.0 / vtkMath::Pi());
  cam->SetParallelScale(parallelScale / this->Magnification);

  // are we double buffering?  If so, read from back buffer ....
  doublebuffer = this->Input->GetRenderWindow()->GetDoubleBuffer();
  if (doublebuffer)
  {
    // save swap buffer state to restore later
    swapbuffers = this->Input->GetRenderWindow()->GetSwapBuffers();
    this->Input->GetRenderWindow()->SetSwapBuffers(0);
  }

  // render each of the tiles required to fill this request
  double ySize = static_cast<double>(inWindowExtent[3] - inWindowExtent[2] + 1);
  bool abort = false;
  for (y = inWindowExtent[2]; y <= inWindowExtent[3] && !abort; y++)
  {
    for (x = inWindowExtent[0]; x <= inWindowExtent[1]; x++)
    {
      if (this->CheckAbort())
      {
        abort = true;
        break;
      }
      cam->SetWindowCenter(x * 2 - this->Magnification * (1 - windowCenter[0]) + 1,
        y * 2 - this->Magnification * (1 - windowCenter[1]) + 1);
      // shift 2D actors to correct origin for this tile
      this->Shift2DActors(size[0] * x, size[1] * y);

      // update gradient background colors for this tile
      if (gradientBackground)
      {
        double t1 = y / ySize;
        double t2 = (y + 1) / ySize;
        double tileBackground1[3] = { (1.0 - t1) * background1[0] + t1 * background2[0],
          (1.0 - t1) * background1[1] + t1 * background2[1],
          (1.0 - t1) * background1[2] + t1 * background2[2] };
        double tileBackground2[3] = { (1.0 - t2) * background1[0] + t2 * background2[0],
          (1.0 - t2) * background1[1] + t2 * background2[1],
          (1.0 - t2) * background1[2] + t2 * background2[2] };

        this->Input->SetBackground(tileBackground1);
        this->Input->SetBackground2(tileBackground2);
      }

      // Render
      this->Input->GetRenderWindow()->Render();
      pixels =
        this->Input->GetRenderWindow()->GetPixelData(0, 0, size[0] - 1, size[1] - 1, !doublebuffer);

      // now stuff the pixels into the data row by row
      colStart = inExtent[0] - x * size[0];
      if (colStart < 0)
      {
        colStart = 0;
      }
      colEnd = size[0] - 1;
      if (colEnd > (inExtent[1] - x * size[0]))
      {
        colEnd = inExtent[1] - x * size[0];
      }
      rowSize = colEnd - colStart + 1;

      // get the output pointer and do arith on it if necessary
      outPtr = (unsigned char*)data->GetScalarPointer(inExtent[0], inExtent[2], 0);
      outPtr =
        outPtr + (x * size[0] - inExtent[0]) * inIncr[0] + (y * size[1] - inExtent[2]) * inIncr[1];

      rowStart = inExtent[2] - y * size[1];
      if (rowStart < 0)
      {
        rowStart = 0;
      }
      rowEnd = size[1] - 1;
      if (rowEnd > (inExtent[3] - y * size[1]))
      {
        rowEnd = (inExtent[3] - y * size[1]);
      }
      for (row = rowStart; row <= rowEnd; row++)
      {
        memcpy(outPtr + row * inIncr[1] + colStart * inIncr[0],
          pixels + row * size[0] * 3 + colStart * 3, rowSize * 3);
      }
      // free the memory
      delete[] pixels;
    }
  }

  // restore the state of the SwapBuffers bit before we mucked with it.
  if (doublebuffer && swapbuffers)
  {
    this->Input->GetRenderWindow()->SetSwapBuffers(swapbuffers);
  }

  cam->SetViewAngle(viewAngle);
  cam->SetParallelScale(parallelScale);
  cam->SetWindowCenter(windowCenter[0], windowCenter[1]);
  this->Restore2DActors();

  // restore original background colors
  if (gradientBackground)
  {
    this->Input->SetBackground(background1);
    this->Input->SetBackground2(background2);
  }
}
//------------------------------------------------------------------------------
int vtkRenderLargeImage::FillOutputPortInformation(int vtkNotUsed(port), vtkInformation* info)
{
  // now add our info
  info->Set(vtkDataObject::DATA_TYPE_NAME(), "vtkImageData");
  return 1;
}
//------------------------------------------------------------------------------
// This code is designed to handle multiple renders even though
// RenderLargeImage currently only handles one explicitly.
//------------------------------------------------------------------------------
void vtkRenderLargeImage::Rescale2DActors()
{
  vtkActor2D* actor;
  vtkProp* aProp;
  vtkRenderer* aren;
  vtkPropCollection* pc;
  vtkRendererCollection* rc;
  vtkCoordinate *c1, *c2;
  vtkCoordinate *n1, *n2;
  int *p1, *p2;
  double d1[3], d2[3];
  //
  rc = this->Input->GetRenderWindow()->GetRenderers();
  for (rc->InitTraversal(); (aren = rc->GetNextItem());)
  {
    pc = aren->GetViewProps();
    if (pc)
    {
      for (pc->InitTraversal(); (aProp = pc->GetNextProp());)
      {
        actor = vtkActor2D::SafeDownCast((aProp));
        if (actor)
        {
          // put the actor in our list for retrieval later
          this->StoredData->StoredActors->AddItem(actor);
          // Copy all existing coordinate stuff
          n1 = actor->GetPositionCoordinate();
          n2 = actor->GetPosition2Coordinate();
          c1 = vtkCoordinate::New();
          c2 = vtkCoordinate::New();
          c1->SetCoordinateSystem(n1->GetCoordinateSystem());
          c1->SetReferenceCoordinate(n1->GetReferenceCoordinate());
          c1->SetReferenceCoordinate(n1->GetReferenceCoordinate());
          c1->SetValue(n1->GetValue());
          c2->SetCoordinateSystem(n2->GetCoordinateSystem());
          c2->SetReferenceCoordinate(n2->GetReferenceCoordinate());
          c2->SetValue(n2->GetValue());
          this->StoredData->Coord1s->AddItem(c1);
          this->StoredData->Coord2s->AddItem(c2);
          c1->Delete();
          c2->Delete();
          // work out the position in new magnified pixels
          p1 = n1->GetComputedDisplayValue(aren);
          p2 = n2->GetComputedDisplayValue(aren);
          d1[0] = p1[0] * this->Magnification;
          d1[1] = p1[1] * this->Magnification;
          d1[2] = 0.0;
          d2[0] = p2[0] * this->Magnification;
          d2[1] = p2[1] * this->Magnification;
          d2[2] = 0.0;
          this->StoredData->Coords1.emplace_back(static_cast<int>(d1[0]), static_cast<int>(d1[1]));
          this->StoredData->Coords2.emplace_back(static_cast<int>(d2[0]), static_cast<int>(d2[1]));
          // Make sure they have no dodgy offsets
          n1->SetCoordinateSystemToDisplay();
          n2->SetCoordinateSystemToDisplay();
          n1->SetReferenceCoordinate(nullptr);
          n2->SetReferenceCoordinate(nullptr);
          n1->SetValue(d1[0], d1[1]);
          n2->SetValue(d2[0], d2[1]);
          //
        }
      }
    }
  }
}
//------------------------------------------------------------------------------
// On each tile we must subtract the origin of each actor to ensure
// it appears in the correct relative location
void vtkRenderLargeImage::Shift2DActors(int x, int y)
{
  vtkActor2D* actor;
  vtkCoordinate *c1, *c2;
  double d1[3], d2[3];
  int i;
  //
  for (this->StoredData->StoredActors->InitTraversal(), i = 0;
       (actor = this->StoredData->StoredActors->GetNextItem()); i++)
  {
    c1 = actor->GetPositionCoordinate();
    c2 = actor->GetPosition2Coordinate();
    c1->GetValue(d1);
    c2->GetValue(d2);
    d1[0] = this->StoredData->Coords1[i].first - x;
    d1[1] = this->StoredData->Coords1[i].second - y;
    d2[0] = this->StoredData->Coords2[i].first - x;
    d2[1] = this->StoredData->Coords2[i].second - y;
    c1->SetValue(d1);
    c2->SetValue(d2);
  }
}
//------------------------------------------------------------------------------
// On each tile we must subtract the origin of each actor to ensure
// it appears in the correct relative location
void vtkRenderLargeImage::Restore2DActors()
{
  vtkActor2D* actor;
  vtkCoordinate *c1, *c2;
  vtkCoordinate *n1, *n2;
  int i;
  //
  for (this->StoredData->StoredActors->InitTraversal(), i = 0;
       (actor = this->StoredData->StoredActors->GetNextItem()); i++)
  {
    c1 = actor->GetPositionCoordinate();
    c2 = actor->GetPosition2Coordinate();
    n1 = vtkCoordinate::SafeDownCast(this->StoredData->Coord1s->GetItemAsObject(i));
    n2 = vtkCoordinate::SafeDownCast(this->StoredData->Coord2s->GetItemAsObject(i));
    c1->SetCoordinateSystem(n1->GetCoordinateSystem());
    c1->SetReferenceCoordinate(n1->GetReferenceCoordinate());
    c1->SetReferenceCoordinate(n1->GetReferenceCoordinate());
    c1->SetValue(n1->GetValue());
    c2->SetCoordinateSystem(n2->GetCoordinateSystem());
    c2->SetReferenceCoordinate(n2->GetReferenceCoordinate());
    c2->SetValue(n2->GetValue());
  }
  this->StoredData->Coord1s->RemoveAllItems();
  this->StoredData->Coord2s->RemoveAllItems();
  this->StoredData->StoredActors->RemoveAllItems();
}
//------------------------------------------------------------------------------
VTK_ABI_NAMESPACE_END
