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

import Distance as Distance


class FIB(object):
    '''
    Forward Information Base (FIB) class
    '''


    def __init__(self, maxSize):
        '''
        Constructor
        @param maxSize: maximum size 
        '''
        self._maxSize = maxSize
        self._FIB = {} #Routing table for CCN
        self.numFIBUpdates = 0

        
    def put(self, NID, loc, zone, appType, nextHop, inOverlay = True):
        '''
        Add to FIB
        @param NID: Node ID
        @param loc: Location of node as [x, y]
        @param zone: Zone assigned to node as [x1, y1, x2, y2]
        @param appType: Application type 
        @param nextHop: nextHop node
        @param inOverlay: Node is an overlay node (can route.forward) 
        @return: False - unable to add, True - success   
        '''       
        if NID in self._FIB: #Update
            self._FIB[NID] = [loc, zone, appType, nextHop, inOverlay] 
        else: #Add new one
            if (self._maxSize > 0) and (len(self._FIB) + 1 > self._maxSize):
                return False
            self._FIB[NID] = [loc, zone, appType, nextHop, inOverlay]
        self.numFIBUpdates += 1
        #clean up links to nodes that are not adjacent
        directApps = []
        for i in self._FIB:
            if i == self._FIB[i][3]: #source & next hop same 
                if self._FIB[i][2] not in directApps: 
                    directApps.append(self._FIB[i][2])
        toRem = []
        for i in self._FIB:
            if i == self._FIB[i][3]: continue #Skip as direct neighbors
            if self._FIB[i][2] not in directApps: continue #Needed node type
            toRem.append(i)
        for j in toRem: del self._FIB[j] #Remove unwanted ones             
        return True

    
    def remove(self, NID):
        '''
        Remove from FIB
        @param NID: Node ID
        @return: False - Failed, True - Success 
        '''
        if NID in self._FIB: 
            del self._FIB[NID]
            return True
        else: return False


    def getNextHop(self, loc):
        '''
        Find next hop to forward message.
        @param loc: Location to route to
        @return: next hop, -1 if not able to find         
        '''
        lowestDist = 999999.9
        idx = -1
        for i in self._FIB:
            if len(self._FIB[i][1]) == 0: continue #Not a zone
            if Distance.inZone(self._FIB[i][1], loc) == True: #In neighbors zone
                return self._FIB[i][3]
            dist = Distance.distZone(loc, self._FIB[i][1])
            if (dist < lowestDist):
                lowestDist = dist 
                idx = i
        return idx


    def getLocalNextHop(self, loc, appType = ''):
        '''
        Get closet nodes among entries in FIB
        @param loc: Location
        @param appType: Application type
        @requires: [Node ID, next hop node, distance]  
        '''
        lowestDist = 999999.9
        idx = -1
        idxNextHop = -1   
        for i in self._FIB:
            if appType == '' or self._FIB[i][2] == appType:
                dist = Distance.dist(loc, self._FIB[i][0])
                if (dist < lowestDist):
                    lowestDist = dist 
                    idx = i
                    idxNextHop = self._FIB[i][3]
        return [idx, idxNextHop, lowestDist]


    def getNodeInRange(self, loc, nodeRange, nodeType):
        '''
        Find any 1 node in my FIB that covers the given location
        @param loc: Middle point of region of interest
        @param nodeRange: Maximum distance to be acceptable as in range of node
        @param nodeType: Node type 
        @return: Next hop or -1 if not  
        '''
        for i in self._FIB: #If any of radars in my FIB within coverage
            if self._FIB[i][2] != nodeType: continue
            if Distance.inRange(loc, self._FIB[i][0], nodeRange) == True:
                return i
        return -1

   
    def nbrsToUpdate(self, myZone, newZone):
        '''
        Determine which neighbors to remove, add, move, & update based on newly
        formed zone.
        @param myZone: Zone of the node having the FIB 
        @param newZone: New neighbor's zone
        @return: List of new neighbors to add, old neighbors to remove, to move,
        & update
        '''
        nbrsToAdd = []
        nbrsToRemove = []
        nbrsToMove = []
        nbrsToUpdate = []
        for i in self._FIB:            
            if len(self._FIB[i][1]) == 0: #Not a zone. Move them, if required
                if Distance.inZone(newZone, self._FIB[i][0]) == True:
                    nbrsToMove.append(i)
                    nbrsToRemove.append(i)
                else:
                    if i == self._FIB[i][3]: #Directly connected
                        nbrsToUpdate.append(i)#Still in my zone. Inform my new zone
                continue
            #Overlap in 1 dimension & abut (adjacent) in the other
            if self.isNbrZone(newZone, self._FIB[i][1]) == True:
                nbrsToAdd.append([self._FIB[i][0], i, self._FIB[i][1], \
                                      self._FIB[i][2]])
            #Remove nodes that are no longer my neighbors 
            if self.isNbrZone(myZone, self._FIB[i][1]) == True: 
                nbrsToUpdate.append(i) #Inform my new zone  
            else: nbrsToRemove.append(i) #No longer a neighbor. Remove
        
        for j in nbrsToRemove: del self._FIB[j]  #Remove old neighbors
        return [nbrsToAdd, nbrsToRemove, nbrsToMove, nbrsToUpdate]


    def routingTableSize(self):
        '''
        Get routing table size
        '''
        return len(self._FIB)
      

    def getNbrZones(self):
        '''
        Get list of neighboring zones
        @return: List of neighbor zones
        '''
        nbrs = []
        for i in self._FIB: #Is a zone & direct neighbor
            if len(self._FIB[i][1]) > 0 and i == self._FIB[i][3]: nbrs.append(i)
        return nbrs


    def getAppList(self):
        '''
        Get list of applications in routing table
        @return: [list of apps]
        '''
        apps = []
        for i in self._FIB:
            if self._FIB[i][2] not in apps: apps.append(self._FIB[i][2])
        return apps


    def getAppsLoc(self, appTypes, direct = False):
        '''
        Find nodes of given application type & their locations. If there is a 
        direct neighbor also indicate that 
        @param appTypes: List of application types to get info about
        @param diret: Need only directly connected nodes  
        @return: [[NID of node, location node, direct neighbors]] 
        '''
        apps = []
        for i in self._FIB:
            if self._FIB[i][2] not in appTypes: continue #Not of required type
            #direct neighbor
            if i == self._FIB[i][3]: apps.append([i, self._FIB[i][0], True])  
            elif direct == False: apps.append([i, self._FIB[i][0], False])
        return apps


    def getAppNodes(self, skipList, direct = False):
        '''
        Get list of applications that are not of the given type
        @param skipList: Types of applications to skip
        @param diret: Need only directly connected nodes 
        @return: List of apps [NID, loc, zone, type] 
        '''
        apps = []
        for i in self._FIB:
            if self._FIB[i][2] in skipList: continue
            if i == self._FIB[i][3]: 
                apps.append([i, self._FIB[i][0], self._FIB[i][1], self._FIB[i][2]])
            elif direct == False:
                apps.append([i, self._FIB[i][0], self._FIB[i][1], self._FIB[i][2]])
        return apps


    def checkNode(self, NID):
        '''
        Check whether given node in routing table
        @param NID: NID to check
        @requires: Success or failure 
        '''
        if NID in self._FIB: return True
        else: return False


    def isNbrZone(self, zoneA, zoneB):
        '''
        Are given zones neighbors
        @param zoneA: First zone to compare
        @param zoneB: Second zone to compare
        @return: True or False
        '''
        if len(zoneA) == 0 or len(zoneB) == 0: return False
        #Overlap in 1 dimension & abut (adjacent) in the other
        if (zoneA[0].x <= zoneB[0].x and zoneA[1].x >= zoneB[1].x)\
            or (zoneA[0].x >= zoneB[0].x and zoneA[1].x <= zoneB[1].x):
            if zoneA[0].y == zoneB[0].y or zoneA[1].y == zoneB[1].y\
                or zoneA[0].y == zoneB[1].y or zoneA[1].y == zoneB[0].y:
                return True
        if (zoneA[0].y <= zoneB[0].y and zoneA[1].y >= zoneB[1].y)\
            or (zoneA[0].y >= zoneB[0].y and zoneA[1].y <= zoneB[1].y):
            if zoneA[0].x == zoneB[0].x or zoneA[1].x == zoneB[1].x\
                or zoneA[0].x == zoneB[1].x or zoneA[1].x == zoneB[0].x:
                return True
        return False

    
    def getStat(self):
        '''
        Get statistics
        @return: Stat        
        '''
        return [len(self._FIB), self.numFIBUpdates]
