# Copyright 2010-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

__all__ = ["digraph"]

import bisect
from collections import deque

from portage.util import writemsg


class digraph:
    """
    A directed graph object.
    """

    def __init__(self):
        """Create an empty digraph"""

        # { node : ( { child : priority } , { parent : priority } ) }
        self.nodes = {}
        self.order = []

    def add(self, node, parent, priority=0):
        """Adds the specified node with the specified parent.

        If the dep is a soft-dep and the node already has a hard
        relationship to the parent, the relationship is left as hard."""

        if node not in self.nodes:
            self.nodes[node] = ({}, {}, node)
            self.order.append(node)

        if not parent:
            return

        if parent not in self.nodes:
            self.nodes[parent] = ({}, {}, parent)
            self.order.append(parent)

        priorities = self.nodes[node][1].get(parent)
        if priorities is None:
            priorities = []
            self.nodes[node][1][parent] = priorities
            self.nodes[parent][0][node] = priorities

        if not priorities or priorities[-1] is not priority:
            bisect.insort(priorities, priority)

    def discard(self, node):
        """
        Like remove(), except it doesn't raises KeyError if the
        node doesn't exist.
        """
        try:
            self.remove(node)
        except KeyError:
            pass

    def remove(self, node):
        """Removes the specified node from the digraph, also removing
        and ties to other nodes in the digraph. Raises KeyError if the
        node doesn't exist."""

        if node not in self.nodes:
            raise KeyError(node)

        for parent in self.nodes[node][1]:
            del self.nodes[parent][0][node]
        for child in self.nodes[node][0]:
            del self.nodes[child][1][node]

        del self.nodes[node]
        self.order.remove(node)

    def update(self, other):
        """
        Add all nodes and edges from another digraph instance.
        """
        for node in other.order:
            children, parents, node = other.nodes[node]
            if parents:
                for parent, priorities in parents.items():
                    for priority in priorities:
                        self.add(node, parent, priority=priority)
            else:
                self.add(node, None)

    def clear(self):
        """
        Remove all nodes and edges.
        """
        self.nodes.clear()
        del self.order[:]

    def difference_update(self, t):
        """
        Remove all given nodes from node_set. This is more efficient
        than multiple calls to the remove() method.
        """
        if isinstance(t, (list, tuple)) or not hasattr(t, "__contains__"):
            t = frozenset(t)
        order = []
        for node in self.order:
            if node not in t:
                order.append(node)
                continue
            for parent in self.nodes[node][1]:
                del self.nodes[parent][0][node]
            for child in self.nodes[node][0]:
                del self.nodes[child][1][node]
            del self.nodes[node]
        self.order = order

    def has_edge(self, child, parent):
        """
        Return True if the given edge exists.
        """
        try:
            return child in self.nodes[parent][0]
        except KeyError:
            return False

    def remove_edge(self, child, parent):
        """
        Remove edge in the direction from child to parent. Note that it is
        possible for a remaining edge to exist in the opposite direction.
        Any endpoint vertices that become isolated will remain in the graph.
        """

        # Nothing should be modified when a KeyError is raised.
        for k in parent, child:
            if k not in self.nodes:
                raise KeyError(k)

        # Make sure the edge exists.
        if child not in self.nodes[parent][0]:
            raise KeyError(child)
        if parent not in self.nodes[child][1]:
            raise KeyError(parent)

        # Remove the edge.
        del self.nodes[child][1][parent]
        del self.nodes[parent][0][child]

    def __iter__(self):
        return iter(self.order)

    def contains(self, node):
        """Checks if the digraph contains mynode"""
        return node in self.nodes

    def get(self, key, default=None):
        node_data = self.nodes.get(key, self)
        if node_data is self:
            return default
        return node_data[2]

    def all_nodes(self):
        """Return a list of all nodes in the graph"""
        return self.order[:]

    def child_nodes(self, node, ignore_priority=None):
        """Return all children of the specified node"""
        if ignore_priority is None:
            return list(self.nodes[node][0])
        children = []
        if hasattr(ignore_priority, "__call__"):
            for child, priorities in self.nodes[node][0].items():
                for priority in reversed(priorities):
                    if not ignore_priority(priority):
                        children.append(child)
                        break
        else:
            for child, priorities in self.nodes[node][0].items():
                if ignore_priority < priorities[-1]:
                    children.append(child)
        return children

    def child_nodes_iter(self, node, ignore_priority=None):
        """Yield all children of the specified node, in child_nodes() order"""
        children = self.nodes[node][0]
        if ignore_priority is None:
            yield from children
        elif hasattr(ignore_priority, "__call__"):
            for child, priorities in children.items():
                for priority in reversed(priorities):
                    if not ignore_priority(priority):
                        yield child
                        break
        else:
            for child, priorities in children.items():
                if ignore_priority < priorities[-1]:
                    yield child

    def parent_nodes(self, node, ignore_priority=None):
        """Return all parents of the specified node"""
        if ignore_priority is None:
            return list(self.nodes[node][1])
        parents = []
        if hasattr(ignore_priority, "__call__"):
            for parent, priorities in self.nodes[node][1].items():
                for priority in reversed(priorities):
                    if not ignore_priority(priority):
                        parents.append(parent)
                        break
        else:
            for parent, priorities in self.nodes[node][1].items():
                if ignore_priority < priorities[-1]:
                    parents.append(parent)
        return parents

    def leaf_nodes(self, ignore_priority=None):
        """Return all nodes that have no children

        If ignore_soft_deps is True, soft deps are not counted as
        children in calculations."""

        leaf_nodes = []
        if ignore_priority is None:
            for node in self.order:
                if not self.nodes[node][0]:
                    leaf_nodes.append(node)
        elif hasattr(ignore_priority, "__call__"):
            for node in self.order:
                is_leaf_node = True
                for child, priorities in self.nodes[node][0].items():
                    for priority in reversed(priorities):
                        if not ignore_priority(priority):
                            is_leaf_node = False
                            break
                    if not is_leaf_node:
                        break
                if is_leaf_node:
                    leaf_nodes.append(node)
        else:
            for node in self.order:
                is_leaf_node = True
                for child, priorities in self.nodes[node][0].items():
                    if ignore_priority < priorities[-1]:
                        is_leaf_node = False
                        break
                if is_leaf_node:
                    leaf_nodes.append(node)
        return leaf_nodes

    def root_nodes(self, ignore_priority=None):
        """Return all nodes that have no parents.

        If ignore_soft_deps is True, soft deps are not counted as
        parents in calculations."""

        root_nodes = []
        if ignore_priority is None:
            for node in self.order:
                if not self.nodes[node][1]:
                    root_nodes.append(node)
        elif hasattr(ignore_priority, "__call__"):
            for node in self.order:
                is_root_node = True
                for parent, priorities in self.nodes[node][1].items():
                    for priority in reversed(priorities):
                        if not ignore_priority(priority):
                            is_root_node = False
                            break
                    if not is_root_node:
                        break
                if is_root_node:
                    root_nodes.append(node)
        else:
            for node in self.order:
                is_root_node = True
                for parent, priorities in self.nodes[node][1].items():
                    if ignore_priority < priorities[-1]:
                        is_root_node = False
                        break
                if is_root_node:
                    root_nodes.append(node)
        return root_nodes

    def __bool__(self):
        return bool(self.nodes)

    def is_empty(self):
        """Checks if the digraph is empty"""
        return len(self.nodes) == 0

    def induced_subgraph(self, nodes):
        """Return a new digraph containing only the given nodes, and the
        edges of this graph whose endpoints are both in nodes.

        Node order is this graph's insertion order restricted to nodes."""
        if not isinstance(nodes, (set, frozenset, dict)):
            nodes = frozenset(nodes)
        sub = digraph()
        sub_nodes = sub.nodes
        order = [node for node in self.order if node in nodes]
        for node in order:
            sub_nodes[node] = ({}, {}, node)
        for node in order:
            for child, priorities in self.nodes[node][0].items():
                if child in nodes:
                    # Share one fresh priorities list between the child and
                    # parent views of the edge, as add()/clone() do.
                    priorities = priorities[:]
                    sub_nodes[node][0][child] = priorities
                    sub_nodes[child][1][node] = priorities
        sub.order = order
        return sub

    def clone(self):
        clone = digraph()
        clone.nodes = {}
        memo = {}
        for children, parents, node in self.nodes.values():
            children_clone = {}
            for child, priorities in children.items():
                priorities_clone = memo.get(id(priorities))
                if priorities_clone is None:
                    priorities_clone = priorities[:]
                    memo[id(priorities)] = priorities_clone
                children_clone[child] = priorities_clone
            parents_clone = {}
            for parent, priorities in parents.items():
                priorities_clone = memo.get(id(priorities))
                if priorities_clone is None:
                    priorities_clone = priorities[:]
                    memo[id(priorities)] = priorities_clone
                parents_clone[parent] = priorities_clone
            clone.nodes[node] = (children_clone, parents_clone, node)
        clone.order = self.order[:]
        return clone

    def delnode(self, node):
        try:
            self.remove(node)
        except KeyError:
            pass

    def firstzero(self):
        leaf_nodes = self.leaf_nodes()
        if leaf_nodes:
            return leaf_nodes[0]
        return None

    def hasallzeros(self, ignore_priority=None):
        return len(self.leaf_nodes(ignore_priority=ignore_priority)) == len(self.order)

    def debug_print(self):
        def output(s):
            writemsg(s, noiselevel=-1)

        for node in self.nodes:
            output(f"{node} ")
            if self.nodes[node][0]:
                output("depends on\n")
            else:
                output("(no children)\n")
            for child, priorities in self.nodes[node][0].items():
                output(f"  {child} ({priorities[-1]})\n")

    def bfs(self, start, ignore_priority=None):
        if start not in self:
            raise KeyError(start)

        queue, enqueued = deque([(None, start)]), {start}
        while queue:
            parent, n = queue.popleft()
            yield parent, n
            new = set(self.child_nodes_iter(n, ignore_priority)) - enqueued
            enqueued |= new
            queue.extend([(n, child) for child in new])

    def shortest_path(self, start, end, ignore_priority=None):
        if start not in self:
            raise KeyError(start)
        elif end not in self:
            raise KeyError(end)

        paths = {None: []}
        for parent, child in self.bfs(start, ignore_priority):
            paths[child] = paths[parent] + [child]
            if child == end:
                return paths[child]
        return None

    def get_cycles(self, ignore_priority=None, max_length=None):
        """
        Returns all cycles that have at most length 'max_length'.
        If 'max_length' is 'None', all cycles are returned.
        """
        all_cycles = []
        for node in self.nodes:
            # If we have multiple paths of the same length, we have to
            # return them all, so that we always get the same results
            # even with PYTHONHASHSEED="random" enabled.
            shortest_path = None
            candidates = []
            for child in self.child_nodes(node, ignore_priority):
                path = self.shortest_path(child, node, ignore_priority)
                if path is None:
                    continue
                if not shortest_path or len(shortest_path) >= len(path):
                    shortest_path = path
                    candidates.append(path)
            if shortest_path and (not max_length or len(shortest_path) <= max_length):
                for path in candidates:
                    if len(path) == len(shortest_path):
                        all_cycles.append(path)
        return all_cycles

    # Backward compatibility
    addnode = add
    allnodes = all_nodes
    allzeros = leaf_nodes
    hasnode = contains
    __contains__ = contains
    empty = is_empty
    copy = clone
