import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import networkx as nx

"""
Explanation: Network Analysis using Networkx
End of explanation
"""


nodes = list(range(1, 8))
edges = [(i, j) for i in nodes for j in nodes[i:]]

g = nx.Graph()
g.add_edges_from(edges)

(1, 2) in g.edges

def check_completeness(graph):
    for node1 in graph.nodes:
        for node2 in graph.nodes:
            if node1 != node2 and (node1, node2) not in graph.edges:
                return 'not complete'
    return 'complete'

h = nx.Graph()
h.add_edges_from([(1, 2), (2, 3), (3, 4), (4, 1)])

check_completeness(h)

check_completeness(g)

g = nx.complete_graph(7)
g.edges

check_completeness(g)

"""
Explanation: 1. Create a complete graph with 7 nodes and verify that it is complete by looking at the edges. Do this manually and using a built-in method.
End of explanation
"""


def draw_with_labels(graph, layout_type=None, **kwargs):
    plt.figure(figsize=(16, 10))
    if layout_type == None:
        nx.draw(graph, with_labels=True)
    elif layout_type == 'circular':
        nx.draw_circular(graph, with_labels=True)
    elif layout_type == 'random':
        nx.draw_random(graph, with_labels=True)
    elif layout_type == 'spectral':
        nx.draw_spectral(graph, with_labels=True)
    elif layout_type == 'shell':
        nx.draw_shell(graph, with_labels=True, **kwargs)

draw_with_labels(g, 'circular')

draw_with_labels(g, 'shell', nlist=[range(4, 7), range(4)])

"""
Explanation: 2. Create a function that will draw a given graph that has a layout type parameter and labels the nodes. Now draw the graph created in the last problem using circular layout.
End of explanation
"""


h1 = nx.cycle_graph(4)
h2 = nx.complete_graph(range(4, 7))
h3 = nx.complete_bipartite_graph([7, 8], [9])
h = nx.Graph()
h.add_edges_from(h1.edges)
h.add_edges_from(h2.edges)
h.add_edges_from(h3.edges)

draw_with_labels(h)

"""
Explanation: 3. Create a graph with 10 nodes and 3 components. Draw this graph.
End of explanation
"""


d = nx.DiGraph()
d.add_nodes_from(range(4))
d.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (3, 1), (0, 2), (1, 0)])
nx.diameter(d)

draw_with_labels(d, 'circular')

"""
Explanation: 4. Create a simple connected digraph with 4 nodes and a diameter of 2.
End of explanation
"""


d = nx.DiGraph()
d.add_nodes_from(range(4))
d.add_weighted_edges_from([(0, 1, 0.5), (1, 2, 0.2),
                           (2, 3, 0.1), (0, 2, 0.65), (1, 0, 0.9)])

draw_with_labels(d, 'circular')
nx.draw_networkx_edge_labels(d, pos=nx.circular_layout(d),
                             edge_labels={item[0]:item[1]['weight'] for item in d.edges.items()});

"""
Explanation: 5. Create another 4 node digraph with weighted edges. Draw this graph with node and edge weight labels.
End of explanation
"""


edgelist = nx.read_gpickle('edges_1.pkl')
g1 = nx.Graph()
g1.add_edges_from(edgelist)
g1.nodes

draw_with_labels(g1)

adj = np.zeros([12, 12], dtype=int)
for edge in g1.edges:
    adj[edge[0], edge[1]] = 1
    adj[edge[1], edge[0]] = 1

adj

nx.adj_matrix(g1).todense()

"""
Explanation: 6. Create the adjacnency matrix from the edge data in edges_1.pkl
End of explanation
"""


dist = np.zeros([12, 12], dtype=int)
sp = nx.all_pairs_shortest_path_length(g1)
for item in sp:
    for key in item[1]:
        dist[item[0], key] = item[1][key]

dist

"""
Explanation: 7. Using networkx built-in functions, create the distance matrix for the same graph from the previous problem
End of explanation
"""


cut = [(5, 6)]
g1.remove_edges_from(cut)
draw_with_labels(g1)

"""
Explanation: 8. Identify and remove a cutpoint from this graph and re-draw it
A cutpoint (whatever this means mathematically, I thing nothing though, it should be a cut set) could be the edge between 5 and 6:
End of explanation
"""


edgelist = nx.read_gpickle('edges_2.pkl')
g2 = nx.Graph()
g2.add_edges_from(edgelist)
g2.nodes

draw_with_labels(g2)

# clique = subgraph such that every two nodes are adjacent (i.e. it is a complete graph if considered by itself)
# maximal clique = a clique which isn't contained in any other clique
for mc in nx.find_cliques(g2):
    print(mc)

"""
Explanation: 9. Use edges_2 to create a graph. List any subgraphs that are maximal cliques
End of explanation
"""


# degree = number of adjacent nodes
g2.degree

# closeness centrality or closeness = reciprocal of the sum of the lengths of the shortest paths between
# the node and all other nodes divided by N-1 - the greater the more central
nx.closeness_centrality(g2)

# betweenness centrality or betweenness = sum of the number of shortest paths from all nodes different from v that
# pass through v divided by the number of shortest paths from all nodes different from v
nx.betweenness_centrality(g2)

"""
Explanation: 10. Determine the Degree, Closeness, and Betweenness measures of centrality for this network
End of explanation
"""


g2_copy = g2.copy()
g2_copy.remove_node(0)
g2_copy.add_edge(10, 14)
g2_copy.remove_edge(1, 2)
draw_with_labels(g2_copy)

# eccentricity = greatest geodesic distance between v and any other node
nx.eccentricity(g2_copy, 1)

# for mc in nx.find_cliques(g2_copy):
#     if 1 in mc:
#         print(mc)
nx.cliques_containing_node(g2_copy, 1)

# density = how much the number of edges is close to the maximum possible (complete graph)
nx.density(g2_copy)

g2_copy.remove_node(1)
nx.density(g2_copy)

"""
Explanation: 11. Based on the measures above, which actors have the greatest control over the flow of information? Why do some actors have betweenness measures of zero?
Actors 0, 1 and 2 have the greatest control: they are more and more easily connected to others and, most of all, the betweenness tells us that most of the shortest paths from one person to another go through them.
Some actors have betweenness measures of 0 because no shortest path needs them: this may be because they have a low number of connection that are already connected to others, i.e. they are peripheral nodes of the graph and they don't know someone which isn't known by someone else.
12. Create a copy of the last network and do the following:

remove node 0
add and edge between 10 and 14 and remove the edge between 1 and 2
draw the graph
determine the eccentricity of node 1
find all cliques containing node 1
compute the density of the graph
remove node 1 and compute the density of the resulting graph
End of explanation
"""


edgelist = nx.read_gpickle('edges_3.pkl')
g3 = nx.DiGraph()
g3.add_edges_from(edgelist)
g3.nodes

draw_with_labels(g3)

# create the adjacency matrix
adj = nx.adjacency_matrix(g3)
adj = adj.todense()
# node 1 comes before node 0
adj

# in-out degree centrality = fraction of number of nodes incoming-outcoming edges are connected to
n_edges = len(g3.edges)
insum = np.sum(adj, axis=0).tolist()[0]
outsum = np.sum(adj, axis=1).reshape(1, -1).tolist()[0]
# remember that node 1 comes before node 0
insum[0], insum[1] = insum[1], insum[0]
outsum[0], outsum[1] = outsum[1], outsum[0]

[d / n_edges for d in insum]

[d / n_edges for d in outsum]

# compare your results to the in and out_degree_centrality methods
nx.in_degree_centrality(g3)

nx.out_degree_centrality(g3)

"""
Explanation: 13. Create a directed graph from edges_3.pkl and do the following:

create the adjacency matrix
compute the indegree and outdegree centrality for all actors in this network using the adjacency matrix
compare your results to the in and out_degree_centrality methods
End of explanation
"""


