# GraphWidget.py -- Pamhyr
# Copyright (C) 2023-2026  INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# -*- coding: utf-8 -*-

import math
import logging

from tools import timer

from PyQt5.QtCore import (
    Qt, QPoint, QPointF, QSizeF, QLineF, QRectF,
    pyqtSlot, pyqtSignal, QCoreApplication,
)
from PyQt5.QtGui import (
    QPainter, QColor, QBrush, QPen, QPainterPath,
    QPolygonF,
)
from PyQt5.QtWidgets import (
    QApplication, QGraphicsScene, QGraphicsView,
    QGraphicsItem, QGraphicsTextItem, QMenu,
)

from Model.Network.Node import Node
from Model.Network.Edge import Edge
from Model.Network.Graph import Graph

from View.Network.ProfileDialog import SelectProfileDialog
from View.Network.UndoCommand import *
from View.Network.ContextMenu import (
    DefaultMenu, NodeMenu, EdgeMenu,
)

logger = logging.getLogger()

_translate = QCoreApplication.translate


class NodeItem(QGraphicsItem):
    Type = QGraphicsItem.UserType + 1

    def __init__(self, node, graph_widget):
        super(NodeItem, self).__init__()

        self.node = node
        self.setPos(QPointF(self.node.pos.x, self.node.pos.y))

        self.graph = graph_widget

        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        self.setZValue(1)

    def type(self):
        return NodeItem.Type

    def boundingRect(self):
        adjust = 2.0
        return QRectF(-10 - adjust, -10 - adjust, 20 + adjust, 20 + adjust)

    def shape(self):
        path = QPainterPath()
        reservoir = self.graph.graph._reservoir.get_assoc_to_node(self.node)

        if reservoir is None or reservoir.is_deleted():
            path.addEllipse(-10, -10, 20, 20)
        else:
            path.addRect(-10, -10, 20, 20)

        return path

    @timer
    def paint(self, painter, option, widget):
        painter.setPen(Qt.NoPen)

        # Select color in function of node position in graph and
        # status
        color = Qt.darkBlue
        if self.graph.selected_new_edge_src_node() == self:
            color = Qt.darkRed
        elif self.graph.selected_item() == self:
            color = Qt.red
        elif not self.graph.graph.is_enable_node(self.node):
            color = Qt.darkGray
        elif self.graph.graph.is_upstream_node(self.node):
            color = Qt.yellow
        elif self.graph.graph.is_downstream_node(self.node):
            color = Qt.green

        painter.setBrush(QBrush(color))
        reservoir = self.graph.graph._reservoir.get_assoc_to_node(self.node)

        if reservoir is None or reservoir.is_deleted():
            painter.drawEllipse(-10, -10, 20, 20)
        else:
            painter.drawRect(-10, -10, 20, 20)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionHasChanged:
            self.graph.node_change_position(value, self)

        self.graph.update_edges(self)
        return super(NodeItem, self).itemChange(change, value)

    def mousePressEvent(self, event):
        self.update()
        if not self.graph._only_display:
            super(NodeItem, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.update()
        super(NodeItem, self).mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        self.update()
        if not self.graph._only_display:
            super(NodeItem, self).mouseMoveEvent(event)


class EdgeItem(QGraphicsItem):
    _arc_dist = 0.1

    Type = QGraphicsItem.UserType + 2

    def __init__(self, src_node_item, dest_node_item,
                 edge, graph_widget):
        super(EdgeItem, self).__init__()

        self.src_node = src_node_item
        self.dest_node = dest_node_item
        self.edge = edge

        self.graph = graph_widget
        self._ind = 1

        geometry = self.compute_arc_two_point(
            QPointF(
                self.src_node.pos().x(),
                self.src_node.pos().y()
            ),
            QPointF(
                self.dest_node.pos().x(),
                self.dest_node.pos().y()
            ),
        )

        self._bound_rect = geometry['bound_rect']
        self._arc = geometry['arc']

        self.setAcceptedMouseButtons(Qt.NoButton)

    def type(self):
        return Edge.Type

    def boundingRect(self):
        # Rectangle of edge for display update
        pen_width = 2.0
        extra = 0  # (pen_width + 5) / 2.0

        return self._bound_rect.normalized().adjusted(
            -extra, -extra, extra, extra
        )

    def shape(self):
        return self._arc

    def paint(self, painter, option, widget):
        # self.paint_line(painter, option, widget)
        self.paint_arc(painter, option, widget)

    @timer
    def paint_line(self, painter, option, widget):
        # Draw shape of the edge
        # color = QColor(Qt.white)
        # color.setAlpha(128)
        # painter.setBrush(color)
        # painter.drawPath(self.shape())

        line = QLineF(self.src_node.pos(), self.dest_node.pos())
        if line.length() == 0.0:
            return

        # Select color
        # color = Qt.darkBlue
        color = Qt.black
        if self.graph.selected_item() == self:
            color = Qt.red
        elif self.graph.current_edge() == self:
            color = Qt.blue
        elif not self.graph.graph.is_enable_edge(self.edge):
            color = Qt.darkGray

        painter.setPen(
            QPen(
                color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
            )
        )

        painter.drawLine(line)

        # Draw arrow
        center = QPointF(
            (self.src_node.pos().x() + self.dest_node.pos().x()) / 2,
            (self.src_node.pos().y() + self.dest_node.pos().y()) / 2,
        )
        angle = math.acos(line.dx() / line.length())
        if line.dy() >= 0:
            angle = (math.pi * 2.0) - angle

        self.paint_arrow(
            painter, option, widget,
            color, center, angle
        )

    @timer
    def paint_arc(self, painter, option, widget):
        line = QLineF(self.src_node.pos(), self.dest_node.pos())
        if line.length() == 0.0:
            return

        # Select color
        # color = Qt.darkBlue
        color = Qt.black
        if self.graph.selected_item() == self:
            color = Qt.red
        elif self.graph.current_edge() == self:
            color = Qt.blue
        elif not self.graph.graph.is_enable_edge(self.edge):
            color = Qt.darkGray

        painter.setPen(
            QPen(
                color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
            )
        )

        geometry = self.compute_arc_two_point(
            QPointF(
                self.src_node.pos().x(),
                self.src_node.pos().y()
            ),
            QPointF(
                self.dest_node.pos().x(),
                self.dest_node.pos().y()
            ),
        )

        self._bound_rect = geometry['bound_rect']
        self._arc = geometry['arc']

        painter.drawPath(self._arc)

        line = geometry['line']
        angle = math.acos(line.dx() / line.length())
        if line.dy() >= 0:
            angle = (math.pi * 2.0) - angle

        self.paint_arrow(
            painter, option, widget,
            color, geometry['c'], angle,
        )

    def compute_arc_two_point(self, a, b):
        # This function is a black magic spell for invoking an arc
        # path between two points in Qt! (In fact, it's just cryptic
        # trigonometry functions from stackoverflow:
        # https://stackoverflow.com/questions/26901540/arc-in-qgraphicsscene)
        c = self.compute_arc_p3(a, b)

        line_ac = QLineF(a, c)
        line_bc = QLineF(b, c)
        line_ab = QLineF(a, b)

        rad = abs(
            line_bc.length() / (
                2 * math.sin(
                    math.radians(
                        line_ac.angleTo(line_ab)
                    )
                )
            )
        )

        bisector_bc = QLineF(line_bc.pointAt(0.5), line_bc.p2())
        bisector_bc.setAngle(line_bc.normalVector().angle())

        bisector_ab = QLineF(line_ab.pointAt(0.5), line_ab.p2())
        bisector_ab.setAngle(line_ab.normalVector().angle())

        center = QPointF(0, 0)
        bisector_ab.intersect(bisector_bc, center)

        circle = QRectF(
            center.x() - rad,
            center.y() - rad,
            rad * 2, rad * 2
        )

        line_oa = QLineF(center, a)
        line_ob = QLineF(center, b)
        line_oc = QLineF(center, c)

        start = line_ob
        end = line_oa

        w = 1
        bounding_rect = circle.adjusted(
            -w, -w, w, w
        )

        path = QPainterPath()
        path.arcMoveTo(circle, start.angle())
        path.arcTo(circle, start.angle(), start.angleTo(end))

        return {
            'bound_rect': bounding_rect,
            'arc': path,
            'c': c,
            'line': line_ab,
        }

    def compute_arc_p3(self, p1, p2):
        dist_p3 = self._ind * self._arc_dist

        center_p1_p2 = QPointF(
            (p1.x() + p2.x()) / 2,
            (p1.y() + p2.y()) / 2
        )

        # u = (p2.x() - p1.x(), p2.y() - p1.y())
        v = (p2.y() - p1.y(), p1.x() - p2.x())
        p3 = QPointF(
            center_p1_p2.x() + dist_p3 * v[0],
            center_p1_p2.y() + dist_p3 * v[1]
        )

        return p3

    def paint_arrow(self, painter, option, widget,
                    color, center, angle):
        brush = QBrush()
        brush.setColor(color)
        brush.setStyle(Qt.SolidPattern)

        size = 10.0
        arrow_p1 = center + QPointF(
            math.sin(angle - math.pi / 3) * size,
            math.cos(angle - math.pi / 3) * size
        )
        arrow_p2 = center + QPointF(
            math.sin(angle - math.pi + math.pi / 3) * size,
            math.cos(angle - math.pi + math.pi / 3) * size
        )
        poly = QPolygonF([center, arrow_p1, arrow_p2])
        path = QPainterPath()
        path.addPolygon(poly)

        painter.drawPolygon(poly)
        painter.fillPath(path, brush)


class NodeText(QGraphicsTextItem):
    def __init__(self, node_item, graph):
        super(NodeText, self).__init__()

        self.item = node_item
        self.graph = graph

        name = self.item.node.name
        if name == "":
            name = self.graph._trad.node_name(self.item.node)

        self.setPlainText(name)

        self.setDefaultTextColor(Qt.black)
        self.set_custom_pos(self.item.pos())
        self.setZValue(2)

    def set_custom_pos(self, pos):
        x = pos.x()
        y = pos.y() - 42  # Dont panic! The answer is 42

        self.setPos(QPointF(x, y))

    @timer
    def paint(self, painter, option, widget):
        color = QColor(Qt.white)
        color.setAlpha(128)

        # Draw text background
        painter.setBrush(color)
        painter.drawRect(self.boundingRect())

        # Draw text
        super(NodeText, self).paint(painter, option, widget)

    def rename(self):
        # Update the node text
        self.setPlainText(self.item.node.name)


class NewEdgeLine(QGraphicsItem):
    def __init__(self, src, dest):
        super(NewEdgeLine, self).__init__()

        self.src = src
        self.dest = dest

    def boundingRect(self):
        pen_width = 2.0
        extra = (pen_width + 5) / 2.0

        return QRectF(
            self.src,
            QSizeF(
                self.dest.x() - self.src.x(),
                self.dest.y() - self.src.y()
            )
        ).normalized().adjusted(-extra, -extra, extra, extra)

    @timer
    def paint(self, painter, option, widget):
        line = QLineF(self.src, self.dest)
        if line.length() == 0.0:
            return

        color = Qt.darkGray
        painter.setPen(QPen(color, 2, Qt.SolidLine, Qt.RoundCap,
                            Qt.RoundJoin))
        painter.drawLine(line)


class GraphWidget(QGraphicsView):
    changeEdge = pyqtSignal(object)
    changeNode = pyqtSignal(object)

    def __init__(self, graph, parent=None,
                 min_size=(400, 400), max_size=None,
                 size=None, only_display=False, undo=None,
                 trad=None):
        super(GraphWidget, self).__init__(parent=parent)

        self.parent = parent
        self._state = "move"
        self._only_display = only_display
        self._undo = undo
        self._trad = trad

        self.graph = graph

        self._selected_item = None
        self._selected_new_edge_src_node = None
        self._current_edge = None
        self._current_moved_node = None
        self.tmp_line = None

        self.node_items = []
        self.edge_items = []
        self.texts = {}

        self.m_origin_x = 0.0
        self.m_origin_y = 0.0
        self.clicked = False

        self.setup_scene(min_size, max_size, size)

    def setup_scene(self, min_size, max_size, size):
        scene = QGraphicsScene(self)
        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
        scene.setSceneRect(0, 0, 6000, 6000)

        self.setScene(scene)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        self.setRenderHint(QPainter.Antialiasing)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorViewCenter)

        self.scale(1, 1)
        self.previousScale = 1
        self.centerOn(1000.0, 1000.0)

        if min_size:
            self.setMinimumSize(*min_size)
        if max_size:
            self.setMaximumSize(*max_size)
        if size:
            self.resize(*size)

        self.create_items()

    @timer
    def create_items(self):
        """Create all items and draw its

        Returns:
            Nothing
        """
        self.node_items = []
        self.edge_items = []
        self.texts = {}

        for node in self.graph.nodes():
            inode = NodeItem(node, self)
            self.texts[inode] = NodeText(inode, self)
            self.scene().addItem(self.texts[inode])
            self.scene().addItem(inode)
            self.node_items.append(inode)

        curr_edge = self.graph.current_reach()

        iedges = []
        multiple_edges = {}
        for edge in self.graph.edges():
            n1 = next(
                filter(
                    lambda n: n.node == edge.node1,
                    self.node_items
                )
            )

            n2 = next(
                filter(
                    lambda n: n.node == edge.node2,
                    self.node_items
                )
            )

            # Multiple edges counter
            if (n1, n2) not in multiple_edges:
                ind = 1
            else:
                ind = multiple_edges[(n1, n2)] + 1

            multiple_edges[(n1, n2)] = ind

            iedge = EdgeItem(n1, n2, edge, self)
            iedge._ind = ind

            if edge == curr_edge:
                self._current_edge = iedge

            iedges.append(iedge)

        for iedge in reversed(iedges):
            self.scene().addItem(iedge)
            self.edge_items.append(iedge)

    def state(self, status):
        """Set the current status of draw widget

        Set the current status of draw widget, like "move", "add" or
        "del"

        Args:
            status: String of current status

        Returns:
            Nothing
        """
        self._state = status

    def add_node(self, pos):
        """Create a new node in graph and draw it

        Args:
            pos: The position of new node

        Returns:
            Nothing
        """
        node = self.graph.create_node(pos.x(), pos.y())
        self._undo.push(
            AddNodeCommand(
                self.graph, node
            )
        )
        inode = NodeItem(node, self)
        self.scene().addItem(inode)
        self.node_items.append(inode)
        self.texts[inode] = NodeText(inode, self)
        self.scene().addItem(self.texts[inode])

        self.changeNode.emit(self.sender())

    def del_node(self, item):
        """Delete a node and update display

        Args:
            item: The node item to delete

        Returns:
            Nothing
        """
        node = item.node
        self._undo.push(
            DelNodeCommand(
                self.graph, node
            )
        )

        self.scene().clear()
        self.create_items()
        self.changeNode.emit(self.sender())
        self.changeEdge.emit(self.sender())

    def rename_nodes(self):
        """Update all nodes item name in scene

        Returns:
            Nothing
        """
        for i in self.texts:
            if type(i) is NodeItem:
                self.texts[i].rename()

    def add_node_reservoir(self, node):
        self.parent.add_node_reservoir(node)
        self.display_update()

    def del_node_reservoir(self, node):
        self.parent.del_node_reservoir(node)
        self.changeNode.emit(self.sender())
        self.display_update()

    def edit_node_reservoir(self, node):
        self.changeNode.emit(self.sender())
        self.parent.edit_node_reservoir(node)

    def enable_edge(self, edge, prev):
        self._undo.push(
            EnableEdgeCommand(
                edge.edge, not prev
            )
        )
        self.changeEdge.emit(self.sender())
        self.changeNode.emit(self.sender())
        self.display_update()

    def reverse_edge(self, edge):
        self._undo.push(
            ReverseEdgeCommand(
                edge.edge
            )
        )
        self.changeEdge.emit(self.sender())
        self.display_update()

    def display_update(self):
        """Clear the scene and redraw it

        Returns:
            Nothing
        """
        self.scene().clear()
        self.create_items()

    def add_edge(self, node1, node2):
        """Create a new edge in graph and draw it

        Args:
            node1: The source node
            node2: The desitnation node

        Returns:
            Nothing
        """
        if node1 == node2:
            return

        edge = self.graph.create_edge(node1.node, node2.node)
        self._undo.push(
            AddEdgeCommand(
                self.graph, edge
            )
        )
        self.set_selected_item(None)
        self.set_selected_new_edge_src_node(None)
        # Reset the temporary line
        self.tmp_line = None

        # Clear all scene and redraw it
        self.scene().clear()
        self.create_items()

        self.changeEdge.emit(self.sender())

    def del_edge(self, item):
        """Delete an edge and update display

        Args:
            item: The edge item to delete

        Returns:
            Nothing
        """
        edge = item.edge
        self._undo.push(
            DelEdgeCommand(
                self.graph, edge
            )
        )

        # Clear all scene and redraw it
        self.scene().clear()
        self.create_items()

        self.changeNode.emit(self.sender())
        self.changeEdge.emit(self.sender())

    def split_edge(self, item):
        edge = item.edge

        dlg = SelectProfileDialog(
            study=self.parent._study,
            reach=edge,
            trad=self._trad,
            parent=self.parent,
        )
        if not dlg.exec():
            return

        profile = dlg._profile
        self._undo.push(
            SplitEdgeCommand(
                self.graph, edge, profile
            )
        )

        self.scene().clear()
        self.create_items()

        self.changeNode.emit(self.sender())
        self.changeEdge.emit(self.sender())

    def update_edges(self, node):
        """Update display of all edges linked with the node

        Args:
            node: The node item linked with edges to update

        Returns:
            Nothing
        """
        edges = list(
            filter(
                lambda ie: (ie.edge.node1 == node.node or
                            ie.edge.node2 == node.node),
                self.edge_items
            )
        )

        for edge in edges:
            edge.update()

    def node_change_position(self, pos, node):
        """Update node position and node text position

        Args:
            pos: The new position
            node: The node item

        Returns:
            Nothing
        """
        node.node.setPos(pos.x(), pos.y())
        self.texts[node].set_custom_pos(pos)
        self.texts[node].update()

    def selected_item(self):
        """Current selected item

        Returns:
            Item if item ar selected, otherelse None
        """
        return self._selected_item

    def set_selected_item(self, item):
        """Set current selected item

        Args:
            item: The new item to select

        Returns:
            Nothing
        """
        try:
            previous_item = self._selected_item
            self._selected_item = item

            if previous_item:
                previous_item.update()

                if item:
                    item.update()
        except Exception as e:
            logger.warning(str(e))

    def selected_new_edge_src_node(self):
        """The current node item selected to add new edge

        Returns:
            Item if item ar selected, otherelse None
        """
        return self._selected_new_edge_src_node

    def set_selected_new_edge_src_node(self, node):
        """Set the current node item selected to add new edge

        Args:
            node: The new node to select

        Returns:
            Nothing
        """
        try:
            previous_node = self._selected_new_edge_src_node
            self._selected_new_edge_src_node = node

            if node is None:
                if self.tmp_line is not None:
                    self.tmp_line = None

            if previous_node is not None:
                previous_node.update()
        except Exception as e:
            logger.warning(str(e))

    def current_edge(self):
        """The current selected edge

        Returns:
            Item if edge are selected, otherelse None
        """
        return self._current_edge

    def set_current_edge(self, edge):
        """Set the current edge item selected

        Args:
            edge: The new edge to select

        Returns:
            Nothing
        """
        try:
            previous_edge = self._current_edge
            self._current_edge = edge
            self.graph.set_current_reach(edge.edge)

            if previous_edge:
                previous_edge.update()
        except Exception as e:
            logger.warning(str(e))

    def reset_selection(self):
        """Reset all the selected items

        Returns:
            Nothing
        """
        self.set_selected_new_edge_src_node(None)
        if self.tmp_line is not None:
            self.tmp_line = None
            self.scene().clear()
            self.create_items()

    def keyPressEvent(self, event):
        key = event.key()

        if key == Qt.Key_Plus:
            self.scaleView(1.2)
        elif key == Qt.Key_Minus:
            self.scaleView(1 / 1.2)
        elif key == Qt.Key_Escape:
            self.reset_selection()
        else:
            super(GraphWidget, self).keyPressEvent(event)

    def drawBackground(self, painter, rect):
        sceneRect = self.sceneRect()
        painter.fillRect(
            rect.intersected(sceneRect),
            QBrush(Qt.lightGray)
        )
        painter.setBrush(Qt.NoBrush)
        painter.drawRect(sceneRect)

    def wheelEvent(self, event):
        factor = math.pow(2.0, event.angleDelta().y() / 240.0)

        old_pos = self.mapToScene(event.pos())

        self.scaleView(factor)

        new_pos = self.mapToScene(event.pos())

        delta = old_pos - new_pos

        # Compensation pour garder le point sous la souris fixe
        self.translate(delta.x(), delta.y())

    def scaleView(self, scaleFactor):
        factor = self.transform().scale(
            scaleFactor, scaleFactor
        ).mapRect(QRectF(0, 0, 1, 1)).width()

        if factor < 0.05 or factor > 1.5:
            return

        self.scale(scaleFactor, scaleFactor)

    def mousePressEvent(self, event):
        if self._only_display:
            return

        locked = self.graph._status.is_read_only()

        pos = self.mapToScene(event.pos())
        self.clicked = True

        if event.buttons() & Qt.RightButton:
            self.update()
            super(GraphWidget, self).mousePressEvent(event)
            return

        # Move item and select edge item
        if self._state == "move":
            self._selected_new_edge_src_node = None

            items = self.items(event.pos())
            if items and type(items[0]) is EdgeItem:
                edge = items[0]
                if edge:
                    self.set_current_edge(edge)
            if not locked:
                if items and type(items[0]) is NodeItem:
                    self._mouse_origin_x = pos.x()
                    self._mouse_origin_y = pos.y()
                    self._current_moved_node = items[0]

        if not locked:
            # Add nodes and edges
            if self._state == "add":
                items = self.items(event.pos())
                nodes = list(filter(lambda i: type(i) is NodeItem, items))
                if not nodes:
                    self.add_node(pos)
                    self.set_selected_new_edge_src_node(None)
                else:
                    if self.selected_new_edge_src_node() is None:
                        self.set_selected_new_edge_src_node(nodes[0])
                    else:
                        self.add_edge(self.selected_new_edge_src_node(),
                                      nodes[0])

            # Delete nodes and edges
            elif self._state == "del":
                self._selected_new_edge_src_node = None
                items = list(
                    filter(
                        lambda i: type(i) is NodeItem or type(i) is EdgeItem,
                        self.items(event.pos())
                    )
                )
                if len(items) > 0:
                    item = items[0]
                    if type(item) is NodeItem:
                        self.del_node(item)
                    elif type(item) is EdgeItem:
                        self.del_edge(item)

        self.update()
        super(GraphWidget, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.clicked = False

        if self._only_display:
            return

        locked = self.graph._status.is_read_only()

        if not locked and self._state == "move":
            if self._current_moved_node is not None:
                pos = self.mapToScene(event.pos())
                self._undo.push(
                    SetNodePosCommand(
                        self._current_moved_node,
                        (pos.x(), pos.y()),
                        (self._mouse_origin_x,
                         self._mouse_origin_y)
                    )
                )

        self.update()
        super(GraphWidget, self).mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        pos = self.mapToScene(event.pos())
        locked = self.graph._status.is_read_only()

        # Selecte item on the fly
        items = self.items(event.pos())
        selectable_items = list(
            filter(
                lambda i: (type(i) is NodeItem or type(i) is EdgeItem),
                items
            )
        )

        if selectable_items:
            self.set_selected_item(selectable_items[0])
        elif not self.clicked:
            self.set_selected_item(None)

        # Update temporary line
        if self.selected_new_edge_src_node() is not None:
            if self.tmp_line is None:
                self.tmp_line = NewEdgeLine(
                    self.selected_new_edge_src_node().pos(),
                    pos
                )
                self.scene().addItem(self.tmp_line)
            else:
                self.tmp_line.dest = pos
            self.tmp_line.update()

        # If state is "move"
        if self._state == "move":
            # Move on scene
            if not self.selected_item():
                if event.buttons() & Qt.LeftButton:
                    old_p = self.mapToScene(
                        int(self.m_origin_x), int(self.m_origin_y)
                    )
                    new_p = self.mapToScene(event.pos())
                    translation = new_p - old_p

                    self.translate(translation.x(), translation.y())

                    self.m_origin_x = event.x()
                    self.m_origin_y = event.y()

            # Propagate event
            self.update()
            super(GraphWidget, self).mouseMoveEvent(event)

    # Contextual menu

    def contextMenuEvent(self, event):
        if self._only_display or self.graph._status.is_read_only():
            return

        pos = self.mapToScene(event.pos())
        items = self.items(event.pos())

        # Select current menu
        while len(items) > 0:
            if type(items[0]) in [NodeItem, EdgeItem]:
                break
            else:
                items = items[1:]

        if len(items) == 0:
            m_type = DefaultMenu
        elif type(items[0]) is NodeItem:
            m_type = NodeMenu
        elif type(items[0]) is EdgeItem:
            m_type = EdgeMenu
        else:
            m_type = DefaultMenu

        # Create and exec menu
        m = m_type(
            event=event, pos=pos, items=items,
            graph=self.graph, trad=self._trad, parent=self
        )
        m.run()
        self.clicked = False
