'''
Created on Nov 2, 2011
Modified Mar 25, 2012
@version: 0.2
@author: Dilum Bandara
@email: dilumb@engr.colostate.edu
@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.
'''

from DES import DES
from Application import Application
from NIC import NIC
from CCNNode import CCNNode
from Radar import Radar
from Proxy import Proxy
from Location import Location 
import sys
import random
import time
import math
import Config as Config
import PeerManager as PM
import Distance as Distance


PM.EVENTS = DES() #Discrete event engine
fdQueries = open(Config.QUERY_FILE, 'r')
fdResults = open(Config.RESULTS_FILE, 'w')


def newRndLocation(locMin, locMax, allocatedLocs):
    '''
    Find new random location to place application
    @param locMin: Lower edge of boundary
    @param locMax: Upper edge of boundary
    @param allocatedLocs: Locations that are already allocated 
    @return: New location
    '''
    while True:
        x = random.randint(locMin.x, locMax.x)  
        y = random.randint(locMin.y, locMax.y)
        if [x, y] not in allocatedLocs: return Location(x, y)


def initApps(rndSeed):
    '''
    Initialize applications
    @param rndSeed: Random seed
    @return: List of proxy locations
    '''
    random.seed(rndSeed) #Set seed
    numApps = 0
    allocatedLocs = []
    for i in range(Config.NUM_RADARS_X): #Add radars
        for j in range(Config.NUM_RADARS_Y):
            PM.APPS[numApps] = Radar(numApps, Location(i * \
                        Config.INTER_RADAR_DIST, j * Config.INTER_RADAR_DIST))
            #Schedule radar data generation timer
            PM.EVENTS.addEvent(Config.TIME_INFORM_NEIGHBORS + \
                random.randint(0, Config.RADAR_SKEW), 'RadarGen', numApps, [])
            numApps += 1
            allocatedLocs.append([i * Config.INTER_RADAR_DIST, \
                                    j * Config.INTER_RADAR_DIST])
    print 'Added radars. Total nodes:', numApps
    
    locMin = Location(0, 0)
    locMax = Location(2 * Config.INTER_RADAR_DIST, 2 * Config.INTER_RADAR_DIST)
    while True: #Add 1 copy of app for each data fusion group
        for i in Config.APPS_IN_ALL_DFS:
            loc = newRndLocation(locMin, locMax, allocatedLocs)
            PM.APPS[numApps] = Application(numApps, loc, i)
            allocatedLocs.append([loc.x, loc.y])
            numApps += 1
        locMin.y += 3 * Config.INTER_RADAR_DIST #New boundary of data fusion group
        if locMin.y > Config.SIZE_TORUS.y:
            locMin.y = 0
            locMax.y = 2 * Config.INTER_RADAR_DIST
            locMin.x += 3 * Config.INTER_RADAR_DIST
            if locMin.x > Config.SIZE_TORUS.x: break #Added all nodes
            locMax.x += 3 * Config.INTER_RADAR_DIST
            if locMax.x > Config.SIZE_TORUS.x: locMax.x = Config.SIZE_TORUS.x
        else:
            locMax.y += 3 * Config.INTER_RADAR_DIST
            if locMax.y > Config.SIZE_TORUS.y: locMax.y = Config.SIZE_TORUS.y
    print 'Added popular apps to data fusion groups. Total nodes', numApps

    #Add apps randomly to subset of data fusion groups. Don't add 2 copies of 
    #same app to same DF group
    for i in range(Config.NUM_APP_IN_SUB_DFS):
        assignedDFs = [] #DF groups that nodes are already added
        for j in Config.APPS_IN_SUB_DFS:
            loc = []
            while True:
                loc = newRndLocation(Location(0, 0), Config.SIZE_TORUS, \
                                     allocatedLocs)
                #Find DF (x, y)
                x = int(math.ceil((loc.x - 2 * Config.INTER_RADAR_DIST)\
                              / (3.0 * Config.INTER_RADAR_DIST)))
                y = int(math.ceil((loc.y - 2 * Config.INTER_RADAR_DIST)\
                              / (3.0 * Config.INTER_RADAR_DIST)))
                if [x, y] not in assignedDFs: #Not assigned to
                    assignedDFs.append([x, y])
                    break
            PM.APPS[numApps] = Application(numApps, loc, j)
            allocatedLocs.append([loc.x, loc.y]) 
            numApps += 1
    print 'Added rare apps to subset of data fusion groups. Total nodes', numApps
    
    proxies = {}
    for i in range(Config.NUM_APP_PROXIES): #Add proxies
        loc = newRndLocation(Location(0, 0), Config.SIZE_TORUS, allocatedLocs)    
        PM.APPS[numApps] = Proxy(numApps, loc)
        allocatedLocs.append([loc.x, loc.y])
        proxies[numApps] = loc
        numApps += 1
    print str(time.asctime(time.localtime(time.time()))), \
                                'Added proxies. Total nodes', numApps

    #Create overlay & underlay for nodes randomly to support random deployment
    nodes = PM.APPS.keys()
    random.shuffle(nodes)
    nextJoin = 0
    for i in nodes:        
        PM.EVENTS.addEvent(nextJoin, 'OvJoin', i, i)
        nextJoin += Config.JOIN_DELAY
    return proxies


def findNearestProxy(loc, proxies):
    '''
    Find nearest proxy for a given user
    @param loc: Location of user
    @param proxies: List of proxy location
    @return: Nearest proxy's NID  
    '''
    lowestDist = 999999.9
    idx = -1
    for i in proxies:
        distance = Distance.dist(loc, proxies[i])
        if distance < lowestDist:
            lowestDist = distance
            idx = i
    return idx


def informNbrs():
    '''
    Inform presence of nodes to neighbors in data fusion groups or vicinity
    Radars know all other radars within 2R
    '''
    #Add radars not in a zone to zone controller
    for i in PM.pendingRadars: PM.OVERLAY[i].rejoinOverlay() 
    #Add radars to applications' routing table
    for i in PM.OVERLAY:
        #Advertise application presence to neighbors
        PM.OVERLAY[i].advAppsInFIB()
        if PM.OVERLAY[i].type == 'RADAR': #Add to neighboring radars
            for j in PM.OVERLAY:
                if i == j: continue #Don't add radar to itself
                #Add radars to zones if it's at the edge of a zone boundary  
                if PM.OVERLAY[j].type != 'RADAR': 
                    if len(PM.OVERLAY[j].getZone()) > 0: #Is a zone & radar in it?
                        if Distance.inZone(PM.OVERLAY[j].getZone(), \
                                           PM.OVERLAY[i].loc, True) == True:
                            PM.OVERLAY[j].addNodeToFIB(i, PM.OVERLAY[i].loc, \
                                PM.OVERLAY[i].getZone(), 'RADAR', \
                                PM.OVERLAY[i].inOverlay)
                else:                    
                    if Distance.inRange(PM.OVERLAY[i].loc, PM.OVERLAY[j].loc,
                                        2 * Config.RADAR_RANGE) == True:
                        PM.OVERLAY[i].addNodeToFIB(j, PM.OVERLAY[j].loc, \
                            PM.OVERLAY[j].getZone(), 'RADAR', \
                            PM.OVERLAY[j].inOverlay)


def loadQueries(proxies):
    '''
    Load queries form query file
    @param proxies: List of proxies
    @return: Last query time 
    '''
    #TODO - Optimize this if query file is very large
    proxyForQuery = {} #Proxy assigned to query nodes
    lastTime = 0
    for line in fdQueries: #Read all queries & add to event list.
        query = line.split('\t')
        if query[0] not in proxyForQuery: #Find nearest proxy
            proxyNID = findNearestProxy(Location(query[1], query[2]), proxies)
            proxyForQuery[query[0]] = proxyNID 
        PM.EVENTS.addEvent(float(query[3]), 'Query', proxyForQuery[query[0]], \
                            [Location(float(query[1]), float(query[2])), \
                             float(query[4]), query[5], int(query[0]), \
                             int(query[6])], False)
        lastTime = float(query[3])
    return lastTime


def collectStat(time, finish):
    '''
    Collect statistics from nodes
    @param time: Current time 
    @param finish: Simulation is complete 
    '''
    for i in PM.APPS: #Collect app level data
        tmpStr = 'App\t' + PM.APPS[i].type + '\t' + str(time) + '\t' + str(i) 
        results = PM.APPS[i].getStat()
        for j in results: tmpStr += '\t' + str(j)
        fdResults.write(tmpStr + '\n')
        if PM.APPS[i].type == 'PROXY':
            results2 = PM.APPS[i].dumpResults()
            for k in range(len(results2)):
                tmpStr = 'PROXY' + '\t' + str(i)
                for l in results2[k]: tmpStr += '\t' + str(l)
                fdResults.write(tmpStr + '\n')
        
    if finish == True: #Get overlay & underlay data
        for i in PM.OVERLAY:
            tmpStr = 'Ov\t' + PM.OVERLAY[i].type + '\t' + str(time) +'\t' +str(i)
            results = PM.OVERLAY[i].getStat()
            for j in results: tmpStr += '\t' + str(j)
            fdResults.write(tmpStr + '\n')
        for i in PM.UNDERLAY:
            tmpStr = 'Un\t' + PM.UNDERLAY[i].type + '\t' + str(time) +'\t'+ str(i)
            results = PM.UNDERLAY[i].getStat()
            for j in results: tmpStr += '\t' + str(j)
            fdResults.write(tmpStr + '\n')


def main(*argv):
    '''
    Main function to indicate start of program
    '''
    assert(len(sys.argv) == 2), \
            'Incorrect parameters. Input should be \'python main.py <seed>\''
    proxies = initApps(sys.argv[1]) #Initialize & get list of proxies
    nbrInformed = False
    simTime = 999999
    eventCount = 0
    while True:
        event = PM.EVENTS.getNextEvent()
        if len(event) == 0:  #No more events
            print str(time.asctime(time.localtime(time.time()))), 'No more events'
            break
        
        if eventCount % 100000 == 0: #Status update
            if eventCount != 0: print event[0]
        eventCount += 1
        
        if event[0] > simTime:                   
            print str(time.asctime(time.localtime(time.time()))), \
                'End of simulation time reached', event[0]
            break
        
        if (nbrInformed == False) and (event[0] >= Config.TIME_INFORM_NEIGHBORS):
            print str(time.asctime(time.localtime(time.time()))), \
                    'Finish adding all nodes to overlay & underlay'
            informNbrs()
            nbrInformed = True
            print str(time.asctime(time.localtime(time.time()))), \
                        'Finish informing presence to neighbors'
            simTime = loadQueries(proxies) + Config.SIM_TIME_AFTER_LAST_QUERY
            fdQueries.close()
            PM.EVENTS.addEvent(event[0] + Config.STAT_COLLECTION_INTERVAL, \
                               'STAT', -1, [])                
            print str(time.asctime(time.localtime(time.time()))), \
                        'Finish loading queries'

        if event[1] == 'OvJoin':
            PM.UNDERLAY[event[2]] = NIC(event[2], PM.APPS[event[2]].loc, \
                                        PM.APPS[event[2]].type)
            if PM.APPS[event[2]].type == 'RADAR':  #If radar
                PM.OVERLAY[event[2]] = CCNNode(event[2], PM.APPS[event[2]].loc, \
                                PM.APPS[event[2]].type, Config.SIZE_CS, \
                                Config.SIZE_PIT, Config.SIZE_FIB, \
                                Config.ADD_RADAR_TO_OVERLAY)
            else:
                PM.OVERLAY[event[2]] = CCNNode(event[2], PM.APPS[event[2]].loc, \
                                PM.APPS[event[2]].type, Config.SIZE_CS, \
                                Config.SIZE_PIT, Config.SIZE_FIB)
        elif event[1] == 'OvRejoin': PM.OVERLAY[event[2]].rejoinOverlay()
        elif event[1] == 'UnToOv':
            PM.OVERLAY[event[2]].recMsg(event[3][0], event[3][1])
        elif event[1] == 'Query':
            PM.APPS[event[2]].genInterests(event[3][0], event[3][2], event[0], \
                                        event[3][1], event[3][3], event[3][4])
        elif event[1] == 'RadarGen': #Generate radar data, reschedule timer
            PM.APPS[event[2]].genNewData(event[0])
            PM.EVENTS.addEvent(event[0] + Config.RADAR_HEARTBEAT, \
                               'RadarGen', event[2], [])
        elif event[1] == 'STAT': #Collect stat & reschedule timer
            collectStat(event[0], False)
            PM.EVENTS.addEvent(event[0] + Config.STAT_COLLECTION_INTERVAL, \
                               'STAT', -1, [])
        elif event[1] == 'QSUB': #Query subscription
            PM.APPS[event[2]].genSubInterests(event[3][0], event[3][1], event[0])
        else:
            print 'Unknown event type. Terminating', event[1]
            sys.exit()

    collectStat(PM.EVENTS.now, True) #Collect remaining stat
    fdResults.close()


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