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

#include <QGraphicsEllipseItem>
#include <QGraphicsLineItem>
#include <QPen>
#include <QBrush>
#include <QEvent>
#include <QGraphicsSceneMouseEvent>
#include <QResizeEvent>

#include <cmath>
#include <numbers>
#include <qdebug.h>

using namespace camitk;

//----------------------- constructor ------------------------
GraphDisplayWidget::GraphDisplayWidget(QWidget* parent): QGraphicsView(new QGraphicsScene(), parent) {
    setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    installEventFilter(this); // for resize events

    // selection or item was changed by the mouse, update the view
    connect(scene(), &QGraphicsScene::changed, this, &GraphDisplayWidget::updateView);

    // init current selection
    currentSelectedNode = nullptr;
    currentSelectedLink = nullptr;

    // default diameter for nodes
    diameter = 20.0;

    // highlighters
    selectedPen.setColor(palette().color(QPalette::Highlight));
    halfPenWidth = 1.0;
    selectedPen.setWidth(halfPenWidth * 2);
}

//----------------------- destructor ------------------------
GraphDisplayWidget::~GraphDisplayWidget() {
    delete scene();
}

//----------------------- mouseDoubleClickEvent ------------------------
void GraphDisplayWidget::mouseDoubleClickEvent(QMouseEvent* mouseEvent) {
    QPointF scenePoint = mapToScene(mouseEvent->pos());
    QGraphicsItem* item = scene()->itemAt(scenePoint, QTransform());
    QGraphicsLineItem* linkItem = dynamic_cast<QGraphicsLineItem*>(item);
    if (item2link.contains(linkItem)) {
        emit linkDoubleClicked(item2link[linkItem]);
    }
    else {
        QGraphicsEllipseItem* nodeItem = dynamic_cast<QGraphicsEllipseItem*>(item);
        if (item2node.contains(nodeItem)) {
            emit nodeDoubleClicked(item2node[nodeItem]);
        }
        else {
            QGraphicsSimpleTextItem* textItem = dynamic_cast<QGraphicsSimpleTextItem*>(item);
            if (textItem2node.contains(textItem)) {
                emit nodeDoubleClicked(textItem2node[textItem]);
            }
        }
    }
}

//----------------------- resizeEvent ------------------------
void GraphDisplayWidget::resizeEvent(QResizeEvent* resizeEvent) {
    QGraphicsView::resizeEvent(resizeEvent);
    autoScale();
}

//----------------------- mouseReleaseEvent ------------------------
void GraphDisplayWidget::mouseReleaseEvent(QMouseEvent* mouseEvent) {
    QGraphicsView::mouseReleaseEvent(mouseEvent);
    autoScale();
}

//----------------------- autoScale ------------------------
void GraphDisplayWidget::autoScale() {
    // Fit the view to the adjusted rectangle
    fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio);
}

//----------------------- setNodeDiameter ------------------------
void GraphDisplayWidget::setNodeDiameter(float d) {
    diameter = d;
    refresh();
}

//----------------------- refresh ------------------------
void GraphDisplayWidget::refresh() {
    QList<const void*> nodes = node2item.keys();
    // Iy you need to order nodes by number of links, sort the nodes and then check size
    // std::sort(nodes.begin(), nodes.end(), [=](const void* & a, const void* & b){ return nodeArity[a] < nodeArity[b]; });

    // add the first node in the center and spread the rest of the nodes as evenly as possible on a circle
    // other possible placement algorithm include
    // - adapt node diameter and circle diameter to number of nodes
    // - add a specific node (e.g. World Frame) in the center, put nodes with links to it in a first circle
    //   and add the others along a second circle outside the first one
    const void* firstNode = nodes.first();
    node2item[firstNode]->setPos(- diameter / 2.0, - diameter / 2.0);
    nodes.removeFirst();

    float angle = 0;
    float angleStep = 2.0 * std::numbers::pi / nodes.size();
    for (const void*& n : nodes) {
        QGraphicsEllipseItem* item = node2item[n];
        if (item != nullptr) {
            item->setX(3.0 * (std::cos(angle) * diameter) - diameter / 2.0);
            item->setY(3.0 * (std::sin(angle) * diameter) - diameter / 2.0);
        }
        angle += angleStep;
    }

    // Update the links
    updateLinkPosition();

    autoScale();
}

//----------------------- clear ------------------------
void GraphDisplayWidget::clear() {
    // empty all data intern structures
    nodeArity.clear();
    node2item.clear();
    node2textItem.clear();
    link2arrowHead.clear();
    item2node.clear();
    textItem2node.clear();
    links.clear();
    link2item.clear();
    item2link.clear();
    unselectedPen.clear();
    // remove and deletes all items from the scene
    scene()->clear();
}

//----------------------- addNode ------------------------
void GraphDisplayWidget::addNode(const void* ptr, QString tooltip, QColor color, QColor linecolor, QString internalText) {
    QPen unselectedNodePen = QPen(linecolor);
    node2item[ptr] = scene()->addEllipse(0, 0, diameter, diameter, unselectedNodePen, QBrush(color));
    unselectedPen[ptr] = unselectedNodePen;
    node2item[ptr]->setFlag(QGraphicsItem::ItemIsSelectable);
    node2item[ptr]->setToolTip(tooltip);
    node2item[ptr]->setFlag(QGraphicsItem::ItemIsMovable);
    node2item[ptr]->setFlag(QGraphicsItem::ItemSendsGeometryChanges);
    item2node[node2item[ptr]] = ptr;
    nodeArity[ptr] = 0;

    if (!internalText.isEmpty()) {
        node2textItem[ptr] = new QGraphicsSimpleTextItem(internalText, node2item[ptr]);
        // Calculate the center of the ellipse
        double ellipseCenterX = node2item[ptr]->rect().center().x();
        double ellipseCenterY = node2item[ptr]->rect().center().y();
        // Calculate the position to center the text within the ellipse
        double textX = ellipseCenterX - node2textItem[ptr]->boundingRect().width() / 2;
        double textY = ellipseCenterY - node2textItem[ptr]->boundingRect().height() / 2;
        // Set the position of the text item
        node2textItem[ptr]->setPos(textX, textY);
        textItem2node[node2textItem[ptr]] = ptr;
    }
}

//----------------------- addLink ------------------------
void GraphDisplayWidget::addLink(const void* ptr, const void* from, const void* to, QString tooltip, const QPen& pen, bool arrowHead) {
    links[ptr] = std::make_pair(from, to);
    link2item[ptr] = scene()->addLine(0, 0, 1, 1, pen);
    unselectedPen[ptr] = pen;
    link2item[ptr]->setZValue(-1.0);  // lines below nodes
    link2item[ptr]->setFlag(QGraphicsItem::ItemIsSelectable);
    link2item[ptr]->setToolTip(tooltip);
    item2link[link2item[ptr]] = ptr;
    nodeArity[from]++;
    nodeArity[to]++;

    if (arrowHead) {
        // In QGraphicsView, (0,0) is centered in the view, x goes toward the right, y goes downward
        QPolygonF arrowHead;
        arrowHead << QPointF(0, 0) << QPointF(-diameter / 4.0, -diameter / 8.0) << QPointF(-diameter / 4.0, diameter / 8.0);
        QPen arrowPen = pen;
        arrowPen.setStyle(Qt::SolidLine);
        link2arrowHead[ptr] = scene()->addPolygon(arrowHead, arrowPen, QBrush(QColorConstants::White));
    }
}

//----------------------- updateLinkPosition ------------------------
void GraphDisplayWidget::updateLinkPosition() {
    // line position
    for (const void* link : links.keys()) {
        link2item[link]->setLine(node2item[links[link].first]->x() + diameter / 2.0,
                                 node2item[links[link].first]->y() + diameter / 2.0,
                                 node2item[links[link].second]->x() + diameter / 2.0,
                                 node2item[links[link].second]->y() + diameter / 2.0);
    }

    // arrow orientation
    for (const void* link : link2arrowHead.keys()) {
        QLineF line = link2item[link]->line();
        double angle = line.angle();
        link2arrowHead[link]->setRotation(-angle);
        QLineF unitVector = line.unitVector();
        link2arrowHead[link]->setPos(line.p2() - QPointF((diameter / 2.0)* unitVector.dx(), (diameter / 2.0) * unitVector.dy()));
    }
}

//----------------------- updateView ------------------------
void GraphDisplayWidget::updateView() {
    scene()->blockSignals(true);

    //-- check current selection
    QList<QGraphicsItem*> newSelectedItems = scene()->selectedItems();

    // now that we have the list of selected items, clear the selection
    // so that no dash-line rectangle is drawn by the scene
    scene()->clearSelection();

    // Now update the selection state
    const void* previouslySelectedNode = currentSelectedNode;
    const void* previouslySelectedLink = currentSelectedLink;
    for (QGraphicsItem* item : newSelectedItems) {
        QGraphicsLineItem* linkItem = dynamic_cast<QGraphicsLineItem*>(item);
        if (linkItem != nullptr && item2link.contains(linkItem)) {
            // This is a link
            setSelectedLink(item2link[linkItem]);
        }
        else {
            QGraphicsEllipseItem* nodeItem = dynamic_cast<QGraphicsEllipseItem*>(item) ;
            if (nodeItem != nullptr && item2node.contains(nodeItem)) {
                // this is a node
                setSelectedNode(item2node[nodeItem]);
            }
        }
    }

    if (previouslySelectedNode != currentSelectedNode) {
        emit nodeSelectionChanged(currentSelectedNode);
    }

    if (previouslySelectedLink != currentSelectedLink) {
        emit linkSelectionChanged(currentSelectedLink);
    }

    updateLinkPosition();

    scene()->blockSignals(false);
}


//----------------------- setSelectedNode ------------------------
void GraphDisplayWidget::setSelectedNode(const void* selected) {
    // reset pen for all nodes
    for (const void* node : node2item.keys()) {
        node2item[node]->setPen(unselectedPen[node]);
    }

    // highlight selection (do not modify the selection state of the QGraphicsItem otherwise
    // the scene will draw the bounding rectangle using dash line, which is
    // not visually satisfying)
    if (node2item.contains(selected)) {
        node2item[selected]->setPen(selectedPen);
    }

    currentSelectedNode = selected;
}

//----------------------- setSelectedLink ------------------------
void GraphDisplayWidget::setSelectedLink(const void* selected) {
    // reset pen for all links and arrow head
    for (const void* link : link2item.keys()) {
        QPen unselectedLinkPen = unselectedPen[link];
        link2item[link]->setPen(unselectedLinkPen);
        unselectedLinkPen.setStyle(Qt::SolidLine);
        link2arrowHead[link]->setPen(unselectedLinkPen);
    }

    // highlight selection (do not modify the selection state of the QGraphicsItem otherwise
    // the scene will draw the bounding rectangle using dash line, which is
    // not visually satisfying)
    if (link2item.contains(selected)) {
        QPen unselectedLinkPen = unselectedPen[selected];
        link2item[selected]->setPen(selectedPen);
        unselectedLinkPen.setStyle(Qt::SolidLine);
        link2arrowHead[selected]->setPen(selectedPen);
    }

    currentSelectedLink = selected;
}
