/*****************************************************************************
 * $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 "FillWithPoints.h"
#include <Application.h>
#include <MeshComponent.h>
#include <Property.h>
#include <Log.h>

// Vtk includes
// disable warning generated by clang about the surrounded headers
#include <CamiTKDisableWarnings>
#include <vtkSelectEnclosedPoints.h>
#include <CamiTKReEnableWarnings>

#include <VtkMeshUtil.h>
#include <vtkPointLocator.h>
#include <vtkCallbackCommand.h>
#include <vtkPolyData.h>
#include <vtkAppendPolyData.h>
#include <vtkPolyVertex.h>

// Qt includes
#include <QDateTime>
#include <QRandomGenerator>

using namespace camitk;

// -------------------- FillWithPoints --------------------
FillWithPoints::FillWithPoints(ActionExtension* extension) : Action(extension) {
    setName("Fill With Points");
    setDescription(tr("Fill a surfacic mesh with regularly spaced nodes by creating new nodes inside the mesh."));

    setComponentClassName("MeshComponent");
    setFamily("Mesh Processing");
    addTag(tr("Add Nodes"));

    Property* bucketSizeProperty = new Property(tr("Points per bucket"), 1, tr("The number of points per bucket. <br />A bucket represent a group of points in the input mesh.<br /> The less this number is, the higher the output mesh points density will be."), "Number of points");
    bucketSizeProperty->setAttribute("minimum", 1);
    bucketSizeProperty->setAttribute("singleStep", 1);
    addParameter(bucketSizeProperty);

    Property* randomizeProperty = new Property(tr("Randomize?"), true, tr("Randomize the position of the added points by +/- 0.5"), "");
    addParameter(randomizeProperty);
}

// --------------- apply -------------------
Action::ApplyStatus FillWithPoints::apply() {
    Action::ApplyStatus status = SUCCESS;

    // set waiting cursor and status bar
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    Application::showStatusBarMessage("Filling With Nodes...");
    Application::resetProgressBar();
    vtkSmartPointer<vtkCallbackCommand> progressCallback = vtkSmartPointer<vtkCallbackCommand>::New();
    progressCallback->SetCallback(&Application::vtkProgressFunction);

    // use the last target
    MeshComponent* targetMesh = dynamic_cast<MeshComponent*>(getTargets().last());

    //-- check if this is a polydata and if this is an unstructured grid, extract the surface
    vtkSmartPointer<vtkPolyData> targetPolyData = VtkMeshUtil::vtkPointSetToVtkPolyData(targetMesh->getPointSet());

    //-- computes the subdivision depending on the mesh resolution
    vtkSmartPointer<vtkPointLocator> subdivisionLocator = vtkSmartPointer<vtkPointLocator>::New();
    subdivisionLocator->SetDataSet(targetPolyData);
    subdivisionLocator->SetNumberOfPointsPerBucket(getParameterValue("Points per bucket").toInt());
    subdivisionLocator->BuildLocator();
    // number of subdivisions for the x, y and z axis. If the mesh density is high the nbDiv will be high
    // nbDiv is computed so that there is bucketSize nodes per division
    int nbDiv[3];
    subdivisionLocator->GetDivisions(nbDiv);

    // length of the divisions in the x-axis, y-axis and z-axis
    double* bounds = targetPolyData->GetPoints()->GetBounds(); // [xmin,xmax, ymin,ymax, zmin,zmax]
    double xDiv = (bounds[1] - bounds[0]) / nbDiv[0];
    double yDiv = (bounds[3] - bounds[2]) / nbDiv[1];
    double zDiv = (bounds[5] - bounds[4]) / nbDiv[2];

    //-- generates a regular grid in the bounding box
    vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();

    // position for generated points
    double x, y, z;

    for (int i = 0; i <= nbDiv[0]; i++) {
        x = bounds[0] + i * xDiv;

        for (int j = 0; j <= nbDiv[1]; j++) {
            y = bounds[2] + j * yDiv;

            for (int k = 0; k <= nbDiv[2]; k++) {
                z = bounds[4] + k * zDiv;
                // randomize
                if (getParameterValue("Randomize?").toBool()) {
                    x += (xDiv / 10.0) * (0.5 - QRandomGenerator::global()->generateDouble());
                    y += (yDiv / 10.0) * (0.5 - QRandomGenerator::global()->generateDouble());
                    z += (zDiv / 10.0) * (0.5 - QRandomGenerator::global()->generateDouble());
                }
                points->InsertNextPoint(x, y, z);
            }
        }
    }

    //-- select internal points only
    vtkSmartPointer<vtkPolyData> gridPoints = vtkSmartPointer<vtkPolyData>::New();
    gridPoints->Allocate();
    gridPoints->SetPoints(points);
    vtkSmartPointer<vtkSelectEnclosedPoints> select = vtkSmartPointer<vtkSelectEnclosedPoints>::New();
    select->SetInputData(gridPoints);
    select->SetSurfaceData(targetPolyData);
    select->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    select->Update();

    //-- create a new polydata that contains only the internal selected points
    vtkSmartPointer<vtkPoints> insidePointsPoints = vtkSmartPointer<vtkPoints>::New();
    for (vtkIdType i = 0; i < gridPoints->GetPoints()->GetNumberOfPoints(); i++) {
        if (select->IsInside(i)) {
            insidePointsPoints->InsertNextPoint(gridPoints->GetPoints()->GetPoint(i));
        }
    }

    if (insidePointsPoints->GetNumberOfPoints() > 0) {
        // create a poly vertex cell with all the inside points
        vtkSmartPointer<vtkPolyVertex> polyVertex = vtkSmartPointer<vtkPolyVertex>::New();
        polyVertex->GetPointIds()->SetNumberOfIds(insidePointsPoints->GetNumberOfPoints());
        for (vtkIdType i = 0; i < insidePointsPoints->GetNumberOfPoints(); i++) {
            polyVertex->GetPointIds()->SetId(i, i);
        }
        // create the polydata with this cell
        vtkSmartPointer<vtkPolyData> insidePoints = vtkSmartPointer<vtkPolyData>::New();
        insidePoints->Allocate(1);
        insidePoints->SetPoints(insidePointsPoints);
        insidePoints->InsertNextCell(polyVertex->GetCellType(), polyVertex->GetPointIds());
        insidePoints->AddObserver(vtkCommand::ProgressEvent, progressCallback);

        //-- Generates one structure with all the points (append targetPolyData and insidePoints)
        vtkSmartPointer<vtkAppendPolyData> appender = vtkSmartPointer<vtkAppendPolyData>::New();
        appender->AddInputData(targetPolyData);
        appender->AddInputData(insidePoints);
        appender->AddObserver(vtkCommand::ProgressEvent, progressCallback);
        appender->Update();

        //-- Create a new mesh Component using an unstructured grid (to show the isolated nodes)
        vtkSmartPointer<vtkPointSet> resultPointSet = appender->GetOutput();
        new MeshComponent(resultPointSet, targetMesh->getName() + " filled");
    }
    else {
        CAMITK_WARNING(tr("No points can be added inside this mesh"));
        status = ABORTED;
    }

    refreshApplication();

    // restore the normal cursor and progress bar
    Application::resetProgressBar();
    QApplication::restoreOverrideCursor();
    return status;
}
