/*****************************************************************************
 * $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 "MeshProjection.h"
#include <Property.h>
#include <Application.h>
#include <InteractiveViewer.h>
#include <SingleImageComponent.h>
#include <ArbitrarySingleImageComponent.h>
#include <TransformationManager.h>

// Qt includes
#include <QBoxLayout>
#include <QFrame>
#include <QLabel>
#include <QListIterator>
#include <QMessageBox>
#include <QMap>

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


#include <vtkCutter.h>
#include <vtkPolyDataMapper.h>
#include <vtkTransformPolyDataFilter.h>
#include <vtkGeometryFilter.h>
#include <vtkVector.h>

#include <Log.h>

#include <utility> // as_const

using namespace camitk;

// --------------- Constructor -------------------
MeshProjection::MeshProjection(ActionExtension* extension) : Action(extension) {
    // Setting name, description and input component
    setName("Mesh Projection");
    setDescription("Project the mesh contours onto an image in the 2D slice viewer");
    setComponentClassName("MeshComponent");

    // Setting classification family and tags
    setFamily("View");
    addTag("Projection");
    addTag("Contour");
    addTag("Cutter");

    // init the currently managed components
    meshToProject = nullptr;
    targetImage = nullptr;

    initializationPending = true;

    // combobox for selecting either the mesh to project or the image to project to
    Property* componentList = new Property("ImageComponent List", 0, "List of possible image component to project onto", "");
    // Set the enum type
    componentList->setEnumTypeName("ImageComponentList");
    // Start with an empty values
    QStringList componentListNames;
    componentList->setAttribute("enumNames", componentListNames);
    // Add the property as an action parameter
    addParameter(componentList);

    // show/hide contour
    addParameter(new Property(tr("Show Mesh Projection"), false,
                              tr("Show/Hide the mesh projection on the selected image slices"), ""));

    // size of the contour
    Property* contourSize = new Property("Contour Line Width", 1.0, "Line width of the contour", "mm");
    contourSize->setAttribute("minimum", 0.0);
    contourSize->setAttribute("singleStep", 0.25);
    contourSize->setAttribute("decimals", 2);
    addParameter(contourSize);

    // immediately take the property changes into account
    setAutoUpdateProperties(true);

    initializationPending = false;
}


// --------------- destructor -------------------
MeshProjection::~MeshProjection() {
    // nothing to do here
}

// --------------- getWidget -------------------
QWidget* MeshProjection::getWidget() {
    // update combobox
    updateImageComponentList();

    if (imageComponentList.empty()) {
        CAMITK_WARNING(tr("MeshProjection requires an image to project onto. There are no ImageComponent opened. Aborting."))
        Application::showStatusBarMessage("MeshProjection: cannot apply action: please open at least one image.");
        return nullptr;
    }

    // select the first value in the combobox
    setProperty("ImageComponent List", 0);

    //-- check if the current mesh/image is still the same
    updateComponents(dynamic_cast<MeshComponent*>(getTargets().last()));

    return Action::getWidget();
}

// ---------------------- updateImageComponentList ----------------------------
void MeshProjection::updateImageComponentList() {
    //-- Update the list of possible parents
    imageComponentList.clear();

    // combobox strings
    QStringList componentListNames;

    ComponentList allComps = Application::getAllComponents();
    ImageComponent* inputImage;

    for (Component* comp : allComps) {
        inputImage = dynamic_cast<ImageComponent*>(comp);

        if (inputImage != nullptr) {
            componentListNames << inputImage->getName();
            imageComponentList.append(inputImage);
        }
    }

    Property* componentList = getProperty("ImageComponent List");
    componentList->setAttribute("enumNames", componentListNames);
}

// --------------- apply -------------------
Action::ApplyStatus MeshProjection::apply() {
    // when called in a pipeline, make sure inputImage and meshToProject are updated
    updateImageComponentList();
    updateComponents(dynamic_cast<MeshComponent*>(getTargets().last()));
    refreshApplication();
    return SUCCESS;
}

// ---------------------- event ----------------------------
bool MeshProjection::event(QEvent* e) {
    if (e->type() == QEvent::DynamicPropertyChange && !initializationPending) {
        e->accept();
        QDynamicPropertyChangeEvent* changeEvent = dynamic_cast<QDynamicPropertyChangeEvent*>(e);

        if (!changeEvent) {
            return false;
        }

        if (changeEvent->propertyName() == "Show Mesh Projection") {
            updateVisibility();
        }
        else {
            if (changeEvent->propertyName() == "ImageComponent List") {
                updateComponents(meshToProject);
                refreshApplication();
            }
            else {
                if (changeEvent->propertyName() == "Contour Line Width") {
                    updateContourLineWidth();
                }
            }
        }
        return true;
    }

    // this is important to continue the process if the event is a different one
    return QObject::event(e);
}

// --------------- updateComponents -------------------
void MeshProjection::updateComponents(MeshComponent* inputMesh) {
    int selectedImage = property("ImageComponent List").toInt();
    camitk::ImageComponent* inputImage = imageComponentList.value(selectedImage);

    if (targetImage != inputImage) {
        if (targetImage != nullptr && Application::isAlive(targetImage)) {
            disconnect(targetImage, SIGNAL(destroyed()), this, SLOT(hide()));
            hide();
        }

        targetImage = inputImage;
        connect(targetImage, SIGNAL(destroyed()), this, SLOT(hide()));
    }

    if (inputMesh != meshToProject) {
        hide();

        // update mesh and image
        meshToProject = inputMesh;

        // make sure to remove the contour of the renderer screen if the components die
        connect(meshToProject, SIGNAL(destroyed()), this, SLOT(hide()));
    }

}

// --------------- updateContourLineWidth -------------------
void MeshProjection::updateContourLineWidth() {
    double newWidth = getParameterValue("Contour Line Width").toDouble();

    // update the property of all actors
    for (vtkSmartPointer<vtkActor> contourActor : contourActorMap) {
        contourActor->GetProperty()->SetLineWidth(newWidth);
    }

    for (vtkSmartPointer<vtkActor> contourActorIn2DViewer : contourActorIn2DViewerMap) {
        contourActorIn2DViewer->GetProperty()->SetLineWidth(newWidth);
    }

    refreshApplication();

}

// --------------- updateVisibility -------------------
void MeshProjection::updateVisibility() {
    if (property("Show Mesh Projection").toBool() && Application::isAlive(targetImage) && Application::isAlive(meshToProject)) {
        //-- create the cutting planes
        cuttingPlaneMap.insert(Slice::AXIAL, vtkSmartPointer<vtkPlane>::New());
        updatePlaneNormal(Slice::AXIAL);
        cuttingPlaneMap.insert(Slice::CORONAL, vtkSmartPointer<vtkPlane>::New());
        updatePlaneNormal(Slice::CORONAL);
        cuttingPlaneMap.insert(Slice::SAGITTAL, vtkSmartPointer<vtkPlane>::New());
        updatePlaneNormal(Slice::SAGITTAL);
        cuttingPlaneMap.insert(Slice::ARBITRARY, vtkSmartPointer<vtkPlane>::New());
        updatePlaneNormal(Slice::ARBITRARY);

        //-- set the position of the plane depending on the current slice
        updateCuttingPlane();

        //-- Always use the mesh surface (i.e., a vtkPolyData)
        // A mesh can be a vtkUnstructuredGrid, vtkPolyData or vtkStructuredGrid
        // If the mesh is an unstructured or structured grid, the outside surface is needed to
        // get the contour. In this cases a convertion to polydata is needed
        vtkSmartPointer<vtkGeometryFilter> geometryFilter;
        bool convertedToPolyData = meshToProject->getPointSet()->IsA("vtkUnstructuredGrid") || meshToProject->getPointSet()->IsA("vtkStructuredGrid");

        if (convertedToPolyData) {
            geometryFilter = vtkSmartPointer<vtkGeometryFilter>::New();
            geometryFilter->SetInputData(meshToProject->getPointSet());
        }

        //-- Transform the mesh to the image data frame
        Transformation* trToImageDataFrame = TransformationManager::getTransformation(meshToProject->getFrame(), targetImage->getDataFrame());
        vtkSmartPointer<vtkTransformPolyDataFilter> transformPolyDataFilter = vtkTransformPolyDataFilter::New();
        if (trToImageDataFrame != nullptr) {
            transformPolyDataFilter->SetTransform(trToImageDataFrame->getTransform());
        }
        else {
            transformPolyDataFilter->SetTransform(vtkSmartPointer<vtkTransform>::New());
        }


        if (convertedToPolyData) {
            transformPolyDataFilter->SetInputConnection(geometryFilter->GetOutputPort());
        }
        else {
            transformPolyDataFilter->SetInputData(meshToProject->getPointSet());
        }

        //-- create the actual cutters mappers and actors for each plane
        QMapIterator<camitk::Slice::SliceOrientation, vtkSmartPointer<vtkPlane>> it(cuttingPlaneMap);

        while (it.hasNext()) {
            it.next();
            //-- Create the contour using a cutter
            // the cutter filter is the algorithm that build the contour from two inputs: the mesh and cuttingPlane
            vtkSmartPointer<vtkCutter> cutter = vtkSmartPointer<vtkCutter>::New();
            cutter->SetCutFunction(cuttingPlaneMap.value(it.key()));
            // use the position +/- 1% of the voxelSize
            cutter->SetValue(0.0, 0.0);
            cutter->SetInputConnection(transformPolyDataFilter->GetOutputPort());
            cutter->Update();

            // create a new actor for the 3D viewer
            vtkSmartPointer<vtkActor> contourActorIn3DViewer = getNewActor(it.key(), cutter->GetOutputPort());
            contourActorIn3DViewer->SetUserMatrix(targetImage->getMainTransformation()->getMatrix());
            // add the actor to the corresponding map
            contourActorMap.insert(it.key(), contourActorIn3DViewer);

            //-- add the actor to the 3D viewer in order to see the actual contour position
            InteractiveViewer* viewer3D = dynamic_cast<InteractiveViewer*>(Application::getViewer("3D Viewer"));

            if (viewer3D != nullptr) {
                viewer3D->getRendererWidget()->addProp(contourActorIn3DViewer, true);
            }
            else {
                CAMITK_WARNING("3D InteractiveGeometryViewer extension not available but required")
            }

            //-- for the 2D slice viewer, the contour needs to be transformed back to the world frame
            //   otherwise, if the image is not in the same frame, it won't be visible
            // NOTE : the 2D actor ALSO needs to be translated a little bit offside the slice plane
            // otherwise it won't be visible (this is a display trick and does not change
            // the inner data)
            // Get a new contour actor
            vtkSmartPointer<vtkActor> contourActorIn2DViewer = getNewActor(it.key(), cutter->GetOutputPort());
            // add the actor to the corresponding map
            contourActorIn2DViewerMap.insert(it.key(), contourActorIn2DViewer);

            //-- set the transform in the slice viewer
            if (it.key() != Slice::ARBITRARY) {
                // compute the slight offset to make sure the contour is visible on top of the slice
                double offsetFactor = 0.0;

                switch (it.key()) {
                    case Slice::SAGITTAL:
                        // for sagittal move toward z
                        offsetFactor = 1.0;
                        break;

                    case Slice::CORONAL:
                    case Slice::AXIAL:
                        // for coronal and axial move backward z
                        offsetFactor = -1.0;
                        break;

                    default:
                        break;
                }

                // Compute the transform
                vtkSmartPointer<vtkMatrix4x4> displayTranformIn2D = vtkSmartPointer<vtkMatrix4x4>::New();

                int orientationIndex = getOrientationIndex(it.key());
                // compute the "slightly" out of plane transform
                double translationInSlice = offsetFactor * getVoxelSize(it.key()) / 10.0;
                displayTranformIn2D->SetElement(orientationIndex, 3, translationInSlice);
                contourActorIn2DViewer->SetUserMatrix(displayTranformIn2D);
            }
            else {
                if (targetImage->getArbitrarySlices() != nullptr) {
                    // Arbitrary slice viewer is not moving with the plane, contrary to the other 2D viewers
                    // But the arbitrary frame is modified everytime the rotation or translation is modified.
                    // → ask the transformation manager to create the inverse of the arbitrary transformation
                    // (as a transformation it will be updated with the frame)
                    contourActorIn2DViewer->SetUserMatrix(TransformationManager::getTransformation(targetImage->getArbitrarySlices()->getFrame(), targetImage->getArbitrarySlices()->getArbitraryFrame())->getMatrix());
                }
            }

            //-- finally add this actor to the 2D viewer
            // FIXME This should be added to the component with the right frame (and use CamiTK actor feature)
            getViewer(it.key())->getRendererWidget()->addProp(contourActorIn2DViewer, true);
            //@DEBUG show the displaced actor in 3D
            //InteractiveViewer::get3DViewer()->getRendererWidget()->addProp(contourActorIn2DViewer,true);
        }

        //-- make sure to be notified when the user change the slice
        InteractiveViewer* axialViewer = dynamic_cast<InteractiveViewer*>(Application::getViewer("Axial Viewer"));

        if (axialViewer != nullptr) {
            QObject::connect(axialViewer, SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            // connect the other viewers
            QObject::connect(Application::getViewer("Coronal Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            QObject::connect(Application::getViewer("Sagittal Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            QObject::connect(Application::getViewer("Arbitrary Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
        }
        else {
            CAMITK_WARNING("InteractiveSliceViewer extension not available but required")
        }

        // @DEBUG check where is the axial camera
        //#include <vtkCameraActor.h>
        //vtkSmartPointer<vtkCameraActor> cameraActor = vtkSmartPointer<vtkCameraActor>::New();
        //cameraActor->SetCamera(InteractiveViewer::getAxialViewer()->getRendererWidget()->getActiveCamera());
        //InteractiveViewer::get3DViewer()->getRendererWidget()->addProp(cameraActor);
    }
    else {
        //-- do not notify this action anymore
        InteractiveViewer* axialViewer = dynamic_cast<InteractiveViewer*>(Application::getViewer("Axial Viewer"));

        if (axialViewer != nullptr) {
            QObject::disconnect(axialViewer, SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            // disconnect the other viewers
            QObject::disconnect(Application::getViewer("Coronal Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            QObject::disconnect(Application::getViewer("Sagittal Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
            QObject::disconnect(Application::getViewer("Arbitrary Viewer"), SIGNAL(selectionChanged()), this, SLOT(updateCuttingPlane()));
        }
        else {
            CAMITK_WARNING("InteractiveSliceViewer extension not available but required")
        }

        //-- remove the actors from the viewers
        InteractiveViewer* viewer3D = dynamic_cast<InteractiveViewer*>(Application::getViewer("3D Viewer"));

        if (viewer3D != nullptr) {
            for (auto& contourActor : std::as_const(contourActorMap)) {
                viewer3D->getRendererWidget()->removeProp(contourActor);
            }
        }
        else {
            CAMITK_WARNING("3D InteractiveGeometryViewer extension not available but required")
        }

        QMapIterator<camitk::Slice::SliceOrientation, vtkSmartPointer<vtkActor>> it(contourActorIn2DViewerMap);

        while (it.hasNext()) {
            it.next();
            getViewer(it.key())->getRendererWidget()->removeProp(it.value());
        }

    }

    // prevent refresh during application quit (called by hide() called by destroyed() signal)
    if (Application::isAlive(targetImage)) {
        refreshApplication();
    }

}

// --------------- updateCuttingPlane -------------------
void MeshProjection::updateCuttingPlane(Slice::SliceOrientation orientation) {
    // Note: Image origin convention indicates that the origin of the image is centered
    // on the first top left corner pixel
    // -> therefore image slices are displayed with a little shift of 0.5*voxelSize[i]
    // BUT that is only for the planar coordinates (the slice 0 is still at 0, not at 0.5*voxelSize[getOrientationIndex()]
    // Nothing has to be done in the direction perpendicular to the orientation
    //
    // The position of the current slice plane in the image frame (positionInImageFrame) is:
    // position of the image origin (0,0,0) + translation to the current slice
    //
    // The translation to the current slice is equal to:
    // index of the current slice in the considered orientation * slice thickness in this direction
    //
    // The translation to the current slice therefore depends on the current orientation

    //-- Get the current slice from the proper orientation
    SingleImageComponent* orientationSlices = nullptr;

    switch (orientation) {
        case Slice::ARBITRARY:
            orientationSlices = targetImage->getArbitrarySlices();
            break;

        case Slice::SAGITTAL:
            orientationSlices = targetImage->getSagittalSlices();
            break;

        case Slice::CORONAL:
            orientationSlices = targetImage->getCoronalSlices();
            break;

        case Slice::AXIAL:
        default:
            orientationSlices = targetImage->getAxialSlices();
            break;
    }

    if (orientationSlices != nullptr) {
        double sliceOriginInImageFrame[4];

        if (orientation != Slice::ARBITRARY) {
            // compute the position of the slice origin
            sliceOriginInImageFrame[0] = sliceOriginInImageFrame[1] = sliceOriginInImageFrame[2] = 0.0;
            sliceOriginInImageFrame[3] = 1.0;
            // adjust to take the thickness into account
            sliceOriginInImageFrame[getOrientationIndex(orientation)] += orientationSlices->getSlice() * getVoxelSize(orientation);
        }
        else {
            // for arbitrary orientation the shift of 0.5*voxelSize[i] in (x,y) plane has to be taken into account
            // and the transformation has to be computed by the orientationSlices itself
            targetImage->getArbitrarySlices()->getArbitraryCenter(sliceOriginInImageFrame);
        }

        //-- update the cutting plane
        cuttingPlaneMap.value(orientation)->SetOrigin(sliceOriginInImageFrame);

        //-- specific arbitrary slice update
        if (orientation == Slice::ARBITRARY) {
            // update the arbitrary cutting plane normal
            updatePlaneNormal(Slice::ARBITRARY);
        }
    }
}

void MeshProjection::updateCuttingPlane() {
    if (property("Show Mesh Projection").toBool()) {
        // -- update all orientation planes
        updateCuttingPlane(Slice::AXIAL);
        updateCuttingPlane(Slice::CORONAL);
        updateCuttingPlane(Slice::SAGITTAL);
        updateCuttingPlane(Slice::ARBITRARY);
        refreshApplication();
    }
}

// --------------- hide -------------------
void MeshProjection::hide() {
    // changed the property (this will automatically call ::event(..)
    setProperty("Show Mesh Projection", false);
}

// --------------- updatePlaneNormal -------------------
vtkSmartPointer< vtkPlane > MeshProjection::updatePlaneNormal(Slice::SliceOrientation orientation) {
    //-- Create a plane to cut
    vtkSmartPointer<vtkPlane> cuttingPlane = cuttingPlaneMap.value(orientation);
    double normalVector[4];
    double normalVectorInWorldFrame[4];
    double origin[4];
    double originInWorldFrame[4];

    if (orientation != Slice::ARBITRARY || targetImage->getArbitrarySlices() == nullptr) {
        origin[0] = origin[1] = origin[2] = 0.0;
        origin[3] = 1.0;

        // Correct the normal vector
        // For instance, axial slices are the XY direction -> XY direction has a plan normal = (0,0,1)
        normalVector[0] = normalVector[1] = normalVector[2] = 0.0;
        normalVector[3] = 1.0;
        normalVector[getOrientationIndex(orientation)] = 1.0;

        // remove origin translation to only get the new orientation
        for (int i = 0; i < 3; i++) {
            normalVector[i] -= origin[i];
        }
    }
    else {
        // arbitrary orientation → just ask for the current z plane vector expressed in the data image frame
        targetImage->getArbitrarySlices()->getArbitraryPlaneNormal(normalVector);
    }

    cuttingPlane->SetNormal(normalVector);

    return cuttingPlane;
}

// --------------- getOrientationIndex -------------------
int MeshProjection::getOrientationIndex(Slice::SliceOrientation orientation) {
    switch (orientation) {
        case Slice::SAGITTAL:
            return 0;
            break;

        case Slice::CORONAL:
            return 1;
            break;

        case Slice::AXIAL:
        default:
            return 2;
            break;
    }
}

// --------------- getVoxelSize -------------------
double MeshProjection::getVoxelSize(Slice::SliceOrientation orientation) {
    double voxelSize[3];
    targetImage->getImageData()->GetSpacing(voxelSize);
    return voxelSize[getOrientationIndex(orientation)];
}

// --------------- getNewActor -------------------
vtkSmartPointer<vtkActor> MeshProjection::getNewActor(Slice::SliceOrientation orientation, vtkAlgorithmOutput* cutterOuput) {
    vtkSmartPointer<vtkActor> newActor = vtkSmartPointer<vtkActor>::New();
    // set rgb components to mach the color of the current orientation (blue=axial, green=coronal, red=sagittal, yellow=ARBITRARY)
    double color[3];
    color[0] = color[1] = color[2] = 0.3;

    if (orientation != Slice::ARBITRARY) {
        color[getOrientationIndex(orientation)] = 1.0;
    }
    else {
        color[0] = color[1] = 1.0;
    }

    newActor->GetProperty()->SetColor(color);
    // width of the contour is defined by the user
    newActor->GetProperty()->SetLineWidth(getParameterValue("Contour Line Width").toDouble());
    // create the 3D mapper
    vtkSmartPointer<vtkPolyDataMapper> cutterMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
    cutterMapper->SetInputConnection(cutterOuput);

    newActor->SetMapper(cutterMapper);
    // remove lightning effect for better visibility
    newActor->GetProperty()->LightingOff();
    return newActor;
}

// --------------- getViewer -------------------
InteractiveViewer* MeshProjection::getViewer(Slice::SliceOrientation orientation) {
    switch (orientation) {
        case Slice::AXIAL:
            return dynamic_cast<InteractiveViewer*>(Application::getViewer("Axial Viewer"));
            break;

        case Slice::CORONAL:
            return dynamic_cast<InteractiveViewer*>(Application::getViewer("Coronal Viewer"));
            break;

        case Slice::SAGITTAL:
            return dynamic_cast<InteractiveViewer*>(Application::getViewer("Sagittal Viewer"));
            break;

        case Slice::ARBITRARY:
            return dynamic_cast<InteractiveViewer*>(Application::getViewer("Arbitrary Viewer"));
            break;

        default:
            break;
    }

    return nullptr;
}
