'''
Created on Sep 11, 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(rndSeed, numWStattions, numInOlay, numUsers, pTime, prefetchInt):
    '''
    Initialize weather stations
    @param rndSeed: Random seed 
    @param numWStattionsPhy: Number of physical resources 
    @param numInOlay: Number of peers in overlay at beginning
    @param numUsers:
    @param prefetchTime: prefetch sensor readings & queries up to this time 
    @param prefetchInt: prefetch interval
    @return: User to resource mapping    
    '''
    random.seed(int(sys.argv[1])) #Set seed
    for i in range(numWStattions): #Add weather stations
        station = PM.IN_DATA.getWStation()
        assert(station[0][1] == i), 'Wrong weather station ID ' + str(i) + \
                                                    ' & ' + str(station[0][1])
        PM.RESOURCES[i] = Resource(PeerID(i, -1), station[5])
    PM.RESOURCEFUL_PEERS = range(numWStattions)#Start with many nodes
    
    addAllNodes(numInOlay) #Add all overlay nodes
    assignProxyToResources() #Assign other weather stations to onces in overlay
    userToResource = assignResourceToUsers()
    #Prefetch sensor readings & queries 
    fetchSenQue(0, pTime, prefetchInt, userToResource)
    return userToResource


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():
    '''
    Assign proxy node to resources.
    '''
    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])


def assignResourceToUsers(numUsers):
    '''
    Assign resource node to users. To simplify the simulation resources also act
    as query issuing agents on behalf of users. This is not a problem as our 
    performance metric is based on hops. Users are not part of overlay hence
    they should be assigned to resources that are not in overlay. Users IDs 
    start with 0 so to prevent conflict with weather stations which also has a 
    0-based index mapping from user ID to resource ID is tracked
    @param numUsers: Number of users
    @results: user to resource mapping as a dictionary {user ID: resource ID}   
    '''
    userToResource = {} #Users to resource mapping
    usedResouces = []
    for i in range(numUsers):
        idx = random.randint(0, len(PM.RESOURCEFUL_PEERS) - 1)#Pick rnd resource
        NID = PM.RESOURCEFUL_PEERS[idx] #Its node ID
        if NID not in usedResouces:
            userToResource[i] = NID
            usedResouces.append(NID)
    return userToResource


def fetchSenQue(cTime, prefetchTime, prefetchInt, userToResource):
    '''
    Load sensor readings & queries to discrete even engine. This is necessary 
    as all sensors/users do not generate data/queries at the same time.
    To make sure sensor readings are properly indexed & queries are correctly
    answered advertising time of sensors & issue time of queries high be higher
    than overlay construction time
    @param cTime: Current time
    @param prefetcTime: Prefetch queries this far ahead in time
    @param prefetcInt: Time interval between 2 prefect operations
    @param userToResource: End user to resource mapping 
    @return: Did reached last query or not  
    '''
    endTime = cTime + prefetchTime
    noReading = False
    noQueries = False
    while 1:
        reading = PM.IN_DATA.getSReading()
        if len(reading) == 0:
            noReading = True 
            break
        PM.EVENTS.addEvent(reading[1], 'ADV', reading[0], reading)
        if reading[1] > endTime: break
    while 1:
        query = PM.IN_DATA.getQuery()
        if len(query) == 0:
            noQueries = True
            break
        #Map user to resource & then add event
        PM.EVENTS.addEvent(query[1], 'QUE', userToResource[query[0]], query)
        if query[1] > endTime: break
    if noReading and noQueries: return True #No more sensor data or queries
    #Schedule next prefetch event 
    PM.EVENTS.addEvent(cTime + prefetchInt, 'FETCH', -1, -1)
    return False


def dumpNodeState():
    '''
    Dump stat at all nodes & levels
    '''
    for i in PM.RESOURCES: #Track resources
        PM.STAT.trackStat('Res', PM.RESOURCES[i].getStat())
    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())
    PM.STAT.summStat()


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()
    print str(time.asctime(time.localtime(time.time()))) + '\tStarting'    
    userToResource = initialize(sys.argv[1], Config.NUM_WSTATIONS, \
                    Config.NUM_IN_OVERLAY, Config.NUM_USERS, \
                    Config.PREFETCH_TIME, Config.PREFETCH_INTERVAL)#Initialize
    
    #Process event list
    numEvents = 0 #Use to indicate simulation progress
    simTime = 9999999
    print str(time.asctime(time.localtime(time.time()))) + '\tProcess event list'
    
    while True:
        event = PM.EVENTS.getNextEvent() #Get next event
        if event == None: #No more events
            print str(time.asctime(time.localtime(time.time()))),'No more events'
            break                    
        if event.startTime > simTime:
            print str(time.asctime(time.localtime(time.time()))), \
                'End of simulation time reached', event[0]
            break
        
        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, event.startTime)
        elif event.eventType == 'QUE': #Query event
            PM.RESOURCES[event.NID].query(event.data, event.startTime)
        elif event[1] == 'FETCH': #Fetch sensor readings & queries
            lastQue = fetchSenQue(event[0], Config.PREFETCH_TIME, \
                                    Config.PREFETCH_INTERVAL, userToResource)
            if lastQue: #Last query, so set simulation end time
                simTime = event[0] + Config.SIM_TIME_AFTER_LAST_QUERY
        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 == 'STAT':
            dumpNodeState()
            PM.EVENTS.addEvent(event.startTime + \
                               Config.STAT_COLLECTION_INTERVAL, 'STAT')
        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))