/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/
//-- CamiTK includes
#include <MeshComponent.h>
#include <Application.h>
#include "Decimation.h"
#include <Property.h>

//-- Vtk Stuff
// disable warning generated by clang about the surrounded headers
#include <CamiTKDisableWarnings>
#include <vtkPolyDataWriter.h>
#include <CamiTKReEnableWarnings>

#include <vtkSmartPointer.h>
#include <vtkCallbackCommand.h>
#include <vtkDecimatePro.h>
#include <vtkCleanPolyData.h>

using namespace camitk;

// -------------------- Decimation constructor --------------------
Decimation::Decimation(ActionExtension* extension) : Action(extension) {
    setName("Decimation");
    setDescription(tr("This action reduce the number of triangles in a triangle mesh, forming a good approximation to the original geometry. <br/><br /> The algorithm proceeds as follows. Each vertex in the mesh is classified and inserted into a priority queue. The priority is based on the error to delete the vertex and retriangulate the hole. Vertices that cannot be deleted or triangulated (at this point in the algorithm) are skipped. Then, each vertex in the priority queue is processed (i.e., deleted followed by hole triangulation using edge collapse). This continues until the priority queue is empty. Next, all remaining vertices are processed, and the mesh is split into separate pieces along sharp edges or at non-manifold attachment points and reinserted into the priority queue. Again, the priority queue is processed until empty. If the desired reduction is still not achieved, the remaining vertices are split as necessary (in a recursive fashion) so that it is possible to eliminate every triangle as necessary. <br/><br />To use this object, at a minimum you need to specify the parameter <b>Decimation percentage</b>. The algorithm is guaranteed to generate a reduced mesh at this level as long as the following four conditions are met:<ul> \
                   <li>topology modification is allowed (i.e., the parameter <b>Preserve topology</b> is false); </li>\
                   <li>mesh splitting is enabled (i.e., the parameter <b>Splitting</b> is true); </li>\
                   <li>the algorithm is allowed to modify the boundaries of the mesh (i.e., the parameter <b>Boundary vertex deletion?</b> is true); </li>\
                   <li>the maximum allowable error (i.e., the parameter <b>Maximum error</b>) is set to <i>1.0e+38f</i> (default value). </ul> \
            Other important parameters to adjust are the <b>Feature angle</b> and <b>Split angle</b> parameters, since these can impact the quality of the final mesh."));

    setComponentClassName("MeshComponent");
    setFamily("Mesh Processing");
    addTag(tr("Decimation"));
    addTag(tr("Simplify"));

    // lazy instantiation
    actionWidget = nullptr;

    // decimation properties
    Property* percentageOfReductionProperty = new Property(tr("Decimation percentage"), 90, tr("Specify the desired reduction in the total number of polygons. <br/>Because of various constraints, this level of reduction may not be realized. If you want to guarantee a particular reduction, you must turn off <b>Preserve topology?</b> and <b>Boundary vertex deletion?</b>, turn on <b>Split mesh?</b>, and set the <b>Maximum error</b> to its maximum value (these parameters are initialized this way by default). "), "");
    percentageOfReductionProperty->setAttribute("minimum", 1);
    percentageOfReductionProperty->setAttribute("maximum", 100);
    percentageOfReductionProperty->setAttribute("singleStep", 1);
    addParameter(percentageOfReductionProperty);

    Property* preserveTopologyProperty = new Property(tr("Preserve topology?"), false, tr("Turn on/off whether to preserve the topology of the original mesh. <br/>If on, mesh splitting and hole elimination will not occur. This may limit the maximum reduction that may be achieved. "), "");
    addParameter(preserveTopologyProperty);

    Property* maxErrorProperty = new Property(tr("Maximum error"), 1e+38, tr("Set the largest decimation error that is allowed during the decimation process.<br/> This may limit the maximum reduction that may be achieved. The maximum error is specified as a fraction of the maximum length of the input data bounding box."), "");
    maxErrorProperty->setAttribute("minimum", 0);
    maxErrorProperty->setAttribute("maximum", 1e+38);
    maxErrorProperty->setAttribute("singleStep", 1);
    addParameter(maxErrorProperty);

    Property* featureAngleProperty = new Property(tr("Feature angle"), 15.0, tr("Specify the mesh feature angle. <br/>This angle is used to define what an edge is (i.e., if the surface normal between two adjacent triangles is >= FeatureAngle, an edge exists). "), "Degrees");
    featureAngleProperty->setAttribute("minimum", 0.0);
    featureAngleProperty->setAttribute("maximum", 359.0);
    featureAngleProperty->setAttribute("singleStep", 0.1);
    addParameter(featureAngleProperty);

    Property* splitMeshProperty = new Property(tr("Split edge?"), true, tr("Turn on/off the splitting of the mesh at corners, along edges, at non-manifold points, or anywhere else a split is required. <br/>Turning splitting off will better preserve the original topology of the mesh, but you may not obtain the requested reduction. "), "");
    addParameter(splitMeshProperty);

    Property* splitAngleProperty = new Property(tr("Split angle"), 75.0, tr("Specify the mesh split angle. This angle is used to control the splitting of the mesh. <br/>A split line exists when the surface normals between two edge connected triangles are >= SplitAngle. "), "Degrees");
    splitAngleProperty->setAttribute("minimum", 0.0);
    splitAngleProperty->setAttribute("maximum", 359.0);
    splitAngleProperty->setAttribute("singleStep", 0.1);
    addParameter(splitAngleProperty);

    Property* boundaryDeletionProperty = new Property(tr("Boundary vertex deletion?"), true, tr("Turn on/off the deletion of vertices on the boundary of a mesh. <br/>This may limit the maximum reduction that may be achieved."), "");
    addParameter(boundaryDeletionProperty);

    Property* degreeProperty = new Property(tr("Max triangles for 1 vertex"), 0, tr("If the number of triangles connected to a vertex exceeds <b>Max triangles for 1 vertex</b>, then the vertex will be split. <br/><br/>NOTE: the complexity of the triangulation algorithm is proportional to Degree^2. <br/>Setting degree small can improve the performance of the algorithm."), "");
    degreeProperty->setAttribute("minimum", 0);
    degreeProperty->setAttribute("singleStep", 1);
    addParameter(degreeProperty);

    Property* inflectionRatioProperty = new Property(tr("Inflection ratio"), 10.0, tr("Specify the inflection point ratio. <br/>An inflection point occurs when the ratio of reduction error between two iterations is greater than or equal to the InflectionPointRatio. "), "");
    inflectionRatioProperty->setAttribute("minimum", 0.0);
    inflectionRatioProperty->setAttribute("singleStep", 0.1);
    addParameter(inflectionRatioProperty);
}

// -------------------- apply --------------------
Action::ApplyStatus Decimation::apply() {
    // set waiting cursor and status bar
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    Application::showStatusBarMessage(tr("Performing Decimation"));
    Application::resetProgressBar();

    MeshComponent* targetMesh = dynamic_cast<MeshComponent*>(getTargets().last());

    vtkSmartPointer<vtkDecimatePro> mcDecimate = vtkSmartPointer<vtkDecimatePro>::New();

    // get back action parameters
    mcDecimate->SetInputData(targetMesh->getPointSet());
    mcDecimate->SetTargetReduction(getParameterValue("Decimation percentage").toDouble() / 100.0);
    mcDecimate->SetPreserveTopology(getParameterValue("Preserve topology?").toBool());
    mcDecimate->SetMaximumError(getParameterValue("Maximum error").toDouble());
    mcDecimate->SetFeatureAngle(getParameterValue("Feature angle").toDouble());
    mcDecimate->SetSplitting(getParameterValue("Split edge?").toBool());
    mcDecimate->SetSplitAngle(getParameterValue("Split angle").toDouble());
    mcDecimate->SetBoundaryVertexDeletion(getParameterValue("Boundary vertex deletion?").toBool());
    mcDecimate->SetDegree(getParameterValue("Max triangles for 1 vertex").toInt());
    mcDecimate->SetInflectionPointRatio(getParameterValue("Inflection ratio").toDouble());

    vtkSmartPointer<vtkCallbackCommand> progressCallback = vtkSmartPointer<vtkCallbackCommand>::New();
    progressCallback->SetCallback(&Application::vtkProgressFunction);
    mcDecimate->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    mcDecimate->Update();

    // clean
    vtkSmartPointer<vtkCleanPolyData> cleaner = vtkSmartPointer<vtkCleanPolyData>::New();
    cleaner->SetInputData(mcDecimate->GetOutput());
    cleaner->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    cleaner->Update();

    // Create a mesh Component
    vtkSmartPointer<vtkPointSet> resultPointSet = cleaner->GetOutput();
    new MeshComponent(resultPointSet, targetMesh->getName() + "_decimated");
    refreshApplication();

    Application::resetProgressBar();
    QApplication::restoreOverrideCursor();
    Application::showStatusBarMessage("");

    return SUCCESS;
}
