# Class to represent nodes (vertices) of a graph
#
class Node(object):

    # name must be a string
    def __init__(self, name):
        self.name = name
        self.status = 'unseen'
        
    def getName(self):
        return self.name

    def getStatus(self):
        return self.status

    # should be one of 'unseen', 'seen', 'processed'
    def setStatus(self, status):
        self.status = status
        
    def __repr__(self):
        return "<{}>".format(self.name)


# Class for representing undirected graphs, i.e. graphs in which edges
# have no direction - if there is an edge between a and b,
# you can "move" from a to b and/or from b to a
#
class Graph():
    
    #nodes is a list of the nodes in the graph
    #
    # adjacencyLists is a dictionary with the set of nodes as the set of keys.  For each node, n1,
    # adjacencyLists[n1] is a list of the nodes n2 such that (n1,n2) is an edge.
    # i.e. it is a list of all nodes to which n1 is connected directly by an edge.
    #
    def __init__(self):
        self.nodes = []
        self.adjacencyLists = {}
        
    def addNode(self, node):
        if type(node) != Node:
            raise TypeError("You can only add Node objects to a graph.")
        elif node in self.nodes:
            raise ValueError("node is already in graph. You can't add it again.")
        else:
            self.nodes.append(node)
            self.adjacencyLists[node] = []

    # To add an edge between node1 and node2, node1 and node2 must already be in the graph
    def addEdge(self, node1, node2):
        if node1 == node2:
            raise ValueError("edges to self are not allowed in undirected graphs")
        if not((node1 in self.nodes) and (node2 in self.nodes)):
            raise ValueError("at least one node of given edge is not in the graph")
        if node2 in self.adjacencyLists[node1]:
            raise ValueError("edge is already in graph. You can't add it again.")

        self.adjacencyLists[node1].append(node2)
        self.adjacencyLists[node2].append(node1)
        
    def neighborsOf(self, node):
        return self.adjacencyLists[node]

    def getNode(self, name):
        for node in self.nodes:
            if node.getName() == name:
                return node
        return None
    
    def hasNode(self, node):
        return node in self.nodes

    def hasEdge(self, node1, node2):
        return node2 in self.adjacencyLists[node1]
    
    def __repr__(self):
        result = "[Graph with:\n Nodes:"
        edgesString = "\n Edges: "
        for node in self.nodes:
            result = result + " " + str(node)
            for node2 in self.neighborsOf(node):
                if node.getName() < node2.getName():
                    edgesString += ' {}-{},'.format(node.getName(), node2.getName())
        result = result + edgesString[:-1]
        return result
                                   
def genGraph():
    n1 = Node("NYC")
    n2 = Node("Miami")
    g = Graph()
    print(g)
    g.addNode(n1)
    g.addNode(n2)
    print(g)
    g.addEdge(n1, n2)
    print(g)
    return g

def genCompleteGraph(n):
    nodes = []
    g = Graph()
    for i in range(n):
        g.addNode(Node(str(i)))

    nodes = g.nodes
    for n1 in nodes:
        for n2 in nodes:
            if (n1 != n2) and (not g.hasEdge(n1, n2)):
                g.addEdge(n1,n2)
    return g

import random
# return a new list with the same elements as input L but randomly rearranged
def mixup(L):
    newL = L[:]
    length = len(L)
    for i in range(length):
        newIndex = random.randint(i,length-1)
        newL[newIndex], newL[i] = newL[i], newL[newIndex]
    return(newL)

def genRandomGraph(numNodes, numEdges):
   
    g = Graph()
    
    for i in range(numNodes):
        g.addNode(Node(str(i)))

    allPairs = []
    for i in range(numNodes):
        for j in range(i+1, numNodes):
                allPairs.append((str(i),str(j)))
                    
    allPairs = mixup(allPairs)
    
    edgesAdded = 0
    while edgesAdded < min(numEdges, len(allPairs)):
        g.addEdge(g.getNode(allPairs[edgesAdded][0]), g.getNode(allPairs[edgesAdded][1]))
        edgesAdded = edgesAdded + 1

    return g

# graph used for bfs demo in class
#
def genDemoGraph():
    nodes = [Node("A"), Node("B"), Node("C"), Node("D"), Node("E"), Node("F"), Node("G"), Node("H")]
    # used to save typing :)
    edgeIndexTuples = [(0,1), # A-B
             (0,2), # A-C
             (0,4), # A-E
             (0,7), # A-H
             (1,2), # B-C
             (1,3), # B-D
             (1,5), # B-F
             (2,5), # C-F
             (4,6), # E-G
             (6,7) # G-H
             ]
    g = Graph()
    for n in nodes:
        g.addNode(n)
    for e in edgeIndexTuples:
        g.addEdge(nodes[e[0]], nodes[e[1]])
    return g

            












