'''
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 Config as Config
import PeerManager as PM
from CS import CS
from PIT import PIT
from CANNode import CANNode
from CCNData import CCNData

 
class CCNNode(object):
    '''
    Content-centric networking class
    '''


    def __init__(self, NID, loc, nodeType, sizeCS, sizePIT, sizeFIB, inOverlay):
        '''
        Constructor
        @param NID: Node ID
        @param loc: Location
        @param nodeType: Node type (user or sensor)
        @param sizeCS: Size of content store
        @param sizePIT: Size of Pending Interest Table
        @param sizeFIB: Size of Forwarding Information Base
        @param inOverlay: Add to overlay or not 
        '''
        self.NID = NID
        self.loc = loc #Weather stations contain (x, y, elev)
        self.type = nodeType
        self._CS = CS(sizeCS, Config.CACHING_POLICY)
        self._PIT = PIT(sizePIT)        
        self._CAN = CANNode(NID, loc, sizeFIB)
        self.inOverlay = inOverlay
        self._CAN.joinOverlay(inOverlay)
        self.numInterestsRec = 0    #Count
        self.numInterestsFwd = 0
        self.numDataPacketsRec = 0
        self.numDataPacketsFwd = 0
        self.totalInterestsRec = 0  #Total bytes
        self.totalInterestsFwd = 0
        self.totalDataRec = 0
        self.totalDataFwd = 0
         
                    
    def fwdInter(self, msg, nextHop = -1):
        '''
        Forward interest message. Called by application layer
        @param msg: Message
        @param nextHop: Next hop node if known
        @return: 
        '''
        assert(msg.msgType == 'CCNI'), 'Wrong message type ' + msg.type
        if nextHop != -1: msg.setDirect() #Send direct
        return self._handleIntMsg(msg, self.NID, nextHop)


    def fwdData(self, msg):
        '''
        Forward data. Call by application layer
        @param msg: Data message
        '''
        assert(msg.msgType == 'CCND'), 'Wrong message type ' + msg.type
        
        nodes = self._PIT.get(msg.realName, PM.EVENTS.now, msg.genTime)
        if len(nodes) == 0:
            print 'CCNNode::fwdData PIT timeout or unknown interest @', self.NID
            return
        else: #Forward to each face that had an interest
            for i in nodes:
                if i == self.NID: PM.APPS[self.NID].recMsg(msg) 
                else:
                    self.numDataPacketsFwd += 1
                    self.totalDataFwd += msg.size 
                    reply = PM.UNDERLAY[self.NID].fwdMsg(msg, i)
                    assert(reply), 'Unable to send data message at ' + \
                                                                str(self.NID)
        if self.type != 'WS': #Weather stations should not cache its own data
            result = self._CS.put(msg.name, msg.dataSize, msg.data, msg.genTime\
                                  , msg.source, PM.EVENTS.now)
            if result == -1: #Add to CS
                print 'CCNNode::fwdData object too big for cache at ', self.NID        


    def recMsg(self, msg, sourceNID):
        '''
        Receive message from underlay. Handle accordingly
        @param msg: Message
        @param sourceNID: Node ID of neighbor that send/fwd message
        '''
        if msg.msgType == 'CCNI': self._handleIntMsg(msg, sourceNID)
        elif msg.msgType == 'CCND': self._handleDataMsg(msg, sourceNID)
        elif msg.msgType == 'Join' or msg.msgType == 'JoinRply' or msg.msgType\
            == 'AddNbr' or msg.msgType == 'RemNbr' or msg.msgType == \
            'AppNode' or msg.msgType == 'Rejoin': #Related to underlay
            assert(PM.EVENTS.now <= 300.0)
            self._CAN.recMsg(msg, sourceNID)
        else: assert(False), 'Unknown message type ' + msg.msgType


    def _handleIntMsg(self, msg, sourceNID, destNID = -1):
        '''
        Handle interest message. First check CS & PIT. If failed find next hop 
        neighbor & forward to it. If message for me, forward to app layer
        @param msg: Message
        @param sourceNID: source node
        @param destNID: Destination node, if known. - If so send direct   
        @return: CCNData message if in cache
        '''
        self.numInterestsRec += 1
        self.totalInterestsRec += msg.size
        data = self._CS.get(msg.name, msg.genTime, PM.EVENTS.now)
        if len(data) > 0: #All or part of data in cache.
            #TODO - Handle when there are still more areas to visit while the current area in cache            
            newMsg = CCNData(msg.name, msg.realName, data[0][1], data[0][2], \
                          msg.dataType, data[0][3], msg.hops, data[0][0], None)
            if sourceNID == self.NID: #To my application layer 
                return newMsg                  
            else: #To a remote node
                assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, sourceNID)), \
                    'Unable to send message at ' + str(self.NID)
                return None
        #Not in cache. First find how to forward, then add to PIT only if needed
        else: #Data request from user or sensor readings from weather station 
            result = self._PIT.put(msg.realName, sourceNID, PM.EVENTS.now, \
                                   msg.genTime)
            if result == 0: return None #Already in PIT 
            elif result == 1: #Need to forward
                nextHop = self._CAN.getNextHop(msg.loc)
                assert(nextHop != -1), 'No next hop at ' + str(self.NID)
                if nextHop == self.NID: #For me. Forward to app layer
                    PM.APPS[self.NID].recMsg(msg)
                else:
                    msg.incrementHops()
                    self.numInterestsFwd += 1
                    self.totalInterestsFwd += msg.size
                    assert(PM.UNDERLAY[self.NID].fwdMsg(msg, nextHop)), \
                            'Unable to send message at ' + str(self.NID)
            elif result == -1 : 
                assert(False), 'PIT capacity exceed at node ' + str(self.NID)
            else: assert(False), 'Unhandled state from PIT'
        return None


    def _handleDataMsg(self, msg, sourceNID):
        '''
        Handle data from overlay
        @param msg: Message
        @param sourceNID: Source node  
        '''
        self.numDataPacketsRec += 1
        self.totalDataRec += msg.size
        nodes = self._PIT.get(msg.realName, PM.EVENTS.now, msg.genTime)
        if len(nodes) == 0:
            print 'CCNNode::_handleDataMsg PIT timeout or unknown interest at',\
                self.NID
            return
        else: #Forward to each face that had an interest
            for i in nodes:
                if i == self.NID: PM.APPS[self.NID].recMsg(msg) 
                else:
                    self.numDataPacketsFwd += 1
                    self.totalDataFwd += msg.size
                    assert(PM.UNDERLAY[self.NID].fwdMsg(msg, i)), \
                                'Unable to forward message at ' + str(self.NID)
        #Add to cache. Check whether data is from remote cache        
        if msg.dataType != 'WS': 
            self._CS.put(msg.name, msg.dataSize, msg.data, msg.genTime,\
                                                    msg.source, PM.EVENTS.now)


    def rejoinOverlay(self):
        '''
        Rejoin overlay
        '''
        self._CAN.joinOverlay(self.inOverlay)
        

    def getZone(self):
        '''
        @return: Get the zone
        '''
        return self._CAN.zone


    def removeAreaInZone(self, areas):
        '''
        Find which of the given areas are within this node's zone then remove
        @param areas: List of areas as [[integer, [lower left], [upper right]]]
        @return: Modified areas
        '''
        return self._CAN.removeAreaInZone(areas)


    def getStat(self):
        '''
        Get statistics
        @return: results
        '''
        result = ['CS']
        tmp = self._CS.getStat()
        for i in tmp: result.append(i)
        result.append('PIT')
        tmp = self._PIT.getStat()
        for i in tmp: result.append(i)
        result.append('CAN')
        tmp = self._CAN.getStat()
        for i in tmp: result.append(i)
        result.append('CCN')
        result += [self.numInterestsRec, self.numInterestsFwd, \
                   self.numDataPacketsRec, self.numDataPacketsFwd, \
                   self.totalInterestsRec, self.totalInterestsFwd, \
                   self.totalDataRec, self.totalDataFwd]
        return result
