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

// Vtk includes
#include <vtkPointSet.h>
#include <vtkPolyDataNormals.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkPointData.h>
#include <vtkIdTypeArray.h>
#include <vtkCellLocator.h>
#include <vtkPolyDataConnectivityFilter.h>
#include <vtkUnstructuredGrid.h>
#include <vtkCallbackCommand.h>
#include <vtkFeatureEdges.h>

using namespace camitk;

//#include <vtkFeatureEdges.h>

// -------------------- WarpOut --------------------
WarpOut::WarpOut(ActionExtension* extension) : Action(extension) {
    // Setting name, description and input component
    setName("Warp Out");
    setDescription(tr("Move the outside points along the normal in order to thicken a volumic mesh (works only with closed- centered- mesh)."));
    setComponentClassName("MeshComponent");

    // Setting classification family and tags
    setFamily("Mesh Processing");
    addTag(tr("Grow"));
    addTag(tr("Thicken"));

    Property* displacementProperty = new Property(tr("Displacement"), 1.0, tr("The length of the displacement of the mesh toward its normals."), "");
    displacementProperty->setAttribute("minimum", 0.0);
    displacementProperty->setAttribute("singleStep", 0.1);
    addParameter(displacementProperty);

}


// --------------- apply -------------------
Action::ApplyStatus WarpOut::apply() {
    // set waiting cursor and status bar
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    Application::showStatusBarMessage("Warping out...");
    Application::resetProgressBar();
    vtkSmartPointer<vtkCallbackCommand> progressCallback = vtkSmartPointer<vtkCallbackCommand>::New();
    progressCallback->SetCallback(&Application::vtkProgressFunction);

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

    if (vtkUnstructuredGrid::SafeDownCast(targetMesh->getPointSet())) {
        resultPointSet = vtkSmartPointer<vtkUnstructuredGrid>::New();
        vtkUnstructuredGrid::SafeDownCast(resultPointSet)->Allocate();
    }
    else if (vtkPolyData::SafeDownCast(targetMesh->getPointSet())) {
        resultPointSet = vtkSmartPointer<vtkPolyData>::New();
        vtkPolyData::SafeDownCast(resultPointSet)->Allocate();
    }
    else {
        QApplication::restoreOverrideCursor();
        CAMITK_WARNING(tr("Invalid mesh: the selected component \"%1\" is not a vtkUnstructuredGrid nor a vtkPolyData. Action aborted.").arg(targetMesh->getName()))
        return ABORTED;
    }

    if (resultPointSet->GetNumberOfPoints() == 0) {
        QApplication::restoreOverrideCursor();
        CAMITK_WARNING(tr("Invalid mesh: no points in the point set. Action aborted."))
        return ABORTED;
    }

    resultPointSet->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    resultPointSet->DeepCopy(targetMesh->getPointSet());
    //-- add the id as point data in order to match normal to the points
    vtkSmartPointer<vtkIdTypeArray> pointId = vtkSmartPointer<vtkIdTypeArray>::New();
    pointId->SetNumberOfComponents(1);
    pointId->SetName("PointId");

    for (vtkIdType id = 0; id < resultPointSet->GetNumberOfPoints(); id++) {
        pointId->InsertNextValue(id);
    }

    resultPointSet->GetPointData()->AddArray(pointId);

    //-- extract the surface
    vtkSmartPointer<vtkDataSetSurfaceFilter> surfaceFilter = vtkSmartPointer<vtkDataSetSurfaceFilter>::New();
    surfaceFilter->SetInputData(resultPointSet);
    surfaceFilter->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    surfaceFilter->Update();

    //-- generate the normals
    vtkSmartPointer<vtkPolyDataNormals> normalGenerator = vtkSmartPointer<vtkPolyDataNormals>::New();
    normalGenerator->SetInputConnection(surfaceFilter->GetOutputPort());
    normalGenerator->ComputePointNormalsOn();
    normalGenerator->ComputeCellNormalsOff();
    normalGenerator->SplittingOff();
    normalGenerator->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    normalGenerator->Update();

    //-- Find the outer surface (see ExtractOutsideSurface Cxx example)
    vtkSmartPointer<vtkCellLocator> cellLocator = vtkSmartPointer<vtkCellLocator>::New();
    cellLocator->SetDataSet(normalGenerator->GetOutput());
    cellLocator->BuildLocator();

    // computer center and bounds
    double center[3] = {0.0};
    normalGenerator->GetOutput()->GetCenter(center);
    double bounds[6] = {0.0};
    if (normalGenerator->GetOutput()->GetNumberOfPoints() > 0) {
        normalGenerator->GetOutput()->GetPoints()->GetBounds(bounds);
    }

    // Now fire a ray from outside the bounds to the center and find a
    // cell. This cell should be on the outside surface
    double rayStart[3];

    for (unsigned int i = 0; i < 3; i++) {
        rayStart[i] = bounds[2 * i + 1] * 1.1;
    }

    vtkIdType cellId = -1;
    double xyz[3], t, pcoords[3];
    int subId;

    cellLocator->IntersectWithLine(rayStart, center, 0.0001, t, xyz, pcoords, subId, cellId);

    // use the connectivity filter to select only the outer surface
    vtkSmartPointer<vtkPolyDataConnectivityFilter> connectivityFilter = vtkSmartPointer<vtkPolyDataConnectivityFilter>::New();
    connectivityFilter->SetInputConnection(normalGenerator->GetOutputPort());
    connectivityFilter->SetExtractionModeToCellSeededRegions();
    connectivityFilter->InitializeSeedList();
    connectivityFilter->AddSeed(cellId);
    connectivityFilter->AddObserver(vtkCommand::ProgressEvent, progressCallback);
    connectivityFilter->Update();

    // Check if selected surface is closed or not
    vtkSmartPointer<vtkFeatureEdges> featureEdges = vtkSmartPointer<vtkFeatureEdges>::New();
    featureEdges->FeatureEdgesOff();
    featureEdges->BoundaryEdgesOn();
    featureEdges->NonManifoldEdgesOn();
    featureEdges->SetInputConnection(connectivityFilter->GetOutputPort());
    featureEdges->Update();

    int numberOfOpenEdges = featureEdges->GetOutput()->GetNumberOfCells();

    if (numberOfOpenEdges > 0) {
        CAMITK_INFO(tr("Surface is not closed."))
    }
    else {
        CAMITK_INFO(tr("Surface is closed."))
    }

    //-- get the original point id
    pointId = vtkIdTypeArray::SafeDownCast(connectivityFilter->GetOutput()->GetPointData()->GetArray("PointId"));

    //-- move along the normal (warp it)
    vtkSmartPointer<vtkDataArray> pointNormals = vtkDataArray::SafeDownCast(connectivityFilter->GetOutput()->GetPointData()->GetNormals());
    vtkIdType id;
    double pos[3];
    double normal[3];
    double displacement = getParameterValue("Displacement").toDouble();

    for (vtkIdType i = 0; i < connectivityFilter->GetOutput()->GetNumberOfPoints(); i++) {
        // get the id of the point in the original point set
        pointId->GetTypedTuple(i, &id);
        // get the initial point position
        resultPointSet->GetPoint(id, pos);
        // get the normal
        pointNormals->GetTuple(i, normal);

        // move point
        for (unsigned int j = 0; j < 3; j++) {
            pos[j] += displacement * normal[j];
        }

        resultPointSet->GetPoints()->SetPoint(id, pos);
    }

    // Create a mesh Component
    new MeshComponent(resultPointSet, targetMesh->getName() + " warped");
    refreshApplication();

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

    return SUCCESS;
}
