// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkFrustumSource.h"
#include "vtkCellArray.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkPlane.h"
#include "vtkPlanes.h"
#include <cassert>

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkFrustumSource);
vtkCxxSetObjectMacro(vtkFrustumSource, Planes, vtkPlanes);

//------------------------------------------------------------------------------
vtkFrustumSource::vtkFrustumSource()
{
  this->Planes = nullptr;
  this->ShowLines = true;
  this->LinesLength = 1.0;
  this->OutputPointsPrecision = vtkAlgorithm::SINGLE_PRECISION;

  // a source has no input port.
  this->SetNumberOfInputPorts(0);
}

//------------------------------------------------------------------------------
vtkFrustumSource::~vtkFrustumSource()
{
  if (this->Planes != nullptr)
  {
    this->Planes->Delete();
  }
}

//------------------------------------------------------------------------------
int vtkFrustumSource::RequestData(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  if (this->Planes == nullptr || this->Planes->GetNumberOfPlanes() != 6)
  {
    vtkErrorMacro(<< " 6 planes required.");
    return 0;
  }
  if (this->ShowLines)
  {
    if (this->LinesLength <= 0.0)
    {
      vtkErrorMacro(<< " LinesLength<=0.0");
      return 0;
    }
  }

  // get the info object
  vtkInformation* outInfo = outputVector->GetInformationObject(0);

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

  // Geometry
  vtkIdType nbPts = 8;

  bool leftRightNull = false;
  bool bottomTopNull = false;
  bool parallelFrustum = false;

  double n0[3];
  double n1[3];
  double c[3];

  // angle between left and right planes
  this->Planes->GetPlane(0)->GetNormal(n0);
  this->Planes->GetPlane(1)->GetNormal(n1);

  vtkMath::Normalize(n0);
  vtkMath::Normalize(n1);
  vtkMath::Dot(n0, n1);

  vtkMath::Cross(n0, n1, c);
  vtkMath::Norm(c);

  // angle between bottom and top planes
  this->Planes->GetPlane(2)->GetNormal(n0);
  this->Planes->GetPlane(3)->GetNormal(n1);

  vtkMath::Normalize(n0);
  vtkMath::Normalize(n1);
  vtkMath::Dot(n0, n1);

  vtkMath::Cross(n0, n1, c);
  vtkMath::Norm(c);

  if (this->ShowLines)
  {
    double left[3];
    this->Planes->GetPlane(0)->GetNormal(left);
    double right[3];
    this->Planes->GetPlane(1)->GetNormal(right);
    double bottom[3];
    this->Planes->GetPlane(2)->GetNormal(bottom);
    double top[3];
    this->Planes->GetPlane(3)->GetNormal(top);

    double leftRight[3];
    vtkMath::Cross(left, right, leftRight);
    leftRightNull = leftRight[0] == 0.0 && leftRight[1] == 0.0 && leftRight[2] == 0.0;

    double bottomTop[3];
    vtkMath::Cross(bottom, top, bottomTop);
    bottomTopNull = bottomTop[0] == 0.0 && bottomTop[1] == 0.0 && bottomTop[2] == 0.0;
    parallelFrustum = leftRightNull && bottomTopNull;

    if (parallelFrustum)
    {
      // start at near points, just add the 4 extra far points.
      nbPts += 4;
    }
    else
    {
      if (leftRightNull || bottomTopNull)
      {
        // two extra starting points , and 4 extra far points.
        nbPts += 6;
      }
      else
      {
        // there is an apex, and 4 extra far points
        nbPts += 5;
      }
    }
    // parallel frustum = bottom//top && left//right
  }

  vtkPoints* 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->SetNumberOfPoints(nbPts);
  // Ref: Real-Time Rendering, 3rd edition, Thomas Akenine-Moller, Eric Haines,
  // Naty Hoffman, page 783, section 16.17,
  // "Intersection Between Three Planes"

  double pt[3];
  int planes[3];

  planes[0] = 0; // left
  planes[1] = 2; // bottom
  planes[2] = 5; // near 5
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(0, pt);

  planes[0] = 1;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(1, pt);

  planes[1] = 3;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(2, pt);

  planes[0] = 0;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(3, pt);

  planes[1] = 2;
  planes[2] = 4; // 4
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(4, pt);

  planes[0] = 1;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(5, pt);

  planes[1] = 3;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(6, pt);

  planes[0] = 0;
  this->ComputePoint(planes, pt);
  newPoints->SetPoint(7, pt);

  newPoints->Modified();

  // Topology
  vtkIdType numPolys = 6;
  vtkCellArray* newPolys = vtkCellArray::New();
  newPolys->AllocateEstimate(numPolys, 4);

  vtkIdType pts[4];

  // left
  pts[0] = 4;
  pts[1] = 0;
  pts[2] = 3;
  pts[3] = 7;
  newPolys->InsertNextCell(4, pts);

  // right
  pts[0] = 1;
  pts[1] = 5;
  pts[2] = 6;
  pts[3] = 2;
  newPolys->InsertNextCell(4, pts);

  // bottom
  pts[0] = 0;
  pts[1] = 4;
  pts[2] = 5;
  pts[3] = 1;
  newPolys->InsertNextCell(4, pts);

  // top
  pts[0] = 3;
  pts[1] = 2;
  pts[2] = 6;
  pts[3] = 7;
  newPolys->InsertNextCell(4, pts);

  // near
  pts[0] = 0;
  pts[1] = 1;
  pts[2] = 2;
  pts[3] = 3;
  newPolys->InsertNextCell(4, pts);

  // far
  pts[0] = 4;
  pts[1] = 7;
  pts[2] = 6;
  pts[3] = 5;
  newPolys->InsertNextCell(4, pts);

  vtkCellArray* newLines = nullptr;
  if (this->ShowLines)
  {
    vtkIdType numLines = 4;

    newLines = vtkCellArray::New();
    newLines->AllocateEstimate(numLines, 2);

    pts[0] = 12; // apex, or first of the two extra near points.

    // line from lower-left corner
    if (parallelFrustum)
    {
      pts[0] = 0;
    }
    pts[1] = 8;
    newLines->InsertNextCell(2, pts);

    // line from lower-right corner
    if (parallelFrustum)
    {
      ++pts[0];
    }
    else
    {
      if (leftRightNull)
      {
        pts[0] = 13;
      }
    }
    ++pts[1];
    newLines->InsertNextCell(2, pts);

    // line from upper-right corner
    if (parallelFrustum)
    {
      ++pts[0];
    }
    else
    {
      if (bottomTopNull)
      {
        pts[0] = 13;
      }
    }
    ++pts[1];
    newLines->InsertNextCell(2, pts);

    // line from upper-left corner
    if (parallelFrustum)
    {
      ++pts[0];
    }
    else
    {
      if (leftRightNull)
      {
        pts[0] = 12;
      }
    }
    ++pts[1];
    newLines->InsertNextCell(2, pts);
  }

  output->SetPoints(newPoints);
  newPoints->Delete();

  if (newLines != nullptr)
  {
    newLines->Squeeze(); // since we've estimated size; reclaim some space
    output->SetLines(newLines);
    newLines->Delete();
  }

  newPolys->Squeeze(); // since we've estimated size; reclaim some space
  output->SetPolys(newPolys);
  newPolys->Delete();

  return 1;
}

// Description:
// Compute the intersection of 3 planes.
void vtkFrustumSource::ComputePoint(int planes[3], double* pt)
{
  // Ref: Real-Time Rendering, 3rd edition, Thomas Akenine-Moller, Eric Haines,
  // Naty Hoffman, page 783, section 16.17,
  // "Intersection Between Three Planes"

  vtkPlane* plane = this->Planes->GetPlane(planes[0]);

  double n0[3];
  double p0[3];
  plane->GetNormal(n0);
  plane->GetOrigin(p0);

  this->Planes->GetPlane(planes[1]); // return the same pointer.

  double n1[3];
  double p1[3];
  plane->GetNormal(n1);
  plane->GetOrigin(p1);

  this->Planes->GetPlane(planes[2]); // return the same pointer.

  double n2[3];
  double p2[3];
  plane->GetNormal(n2);
  plane->GetOrigin(p2);

  double d0 = vtkMath::Dot(p0, n0);
  double d1 = vtkMath::Dot(p1, n1);
  double d2 = vtkMath::Dot(p2, n2);

  double c12[3];
  vtkMath::Cross(n1, n2, c12);
  double c20[3];
  vtkMath::Cross(n2, n0, c20);
  double c01[3];
  vtkMath::Cross(n0, n1, c01);

  double d = vtkMath::Determinant3x3(n0, n1, n2);

  int i = 0;
  while (i < 3)
  {
    pt[i] = (d0 * c12[i] + d1 * c20[i] + d2 * c01[i]) / d;
    ++i;
  }
}

//------------------------------------------------------------------------------
// Description:
// Modified GetMTime because of Planes.
vtkMTimeType vtkFrustumSource::GetMTime()
{
  vtkMTimeType mTime = this->Superclass::GetMTime();
  if (this->Planes != nullptr)
  {
    vtkMTimeType time;
    time = this->Planes->GetMTime();
    mTime = std::max(time, mTime);
  }
  return mTime;
}

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

  os << indent << "Planes:";
  if (this->Planes != nullptr)
  {
    this->Planes->PrintSelf(os, indent);
  }
  else
  {
    os << "(none)" << endl;
  }

  os << indent << "ShowLines:";
  if (this->ShowLines)
  {
    os << "true" << endl;
  }
  else
  {
    os << "false" << endl;
  }

  os << indent << "LinesLength:" << this->LinesLength << endl;
  os << indent << "Output Points Precision: " << this->OutputPointsPrecision << endl;
}
VTK_ABI_NAMESPACE_END
