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

//- Action stuffs
#include "MeshClipping.h"

//- Core stuffs
#include <ImageComponent.h>
#include <Application.h>
#include <InteractiveGeometryViewer.h>
#include <RendererWidget.h>
#include <ActionWidget.h>
#include <Log.h>
using namespace camitk;

// Vtk stuffs (Clipping functions...)
// disable warning generated by clang about the surrounded headers
#include <CamiTKDisableWarnings>
#include <vtkProperty.h>
#include <vtkExtractGeometry.h>
#include <vtkExtractPolyDataGeometry.h>
#include <vtkRenderWindow.h>
#include <CamiTKReEnableWarnings>

#include <vtkClipDataSet.h>
#include <vtkClipPolyData.h>

//- Qt stuffs (Action Viewer)
#include <QVector3D>
#include <QLayout>

// --------------- constructor -------------------
MeshClipping::MeshClipping(ActionExtension* extension): Action(extension) {
    //-- Initializing action parameters
    this->setName("Mesh Clipping");
    this->setDescription(tr("Interactive Mesh Clipping in the 3D Viewer"));
    this->setComponentClassName("MeshComponent");    // nothing needed
    this->setFamily("Mesh Processing");

    this->addTag(tr("Mesh"));
    this->addTag(tr("Clipping"));
    this->addTag(tr("3D Interaction"));

    // please always use lazy instantiation for GUI
    // (i.e., no GUI related code in action constructors)
    this->widget = nullptr;
    this->smoothing = false;
    this->allComponents = false;
    this->planeVisibility = true;
    this->save = false;

}

// --------------- destructor -------------------
MeshClipping::~MeshClipping() {
    delete (this->widget);
}

// --------------- getWidget -------------------
QWidget* MeshClipping::getWidget() {
    //-- Customizing the action widget (specific UI, specific buttons...)
    if (widget == nullptr) {
        widget = new PlaneWidget(this);
        customizeActionLayout();
    }

    //-- Creating the clipping plane widget (the first time getWidget() is called)
    if (widget->planeWidget == nullptr) {
        InteractiveGeometryViewer* default3DViewer = dynamic_cast<InteractiveGeometryViewer*>(Application::getViewer("3D Viewer"));

        if (default3DViewer != nullptr) {
            vtkRenderWindowInteractor* iren = default3DViewer->getRendererWidget()->getInteractor();
            //-- Configure the plane widget including callbacks
            widget->planeWidget = vtkSmartPointer <vtkImplicitPlaneWidget>::New();
            widget->planeWidget->SetInteractor(iren);

            //-- Bounding box definition and placement
            widget->planeWidget->SetPlaceFactor(1.5);

            default3DViewer->getBounds(limBounds);
            widget->planeWidget->PlaceWidget(limBounds);
            QVector3D origin((limBounds[1] + limBounds[0]) / 2,
                             (limBounds[3] + limBounds[2]) / 2,
                             (limBounds[5] + limBounds[4]) / 2);
            QVector3D normal(0, 0, (limBounds[4] - limBounds[5]));
            widget->planeWidget->SetNormal(normal.x(), normal.y(), normal.z());
            widget->planeWidget->SetOrigin(origin.x(), origin.y(), origin.z());
            widget->planeWidget->GetOutlineProperty()->SetColor(0, 0, 0);
            widget->planeWidget->GetOutlineProperty()->SetLineWidth(1);
            widget->planeWidget->GetPlaneProperty()->SetOpacity(0);

            widget->planeWidget->UpdatePlacement();

            //-- Widget Action Parameters
            widget->planeWidget->OutlineTranslationOff();
            widget->planeWidget->ScaleEnabledOff();
            widget->planeWidget->AddObserver(vtkCommand::EndInteractionEvent, this->widget);
        }
    }
    else {
        // just update the box
        updateBox(false);
    }

    if (planeVisibility) {
        widget->planeWidget->EnabledOn();
    }
    else {
        widget->planeWidget->EnabledOff();
    }

    return Action::getWidget();
}

// --------------- apply -------------------
Action::ApplyStatus MeshClipping::apply() {
    //-- Plane creation
    vtkSmartPointer < vtkPlane > plane = vtkSmartPointer < vtkPlane >::New();

    if (widget != nullptr) {
        widget->planeWidget->GetPlane(plane);
    }

    currentlyClippedMeshes = Application::getAllComponents();

    //-- Clipping the targeted components
    if (currentlyClippedMeshes.size() != 0) {
        //-- Clipping
        for (Component* candidate : currentlyClippedMeshes) {
            if (candidate->isInstanceOf("MeshComponent")) {
                if (allComponents || candidate->isSelected()) {
                    ApplyStatus clipStatus = clipMesh(dynamic_cast < MeshComponent* >(candidate), plane);

                    if (clipStatus == SUCCESS) {
                        // proceed and make sure to remove the vtk Widget of the screen if the component dies
                        connect(candidate, SIGNAL(destroyed()), this, SLOT(meshDeleted()));
                    }
                    else {
                        // the message was already logged by clipMesh(...)
                        return clipStatus;
                    }
                }
            }
        }
    }
    else {
        //--No targets : disabling the widget
        if (widget != nullptr) {
            widget->planeWidget->EnabledOff();
        }
    }

    if (widget != nullptr) {
        if (planeVisibility) {
            widget->planeWidget->EnabledOn();
        }
        else {
            widget->planeWidget->EnabledOff();
        }
    }

    save = false;
    refreshApplication();
    QApplication::restoreOverrideCursor();
    return SUCCESS;
}

// --------------- meshDeleted -------------------
void MeshClipping::meshDeleted() {
    widget->planeWidget->EnabledOff();
}

// --------------- customizeActionLayout -------------------
void MeshClipping::customizeActionLayout() {
    QLayout* informationFrameLayout = Action::getWidget()->layout();

    //-- Hidding apply/revert buttons
    ActionWidget* actionWidget = dynamic_cast<ActionWidget* >(this->actionWidget);
    actionWidget->setButtonVisibility(false);

    //-- Global/Selected component clipping Button
    QPushButton* allComponentsButton = new QPushButton("Clip All Components");
    informationFrameLayout->addWidget(allComponentsButton);
    QObject::connect(allComponentsButton, SIGNAL(released()), SLOT(changeAllComponents()));

    //-- Smoothing button
    QPushButton* smoothButton = new QPushButton("Smooth Clipping");
    informationFrameLayout->addWidget(smoothButton);
    QObject::connect(smoothButton, SIGNAL(released()), SLOT(changeSmoothing()));

    //-- Visibility button
    QPushButton* visibilityButton = new QPushButton("Hide clipping widget");
    informationFrameLayout->addWidget(visibilityButton);
    QObject::connect(visibilityButton, SIGNAL(released()), SLOT(changeVisibility()));

    //-- Restore unclipped meshes
    QPushButton* restoreButton = new QPushButton("Restore unclipped meshes");
    informationFrameLayout->addWidget(restoreButton);
    QObject::connect(restoreButton, SIGNAL(released()), SLOT(restoreMeshes()));

    //-- Update button (when component added?)
    QPushButton* updateButton = new QPushButton("Update");
    informationFrameLayout->addWidget(updateButton);
    QObject::connect(updateButton, SIGNAL(released()), this, SLOT(updateBox()));

    //-- Save button (when component added?)
    QPushButton* saveButton = new QPushButton("Save Clipped Component(s)");
    informationFrameLayout->addWidget(saveButton);
    QObject::connect(saveButton, SIGNAL(released()), this, SLOT(saveClippedMeshes()));

    //-- set the layout for the action widget
    Action::getWidget()->setLayout(informationFrameLayout);
}

// --------------- updateBox -------------------
void MeshClipping::updateBox() {
    updateBox(true);
}

// --------------- clipMesh -------------------
void MeshClipping::updateBox(bool refreshViewer) {
    //-- Saving previous values for the plane normal and origin
    double origin[3];
    widget->planeWidget->GetOrigin(origin);
    double normal[3];
    widget->planeWidget->GetNormal(normal);

    //-- Looking for new 3D viewer bounds
    if (Application::getTopLevelComponents().size() > 0) {
        Application::getTopLevelComponents().first()->getBounds(limBounds);
        for (int i = 1; i < Application::getTopLevelComponents().size(); i++) {
            double currentBounds[6];
            Application::getTopLevelComponents().at(i)->getBounds(currentBounds);
            for (int j = 0; j < 3; j++) {
                if (currentBounds[j] < limBounds[j]) {
                    limBounds[j] = currentBounds[j];
                }
                if (currentBounds[j + 1] > limBounds[j + 1]) {
                    limBounds[j + 1] = currentBounds[j + 1];
                }
            }
        }
        widget->planeWidget->PlaceWidget(limBounds);
    }
    //-- Replacing the plane in the newly updated box
    widget->planeWidget->SetNormal(normal);
    widget->planeWidget->SetOrigin(origin);
    widget->planeWidget->UpdatePlacement();

    if (refreshViewer) {
        refreshApplication();
    }
}

// --------------- clipMesh -------------------
Action::ApplyStatus MeshClipping::clipMesh(MeshComponent* mesh, vtkSmartPointer < vtkPlane > plane) {
    vtkSmartPointer<vtkPointSet> meshPointSet = mesh->getPointSet();
    int vtkObjectDataType = -1;

    if (meshPointSet != nullptr) {
        vtkObjectDataType = meshPointSet->GetDataObjectType();
    }

    switch (vtkObjectDataType) {
        case VTK_POLY_DATA:

            //-- mesh is a vtkPolyData
            if (smoothing) {
                //-- smoothed clip
                vtkSmartPointer < vtkClipPolyData > filter =
                    vtkSmartPointer < vtkClipPolyData >::New();
                filter->SetInputConnection(mesh->getDataPort());
                filter->InsideOutOn();
                filter->SetClipFunction(plane);
                filter->Update();
                mesh->setDataConnection(filter->GetOutputPort());

                if (save) {
                    new MeshComponent(filter->GetOutput(), mesh->getName() + " clipped");
                }
            }
            else {
                //-- chiselled clip
                vtkSmartPointer < vtkExtractPolyDataGeometry > filter =
                    vtkSmartPointer < vtkExtractPolyDataGeometry >::New();
                filter->SetInputConnection(mesh->getDataPort());
                filter->ExtractInsideOn();
                filter->SetImplicitFunction(plane);
                filter->Update();
                mesh->setDataConnection(filter->GetOutputPort());

                if (save) {
                    new MeshComponent(filter->GetOutput(), mesh->getName() + " clipped");
                }
            }

            break;

        case VTK_STRUCTURED_GRID:
        case VTK_UNSTRUCTURED_GRID:

            //-- mesh is a vtkUnstructuredGrid or vtkStructuredGrid
            if (smoothing) {
                //-- smoothed clip
                vtkSmartPointer < vtkClipDataSet > filter =
                    vtkSmartPointer < vtkClipDataSet >::New();
                filter->SetInputConnection(mesh->getDataPort());
                filter->InsideOutOn();
                filter->SetClipFunction(plane);
                filter->Update();
                mesh->setDataConnection(filter->GetOutputPort());

                if (save) {
                    new MeshComponent((vtkPointSet*)filter->GetOutput(), mesh->getName() + " clipped");
                }
            }
            else {
                //-- chiselled clip
                vtkSmartPointer < vtkExtractGeometry > filter =
                    vtkSmartPointer < vtkExtractGeometry >::New();
                filter->SetInputConnection(mesh->getDataPort());
                filter->ExtractInsideOn();
                filter->SetImplicitFunction(plane);
                mesh->setDataConnection(filter->GetOutputPort());

                if (save) {
                    new MeshComponent((vtkPointSet*)filter->GetOutput(), mesh->getName() + " clipped");
                }
            }

            break;

        default:
            //-- Not implemented
            CAMITK_WARNING(tr("Cannot clip mesh \"%1\": not a vtkPolyData, a vtkUnstructuredGrid nor a vtkStructuredGrid. Action aborted.").arg(mesh->getName()))
            return ABORTED;
            break;
    }

    // if everything went well
    return SUCCESS;
}

// --------------- restoreMeshes -------------------
void MeshClipping::restoreMeshes() {
    for (Component* candidate : currentlyClippedMeshes) {
        candidate->setDataConnection(candidate->getDataPort());
    }

    currentlyClippedMeshes.clear();
    refreshApplication();
}

// --------------- changeVisibility -------------------
void MeshClipping::changeVisibility() {
    planeVisibility = !planeVisibility;

    //- Updating button text
    QPushButton* button = dynamic_cast< QPushButton* >(sender());

    if (planeVisibility) {
        button->setText("Hide clipping widget");
    }
    else {
        button->setText("Show clipping widget");
    }

    apply();
}

// --------------- changeSmoothing -------------------
void MeshClipping::changeSmoothing() {
    smoothing = !smoothing;

    //- Updating button text
    QPushButton* button = dynamic_cast< QPushButton* >(sender());

    if (smoothing) {
        button->setText(tr("Raw clipping"));
    }
    else {
        button->setText(tr("Smooth clipping"));
    }

    apply();

}

// --------------- changeAllComponents -------------------
void MeshClipping::changeAllComponents() {
    allComponents = !allComponents;

    //- Updating button text
    QPushButton* button = dynamic_cast< QPushButton* >(sender());

    if (allComponents) {
        button->setText(tr("Clip Selected Components only"));
    }
    else {
        button->setText(tr("Clip All Components"));
        for (Component* candidate : currentlyClippedMeshes) {
            candidate->setDataConnection(candidate->getDataPort());
        }
    }

    apply();
}

// --------------- saveClippedMeshes -------------------
void MeshClipping::saveClippedMeshes() {
    save = true;
    apply();
}


