import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import random
import math
from time import time

def convert_to_list_of_neighbors(G):
    return {node: list(G.neighbors(node)) for node in G.nodes()}

def node_mapping(G):
    old_labels=sorted(G.nodes())
    mapping={old:new for new,old in enumerate(old_labels,start=1)}
    G_new=nx.relabel_nodes(G, mapping)
    return G_new, mapping

#this works fine and is tested, expects the graph to be bamed from 1-n
def core_decomposition(g):
    
    "Assumes that the vertex are numbered from 1 to n"   
    vert=[0]*(g.number_of_nodes()+1) #indexing will start with 1
    pos=[0]*(g.number_of_nodes()+1) #indexing will start with 1
    n=g.number_of_nodes()
    
    deg=[0]*(g.number_of_nodes()+1) #indexing will start with 1
    bin=[0]*(g.number_of_nodes()) #degree will start with 0 bins of degree
    md=0
    for node, degree in g.degree():
        deg[node] = degree #calculating degree for each node
        bin[degree] += 1 #incrementing the bin for the degree
        if degree>md:
            md=degree
    bin=bin[:md+1] #slicing the bin uptill the maximum degree
    start=1
    for d in range(0,md+1):
        num=bin[d]
        bin[d]=start
        start+=num #incrementing the start for the next bin
        
    for v in range(1,n+1):
        pos[v]=bin[deg[v]] #position of the vertex in the bin
        vert[pos[v]]=v #vertex at the position of the vertex in the bin
        bin[deg[v]]+=1 #incrementing the bin for the degree
    
    for d in range(md,0,-1):
        bin[d]=bin[d-1] #shifting the bin
    bin[0]=1 #setting the first bin to 1
    
    for i in range(1,n+1):
        v=vert[i] #vertex at the position of the vertex in the bin
        for u in g.neighbors(v):
            if deg[u]>deg[v]: 
                du=deg[u] 
                pu=pos[u] 
                pw=bin[du]
                w=vert[pw] 
                
                if u != w:
                    pos[u]=pw
                    vert[pu]=w
                    pos[w]=pu
                    vert[pw]=u
                bin[du]+=1
                deg[u]-=1
                
    return deg[1:] #sliced the first positionas we now map from node starting from 0


def recolor(G,v,coloring):
    new_color=coloring[v]
    #coloring[v]=-1
    conflicts=[0]*(new_color+1)
    used=[0]*(new_color+1)
    for w in G.neighbors(v):
        conflicts[coloring[w]]+=1
        used[coloring[w]]=w 
    
    for i in range(1,new_color):
        if conflicts[i]==1:
            w=used[i]
            for u in G.neighbors(w):
                used[coloring[u]]=w
                
            c=0
            for j in range(i+1,new_color):
                if (used[j]!=w):
                    c=j
                    break
                
            if c<new_color:
                coloring[v]=coloring[w]
                coloring[w]=c
                return True, coloring

    return False, coloring

def find_saturation_degree(G, coloring):
    sat_degree = [0] * G.number_of_nodes()
    for node in G.nodes():
        color_neighbors=set()
        for neighbor in G.neighbors(node):
            if coloring[neighbor] != -1:
                color_neighbors.add(coloring[neighbor])
        sat_degree[node] = len(color_neighbors)
    return sat_degree

def update_saturation_degree(G, coloring, node, sat_degree):
    # Update the saturation degree of the neighbors of the given node
    for neighbor in G.neighbors(node):
        color_used=set()
        for nodes in G.neighbors(neighbor):
            if coloring[nodes] != -1:
                color_used.add(coloring[nodes])
        sat_degree[neighbor] = len(color_used)  
    return sat_degree

def ColorKernel(G, is_colored=False):
    coloring= [-1]*G.number_of_nodes()
    max_color_used=0
    V=set(G.nodes())
    G_core,_=node_mapping(G)
    if is_colored==False:
        cores=core_decomposition(G_core)
        sorted_cores = sorted(range(len(cores)), key=lambda i: cores[i], reverse=True)
        for v in sorted_cores:
            #find the value i>0 such that no neighbor of u has taken color i 
            ct=0
            neighbor_coloring=set()
            for u in G.neighbors(v):
                if coloring[u]!=-1:
                    neighbor_coloring.add(coloring[u])
            for i in range(1, G.number_of_nodes()+1):
                if i not in neighbor_coloring:
                    ct=i
                    break
                
            coloring[v]=ct   
            if ct>max_color_used:
                status,coloring=recolor(G,v,coloring)
                if status==False:
                    max_color_used+=1
            #color_used=max(coloring)
                
    else:
        
        saturation_degree=find_saturation_degree(G, coloring)
        while len(V)>0:
            
            v=saturation_degree.index(max(saturation_degree))
            V.remove(v)
            ct=0
            neighbor_coloring=set()
            for u in G.neighbors(v):
                if coloring[u]!=-1:
                    neighbor_coloring.add(coloring[u])
            for i in range(1, G.number_of_nodes()+1):
                if i not in neighbor_coloring:
                    ct=i
                    break
                
            coloring[v]=ct   
            if ct>max_color_used:
                status,coloring=recolor(G,v,coloring)
                if status==False:
                    max_color_used+=1
            #update saturation degree of the neighbors of v
            saturation_degree=update_saturation_degree(G, coloring, v, saturation_degree)

    return coloring

#working fine, some tests are required as well, if using this, we have to cahange t, remember this            
def FindClq(G,lb,t=1):
    startset_size=math.ceil(G.number_of_nodes()/100)
    startset=set(random.sample(G.nodes(), startset_size))
    t_max=64
    t=1
    while len(startset)>0:
        u=startset.pop()
        C=set()
        C.add(u)
        CS=set(G.neighbors(u))
        
        while len(CS)>0:
            sample_CS=random.sample(list(CS), t)
            v=sample_CS[0]
            max_int=0
            for i in sample_CS:
                NV=set(G.neighbors(i))
                CS_NV=CS.intersection(NV)
                if len(CS_NV)>max_int:
                    max_int=len(CS_NV)
                    v=i
            NV=set(G.neighbors(v))
            if len(C)+1+len(CS.intersection(NV))<=lb:
                break
            C.add(v)
            CS.remove(v)
            CS=CS.intersection(NV)
            
        if len(C)>lb:
            lb=len(C)
            
        #this may not be here because they are saying that when findclq fails they change t, means when findclq fn is over
        #this may be required in the main function
        # if lb==lb_old:
        #     t=2*t
        # if t>t_max:
        #     t=1
            
    return lb


#tested, working fine
def FindLBIS(G,lb):
    
    LBIS=set()
    
    for i in G.nodes():
        if G.degree(i)<lb and len(LBIS.intersection(set(G.neighbors(i))))==0:
            LBIS.add(i)
    return LBIS



#very confusing, try recursion as it is problematic to when 
def FastColor(G):
    
    G_k=G.copy()
    G_m=set()
    lb_G=0
    ub_G=G.number_of_nodes()
    aplpha_best=[-1]*G.number_of_nodes()
    lb_k=0
    isColored=False
    
    time_start=time()
    
    while 1:
        lb_k=max(len(vv) for vv in nx.find_cliques(G_k))#nx.graph_clique_number(G_k)#FindClq(G_k, lb_k, t=1)
        #if the lower bound on kernel graph is greater than the lower bound on the original graph, reassign ower bound on original graph
        if lb_k>lb_G:
            lb_G=lb_k
            
        I=FindLBIS(G_k, lb_k)
        remaining_nodes=set(G_k.nodes()).difference(I)
        G_k.remove_nodes_from(I) #this is new Gk but the nodes will not be renumbered, if you remove 0 then there is no 9 in the node set
        G_m=G_m.union(I)
        
        #if there is no independednt set, then we need to reset some values 
        if len(I)!=0:
            lb_k=0
            isColored=False
        
        alpha=ColorKernel(G_k, is_colored=isColored) #this is problematic as it's lebgth is dynamic
        alpha_temp=alpha.copy()
        
        if max(alpha)<lb_k:
            new_color=max(aplpha_best)+1
            for i in I:
                aplpha_best[i]=new_color
            for i in remaining_nodes:
                aplpha_best[i]= alpha_temp.pop(0)
        else:
            new_color=min(aplpha_best)
            
            for i in I:
                aplpha_best[i]=new_color
            for i in remaining_nodes:
                aplpha_best[i]= alpha_temp.pop(0)
        
        isColored=True
        
        if max(aplpha_best)<ub_G:
                    
            pass
            
            
            
                    
nodes = [1, 2, 3, 4, 5]
nodes= [i for i in range(21)] #node name starts with 0
edges = [(1, 2),(2, 3),(3, 4),(4, 5),(2, 4),(2, 5),(5, 6),(6, 7),(5, 7),(5, 8), (6,8), (7,8), (7, 9), (9, 10), (10,11), 
         (4, 11),(4, 10), (4, 12), (10,12), (11,12), (9,13), (10, 13), (13,14), (13, 15), (16, 17), (17, 18),(18, 19),(19, 20),(17, 20)]

G = nx.Graph()
G.add_nodes_from(nodes)
G.add_edges_from(edges)
print(G.nodes())
print(G.edges())
#coloring=ColorKernel(G)
lb=max(len(c) for c in nx.find_cliques(G))
#lb=FindClq(G,0)
print(lb)



# print(G.number_of_nodes(), G.number_of_edges())
# print(G.nodes(), G.edges())
# #print(convert_to_list_of_neighbors(G))
# print("degree",G.degree())
# print(G.neighbors(1))
a=input()
# Draw the graph
pos = nx.spring_layout(G, seed=45)  # or use nx.kamada_kawai_layout(G), etc.
nx.draw(G, pos, with_labels=True, node_color='skyblue', node_size=500, font_weight='bold', edge_color='gray')
plt.title("NCX Graph Visualization")
plt.show()