"""
altgraph.ObjectGraph - Graph of objects with an identifier
==========================================================

A graph of objects that have a "graphident" attribute.
graphident is the key for the object in the graph
"""

from altgraph import GraphError
from altgraph.Graph import Graph
from altgraph.GraphUtil import filter_stack


class ObjectGraph(object):
    """
    A graph of objects that have a "graphident" attribute.
    graphident is the key for the object in the graph
    """

    def __init__(self, graph=None, debug=0):
        if graph is None:
            graph = Graph()
        self.graphident = self
        self.graph = graph
        self.debug = debug
        self.indent = 0
        graph.add_node(self, None)

    def __repr__(self):
        return '<%s>' % (type(self).__name__,)

    def flatten(self, condition=None, start=None):
        """
        Iterate over the subgraph that is entirely reachable by condition
        starting from the given start node or the ObjectGraph root
        """
        if start is None:
            start = self
        start = self.getRawIdent(start)
        return self.graph.iterdata(start=start, condition=condition)

    def nodes(self):
        for ident in self.graph:
            node = self.graph.node_data(ident)
            if node is not None:
                yield self.graph.node_data(ident)

    def get_edges(self, node):
        if node is None:
            node = self
        start = self.getRawIdent(node)
        _, _, outraw, incraw = self.graph.describe_node(start)

        def iter_edges(lst, n):
            seen = set()
            for tpl in (self.graph.describe_edge(e) for e in lst):
                ident = tpl[n]
                if ident not in seen:
                    yield self.findNode(ident)
                    seen.add(ident)
        return iter_edges(outraw, 3), iter_edges(incraw, 2)

    def edgeData(self, fromNode, toNode):
        if fromNode is None:
            fromNode = self
        start = self.getRawIdent(fromNode)
        stop = self.getRawIdent(toNode)
        edge = self.graph.edge_by_node(start, stop)
        return self.graph.edge_data(edge)

    def updateEdgeData(self, fromNode, toNode, edgeData):
        if fromNode is None:
            fromNode = self
        start = self.getRawIdent(fromNode)
        stop = self.getRawIdent(toNode)
        edge = self.graph.edge_by_node(start, stop)
        self.graph.update_edge_data(edge, edgeData)

    def filterStack(self, filters):
        """
        Filter the ObjectGraph in-place by removing all edges to nodes that
        do not match every filter in the given filter list

        Returns a tuple containing the number of:
            (nodes_visited, nodes_removed, nodes_orphaned)
        """
        visited, removes, orphans = filter_stack(self.graph, self, filters)

        for last_good, tail in orphans:
            self.graph.add_edge(last_good, tail, edge_data='orphan')

        for node in removes:
            self.graph.hide_node(node)

        return len(visited)-1, len(removes), len(orphans)

    def removeNode(self, node):
        """
        Remove the given node from the graph if it exists
        """
        ident = self.getIdent(node)
        if ident is not None:
            self.graph.hide_node(ident)

    def removeReference(self, fromnode, tonode):
        """
        Remove all edges from fromnode to tonode
        """
        if fromnode is None:
            fromnode = self
        fromident = self.getIdent(fromnode)
        toident = self.getIdent(tonode)
        if fromident is not None and toident is not None:
            while True:
                edge = self.graph.edge_by_node(fromident, toident)
                if edge is None:
                    break
                self.graph.hide_edge(edge)

    def getIdent(self, node):
        """
        Get the graph identifier for a node
        """
        ident = self.getRawIdent(node)
        if ident is not None:
            return ident
        node = self.findNode(node)
        if node is None:
            return None
        return node.graphident

    def getRawIdent(self, node):
        """
        Get the identifier for a node object
        """
        if node is self:
            return node
        ident = getattr(node, 'graphident', None)
        return ident

    def __contains__(self, node):
        return self.findNode(node) is not None

    def findNode(self, node):
        """
        Find the node on the graph
        """
        ident = self.getRawIdent(node)
        if ident is None:
            ident = node
        try:
            return self.graph.node_data(ident)
        except KeyError:
            return None

    def addNode(self, node):
        """
        Add a node to the graph referenced by the root
        """
        self.msg(4, "addNode", node)

        try:
            self.graph.restore_node(node.graphident)
        except GraphError:
            self.graph.add_node(node.graphident, node)

    def createReference(self, fromnode, tonode, edge_data=None):
        """
        Create a reference from fromnode to tonode
        """
        if fromnode is None:
            fromnode = self
        fromident, toident = self.getIdent(fromnode), self.getIdent(tonode)
        if fromident is None or toident is None:
            return
        self.msg(4, "createReference", fromnode, tonode, edge_data)
        self.graph.add_edge(fromident, toident, edge_data=edge_data)

    def createNode(self, cls, name, *args, **kw):
        """
        Add a node of type cls to the graph if it does not already exist
        by the given name
        """
        m = self.findNode(name)
        if m is None:
            m = cls(name, *args, **kw)
            self.addNode(m)
        return m

    def msg(self, level, s, *args):
        """
        Print a debug message with the given level
        """
        if s and level <= self.debug:
            print("%s%s %s" % (
                "  " * self.indent, s, ' '.join(map(repr, args))))

    def msgin(self, level, s, *args):
        """
        Print a debug message and indent
        """
        if level <= self.debug:
            self.msg(level, s, *args)
            self.indent = self.indent + 1

    def msgout(self, level, s, *args):
        """
        Dedent and print a debug message
        """
        if level <= self.debug:
            self.indent = self.indent - 1
            self.msg(level, s, *args)
