/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2018 Univ. Grenoble Alpes, CNRS, TIMC-IMAG UMR 5525 (GMCAO)
 *
 * 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$
 ****************************************************************************/

// C++ stuff
#include <algorithm>

// -- Core stuff
#include "Slice.h"

// -- vtk stuff
#include <vtkImageData.h>
#include <vtkProperty.h>
#include <vtkViewport.h>
#include <vtkCaptionActor2D.h>
#include <vtkTextActor.h>
#include <vtkProperty2D.h>
#include <vtkImageFlip.h>
#include <vtkImageMapper3D.h>

using namespace std;


namespace camitk {
// -------------------- constructor --------------------
Slice::Slice(vtkSmartPointer<vtkImageData> volume, SliceOrientation orientation, vtkSmartPointer<vtkWindowLevelLookupTable> lookupTable) {
    this->sliceOrientation = orientation;
    this->lut = lookupTable;
    init();
    setOriginalVolume(volume);
    setSlice(getNumberOfSlices() / 2);
}


// -------------------- Destructor --------------------
Slice::~Slice() {
    // Let's unreference vtkSmartPointers

    originalVolume = nullptr;
    lut = nullptr;
    imgToMapFilter = nullptr;
    image3DActor = nullptr;
    image2DActor = nullptr;
    pickPlane = nullptr;
    pickPlaneActor = nullptr;
    pickPlaneMapper = nullptr;
    pixelActor = nullptr;
}


// -------------------- init --------------------
void Slice::init() {
    currentSliceIndex = 0;

    extent[0] = 0;
    extent[1] = 0;
    extent[2] = 0;
    extent[3] = 0;
    extent[4] = 0;
    extent[5] = 0;

    originalDimensions[0] = -1;
    originalDimensions[1] = -1;
    originalDimensions[2] = -1;

    originalSpacing[0] = -1.0;
    originalSpacing[1] = -1.0;
    originalSpacing[2] = -1.0;

    originalVolume  = nullptr;

    imgToMapFilter  = nullptr;
    image3DActor    = nullptr;
    image2DActor    = nullptr;

    pickPlane       = nullptr;
    pickPlaneMapper = nullptr;
    pickPlaneActor  = nullptr;
    pixelActor      = nullptr;

    imgToMapFilter  = vtkSmartPointer<vtkImageMapToColors>::New();
    image3DActor    = vtkSmartPointer<vtkImageActor>::New();
    image2DActor    = vtkSmartPointer<vtkImageActor>::New();

    pickPlane       = vtkSmartPointer<vtkPlaneSource>::New();
    pickPlaneMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
    pickPlaneActor  = vtkSmartPointer<vtkActor>::New();

}

// -------------------- getImageData --------------------
vtkSmartPointer<vtkImageData> Slice::getImageData() const {
    return originalVolume;
}

// -------------------- setOriginalVolume --------------------
void Slice::setOriginalVolume(vtkSmartPointer<vtkImageData> volume) {
    // If there were already a referenced volume,
    //  de-reference the smart pointer.
    originalVolume = volume;

    // Get volume information
    originalVolume->GetDimensions(originalDimensions);
    originalVolume->GetSpacing(originalSpacing);
    originalVolume->GetExtent(extent);

    originalSize[0] = originalDimensions[0] * originalSpacing[0];
    originalSize[1] = originalDimensions[1] * originalSpacing[1];
    originalSize[2] = originalDimensions[2] * originalSpacing[2];

    // In other methods, there will be divisions by originalSpacing[k]
    // so let's avoid division by 0
    if (originalSpacing[0] == 0.0) {
        originalSpacing[0] = -1.0;
    }

    if (originalSpacing[1] == 0.0) {
        originalSpacing[1] = -1.0;
    }

    if (originalSpacing[2] == 0.0) {
        originalSpacing[2] = -1.0;
    }

    // Sets the end of the pipeline
    initActors();
}

// -------------------- initActors --------------------
void Slice::initActors() {
    imgToMapFilter->SetInputData(originalVolume);

    if (nullptr == lut) {
        imgToMapFilter->SetLookupTable(nullptr);
    }
    else {
        imgToMapFilter->SetLookupTable(lut);
    }

    /* 3D Actor case: directly pluged to the output of imgToMapFilter */
    image3DActor->GetMapper()->SetInputConnection(imgToMapFilter->GetOutputPort());
    image3DActor->InterpolateOn();

    // 2D actors
    image2DActor->GetMapper()->SetInputConnection(imgToMapFilter->GetOutputPort());
    image2DActor->InterpolateOn();

    // Pick plane
    updatePickPlane();

    pickPlaneMapper->SetInputConnection(pickPlane->GetOutputPort());
    pickPlaneActor->SetMapper(pickPlaneMapper);
    pickPlaneActor->GetProperty()->SetRepresentationToWireframe();

    switch (sliceOrientation) {
        default:
            break;
        case AXIAL:
            pickPlaneActor->GetProperty()->SetColor(0.0, 0.0, 1.0);
            pickPlaneActor->GetProperty()->SetEdgeColor(0.0, 0.0, 1.0);
            break;
        case CORONAL:
            pickPlaneActor->GetProperty()->SetColor(0.0, 1.0, 0.0);
            pickPlaneActor->GetProperty()->SetEdgeColor(0.0, 1.0, 0.0);
            break;
        case SAGITTAL:
            pickPlaneActor->GetProperty()->SetColor(1.0, 0.0, 0.0);
            pickPlaneActor->GetProperty()->SetEdgeColor(1.0, 0.0, 0.0);
            break;
    }

    pickPlaneActor->GetProperty()->SetOpacity(1.0);
    pickPlaneActor->GetProperty()->SetLineWidth(2.0);

    // Orientation 2D Axes
    init2DAxesActor();
    update2DAxesActorPosition();
}


// -------------------- setImageWorldTransform --------------------
void Slice::setImageWorldTransform(vtkSmartPointer<vtkTransform> transform) {
    image3DActor->SetUserTransform(transform);
}


// -------------------- get2DImageActor --------------------
vtkSmartPointer<vtkImageActor> Slice::get2DImageActor() const {
    return image2DActor;
}

// -------------------- get3DImageActor --------------------
vtkSmartPointer<vtkImageActor> Slice::get3DImageActor() const {
    return image3DActor;
}

// -------------------- getPickPlaneActor --------------------
vtkSmartPointer<vtkActor> Slice::getPickPlaneActor() const {
    return pickPlaneActor;
}

// -------------------- pixelPicked --------------------
void Slice::pixelPicked(double x, double y, double z) {
    return;
}

// -------------------- reslicedToVolumeCoords --------------------
void Slice::reslicedToVolumeCoords(const double* ijk, double* xyz) {
    xyz[0] = ijk[0] * originalSpacing[0];
    xyz[1] = ijk[1] * originalSpacing[1];
    xyz[2] = ijk[2] * originalSpacing[2];
}

// -------------------- volumeToReslicedCoords --------------------
void Slice::volumeToReslicedCoords(const double* xyz, double* ijk) {
    ijk[0] = xyz[0] / originalSpacing[0];
    ijk[1] = xyz[1] / originalSpacing[1];
    ijk[2] = xyz[2] / originalSpacing[2];
}

// -------------------- updatePickPlane --------------------
void Slice::updatePickPlane() {
    // Be carefull, The center of the first voxel (0,0,0) is displayed at coordinates (0.0, 0.0, 0.0).
    // Pixels winthin borders are represented (in 2D) only by their half, quarter or eigth depending on their coordinates.

    double planeXCoord, planeYCoord, planeZCoord;

    switch (sliceOrientation) {
        default:
            break;
        case AXIAL:
            planeZCoord = currentSliceIndex * originalSpacing[2];
            pickPlane->SetOrigin(0.0, 0.0, planeZCoord);
            pickPlane->SetPoint1(originalSize[0], 0.0, planeZCoord);
            pickPlane->SetPoint2(0.0, originalSize[1], planeZCoord);
            break;

        case CORONAL:
            planeYCoord = currentSliceIndex * originalSpacing[1];
            pickPlane->SetOrigin(0.0, planeYCoord, 0.0);
            pickPlane->SetPoint1(originalSize[0], planeYCoord, 0.0);
            pickPlane->SetPoint2(0.0, planeYCoord, originalSize[2]);
            break;

        case SAGITTAL:
            planeXCoord = currentSliceIndex * originalSpacing[0];
            pickPlane->SetOrigin(planeXCoord, 0.0, 0.0);
            pickPlane->SetPoint1(planeXCoord, originalSize[1], 0.0);
            pickPlane->SetPoint2(planeXCoord, 0.0, originalSize[2]);
            break;
    }

    pickPlane->UpdateWholeExtent();
}

// -------------------- getNumberOfSlices --------------------
int Slice::getNumberOfSlices() const {
    int nbSlices = 0;

    switch (sliceOrientation) {
        default:
            break;
        case AXIAL:
        case AXIAL_NEURO:
            nbSlices = extent[5] - extent[4] + 1;
            break;
        case CORONAL:
            nbSlices = extent[3] - extent[2] + 1;
            break;
        case SAGITTAL:
            nbSlices = extent[1] - extent[0] + 1;
            break;
    }

    return nbSlices;
}

// -------------------- getSlice --------------------
int Slice::getSlice() const {
    return currentSliceIndex;
}

// -------------------- setSlice --------------------
void Slice::setSlice(int s) {
    // Check if s is inside the volume bounds
    if (s < 0) {
        currentSliceIndex = 0.0;
    }
    else if (s >= getNumberOfSlices()) {
        currentSliceIndex = getNumberOfSlices() - 1;
    }
    else {
        currentSliceIndex = s;
    }

    switch (sliceOrientation) {
        default:
            break;
        case AXIAL:
            image3DActor->SetDisplayExtent(
                extent[0], extent[1],
                extent[2], extent[3],
                s, s);
            image2DActor->SetDisplayExtent(
                extent[0], extent[1],
                extent[2], extent[3],
                s, s);
            break;

        case CORONAL:
            image3DActor->SetDisplayExtent(
                extent[0], extent[1],
                s, s,
                extent[4], extent[5]);
            image2DActor->SetDisplayExtent(
                extent[0], extent[1],
                s, s,
                extent[4], extent[5]);
            break;

        case SAGITTAL:
            image3DActor->SetDisplayExtent(
                s, s,
                extent[2], extent[3],
                extent[4], extent[5]);
            image2DActor->SetDisplayExtent(
                s, s,
                extent[2], extent[3],
                extent[4], extent[5]);
            break;
    }

    updatePickPlane();
    update2DAxesActorPosition();
}


// -------------------- setSlice --------------------
void Slice::setSlice(double x, double y, double z) {
    // At this point, coordinates are expressed in Image coordinate system

    double ijk[3] = {0.0, 0.0, 0.0};
    double xyz[3] = {x, y, z};

    // Tranform real coordinates to index coordinates
    volumeToReslicedCoords(xyz, ijk);

    // Set pixel position in current slice.
    setPixelRealPosition(x, y, z);

    switch (sliceOrientation) {
        default:
            break;
        case AXIAL:
            setSlice(rint(ijk[2]));
            break;
        case CORONAL:
            setSlice(rint(ijk[1]));
            break;
        case SAGITTAL:
            setSlice(rint(ijk[0]));
            break;
    }
}

// -------------------- getNumberOfColors --------------------
int Slice::getNumberOfColors() const {
    return originalVolume->GetScalarTypeMax() - originalVolume->GetScalarTypeMin();
}

// -------------------- setPixelRealPosition --------------------
void Slice::setPixelRealPosition(double x, double y, double z) {
    updatePixelActorPosition(x, y, z);
}

// -------------------- initPixelActor --------------------
void Slice::initPixelActor() {
    double xMin = 0.0;
    double xMax = originalSize[0];
    double yMin = 0.0;
    double yMax = originalSize[1];
    double zMin = 0.0;
    double zMax = originalSize[2];
    // Instanciate the actor
    pixelActor = vtkSmartPointer<vtkActor>::New();

    // Draw the bounding box around the center of the slice
    updatePixelActorPosition(xMin + (xMax - xMin) / 2,  yMin + (yMax - yMin) / 2, zMin + (zMax - zMin) / 2);
}

// ----------------- updatePixelActorPosition -----------------
void Slice::updatePixelActorPosition(double x, double y, double z) {
    // if the interactive viewer is not refreshed/displayed, the pixelActor is null. In this case, no point to update it
    if (pixelActor) {
        // Create the cloud points used to describe the bounding box around
        vtkSmartPointer<vtkPoints> pixelPoints = vtkSmartPointer<vtkPoints>::New();
        pixelPoints->SetNumberOfPoints(4);
        // Those points describes the (red / blue / green) cross around the picked pixel.
        double planeZCoord, planeYCoord, planeXCoord;

        switch (sliceOrientation) {
            case AXIAL:
                planeZCoord = z - (originalSpacing[2] / 2.0);
                // 0 -> Anterior Point
                pixelPoints->InsertPoint(0, x, 0.0, planeZCoord);
                // 1 -> Posterior Point
                pixelPoints->InsertPoint(1, x, originalSize[1], planeZCoord);
                // 2 -> Right Point
                pixelPoints->InsertPoint(2, 0.0, y, planeZCoord);
                // 3 -> Left Point
                pixelPoints->InsertPoint(3, originalSize[0], y, planeZCoord);
                break;

            case CORONAL:
                planeYCoord = y - (originalSpacing[1] / 2.0);
                // 0 -> Superior Point
                pixelPoints->InsertPoint(0, x, planeYCoord, originalSize[2]);
                // 1 -> Inferior Point
                pixelPoints->InsertPoint(1, x, planeYCoord, 0.0);
                // 2 -> Right Point
                pixelPoints->InsertPoint(2, 0.0, planeYCoord, z);
                // 3 -> Left Point
                pixelPoints->InsertPoint(3, originalSize[0], planeYCoord, z);
                break;

            case SAGITTAL:
                planeXCoord = x + (originalSpacing[0] / 2.0);
                // 0 -> Superior Point
                pixelPoints->InsertPoint(0, planeXCoord, y, originalSize[2]);
                // 1 -> Inferior Point
                pixelPoints->InsertPoint(1, planeXCoord, y, 0.0);
                // 2 -> Anterior Point
                pixelPoints->InsertPoint(2, planeXCoord, 0.0, z);
                // 3 -> Posterior Point
                pixelPoints->InsertPoint(3, planeXCoord, originalSize[1], z);
                break;

            default:
                pixelPoints->InsertPoint(0, 0.0, 0.0, 0.0);
                pixelPoints->InsertPoint(1, 0.0, 0.0, 0.0);
                pixelPoints->InsertPoint(2, 0.0, 0.0, 0.0);
                pixelPoints->InsertPoint(3, 0.0, 0.0, 0.0);
                break;
        }

        // Create the lines crossing the picked pixel
        vtkSmartPointer<vtkLine> vLine = vtkSmartPointer<vtkLine>::New();
        vLine->GetPointIds()->SetId(0, 0);
        vLine->GetPointIds()->SetId(1, 1);

        vtkSmartPointer<vtkLine> hLine = vtkSmartPointer<vtkLine>::New();
        hLine->GetPointIds()->SetId(0, 2);
        hLine->GetPointIds()->SetId(1, 3);

        // Create the unstructured grid according to these 2 lines and cloup points
        vtkSmartPointer<vtkUnstructuredGrid> aPixelGrid = vtkSmartPointer<vtkUnstructuredGrid>::New();
        aPixelGrid->Allocate(2, 2);
        aPixelGrid->InsertNextCell(hLine->GetCellType(), hLine->GetPointIds());
        aPixelGrid->InsertNextCell(vLine->GetCellType(), vLine->GetPointIds());
        aPixelGrid->SetPoints(pixelPoints);

        // Create the corresponding mapper
        vtkSmartPointer<vtkDataSetMapper> aPixelMapper = vtkSmartPointer<vtkDataSetMapper>::New();
        aPixelMapper->SetInputData(aPixelGrid);

        pixelActor->SetMapper(aPixelMapper);
        pixelActor->GetProperty()->SetAmbient(1.0);
        pixelActor->GetProperty()->SetDiffuse(1.0);

        // Update pixel actor properties
        switch (sliceOrientation) {
            default:
            case AXIAL_NEURO:
                pixelActor->GetProperty()->SetColor(1.0, 1.0, 0.0);
                break;
            case AXIAL:
                pixelActor->GetProperty()->SetColor(0.0, 0.0, 1.0);
                break;
            case CORONAL:
                pixelActor->GetProperty()->SetColor(0.0, 1.0, 0.0);
                break;
            case SAGITTAL:
                pixelActor->GetProperty()->SetColor(1.0, 0.0, 0.0);
                break;
        }

        pixelActor->GetProperty()->SetLineWidth(1.0);
//        pixelActor->SetPosition( 0.0, 0.0, 0.0 );
        //-- pixelActor can not be picked
        pixelActor->PickableOff();
    }
    // else no point to update the pixelActor
}


// --------------------  --------------------
vtkSmartPointer<vtkActor> Slice::getPixelActor() {
    if (!pixelActor) {
        initPixelActor();
    }
    return pixelActor;
}

// --------------------  --------------------
void Slice::init2DAxesActor() {
    axes2DActor = vtkSmartPointer<vtkAxesActor>::New();
    axes2DActor->SetShaftTypeToCylinder();
    axes2DActor->SetXAxisLabelText("x");
    axes2DActor->SetYAxisLabelText("y");
    axes2DActor->SetZAxisLabelText("z");
    axes2DActor->SetTotalLength(20.0, 20.0, 20.0); // 10 unit length, mm ?
    vtkSmartPointer<vtkTextProperty> axeXTextProp = vtkSmartPointer<vtkTextProperty>::New();
    axeXTextProp->SetFontSize(10);
    axeXTextProp->BoldOn();
    axeXTextProp->ItalicOn();
    axeXTextProp->ShadowOff();
    axeXTextProp->SetFontFamilyToArial();
    // remove the autoscaling so that the font size is smaller than default autoscale
    axes2DActor->GetXAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
    // set the color
    axes2DActor->GetXAxisCaptionActor2D()->SetCaptionTextProperty(axeXTextProp);
    // set the color
    axes2DActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(1, 0.3, 0.3);
    // make sure the label can be hidden by any geometry, like the axes
//    axes2DActor->GetXAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();

    vtkSmartPointer<vtkTextProperty> axeYTextProp = vtkSmartPointer<vtkTextProperty>::New();
    axeYTextProp->ShallowCopy(axeXTextProp);
    axes2DActor->GetYAxisCaptionActor2D()->SetCaptionTextProperty(axeYTextProp);
    axes2DActor->GetYAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
    axes2DActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.3, 1, 0.3);
//    axes2DActor->GetYAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();

    vtkSmartPointer<vtkTextProperty> axeZTextProp = vtkSmartPointer<vtkTextProperty>::New();
    axeZTextProp->ShallowCopy(axeXTextProp);
    axes2DActor->GetZAxisCaptionActor2D()->SetCaptionTextProperty(axeZTextProp);
    axes2DActor->GetZAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
    axes2DActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.3, 0.3, 1.0);
//    axes2DActor->GetZAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();
    // add the axes (not visible)

    update2DAxesActorPosition();
}

// --------------------  --------------------
void Slice::update2DAxesActorPosition() {
    vtkSmartPointer<vtkTransform> transform = vtkSmartPointer<vtkTransform>::New();
    transform->Identity();

    if (axes2DActor) {
        double currentX, currentY, currentZ;
        // Update pixel actor properties
        switch (sliceOrientation) {

            default:
            case AXIAL_NEURO:
                currentZ = currentSliceIndex * originalSpacing[2] - 0.001;
                transform->Translate(0.0, 0.0, currentZ);
                axes2DActor->SetUserTransform(transform);
                break;
            case AXIAL:
                currentZ = (currentSliceIndex - 1) * originalSpacing[2];
                transform->Translate(0.0, 0.0, currentZ);
                axes2DActor->SetUserTransform(transform);
                break;
            case CORONAL:
                currentY = (currentSliceIndex - 1) * originalSpacing[1];
                transform->Translate(0.0, currentY, 0.0);
                axes2DActor->SetUserTransform(transform);
                break;
            case SAGITTAL:
                currentX = (currentSliceIndex + 1) * originalSpacing[0];
                transform->Translate(currentX, 0.0, 0.0);
                axes2DActor->SetUserTransform(transform);
                break;
        }
    }
    // else no point to update the pixelActor
}

// --------------------  --------------------
vtkSmartPointer<vtkAxesActor> Slice::get2DAxesActor() {
    if (! axes2DActor) {
        init2DAxesActor();
    }
    return axes2DActor;
}

// TODO : put those methods into a dedicated interface
//------------------------------- addProp ----------------------------------------
bool Slice::addProp(const QString& name, vtkSmartPointer< vtkProp > prop) {
    if (!extraProp.contains(name)) {
        extraProp.insert(name, prop);
        return true;
    }
    else {
        return false;
    }
}

//------------------------------- getProp ----------------------------------------
vtkSmartPointer< vtkProp > Slice::getProp(const QString& name) {

    if (extraProp.contains(name)) {
        return extraProp.value(name);
    }
    else {
        return nullptr;
    }
}

//------------------------------- getNumberOfProp ----------------------------------------
unsigned int Slice::getNumberOfProp() const {
    return extraProp.values().size();
}

//------------------------------- getProp ----------------------------------------
vtkSmartPointer< vtkProp > Slice::getProp(unsigned int index) {
    return extraProp.values().at(index);
}

//------------------------------- removeProp ----------------------------------------
bool Slice::removeProp(const QString& name) {
    if (extraProp.contains(name)) {
        // remove the prop from any renderer/consummers
        vtkSmartPointer<vtkProp> prop = extraProp.value(name);
        prop->VisibilityOff();

        for (int i = 0; i < prop->GetNumberOfConsumers(); i++) {
            vtkViewport* viewer = vtkViewport::SafeDownCast(prop->GetConsumer(i));

            if (viewer) {
                viewer->RemoveViewProp(prop);
            }
        }

        // remove it from the maps
        extraProp.remove(name);
        return true;
    }
    else {
        return false;
    }
}




}

