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

#include "vtkCardinalSpline.h"
#include "vtkDataArray.h"
#include "vtkImageData.h"
#include "vtkImageStencilData.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkPoints.h"
#include "vtkSmartPointer.h"
#include "vtkStreamingDemandDrivenPipeline.h"

#include <algorithm>
#include <cmath>
#include <map>

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkLassoStencilSource);
vtkCxxSetObjectMacro(vtkLassoStencilSource, Points, vtkPoints);

//------------------------------------------------------------------------------
class vtkLSSPointMap : public std::map<int, vtkSmartPointer<vtkPoints>>
{
};

//------------------------------------------------------------------------------
vtkLassoStencilSource::vtkLassoStencilSource()
{
  this->SetNumberOfInputPorts(0);

  this->Shape = vtkLassoStencilSource::POLYGON;
  this->SliceOrientation = 2;
  this->Points = nullptr;
  this->SplineX = vtkCardinalSpline::New();
  this->SplineY = vtkCardinalSpline::New();

  this->PointMap = new vtkLSSPointMap();
}

//------------------------------------------------------------------------------
vtkLassoStencilSource::~vtkLassoStencilSource()
{
  this->SetPoints(nullptr);
  if (this->SplineX)
  {
    this->SplineX->Delete();
    this->SplineX = nullptr;
  }
  if (this->SplineY)
  {
    this->SplineY->Delete();
    this->SplineY = nullptr;
  }
  delete this->PointMap;
  this->PointMap = nullptr;
}

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

  os << indent << "Shape: " << this->GetShapeAsString() << "\n";
  os << indent << "Points: " << this->Points << "\n";
  os << indent << "SliceOrientation: " << this->GetSliceOrientation() << "\n";
  os << indent << "SlicePoints: " << this->PointMap->size() << "\n";
}

//------------------------------------------------------------------------------
const char* vtkLassoStencilSource::GetShapeAsString()
{
  switch (this->Shape)
  {
    case vtkLassoStencilSource::POLYGON:
      return "Polygon";
    case vtkLassoStencilSource::SPLINE:
      return "Spline";
  }
  return "";
}

//------------------------------------------------------------------------------
vtkMTimeType vtkLassoStencilSource::GetMTime()
{
  vtkMTimeType mTime = this->vtkImageStencilSource::GetMTime();

  if (this->Points != nullptr)
  {
    mTime = std::max(this->Points->GetMTime(), mTime);
  }

  if (!this->PointMap->empty())
  {
    vtkLSSPointMap::iterator iter = this->PointMap->begin();
    while (iter != this->PointMap->end())
    {
      mTime = std::max(iter->second->GetMTime(), mTime);
      ++iter;
    }
  }

  return mTime;
}

//------------------------------------------------------------------------------
void vtkLassoStencilSource::SetSlicePoints(int i, vtkPoints* points)
{
  vtkLSSPointMap::iterator iter = this->PointMap->find(i);
  if (iter != this->PointMap->end())
  {
    if (iter->second == points)
    {
      return;
    }
    else if (points == nullptr)
    {
      this->PointMap->erase(iter);
    }
    else
    {
      iter->second = points;
    }
  }
  else
  {
    if (points == nullptr)
    {
      return;
    }
    else
    {
      this->PointMap->insert(iter, vtkLSSPointMap::value_type(i, points));
    }
  }

  this->Modified();
}

//------------------------------------------------------------------------------
void vtkLassoStencilSource::RemoveAllSlicePoints()
{
  this->PointMap->clear();
}

//------------------------------------------------------------------------------
vtkPoints* vtkLassoStencilSource::GetSlicePoints(int i)
{
  vtkLSSPointMap::iterator iter = this->PointMap->find(i);
  if (iter != this->PointMap->end())
  {
    return iter->second;
  }
  return nullptr;
}

//------------------------------------------------------------------------------
// tolerance for stencil operations

#define VTK_STENCIL_TOL 7.62939453125e-06

//------------------------------------------------------------------------------
// Compute a reduced extent based on the bounds of the shape.
static void vtkLassoStencilSourceSubExtent(vtkPoints* points, const double origin[3],
  const double spacing[3], const int extent[6], int subextent[6])
{
  double bounds[6];
  points->GetBounds(bounds);

  for (int i = 0; i < 3; i++)
  {
    double emin = (bounds[2 * i] - origin[i]) / spacing[i] - VTK_STENCIL_TOL;
    double emax = (bounds[2 * i + 1] - origin[i]) / spacing[i] + VTK_STENCIL_TOL;

    subextent[2 * i] = extent[2 * i];
    subextent[2 * i + 1] = extent[2 * i + 1];

    if (extent[2 * i] < emin)
    {
      subextent[2 * i] = VTK_INT_MAX;
      if (extent[2 * i + 1] >= emin)
      {
        subextent[2 * i] = vtkMath::Floor(emin) + 1;
      }
    }

    if (extent[2 * i + 1] > emax)
    {
      subextent[2 * i + 1] = VTK_INT_MIN;
      if (extent[2 * i] <= emax)
      {
        subextent[2 * i + 1] = vtkMath::Floor(emax);
      }
    }
  }
}

//------------------------------------------------------------------------------
// Rasterize a polygon into the stencil
static int vtkLassoStencilSourcePolygon(vtkPoints* points, vtkImageStencilData* data,
  vtkImageStencilRaster* raster, const int extent[6], const double origin[3],
  const double spacing[3], int xj, int yj)
{
  // get the bounds of the polygon
  int subextent[6];
  vtkLassoStencilSourceSubExtent(points, origin, spacing, extent, subextent);

  // allocate the raster
  raster->PrepareForNewData(&subextent[2 * yj]);

  // rasterize each line
  vtkIdType n = points->GetNumberOfPoints();
  double p[3];
  double p0[2], p1[2], p2[2], p3[2];

  points->GetPoint(n - 1, p);
  p0[0] = (p[xj] - origin[xj]) / spacing[xj];
  p0[1] = (p[yj] - origin[yj]) / spacing[yj];

  points->GetPoint(0, p);
  p1[0] = (p[xj] - origin[xj]) / spacing[xj];
  p1[1] = (p[yj] - origin[yj]) / spacing[yj];

  double dx = p1[0] - p0[0];
  double dy = p1[1] - p0[1];
  if (dx * dx + dy * dy <= VTK_STENCIL_TOL * VTK_STENCIL_TOL)
  {
    n -= 1;
    points->GetPoint(n - 1, p);
    p0[0] = (p[xj] - origin[xj]) / spacing[xj];
    p0[1] = (p[yj] - origin[yj]) / spacing[yj];
  }

  points->GetPoint(1, p);
  p2[0] = (p[xj] - origin[xj]) / spacing[xj];
  p2[1] = (p[yj] - origin[yj]) / spacing[yj];

  for (vtkIdType i = 0; i < n; i++)
  {
    points->GetPoint((i + 2) % n, p);
    p3[0] = (p[xj] - origin[xj]) / spacing[xj];
    p3[1] = (p[yj] - origin[yj]) / spacing[yj];

    raster->InsertLine(p1, p2);

    p0[0] = p1[0];
    p0[1] = p1[1];
    p1[0] = p2[0];
    p1[1] = p2[1];
    p2[0] = p3[0];
    p2[1] = p3[1];
  }

  raster->FillStencilData(data, extent, xj, yj);

  return 1;
}

//------------------------------------------------------------------------------
// Generate the splines for the given set of points.  The splines
// will be closed if the final point is equal to the first point.
// The parametric value for the resulting spline will be valid over
// the range [0, tmax] where the tmax value is returned by reference.
static void vtkLassoStencilSourceCreateSpline(vtkPoints* points, const double origin[3],
  const double spacing[3], int xj, int yj, vtkSpline* xspline, vtkSpline* yspline, double& tmax,
  double& dmax)
{
  // initialize the spline
  xspline->RemoveAllPoints();
  yspline->RemoveAllPoints();
  xspline->ClosedOff();
  yspline->ClosedOff();

  // get the number of points and the first/last point
  vtkIdType n = points->GetNumberOfPoints();
  double p[3];
  double p0[2], p1[2];

  points->GetPoint(n - 1, p);
  p0[0] = (p[xj] - origin[xj]) / spacing[xj];
  p0[1] = (p[yj] - origin[yj]) / spacing[yj];

  points->GetPoint(0, p);
  p1[0] = (p[xj] - origin[xj]) / spacing[xj];
  p1[1] = (p[yj] - origin[yj]) / spacing[yj];

  // factor between real distance and parametric distance
  double f = 1.0;
  // the length of the implicit segment for closed loops
  double lastd = 0;

  // aspect ratio
  double xf = 1.0;
  double yf = 1.0;
  if (spacing[xj] > spacing[yj])
  {
    xf = spacing[xj] / spacing[yj];
  }
  else
  {
    yf = spacing[yj] / spacing[xj];
  }

  // if first and last point are same, spline is closed
  double dx = (p1[0] - p0[0]) * xf;
  double dy = (p1[1] - p0[1]) * yf;
  double d2 = dx * dx + dy * dy;
  while (d2 <= VTK_STENCIL_TOL * VTK_STENCIL_TOL && n > 1)
  {
    n -= 1;
    points->GetPoint(n - 1, p);
    p0[0] = (p[xj] - origin[xj]) / spacing[xj];
    p0[1] = (p[yj] - origin[yj]) / spacing[yj];

    xspline->ClosedOn();
    yspline->ClosedOn();

    // vtkSpline considers the parametric length of the implicit
    // segment of closed loops to be unity, so set "f" so that
    // multiplying the real length of that segment by "f" gives unity.
    dx = (p1[0] - p0[0]) * xf;
    dy = (p1[1] - p0[1]) * yf;
    d2 = dx * dx + dy * dy;
    lastd = sqrt(d2);
    if (lastd > 0)
    {
      f = 1.0 / lastd;
    }
  }

  // Add all the points to the spline.
  double d = 0.0;
  for (vtkIdType i = 0; i < n; i++)
  {
    p0[0] = p1[0];
    p0[1] = p1[1];

    points->GetPoint(i, p);
    p1[0] = (p[xj] - origin[xj]) / spacing[xj];
    p1[1] = (p[yj] - origin[yj]) / spacing[yj];

    dx = (p1[0] - p0[0]) * xf;
    dy = (p1[1] - p0[1]) * yf;

    d += sqrt(dx * dx + dy * dy);

    double t = f * d;

    xspline->AddPoint(t, p1[0]);
    yspline->AddPoint(t, p1[1]);
  }

  // Do the spline precomputations
  xspline->Compute();
  yspline->Compute();

  // The spline is valid over t = [0, tmax]
  d += lastd;
  tmax = f * d;
  dmax = d;
}

//------------------------------------------------------------------------------
// Rasterize a spline contour into the stencil
static int vtkLassoStencilSourceSpline(vtkPoints* points, vtkImageStencilData* data,
  vtkImageStencilRaster* raster, const int extent[6], const double origin[3],
  const double spacing[3], int xj, int yj, vtkSpline* xspline, vtkSpline* yspline)
{
  // create the splines
  double tmax, dmax;
  vtkLassoStencilSourceCreateSpline(points, origin, spacing, xj, yj, xspline, yspline, tmax, dmax);

  if (dmax <= VTK_STENCIL_TOL)
  {
    return 1;
  }

  // get the bounds of the polygon as a first guess of the spline bounds
  int subextent[6];
  vtkLassoStencilSourceSubExtent(points, origin, spacing, extent, subextent);

  // allocate the raster
  raster->PrepareForNewData(&subextent[2 * yj]);

  // go around the spline
  vtkIdType n = vtkMath::Floor(dmax) + 1;
  double delta = tmax / n;

  double p1[2], p2[2], p3[2];

  double t = 0;
  p1[0] = xspline->Evaluate(t);
  p1[1] = yspline->Evaluate(t);

  t = delta;
  p2[0] = xspline->Evaluate(t);
  p2[1] = yspline->Evaluate(t);

  for (vtkIdType i = 0; i < n; i++)
  {
    t += delta;
    if (i == n - 2)
    {
      t = 0;
    }

    p3[0] = xspline->Evaluate(t);
    p3[1] = yspline->Evaluate(t);

    raster->InsertLine(p1, p2);

    p1[0] = p2[0];
    p1[1] = p2[1];
    p2[0] = p3[0];
    p2[1] = p3[1];
  }

  raster->FillStencilData(data, extent, xj, yj);

  return 1;
}

//------------------------------------------------------------------------------
static int vtkLassoStencilSourceExecute(vtkPoints* points, vtkImageStencilData* data,
  vtkImageStencilRaster* raster, int extent[6], double origin[3], double spacing[3], int shape,
  int xj, int yj, vtkSpline* xspline, vtkSpline* yspline)
{
  int result = 1;

  if (points == nullptr || points->GetNumberOfPoints() < 3)
  {
    return 1;
  }

  switch (shape)
  {
    case vtkLassoStencilSource::POLYGON:
      result = vtkLassoStencilSourcePolygon(points, data, raster, extent, origin, spacing, xj, yj);
      break;
    case vtkLassoStencilSource::SPLINE:
      result = vtkLassoStencilSourceSpline(
        points, data, raster, extent, origin, spacing, xj, yj, xspline, yspline);
      break;
  }

  return result;
}

//------------------------------------------------------------------------------
int vtkLassoStencilSource::RequestData(
  vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
{
  int extent[6];
  double origin[3];
  double spacing[3];
  int result = 1;

  this->Superclass::RequestData(request, inputVector, outputVector);

  vtkInformation* outInfo = outputVector->GetInformationObject(0);
  vtkImageStencilData* data =
    vtkImageStencilData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT()));

  outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_EXTENT(), extent);
  outInfo->Get(vtkDataObject::ORIGIN(), origin);
  outInfo->Get(vtkDataObject::SPACING(), spacing);

  int slabExtent[6];
  slabExtent[0] = extent[0];
  slabExtent[1] = extent[1];
  slabExtent[2] = extent[2];
  slabExtent[3] = extent[3];
  slabExtent[4] = extent[4];
  slabExtent[5] = extent[5];

  int xj = 0;
  int yj = 1;
  int zj = 2;

  if (this->SliceOrientation == 0)
  {
    xj = 1;
    yj = 2;
    zj = 0;
  }
  else if (this->SliceOrientation == 1)
  {
    xj = 0;
    yj = 2;
    zj = 1;
  }

  vtkImageStencilRaster raster(&extent[2 * yj]);
  raster.SetTolerance(VTK_STENCIL_TOL);

  int zmin = extent[2 * zj];
  int zmax = extent[2 * zj + 1];

  vtkLSSPointMap::iterator iter = this->PointMap->lower_bound(zmin);
  vtkLSSPointMap::iterator maxiter = this->PointMap->upper_bound(zmax);

  while (iter != maxiter && result != 0)
  {
    this->UpdateProgress((slabExtent[2 * zj] - zmin) * 1.0 / (zmax - zmin + 1));

    int i = iter->first;
    vtkPoints* points = iter->second;

    // fill in the slices with no SlicePoints
    if (this->Points && i > slabExtent[2 * zj])
    {
      slabExtent[2 * zj + 1] = i - 1;

      result = vtkLassoStencilSourceExecute(this->Points, data, &raster, slabExtent, origin,
        spacing, this->Shape, xj, yj, this->SplineX, this->SplineY);
    }

    // do the slice with its SlicePoints
    if (result)
    {
      slabExtent[2 * zj] = i;
      slabExtent[2 * zj + 1] = i;

      result = vtkLassoStencilSourceExecute(points, data, &raster, slabExtent, origin, spacing,
        this->Shape, xj, yj, this->SplineX, this->SplineY);

      slabExtent[2 * zj] = slabExtent[2 * zj + 1] + 1;
    }

    ++iter;
  }

  this->UpdateProgress((slabExtent[2 * zj] - zmin) * 1.0 / (zmax - zmin + 1));

  // fill in the rest
  if (result && slabExtent[2 * zj] <= zmax)
  {
    slabExtent[2 * zj + 1] = zmax;

    result = vtkLassoStencilSourceExecute(this->Points, data, &raster, slabExtent, origin, spacing,
      this->Shape, xj, yj, this->SplineX, this->SplineY);

    this->UpdateProgress(1.0);
  }

  return result;
}
VTK_ABI_NAMESPACE_END
