/*****************************************************************************
 * $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 "TransformationExplorer.h"

//-- CamiTK stuff
#include <TransformationManager.h>
#include <Transformation.h>
#include <FrameOfReference.h>
#include <GraphDisplayWidget.h>
#include <Component.h>
#include <InteractiveViewer.h>
#include <Application.h>
#include <Action.h>
#include <Log.h>

//-- Qt stuff
#include <QHeaderView>
#include <QGraphicsView>
#include <QColor>
#include <QMessageBox>
#include <QMenu>
#include <QAction>
#include <QSplitter>
#include <QTimer>

//-- vtk
#include <vtkMatrix4x4.h>

using namespace camitk;

//----------------------- constructor ------------------------
TransformationExplorer::TransformationExplorer(QString name) : Viewer(name, Viewer::DOCKED) {
    transformationTree = nullptr;
    frameTree = nullptr;
    transformationGraphDisplay = nullptr;
    mainWidget = nullptr;
    setIcon(QPixmap(":/axes"));
    setDescription("The transformation explorer shows all transformations and frames of reference");
}

//----------------------- destructor ------------------------
TransformationExplorer::~TransformationExplorer() {
    // do not delete mainWidget as it will automatically be deleted
    // when its parent widget will be deleted
}

//----------------------- getWidget ------------------------
QWidget* TransformationExplorer::getWidget() {
    if (mainWidget == nullptr) {
        mainWidget = new QFrame();
        QSplitter* splitter = new QSplitter(Qt::Vertical, mainWidget);

        //-- create the explorer trees
        frameTree = new QTreeWidget(mainWidget);
        // For explorerTree to emit the customMenu.. signal
        frameTree->setContextMenuPolicy(Qt::CustomContextMenu);
        // headers
        QStringList headers;
        headers << "Frame" << "Uuid" << "Components" << "Viewers";
        frameTree->setHeaderLabels(headers);
        // hide the UUid and viewer column for now
        frameTree->hideColumn(1);
        frameTree->hideColumn(3);
        // size policy so that an extensive list of component does not over stretch the explorer
        frameTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
        // Sortable column
        frameTree->setSortingEnabled(true);
        frameTree->sortByColumn(0, Qt::SortOrder::AscendingOrder);
        // Single selection
        frameTree->setSelectionMode(QAbstractItemView::SingleSelection);
        splitter->addWidget(frameTree);

        //-- transformations
        transformationTree = new QTreeWidget(mainWidget);
        transformationTree->setContextMenuPolicy(Qt::CustomContextMenu);
        headers.clear();
        headers << "Transformation" << "From" << "To" << "Uuid" ;
        transformationTree->setHeaderLabels(headers);
        // hide UUid for now
        transformationTree->hideColumn(3);
        // size policy: First column / expands to fill remaining space
        transformationTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
        // Columns 1 and 2 auto-fit their content
        transformationTree->header()->setSectionResizeMode(1, QHeaderView::Fixed);
        transformationTree->header()->setSectionResizeMode(2, QHeaderView::Fixed);
        transformationTree->setColumnWidth(1, 50);
        transformationTree->setColumnWidth(2, 50);
        transformationTree->header()->setStretchLastSection(false);
        // sort possible, one selection only
        transformationTree->setSortingEnabled(true);
        transformationTree->setSelectionMode(QAbstractItemView::SingleSelection);

        splitter->addWidget(transformationTree);


        //-- graphic view
        transformationGraphDisplay = new GraphDisplayWidget();
        splitter->addWidget(transformationGraphDisplay);

        //-- layout
        QVBoxLayout* layout = new QVBoxLayout(mainWidget);
        splitter->setStretchFactor(0, 1);
        splitter->setStretchFactor(1, 1);
        splitter->setStretchFactor(2, 1);
        layout->addWidget(splitter);

        // force equal sizes for the first show
        QTimer::singleShot(0, this, [ = ]() {
            splitter->setSizes({1, 1, 1});
        });

        //-- Transmit selection between widgets
        connect(transformationGraphDisplay, &GraphDisplayWidget::nodeSelectionChanged, this, [this](const void* node) {
            setSelectedFrame(static_cast<const FrameOfReference*>(node));
        });
        connect(transformationGraphDisplay, &GraphDisplayWidget::linkSelectionChanged, this, [this](const void* link) {
            setSelectedTransformation(static_cast<const Transformation*>(link));
        });
        connect(transformationGraphDisplay, &GraphDisplayWidget::linkDoubleClicked, this, [this](const void* link) {
            Action* action = Application::getAction("Transformation Editor");
            if (action != nullptr) {
                action->trigger();
            }
            setSelectedTransformation(static_cast<const Transformation*>(link));
        });
        connect(transformationGraphDisplay, &GraphDisplayWidget::nodeDoubleClicked, this, [this](const void* node) {
            Action* action = Application::getAction("Edit Anatomical Information");
            if (action != nullptr) {
                action->trigger();
            }
            setSelectedFrame(static_cast<const FrameOfReference*>(node));
        });

        connect(frameTree, &QTreeWidget::itemSelectionChanged, this, [this]() {
            // get the pointer to the frame corresponding to the selected item (Uuid) if it exists
            if (frameTree->selectedItems().size() > 0) {
                setSelectedFrame(camitk::TransformationManager::getFrameOfReferenceOwnership(QUuid((frameTree->selectedItems()[0] == nullptr) ? nullptr : frameTree->selectedItems()[0]->text(1))).get());
            }
            else {
                setSelectedFrame(nullptr);
            }
        });

        connect(frameTree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem * item, int column) {
            Action* action = Application::getAction("Edit Anatomical Information");
            if (action != nullptr) {
                action->trigger();
            }
            // get the pointer to the frame corresponding to the selected item (Uuid) if it exists
            setSelectedFrame(camitk::TransformationManager::getFrameOfReferenceOwnership(QUuid(((item == nullptr) ? nullptr : item->text(1)))).get());
        });

        connect(transformationTree, &QTreeWidget::itemSelectionChanged, this, [this]() {
            if (transformationTree->selectedItems().size() > 0) {
                setSelectedTransformation(TransformationManager::getTransformationOwnership(QUuid((transformationTree->selectedItems()[0] == nullptr) ? nullptr : transformationTree->selectedItems()[0]->text(3))).get());
            }
            else {
                setSelectedTransformation(nullptr);
            }
        });
        connect(transformationTree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem * item, int column) {
            Action* action = Application::getAction("Transformation Editor");
            if (action != nullptr) {
                action->trigger();
            }
            setSelectedTransformation(TransformationManager::getTransformationOwnership(QUuid((item == nullptr) ? nullptr : item->text(3))).get());
        });

        // Manage right-click menu for Frames
        connect(frameTree, &QTreeWidget::customContextMenuRequested, this, &TransformationExplorer::getFrameMenu);
        // Manage right-click menu for Transformations
        connect(transformationTree, &QTreeWidget::customContextMenuRequested, this, &TransformationExplorer::getTransformationMenu);
    }

    return mainWidget;
}

//----------------------- refresh ------------------------
void TransformationExplorer::refresh(Viewer* whoIsAsking) {
    if (mainWidget == nullptr) {
        // no widget → no refresh needed
        return;
    }

    // if it is this instance who is asking the refresh, then only the Component names need to be checked...
    if (whoIsAsking != this) {
        const QVector<FrameOfReference*> frames = TransformationManager::getFramesOfReference();
        const QVector<Transformation*> directTransformations = TransformationManager::getDirectTransformations();
        const QVector<Transformation*> allTransformations = TransformationManager::getTransformations();
        bool refreshGraphDisplayWidget = false;

        // Do not recreate the graph view if there are no new frames or transformations
        if (frames != currentFrames) {
            currentFrames = frames;
            refreshGraphDisplayWidget = true;
            emit framesUpdated(currentFrames);
        }
        if (directTransformations != currentDirectTransformations) {
            currentDirectTransformations = directTransformations;
            refreshGraphDisplayWidget = true;
            emit directTransformationsUpdated(currentDirectTransformations);
        }

        if (refreshGraphDisplayWidget) {
            // clear the graphic view
            transformationGraphDisplay->clear();
        }

        // Before clearing, check if there is a selected item (to restore it later)
        QString selectedFrameUuid;
        QString selectedTransformationUuid;
        if (frameTree->selectedItems().size() > 0) {
            selectedFrameUuid = frameTree->selectedItems()[0]->text(1);
        }
        if (transformationTree->selectedItems().size() > 0) {
            selectedTransformationUuid = transformationTree->selectedItems()[0]->text(3);
        }

        // clear both tree explorers, but prevent signals of selection change from being emitted
        frameTree->blockSignals(true);
        frameTree->clear();
        frameTree->blockSignals(false);
        transformationTree->blockSignals(true);
        transformationTree->clear();
        transformationTree->blockSignals(false);

        // Get the components'frames to fill componentsPerFrame
        QHash<const FrameOfReference*, QString> componentsPerFrame;
        for (auto& component : Application::getTopLevelComponents()) {
            // For each component,
            auto componentFramesMap = component->getAllFrames(true);
            for (auto it = componentFramesMap.cbegin(), end = componentFramesMap.cend(); it != end; ++it) {
                if (it.key() != nullptr && it.value() != nullptr) {
                    if (componentsPerFrame.contains(it.key())) {
                        componentsPerFrame[it.key()] = componentsPerFrame.value(it.key()) + ", " + it.value()->getName();
                    }
                    else {
                        componentsPerFrame.insert(it.key(), it.value()->getName());
                    }

                }
            }
        }

        // Get the viewers'frames to fill viewersPerFrame
        QHash<const FrameOfReference*, QString> viewersPerFrame;
        for (auto& viewer : Application::getViewers()) {
            InteractiveViewer* interactiveViewer = dynamic_cast<InteractiveViewer*>(viewer);
            if (interactiveViewer && interactiveViewer->getFrame() != nullptr) {
                if (!viewersPerFrame.contains(interactiveViewer->getFrame())) {
                    viewersPerFrame[interactiveViewer->getFrame()] += viewer->getName();
                }
                else {
                    viewersPerFrame[interactiveViewer->getFrame()] += ", " + viewer->getName();
                }
            }
        }

        // rebuild the frame explorer tree
        for (FrameOfReference* fr : frames) {
            addFrame(fr, componentsPerFrame.contains(fr) ? componentsPerFrame[fr] : "",
                     viewersPerFrame.contains(fr) ? viewersPerFrame[fr] : "", refreshGraphDisplayWidget);
        }

        // rebuild the transformation explorer tree and GraphDisplay if necessary
        for (const Transformation* tr : allTransformations) {
            addTransformation(tr, refreshGraphDisplayWidget);
        }

        if (refreshGraphDisplayWidget) {
            transformationGraphDisplay->refresh();
        }

        // Restore selection
        if (!selectedFrameUuid.isEmpty()) {
            QList<QTreeWidgetItem*> matches = frameTree->findItems(selectedFrameUuid, Qt::MatchExactly, 1);
            if (matches.size() > 0) {
                matches[0]->setSelected(true);
            }
            else {
                emit currentFrameChanged(nullptr);
            }
        }
        if (!selectedTransformationUuid.isNull()) {
            QList<QTreeWidgetItem*> matches = transformationTree->findItems(selectedTransformationUuid, Qt::MatchExactly, 1);
            if (matches.size() > 0) {
                matches[0]->setSelected(true);
            }
            else {
                emit currentTransformationChanged(nullptr);
            }
        }
    }
}

//----------------------- circledNumber ------------------------
QString TransformationExplorer::circledNumber(int n) {
    if (n == 0) {
        return "";
    }

    if (n >= 1 && n <= 20) {
        return QString(QChar(0x2460 + (n - 1))) + " ";    // ①–⑳
    }

    if (n >= 21 && n <= 35) {
        return QString(QChar(0x3251 + (n - 21))) + " ";    // ㉑–㉟
    }

    if (n >= 36 && n <= 50) {
        return QString(QChar(0x32B1 + (n - 36))) + " ";    // ㊱–㊿
    }

    // fallback: "(n)" for numbers above Unicode support
    return QString("(%1) ").arg(n);
}

//----------------------- addFrame ------------------------
void TransformationExplorer::addFrame(FrameOfReference* frame, QString componentsNames, QString viewersNames, bool addToGraphDisplay) {
    QString frameToolTip = "<table border='1' align='center' width='90%' cellspacing='0' cellpadding='4'>";
    frameToolTip += "<tr><td><b>Frame</b></td><td>" + frame->getName() + "</td></tr>";
    frameToolTip += "<tr><td><b>Description</b></td><td>" + frame->getDescription() + "</td></tr>";
    frameToolTip += "<tr><td><b>Uuid</b></td><td>" + frame->getUuid().toString() + "</td></tr>";
    QString worldToolTip = frameToolTip;
    worldToolTip += "</table>";
    frameToolTip += "<tr><td><b>Component" + QString(componentsNames.contains(",") ? "s" : "") + "</b></td><td>" + componentsNames + "</td></tr>";
    frameToolTip += "</table>";

    QString toolTip;
    QString internalText;
    QString formattedNumber = circledNumber(frame->getIndex());

    if (frame == TransformationManager::getWorldFrame()) {
        toolTip = worldToolTip;
        internalText = "🌐";
    }
    else {
        toolTip = frameToolTip;
        internalText = QString::number(frame->getIndex());
    }

    // Create the item in the list of frames and fill its columns and tooltip
    QTreeWidgetItem* frameItem = new QTreeWidgetItem(frameTree);
    frameItem->setText(0, QString("%1%2").arg(formattedNumber).arg(frame->getName()));
    frameItem->setText(1, frame->getUuid().toString());
    frameItem->setText(2, componentsNames);
    frameItem->setText(3, viewersNames);
    frameItem->setToolTip(0, toolTip);
    frameItem->setToolTip(2, toolTip);

    // Set the background color to the frame color
    frameItem->setBackground(0, frame->getColor());

    // Insert the item in the frame list
    frameTree->addTopLevelItem(frameItem);

    // Now add the Frame to the GraphDisplayWidget
    if (addToGraphDisplay) {
        transformationGraphDisplay->addNode(frame, toolTip, frame->getColor(), QColorConstants::Black, internalText);
    }
}

//----------------------- addTransformation ------------------------
void TransformationExplorer::addTransformation(const Transformation* tr, bool addToGraphDisplay) {
    // Ignore composite and inverse transformations
    if (TransformationManager::isCompositeTransformation(tr) || TransformationManager::isInverseTransformation(tr)) {
        return;
    }

    // Create the item in the list of transformations (transformationTree)
    QTreeWidgetItem* trItem = new QTreeWidgetItem(transformationTree);

    trItem->setText(0,  tr->getName());
    if (TransformationManager::isDefaultIdentityToWorld(tr)) {
        QFont italicFont;
        italicFont.setItalic(true);
        trItem->setFont(0, italicFont);
    }

    // Fill the "from" column
    trItem->setText(1, " " + circledNumber(((FrameOfReference*)tr->getFrom())->getIndex()));
    trItem->setBackground(1, QBrush(((FrameOfReference*)tr->getFrom())->getColor()));
    trItem->setTextAlignment(1, Qt::AlignCenter);

    // Fill the "to" column
    trItem->setText(2, " " + circledNumber(((FrameOfReference*)tr->getTo())->getIndex()));
    trItem->setBackground(2, QBrush(((FrameOfReference*)tr->getTo())->getColor()));
    trItem->setTextAlignment(2, Qt::AlignCenter);

    // Fill the UUID column
    trItem->setText(3, tr->getUuid().toString());

    // Set the tooltip (including sources and matrix)
    vtkMatrix4x4* m = tr->getMatrix();
    trItem->setToolTip(0, "Transformation: <i>" + tr->getName() + "</i><br/>"
                       + ((!TransformationManager::isDefaultIdentityToWorld(tr)) ? "<b>Double-Click to Edit</b><br/>" : "<i>(not editable)</i><br/>")
                       + "From: " + tr->getFrom()->getName()
                       + "<br/>To: " + tr->getTo()->getName() + "<br/>Uuid: " + tr->getUuid().toString()
                       + "<br/>Matrix:<br/>" + (m == nullptr ? "--" :
                               QString::number(m->GetElement(0, 0)) + ", " + QString::number(m->GetElement(0, 1)) + ", " + QString::number(m->GetElement(0, 2)) + ", " + QString::number(m->GetElement(0, 3)) + ",<br/>"
                               + QString::number(m->GetElement(1, 0)) + ", " + QString::number(m->GetElement(1, 1)) + ", " + QString::number(m->GetElement(1, 2)) + ", " + QString::number(m->GetElement(1, 3)) + ",<br/>"
                               + QString::number(m->GetElement(2, 0)) + ", " + QString::number(m->GetElement(2, 1)) + ", " + QString::number(m->GetElement(2, 2)) + ", " + QString::number(m->GetElement(2, 3)) + ",<br/>"
                               + QString::number(m->GetElement(3, 0)) + ", " + QString::number(m->GetElement(3, 1)) + ", " + QString::number(m->GetElement(3, 2)) + ", " + QString::number(m->GetElement(3, 3))));
    trItem->setToolTip(1, "From: " + tr->getFrom()->getName() + "<br/>Uuid: " + tr->getFrom()->getUuid().toString());
    trItem->setToolTip(2, "To: " + tr->getTo()->getName() + "<br/>Uuid: " + tr->getTo()->getUuid().toString());

    // Insert the item in the transformation list
    transformationTree->addTopLevelItem(trItem);

    // Now add the Transformation to the GraphDisplayWidget
    if (addToGraphDisplay) {
        // Color depends on the type of Transformation
        if (!TransformationManager::isInverseTransformation(tr)) { // We don't display inverse transformations in the graph
            if (TransformationManager::isDefaultIdentityToWorld(tr)) { // Default Identity transformation
                QPen pen(QColorConstants::DarkGray, 1, Qt::DotLine);
                transformationGraphDisplay->addLink(tr, tr->getFrom(), tr->getTo(), trItem->toolTip(0), pen, true);
            }
            else { // Direct transformation only
                QPen pen(QColorConstants::Black, 1);
                transformationGraphDisplay->addLink(tr, tr->getFrom(), tr->getTo(), trItem->toolTip(0), pen, true);
            }
        }
    }
}

//----------------------- getTransformationMenu ------------------------
void TransformationExplorer::getTransformationMenu(const QPoint& pos) {
    QTreeWidgetItem* trItem = transformationTree->itemAt(pos);
    QMenu menu(mainWidget);

    // Menu - Add a transformation
    Action* action = Application::getAction("Add Transformation");
    if (action != nullptr) {
        menu.addAction(action->getQAction());
    }

    // If there is a clicked Transformation item, we can edit or remove it
    if (trItem != nullptr) {
        // Make sure the item is selected
        trItem->setSelected(true);

        // Menu - Edit the transformation
        action = Application::getAction("Transformation Editor");
        if (action != nullptr) {
            menu.addAction(action->getQAction());
        }

        // Menu - Remove the transformation
        if (TransformationManager::getTransformationOwnership(QUuid(trItem->text(3))) != nullptr) {
            // the selected transformation is removable, show the remove transformation menu
            action = Application::getAction("Remove Transformation");
            if (action != nullptr) {
                menu.addAction(action->getQAction());
            }
        }
    }

    // Display the menu where the mouse pointer was
    menu.exec(transformationTree->mapToGlobal(pos));
}

//----------------------- getFrameMenu ------------------------
void TransformationExplorer::getFrameMenu(const QPoint& pos) {
    QTreeWidgetItem* frItem = frameTree->itemAt(pos);

    if (frItem != nullptr) {

        QMenu menu(mainWidget);
        // Menu - Edit the Frame
        Action* action = Application::getAction("Edit Anatomical Information");
        if (action != nullptr) {
            menu.addAction(action->getQAction());
        }

        // Menu - Show/Hide the Frame
        action = Application::getAction("Show Frame");
        if (action != nullptr) {
            menu.addAction(action->getQAction());
        }

        if (!frItem->isSelected()) {
            frItem->setSelected(true);
        }

        // Display the menu where the mouse pointer was
        menu.exec(frameTree->mapToGlobal(pos));
    }
}

//----------------------- setSelectedFrame ------------------------
void TransformationExplorer::setSelectedFrame(const FrameOfReference* fr) {
    // Check that the FrameOfReference still exists by taking ownership
    std::shared_ptr<FrameOfReference> frOwner = TransformationManager::getFrameOfReferenceOwnership(fr);
    if (frOwner != nullptr) {
        // Block signals to avoid loops
        frameTree->blockSignals(true);

        QList<QTreeWidgetItem*> items = frameTree->findItems(frOwner->getUuid().toString(), Qt::MatchExactly, 1);
        if (!items.isEmpty()) {
            frameTree->setCurrentItem(items[0]);
        }

        // Unblock signals
        frameTree->blockSignals(false);
    }

    // update graph display
    transformationGraphDisplay->setSelectedNode(frOwner.get());
    transformationGraphDisplay->setSelectedLink(nullptr);

    emit currentFrameChanged(frOwner.get());
}

//----------------------- setSelectedTransformation ------------------------
void TransformationExplorer::setSelectedTransformation(const Transformation* tr) {
    // Check that the Transformation still exists by taking ownership
    std::shared_ptr<Transformation> trOwner = TransformationManager::getTransformationOwnership(tr);
    if (trOwner != nullptr) {
        // Block signals to avoid loops
        transformationTree->blockSignals(true);

        QList<QTreeWidgetItem*> items = transformationTree->findItems(trOwner->getUuid().toString(), Qt::MatchExactly, 3);
        if (!items.isEmpty()) {
            transformationTree->setCurrentItem(items[0]);
        }

        // Unblock signals
        transformationTree->blockSignals(false);
    }

    // update graph display
    transformationGraphDisplay->setSelectedLink(trOwner.get());
    transformationGraphDisplay->setSelectedNode(nullptr);

    emit currentTransformationChanged(trOwner.get());
}