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

// -- Core stuff
#include "Component.h"
#include "Application.h"
#include "Viewer.h"
#include "Property.h"
#include "Action.h"
#include "PersistenceManager.h"
#include "TransformationManager.h"
#include "AbortException.h"
#include "Log.h"


// -- QT stuff
#include <QEvent>
#include <QMetaObject>
#include <QSet>
#include <utility>

// -- Vtk stuff
#include <vtkTextProperty.h>
#include <vtkCaptionActor2D.h>
#include <vtkTextActor.h>
#include <vtkProperty2D.h>

// -- Std stuff
#include <algorithm>
#include <cmath>
#include <utility> // as_const
namespace camitk {
// -------------------- constructor --------------------
Component::Component(QString file, QString n, Representation s, bool createDefaultFrame) :  myParentNode(nullptr), myFileName(std::move(file)), myService(s) {
    init(n, createDefaultFrame);
}


Component::Component(Component* parentComponent, const QString& n, Representation s, bool createDefaultFrame) : myParentNode(parentComponent), myService(s) {
    if (myParentNode == nullptr) {
        throw AbortException(tr("Inconsistency: cannot instantiate a sub component with a null parent, please use the parent component pointer as the first parameter of the constructor or use the top-level Component constructor.").toStdString());
    }

    init(n, createDefaultFrame);
    myFileName = n;
    // register as a parent child
    myParentNode->addChild(this);
}

// -------------------- default destructor --------------------
Component::~Component() {
    // delete all reference of this component in the application
    Application::removeComponent(this);

    // delete all its children Components
    deleteChildren();

    // unregister if I got a parent component
    if (Component* parent = getParentComponent()) {
        parent->removeChild(this);
    }

    // erase all the viewers
    myViewers.clear();

    // clear the adaptee
    if (myGeometry) {
        delete myGeometry;
    }

    myGeometry = nullptr;

    if (mySlice) {
        delete mySlice;
    }

    mySlice = nullptr;

    // remove from selection
    if (isSelectedFlag) {
        // Beware that this calls the method 'Component::setSelected' (as define in this class)
        // which bypass any redefined setSelected method in a derived class
        Component::setSelected(false, false);
    }

    // delete all properties
    for (auto prop : std::as_const(propertyMap)) {
        delete prop;
    }

    propertyMap.clear();

    if (actionsMenu != nullptr) {
        actionsMenu->clear();
        // deleteLater() is required instead of a simple delete to avoid crash
        // Explanation:
        // This menu is a context menu of the Explorer Viewer.
        // At one point the user can right-click on this Component node representation in the explorer,
        // which will run the exec() method of this actionsMenu.
        // Now, during this exec() call, if the user selects the "Close" action, the processor
        // will arrive in this destructor.
        // If we leave a classic "delete" here, this will delete the currently opened
        // context menu and free all the memory, while it is still being currently in its exec()
        // This might crash the event loop (which runs in another thread) as it is still using the object.
        // deleteLater() is safe as it will handle by the event loop later on.
        actionsMenu->deleteLater();
        actionsMenu = nullptr;
    }
}

// -------------------- init --------------------
void Component::init(const QString& name, bool createDefaultFrame) {
    modifiedFlag = false;
    interfaceNodeModifiedFlag = false;
    childrenComponent.clear();

    // initialize representation
    myGeometry = nullptr;
    mySlice = nullptr;
    actionsMenu = nullptr;
    visibilityMenu = nullptr;
    frameActors.clear();
    frameVisibilities.clear();

    // initialize name property
    Component::addProperty(new Property("Name", name, tr("Name of the Component"), ""));

    // initialize other state
    isSelectedFlag = false;

    // insert the Component in the list of top-level components as well as the full list
    Application::addComponent(this);

    // create the default Frame (does not depend on any representation, should be created for any Component)
    if (createDefaultFrame) {
        Component::setFrame(TransformationManager::addFrameOfReference(name, name));
    }
    else {
        Component::setFrame(nullptr);
    }

    // install a filter to get the modification of the dynamic properties (see event method)
    installEventFilter(this);

    // by default, the 1st PropertyExplorer tab is displayed
    setIndexOfPropertyExplorerTab(0);
}

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

        if (!changeEvent) {
            return false;
        }

        // notify the instance that the corresponding property was changed by calling the propertyValueChanged method
        // which should be overriden in the derived classes if needed
        // warning: to avoid never ending notification loops, do this only if signals are not blocked
        if (!signalsBlocked()) {
            propertyValueChanged(changeEvent->propertyName());
        }

        return true;
    }

    return QObject::event(e);
}

// -------------------- setName --------------------
void Component::setName(const QString& name) {
    // just modify the property, all the required updates are managed in propertyValueChanged
    setPropertyValue("Name", name);
}

// -------------------- isTopLevel --------------------
bool Component::isTopLevel() const {
    return (myParentNode == nullptr);
}


// -------------------- getParentComponent --------------------
Component* Component::getParentComponent() {
    return (Component*)myParentNode;
}

// -------------------- getTopLevelComponent --------------------
Component* Component::getTopLevelComponent() {
    if (!isTopLevel()) {
        return getParentComponent()->getTopLevelComponent();
    }
    else {
        return this;
    }
}

// -------------------- setVisibility --------------------
void Component::setVisibility(QString viewerName, bool b) {
    // first check if viewer is registered
    Viewer* v = Application::getViewer(viewerName);

    // only insert registered viewers
    if (v != nullptr) {
        auto it = myViewers.find(viewerName);

        if (it == myViewers.end()) {
            // insert the new viewer with the corresponding boolean
            myViewers.insert(viewerName, b);
        }
        else {
            it.value() = b;
        }
    }
}

// -------------------- getVisibility --------------------
bool Component::getVisibility(QString viewerName) const {
    auto it = myViewers.constFind(viewerName);

    if (it == myViewers.end()) {
        return false;
    }
    else {
        return it.value();
    }
}

// -------------------- refresh --------------------
void Component::refresh() {
    for (auto it = myViewers.begin(); it != myViewers.end(); it++) {
        if (it.value()) {
            Application::getViewer(it.key())->refresh();
        }
    }
    setNodeModified(false);
}

// -------------------- getRepresentation --------------------
Component::Representation Component::getRepresentation() const {
    static bool checked = false; // only check once for each Component

    if (!checked) {
        checked = true;
        QString representationString;
        QString instantiatedMember;
        QString shouldInstantiateMember;
        bool instantiationError = false;

        // check the service
        switch (myService) {
            case GEOMETRY:
                representationString = "GEOMETRY";
                shouldInstantiateMember = "myGeometry";
                instantiationError = (myGeometry == nullptr);
                break;

            case SLICE:
                representationString = "SLICE";
                shouldInstantiateMember = "mySlice";
                instantiationError = (mySlice == nullptr);
                break;

            case NO_REPRESENTATION:
                instantiatedMember = (myGeometry != nullptr) ? "myGeometry" : (mySlice != nullptr) ? "mySlice" : "<<INTERNAL_ERROR>>";
                instantiationError = (mySlice != nullptr || myGeometry != nullptr);
                break;
        }

        if (instantiationError) {
            const QMetaObject* qmetaObject = metaObject();
            QString classname = qmetaObject->className();

            if (myService != NO_REPRESENTATION) {
                // the Component has to instantiate
                CAMITK_ERROR(tr("Component class \"%1\" implements service %2 but does not instantiate it!\n"
                                "\tPlease instantiate %3 in initRepresentation() or change the implemented representation in the constructor.").arg(classname, representationString, shouldInstantiateMember))
            }
            else {
                // no representation should be instantiated!
                CAMITK_ERROR(tr("Component class \"%1\" has no implemented representation (NO_REPRESENTATION) but %2 is instantiated. Please do not instantiate %3 in initRepresentation() or change the implemented service in the constructor.").arg(classname, instantiatedMember, instantiatedMember))
            }
        }
    }

    return myService;
}

// ---------------- actionLessThan ----------------
extern bool actionLessThan(const camitk::Action* a1, const camitk::Action* a2);

// -------------------- getActionMenu -----------------------
QMenu* Component::getActionMenu() {

    ActionList allActions = Application::getActions(this);
    if (actionsMenu == nullptr) {
        actionsMenu = new QMenu();

        //-- add all actions sorted by family
        QMap<QString, ActionSet> familyMap;

        // group actions by family
        for (Action* action : std::as_const(allActions)) {
            familyMap[action->getFamily().toLower()].insert(action);
        }

        //-- create the "File" and "View" submenu first
        QMenu* fileMenu = actionsMenu->addMenu("File");
        QMenu* viewMenu = actionsMenu->addMenu("View");

        //-- create one sub menu per family (unless there is only one action)
        for (const QSet<Action*>& familySet : std::as_const(familyMap)) {
            // sort actions by name
            ActionList familyList = familySet.values();
            std::sort(familyList.begin(), familyList.end(), actionLessThan);

            if (familyList.size() >= 1) {
                QMenu* familyMenu;
                if (familyList.first()->getFamily() == "File") {
                    familyMenu = fileMenu;
                }
                else {
                    if (familyList.first()->getFamily() == "View") {
                        familyMenu = viewMenu;
                    }
                    else {
                        // this is a new family
                        familyMenu = actionsMenu->addMenu(familyList.first()->getFamily());
                    }
                }

                for (Action* action : familyList) {
                    // ownership not taken here
                    familyMenu->addAction(action->getQAction(this));
                }
            }
            else {
                // ownership not taken here
                actionsMenu->addAction(familyList.first()->getQAction(this));
            }
        }

        // add the visibility submenu
        visibilityMenu = viewMenu->addMenu("Viewer Visibility");
    }

    //-- update viewer visibility and all other actions that are checkable
    for (Action* a : std::as_const(allActions)) {
        a->getQAction(this);
    }
    visibilityMenu->clear();
    for (const Viewer* viewer : Application::getViewers(this)) {
        // show only embedded or stacked viewers
        if (viewer->getType() != Viewer::DOCKED) {
            // viewerAction is own by visibilityMenu, visibilityMenu.clear() will delete it
            QAction* visibilityAction = visibilityMenu->addAction(viewer->getName());
            visibilityAction->setCheckable(true);
            visibilityAction->setChecked(getVisibility(viewer->getName()));
            visibilityAction->setIcon(viewer->getIcon());
            QString tipString = "Set the visibility of this component in the \"" + viewer->getName() + "\" viewer\n(note that it might not have effect depending on the viewer)";
            visibilityAction->setStatusTip(tr(tipString.toStdString().c_str()));
            visibilityAction->setWhatsThis(tr(tipString.toStdString().c_str()));
            // add the toggle action slot using C++11 lambda so that everything is contained inside viewMenu
            connect(visibilityAction, &QAction::toggled, [ this, viewer ](bool visible) {
                setVisibility(viewer->getName(), visible);
                Application::refresh();
            });
        }
    }

    // no menu if no action
    if (actionsMenu->actions().size() == 0) {
        delete actionsMenu;
        actionsMenu = nullptr;
    }

    return actionsMenu;
}

// -------------------- setSelected --------------------
void Component::setSelected(const bool b, const bool recursive) {
    isSelectedFlag = b;

    // maintain the children selection state as well
    if (recursive) {
        for (Component* child : childrenComponent) {
            child->setSelected(b, recursive);
        }
    }

    //-- Now add to global selection
    // do that only in the end, so that the last selected will be this Component (which is the one called first!)
    Application::setSelected(this, b);
}

// -------------------- getFileName --------------------
const QString Component::getFileName() const {
    return myFileName;
}

// -------------------- setFileName --------------------
void Component::setFileName(const QString& fName) {
    myFileName = fName;
}



// -------------------------------------------------------
//
//              InterfacePersistence methods
//
// -------------------------------------------------------

// -------------------- toVariant --------------------

QVariant Component::toVariant() const {
    return QVariantMap{
        {"filename", getFileName()},
        {"properties", PersistenceManager::fromProperties(getPropertyObject())},
        {"frame", getFrame()->getUuid()},
    };
}

// -------------------- fromVariant --------------------
void Component::fromVariant(const QVariant& newValues) {
    QVariantMap newValuesMap = newValues.toMap();
    if (newValuesMap.contains("properties")) {
        PersistenceManager::loadProperties(this, newValuesMap.value("properties"));
    }

    if (newValuesMap.contains("frame")) {
        std::shared_ptr<FrameOfReference> fr = TransformationManager::getFrameOfReferenceOwnership(newValuesMap.value("frame").toUuid());
        if (fr != nullptr) {
            setFrame(fr); // To update potential child components
        }
        else {
            CAMITK_WARNING_ALT("Could not load frame for Component")
        }
    }
}

// -------------------- getUuid --------------------
QUuid Component::getUuid() const {
    return PersistenceManager::getUuidFromProperties(this);
}

// -------------------- setUuid --------------------
bool Component::setUuid(QUuid uuid) {
    return PersistenceManager::setUuidInProperties(this, uuid);
}

// -------------------------------------------------------
//
//                        InterfaceProperty methods
//
// -------------------------------------------------------

// -------------------- getHierarchy --------------------
QStringList Component::getHierarchy() const {
    const QMetaObject* qmetaObject = metaObject();
    QStringList classnameList;

    while (qmetaObject && !QString(qmetaObject->className()).contains("InterfaceProperty")) {
        classnameList.append(QString(qmetaObject->className()).remove(0, QString(qmetaObject->className()).lastIndexOf(':') + 1));

        if (qmetaObject->superClass()) {
            qmetaObject = qmetaObject->superClass();
        }
    }

    return classnameList;
}

// -------------------- isInstanceOf --------------------
bool Component::isInstanceOf(QString className) const {
    if (getHierarchy().contains(className)) {
        return true;
    }
    else {
        return false;
    }
}

// ---------------------- propertyValueChanged --------------------
void Component::propertyValueChanged(QString name) {
    if (name == "Name") {
        updateLabel(getPropertyValue(name).toString());
        setNodeModified(true);
    }
    else {
        CAMITK_INFO(tr("Component::propertyValueChanged: The value of property \"%1\" has changed to: \"%2\"\n"
                       "If you want to take this change/property into account, please redefine method \"propertyValueChanged(QString name)\" in class %3").arg(name, getPropertyValue(name).toString(), metaObject()->className()))
    }
}

// ---------------------- getProperty --------------------
Property* Component::getProperty(QString name) {
    return propertyMap.value(name);
}

// ---------------------- addProperty --------------------
bool Component::addProperty(Property* prop) {
    // from QObject documentation, section "Detailed Description":
    // "To avoid never ending notification loops you can temporarily block signals with blockSignals()."
    blockSignals(true);
    // add a dynamic Qt Meta Object property with the same name
    bool returnStatus = setProperty(prop->getName().toStdString().c_str(), prop->getInitialValue());
    // add to the map
    propertyMap.insert(prop->getName(), prop);
    blockSignals(false);

    return returnStatus;
}


// -------------------- getPropertyValue --------------------
QVariant Component::getPropertyValue(const QString& name) const {
    if (!propertyMap.contains(name)) {
        CAMITK_WARNING(tr("Property \"%1\" undeclared. Check the spelling.").arg(name))
        return QVariant(); // invalid QVariant
    }
    else {
        return property(name.toStdString().c_str());
    }
}

// -------------------- setPropertyValue --------------------
bool Component::setPropertyValue(const QString& name, QVariant newValue) {
    if (!propertyMap.contains(name)) {
        CAMITK_WARNING(tr("Property \"%1\" undeclared. Check the spelling. Cannot set value to %2").arg(name).arg(newValue.toString()))
        return false;
    }
    else {
        setProperty(name.toStdString().c_str(), newValue);
        return true;
    }
}


// -------------------------------------------------------
//
//                        InterfaceNode methods
//
// -------------------------------------------------------


// -------------------- addChild --------------------
void Component::addChild(InterfaceNode* childNode) {
    attachChild(childNode);
    childNode->setParent(this);
    setNodeModified(true);
}


// -------------------- removeChild --------------------
void Component::removeChild(InterfaceNode* childNode) {
    if (childrenComponent.removeAll((Component*)childNode)) {
        // remove myself from the childNode's parent item list
        childNode->setParent(nullptr);
        setNodeModified(true);
    }
}

// -------------------- deleteChildren --------------------
void Component::deleteChildren() {
    while (!childrenComponent.empty()) {
        Component* childComp = childrenComponent.takeLast();
        if (Application::isAlive(childComp)) {
            delete childComp;
        }
    }

    setNodeModified(true);
}

// -------------------- attachChild --------------------
void Component::attachChild(InterfaceNode* childNode) {
    auto* comp = dynamic_cast<Component*>(childNode);

    if (comp != nullptr) {
        // add a sub item
        if (!childrenComponent.contains(comp)) {
            // add it to the list
            childrenComponent.append(comp);
        }
        setNodeModified(true);
    }
}

// -------------------- setParent ------------------
void Component::setParent(InterfaceNode* pes) {
    if (myParentNode == pes || pes == this) {
        return;
    }

    if (myParentNode != nullptr) {
        // tell my parent I have been adopted by someone else
        myParentNode->removeChild(this);
        // avoid infinite recursion
        myParentNode = nullptr;
    }
    else {
        // remove this from the top-level list if needed
        Application::getTopLevelComponentList().removeAll(this);
    }

    myParentNode = pes;
    setNodeModified(true);
}








// -------------------------------------------------------
//
//                        InterfaceGeometry methods
//
// -------------------------------------------------------


// -------------------- getRenderingModes --------------------
const InterfaceGeometry::RenderingModes Component::getRenderingModes() const {
    if (myGeometry)
        // return the Rendering mode of myGeometry, is exists
    {
        return myGeometry->getRenderingModes();
    }
    else {
        // else return the added rendering mode of all children
        if (childrenComponent.size() > 0) {
            InterfaceGeometry::RenderingModes m = InterfaceGeometry::None;

            for (Component* childComponent : childrenComponent) {
                m |= childComponent->getRenderingModes();
            }

            return m;
        }
    }

    // Should never comes to this extremity
    return None;
}

// -------------------- getActorColor --------------------
void Component::getActorColor(InterfaceGeometry::RenderingModes m, double d[4], bool ignoreEnhancedModes) const {
    if (myGeometry) {
        // get the color of myGeometry, is exists, and finished
        myGeometry->getActorColor(m, d, ignoreEnhancedModes);
    }
    else {
        // return the first existing Color in my children
        int i = 0;
        bool found = false;

        while (i < childrenComponent.size() && !found) {
            childrenComponent[i]->getActorColor(m, d, ignoreEnhancedModes);
            found = (d[0] != 0.0 || d[1] != 0.0 || d[2] != 0.0 || d[3] != 0.0);
            i++;
        }

        if (!found) {
            for (unsigned int j = 0; j < 4; j++) {
                d[j] = 0.0;
            }
        }
    }
}

// -------------------- getBounds --------------------
void Component::getBounds(double* bounds) {
    invoke1(myGeometry, getBounds, bounds)
    else {
        bounds[0] = bounds[2] = bounds[4] = 0.0;
        bounds[1] = bounds[3] = bounds[5] = 1.0;

        // compute bounds using the children's
        for (Component* childComponent : childrenComponent) {
            double childBounds[6]; //xmin,xmax, ymin,ymax, zmin,zmax
            // get child bounds
            childComponent->getBounds(childBounds);

            // check compared to global bound
            for (int i = 0; i < 3; i++) {
                // compare min
                if (bounds[i * 2] > childBounds[i * 2]) {
                    bounds[i * 2] = childBounds[i * 2];
                }

                // compare max
                if (bounds[i * 2 + 1] < childBounds[i * 2 + 1]) {
                    bounds[i * 2 + 1] = childBounds[i * 2 + 1];
                }
            }
        }

    }
}

// -------------------- getBoundingRadius --------------------
double Component::getBoundingRadius() {
    if (myGeometry) {
        return myGeometry->getBoundingRadius();
    }
    else {
        // compute bounding radius using the children's
        double radius = 0.0;

        for (Component* childComponent : childrenComponent) {
            double childRadius = childComponent->getBoundingRadius();

            if (childRadius > radius) {
                radius = childRadius;
            }
        }

        return radius;
    }
}

// -------------------- setGlyphType --------------------
void Component::setGlyphType(const camitk::InterfaceGeometry::GlyphTypes type, const double size) {
    if (myGeometry) {
        myGeometry->setGlyphType(type, size);
    }
}




// -------------------------------------------------------
//
//                        InterfaceBitMap methods
//
// -------------------------------------------------------


// -------------------- getNumberOfSlices --------------------
int Component::getNumberOfSlices() const {
    if (mySlice) {
        return mySlice->getNumberOfSlices();
    }
    else {
        return -1;
    }
}

// -------------------- getSlice --------------------
int Component::getSlice() const {
    if (mySlice) {
        return mySlice->getSlice();
    }
    else {
        return -1;
    }
}

// -----------------------------------------------------------------------
//
//                        InterfaceFrame methods
//
// -----------------------------------------------------------------------

// -------------------- getAllFrames --------------------
QMultiMap<const FrameOfReference*, Component*> Component::getAllFrames(bool includeChildrenFrames) {
    QMultiMap<const FrameOfReference*, Component*> allFrames;
    allFrames.insert(this->getFrame(), this);
    if (includeChildrenFrames) {
        for (Component* child : getChildren()) {
            allFrames = child->getAllFrames() + allFrames;
        }
    }
    return allFrames;
}

// -------------------- getAllTransformations --------------------
QMultiMap<const Transformation*, Component*> Component::getAllTransformations(bool includeChildrenTransformations) {
    QMultiMap<const Transformation*, Component*> allTransformations;
    if (includeChildrenTransformations) {
        for (Component* child : getChildren()) {
            allTransformations = child->getAllTransformations() + allTransformations;
        }
    }
    return allTransformations;
}

// -------------------- setFrameFrom --------------------
void Component::setFrameFrom(const InterfaceFrame* fr) {
    setFrame(TransformationManager::getFrameOfReferenceOwnership(fr->getFrame()));
}

// -------------------- resetFrame --------------------
void Component::resetFrame() {
    setFrame(TransformationManager::addFrameOfReference(getName(), getName()));
}

// -------------------- getFrameAxisActor --------------------
vtkSmartPointer<vtkAxesActor> Component::getFrameAxisActor(QString viewerName) {
    vtkSmartPointer<vtkAxesActor> frameActor = frameActors.value(viewerName, nullptr);
    if (frameActor == nullptr) {
        frameActor = vtkSmartPointer<vtkAxesActor>::New();
        frameActor->SetShaftTypeToCylinder();
        frameActor->SetXAxisLabelText("x");
        frameActor->SetYAxisLabelText("y");
        frameActor->SetZAxisLabelText("z");
        frameActor->SetTotalLength(1.0, 1.0, 1.0); // unit length
        vtkSmartPointer<vtkTextProperty> axeXTextProp = vtkSmartPointer<vtkTextProperty>::New();
        axeXTextProp->SetFontSize(10);
        axeXTextProp->BoldOn();
        axeXTextProp->ItalicOn();
        axeXTextProp->ShadowOff();
        axeXTextProp->SetFontFamilyToArial();
        frameActor->GetXAxisCaptionActor2D()->SetCaptionTextProperty(axeXTextProp);
        // remove the autoscaling so that the font size is smaller than default autoscale
        frameActor->GetXAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
        // set the color
        frameActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.9, 0.4, 0.4);
        // make sure the label can be hidden by any geometry, like the axes
        frameActor->GetXAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();

        vtkSmartPointer<vtkTextProperty> axeYTextProp = vtkSmartPointer<vtkTextProperty>::New();
        axeYTextProp->ShallowCopy(axeXTextProp);
        frameActor->GetYAxisCaptionActor2D()->SetCaptionTextProperty(axeYTextProp);
        frameActor->GetYAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
        frameActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.4, 0.9, 0.4);
        frameActor->GetYAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();

        vtkSmartPointer<vtkTextProperty> axeZTextProp = vtkSmartPointer<vtkTextProperty>::New();
        axeZTextProp->ShallowCopy(axeXTextProp);
        frameActor->GetZAxisCaptionActor2D()->SetCaptionTextProperty(axeZTextProp);
        frameActor->GetZAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone();
        frameActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.4, 0.4, 0.9);
        frameActor->GetZAxisCaptionActor2D()->GetProperty()->SetDisplayLocationToBackground();
        frameActors[viewerName] = frameActor;
    }

    // Resize frame actor according to the current Component size
    double bounds[6];
    if (myParentNode == nullptr) {
        getBounds(bounds);
    }
    else {
        getParentComponent()->getBounds(bounds);
    }

    // 10% of the maximum dimension of the component's bounds
    double length = std::max({std::abs(bounds[1] - bounds[0]), std::abs(bounds[3] - bounds[2]), std::abs(bounds[5] - bounds[4])}) * 0.1;
    if (length > 1.0) { // Avoid invisible size
        frameActor->SetTotalLength(length, length, length);
    }
    else {
        frameActor->SetTotalLength(1.0, 1.0, 1.0);
    }

    return frameActor;
}

// ------------------- getFrameVisibility --------------------------
bool Component::getFrameVisibility(QString viewerName) const {
    return frameVisibilities.value(viewerName, false);
}


// ------------------- setFrameVisibility --------------------------
void Component::setFrameVisibility(QString viewerName, bool visibility) {
    frameVisibilities[viewerName] = visibility;
}

} // namespace camitk
