'''
Created on Sep 3, 2012
@version: 0.1
@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.
'''

import datetime
import math
import os
import random
import sys
import time
from User import User
from WeatherStation import WeatherStation
from CCNNode import CCNNode
from NIC import NIC
import Config as Config
import PeerManager as PM

fdResults = open(Config.RESULTS_FILE, 'w')


def initApps(rndSeed):
    '''
    Initialize applications
    @param rndSeed: Random seed
    @return: List of proxy locations
    '''
    random.seed(rndSeed) #Set seed
    
    #Add weather stations. First find fraction of nodes to be part of overlay.
    numInOverlay = int(math.ceil(Config.NUM_W_STATIONS * \
                                                Config.FRACTION_IN_OVERLAY))
    wInOverlay = [] #Weather stations in overlay
    wStations = range(Config.NUM_W_STATIONS) #IDs of all weather stations
    random.shuffle(wStations)   #To simulate random node addition

    while 1:
        sID = wStations[random.randint(0, Config.NUM_W_STATIONS - 1)]
        if sID not in wInOverlay: wInOverlay.append(sID)
        if len(wInOverlay) == numInOverlay: break

    for i in range(Config.NUM_W_STATIONS):
        station = PM.IN_DATA.getWStation()
        assert(station[0][1] == i), 'Wrong weather station ID ' + str(i) + \
                                                    ' & ' + str(station[0][1])
        PM.APPS[i] = WeatherStation(i, [station[1][1], station[2][1],\
                station[3][1]], station[4][1], i in wInOverlay, \
                Config.USE_THRESHOLDS, station[5])
       
    #Create overlay & underlay for weather stations. 
    #First add nodes to be in overlay & then others
    nextJoin = 0
    for i in wInOverlay:  #Add nodes in overlay        
        PM.EVENTS.addEvent(nextJoin, 'JOIN', i, i)
        nextJoin += Config.JOIN_DELAY
    for i in wStations:  #Add remaining nodes
        if i in wInOverlay: continue #Already asked to join
        PM.EVENTS.addEvent(nextJoin, 'JOIN', i, i)
        nextJoin += Config.JOIN_DELAY

    #Add end users. Also create overlay & underlay for end users
    #End users only specify (x, y) locations
    allocatedLocs = []
    UID = Config.NUM_W_STATIONS
    for i in Config.USERS:
        if len(i[1]) == 1: #Location doesn't matter as far as unique
            AOI = [[0, 0], Config.SIZE_TORUS[0:2]]
            for j in range(i[1][0]):
                loc = newRndLocation(AOI, allocatedLocs)
                PM.APPS[UID] = User(UID, loc, i[0], AOI)
                PM.EVENTS.addEvent(nextJoin, 'JOIN', UID, UID)
                nextJoin += Config.JOIN_DELAY
                UID += 1
        else: #Location matter
            deltaX = Config.SIZE_TORUS[0]/float(i[1][0])
            deltaY = Config.SIZE_TORUS[1]/float(i[1][1])
            for j in range(i[1][0]):
                for k in range(i[1][1]):
                    ll = [j * deltaX, k * deltaY] #Lower left
                    ur = [(j + 1) * deltaX, (k + 1) * deltaY] #Upper right
                    AOI = [ll, ur]
                    loc = newRndLocation(AOI, allocatedLocs)
                    PM.APPS[UID] = User(UID, loc, i[0], AOI)
                    PM.EVENTS.addEvent(nextJoin, 'JOIN', UID, UID)
                    nextJoin += Config.JOIN_DELAY                    
                    UID += 1
    #Prefetch sensor readings & queries 
    fetchSenQue(0, Config.PREFETCH_TIME, Config.PREFETCH_INTERVAL)


def newRndLocation(AOI, allocatedLocs):
    '''
    Find new random location to place application
    @param AOI: Areas of interest defined using lower left edge of boundary 
    [x, y] & upper right edge of boundary [x, y]
    @param allocatedLocs: Locations that are already allocated 
    @return: (x, y) coordinates of new location
    '''
    while True:
        x = round(random.uniform(AOI[0][0], AOI[1][0]), 4) #X  
        y = round(random.uniform(AOI[0][1], AOI[1][1]), 4) #Y
        if [x, y] not in allocatedLocs: return [x, y] 


def fetchSenQue(cTime, prefetchTime, prefetchInterval):
    '''
    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 prefetcInterval: Time interval between 2 prefect operations
    @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], 'WS', reading[0], reading)
        if PM.advStart == -1: PM.advStart = reading[1]
        if reading[1] > endTime: break
    while 1:
        query = PM.IN_DATA.getQuery()
        if len(query) == 0:
            noQueries = True
            break
        query[0] += Config.NUM_W_STATIONS #Offset user id with weather stations
        PM.EVENTS.addEvent(query[1], 'QUE', query[0], query)
        if PM.queStart == -1: PM.queStart = query[1]
        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 + prefetchInterval, 'FETCH', -1, -1)
    return False


def collectStat(cTime, finish):
    '''
    Collect statistics from nodes
    @param cTime: 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(cTime) + '\t' + str(i) 
        results = PM.APPS[i].getStat()
        for j in results: tmpStr += '\t' + str(j)
        fdResults.write(tmpStr + '\n')
        results2 = PM.APPS[i].dumpResults()
        for k in results2:
            tmpStr = 'WS\t' if PM.APPS[i].type == 'WS' else 'User\t'
            tmpStr += str(i)
            for l in k: tmpStr += '\t' + str(l)
            fdResults.write(tmpStr + '\n')

    if finish == False: #No the end. So set next timer
        PM.EVENTS.addEvent(cTime + Config.STAT_COLLECTION_INTERVAL, 'STAT', \
                           -1, [])                
    else: #End sp gGet overlay & underlay data
        for i in PM.OVERLAY:
            tmpStr = 'Ov\t' + PM.OVERLAY[i].type + '\t' + str(cTime) +'\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(cTime) +'\t'+ \
                                                                        str(i)
            results = PM.UNDERLAY[i].getStat()
            for j in results: tmpStr += '\t' + str(j)
            fdResults.write(tmpStr + '\n')


def main(*argv):
    '''
    Start of program
    '''
    assert(len(sys.argv) == 2), \
            'Incorrect parameters. Input should be \'python main.py <seed>\''
    startTime = datetime.datetime.now()
    
    initApps(sys.argv[1]) #Initialize weather stations & end users
    print str(time.asctime(time.localtime(time.time()))), 'Finished loading '+\
        'weather stations & end users. Pre-fetched sensor readings & queries'
    #Set timer to collect stat
    PM.EVENTS.addEvent(Config.STAT_COLLECTION_INTERVAL, 'STAT', -1, [])    
    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'
#            for i in PM.OVERLAY: 
#                for j in PM.OVERLAY[i]._CAN._FIB._FIB:
#                    if PM.OVERLAY[i]._CAN._FIB._FIB[j][1] is not None:
#                        print i, j, PM.OVERLAY[i]._CAN._FIB._FIB[j][0], \
#                        PM.OVERLAY[i]._CAN._FIB._FIB[j][1].min, PM.OVERLAY[i]._CAN._FIB._FIB[j][1].max
#                    else:
#                        print i, j, PM.OVERLAY[i]._CAN._FIB._FIB[j][0], \
#                        PM.OVERLAY[i]._CAN._FIB._FIB[j][1]
            break
        
        if eventCount % 20000 == 0: 
            print event[0] #Status update
        eventCount += 1
        
        if event[0] > simTime:
            print str(time.asctime(time.localtime(time.time()))), \
                'End of simulation time reached', event[0]
            break
        
        if event[1] == 'QUE': #Query
            PM.APPS[event[2]].genInterest(event[3], event[0])
        elif event[1] == 'WS': #Sensor readings from weather station
            PM.APPS[event[2]].genInterest(event[3], event[0])
        elif event[1] == 'UN2OV': #Message from underlay
            PM.OVERLAY[event[2]].recMsg(event[3][0], event[3][1])
        elif event[1] == 'FETCH': #Fetch sensor readings & queries
            lastQue = fetchSenQue(event[0], Config.PREFETCH_TIME, \
                                                    Config.PREFETCH_INTERVAL)
            if lastQue: #Last query, so set simulation end time
                simTime = event[0] + Config.SIM_TIME_AFTER_LAST_QUERY
        elif event[1] == 'JOIN': #Create underlay, then overlay & join overlay
            assert(event[0] < PM.advStart and event[0] < PM.queStart), \
                'Sensor reading ads or queries started before network stabilized' 
            PM.UNDERLAY[event[2]] = NIC(event[2], PM.APPS[event[2]].loc, \
                                        PM.APPS[event[2]].type)
            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, PM.APPS[event[2]].inOverlay)
        elif event[1] == 'REJOIN': #Rejoin overlay 
            assert(event[0] < PM.advStart and event[0] < PM.queStart), \
                'Sensor reading ads or queries started before network stabilized' 
            PM.OVERLAY[event[2]].rejoinOverlay()
        elif event[1] == 'STAT': #Collect stat & reschedule timer
            collectStat(event[0], False)
        else:
            assert(False), 'Unknown event type. Terminating ' + event[1]

    collectStat(PM.EVENTS.now, True) #Collect remaining stat
    fdResults.close()
    print 'Total execution time', datetime.datetime.now() - startTime
    print 'Results located at:', os.getcwd()
    return 0

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