/*****************************************************************************
 * $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$
 ****************************************************************************/

#include "VolumeRenderingAction.h"

// -- CamiTK
#include <MeshComponent.h>
#include <InteractiveGeometryViewer.h>
#include <Property.h>
#include <Transformation.h>
#include <Log.h>

// -- vtk stuff --
// disable warning generated by clang about the surrounded headers
#include <CamiTKDisableWarnings>
#include <vtkImageCast.h>
#include <vtkVolume.h>
#include <vtkFixedPointVolumeRayCastMapper.h>
#include <vtkColorTransferFunction.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkVolumeProperty.h>
#include <vtkImageMagnitude.h>
#include <CamiTKReEnableWarnings>

#include <vtkPiecewiseFunction.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkPolyData.h>
#include <vtkCellArray.h>
#include <vtkFloatArray.h>
#include <vtkPointData.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>

using namespace camitk;


// --------------- constructor -------------------
VolumeRenderingAction::VolumeRenderingAction(ActionExtension* extension) : Action(extension),
    volumeName{"volume-rendering"} {
    // Init fields
    myWidget = nullptr;
    image = nullptr;

    // Setting name, description and input component
    setName("Volume Rendering");
    setDescription(tr("Volume rendering of 3D medical image using ray casting<br/>(Use Ctrl+R to render a selected image volume)"));
    setComponentClassName("ImageComponent");

    // Setting classification family and tags
    setFamily("View");
    addTag(tr("Volume rendering"));
    addTag(tr("volume"));
    addTag(tr("render"));
    addTag(tr("ray tracing"));

    // This parameters are defined so that their values can be control by the widget and/or from an action state machine
    addParameter(new Property(tr("Volume Rendering Visibility"), false, tr("Should the volume rendering be visible in 3D"), ""));
    addParameter(new Property(tr("Color Map File"), ":resources/default.clm", tr("Path to the colormap file name"), ""));

    // add shortcut
    VolumeRenderingAction::getQAction()->setShortcut(QKeySequence("Ctrl+R"));
    VolumeRenderingAction::getQAction()->setShortcutContext(Qt::ApplicationShortcut);
}

// --------------- getWidget ---------------
QWidget* VolumeRenderingAction::getWidget() {
    if (myWidget == nullptr) {
        myWidget = new VolumeRenderingWidget(this);
        connect(myWidget, SIGNAL(refresh()), this, SLOT(apply()));
    }

    image = dynamic_cast<ImageComponent*>(getTargets().last());

    //--  update the GUI
    if (image != nullptr && image->getVolumeRenderingChild() != nullptr) {
        myWidget->setEnabled(true);
        setParameterValue("Volume Rendering Visibility", (image->getVolumeRenderingChild()->getProp(volumeName) != nullptr) && image->getVolumeRenderingChild()->getVisibility("3D Viewer"));
        myWidget->updateUI(image->getName(), (image->getVolumeRenderingChild()->getProp(volumeName) != nullptr));
        // update UI
        getQAction(image);
    }
    else {
        myWidget->setEnabled(false);
    }

    return myWidget;
}

// -------------------- getQAction --------------------
QAction* VolumeRenderingAction::getQAction(Component* target) {
    QAction* myQAction = Action::getQAction();
    if (target != nullptr) {
        ImageComponent* img = dynamic_cast<ImageComponent*>(target);

        if (img != nullptr && img->getVolumeRenderingChild() != nullptr) {
            // make sure the status is visible
            myQAction->setEnabled(true);
            myQAction->setCheckable(true);
            // update visibility GUI
            myQAction->setChecked((img->getVolumeRenderingChild()->getProp(volumeName) != nullptr) && img->getVolumeRenderingChild()->getVisibility("3D Viewer"));
        }
        else {
            myQAction->setEnabled(false);
        }
    }

    return myQAction;
}


// --------------- apply ---------------
Action::ApplyStatus VolumeRenderingAction::apply() {
    bool visibility = getParameterValue("Volume Rendering Visibility").toBool();

    if ((image == nullptr) || (myWidget == nullptr)) {
        CAMITK_WARNING(tr("Cannot be called without a GUI yet. Action Aborted."))
        return ABORTED;
    }

    // create or recreate VR
    if (visibility) {
        createVolumeRendering();
    }

    if (image->getVolumeRenderingChild()->getProp(volumeName) != nullptr) {
        image->getVolumeRenderingChild()->getProp(volumeName)->SetVisibility(visibility);
    }

    image->getVolumeRenderingChild()->setVisibility("3D Viewer", visibility);

    refreshApplication();
    return SUCCESS;
}

// --------------- createVolumeRendering ---------------
void VolumeRenderingAction::createVolumeRendering() {
    // TODO instead of getting all the values directly from the GUI
    // this action should manage a ColorMapModel that is updated by the widget
    // so that there is no direct call from the action to the widget
    if (myWidget != nullptr) {
        // Get the transfer function from the widget
        QMap<int, double> transparencies = myWidget->getTransparencyPoints();
        QMap<int, double>::const_iterator transIt;

        QMap<int, QColor> transferColors = myWidget->getColorPoints();
        QMap<int, QColor>::const_iterator colorIt;

        QMap<int, double> gradientOpacities = myWidget->getOpacityPoints();
        QMap<int, double>::const_iterator gradientIt;

        double ambient = myWidget->getAmbient();
        double diffuse = myWidget->getDiffuse();
        double specular = myWidget->getSpecular();

        // Convert RGB images to magnitude
        vtkSmartPointer<vtkImageMagnitude> magnitudeFilter = vtkSmartPointer<vtkImageMagnitude>::New();
        magnitudeFilter->SetInputData(image->getImageData());
        magnitudeFilter->Update();

        // Mapper
        vtkSmartPointer<vtkFixedPointVolumeRayCastMapper> volumeMapper = vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New();
        volumeMapper->SetInputData(magnitudeFilter->GetOutput());

        // 1st property : volume color
        // This function associates a voxel gray level valut to a color one (r, g, b)
        // Function definition
        // f : gray |-> (r, g, b)
        // where gray is the grayscale value (of type unsigned short)
        vtkSmartPointer<vtkColorTransferFunction> volumeColor = vtkSmartPointer<vtkColorTransferFunction>::New();

        // Set the value that the user has entered in the ui sping box
        for (colorIt = transferColors.constBegin(); colorIt != transferColors.constEnd(); ++colorIt) {
            int gray = colorIt.key();
            QColor color = colorIt.value();
            volumeColor->AddRGBPoint((double) gray, color.redF(), color.greenF(), color.blueF());
        }

        // 2nd property : volume opacity
        // The opacity transfer function is used to control the opacity of different tissue types.
        // This function associate to a gray scale value a transparancy value (alpha)
        // Function definition
        // f : gray |-> alpha
        // where gray is the grayscale value (of type unsigned short)
        vtkSmartPointer<vtkPiecewiseFunction> volumeScalarOpacity = vtkSmartPointer<vtkPiecewiseFunction>::New();

        for (transIt = transparencies.constBegin(); transIt != transparencies.constEnd(); ++transIt) {
            int gray = transIt.key();
            double trans = transIt.value();
            volumeScalarOpacity->AddPoint((double) gray, trans);
        }

        // 3rd property : gradient of opacity function function of the distance
        // The gradient opacity function is used to decrease the opacity
        // in the "flat" regions of the volume while maintaining the opacity
        // at the boundaries between tissue types.  The gradient is measured
        // as the amount by which the intensity changes over unit distance.
        // For most medical data, the unit distance is 1mm.
        // This function associates a distance in mm to an opacity.
        // The higher the distance is, the less opacity it has
        // It enables the function to better render the boundaries between different tissues
        vtkSmartPointer<vtkPiecewiseFunction> volumeGradientOpacity = vtkSmartPointer<vtkPiecewiseFunction>::New();

        for (gradientIt = gradientOpacities.constBegin(); gradientIt != gradientOpacities.constEnd(); ++gradientIt) {
            int gray = gradientIt.key();
            double opacity = gradientIt.value();
            volumeGradientOpacity->AddPoint((double) gray, opacity);
        }

        // Finally : set the properties
        // The VolumeProperty attaches the color and opacity functions to the
        // volume, and sets other volume properties.  The interpolation should
        // be set to linear to do a high-quality rendering.  The ShadeOn option
        // turns on directional lighting, which will usually enhance the
        // appearance of the volume and make it look more "3D".  However,
        // the quality of the shading depends on how accurately the gradient
        // of the volume can be calculated, and for noisy data delete readerthe gradient
        // estimation will be very poor.  The impact of the shading can be
        // decreased by increasing the Ambient coefficient while decreasing
        // the Diffuse and Specular coefficient.  To increase the impact
        // of shading, decrease the Ambient and increase the Diffuse and Specular.

        vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New();
        volumeProperty->SetColor(volumeColor);
        volumeProperty->SetScalarOpacity(volumeScalarOpacity);
        volumeProperty->SetGradientOpacity(volumeGradientOpacity);
        volumeProperty->SetInterpolationTypeToLinear();
        volumeProperty->ShadeOn();
        volumeProperty->SetAmbient(ambient);
        volumeProperty->SetDiffuse(diffuse);
        volumeProperty->SetSpecular(specular);

        // The vtkVolume is a vtkProp3D (like a vtkActor) and controls the position
        // and orientation of the volume in world coordinates.
        vtkSmartPointer<vtkVolume> volume = vtkSmartPointer<vtkVolume>::New();
        volume->SetUserTransform(image->getMainTransformation()->getTransform());
        volume->SetMapper(volumeMapper);
        volume->SetProperty(volumeProperty);
        volume->Update();

        // Remove old volume from the component and the 3D viewer
        vtkSmartPointer<vtkProp> oldVolume = image->getVolumeRenderingChild()->getProp(volumeName);

        if (oldVolume) {
            image->getVolumeRenderingChild()->removeProp(volumeName);
        }

        // Add the new computed volume to the component and display it in the 3D viewer
        image->getVolumeRenderingChild()->addProp(volumeName, volume);
    }
}



