'''
Created on Jan 26, 2012
Modified on Apr 20, 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 BaseConfig
import copy
import math
import random
import Config as Config
import PeerManager as PM
from AdvMsg import AdvMsg
from PeerID import PeerID
from QueryMsg import QueryMsg


class Resource(object):
    '''
    Resource class. Responsible for advertising resource attributes & issuing
    queries. 
    '''

    def __init__(self, PID, startTime, attribs):
        '''
        Constructor
        @param PID: Resource's peer ID
        @param startTime: Arrival time of resource  
        '''
        self.PID = PID
        self.proxyNID = -1 #Use this node to send messages through overlay
        self._attVals = []
        self._lastAttVals = [] #Attribute values advertised last
        self._numUpdatesSend = 0
        self._numQueSend = 0
        #Add advertise event
        PM.EVENTS.addEvent(startTime, 'ADV', PID.NID, [attribs, PID.NID], True)
        #Schedule query timer. make sure resources are advertised first
        #Use Poison distribution
        PM.EVENTS.addEvent(Config.DELAY_BEFORE_QUE + \
            random.expovariate(1.0/Config.QUE_RATE), 'QUE', PID.NID, None, True)


    def advertise(self, res, useThresholds, needAllAttri, advNID):
        '''
        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 useThresholds: Use threshold to decide whether attribute changed
        @param needAllAttri: Need to advertise all attributes
        @param advNID: Advertise under this NID
        '''
        atts = self._hasResChanged(res, useThresholds, needAllAttri, advNID)
        if len(atts) == 0: return #No updates to send. So no cost
        #Need to advertise attributes
        contriAtts = []
        hops = [[0], [0]] #[adv hops, remove hops]
        self._numUpdatesSend += 1
        
        #First remove then re-advertise attributes. Only if same NID
        if (len(atts[1]) > 0) and (advNID == self.PID.NID):
            newMsg = AdvMsg(self.PID, atts[1], 'Remove')
            reply = PM.OVERLAY[self.proxyNID].sendMsg(newMsg)
            assert(reply), 'Unable to send Remove msg at ' + str(self.PID.NID)
            hops[1] = reply
            if self.PID.NID != self.proxyNID: hops[1][0] += 1#cost to reach proxy
            
        source = self.PID if (advNID == self.PID.NID) else PeerID(advNID, -1) 
        newMsg = AdvMsg(source, atts[0]) #Advertise attributes
        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
         
        #Track which attribute(s) contribute to update/add cost
        if len(atts[0]) > 0 and len(atts[1]) == 0: #1st time update
            for i in range(len(atts[0])): contriAtts.append(atts[0][i][0])
        elif len(atts[1]) > 0: #caused by changing attribute value(s)
            for i in range(len(atts[1])): contriAtts.append(atts[1][i][0])
        hops.append(contriAtts)
        PM.STAT.trackStat('Adv', hops)


    def query(self, que):
        '''
        Forward query to overlay
        @param que: Query given as a list
        '''
        queMsg = QueryMsg(self.PID, que[0][1], copy.deepcopy(que[1:]))
        #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
        contriAtts = []
        for i in range(len(queMsg.query)): contriAtts.append(queMsg.query[i][0])
        #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, contriAtts])
        PM.EVENTS.addEvent(PM.EVENTS.now + random.expovariate(1/Config.QUE_RATE)\
                           , 'QUE', self.PID.NID) #Reschedule timer


    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 _hasResChanged(self, res, useThreshold, needAllAtt, advNID):
        '''
        Has the resource attributes changed 
        @param res: Resource attribute set
        @param useThreshold: Use threshold to overcome minor variations
        @param needAllAtt: Do we need to return all attributes, even ones 
        that didn't changed
        @param advNID: Advertise under this NID        
        @return: New list of attribute values 
        '''
        atts = copy.deepcopy(res)
        self._attVals = atts
        attsToAdv = []  #Attributes to be advertised
        attsToRemove = []  #Attributes to be re-advertised
        #Never advertised or no threshold used. Just send as it is
        if (len(self._lastAttVals) == 0) or (useThreshold == False) or (advNID \
                                                            != self.PID.NID): 
            #Different from last value or different NID
            if (self._lastAttVals != atts) or (advNID != self.PID.NID):
                tmp = self._lastAttVals
                self._lastAttVals = atts
                return [self._lastAttVals, tmp]
            else: return []
        else:
            for i in range(len(atts)):  #For each attribute check
            #Even static attributes can change once in a while
                if atts[i][0] == 'RspT':  #attrib value has change significantly
                    respTime = atts[i][1]
                    if (respTime >= self._lastAttVals[i][1] + \
                            BaseConfig.T_RESP_TIME) \
                        or (respTime <= self._lastAttVals[i][1] - \
                            BaseConfig.T_RESP_TIME):
                        #This needs to be advertised
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = respTime
                elif atts[i][0] == 'SIn': 
                    swapIn = atts[i][1]
                    if (swapIn >= self._lastAttVals[i][1] + \
                            BaseConfig.T_SWAP_IN) \
                        or (swapIn <= self._lastAttVals[i][1]\
                            - BaseConfig.T_SWAP_IN):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = swapIn
                elif atts[i][0] == 'SOut': 
                    swapOut = atts[i][1]
                    if (swapOut >= self._lastAttVals[i][1] + \
                            BaseConfig.T_SWAP_OUT) \
                        or (swapOut <= self._lastAttVals[i][1] - \
                            BaseConfig.T_SWAP_OUT):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = swapOut
                elif atts[i][0] == 'DIn':
                    diskIn = atts[i][1]
                    if (diskIn >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_IN) \
                        or (diskIn <= self._lastAttVals[i][1] - \
                            BaseConfig.T_DISK_IN):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskIn
                elif atts[i][0] == 'DOut':
                    diskOut = atts[i][1]
                    if (diskOut >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_OUT) \
                        or (diskOut <= self._lastAttVals[i][1] -\
                             BaseConfig.T_DISK_OUT):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskOut
                elif atts[i][0] == 'DSvc':
                    diskSvc = atts[i][1]
                    if (diskSvc >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_SVC) \
                        or (diskSvc <= self._lastAttVals[i][1] - \
                            BaseConfig.T_DISK_SVC):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskSvc
                elif atts[i][0] == 'DUtil':
                    diskUtil = atts[i][1]
                    if (diskUtil >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_UTIL) \
                        or (diskUtil <= self._lastAttVals[i][1] - \
                            BaseConfig.T_DISK_UTIL):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskUtil
                elif atts[i][0] == 'UpT':
                    upTime = atts[i][1]
                    if (upTime >= self._lastAttVals[i][1] + \
                            BaseConfig.T_UPTIME) \
                        or (upTime <= self._lastAttVals[i][1] \
                            - BaseConfig.T_UPTIME):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = upTime
                elif atts[i][0] == '1mLd':
                    oneMLoad = atts[i][1]
                    if (oneMLoad >= self._lastAttVals[i][1] + \
                            BaseConfig.T_1_MIN_LOAD) \
                        or (oneMLoad <= self._lastAttVals[i][1] - \
                            BaseConfig.T_1_MIN_LOAD):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = oneMLoad
                elif atts[i][0] == '5mLd': 
                    fiveMLoad = atts[i][1]
                    if (fiveMLoad >= self._lastAttVals[i][1] + \
                            BaseConfig.T_5_MIN_LOAD) \
                        or (fiveMLoad <= self._lastAttVals[i][1] - \
                            BaseConfig.T_5_MIN_LOAD):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = fiveMLoad
                elif atts[i][0] == '15mLd':
                    fifteenMLoad = atts[i][1]
                    if (fifteenMLoad >= self._lastAttVals[i][1] + \
                            BaseConfig.T_15_MIN_LOAD) \
                        or (fifteenMLoad <= self._lastAttVals[i][1] - \
                            BaseConfig.T_15_MIN_LOAD):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = fifteenMLoad
                elif atts[i][0] == 'TMax':
                    timerMax = atts[i][1]
                    if (timerMax >= self._lastAttVals[i][1] + \
                            BaseConfig.T_TIMER_MAX) \
                        or (timerMax <= self._lastAttVals[i][1] - \
                            BaseConfig.T_TIMER_MAX):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = timerMax
                elif atts[i][0] == 'TAve':
                    timerAve = atts[i][1]
                    if (timerAve >= self._lastAttVals[i][1] + \
                            BaseConfig.T_TIMER_AVE) \
                        or (timerAve <= self._lastAttVals[i][1] - \
                            BaseConfig.T_TIMER_AVE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = timerAve
                elif atts[i][0] == 'MFree':
                    memFree = atts[i][1]
                    if (memFree >= self._lastAttVals[i][1] + \
                            BaseConfig.T_MEM_FREE) \
                        or (memFree <= self._lastAttVals[i][1] - \
                            BaseConfig.T_MEM_FREE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = memFree
                elif atts[i][0] == 'DUsed': 
                    diskUsed = atts[i][1]
                    if (diskUsed >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_USED) \
                        or (diskUsed <= self._lastAttVals[i][1] - \
                            BaseConfig.T_DISK_USED):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskUsed
                elif atts[i][0] == 'DFree':
                    diskFree = atts[i][1]
                    if (diskFree >= self._lastAttVals[i][1] + \
                            BaseConfig.T_DISK_FREE) \
                        or (diskFree <= self._lastAttVals[i][1] - \
                            BaseConfig.T_DISK_FREE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskFree
                elif atts[i][0] == 'DSize':
                    diskSize = atts[i][1]
                    if math.fabs(diskSize - self._lastAttVals[i][1]) > 0.001 :
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = diskSize
                        #print 'Static attribute Disk Size changed'
                elif atts[i][0] == 'MSize':
                    memSize = atts[i][1]
                    if math.fabs(memSize - self._lastAttVals[i][1]) > 0.001:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = memSize
                        #print 'Static attribute Memory Size changed'
                elif atts[i][0] == 'MAct':
                    memActive = atts[i][1]
                    if (memActive >= self._lastAttVals[i][1] + \
                            BaseConfig.T_MEM_ACTIVE) \
                        or (memActive <= self._lastAttVals[i][1] - \
                            BaseConfig.T_MEM_ACTIVE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = memActive
                elif atts[i][0] == 'SUsed':
                    swapUsed = atts[i][1]
                    if (swapUsed >= self._lastAttVals[i][1] + \
                            BaseConfig.T_SWAP_USED) \
                        or (swapUsed <= self._lastAttVals[i][1] - \
                            BaseConfig.T_SWAP_USED):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = swapUsed
                elif atts[i][0] == 'KVer':
                    kernVer = copy.copy(atts[i][1])
                    if kernVer != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = kernVer
                elif atts[i][0] == 'CFree':
                    cpuFree = copy.copy(atts[i][1])
                    if (cpuFree >= self._lastAttVals[i][1] + \
                            BaseConfig.T_CPU_FREE) \
                        or (cpuFree <= self._lastAttVals[i][1] - \
                            BaseConfig.T_CPU_FREE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = cpuFree
                elif atts[i][0] == 'CSp':
                    cpuSpeed = copy.copy(atts[i][1])
                    if math.fabs(cpuSpeed - self._lastAttVals[i][1]) > 0.001:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = cpuSpeed
                elif atts[i][0] == 'NCore':
                    numCores = atts[i][1]
                    if numCores != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = numCores
                elif atts[i][0] == 'CorePC':
                    corePCPU = atts[i][1]
                    if corePCPU != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = corePCPU
                elif atts[i][0] == 'FCName':
                    fcNamex = copy.copy(atts[i][1])
                    if fcNamex != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = fcNamex
                elif atts[i][0] == 'Tx':
                    txRate = atts[i][1]
                    if (txRate >= self._lastAttVals[i][1] + BaseConfig.T_TX_RATE) \
                        or (txRate <= self._lastAttVals[i][1] - \
                            BaseConfig.T_TX_RATE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = txRate
                elif atts[i][0] == 'Rx':
                    rxRate = atts[i][1]
                    if (rxRate >= self._lastAttVals[i][1] + BaseConfig.T_RX_RATE) \
                        or (rxRate <= self._lastAttVals[i][1] - \
                            BaseConfig.T_RX_RATE):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = rxRate
                elif atts[i][0] == 'Boot':
                    boot = atts[i][1]
                    if boot != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = boot
                elif atts[i][0] == 'NType':
                    nodeType = atts[i][1]
                    if nodeType != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = nodeType
                elif atts[i][0] == 'BWLim': 
                    bwLimit = atts[i][1]
                    if bwLimit != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = bwLimit
                elif atts[i][0] == 'Lati':
                    lati = atts[i][1]
                    if math.fabs(lati - self._lastAttVals[i][1]) > 0.001:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = lati
                elif atts[i][0] == 'Longi':
                    longi = atts[i][1]
                    if math.fabs(longi - self._lastAttVals[i][1]) > 0.001:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = longi
                elif atts[i][0] == 'Loc':
                    location = atts[i][1]
                    if location != self._lastAttVals[i][1]:
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = location
                elif atts[i][0] == 'Drift':
                    drift = atts[i][1]
                    if (drift >= self._lastAttVals[i][1] + BaseConfig.T_DRIFT) \
                        or (drift <= self._lastAttVals[i][1] - BaseConfig.T_DRIFT):
                        attsToAdv.append(atts[i])
                        attsToRemove.append(copy.deepcopy(self._lastAttVals[i]))
                        self._lastAttVals[i][1] = drift
                else: 
                    print 'ERROR: Unmatched attribute ' + atts[i][0] + '\n'
                    return []

        if attsToAdv: #Need to advertise
            #Do we need all attributes or only the ones that changed?
            if needAllAtt == True:
                self._lastAttVals = atts #1st copy newest values then send
                return [self._lastAttVals, attsToRemove]
            else :
                return [attsToAdv, attsToRemove]            
        else: return [] #Not changed


    def getStat(self):
        '''
        Dump statistics
        @return: Stat
        '''
        return [self.PID.NID, self._numUpdatesSend, self._numQueSend]