'''
Created on Sep 11, 2012
@author: Dilum Bandara
@version: 0.1
@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 Config as Config
import PeerManager as PM
from AdvMsg import AdvMsg
from QueryMsg import QueryMsg


class Resource(object):
    '''
    Resource class. Responsible for advertising resource attributes & issuing
    queries. Weather stations are simulated as resources. Subset of them are in
    overlay while others are connect to them. End users are simulated as a 
    resource that is not in overlay.
    '''

    def __init__(self, PID, interval):
        '''
        Constructor
        @param PID: Resource's peer ID
        @param interval: Sampling interval of sensors
        '''
        self.PID = PID
        self._vTime = Config.MIN_VALID_TIME \
                            if (interval < Config.MIN_VALID_TIME) else interval
        self.proxyNID = -1 #Use this node to send messages through overlay
        self._readings = []
        self._lastReadingAdv = [] #Attribute values advertised last
        self._lastReadingAdvTime = -1
        self._numUpdatesSend = 0
        self._numQueSend = 0


    def advertise(self, res, cTime):
        '''
        Advertise attribute values that are different from last update or if 
        this node is representing multiple virtual nodes with different NIDS
        @param res: Resource attributes to advertise
        @param cTime: Current time
        '''
        atts = self._hasReadingChanged(res[2:], cTime)
        if len(atts) == 0: return #No updates to send. So no cost
        self._readings = res[2:]
        #Need to advertise attributes
        hops = [[0], [0]] #[adv hops, remove hops]
        self._numUpdatesSend += 1
        expireTime = cTime + self._vTime
        newMsg = AdvMsg(self.PID.NID, atts, cTime, expireTime) #Advertise
        reply = PM.OVERLAY[self.proxyNID].sendMsg(newMsg)
        assert(reply), 'Unable to send Adv message at ' + str(self.PID.NID)
        hops[0] = reply
        if self.PID.NID != self.proxyNID: hops[0][0] += 1
        PM.STAT.trackStat('Adv', hops)


    def query(self, que, cTime):
        '''
        Forward query to overlay
        @param que: Query given as a list
        @param cTime: Current time 
        '''
        vTime = cTime - que[2] #Data generate after this time is acceptable
        if vTime < 0.0 : vTime = 0.0
        queMsg = QueryMsg(self.PID, copy.deepcopy(que[3:]), vTime)
        #Min & max keys are set at overlay-level depending on query scheme
        self._numQueSend += 1
        replyMsg = PM.OVERLAY[self.proxyNID].sendMsg(queMsg) #Query
        assert(replyMsg), 'Unable to send query at ' + str(self.PID.NID)
        assert(replyMsg.msgType == 'Query'), \
            'Wrong message type or unable to send' + replyMsg.msgType
        #Track which attribute contribute to query cost
        #Return hop count & query success/fail
        hops = replyMsg.hops
        if self.PID.NID != self.proxyNID: hops += 1 #Cost of reaching proxy
        PM.STAT.trackStat('Que', [[hops], replyMsg.success])


    def setProxyID(self, NID):
        '''
        Use this node to send message to overlay
        @param NID: Node ID (not peer ID) - As kyes in peer ID may change 
        '''
        self.proxyNID = NID


    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 getStat(self):
        '''
        Dump statistics
        @return: Stat
        '''
        return [self.PID.NID, self._numUpdatesSend, self._numQueSend]