//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt 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 "vtk/aeva/ext/vtkProportionalEditElements.h"

#include "vtkCellArray.h"
#include "vtkDoubleArray.h"
#include "vtkFloatArray.h"
#include "vtkIdTypeArray.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkLineSource.h"
#include "vtkMath.h"
#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkSphereSource.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkTransform.h"
#include "vtkTubeFilter.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

vtkStandardNewMacro(vtkProportionalEditElements);

vtkProportionalEditElements::vtkProportionalEditElements(int res)
  : AnchorPoint{ 0, 0, 0 }
  , DisplacementPoint{ 0, 0, 1 }
  , ProjectionPoint{ 0, 1, 0 }
  , Resolution(res < 3 ? 3 : res)
{
  this->SetNumberOfInputPorts(0);
  this->SetNumberOfOutputPorts(NumberOfOutputs);
}

vtkProportionalEditElements::~vtkProportionalEditElements() = default;

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

  os << indent << "AnchorPoint: (" << this->AnchorPoint[0] << ", " << this->AnchorPoint[1] << ", "
     << this->AnchorPoint[2] << ")\n";
  os << indent << "InfluenceRadius: " << this->InfluenceRadius << "\n";
  os << indent << "DisplacementPoint: (" << this->DisplacementPoint[0] << ", "
     << this->DisplacementPoint[1] << ", " << this->DisplacementPoint[2] << ")\n";
  os << indent << "ProjectionPoint: (" << this->ProjectionPoint[0] << ", "
     << this->ProjectionPoint[1] << ", " << this->ProjectionPoint[2] << ")\n";
  os << indent << "Resolution: " << this->Resolution << "\n";
  os << indent << "Output Points Precision: " << this->OutputPointsPrecision << "\n";
}

int vtkProportionalEditElements::RequestData(vtkInformation* /*request*/,
  vtkInformationVector** /*inputVector*/,
  vtkInformationVector* outputVector)
{
  // vtkInformation* outInfo = outputVector->GetInformationObject(0);
  vtkPolyData* dispAxis = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::Axis));
  vtkPolyData* sphere = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::Sphere));
  vtkPolyData* cylinder =
    vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::Cylinder));
  vtkPolyData* botVert =
    vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::AnchorVertex));
  vtkPolyData* topVert =
    vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::DisplacementVertex));
  vtkPolyData* projVert =
    vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::ProjectionVertex));

  if (!dispAxis || !sphere || !cylinder || !botVert || !topVert || !projVert)
  {
    vtkErrorMacro("No output provided.");
    return 0;
  }
  dispAxis->Initialize();
  // sphere->Initialize();
  // cylinder->Initialize();
  botVert->Initialize();
  topVert->Initialize();
  projVert->Initialize();

  vtkVector3d p0(this->AnchorPoint);
  vtkVector3d p1(this->DisplacementPoint);
  vtkVector3d p2(this->ProjectionPoint);

  double length = (p1 - p0).Norm();
  bool degenerate = (length <= 0.0);
  if (degenerate)
  {
    vtkErrorMacro("Axis requested is degenerate. The length (" << length << ") is not positive");
    return 0;
  }
  length = (p2 - p0).Norm();
  degenerate = (length <= 0.0);
  if (degenerate)
  {
    vtkErrorMacro(
      "Projection requested is degenerate. The length (" << length << ") is not positive");
    return 0;
  }

  vtkDebugMacro("vtkProportionalEditElements Executing");

  vtkVector3d axis = (p1 - p0).Normalized();
  vtkVector3d px(1., 0., 0.);
  vtkVector3d py(0., 1., 0.);

  // I. Compute the point coordinates.
  vtkNew<vtkPoints> axisPoints;
  axisPoints->SetDataType(
    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
  axisPoints->Allocate(2);
  axisPoints->InsertNextPoint(p0.GetData());
  axisPoints->InsertNextPoint(p1.GetData());

  vtkNew<vtkPoints> botPoint;
  botPoint->SetDataType(
    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
  botPoint->Allocate(1);
  botPoint->InsertNextPoint(p0.GetData());

  vtkNew<vtkPoints> topPoint;
  topPoint->SetDataType(
    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
  topPoint->Allocate(1);
  topPoint->InsertNextPoint(p1.GetData());

  vtkNew<vtkPoints> projPoint;
  topPoint->SetDataType(
    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
  projPoint->Allocate(1);
  projPoint->InsertNextPoint(p2.GetData());

  // II. Prepare face connectivity
  vtkNew<vtkCellArray> axisLines;
  vtkNew<vtkCellArray> botVertices;
  vtkNew<vtkCellArray> topVertices;
  vtkNew<vtkCellArray> projVertices;
  axisLines->Allocate(3);

  vtkNew<vtkIdTypeArray> axsEdgeConn;
  axsEdgeConn->SetNumberOfValues(3);
  axsEdgeConn->SetValue(0, 2);
  axsEdgeConn->SetValue(1, 0);
  axsEdgeConn->SetValue(2, 1);
  axisLines->SetCells(1, axsEdgeConn);

  // Update the output
  dispAxis->SetPoints(axisPoints);
  dispAxis->SetLines(axisLines);

  vtkIdType vertId = 0;
  botVert->SetPoints(botPoint);
  botVert->SetVerts(botVertices);
  botVert->InsertNextCell(VTK_VERTEX, 1, &vertId);

  topVert->SetPoints(topPoint);
  topVert->SetVerts(topVertices);
  topVert->InsertNextCell(VTK_VERTEX, 1, &vertId);

  projVert->SetPoints(projPoint);
  projVert->SetVerts(projVertices);
  projVert->InsertNextCell(VTK_VERTEX, 1, &vertId);

  this->SphereSrc->SetThetaResolution(this->Resolution);
  this->SphereSrc->SetPhiResolution(std::max(3, this->Resolution / 2));
  this->SphereSrc->SetRadius(this->InfluenceRadius);
  this->SphereSrc->SetCenter(this->AnchorPoint);
  this->SphereSrc->Update();
  sphere->ShallowCopy(this->SphereSrc->GetOutput(0));

  vtkSmartPointer<vtkLineSource> lineSource = vtkSmartPointer<vtkLineSource>::New();
  lineSource->SetPoint1(this->ProjectionPoint);
  lineSource->SetPoint2((p0 + p0 - p2).GetData());
  this->CylinderSrc->SetInputConnection(lineSource->GetOutputPort());
  this->CylinderSrc->SetRadius(this->InfluenceRadius);
  this->CylinderSrc->SetNumberOfSides(std::max(6, this->Resolution));
  this->CylinderSrc->Update();
  cylinder->ShallowCopy(this->CylinderSrc->GetOutput(0));

  return 1;
}
