'''
Created on Jan 24, 2011
Modified on July 09, 2012
@author: Dilum Bandara
@version: 1.0
@license: Apache License v2.0

   Copyright 2012 H. M. N. Dilum Bandara and Anura P. Jayasumana, Colorado State
   University

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
'''

import Config
from DHT import DHT
from Overlay import Overlay
from Resource import Resource
from PeerID import PeerID
import datetime
import sys
import random
import time
import os
import PeerManager as PM


def initialize(numResources, numPeers):
    '''
    Initialize resources and 1st peer
    @param numResourcesPhy: Number of physical resources 
    @param numPeers: Number of peers in overlay at beginning 
    '''
    for i in range(numResources): #All physical resources
        result = PM.IN_DATA.getResource() #Get resource attributes
        assert((result[0][1] + 1) <= numResources), \
            'No resources or num of them in resource file don\'t match' + str(i)            
        
        if Config.THRE_CPU_SUPERPEER > -1: #Find resourceful nodes. 
            for j in range(len(result) - 2): #For each attribute
                if result[j + 2][0] == 'CSp': #Use CPU speed as it's in all data 
                    if result[j + 2][1] >= Config.THRE_CPU_SUPERPEER:
                        PM.RESOURCEFUL_PEERS.append(i)
                    break
        PM.RESOURCES[i] = Resource(PeerID(i, -1), result[1][1], result[2:])
    #Are we using resourceful nodes? If not, add nodes ID's for given no of peers
    if Config.THRE_CPU_SUPERPEER == -1:
        if Config.MANY_INIT_NODES == False: #Start with 1 node
            if numPeers == 1: PM.RESOURCEFUL_PEERS = range(0, numResources)
            else: PM.RESOURCEFUL_PEERS = range(1, numPeers + 1)
        else: PM.RESOURCEFUL_PEERS = range(0, numResources)#Start with many nodes
    
    if (Config.SIMULATION_TYPE != 'Chord+RDSim') or Config.MANY_INIT_NODES:
        addAllNodes(numPeers) #Add all overlay nodes
    else: addFirstNode() #Add first node
    assignProxyToResources()
    PM.EVENTS.addEvent(PM.EVENTS.now + Config.LOAD_CHECK_INTERVAL, 'TRACKSTAT')
    #Add update resource attributes timer
    PM.EVENTS.addEvent(Config.SAMPLING_INT, 'RESUPDATE') 
    print str(time.asctime(time.localtime(time.time()))) + \
        '\tResource list generated & 1st node(s) added'


def addFirstNode():
    '''
    Add first node to overlay
    '''
    idx = random.randint(0, len(PM.RESOURCEFUL_PEERS) - 1) #Pick random resource
    NID = PM.RESOURCEFUL_PEERS[idx] #Its node ID
    del PM.RESOURCEFUL_PEERS[idx] #Remove it from being a further candidate
    PM.RESOURCES[NID].PID.gKey = random.randint(0, Config.MAX_KEY)#Random gKey
    PM.DHT[NID] = DHT(PM.RESOURCES[NID].PID, {}) #Add to DHT layer
    PM.OVERLAY[NID] = Overlay(PM.RESOURCES[NID].PID) #Add to overlay layer
    PM.OVERLAY[NID].join() #Join overlay. No node to use for joining


def addAllNodes(numPeers):
    '''
    Add all the overlay nodes together
    '''
    pickedKeys = []
    currentNodes = []
    for i in range(numPeers):
        idx = random.randint(0, len(PM.RESOURCEFUL_PEERS) - 1)#Pick rnd resource
        NID = PM.RESOURCEFUL_PEERS[idx] #Its node ID
        del PM.RESOURCEFUL_PEERS[idx] #Remove it from being a further candidate
        gKey = random.randint(0, Config.MAX_KEY) #Pick random gKey
        #Continue to draw a random no until unique one is found
        while gKey in pickedKeys: gKey = random.randint(0, Config.MAX_KEY)
        pickedKeys.append(gKey)
        PM.RESOURCES[NID].PID.gKey = gKey #Set new global key
        PM.DHT[NID] = DHT(PM.RESOURCES[NID].PID, {}) #Add to DHT layer

        #If there are nodes already in overlay pick one randomly to join using
        if len(PM.OVERLAY) == 0:
            PM.OVERLAY[NID] = Overlay(PM.RESOURCES[NID].PID)#Add to overlay-layer
            PM.OVERLAY[NID].join() #No node to join using
        else:
            PM.OVERLAY[NID] = Overlay(PM.RESOURCES[NID].PID)#Add to overlay-layer
            tmpNID = currentNodes[random.randint(0, len(currentNodes)- 1)]
            PM.OVERLAY[NID].join(PM.OVERLAY[tmpNID].PID)
        currentNodes.append(NID)
        
    for i in PM.OVERLAY: #Do finger table update now
        PM.OVERLAY[i].fixFingers() 


def assignProxyToResources(nodeLeave = False):
    '''
    Assign proxy node to resources. Expected to be called periodically
    '''
    overlayNodes = PM.OVERLAY.keys()
    for i in PM.RESOURCES:
        if i in overlayNodes: PM.RESOURCES[i].setProxyID(i)
        else:
            idx = random.randint(0, len(overlayNodes) - 1) #pick randomly
            PM.RESOURCES[i].setProxyID(overlayNodes[idx])
    if nodeLeave == False: #Function not called due to node leaving. Set timer
        PM.EVENTS.addEvent(PM.EVENTS.now + Config.PROXY_ASSIGN_INTERVAL, \
                                                                'PORXASSIGN')


def handleResourceUpdate(numResourcesPhy, numResourcesVir):
    '''
    Time to read the next set of resource attributes from file. Don't set timer
    at individual resources resource file is in order of node IDs 
    @param numResourcesPhy: Number of physical resources
    @param numResourcesVir: Number of virtual resources
    '''    
    for i in range(numResourcesPhy):
        result = PM.IN_DATA.getResource()#Get resource attribs [NID, time, att]
        if len(result) == 0: return #No more resources
        resID = result[0][1] #Resource ID 
        if numResourcesPhy == numResourcesVir: #Same no of physical & virtual
            assert((resID + 1) <= numResourcesPhy), \
                'No of peers in resources file don\'t match'
        PM.EVENTS.addEvent(result[1][1], 'ADV', i, [result[2:], resID], True)
    #reschedule timer
    nextTime = PM.EVENTS.now + Config.SAMPLING_INT
    if (nextTime > Config.FIRST_SAMPLE_END) and \
                                        (nextTime < Config.SECOND_SAMPLE_START):
        PM.EVENTS.addEvent(Config.SECOND_SAMPLE_START, 'RESUPDATE', -1, -1, True)
    else:
        PM.EVENTS.addEvent(nextTime, 'RESUPDATE', -1, -1, True)


def dumpNodeState():
    '''
    Dump stat at all nodes & levels
    '''
    for i in PM.RESOURCES: #Track resources
        PM.STAT.trackStat('Res', PM.RESOURCES[i].getStat())
    clusters = {} #distribution of clusters
    for i in PM.OVERLAY: #Track DHT & overlay layers
        PM.STAT.trackStat('DHT', PM.DHT[i].getStat())
        PM.STAT.trackStat('Ovlay', PM.OVERLAY[i].getStat())
        if PM.DHT[i].PID.gKey in clusters:
            if PM.DHT[i].PID.lKey in clusters[PM.DHT[i].PID.gKey]:
                clusters[PM.DHT[i].PID.gKey][PM.DHT[i].PID.lKey] += 1
            else:
                clusters[PM.DHT[i].PID.gKey][PM.DHT[i].PID.lKey] = 1
        else:
            clusters[PM.DHT[i].PID.gKey] = {PM.DHT[i].PID.lKey:1}
    result = [len(clusters)]
    for i in clusters:
        tmpResult = []
        for j in clusters[i]: tmpResult.append(clusters[i][j])
        result.append(tmpResult)
    PM.STAT.trackStat('Clust', result)#No clusters, [[replicas/fragments]]
    nodes = PM.OVERLAY.keys()
    PM.STAT.trackStat('NIDs', [len(nodes), nodes]) #Node IDs
    PM.STAT.summStat()


def dumpFT():
    '''
    Dump finger table for testing
    '''
    for i in PM.OVERLAY:
        print '--------', i, PM.OVERLAY[i].PID.gKey
        print 'su', PM.OVERLAY[i]._gSucce[0][0].NID, PM.OVERLAY[i]._gSucce[0][0].gKey
        print 'pe', PM.OVERLAY[i]._gPrede[0][0].NID, PM.OVERLAY[i]._gPrede[0][0].gKey
        for j in PM.OVERLAY[i]._FT._FT: print j[0], j[1][0].NID, j[1][0].gKey
        print


def main(*argv): 
    '''
    Main function to indicate start of program
    '''
    assert(len(sys.argv) == 2), 'Wrong parameters. Use \'python main.py [seed]\''
    startTime = datetime.datetime.now()
    random.seed(int(sys.argv[1])) #Set seed
    print str(time.asctime(time.localtime(time.time()))) + '\tStarting'    
    initialize(Config.NUM_RESOURCES_PHY, Config.NUM_PEERS) #Initialize peers
    
    #Process event list
    numEvents = 0 #Use to indicate simulation progress
    simTime = Config.SIM_TIME
    print str(time.asctime(time.localtime(time.time()))) + '\tProcess event list'
    while True:
        event = PM.EVENTS.getNextEvent() #Get next event
        if event == None: break #No more events        
        if event.startTime > simTime: break #Stop simulation
        numEvents += 1
        if (numEvents % 10000) == 0: print PM.EVENTS.now, \
            time.asctime(time.localtime(time.time()))

        if event.eventType == 'ADV': #Advertise event 
            PM.RESOURCES[event.NID].advertise(event.data[0], True, True, \
                                                                event.data[1])
        elif event.eventType == 'QUE': #Query event
            tmpQuery = PM.IN_DATA.getQuery() #Get a new query
            if len(tmpQuery) == 0: #If no more queries. Set Sim time after last
                simTime = event.startTime + Config.LOAD_CHECK_INTERVAL
            else: PM.RESOURCES[event.NID].query(tmpQuery)
        elif event.eventType == 'RESUPDATE': \
            handleResourceUpdate(Config.NUM_RESOURCES_PHY, \
                                Config.NUM_RESOURCES_VIR)#read resource values
        elif event.eventType == 'LOADCHECK':
            if event.NID not in PM.OVERLAY: continue #Node already left
            PM.DHT[event.NID].checkLoad()
            PM.OVERLAY[event.NID].checkLoad()
            PM.STAT.trackStat('LdChk', sum(PM.STAT_CHECK_LOAD)) #Track cost
            PM.STAT_CHECK_LOAD = [] #Reset
            #assert(len(PM.REMOVE_FROM_OVERLAY) <= 1), '1+ nodes to remove'
            for nid in PM.REMOVE_FROM_OVERLAY: #Remove marked nodes
                PM.STAT.trackStat('RemDHT', PM.DHT[nid].getStat())
                PM.STAT.trackStat('RemOvlay', PM.OVERLAY[nid].getStat())
                PM.STAT.trackStat('RemN', [nid, PM.DHT[nid].PID.gKey])
                del PM.DHT[nid]
                del PM.OVERLAY[nid]
            if len(PM.REMOVE_FROM_OVERLAY) > 0:
                assignProxyToResources(True) #Reassign nodes as node left
                PM.REMOVE_FROM_OVERLAY = []
        elif event.eventType == 'PORXASSIGN': assignProxyToResources()
        elif event.eventType == 'FIXFINGER':
            if event.NID not in PM.OVERLAY: continue #Node already left
            PM.OVERLAY[event.NID].fixFingers()
            #reset timer here- as fixFingers() is called under different cases
            PM.EVENTS.addEvent(event.startTime + Config.FIX_FINGER_INTERVAL, \
                               'FIXFINGER', event.NID)
        elif event.eventType == 'TRACKSTAT':
            dumpNodeState()
            PM.EVENTS.addEvent(event.startTime + Config.LOAD_CHECK_INTERVAL, \
                               'TRACKSTAT')
        else: assert(False), 'Unknown event type ' + event.eventType

    print str(time.asctime(time.localtime(time.time()))) + '\t' + \
        str(PM.EVENTS.now) + '\tFinish processing events. Generating stat.'
    dumpNodeState() #Generate statistics
    #Print to see where the results are stored. Useful if running multiple
    #instances on the same node
    print 'Total execution time', datetime.datetime.now() - startTime
    print 'Results located at:', os.getcwd()
    return 0


if __name__ == '__main__':
    sys.exit(main(*sys.argv))