class Item(IntEnum_):
    """
    This class defines all the available elements for graphol diagrams.
    """
    # PREDICATES
    ConceptNode = 65537
    AttributeNode = 65538
    RoleNode = 65539
    ValueDomainNode = 65540
    IndividualNode = 65541

    # CONSTRUCTORS
    DomainRestrictionNode = 65542
    RangeRestrictionNode = 65543
    UnionNode = 65544
    EnumerationNode = 65545
    ComplementNode = 65546
    RoleChainNode = 65547
    IntersectionNode = 65548
    RoleInverseNode = 65549
    DatatypeRestrictionNode = 65550
    DisjointUnionNode = 65551
    PropertyAssertionNode = 65552
    FacetNode = 65553

    # EDGES
    InclusionEdge = 65554
    EquivalenceEdge = 65555
    InputEdge = 65556
    MembershipEdge = 65557

    # EXTRA
    Label = 65558
    Undefined = 65559

    @property
    def realName(self):
        """
        Returns the item readable name, i.e: attribute node, concept node.
        :rtype: str
        """
        return RE_CAMEL_SPACE.sub('\g<1> \g<2>', self.name).lower()

    @property
    def shortName(self):
        """
        Returns the item short name, i.e: attribute, concept.
        :rtype: str
        """
        return rstrip(self.realName, ' node', ' edge') class Identity(Enum_):
    """
    This class defines graphol expression identities.
    """
    Neutral = 'Neutral'
    Concept = 'Concept'
    Role = 'Role'
    Attribute = 'Attribute'
    ValueDomain = 'Value Domain'
    Individual = 'Individual'
    Value = 'Value'
    RoleInstance = 'Role Instance'
    AttributeInstance = 'Attribute Instance'
    Facet = 'Facet'
    Unknown = 'Unknown' class Restriction(Enum_):
    """
    This class defines all the available restrictions for domain and range restriction nodes.
    """
    Exists = 'Existential: exists'
    Forall = 'Universal: forall'
    Cardinality = 'Cardinality: (min, max)'
    Self = 'Self: self'

    @property
    def shortName(self):
        """
        Returns the restriction short name, i.e: existential, universal, cardinality, self.
        :rtype: str
        """
        return self.value[:self.value.index(':')].lower()

    @classmethod
    def forLabel(cls, value):
        """
        Returns the restriction matching the given label.
        :type value: str
        :rtype: Restriction
        """
        value = value.lower().strip()
        for x in {Restriction.Exists, Restriction.Forall, Restriction.Self}:
            if value == x.toString():
                return x
        match = RE_CARDINALITY.match(value)
        if match:
            return Restriction.Cardinality
        return None

    def toString(self, *args):
        """
        Returns a formatted representation of the restriction.
        :rtype: str
        """
        return {
            Restriction.Exists: 'exists',
            Restriction.Forall: 'forall',
            Restriction.Self: 'self',
            Restriction.Cardinality: '({0},{1})',
        }[self].format(*('-' if x is None else x for x in args)) class File(Enum_):
    """
    Enum implementation to deal with file types.
    """
    Bmp = 'Bitmap (*.bmp)'
    Csv = 'Comma-separated values (*.csv)'
    GraphML = 'GraphML (*.graphml)'
    Graphol = 'Graphol (*.graphol)'
    Html = 'Hyper-Text Markup Language (*.html)'
    Jpeg = 'JPEG (*.jpg)'
    Owl = 'Web Ontology Language (*.owl)'
    Pdf = 'Portable Document Format (*.pdf)'
    Png = 'PNG (*.png)'
    Qss = 'Qt Style Sheet (*.qss)'
    Spec = 'Plugin SPEC (*.spec)'
    Zip = 'ZIP (*.zip)'
    Xml = 'XML (*.xml)'

    @classmethod
    def forPath(cls, path):
        """
        Returns the File matching the given path.
        :type path: str
        :rtype: File
        """
        for x in cls:
            if path.endswith(x.extension):
                return x
        return None

    @property
    def extension(self):
        """
        The extension associated with the Enum member.
        :rtype: str
        """
        match = RE_FILE_EXTENSION.match(self.value)
        if match:
            return match.group('extension')
        return None

    def __lt__(self, other):
        """
        Returns True if the current File is "lower" than the given other one.
        :type other: File
        :rtype: bool
        """
        return self.value < other.value

    def __gt__(self, other):
        """
        Returns True if the current File is "greater" than the given other one.
        :type other: File
        :rtype: bool
        """
        return self.value > other.value class Diagram(QtWidgets.QGraphicsScene):
    """
    Extension of QtWidgets.QGraphicsScene which implements a single Graphol diagram.
    Additionally to built-in signals, this class emits:

    * sgnItemAdded: whenever an element is added to the Diagram.
    * sgnItemInsertionCompleted: whenever an item 'MANUAL' insertion process is completed.
    * sgnItemRemoved: whenever an element is removed from the Diagram.
    * sgnModeChanged: whenever the Diagram operational mode (or its parameter) changes.
    * sgnUpdated: whenever the Diagram has been updated in any of its parts.
    """
    GridSize = 10
    KeyMoveFactor = 10
    MinSize = 2000
    MaxSize = 1000000
    SelectionRadius = 4

    sgnItemAdded = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem')
    sgnItemInsertionCompleted = QtCore.pyqtSignal('QGraphicsItem', int)
    sgnItemRemoved = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem')
    sgnModeChanged = QtCore.pyqtSignal(DiagramMode)
    sgnNodeIdentification = QtCore.pyqtSignal('QGraphicsItem')
    sgnUpdated = QtCore.pyqtSignal()

    def __init__(self, name, parent):
        """
        Initialize the diagram.
        :type name: str
        :type parent: Project
        """
        super().__init__(parent)

        self.factory = ItemFactory(self)
        self.guid = GUID(self)
        self.mode = DiagramMode.Idle
        self.modeParam = Item.Undefined
        self.name = name
        self.pasteX = Clipboard.PasteOffsetX
        self.pasteY = Clipboard.PasteOffsetY

        self.mo_Node = None
        self.mp_Data = None
        self.mp_Edge = None
        self.mp_Label = None
        self.mp_LabelPos = None
        self.mp_Node = None
        self.mp_NodePos = None
        self.mp_Pos = None

        connect(self.sgnItemAdded, self.onItemAdded)
        connect(self.sgnItemRemoved, self.onItemRemoved)
        connect(self.sgnNodeIdentification, self.doNodeIdentification)

    #############################################
    #   FACTORY
    #################################

    @classmethod
    def create(cls, name, size, project):
        """
        Build and returns a new Diagram instance, using the given parameters.
        :type name: str
        :type size: int
        :type project: Project
        :rtype: Diagram
        """
        diagram = Diagram(name, project)
        diagram.setSceneRect(QtCore.QRectF(-size / 2, -size / 2, size, size))
        diagram.setItemIndexMethod(Diagram.BspTreeIndex)
        return diagram

    #############################################
    #   PROPERTIES
    #################################

    @property
    def project(self):
        """
        Returns the project this diagram belongs to (alias for Diagram.parent()).
        :rtype: Project
        """
        return self.parent()

    @property
    def session(self):
        """
        Returns the session this diagram belongs to (alias for Diagram.project.parent()).
        :rtype: Session
        """
        return self.project.parent()

    #############################################
    #   EVENTS
    #################################

    def dragEnterEvent(self, dragEvent):
        """
        Executed when a dragged element enters the scene area.
        :type dragEvent: QGraphicsSceneDragDropEvent
        """
        super().dragEnterEvent(dragEvent)
        if dragEvent.mimeData().hasFormat('text/plain'):
            dragEvent.setDropAction(QtCore.Qt.CopyAction)
            dragEvent.accept()
        else:
            dragEvent.ignore()

    def dragMoveEvent(self, dragEvent):
        """
        Executed when an element is dragged over the scene.
        :type dragEvent: QGraphicsSceneDragDropEvent
        """
        super().dragMoveEvent(dragEvent)
        if dragEvent.mimeData().hasFormat('text/plain'):
            dragEvent.setDropAction(QtCore.Qt.CopyAction)
            dragEvent.accept()
        else:
            dragEvent.ignore()

    # noinspection PyTypeChecker
    def dropEvent(self, dropEvent):
        """
        Executed when a dragged element is dropped on the diagram.
        :type dropEvent: QGraphicsSceneDragDropEvent
        """
        super().dropEvent(dropEvent)
        if dropEvent.mimeData().hasFormat('text/plain'):
            snapToGrid = self.session.action('toggle_grid').isChecked()
            node = self.factory.create(Item.valueOf(dropEvent.mimeData().text()))
            node.setPos(snap(dropEvent.scenePos(), Diagram.GridSize, snapToGrid))
            self.session.undostack.push(CommandNodeAdd(self, node))
            self.sgnItemInsertionCompleted.emit(node, dropEvent.modifiers())
            dropEvent.setDropAction(QtCore.Qt.CopyAction)
            dropEvent.accept()
        else:
            dropEvent.ignore()

    # noinspection PyTypeChecker
    def mousePressEvent(self, mouseEvent):
        """
        Executed when a mouse button is clicked on the scene.
        :type mouseEvent: QGraphicsSceneMouseEvent
        """
        mouseModifiers = mouseEvent.modifiers()
        mouseButtons = mouseEvent.buttons()
        mousePos = mouseEvent.scenePos()

        if mouseButtons & QtCore.Qt.LeftButton:

            if self.mode is DiagramMode.NodeAdd:

                #############################################
                # NODE INSERTION
                #################################

                snapToGrid = self.session.action('toggle_grid').isChecked()
                node = self.factory.create(Item.valueOf(self.modeParam))
                node.setPos(snap(mousePos, Diagram.GridSize, snapToGrid))
                self.session.undostack.push(CommandNodeAdd(self, node))
                self.sgnItemInsertionCompleted.emit(node, mouseEvent.modifiers())

            elif self.mode is DiagramMode.EdgeAdd:

                #############################################
                # EDGE INSERTION
                #################################

                node = first(self.items(mousePos, edges=False))
                if node:
                    edge = self.factory.create(Item.valueOf(self.modeParam), source=node)
                    edge.updateEdge(target=mousePos)
                    self.mp_Edge = edge
                    self.addItem(edge)

            else:

                # Execute super at first since this may change the diagram
                # mode: some actions are directly handle by graphics items
                # (i.e: edge breakpoint move, edge anchor move, node shape
                # resize) and we need to check whether any of them is being
                # performed before handling the even locally.
                super().mousePressEvent(mouseEvent)

                if self.mode is DiagramMode.Idle:

                    if mouseModifiers & QtCore.Qt.ShiftModifier:

                        #############################################
                        # LABEL MOVE
                        #################################

                        item = first(self.items(mousePos, nodes=False, edges=False, labels=True))
                        if item and item.isMovable():
                            self.clearSelection()
                            self.mp_Label = item
                            self.mp_LabelPos = item.pos()
                            self.mp_Pos = mousePos
                            self.setMode(DiagramMode.LabelMove)

                    else:

                        #############################################
                        # ITEM SELECTION
                        #################################

                        item = first(self.items(mousePos, labels=True))
                        if item:

                            if item.isLabel():
                                # If we are hitting a label, check whether the label
                                # is overlapping it's parent item and such item is
                                # also intersecting the current mouse position: if so,
                                # use the parent item as placeholder for the selection.
                                parent = item.parentItem()
                                items = self.items(mousePos)
                                item =  parent if parent in items else None

                            if item:

                                if mouseModifiers & QtCore.Qt.ControlModifier:
                                    # CTRL => support item multi selection.
                                    item.setSelected(not item.isSelected())
                                else:
                                    if self.selectedItems():
                                        # Some elements have been already selected in the
                                        # diagram, during a previous mouse press event.
                                        if not item.isSelected():
                                            # There are some items selected but we clicked
                                            # on a node which is not currently selected, so
                                            # make this node the only selected one.
                                            self.clearSelection()
                                            item.setSelected(True)
                                    else:
                                        # No item (nodes or edges) is selected and we just
                                        # clicked on one so make sure to select this item and
                                        # because selectedItems() filters out item Label's,
                                        # clear out the selection on the diagram.
                                        self.clearSelection()
                                        item.setSelected(True)

                                # If we have some nodes selected we need to prepare data for a
                                # possible item move operation: we need to make sure to retrieve
                                # the node below the mouse cursor that will act as as mouse grabber
                                # to compute delta  movements for each component in the selection.
                                selected = self.selectedNodes()
                                if selected:
                                    self.mp_Node = first(self.items(mousePos, edges=False))
                                    if self.mp_Node:
                                        self.mp_NodePos = self.mp_Node.pos()
                                        self.mp_Pos = mousePos
                                        self.mp_Data = self.setupMove(selected)

    def mouseMoveEvent(self, mouseEvent):
        """
        Executed when then mouse is moved on the scene.
        :type mouseEvent: QGraphicsSceneMouseEvent
        """
        mouseButtons = mouseEvent.buttons()
        mousePos = mouseEvent.scenePos()

        if mouseButtons & QtCore.Qt.LeftButton:

            if self.mode is DiagramMode.EdgeAdd:

                #############################################
                # EDGE INSERTION
                #################################

                if self.isEdgeAdd():

                    statusBar = self.session.statusBar()
                    edge = self.mp_Edge
                    edge.updateEdge(target=mousePos)

                    previousNode = self.mo_Node
                    if previousNode:
                        previousNode.updateNode(selected=False)

                    currentNode = first(self.items(mousePos, edges=False, skip={edge.source}))
                    if currentNode:
                        self.mo_Node = currentNode
                        pvr = self.project.profile.checkEdge(edge.source, edge, currentNode)
                        currentNode.updateNode(selected=False, valid=pvr.isValid())
                        if not pvr.isValid():
                            statusBar.showMessage(pvr.message())
                        else:
                            statusBar.clearMessage()
                    else:
                        statusBar.clearMessage()
                        self.mo_Node = None
                        self.project.profile.reset()

            elif self.mode is DiagramMode.LabelMove:

                #############################################
                # LABEL MOVE
                #################################

                if self.isLabelMove():

                    snapToGrid = self.session.action('toggle_grid').isChecked()
                    point = self.mp_LabelPos + mousePos - self.mp_Pos
                    point = snap(point, Diagram.GridSize / 2, snapToGrid)
                    delta = point - self.mp_LabelPos
                    self.mp_Label.setPos(self.mp_LabelPos + delta)

            else:

                if self.mode is DiagramMode.Idle:
                    if self.mp_Node:
                        self.setMode(DiagramMode.NodeMove)

                if self.mode is DiagramMode.NodeMove:

                    #############################################
                    # ITEM MOVEMENT
                    #################################

                    if self.isNodeMove():

                        snapToGrid = self.session.action('toggle_grid').isChecked()
                        point = self.mp_NodePos + mousePos - self.mp_Pos
                        point = snap(point, Diagram.GridSize, snapToGrid)
                        delta = point - self.mp_NodePos
                        edges = set()

                        for edge, breakpoints in self.mp_Data['edges'].items():
                            for i in range(len(breakpoints)):
                                edge.breakpoints[i] = breakpoints[i] + delta

                        for node, data in self.mp_Data['nodes'].items():
                            edges |= set(node.edges)
                            node.setPos(data['pos'] + delta)
                            for edge, pos in data['anchors'].items():
                                node.setAnchor(edge, pos + delta)

                        for edge in edges:
                            edge.updateEdge()

        super().mouseMoveEvent(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        """
        Executed when the mouse is released from the scene.
        :type mouseEvent: QGraphicsSceneMouseEvent
        """
        mouseModifiers = mouseEvent.modifiers()
        mouseButton = mouseEvent.button()
        mousePos = mouseEvent.scenePos()

        if mouseButton == QtCore.Qt.LeftButton:

            if self.mode is DiagramMode.EdgeAdd:

                #############################################
                # EDGE INSERTION
                #################################

                if self.isEdgeAdd():

                    edge = self.mp_Edge
                    edge.source.updateNode(selected=False)
                    currentNode = first(self.items(mousePos, edges=False, skip={edge.source}))
                    insertEdge = False

                    if currentNode:
                        currentNode.updateNode(selected=False)
                        pvr = self.project.profile.checkEdge(edge.source, edge, currentNode)
                        if pvr.isValid():
                            edge.target = currentNode
                            insertEdge = True

                    # We temporarily remove the item from the diagram and we perform the
                    # insertion using the undo command that will also emit the sgnItemAdded
                    # signal hence all the widgets will be notified of the edge insertion.
                    # We do this because while creating the edge we need to display it so the
                    # user knows what he is connecting, but we don't want to truly insert
                    # it till it's necessary (when the mouse is released and the validation
                    # confirms that the generated expression is a valid graphol expression).
                    self.removeItem(edge)

                    if insertEdge:
                        self.session.undostack.push(CommandEdgeAdd(self, edge))
                        edge.updateEdge()

                    self.clearSelection()
                    self.project.profile.reset()
                    statusBar = self.session.statusBar()
                    statusBar.clearMessage()

                    self.sgnItemInsertionCompleted.emit(edge, mouseModifiers)

            elif self.mode is DiagramMode.LabelMove:

                #############################################
                # LABEL MOVE
                #################################

                if self.isLabelMove():
                    pos = self.mp_Label.pos()
                    if self.mp_LabelPos != pos:
                        item = self.mp_Label.parentItem()
                        command = CommandLabelMove(self, item, self.mp_LabelPos, pos)
                        self.session.undostack.push(command)
                    self.setMode(DiagramMode.Idle)

            elif self.mode is DiagramMode.NodeMove:

                #############################################
                # ITEM MOVEMENT
                #################################

                if self.isNodeMove():
                    pos = self.mp_Node.pos()
                    if self.mp_NodePos != pos:
                        moveData = self.completeMove(self.mp_Data)
                        self.session.undostack.push(CommandNodeMove(self, self.mp_Data, moveData))
                    self.setMode(DiagramMode.Idle)

        elif mouseButton == QtCore.Qt.RightButton:

            if self.mode is not DiagramMode.SceneDrag:

                #############################################
                # CONTEXTUAL MENU
                #################################

                item = first(self.items(mousePos))
                if not item:
                    self.clearSelection()
                    items = []
                else:
                    items = self.selectedItems()
                    if item not in items:
                        self.clearSelection()
                        item.setSelected(True)
                        items = [item]

                self.mp_Pos = mousePos
                menu = self.session.mf.create(self, items, mousePos)
                menu.exec_(mouseEvent.screenPos())

        super().mouseReleaseEvent(mouseEvent)

        self.mo_Node = None
        self.mp_Data = None
        self.mp_Edge = None
        self.mp_Label = None
        self.mp_LabelPos = None
        self.mp_Node = None
        self.mp_NodePos = None
        self.mp_Pos = None

    #############################################
    #   SLOTS
    #################################

    @QtCore.pyqtSlot('QGraphicsItem')
    def doNodeIdentification(self, node):
        """
        Perform node identification.
        :type node: AbstractNode
        """
        if Identity.Neutral in node.identities():

            func = lambda x: Identity.Neutral in x.identities()
            collection = bfs(source=node, filter_on_visit=func)
            generators = partition(func, collection)
            excluded = set()
            strong = set(generators[1])
            weak = set(generators[0])

            for node in weak:
                identification = node.identify()
                if identification:
                    strong = set.union(strong, identification[0])
                    strong = set.difference(strong, identification[1])
                    excluded = set.union(excluded, identification[2])

            computed = Identity.Neutral
            identities = set(x.identity() for x in strong)
            if identities:
                computed = first(identities)
                if len(identities) > 1:
                    computed = Identity.Unknown

            for node in weak - strong - excluded:
                node.setIdentity(computed)

    @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem')
    def onItemAdded(self, _, item):
        """
        Executed whenever a connection is created/removed.
        :type _: Diagram
        :type item: AbstractItem
        """
        if item.isEdge():
            # Execute the node identification procedure only if one of the
            # endpoints we are connecting is currently identified as NEUTRAL.
            if (item.source.identity() is Identity.Neutral) ^ (item.target.identity() is Identity.Neutral):
                for node in (item.source, item.target):
                    self.sgnNodeIdentification.emit(node)

    @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem')
    def onItemRemoved(self, _, item):
        """
        Executed whenever a connection is created/removed.
        :type _: Diagram
        :type item: AbstractItem
        """
        if item.isEdge():
            # When an edge is removed we may be in the case where
            # the ontology is split into 2 subgraphs, hence we need
            # to run the identification procedure on the 2 subgraphs.
            for node in (item.source, item.target):
                self.sgnNodeIdentification.emit(node)

    #############################################
    #   INTERFACE
    #################################

    def addItem(self, item):
        """
        Add an item to the Diagram (will redraw the item to reflect its status).
        :type item: AbstractItem
        """
        super().addItem(item)
        if item.isNode():
            item.updateNode()

    @staticmethod
    def completeMove(moveData, offset=QtCore.QPointF(0, 0)):
        """
        Complete item movement, given initializated data for a collection of selected nodes.
        :type moveData: dict
        :type offset: QPointF
        :rtype: dict
        """
        return {
            'nodes': {
                node: {
                    'anchors': {k: v + offset for k, v in node.anchors.items()},
                    'pos': node.pos() + offset,
                } for node in moveData['nodes']},
            'edges': {x: [p + offset for p in x.breakpoints[:]] for x in moveData['edges']}
        }

    def edge(self, eid):
        """
        Returns the edge matching the given id or None if no edge is found.
        :type eid: str
        :rtype: AbstractEdge
        """
        return self.project.edge(self, eid)

    def edges(self):
        """
        Returns a collection with all the edges in the diagram.
        :rtype: set
        """
        return self.project.edges(self)

    def isEdgeAdd(self):
        """
        Returns True if an edge insertion is currently in progress, False otherwise.
        :rtype: bool
        """
        return self.mode is DiagramMode.EdgeAdd and self.mp_Edge is not None

    def isLabelMove(self):
        """
        Returns True if a label is currently being moved, False otherwise.
        :rtype: bool
        """
        return self.mode is DiagramMode.LabelMove and \
           self.mp_Label is not None and \
           self.mp_LabelPos is not None and \
           self.mp_Pos is not None

    def isNodeMove(self):
        """
        Returns True if a node(s) is currently being moved, False otherwise.
        :rtype: bool
        """
        return self.mode is DiagramMode.NodeMove and \
           self.mp_Data is not None and \
           self.mp_Node is not None and \
           self.mp_NodePos is not None and \
           self.mp_Pos is not None

    def isEmpty(self):
        """
        Returns True if this diagram containts no element, False otherwise.
        :rtype: bool
        """
        return len(self.project.items(self)) == 0

    def items(self, mixed=None, mode=QtCore.Qt.IntersectsItemShape, **kwargs):
        """
        Returns a collection of items ordered from TOP to BOTTOM.
        If no argument is supplied, an unordered list containing all the elements in the diagram is returned.
        :type mixed: T <= QPointF | QRectF | QPolygonF | QPainterPath
        :type mode: ItemSelectionMode
        :rtype: list
        """
        if mixed is None:
            items = super().items()
        elif isinstance(mixed, QtCore.QPointF):
            x = mixed.x() - (Diagram.SelectionRadius / 2)
            y = mixed.y() - (Diagram.SelectionRadius / 2)
            w = Diagram.SelectionRadius
            h = Diagram.SelectionRadius
            items = super().items(QtCore.QRectF(x, y, w, h), mode)
        else:
            items = super().items(mixed, mode)
        return sorted([
            x for x in items
                if (kwargs.get('nodes', True) and x.isNode() or
                    kwargs.get('edges', True) and x.isEdge() or
                    kwargs.get('labels', False) and x.isLabel()) and
                    x not in kwargs.get('skip', set())
        ], key=lambda i: i.zValue(), reverse=True)

    def nodes(self):
        """
        Returns a collection with all the nodes in the diagram.
        :rtype: set
        """
        return self.project.nodes(self)

    def node(self, nid):
        """
        Returns the node matching the given id or None if no node is found.
        :type nid: str
        :rtype: AbstractNode
        """
        return self.project.node(self, nid)

    def selectedEdges(self, filter_on_edges=lambda x: True):
        """
        Returns the edges selected in the diagram.
        :type filter_on_edges: callable
        :rtype: list
        """
        return [x for x in super().selectedItems() if x.isEdge() and filter_on_edges(x)]

    def selectedItems(self, filter_on_items=lambda x: True):
        """
        Returns the items selected in the diagram.
        :type filter_on_items: callable
        :rtype: list
        """
        return [x for x in super().selectedItems() if (x.isNode() or x.isEdge()) and filter_on_items(x)]

    def selectedNodes(self, filter_on_nodes=lambda x: True):
        """
        Returns the nodes selected in the diagram.
        :type filter_on_nodes: callable
        :rtype: list
        """
        return [x for x in super().selectedItems() if x.isNode() and filter_on_nodes(x)]

    def setMode(self, mode, param=None):
        """
        Set the operational mode.
        :type mode: DiagramMode
        :type param: Item
        """
        if self.mode != mode or self.modeParam != param:
            #LOGGER.debug('Diagram mode changed: mode=%s, param=%s', mode, param)
            self.mode = mode
            self.modeParam = param
            self.sgnModeChanged.emit(mode)

    @staticmethod
    def setupMove(selected):
        """
        Compute necessary data to initialize item movement, given a collection of selected nodes.
        :type selected: T <= list | tuple
        :rtype: dict
        """
        # Initialize movement data considering only
        # nodes which are involved in the selection.
        moveData = {
            'nodes': {
                node: {
                    'anchors': {k: v for k, v in node.anchors.items()},
                    'pos': node.pos(),
                } for node in selected},
            'edges': {}
        }
        # Figure out if the nodes we are moving are sharing edges:
        # if that's the case, move the edge together with the nodes
        # (which actually means moving the edge breakpoints).
        for node in moveData['nodes']:
            for edge in node.edges:
                if edge not in moveData['edges']:
                    if edge.other(node).isSelected():
                        moveData['edges'][edge] = edge.breakpoints[:]
        return moveData

    # noinspection PyTypeChecker
    def visibleRect(self, margin=0):
        """
        Returns a rectangle matching the area of visible items.
        :type margin: float
        :rtype: QtCore.QRectF
        """
        items = self.items()
        if items:
            x = set()
            y = set()
            for item in items:
                b = item.mapRectToScene(item.boundingRect())
                x.update({b.left(), b.right()})
                y.update({b.top(), b.bottom()})
            return QtCore.QRectF(QtCore.QPointF(min(x) - margin, min(y) - margin), QtCore.QPointF(max(x) + margin, max(y) + margin))
        return QtCore.QRectF() class DiagramNotFoundError(RuntimeError):
    """
    Raised whenever we are not able to find a diagram given its path.
    """
    pass class DiagramNotValidError(RuntimeError):
    """
    Raised whenever a diagram appear to have an invalid structure.
    """
    pass class DiagramParseError(RuntimeError):
    """
    Raised whenever it's not possible parse a Diagram out of a document.
    """
    pass def fexists(path):
    """
    Returns True if the given path identifies a regular file, False otherwise.
    :type path: str
    :rtype: bool
    """
    return os.path.isfile(expandPath(path)) def fread(path):
    """
    Read the file identified by the given 'path' and returns its content.
    :type path: str
    :rtype: str
    """
    path = expandPath(path)
    with io.open(path, 'r', encoding='utf8') as ptr:
        return ptr.read() def getLogger(name=APPNAME):
    """
    Returns an instance of the logger for the given name.
    :type name: str
    :rtype: logging.Logger
    """
    if not name in __output:
        # CREATE A FORMATTER
        formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%Y/%m/%d %H:%M:%S')
        # IN-MEMORY STREAM HANDLER
        handler = logging.StreamHandler(OutputHandler.getDefaultStream())
        handler.setFormatter(formatter)
        logger = logging.getLogger(name)
        logger.setLevel(logging.DEBUG)
        logger.addHandler(handler)
        # CONSOLE HANDLER
        handler = logging.StreamHandler(sys.stderr)
        handler.setFormatter(formatter)
        logger = logging.getLogger(name)
        logger.setLevel(logging.DEBUG)
        logger.addHandler(handler)
        # STORE THE LOGGER INSTANCE
        __output[name] = logger

    return __output.get(name) class Project(QtCore.QObject):
    """
    Extension of QtCore.QObject which implements a Graphol project.
    Additionally to built-in signals, this class emits:

    * sgnDiagramAdded: whenever a Diagram is added to the Project.
    * sgnDiagramRemoved: whenever a Diagram is removed from the Project.
    * sgnItemAdded: whenever an item is added to the Project.
    * sgnItemRemoved: whenever an item is removed from the Project.
    * sgnMetaAdded: whenever predicate metadata are added to the Project.
    * sgnMetaRemoved: whenever predicate metadata are removed from the Project.
    * sgnUpdated: whenever the Project is updated in any of its parts.
    """
    sgnDiagramAdded = QtCore.pyqtSignal('QGraphicsScene')
    sgnDiagramRemoved = QtCore.pyqtSignal('QGraphicsScene')
    sgnItemAdded = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem')
    sgnItemRemoved = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem')
    sgnMetaAdded = QtCore.pyqtSignal(Item, str)
    sgnMetaRemoved = QtCore.pyqtSignal(Item, str)
    sgnUpdated = QtCore.pyqtSignal()

    def __init__(self, **kwargs):
        """
        Initialize the graphol project.
        """
        super().__init__(kwargs.get('session'))
        self.index = ProjectIndex()
        self.iri = kwargs.get('iri', 'NULL')
        self.name = kwargs.get('name')
        self.path = expandPath(kwargs.get('path'))
        self.prefix = kwargs.get('prefix', 'NULL')
        self.profile = kwargs.get('profile')
        self.profile.setParent(self)
        self.version = kwargs.get('version', '1.0')

    #############################################
    #   PROPERTIES
    #################################

    @property
    def session(self):
        """
        Returns the reference to the active session (alias for Project.parent()).
        :rtype: Session
        """
        return self.parent()

    #############################################
    #   INTERFACE
    #################################

    def addDiagram(self, diagram):
        """
        Add the given diagram to the Project, together with all its items.
        :type diagram: Diagram
        """
        if self.index.addDiagram(diagram):
            self.sgnDiagramAdded.emit(diagram)
            for item in diagram.items():
                if item.isNode() or item.isEdge():
                    diagram.sgnItemAdded.emit(diagram, item)

    def diagram(self, did):
        """
        Returns the diagram matching the given id or None if no diagram is found.
        :type did: str
        :rtype: Diagram
        """
        return self.index.diagram(did)

    def diagrams(self):
        """
        Returns a collection with all the diagrams in this Project.
        :rtype: set
        """
        return self.index.diagrams()

    def edge(self, diagram, eid):
        """
        Returns the edge matching the given id or None if no edge is found.
        :type diagram: Diagram
        :type eid: str
        :rtype: AbstractEdge
        """
        return self.index.edge(diagram, eid)

    def edges(self, diagram=None):
        """
        Returns a collection with all the edges in the given diagram.
        If no diagram is supplied a collection with all the edges in the Project will be returned.
        :type diagram: Diagram
        :rtype: set
        """
        return self.index.edges(diagram)

    def isEmpty(self):
        """
        Returns True if the Project contains no element, False otherwise.
        :rtype: bool
        """
        return self.index.isEmpty()

    def item(self, diagram, iid):
        """
        Returns the item matching the given id or None if no item is found.
        :type diagram: Diagram
        :type iid: str
        :rtype: AbstractItem
        """
        return self.index.item(diagram, iid)

    def itemNum(self, item, diagram=None):
        """
        Returns the number of items of the given type which belongs to the given diagram.
        If no diagram is supplied, the counting is extended to the whole Project.
        :type item: Item
        :type diagram: Diagram
        :rtype: int
        """
        return self.index.itemNum(item, diagram)

    def items(self, diagram=None):
        """
        Returns a collection with all the items in the given diagram.
        If no diagram is supplied a collection with all the items in the Project will be returned.
        :type diagram: Diagram
        :rtype: set
        """
        return self.index.items(diagram)

    def meta(self, item, name):
        """
        Returns metadata for the given predicate, expressed as pair (item, name).
        :type item: Item
        :type name: str
        :rtype: dict
        """
        return self.index.meta(item, name)

    def metas(self, *types):
        """
        Returns a collection of pairs 'item', 'name' for all the predicates with metadata.
        :type types: list
        :rtype: list
        """
        return self.index.metas(*types)

    def node(self, diagram, nid):
        """
        Returns the node matching the given id or None if no node is found.
        :type diagram: Diagram
        :type nid: str
        :rtype: AbstractNode
        """
        return self.index.node(diagram, nid)

    def nodes(self, diagram=None):
        """
        Returns a collection with all the nodes in the given diagram.
        If no diagram is supplied a collection with all the nodes in the Project will be returned.
        :type diagram: Diagram
        :rtype: set
        """
        return self.index.nodes(diagram)

    def predicateNum(self, item, diagram=None):
        """
        Returns the number of predicates of the given type which are defined in the given diagram.
        If no diagram is supplied, the counting is extended to the whole Project.
        :type item: Item
        :type diagram: Diagram
        :rtype: int
        """
        return self.index.predicateNum(item, diagram)

    def predicates(self, item=None, name=None, diagram=None):
        """
        Returns a collection of predicate nodes belonging to the given diagram.
        If no diagram is supplied the lookup is performed across the whole Project.
        :type item: Item
        :type name: str
        :type diagram: Diagram
        :rtype: set
        """
        return self.index.predicates(item, name, diagram)

    def removeDiagram(self, diagram):
        """
        Remove the given diagram from the project index, together with all its items.
        :type diagram: Diagram
        """
        if self.index.removeDiagram(diagram):
            for item in self.items(diagram):
                diagram.sgnItemRemoved.emit(diagram, item)
            self.sgnDiagramRemoved.emit(diagram)

    def setMeta(self, item, name, meta):
        """
        Set metadata for the given predicate type/name combination.
        :type item: Item
        :type name: str
        :type meta: dict
        """
        if self.index.setMeta(item, name, meta):
            self.sgnMetaAdded.emit(item, name)

    def unsetMeta(self, item, name):
        """
        Remove metadata for the given predicate type/name combination.
        :type item: Item
        :type name: str
        """
        if self.index.unsetMeta(item, name):
            self.sgnMetaRemoved.emit(item, name)

    #############################################
    #   SLOTS
    #################################

    @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem')
    def doAddItem(self, diagram, item):
        """
        Executed whenever an item is added to a diagram belonging to this Project.
        :type diagram: Diagram
        :type item: AbstractItem
        """
        if self.index.addItem(diagram, item):
            self.sgnItemAdded.emit(diagram, item)

    @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem')
    def doRemoveItem(self, diagram, item):
        """
        Executed whenever an item is removed from a diagram belonging to this project.
        This slot will remove the given element from the project index.
        :type diagram: Diagram
        :type item: AbstractItem
        """
        if self.index.removeItem(diagram, item):
            self.sgnItemRemoved.emit(diagram, item) class ProjectMergeWorker(QtCore.QObject):
    """
    Extends QObject with facilities to merge the content of 2 distinct projects.
    """
    def __init__(self, project, other, session):
        """
        Initialize the project merge worker.
        :type project: Project
        :type other: Project
        :type session: Session
        """
        super().__init__(session)
        self.commands = list()
        self.project = project
        self.other = other

    #############################################
    #   PROPERTIES
    #################################

    @property
    def session(self):
        """
        Returns the reference to the active session (alias for ProjectMergeWorker.parent()).
        :rtype: Session
        """
        return self.parent()

    #############################################
    #   INTERFACE
    #################################

    def mergeDiagrams(self):
        """
        Perform the merge of the diagrams by importing all the diagrams in the 'other' project in the loaded one.
        """
        for diagram in self.other.diagrams():
            # We may be in the situation in which we are importing a diagram with name 'X'
            # even though we already have a diagram 'X' in our project. Because we do not
            # want to overwrite diagrams, we perform a rename of the diagram being imported,
            # to be sure to have a unique diagram name, in the current project namespace.
            occurrence = 1
            name = diagram.name
            while self.project.diagram(diagram.name):
                diagram.name = '{0}_{1}'.format(name, occurrence)
                occurrence += 1
            ## SWITCH SIGNAL SLOTS
            disconnect(diagram.sgnItemAdded, self.other.doAddItem)
            disconnect(diagram.sgnItemRemoved, self.other.doRemoveItem)
            connect(diagram.sgnItemAdded, self.project.doAddItem)
            connect(diagram.sgnItemRemoved, self.project.doRemoveItem)
            ## MERGE THE DIAGRAM IN THE CURRENT PROJECT
            self.commands.append(CommandDiagramAdd(diagram, self.project))

    def mergeMeta(self):
        """
        Perform the merge of predicates metadata.
        """
        conflicts = dict()
        resolutions = dict()

        for item, name in self.other.metas():
            if not self.project.predicates(item, name):
                ## NO PREDICATE => NO CONFLICT
                undo = self.project.meta(item, name).copy()
                redo = self.other.meta(item, name).copy()
                self.commands.append(CommandNodeSetMeta(self.project, item, name, undo, redo))
            else:
                ## CHECK FOR POSSIBLE CONFLICTS
                metac = self.project.meta(item, name)
                metai = self.other.meta(item, name)
                if metac != metai:
                    if item not in conflicts:
                        conflicts[item] = dict()
                    conflicts[item][name] = {K_CURRENT: metac.copy(), K_IMPORTING: metai.copy()}
                    if item not in resolutions:
                        resolutions[item] = dict()
                    resolutions[item][name] = metac.copy()

        ## RESOLVE CONFLICTS
        aconflicts = []
        for item in conflicts:
            for name in conflicts[item]:
                metac = conflicts[item][name][K_CURRENT]
                metai = conflicts[item][name][K_IMPORTING]
                ## RESOLVE DOCUMENTATION CONFLICTS
                docc = metac.get(K_DESCRIPTION, '')
                doci = metai.get(K_DESCRIPTION, '')
                if docc != doci:
                    resolver = PredicateDocumentationConflictResolver(item, name, docc, doci)
                    if resolver.exec_() == PredicateDocumentationConflictResolver.Rejected:
                        raise ProjectStopImportingError
                    resolutions[item][name][K_DESCRIPTION] = resolver.result()
                ## COLLECT ASSERTIONS CONFLICTS FOR ATTRIBUTES
                if item is Item.AttributeNode:
                    vc = metac.get(K_FUNCTIONAL, False)
                    vi = metai.get(K_FUNCTIONAL, False)
                    if vc != vi:
                        aconflicts.append({
                            K_ITEM: item,
                            K_NAME: name,
                            K_PROPERTY: K_FUNCTIONAL,
                            K_CURRENT: vc,
                            K_IMPORTING: vi
                        })
                ## COLLECT ASSERTIONS CONFLICTS FOR ROLES
                if item is Item.RoleNode:
                    for k in (K_ASYMMETRIC, K_INVERSE_FUNCTIONAL, K_IRREFLEXIVE, K_REFLEXIVE, K_SYMMETRIC, K_TRANSITIVE):
                        vc = metac.get(k, False)
                        vi = metai.get(k, False)
                        if vc != vi:
                            aconflicts.append({
                                K_ITEM: item,
                                K_NAME: name,
                                K_PROPERTY: k,
                                K_CURRENT: vc,
                                K_IMPORTING: vi
                            })

        ## RESOLVE BOOLEAN PROPERTIES CONFLICTS
        if aconflicts:
            resolver = PredicateBooleanConflictResolver(aconflicts)
            if resolver.exec_() == PredicateBooleanConflictResolver.Rejected:
                raise ProjectStopImportingError
            for e in resolver.results():
                resolutions[e[K_ITEM]][e[K_NAME]][e[K_PROPERTY]] = e[K_FINAL]

        ## GENERATE UNDOCOMMANDS FOR RESOLUTIONS
        for item in resolutions:
            for name in resolutions[item]:
                undo = self.project.meta(item, name)
                redo = resolutions[item][name]
                self.commands.append(CommandNodeSetMeta(self.project, item, name, undo, redo))

    def mergeFinished(self):
        """
        Completes the merge by executing the commands in the buffer on the undostack.
        """
        if self.commands:
            self.session.undostack.beginMacro('import project "{0}" into "{1}"'.format(self.other.name, self.project.name))
            for command in self.commands:
                self.session.undostack.push(command)
            self.session.undostack.endMacro()

    def run(self):
        """
        Perform the merge of the 2 projects.
        """
        try:
            LOGGER.info('Performing project import: %s <- %s...', self.project.name, self.other.name)
            self.mergeDiagrams()
            self.mergeMeta()
        except ProjectStopImportingError:
            pass
        else:
            self.mergeFinished()
import itertools
import os
from PyQt5 import QtCore
from PyQt5 import QtWidgets
from PyQt5 import QtXml
from eddy.core.datatypes.graphol import Item, Identity, Restriction
from eddy.core.datatypes.system import File
from eddy.core.diagram import Diagram
from eddy.core.diagram import DiagramNotFoundError
from eddy.core.diagram import DiagramNotValidError
from eddy.core.diagram import DiagramParseError
from eddy.core.functions.fsystem import fexists
from eddy.core.functions.fsystem import fread
from eddy.core.functions.misc import snapF, isEmpty, rstrip, snap
from eddy.core.functions.signals import connect
from eddy.core.loaders.common import AbstractOntologyLoader
from eddy.core.output import getLogger
from eddy.core.project import Project, ProjectMergeWorker
                                    return Item.MembershipEdge
                            return Item.InclusionEdge
                        if lineType == 'dashed':
                            return Item.InputEdge

            data = data.nextSiblingElement('data')

        return None

    @staticmethod
    def parseAnchorPos(edge, node, x, y):
        """
        Parse the given edge
        :type edge: AbstractEdge
        :type node: AbstractNode
        :type x: str
        :type y: str
        :rtype: QtCore.QPointF
        """
        path = node.painterPath()
        pos = QtCore.QPointF(float(x), float(y))
        if path.contains(pos):
            return snap(node.mapToScene(pos), Diagram.GridSize)
        return node.anchor(edge)

    @staticmethod
    def parseLabelPos(geometry, label):
        """
        Parse the node label position properly translating it from yEd coordinate system.
        :type geometry: QDomElement
        :type label: QDomElement
        :rtype: QtCore.QPointF
        """
        x1 = float(label.attribute('x'))
        y1 = float(label.attribute('y'))
        w1 = float(label.attribute('width'))
        h1 = float(label.attribute('height'))
        w2 = float(geometry.attribute('width'))
        h2 = float(geometry.attribute('height'))
        return QtCore.QPointF(x1, y1) - QtCore.QPointF(w2 / 2, h2 / 2) + QtCore.QPointF(w1 / 2, h1 / 2)

    @staticmethod
    def parsePos(geometry):
        """
        Parse the position of the node properly translating it from yEd coordinate system.
        :type geometry: QDomElement
        :rtype: QtCore.QPointF
        """
        # yEd uses the TOP-LEFT corner as (0,0) coordinate => we need to translate our
        # position (0,0), which is instead at the center of the shape, so that the TOP-LEFT
        # corner of the shape in yEd matches the TOP-LEFT corner of the shape in Eddy.
        # We additionally snap the position to the grid so that items stay perfectly aligned.
        x = float(geometry.attribute('x'))
        y = float(geometry.attribute('y'))
        w = float(geometry.attribute('width'))
        h = float(geometry.attribute('height'))
        return snap(QtCore.QPointF(x, y) + QtCore.QPointF(w / 2, h / 2), Diagram.GridSize)

    @staticmethod
    def optimizeLabelPos(node):
        """
        Perform updates on the position of the label of the given Domain o Range restriction node.
        This is due to yEd not using the label to denote the 'exists' restriction and because Eddy
        adds the label automatically, it may overlap some other elements hence we try to give it
        some more visibility by moving it around the node till it overlaps less stuff.
        :type node: T <= DomainRestrictionNode|RangeRestrictionNode
        """
        if node.type() in {Item.DomainRestrictionNode, Item.RangeRestrictionNode}:
            if node.restriction() is Restriction.Exists:
                if not node.label.isMoved() and node.label.collidingItems():
                    x = [-30, -15, 0, 15, 30]
                    y = [0, 12, 22, 32, 44]
                    pos = node.label.pos()
                    for offset_x, offset_y in itertools.product(x, y):
                        node.label.setPos(pos + QtCore.QPointF(offset_x, offset_y))
                        if not node.label.collidingItems():
                            break
                    else:
                        node.label.setPos(pos)

    #############################################
    #   MAIN IMPORT
    #################################

    def createDomDocument(self):
        """
        Create the QDomDocument from where to parse information.
        """
        QtWidgets.QApplication.processEvents()

        LOGGER.info('Loading diagram: %s', self.path)

        if not fexists(self.path):
            raise DiagramNotFoundError('diagram not found: {0}'.format(self.path))

        self.document = QtXml.QDomDocument()
        if not self.document.setContent(fread(self.path)):
            raise DiagramNotValidError('could not parse diagram from {0}'.format(self.path))

    def createDiagram(self):
        """
        Creates a diagram and reverse the content of the GraphML document in it.
        """
        LOGGER.debug('Initializing empty diagram with size: %s', Diagram.MaxSize)
        name = os.path.basename(self.path)
        name = rstrip(name, File.GraphML.extension)
        self.diagram = Diagram.create(name, Diagram.MaxSize, self.nproject)

        root = self.document.documentElement()
        graph = root.firstChildElement('graph')
        e = graph.firstChildElement('node')
        while not e.isNull():
            try:
                QtWidgets.QApplication.processEvents()
                item = self.itemFromXmlNode(e)
                if not item:
                    raise DiagramParseError('could not identify item for XML node')
                func = self.importFuncForItem[item]
                node = func(e)
                if not node:

                    raise DiagramParseError('could not generate item for XML node')