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

// Local
#include "ReorientImage.h"

// CamiTK
#include <Application.h>
#include <Core.h>
#include <Transformation.h>
#include <InteractiveGeometryViewer.h>
#include <ImageComponent.h>
#include <ImageOrientationHelper.h>
#include <Property.h>
#include <Log.h>

// From Qt
#include <QString>
#include <QDialog>
#include <QFile>
#include <QTextStream>
#include <QMetaEnum>

// From Vtk
// disable warning generated by clang about the surrounded headers
#include <CamiTKDisableWarnings>
#include <vtkProperty.h>
#include <vtkCaptionActor2D.h>
#include <vtkAnnotatedCubeActor.h>
#include <vtkProperty2D.h>
#include <CamiTKReEnableWarnings>

#include <vtkImageReslice.h>
#include <vtkTransform.h>
#include <vtkAxesActor.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
#include <vtkAssembly.h>
#include <VtkMeshUtil.h>

// Local Components
//#include <ImageOrientation.h>
#include <MeshComponent.h>

using namespace camitk;


static const QString RMeaning = "<b>Right</b>     <i>(right of the patient)</i> to <b>Left</b>      <i>(left  of the patient)</i>";
static const QString LMeaning = "<b>Left</b>      <i>(left  of the patient)</i> to <b>Right</b>     <i>(right of the patient)</i>";
static const QString AMeaning = "<b>Anterior</b>  <i>(face of the patient)</i>  to <b>Posterior</b> <i>(back of the patient)</i>";
static const QString PMeaning = "<b>Posterior</b> <i>(back of the patient)</i>  to <b>Anterior</b>  <i>(face of the patient)</i>";
static const QString IMeaning = "<b>Inferior</b>  <i>(feet of the patient)</i>  to <b>Superior</b>  <i>(head of the patient)</i>";
static const QString SMeaning = "<b>Superior</b>  <i>(head of the patient)</i>  to <b>Inferior</b>  <i>(feet of the patient)</i>";

// --------------- constructor -------------------
ReorientImage::ReorientImage(ActionExtension* extension) : Action(extension) {
    setName("Reorient Medical Image");
    setDescription(tr("This action re-orients a medical image according to Dicom RCS (Reference Coordinates System)."));
    setComponentClassName("ImageComponent");

    // Setting classification family and tags
    setFamily("Image Processing");
    addTag(tr("Dicom"));
    addTag(tr("Orientation"));

    lettersMeaning.insert('R', RMeaning);
    lettersMeaning.insert('L', LMeaning);
    lettersMeaning.insert('A', AMeaning);
    lettersMeaning.insert('P', PMeaning);
    lettersMeaning.insert('I', IMeaning);
    lettersMeaning.insert('S', SMeaning);

    // see initDialog
    dialog = nullptr;

    // see buildAxes
    axes = nullptr;

    // see buildCube
    annotatedCube = nullptr;

    orientationTransform = nullptr;
    axesTransform = nullptr;
    orientationCubeTransform = nullptr;
    cubeTransform = nullptr;

    // see buildGeometries for the 3D representation
    internalViewer   = nullptr;
    modelBoundingBox = nullptr;
    maleModel        = nullptr;
    femaleModel      = nullptr;

    // these properties are for pipeline users to set parameters
    Property* useMaleModel = new Property("Use Male Model", QVariant(true), tr("Use male or female model for image orientation illustration"), "");
    addParameter(useMaleModel);
    Property* backToOrigin = new Property("Reset Image Origin", QVariant(true), tr("Reset the image frame so that the transform origin and the original origin are the same"), "");
    addParameter(backToOrigin);
    Property* sameOutputImage = new Property("No New Image", QVariant(false), tr("Do not create a new re-oriented image, but apply the transformations on the input image."), "");
    addParameter(sameOutputImage);
}

// --------------- destructor -------------------
ReorientImage::~ReorientImage() {
    axes = nullptr;
    annotatedCube = nullptr;
    delete modelBoundingBox;
    modelBoundingBox = nullptr;
    delete femaleModel;
    femaleModel = nullptr;
    delete maleModel;
    maleModel = nullptr;
}

// --------------- initDialog -------------------
void ReorientImage::initDialog() {
    dialog = new QFrame();

    //-- init user interface
    ui.setupUi(dialog);

    //-- add the internal viewer
    initInternalViewer();
    internalViewer->setEmbedder(ui.illustrationLayout);

    ui.xDirection->setTextFormat(Qt::RichText);
    ui.yDirection->setTextFormat(Qt::RichText);
    ui.zDirection->setTextFormat(Qt::RichText);

    // Set the explanation from html resource file
    QString explanationLabelText = "Default Text";
    QFile explanationFile(":/resources/explanation.html");
    explanationFile.open(QIODevice::ReadOnly);
    QTextStream sin(&explanationFile);
    explanationLabelText = sin.readAll();
    explanationFile.close();
    ui.explanationLabel->setText(explanationLabelText);

    // Set Male / Female radiobutton
    ui.maleRadioButton->setChecked(getParameterValue("Use Male Model").toBool());

    // Reset image origin
    ui.resetImageOriginRadioButton->setChecked(getParameterValue("Reset Image Origin").toBool());

    // Duplicate image
    ui.noNewImageCheckBox->setChecked(getParameterValue("No New Image").toBool());

    // Set the Dicom RCS Labels Menu
    ui.rcsComboBox->addItems(ImageOrientationHelper::getPossibleImageOrientations());

    // Connect the slot
    connect(ui.rcsComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(rcsChanged(QString)));
    connect(ui.savePushButton, SIGNAL(clicked()), this, SLOT(apply()));
    connect(ui.maleRadioButton, SIGNAL(toggled(bool)), this, SLOT(modelChanged(bool)));

    modelChanged(true);
    rcsChanged(QString("RAI"));

    internalViewer->refresh();
}

void ReorientImage::showApplyButton(bool show) {
    ui.savePushButton->setVisible(show);
}

// --------------- initInternalViewer -------------------
void ReorientImage::initInternalViewer() {
    // build the axes representation
    buildAxes();
    // build the cube around the 3D person
    buildCube();
    // initial the transformations
    buildTransforms();
    // get the nice vtk 3D representation from file
    buildGeometries();

    // Set up the 3D viewer to visualize actual origin/orientation locations
    internalViewer = dynamic_cast<InteractiveGeometryViewer*>(Application::getNewViewer("Image Orientation Viewer", "InteractiveGeometryViewer"));
    internalViewer->setProperty("Highlight Mode", InteractiveViewer::OFF);
    internalViewer->setHighlightMode();
    internalViewer->getRendererWidget()->setCameraOrientation(RendererWidget::RIGHT_DOWN);

    if (modelBoundingBox) {
        internalViewer->getRendererWidget()->addProp(modelBoundingBox->getActor(InterfaceGeometry::Wireframe));
    }
    internalViewer->getRendererWidget()->addProp(axes);
    internalViewer->getRendererWidget()->addProp(annotatedCube);

}

// --------------- rcsChanged -------------------
void ReorientImage::rcsChanged(const QString index) {
    QChar xLetter = index.at(0);
    QChar yLetter = index.at(1);
    QChar zLetter = index.at(2);

    ui.xDirection->setText(lettersMeaning.value(xLetter));
    ui.yDirection->setText(lettersMeaning.value(yLetter));
    ui.zDirection->setText(lettersMeaning.value(zLetter));

    setAxesOrientation(index);
}

// --------------- modelChanged -------------------
void ReorientImage::modelChanged(bool displayMaleModel) {
    if (femaleModel == nullptr || maleModel == nullptr) {
        return;
    }

    if (displayMaleModel) {
        internalViewer->getRendererWidget()->removeProp(femaleModel->getActor(InterfaceGeometry::Surface));
        internalViewer->getRendererWidget()->addProp(maleModel->getActor(InterfaceGeometry::Surface));
    }
    else {
        internalViewer->getRendererWidget()->removeProp(maleModel->getActor(InterfaceGeometry::Surface));
        internalViewer->getRendererWidget()->addProp(femaleModel->getActor(InterfaceGeometry::Surface));
    }

    internalViewer->refresh();
}

// --------------- getWidget -------------------
QWidget* ReorientImage::getWidget() {
    if (!dialog) {
        initDialog();
    }

    ui.rcsComboBox->setCurrentIndex(0);

    return dialog;
}

//--------------- apply ------------
Action::ApplyStatus ReorientImage::apply() {

    if (!dialog) {
        CAMITK_WARNING(tr("Cannot apply the method before the QWidget initialization. Action aborted."))
        return ABORTED;
    }

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

    return process(img);
}

// --------------- process -------------------
Action::ApplyStatus ReorientImage::process(ImageComponent* image) {
    if (image == nullptr) {
        CAMITK_WARNING(tr("An ImageComponent is required. Action Aborted."))
        return ABORTED;
    }

    // 1- Get the selected new orientation
    int index = ui.rcsComboBox->currentIndex();
    QString indexText = ui.rcsComboBox->currentText();
    QChar xLetter = indexText.at(0);
    QChar yLetter = indexText.at(1);
    QChar zLetter = indexText.at(2);

    ui.xDirection->setText(lettersMeaning.value(xLetter));
    ui.yDirection->setText(lettersMeaning.value(yLetter));
    ui.zDirection->setText(lettersMeaning.value(zLetter));

    ImageOrientationHelper::PossibleImageOrientations orient = (ImageOrientationHelper::PossibleImageOrientations)(index);

    // 2- Compute the reslice matrix according to the new orientation
    double* imgSpacing = image->getImageData()->GetSpacing();
    int* imgDims    = image->getImageData()->GetDimensions();
    double dims[3];
    dims[0] = imgSpacing[0] * imgDims[0];
    dims[1] = imgSpacing[1] * imgDims[1];
    dims[2] = imgSpacing[2] * imgDims[2];

    vtkSmartPointer<vtkMatrix4x4> transformMatrix = ImageOrientationHelper::getTransformToRAI(orient, dims[0], dims[1], dims[2]);
    vtkSmartPointer<vtkTransform> transform = vtkSmartPointer<vtkTransform>::New();
    transform->SetMatrix(transformMatrix);

    // 3- Create a new ImageComponent with the resliced image.

    vtkSmartPointer<vtkImageReslice> imageResliceFilter = vtkSmartPointer<vtkImageReslice>::New();
    imageResliceFilter->SetInputData(image->getImageData());
    imageResliceFilter->SetOutputDimensionality(3);
    imageResliceFilter->SetResliceAxes(transform->GetMatrix());

    imageResliceFilter->Update();

    imageResliceFilter->Update();

    vtkSmartPointer<vtkImageData> newData = imageResliceFilter->GetOutput();

    if (ui.noNewImageCheckBox->isChecked()) {
        image->replaceImageData(newData);

        // Move the image back to the origin thanks to Frame
        vtkSmartPointer<vtkMatrix4x4> originalMatrix = image->getMainTransformation()->getMatrix();

        if (ui.resetImageOriginRadioButton->isChecked()) {
            vtkSmartPointer<vtkMatrix4x4> backToOriginMatrix = ImageOrientationHelper::getTransformToRAI(orient, dims[0], dims[1], dims[2]);
            image->updateMainTransformation(backToOriginMatrix);
        }
        else {
            vtkSmartPointer<vtkMatrix4x4> idty = vtkSmartPointer<vtkMatrix4x4>::New();
            idty->Identity();
            image->updateMainTransformation(idty);
        }

        vtkSmartPointer<vtkMatrix4x4> newMatrix = image->getMainTransformation()->getMatrix();
        vtkSmartPointer<vtkMatrix4x4> updatedMatrix = vtkSmartPointer<vtkMatrix4x4>::New();
        vtkMatrix4x4::Multiply4x4(originalMatrix, newMatrix, updatedMatrix);

        image->updateMainTransformation(updatedMatrix);
    }
    else {
        ImageComponent* result = new ImageComponent(newData, "ReslicedImage");

        // Move the image back to the origin thanks to Frame
        if (ui.resetImageOriginRadioButton->isChecked()) {
            vtkSmartPointer<vtkMatrix4x4> backToOriginMatrix = ImageOrientationHelper::getTransformToRAI(orient, dims[0], dims[1], dims[2]);
            result->updateMainTransformation(backToOriginMatrix);
        }
    }

    refreshApplication();

    return SUCCESS;
}


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

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

    vtkSmartPointer<vtkTextProperty> axeZTextProp = vtkSmartPointer<vtkTextProperty>::New();
    axeZTextProp->ShallowCopy(axeXTextProp);
    axes->GetZAxisCaptionActor2D()->SetCaptionTextProperty(axeZTextProp);
    axes->GetZAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
    axes->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.3, 0.3, 1.0);
    axes->GetZAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();
}

// --------------- buildCube -------------------
void ReorientImage::buildCube() {
    //-- annotated cube (right, left, anterior, posterior, inferior, superior)
    // For image Display, the axis should be RAI :
    // - X: from Right    [R]  (the Right of the patient) to Left      [L] (the Left of the patient)
    // - Y: from Anterior [A]  (the Face of the patient)  to Posterior [P] (the Back of the patient)
    // - Z: from Inferior [I]  (the Feet of the patient)  to Superior  [S] (the Head of the patient)
    annotatedCube = vtkSmartPointer<vtkAnnotatedCubeActor>::New();
    annotatedCube->SetXMinusFaceText("R");
    annotatedCube->SetXPlusFaceText("L");
    annotatedCube->SetYMinusFaceText("A");
    annotatedCube->SetYPlusFaceText("P");
    annotatedCube->SetZMinusFaceText("I");
    annotatedCube->SetZPlusFaceText("S");
    annotatedCube->SetXFaceTextRotation(90);
    annotatedCube->SetZFaceTextRotation(90);
    annotatedCube->SetFaceTextScale(0.65);
    vtkSmartPointer<vtkProperty> acProp = annotatedCube->GetCubeProperty();
    acProp->SetColor(0.5, 1, 1);
    acProp = annotatedCube->GetTextEdgesProperty();
    acProp->SetLineWidth(1);
    acProp->SetDiffuse(0);
    acProp->SetAmbient(1);
    acProp->SetColor(0.18, 0.28, 0.23);
    acProp = annotatedCube->GetXPlusFaceProperty();
    acProp->SetColor(1, 0, 0);
    acProp->SetInterpolationToFlat();
    acProp = annotatedCube->GetXMinusFaceProperty();
    acProp->SetColor(1, 0, 0);
    acProp->SetInterpolationToFlat();
    acProp = annotatedCube->GetYPlusFaceProperty();
    acProp->SetColor(0, 1, 0);
    acProp->SetInterpolationToFlat();
    acProp = annotatedCube->GetYMinusFaceProperty();
    acProp->SetColor(0, 1, 0);
    acProp->SetInterpolationToFlat();
    acProp = annotatedCube->GetZPlusFaceProperty();
    acProp->SetColor(0, 0, 1);
    acProp->SetInterpolationToFlat();
    acProp = annotatedCube->GetZMinusFaceProperty();
    acProp->SetColor(0, 0, 1);
    acProp->SetInterpolationToFlat();
}


// --------------- buildGeometries -------------------
void ReorientImage::buildGeometries() {

    // check for the CamiTK test data dir (not the first valid one, but the right one, i.e., the one that contains
    // the VTK files needed for this action)
    // => TODO use Qt resource files
    QStringList testDataDirectories = Core::getInstallDirectories("share/" + QString(Core::shortVersion()) + "/testdata");
    int i = 0;
    bool foundVtkFiles = false;
    QDir globalTestDir;
    while (i < testDataDirectories.size() && !foundVtkFiles) {
        globalTestDir = QDir(testDataDirectories.at(i));
        foundVtkFiles = globalTestDir.exists("imageBoundingBox.vtk") && globalTestDir.exists("male.vtk") && globalTestDir.exists("female.vtk");
        i++;
    }

    if (foundVtkFiles) {
        QString boundingBoxFilename = globalTestDir.canonicalPath() + "/imageBoundingBox.vtk";
        modelBoundingBox = VtkMeshUtil::vtkToGeometry(boundingBoxFilename.toStdString());
        modelBoundingBox->setRenderingModes(InterfaceGeometry::Wireframe);

        QString maleModelFilename = globalTestDir.canonicalPath() + "/male.vtk";
        maleModel = VtkMeshUtil::vtkToGeometry(maleModelFilename.toStdString());
        maleModel->setColor(1.0, 0.75, 0.64);

        QString femaleModelFilename = globalTestDir.canonicalPath() + "/female.vtk";
        femaleModel = VtkMeshUtil::vtkToGeometry(femaleModelFilename.toStdString());
        femaleModel->setColor(1.0, 0.75, 0.64);
    }
    else {
        CAMITK_WARNING(tr("Reorient Medical Image: 3D Model VTK files not found"))
    }
}

// --------------- buildTransforms -------------------
void ReorientImage::buildTransforms() {
    axesTransform =  vtkSmartPointer<vtkTransform>::New();
    axesTransform->PostMultiply();
    axesTransform->Identity();
    axes->SetUserTransform(axesTransform);

    cubeTransform =  vtkSmartPointer<vtkTransform>::New();
    cubeTransform->PostMultiply();
    cubeTransform->Identity();
    cubeTransform->Scale(0.1, 0.1, 0.1);
    annotatedCube->GetAssembly()->SetUserTransform(cubeTransform);

    orientationTransform = vtkSmartPointer<vtkTransform>::New();
    orientationTransform->Identity();
    axesTransform->Concatenate(orientationTransform);

    orientationCubeTransform = vtkSmartPointer<vtkTransform>::New();
    orientationCubeTransform->Identity();
    cubeTransform->Concatenate(orientationCubeTransform);

}

// --------------- setAxesOrientation -------------------
void ReorientImage::setAxesOrientation(const QString orientationStr) {
    ImageOrientationHelper::PossibleImageOrientations orientation = ImageOrientationHelper::getOrientationAsEnum(orientationStr);
    double dimX = 1.0;
    double dimY = 1.0;
    double dimZ = 2.0;


    orientationTransform->Identity();
    orientationTransform->SetMatrix(ImageOrientationHelper::getTransformFromRAI(orientation, dimX, dimY, dimZ));


    orientationCubeTransform->Identity();
    switch (orientation) {
        default:
        case camitk::ImageOrientationHelper::RAI:
            // Keep Identity for RAI (default orientation)
            break;
        case camitk::ImageOrientationHelper::RPS:
            // For the cube, just translate (in original axis then...)
            orientationCubeTransform->Translate(0.0, dimY, dimZ);
            break;

        case camitk::ImageOrientationHelper::RIP:
            orientationCubeTransform->Translate(0.0, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::RSA:
            orientationCubeTransform->Translate(0.0, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::LAS:
            orientationCubeTransform->Translate(dimX, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::LPI:
            orientationCubeTransform->Translate(dimX, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::LIA:
            orientationCubeTransform->Translate(dimX, 0.0, 0.0);
            break;
        case camitk::ImageOrientationHelper::ARS:
            orientationCubeTransform->Translate(0.0, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::ALI:
            orientationCubeTransform->Translate(dimX, 0.0, 0.0);
            break;
        case camitk::ImageOrientationHelper::AIR:
            break;
        case camitk::ImageOrientationHelper::ASL:
            orientationCubeTransform->Translate(dimX, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::PRI:
            orientationCubeTransform->Translate(0.0, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::PLS:
            orientationCubeTransform->Translate(dimX, dimY, dimZ);
            break;
        case camitk::ImageOrientationHelper::PIL:
            orientationCubeTransform->Translate(dimX, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::PSR:
            orientationCubeTransform->Translate(0.0, dimY, dimZ);
            break;
        case camitk::ImageOrientationHelper::IRA:
            break;
        case camitk::ImageOrientationHelper::ILP:
            orientationCubeTransform->Translate(dimX, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::IAL:
            orientationCubeTransform->Translate(dimX, 0.0, 0.0);
            break;
        case camitk::ImageOrientationHelper::IPR:
            orientationCubeTransform->Translate(0.0, dimY, 0.0);
            break;
        case camitk::ImageOrientationHelper::SRP:
            orientationCubeTransform->Translate(0.0, dimY, dimZ);
            break;
        case camitk::ImageOrientationHelper::SLA:
            orientationCubeTransform->Translate(dimX, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::SAR:
            orientationCubeTransform->Translate(0.0, 0.0, dimZ);
            break;
        case camitk::ImageOrientationHelper::SPL:
            orientationCubeTransform->Translate(dimX, dimY, dimZ);
            break;
    }

    internalViewer->refresh();
}
