'''
Created on Nov 3, 2011
Modified Jan 3, 2012
@version: 0.12
@author: Dilum Bandara
@email: dilumb@engr.colostate.edu
@copyright: Copyright(c) 2011, Dilum Bandara
'''

import random
import sys
from FIB import FIB
from Location import Location
import Config as Config
import PeerManager as PM
import Distance as Distance
from OverlayJoinRequestMsg import OverlayJoinRequestMsg 
from OverlayJoinReplyMsg import OverlayJoinReplyMsg
from OverlayUpdateMsg import OverlayUpdateMsg


class CANNode(object):
    '''
    CAN node class
    Join overlay, maintain connections, support addition of new nodes. Removal is
    not supported. Local routing table is the CCN FIB
    '''

    def __init__(self, NID, loc, nodeType, maxSize):
        '''
        Constructor
        @param NID: Node ID
        @param loc: Location
        @param nodeType: Node type  
        @param maxSize: maximum size of FIB 
        '''
        self.NID = NID
        self.loc = loc
        self.type = nodeType
        self.zone = []
        self.splitZoneUsing = 'x' #Split zone at middle of x coordinate
        self._FIB = FIB(maxSize)
        self.numOvJoinRequests = 0


    def joinOverlay(self, inOveraly):
        '''
        Join CAN overlay
        @param inOveraly: Is node a fully functional overlay node 
        @return: Success or failure
        '''
        if len(PM.BOOTSTRAP) == 0: #Am I the first node?
            #Not suppose to a fully functional node & no other nodes in overlay
            if inOveraly == False:
                PM.pendingRadars.append(self.NID) 
                return True
            self.zone = [Location(0, 0), Location(Config.SIZE_TORUS.x, \
                                                  Config.SIZE_TORUS.y)]
            PM.BOOTSTRAP.append(self.NID)
            return True
        #There are other nodes, pick 1 of them randomly & join        
        node = PM.BOOTSTRAP[random.randint(0, len(PM.BOOTSTRAP) - 1)]
        result = PM.UNDERLAY[self.NID].fwdMsg(OverlayJoinRequestMsg(self.NID, \
                                        self.loc, self.type, inOveraly), node)
        if result == False:
            print 'CANNode::joinOverlay Unable to send msg -->', self.NID, node
        return result


    def recMsg(self, msg, source):
        '''
        Receive message from overlay neighbor
        @param msg: Overlay message
        @param source: Source node 
        '''
        if msg.msgType == 'OvJoin': self._handleJoin(msg)
        elif msg.msgType == 'OvJoinReply': self._handleJoinReply(msg, source)            
        elif msg.msgType == 'OvRemoveNbr': #Remove old neighbor
            if self._FIB.remove(msg.NID) == False:
                print 'CANNode::recMsg Unable to remove neighbor at', self.NID
        elif msg.msgType == 'OvAddNbr':
            if self.NID != msg.NID: 
                if self._FIB.put(msg.NID, msg.loc, msg.zone, msg.nodeType, source)\
                 == False:
                    print 'CANNode::recMsg Unable to add neighbor at', self.NID
        elif msg.msgType == 'OvAppNode': self._handleOverlayAppNode(msg, source)            
        elif msg.msgType == 'OvRejoin':
            if len(self.zone) > 0: 
                print 'CAN::recMsg Asked to rejoin while being a zone controller',\
                 self.NID, msg.NID, source            
            else:
                PM.EVENTS.addEvent(PM.EVENTS.now + Config.JOIN_DELAY/2, \
                                   'OvRejoin', self.NID, []) 
        else: print 'CAN::recMsg Message type not supported', msg.msgType        


    def _handleJoin(self, msg):
        '''
        Handle join request from new neighbor
        @param msg: Overlay message 
        '''
        self.numOvJoinRequests += 1
        nextHop = self.getNextHop(msg.loc)
        if nextHop[0] == self.NID: #Zone owner, is new node within my zone?
            if msg.inOveraly == False: #Just add to routing table, don't split
                if self.NID != msg.NID:
                    if PM.UNDERLAY[self.NID].fwdMsg(OverlayJoinReplyMsg([], \
                        self.splitZoneUsing, [[self.loc, self.NID, self.zone, \
                        self.type, self.NID]]), msg.NID) == False:
                        print 'CANNode::_handleJoin Unable to forward message at',\
                                self.NID
                    if self._FIB.put(msg.NID, msg.loc, [], msg.nodeType, msg.NID,\
                                     msg.inOveraly) == False:
                        print 'CANNode::recMsg Routing table full', self.NID
                    return
            newZone = self.splitZone(msg)
            #Zone was not split or this's my 1st neighbor
            nbrs = []
            if (len(newZone) == 0) or (self._FIB.routingTableSize() == 0): 
                #Send reply directly
                if PM.UNDERLAY[self.NID].fwdMsg(OverlayJoinReplyMsg(newZone, \
                    self.splitZoneUsing, [[self.loc, self.NID, self.zone, \
                                    self.type, self.NID]]), msg.NID) == False:
                    print 'CANNode::_handleJoin Unable to forward message at',\
                        self.NID
                    sys.exit() 
            else: #Zone was split. Need to update routing table
                nbrs = self._FIB.nbrsToUpdate(self.zone, newZone)
                #Add me. Send all messages directly                
                nbrs[0].append([self.loc, self.NID, self.zone, self.type, self.NID])
                if PM.UNDERLAY[self.NID].fwdMsg(OverlayJoinReplyMsg(\
                    newZone, self.splitZoneUsing, nbrs[0]), msg.NID) == False:
                    print 'CANNode::_handleJoin Unable to send message at',\
                        self.NID
                    sys.exit()                        
                for i in nbrs[1]: #neighbors to remove
                    if PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg('OvRemoveNbr',\
                        self.NID, self.loc, self.zone, self.type), i) == False:
                        print 'CANNode::_handleJoin Unable to send message at',\
                            self.NID
                        sys.exit()                      
                for i in nbrs[2]: #neighbors to move & rejoin
                    if PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg('OvRejoin',\
                            self.NID, None, None, None), i) == False: 
                        print 'CANNode::_handleJoin Unable to send message at',\
                            self.NID
                        sys.exit()    
                for i in nbrs[3]: #neighbors to Update
                    if PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg('OvAddNbr',\
                        self.NID, self.loc, self.zone, self.type), i) == False: 
                        print 'CANNode::_handleJoin Unable to send message at',\
                            self.NID
                        sys.exit()               
            #Add to my routing table
            if self.NID != msg.NID:
                if self._FIB.put(msg.NID, msg.loc, newZone, msg.nodeType, msg.NID)\
                    == False:
                    print 'CANNode::recMsg Routing table full', self.NID
        else: #Not in my zone or I'm not a zone owner. Send to neighboring zone             
            if PM.UNDERLAY[self.NID].fwdMsg(msg, nextHop[0]) == False:
                print 'CANNode::_handleJoin Unable to forward message at', \
                        self.NID, nextHop, self._FIB._FIB, self.zone
                sys.exit()
        

    def _handleJoinReply(self, msg, source):
        '''
        Handle JoinReply message. Add new nodes to routing table
        @param msg: Message
        @param source: Source node  
        '''
        self.zone = msg.zone #My new zone
        self.splitZoneUsing = msg.splitZoneUsing
        if len(self.zone) > 0: PM.BOOTSTRAP.append(self.NID) #Add as a bootstrap
        if len(self.zone) == 0: #Not a zone
            if len(msg.nbrs) > 1:
                print 'CANNode::_handleJoinReply Many routing entries given to'\
                 + ' none zone controller'
                sys.exit()
            else:
                if self.NID != msg.nbrs[0][1]:
                    if self._FIB.put(msg.nbrs[0][1], msg.nbrs[0][0], \
                            msg.nbrs[0][2], msg.nbrs[0][3], source)== False:
                        print 'CANNode::_handleJoinReply Routing table full',\
                            self.NID
        else: #Zone controller
            for i in range(len(msg.nbrs)): #Add new neighbors
                if self._FIB.isNbrZone(msg.nbrs[i][2], self.zone) == True: #Is adjacent
                    if self.NID != msg.nbrs[i][1]: #Add if not me
                        if self._FIB.put(msg.nbrs[i][1], msg.nbrs[i][0], \
                            msg.nbrs[i][2], msg.nbrs[i][3], msg.nbrs[i][1])== False:
                            print 'CANNode::_handleJoinReply Routing table full'\
                                , self.NID
                        else:#Inform each direct neighbor about me
                            if msg.nbrs[i][1] != source:
                                if PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg(
                                    'OvAddNbr', self.NID, self.loc, self.zone, 
                                    self.type), msg.nbrs[i][1]) == False:
                                    print 'CANNode::_handleJoinReply Unable to send',\
                                        self.NID
                else: #Indirect neighbor. Add only if no other app or closer
                    #App type already in routing table. Is it useful?
                    if msg.nbrs[i][3] in self.getAppList(): 
                        appLocs = self._FIB.getAppsLoc(msg.nbrs[1][3])
                        cantReplace = False
                        for k in range(len(appLocs)):
                            if appLocs[k][2] == True: 
                                cantReplace = True
                                break 
                        if cantReplace == True: continue #Can't replace
                        #Is new node the closest? If not skip
                        if self.isNewAppCloser([msg.nbrs[i][1], msg.nbrs[i][0]],\
                            appLocs) == False: continue                        
                        for k in range(len(appLocs)): self._FIB.remove(appLocs[k][0]) 
                    #Don't know about this app type
                    if self.NID != msg.nbrs[i][1]:
                        if self._FIB.put(msg.nbrs[i][1], msg.nbrs[i][0],\
                            msg.nbrs[i][2], msg.nbrs[i][3], source)== False:
                            print 'CANNode::_handleJoinReply Routing table full'\
                                , self.NID


    def _handleOverlayAppNode(self, msg, source):
        '''
        Handle new adv of application type
        @param msg: Message
        @param source: Source node  
        '''
        if self.type == msg.nodeType: return #Don't add same type
        if msg.nodeType in self.getAppList(): #Already have            
            appLocs = self._FIB.getAppsLoc(msg.nodeType)
            for i in range(len(appLocs)):
                if appLocs[i][2] == True: return #Can't replace
            #Is new node the closest? If not skip
            if self.isNewAppCloser([msg.NID, msg.loc], appLocs) == False:
                return
            for k in range(len(appLocs)): self._FIB.remove(appLocs[k][0]) 
        #Don't know about this app type
        if self.NID != msg.NID:
            if self._FIB.put(msg.NID, msg.loc, msg.zone, msg.nodeType, source)\
                                                == False:
                print 'CANNode::_handleOverlayAppNode Routing table full', self.NID                                    
            else: #Inform to my neighbors
                for j in self._FIB.getNbrZones():
                    if j == source or j == msg.NID: continue#Don't send back to sender
                    PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg('OvAppNode',\
                        msg.NID, msg.loc, msg.zone, msg.nodeType), j)


    def addNodeToFIB(self, NID, loc, zone, nodeType, inOverlay):
        '''
        Add given node to routing table as a direct connection. Also inform 
        about my presence if required.
        @param NID: Node ID of radar to add
        @param loc: Location
        @param zone: Zone
        @param nodeType: Node type
        @param inOverlay: Is a fully functional overlay node        
        '''
        self._FIB.put(NID, loc, zone, nodeType, NID, inOverlay) #Add to my FIB


    def splitZone(self, msg):
        '''
        Try to split zone.
        @param msg: Overlay message
        @return: new zone if split, no zone else ([])
        '''
        zone = []
        if self.splitZoneUsing == 'x': #Split along x-axis
            newX = (self.zone[0].x + self.zone[1].x) / 2.0 #use float
            #If 2 nodes are in 2 slides, then split. Otherwise don't split
            if (msg.loc.x > newX and self.loc.x <= newX): 
                zone = [Location(newX, self.zone[0].y), Location(self.zone[1].x,\
                                                        self.zone[1].y)]
                self.zone[1].x = newX
                self.splitZoneUsing = 'y' #Now split along y-axis                    
            elif (msg.loc.x <= newX and self.loc.x > newX):
                zone = [Location(self.zone[0].x, self.zone[0].y),\
                        Location(newX, self.zone[1].y)]
                self.zone[0].x = newX
                self.splitZoneUsing = 'y' #Now split along y-axis                    
        else:
            newY = (self.zone[0].y + self.zone[1].y) / 2.0 #use float
            if msg.loc.y > newY and self.loc.y <= newY:
                zone = [Location(self.zone[0].x, newY), \
                        Location(self.zone[1].x, self.zone[1].y)]
                self.zone[1].y = newY
                self.splitZoneUsing = 'x'
            elif msg.loc.y <= newY and self.loc.y > newY:
                zone = [Location(self.zone[0].x, self.zone[0].y), \
                        Location(self.zone[1].x, newY)]
                self.zone[0].y = newY
                self.splitZoneUsing = 'x'
        return zone


    def getNextHop(self, loc, appType = ''):        
        '''
        Find next hop to forward message
        @param loc: Location to route to
        @param appType: Application layer type. '' if type doesn't matter 
        @return: [Next hop or -1 if not able to find a next hop, greedy forwarding] 
        ''' 
        #Reached required zone
        if (len(self.zone) > 0) and (Distance.inZone(self.zone, loc) == True):
            if appType == '': return [self.NID, True] #overlay message
            nextHop = self._FIB.getLocalNextHop(loc, appType) 
            if self.type == appType: #Type agree
                if nextHop[0] == -1: return [self.NID, True]
                else: #Which node is closer?
                    if Distance.dist(loc, self.loc) <= nextHop[2]: 
                        return [self.NID, True]
            return [nextHop[0], False] #Not me, so send direct
        else:
            return [self._FIB.getNextHop(loc), True]
        

    def isNewAppCloser(self, newNode, oldNodes):
        '''
        Is location of new app is closer than existing ones
        @param newNode: New node [NID, loc]
        @param oldNodes: Old nodes [[NID, loc]] 
        '''
        distMin = 9999999.0
        for i in range(len(oldNodes)):
            distOld = Distance.dist(self.loc, oldNodes[i][1])
            if distOld < distMin: distMin = distOld
        if Distance.dist(self.loc, newNode[1]) < distMin: return True #Better
        return False
    

    def advAppsInFIB(self):
        '''
        Advertise applications in my FIB to neighbors
        '''
        if len(self.zone) == 0: return #Not a zone
        apps = self._FIB.getAppNodes(['RADAR', 'PROXY'])
        nbrZones = self._FIB.getNbrZones() #Need to advertise new node to nbrs
        for i in range(len(apps)):
            for j in nbrZones: #Skip nodes already being informed
                if apps[i][0] == j: continue #Don't send apps to node itself            
                if PM.UNDERLAY[self.NID].fwdMsg(OverlayUpdateMsg('OvAppNode', \
                    apps[i][0], apps[i][1], apps[i][2], apps[i][3]), j) == False: 
                    print 'CANNode::_handleJoin Unable to send message at', self.NID                                                             


    def getAppList(self):
        '''
        Get list of apps in routing table
        @return: List of application names in routing table
        '''
        apps = self._FIB.getAppList()
        if self.type not in apps: apps.append(self.type)
        if 'PROXY' not in apps: apps.append('PROXY')
        if 'RADAR' not in apps: apps.append('RADAR')
        return apps
    
    
    def getConnectedApps(self, appTypes):
        '''
        Get list of applications that are directly connected to this node.
        @param appTypes: List of application types to get info about
        @result: List of NIDs of applications
        '''
        result = self._FIB.getAppsLoc(appTypes, True)
        if self.type in appTypes: result.append([self.NID, self.loc]) #Add myself
        return result
            
    
    def getNextHopRadar(self, loc, radarRange):
        '''
        Find next hop to forward message such that it reaches radar covering the 
        given location
        @param loc: Middle point of region of interest
        @param radarRange: Maximum distance to be acceptable as in range
        @return: Next hop  
        '''
        if self.type == 'RADAR':
            if Distance.inRange(self.loc, loc, radarRange) == True: 
                return self.NID
        return self._FIB.getNodeInRange(loc, radarRange, 'RADAR') #Local radars?        
            
    
    def checkNode(self, NID):
        '''
        Check whether given node in routing table
        @param NID: NID to check
        @requires: Success or failure 
        '''
        return self._FIB.checkNode(NID)


    def getStat(self):
        '''
        Get statistics. Combine data from FIB
        @return: Stat
        '''
        result = self._FIB.getStat()
        return [self.numOvJoinRequests, result[0], result[1]]
