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

#include "vtkCellArray.h"
#include "vtkFloatArray.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkPointData.h"
#include "vtkPoints.h"
#include "vtkPolyData.h"
#include "vtkStreamingDemandDrivenPipeline.h"

#include <cmath>

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkSphereSource);

//------------------------------------------------------------------------------
// Construct sphere with radius=0.5 and default resolution 8 in both Phi
// and Theta directions. Theta ranges from (0,360) and phi (0,180) degrees.
// Normals are generated.
vtkSphereSource::vtkSphereSource(int res)
{
  res = res < 4 ? 4 : res;
  this->Radius = 0.5;
  this->Center[0] = 0.0;
  this->Center[1] = 0.0;
  this->Center[2] = 0.0;

  this->ThetaResolution = res;
  this->PhiResolution = res;
  this->StartTheta = 0.0;
  this->EndTheta = 360.0;
  this->StartPhi = 0.0;
  this->EndPhi = 180.0;
  this->LatLongTessellation = 0;

  this->OutputPointsPrecision = vtkAlgorithm::SINGLE_PRECISION;

  this->GenerateNormals = true;

  this->SetNumberOfInputPorts(0);
}

//------------------------------------------------------------------------------
int vtkSphereSource::RequestData(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  // get the info object
  vtkInformation* outInfo = outputVector->GetInformationObject(0);

  // get the output
  vtkPolyData* output = vtkPolyData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT()));

  int i, j;
  int jStart, jEnd, numOffset;
  int numPts, numPolys;
  vtkPoints* newPoints;
  vtkFloatArray* newNormals = nullptr;
  vtkCellArray* newPolys;
  double x[3], n[3], deltaPhi, deltaTheta, phi, theta, radius, norm;
  double startTheta, endTheta, startPhi, endPhi;
  int base, numPoles = 0, thetaResolution, phiResolution;
  vtkIdType pts[4];
  int piece = outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_PIECE_NUMBER());
  int numPieces = outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_NUMBER_OF_PIECES());

  numPieces = std::min(numPieces, this->ThetaResolution);
  if (piece >= numPieces)
  {
    // Although the super class should take care of this,
    // it cannot hurt to check here.
    return 1;
  }

  // I want to modify the ivars resolution start theta and end theta,
  // so I will make local copies of them.  THese might be able to be merged
  // with the other copies of them, ...
  int localThetaResolution = this->ThetaResolution;
  double localStartTheta = this->StartTheta;
  double localEndTheta = this->EndTheta;

  while (localEndTheta < localStartTheta)
  {
    localEndTheta += 360.0;
  }
  deltaTheta = (localEndTheta - localStartTheta) / localThetaResolution;

  // Change the ivars based on pieces.
  int start, end;
  start = piece * localThetaResolution / numPieces;
  end = (piece + 1) * localThetaResolution / numPieces;
  localEndTheta = localStartTheta + (double)(end)*deltaTheta;
  localStartTheta = localStartTheta + (double)(start)*deltaTheta;
  localThetaResolution = end - start;

  // Set things up; allocate memory
  //
  vtkDebugMacro(
    "SphereSource Executing piece index " << piece << " of " << numPieces << " pieces.");

  numPts = this->PhiResolution * localThetaResolution + 2;
  // creating triangles
  numPolys = this->PhiResolution * 2 * localThetaResolution;

  newPoints = vtkPoints::New();

  // Set the desired precision for the points in the output.
  if (this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION)
  {
    newPoints->SetDataType(VTK_DOUBLE);
  }
  else
  {
    newPoints->SetDataType(VTK_FLOAT);
  }
  newPoints->Allocate(numPts);

  if (this->GenerateNormals)
  {
    newNormals = vtkFloatArray::New();
    newNormals->SetNumberOfComponents(3);
    newNormals->Allocate(3 * numPts);
    newNormals->SetName("Normals");
  }

  newPolys = vtkCellArray::New();
  newPolys->AllocateEstimate(numPolys, 3);

  // Create sphere
  //
  // Create north pole if needed
  if (this->StartPhi <= 0.0)
  {
    x[0] = this->Center[0];
    x[1] = this->Center[1];
    x[2] = this->Center[2] + this->Radius;
    newPoints->InsertPoint(numPoles, x);

    if (newNormals)
    {
      x[0] = x[1] = 0.0;
      x[2] = 1.0;
      newNormals->InsertTuple(numPoles, x);
    }
    numPoles++;
  }

  // Create south pole if needed
  if (this->EndPhi >= 180.0)
  {
    x[0] = this->Center[0];
    x[1] = this->Center[1];
    x[2] = this->Center[2] - this->Radius;
    newPoints->InsertPoint(numPoles, x);

    if (newNormals)
    {
      x[0] = x[1] = 0.0;
      x[2] = -1.0;
      newNormals->InsertTuple(numPoles, x);
    }
    numPoles++;
  }

  // Check data, determine increments, and convert to radians
  startTheta = (localStartTheta < localEndTheta ? localStartTheta : localEndTheta);
  startTheta *= vtkMath::Pi() / 180.0;
  endTheta = (localEndTheta > localStartTheta ? localEndTheta : localStartTheta);
  endTheta *= vtkMath::Pi() / 180.0;

  startPhi = (this->StartPhi < this->EndPhi ? this->StartPhi : this->EndPhi);
  startPhi *= vtkMath::Pi() / 180.0;
  endPhi = (this->EndPhi > this->StartPhi ? this->EndPhi : this->StartPhi);
  endPhi *= vtkMath::Pi() / 180.0;

  phiResolution = this->PhiResolution - numPoles;
  deltaPhi = (endPhi - startPhi) / (this->PhiResolution - 1);
  thetaResolution = localThetaResolution;
  if (fabs(localStartTheta - localEndTheta) < 360.0)
  {
    ++localThetaResolution;
  }
  deltaTheta = (endTheta - startTheta) / thetaResolution;

  jStart = (this->StartPhi <= 0.0 ? 1 : 0);
  jEnd = (this->EndPhi >= 180.0 ? this->PhiResolution - 1 : this->PhiResolution);

  this->UpdateProgress(0.1);

  // Create intermediate points
  for (i = 0; i < localThetaResolution; i++)
  {
    theta = localStartTheta * vtkMath::Pi() / 180.0 + i * deltaTheta;

    for (j = jStart; j < jEnd; j++)
    {
      phi = startPhi + j * deltaPhi;
      radius = this->Radius * sin(phi);
      n[0] = radius * cos(theta);
      n[1] = radius * sin(theta);
      n[2] = this->Radius * cos(phi);
      x[0] = n[0] + this->Center[0];
      x[1] = n[1] + this->Center[1];
      x[2] = n[2] + this->Center[2];
      newPoints->InsertNextPoint(x);

      if (newNormals)
      {
        if ((norm = vtkMath::Norm(n)) == 0.0)
        {
          norm = 1.0;
        }
        n[0] /= norm;
        n[1] /= norm;
        n[2] /= norm;
        newNormals->InsertNextTuple(n);
      }
    }
    this->UpdateProgress(0.10 + 0.50 * i / static_cast<float>(localThetaResolution));
  }

  // Generate mesh connectivity
  base = phiResolution * localThetaResolution;

  if (fabs(localStartTheta - localEndTheta) < 360.0)
  {
    --localThetaResolution;
  }

  if (this->StartPhi <= 0.0) // around north pole
  {
    for (i = 0; i < localThetaResolution; i++)
    {
      pts[0] = phiResolution * i + numPoles;
      pts[1] = (phiResolution * (i + 1) % base) + numPoles;
      pts[2] = 0;
      newPolys->InsertNextCell(3, pts);
    }
  }

  if (this->EndPhi >= 180.0) // around south pole
  {
    numOffset = phiResolution - 1 + numPoles;

    for (i = 0; i < localThetaResolution; i++)
    {
      pts[0] = phiResolution * i + numOffset;
      pts[2] = ((phiResolution * (i + 1)) % base) + numOffset;
      pts[1] = numPoles - 1;
      newPolys->InsertNextCell(3, pts);
    }
  }
  this->UpdateProgress(0.70);

  // bands in-between poles
  for (i = 0; i < localThetaResolution; i++)
  {
    for (j = 0; j < (phiResolution - 1); j++)
    {
      pts[0] = phiResolution * i + j + numPoles;
      pts[1] = pts[0] + 1;
      pts[2] = ((phiResolution * (i + 1) + j) % base) + numPoles + 1;
      if (!this->LatLongTessellation)
      {
        newPolys->InsertNextCell(3, pts);
        pts[1] = pts[2];
        pts[2] = pts[1] - 1;
        newPolys->InsertNextCell(3, pts);
      }
      else
      {
        pts[3] = pts[2] - 1;
        newPolys->InsertNextCell(4, pts);
      }
    }
    this->UpdateProgress(0.70 + 0.30 * i / static_cast<double>(localThetaResolution));
  }

  // Update ourselves and release memory
  //
  newPoints->Squeeze();
  output->SetPoints(newPoints);
  newPoints->Delete();

  if (newNormals)
  {
    newNormals->Squeeze();
    output->GetPointData()->SetNormals(newNormals);
    newNormals->Delete();
  }

  newPolys->Squeeze();
  output->SetPolys(newPolys);
  newPolys->Delete();

  return 1;
}

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

  os << indent << "Theta Resolution: " << this->ThetaResolution << "\n";
  os << indent << "Phi Resolution: " << this->PhiResolution << "\n";
  os << indent << "Theta Start: " << this->StartTheta << "\n";
  os << indent << "Phi Start: " << this->StartPhi << "\n";
  os << indent << "Theta End: " << this->EndTheta << "\n";
  os << indent << "Phi End: " << this->EndPhi << "\n";
  os << indent << "Radius: " << this->Radius << "\n";
  os << indent << "Center: (" << this->Center[0] << ", " << this->Center[1] << ", "
     << this->Center[2] << ")\n";
  os << indent << "LatLong Tessellation: " << this->LatLongTessellation << "\n";
  os << indent << "Output Points Precision: " << this->OutputPointsPrecision << "\n";
  os << indent << "Generate Normals: " << this->GenerateNormals << "\n";
}

//------------------------------------------------------------------------------
int vtkSphereSource::RequestInformation(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  // get the info object
  vtkInformation* outInfo = outputVector->GetInformationObject(0);

  outInfo->Set(CAN_HANDLE_PIECE_REQUEST(), 1);

  return 1;
}
VTK_ABI_NAMESPACE_END
