'''
Created on Aug 31, 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 copy
import random
import Config as Config
import PeerManager as PM
from CCNInterest import CCNInterest
from CCNData import CCNData


class WeatherStation(object):
    '''
    Weather station class
    '''
    
    def __init__(self, SID, loc, interval, inOverlay, useThresholds, sensors):
        '''
        Constructor
        @param SID: Station ID
        @param loc: Location
        @param interval: Sampling interval of sensors
        @param inOverlay: Is node part of overlay
        @param useThresholds: Use threshold to decide whether sensor reading 
        changed significantly
        @param sensors: List of sensor names 
        '''
        self.SID = SID
        self.loc = loc     #[X, Y, ELEV] #ELEV is height
        self.type = 'WS'        #This is a weather station
        self._vTime = Config.MIN_VALID_TIME \
                            if (interval < Config.MIN_VALID_TIME) else interval
        self.inOverlay = inOverlay
        self._useThresh = useThresholds
        self._readings = [] #List of [sensor name, values]        
        self._idx = {}              #Index of sensor readings
        self._lastReadingAdv = []   #Last sensor reading that was advertised
        for name in sensors:
            self._lastReadingAdv.append([name, -1.0])
        self._lastReadingAdvTime = -1 #Time last reading was advertised
        self._numAds = 0            #No of times sensor readings were advertised
        self._numAdsSend = 0        #Actual no of ads send
        self._numAdsRec = 0         #No of Ads receive from other nodes
        self._numQueFwd = 0         #No of query messages forwarded
        self._pendAds = {}          #Pending Ads 
        self._pendSensorInter = {}  #Waiting sensor reading interest messages
        self._penQueryInter = {}    #Waiting query interest messages
        self._results = []          #Track cost of sending messages


    def __del__(self):
        '''
        Destructor
        '''
        self._lastReadingAdv = None
        self._pendSensorInter = None
        self._results = None


    def genInterest(self, readings, cTime):
        '''
        Generate interest message to advertise sensor readings that are 
        different from last update. CCN interest message also carry sensor data
        @param readings: Sensor readings to advertise
        @param cTime: Current time 
        '''
        assert(self.SID == readings[0]), 'Wrong sensor ' + str(self.SID) +\
                                                ' ' + str(readings[0])
        toAdv = self._hasReadingChanged(readings[2:], cTime)
        self._readings = readings #Sensor readings
        if len(toAdv) == 0: return  #No updates to send. So no cost

        #Need to advertise attributes. No need to explicitly remove previous 
        #values as we already no sampling interval so that timeout of indexed
        #can be defined        
        #First find hash values
        myHash = PM.HASH.val2Points(toAdv)
        nonce = -1
        while 1: #Find a random nonce
            nonce = random.randint(0, 1000000)
            if nonce not in self._pendAds: break
        #Track Ads [no of ads send, time send, [cost of each ad]]
        self._pendAds[nonce] = [len(myHash), cTime, []]

        for i in myHash: #Advertise to each point in torus
            expireTime = cTime + self._vTime
            name = self._genNameSensor(i, cTime, expireTime, toAdv)
            self._pendSensorInter[name] = nonce #Track each interests 
            newMsg = CCNInterest(name, name, i, i, cTime, 'WS', self.SID, \
                        toAdv, expireTime) #Advertise attributes
            reply = PM.OVERLAY[self.SID].fwdInter(newMsg)
            assert(reply is None), 'Data received for interest message that' + \
                                ' adv sensor readings at ' + str(self.SID)
            self._numAdsSend += 1
        self._numAds += 1


    def _genNameSensor(self, point, genTime, vTime, readings):
        '''
        Generate name based on given parameters for sensor data interest message
        @param point: Point to advertise to
        @param genTime: Time sensor readings were generated 
        @param vTime: Sensor readings is valid until this time
        @param readings: Sensor readings to advertise
        '''
        tmpStr = ''
        for i in point: tmpStr += '/' + str(round(i, 3))
        #tmpStr += tmpStr #Duplicate to crate an area as this is a point
        tmpStr += '/WS/' + str(genTime) + '/' + str(self.SID) + '/' + \
                                                        str(round(vTime, 3)) + '/'
        for i in readings:
            tmpStr += i[0] + '/' + str(round(i[1], 3)) + '/'
        return tmpStr


    def _genNameQue(self, areas, vTime, query):
        '''
        Generate query name based on given parameters
        @param sortedArea: Areas to query as a list of [[SFC index, 
        [(lower left) x, y, z, ...], [(upper right) x, y, z]], [...] ]
        @param vTime: Sensor readings is valid until this time
        @param query: [partial query, actual query]. Partial query useful in 
        handling cache entries
        '''
        tmpStr = ''
        for i in areas[0][1]: tmpStr += '/' + str(round(i, 3)) #Lower left
        for i in areas[0][2]: tmpStr += '/' + str(round(i, 3)) #Upper right
        tmpStr += '/QUE/'
        tmpStr2 = tmpStr + str(vTime)
        for i in query:
            tmpStr2 += '/' + i[0] + '/' + str(i[1]) + '/' + str(i[2])
        if Config.APPEND_TO_VISIT == True: #Append names of areas to visit
            for i in range(1, len(areas)): #Skip 1st one which is already used
                for j in areas[i][1]: tmpStr2 += '/' + str(j) #Lower left
                for j in areas[i][2]: tmpStr2 += '/' + str(j) #Upper right
        else: #Add a nonce to make message unique
            nonce = random.randint(0, 100000000)
            tmpStr2 += str(nonce)

        tmpStr2 += '/'
        return [tmpStr, tmpStr2]


    def recMsg(self, msg):
        '''
        Receive message from overlay/CCN
        @param msg: Message
        '''
        if msg.msgType == 'CCND': #Response to sensor update interest
            if msg.dataType == 'WS': #Sensor update form a weather station
                self._handleDataSensor(msg)
            elif msg.dataType == 'QUE': #Query from end user
                self._handleDataQue(msg)
            else: assert(False), 'Unknown data message type ' + msg.dataType
        elif msg.msgType == 'CCNI': #Sensor readings
            assert(self.inOverlay), 'CCNI msg received by a node not in overlay'
            if msg.dataType == 'WS': #Sensor update form a weather station
                self._addIdx(msg)
            elif msg.dataType == 'QUE': #Query from end user
                self._handleQuery(msg)
            else: assert(False), 'Unknown interest message type ' + msg.dataType
        else: assert(False), 'Unknown message type ' + msg.msgType


    def _handleQuery(self, msg):
        '''
        Handle query from end user. If other zones need to be visited first 
        visit them before checking this nodes index
        @param msg: Interest message containing query 
        '''
        mustSearch = PM.OVERLAY[self.SID].removeAreaInZone(msg.data[1]) \
                        if len(msg.data[1]) > 0 else []

        if len(mustSearch) > 0: #More areas to be searched?
            names = self._genNameQue(mustSearch, msg.genTime, msg.data[0])
            self._penQueryInter[names[1]] = [msg.name, msg.realName, \
                                             msg.data[0], msg.genTime]
            newMsg = CCNInterest(names[0], names[1], mustSearch[0][1], \
                    mustSearch[0][2], msg.genTime, 'QUE', self.SID, \
                    [msg.data[0], mustSearch[1:]], -1, msg.hops)
            reply = PM.OVERLAY[self.SID].fwdInter(newMsg)
            if reply is not None:
                print 'Data received to a query message forwarded by sensor to another ZC' #TODO
            self._numQueFwd += 1
        else: #Search local index 
            results = self._queryIdx(msg.data[0], msg.genTime, PM.EVENTS.now,[]\
                                                                    , 9999999)
            #Data size considering sensor readings are actually send to end user
            dataSize = len(results[0]) * Config.SIZE_SENSOR_READINGS
            newMsg = CCNData(msg.name, msg.realName, dataSize, results[0], \
                                        'QUE', results[1], msg.hops, -1, None)
            PM.OVERLAY[self.SID].fwdData(newMsg)


    def _addIdx(self, msg):
        '''
        Handle sensor readings from weather station 
        @param msg: Interest message containing sensor updates 
        '''
        self._idx[msg.sensorID] = [msg.expireTime, msg.genTime, \
                                                    copy.deepcopy(msg.data)]
        newMsg = CCNData(msg.name, msg.realName, 2, 'OK', 'WS', msg.genTime, \
                                                msg.hops, self.SID, self.loc)
        PM.OVERLAY[self.SID].fwdData(newMsg)
        self._numAdsRec += 1


    def _handleDataSensor(self, msg):
        '''
        Handle data message corresponding to sensor readings. Add stat
        @param msg: Data message  
        '''
        if msg.realName not in self._pendSensorInter:
            print 'WeatherStation:recMsg No pending interest', msg.realName
            return
        nonce = self._pendSensorInter[msg.realName]
        self._pendAds[nonce][2].append(msg.hops) #Track hops
        self._pendAds[nonce][0] -= 1
        if self._pendAds[nonce][0] == 0: #Got all data msgs
            #Track ['Adv', current time, time to get response, hops] 
            self._results.append(['Adv', self._pendAds[nonce][1], \
                PM.EVENTS.now - self._pendAds[nonce][1], \
                self._pendAds[nonce][2]])
            del self._pendAds[nonce]
        del self._pendSensorInter[msg.realName]


    def _handleDataQue(self, msg):
        '''
        Handle data received for query message. Search local index. Combine 
        results with what's already in data message then send results to sender
        @param msg: Data message 
        '''
        origiData = self._penQueryInter[msg.realName] #Info about original msg
        results = self._queryIdx(origiData[2], origiData[3], PM.EVENTS.now, \
                                                        msg.data, msg.genTime)
        #Data size considering sensor readings are actually send to end user
        dataSize = len(results[0]) * Config.SIZE_SENSOR_READINGS
        newMsg = CCNData(origiData[0], origiData[1], dataSize, results[0], \
                         'QUE', results[1], msg.hops, -1, None)
        PM.OVERLAY[self.SID].fwdData(newMsg)


    def _queryIdx(self, query, vTime, cTime, cResults, minGenTime):
        '''
        Handle query from end user. Search resource index & send answer back.
        Assume a query is looking for all the possible weather stations 
        @param query: Query in interest message
        @param vTime: Valid time (data need to be fresh than this) 
        @param cTime: Current time
        @param cResults: Current list of weather stations found
        @param minGenTime: Data generation time of oldest sensor reading   
        '''
        results = cResults
        toRemove = []
        for nID in self._idx: #Try all weather stations
            attribsMatch = False
            if self._idx[nID][0] < cTime: #Expired
                toRemove.append(nID)
                continue
            if self._idx[nID][1] < vTime: continue #Data too old
        
            for j in range(len(query)): #Check each attribute in query
                #Skip RND attribute added to make no of attribute match 
                #dimensions of d-torus. Assume that queries include only the 
                #attributes used to describe a resource. So 'RND' is skipped
                for k in range(len(self._idx[nID][2])):#each attribute in entry
                    if query[j][0] == self._idx[nID][2][k][0]: #Att name match
                        if (query[j][1] <= self._idx[nID][2][k][1] <= \
                                                                query[j][2]):
                            attribsMatch = True #Match
                        else: attribsMatch = False #At least 1 attribute mismatch
                        break
                if attribsMatch == False: break #Break outer loop as well
            if attribsMatch == True: #If all required attributes match
                if nID not in results: 
                    results.append(nID) #No duplicates
                    if self._idx[nID][1] < minGenTime: 
                        minGenTime = self._idx[nID][1]
        for nID in toRemove: #Remove expired entries
            del self._idx[nID]
        return [results, minGenTime]


    def _hasReadingChanged(self, readings, cTime):
        '''
        Did the sensor readings changed significantly? If a sensor reading is 
        missing substitute with previous value (if available) 
        @param readings: List of sensor readings
        @param cTime: Current time 
        @return: readings to advertise 
        '''
        newReadings = copy.deepcopy(readings)
        toAdv = []  #Sensor readings to be advertised

        for i in range(len(self._lastReadingAdv)):
            #Check whether sensor readings changed significantly?
            name = self._lastReadingAdv[i][0]
            hasReading = False
            for j in range(len(readings)): #What's in new readings
                if readings[j][0] != name: continue
                hasReading = True
                v = readings[j][1]
                if name == 'X': #X location
                    if self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                    elif (v >= self._lastReadingAdv[i][1] + Config.T_X) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_X):
                        assert(False), 'X location changed'
                elif name == 'Y': #Y location
                    if self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                    elif (v >= self._lastReadingAdv[i][1] + Config.T_Y) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_Y):
                        assert(False), 'Y location changed'
                elif name == 'ELEV': #Elevation/height
                    if self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                    elif (v >= self._lastReadingAdv[i][1] + Config.T_ELEV) or \
                             (v <= self._lastReadingAdv[i][1] - Config.T_ELEV):
                        assert(False), 'Elevation changed'
                elif name == 'DD': #Wind direction
                    if (v >= self._lastReadingAdv[i][1] + Config.T_DD) or \
                                (v <= self._lastReadingAdv[i][1] - Config.T_DD):
                        toAdv.append(v) #This needs to be advertised
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'FF': #Wind speed
                    if (v >= self._lastReadingAdv[i][1] + Config.T_FF) or \
                                (v <= self._lastReadingAdv[i][1] - Config.T_FF):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'FFGUST': #Wind gust
                    if (v >= self._lastReadingAdv[i][1] + Config.T_FFGUST) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_FFGUST):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'P': #Station pressure 
                    if (v >= self._lastReadingAdv[i][1] + Config.T_P) or \
                                (v <= self._lastReadingAdv[i][1] - Config.T_P):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'PCP5M': #5min accumulated precip
                    if (v >= self._lastReadingAdv[i][1] + Config.T_PCP5M) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_PCP5M):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'PCP15M': #5min accumulated precip
                    if (v >= self._lastReadingAdv[i][1] + Config.T_PCP15M) \
                        or (v <= self._lastReadingAdv[i][1] - Config.T_PCP15M):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'PCP1H': #5min accumulated precip
                    if (v >= self._lastReadingAdv[i][1] + Config.T_PCP1H) \
                        or (v <= self._lastReadingAdv[i][1] - Config.T_PCP1H):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'PCPRATE':
                    if (v >= self._lastReadingAdv[i][1] + Config.T_PCPRATE) \
                        or (v <= self._lastReadingAdv[i][1] - Config.T_PCPRATE):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'RH': #Relative humidity
                    if (v >= self._lastReadingAdv[i][1] + Config.T_RH) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_RH):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                elif name == 'T': #Air temperature
                    if (v >= self._lastReadingAdv[i][1] + Config.T_T) or \
                            (v <= self._lastReadingAdv[i][1] - Config.T_T):
                        toAdv.append(v)
                    elif self._lastReadingAdv[i][1] < 0: #Not available earlier
                        toAdv.append(v) #This needs to be advertised
                else: assert(False), 'Unmatched attribute ' + name
                break
            if hasReading == False: #Sensor reading not available this time
                #Reuse previous reading if available. This is better than adding
                #a random value calculation in hash
                if self._lastReadingAdv[i][1] >= 0:
                    newReadings.append([name, self._lastReadingAdv[i][1]])
        #Has changes, threshold not used, or advertised too old?
        if toAdv or (self._useThresh == False) or \
                (cTime - self._lastReadingAdvTime) >= self._vTime: 
            #update last advertised attributes
            for i in range(len(self._lastReadingAdv)):
                for j in newReadings:
                    if j[0] == self._lastReadingAdv[i][0]:
                        self._lastReadingAdv[i][1] = j[1]
                        break
            self._lastReadingAdvTime = cTime                    
            return newReadings
        else: return [] #Not changed


    def dumpResults(self):
        '''
        Dump current results & reset collector
        @return: Results
        '''
        tmp = self._results
        self._results = []
        return tmp


    def getStat(self):
        '''
        Get statistics 
        '''
        return [len(self._pendAds), len(self._pendSensorInter), len(self._idx),\
            self._numAds, self._numAdsSend, self._numAdsRec, self._numQueFwd]
