#This file contains the methods which add the data (layers,clusters,nodes) 
# from the input JSON to the "layerDict" dicionary for further processing

from db.entities.connected_node import NodeC
from db.entities.connected_cluster import ClusterC
from db.entities.connected_layer import LayerC
from typing import Dict
#from db.repository import Repository
import json
import requests

from db.repository import *
repo = Repository()





def getClusterDataFromSwagger(limitNrCluster:int,limitNrNodes:int,listURLs:List[str] = None):

    ''' Calculates the nr of connections/weights between the clusters contained in the "inputLayerDict". Connections are made between clusters from DIFFERENT layers.

    :param int limitNrCluster: Limits Clusters considered. None or <0 values == No limit

    :param int limitNrNodes: Limits Clusters considered. None or <0 values == No limit

    :param List[str] listURLs : Specifies from which URLs to download data. If not provided/None => uses Default URLs

    :returns: Dict{layername: LayerC}. Returns a Dict with the data gathered from the DB
    
    :rtype: Dict{string: LayerC}
    '''    

    print("Entered dataInput")
    if(listURLs == None):
        
        listURLs = []    
        #"""
        listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/Price_Layer/clusters')
        listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/FinishedTime_Layer/clusters')     
        listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/Destination_Layer/clusters')
        #listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/StartingPoint_Layer/clusters') 
        #listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/Reputation_Layer/clusters')    
        #listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/StartingTime_Layer/clusters')
        #listURLs.append('http://articonf1.itec.aau.at:30103/api/layers/User_Layer/clusters')
        #"""

    #Maximum of these nodes PER Layer will be considered
    if (limitNrCluster == None ) or (limitNrCluster < 0): 
        limitNrCluster = 9223372036854775807  #per Layer (LLONG_MAX == 2^63)
    if (limitNrNodes == None ) or (limitNrNodes < 0): 
        limitNrNodes = 9223372036854775807  #per Layer (LLONG_MAX == 2^63)

    layerDict = dict()
    #imports and translates the data from JSON into usefull format
    #returns layerdiction -> Layer -> clusterDict -> Cluster -> nodesDict -> Nodes    
    for url in listURLs:
        newData = loadJson(url)
        layerDict = populateWithNewNodesSingleLayer(newData[0:limitNrCluster],layerDict,limitNrNodes)


    return layerDict

def loadJson(url:str) :
    res = requests.get(url, timeout=30)
    jsonData = json.loads(res.content) 
    return jsonData


def getClusterDataFromMongo(layerNameList,limitNrCluster,limitNrNodes):
    ''' Calculates the nr of connections/weights between the clusters contained in the "inputLayerDict". Connections are made between clusters from DIFFERENT layers.

    :param List[string] layerNameList: Name of the layers to pull from the DB

    :param int limitNrCluster: Limits Clusters considered. None or <0 values == No limit

    :param int limitNrNodes: Limits Clusters considered. None or <0 values == No limit

    :returns: Dict{layername: LayerC}. Returns a Dict with the data gathered from the DB
    
    :rtype: Dict{string: LayerC}
    '''    

    

    #Maximum of these nodes PER Layer will be considered
    if (limitNrCluster == None ) or (limitNrCluster < 0): 
        limitNrCluster = 9223372036854775807  #per Layer (LLONG_MAX == 2^63)
    if (limitNrNodes == None ) or (limitNrNodes < 0): 
        limitNrNodes = 9223372036854775807  #per Layer (LLONG_MAX == 2^63)

    layerDict = dict()
    #imports and translates the data from JSON into usefull format
    #returns layerdiction -> Layer -> clusterDict -> Cluster -> nodesDict -> Nodes    
    for name in layerNameList:
        newData = get_mongoDB_cluster_by_layerName(name)#repo.get_clusters_for_layer(name)
        layerDict = populateWithNewNodesSingleLayer(newData[0:limitNrCluster],layerDict,limitNrNodes)

    return layerDict


def populateWithNewNodesSingleLayer(inputData, layerDict, limitNrNodes) -> Dict[str,LayerC]:
    ''' Gets Layer,Cluster and Node data from a JSON format, and appends it into the "layerDict" dictionary.

    :param inputData: JSON data to be formated.

    :param Dict{string: Layer} layerDict: If it's empty/null a new one will be created. Otherwise contains the associated Layer and Clusters objects to which JSON data is appended. The dictonary KEY is layername, the Value is a Layer Object. The Layer object has an attribute cluster_Dict which stores the clusters in the Layer.

    :param int limitNrNodes: How many maximum nodes PER layer will be considered.

    :returns: Dict{layername: Layer}. Returns the inputLayerDict with the added data from the JSON

    :rtype: Dict{string: Layer}
    '''  

    print("Entered populateWithNewNodes")
    if(layerDict == None):
        layerDict = dict()
    print(" Layer: "+inputData[0].get("layer_name"))
    
    curLayerName = None
    #newClusterDict
    #clusterDict = layerDict.get(curCluster.get("layer_name"),dict())
    for curCluster in inputData:
        if(curCluster.get("layer_name")!= curLayerName):
            clusterDict = layerDict.get(curCluster.get("layer_name"),dict())
            curLayerName = curCluster.get("layer_name")
            
        oldCluster = clusterDict.get(curCluster.get("cluster_label"),None)
        if oldCluster is None: #means this is a new cluster
            cluster_containedNodesDict = dict()
        else: #means this is an already existing cluster
            cluster_containedNodesDict = oldCluster.cluster_containedNodesDict
        
        for curNode in curCluster.get("nodes"):
            #totalNodesCount+=1
            newNode = NodeC(
                curCluster.get("cluster_label"),
                curLayerName,
                curNode.get("UniqueID"))
            if(newNode != None):
                if(newNode.uniqueID!= None and newNode.node_cluster!= None and newNode.node_layer!= None):
                    if( limitNrNodes>0):
                        auxtuple = (newNode.uniqueID,newNode.node_cluster,newNode.node_layer) #unique because the input data is only from one layer
                        key = frozenset(auxtuple)
                        #check for duplicates 
                        cluster_containedNodesDict[key]= newNode #overwrite if already there
                        limitNrNodes-=1

        #finished node

        if oldCluster is None:
            #    def __init__(self,cluster_label,cluster_layer,cluster_containedNodesDict,cluster_connNodesDict, cluster_connectionsNr,cluster_connClustDict):
            newClusterC = ClusterC(
                curCluster.get("cluster_label"),
                curLayerName,
                None,
                cluster_containedNodesDict,
                dict(),
                dict())

            clusterDict[newClusterC.cluster_label] = newClusterC
        else:
            oldCluster.cluster_containedNodesDict = cluster_containedNodesDict
            clusterDict[curCluster.get("cluster_label")] = oldCluster               
        #finished cluster
        newLayer = LayerC(curLayerName,clusterDict)
        layerDict[curLayerName]= newLayer

    return layerDict


def getConnClusterDataFromMongo():
    '''
    Gets the Connected_clusters data from the MongoDB database. The data is found in the collection "conected_clusters"

    Data returned can be used as input for calculateSimilarity function

    :returns: Dict{layername: Layer}. Returns the layerDict containing connected_clusters from DB

    :rtype: Dict{string: Layer}
    '''  

    mongoArray = repo.get_connected_clusters()
    outputDict = convertRetrievedClustersFromMongo(mongoArray)

    return outputDict

def getSimilarityDataFromMongo(cluster_layer: str= None, batchSize: int=1000, run_id: str=None):
    '''
    Gets the computed Similarity between clusters data from the MongoDB database. The data is found in the collection "similarity"

    :returns: Dict. Returns the a dictionary from DB containing the computed similarity.

    :rtype: Dict
    '''  
    skipNr = 0
    result = []
    batchResult = repo.get_similarity(skipNr,batchSize,cluster_layer,run_id)    
    result.extend(batchResult)
    
    while len(batchResult) == batchSize: 
        skipNr += batchSize
        batchResult = repo.get_similarity(skipNr,batchSize,cluster_layer,run_id)
        result.extend(batchResult)
    return result

def getConnectedRunDataFromMongo():
    '''
    Gets the connected_run data from the MongoDB database.

    The data is found in the collection "connected_run"

    :returns: List[Dict]. Returns the a List with run objects from DB.

    :rtype: List
    ''' 
    result = repo.get_connected_run()
    return result

def convertRetrievedClustersFromMongo(inputArray):
    '''
    Helper function used to convert the JSON from the DB into the Python Code structure used in the functions

    '''
    
    layerDict = {}
    for entry in inputArray:
        curLayerName = entry["cluster_layer"]

        #if it's a new layer, create it
        if not(curLayerName in layerDict):
            clusterDict = dict()
            layerDict[curLayerName] = LayerC(curLayerName,clusterDict)

        #get the new cluster
        newClusterC = ClusterC(entry["cluster_label"],
                                entry["cluster_layer"],
                                entry["cluster_runId"],
                                makeChangeNodesDict(entry["cluster_containedNodesDict"],entry["cluster_label"],
                                entry["cluster_layer"]), 
                                makeFrozensetForConnNodesDict(entry["cluster_connNodesDict"]), ##Change from list of dicts to dicts[frozenset(layer_name,node_id,cluster_label)] 
                                changeDictListToTupleDict(entry["cluster_connClustDict"]))
        
        #add cluster to the layerDict
        layerDict[curLayerName].cluster_Dict[newClusterC.cluster_label]=newClusterC     

    return layerDict


def makeFrozensetForConnNodesDict(inputList):
    '''
    Helper function used to convert the JSON from the DB into the Python Code structure used in the functions

    '''

    outputDict = dict()
    for dic in inputList:
        newNodeC = NodeC(dic["node_cluster"],dic["node_layer"],dic["node_id"])
        tuppleKey = (dic["node_cluster"],dic["node_layer"],dic["node_id"])
        frozenSetKey = frozenset(tuppleKey)
        outputDict[frozenSetKey] = newNodeC
    return outputDict

def changeDictListToTupleDict(inputDictList):
    '''
    Helper function used to convert the JSON from the DB into the Python Code structure used in the functions

    '''

    outputDict = dict()
    for entry in inputDictList:
        tuppleKey=(entry["cluster_label"],entry["cluster_layer"])
        outputDict[tuppleKey] = entry["connectionWeight"]
    return outputDict

def makeChangeNodesDict(inputList,cluster_label,cluster_layer):
    '''
    Helper function used to convert the JSON from the DB into the Python Code structure used in the functions

    '''

    outputDict = dict()
    for entry in inputList:
        newNode = NodeC(cluster_label,cluster_layer,entry)
        auxtuple = (newNode.uniqueID,newNode.node_cluster,newNode.node_layer) #unique because the input data is only from one layer
        key = frozenset(auxtuple)
        #check for duplicates 
        outputDict[key]= newNode
    return outputDict
        
def get_mongoDB_cluster_by_layerName(name):
    res = repo.get_clusters_for_layer(name)
    return [c.to_serializable_dict() for c in res]

