'''
Created on Sep 2, 2012
@version: 0.1
@author: Dilum Bandara
@email: dilumb@engr.colostate.edu

   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 random
import Config as Config
import Distance as Distance
import PeerManager as PM
from FIB import FIB
from OverlayJoinRequestMsg import OverlayJoinRequestMsg 
from OverlayJoinReplyMsg import OverlayJoinReplyMsg
from OverlayUpdateMsg import OverlayUpdateMsg
from Zone import Zone


class CANNode(object):
    '''
    CAN node class
    Join overlay, maintain connections, support addition of new nodes. Removal 
    not supported. Local routing table is the CCN FIB
    Support multi-dimensional torus & routing from one edge to the other like a
    real d-torus.
    '''

    def __init__(self, NID, loc, maxSize):
        '''
        Constructor
        @param NID: Node ID
        @param loc: Location
        @param maxSize: Maximum size of FIB 
        '''
        self.NID = NID
        self.loc = loc       #Location in x, y, z plane (z is optional)
        self._FIB = FIB(maxSize)
        self.pseudoLoc = loc #Location in x, y, z, w, ... plane. useful if d > 2
        #Most nodes have only geographic locations, i,e., (x, y) or (x, y, z)
        #If dimensions of torus is higher than this set locations of other 
        #dimensions to a random point
        for i in range(len(loc), Config.NUM_DIMENSIONS):
            rndLoc = random.random() * Config.SIZE_TORUS[i]
            self.pseudoLoc.append(round(rndLoc, 4))
        self.zone = None
        self._dimToSplit = 0 #Split zone at middle of 1st coordinate
        self.numOvJoinRequests = 0


    def __del__(self):
        '''
        Destructor
        '''
        self._FIB = None


    def joinOverlay(self, inOveraly):
        '''
        Join CAN overlay
        @param inOveraly: Is node a fully functional overlay node 
        @return: Success (True) or failure (False)
        '''
        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.pendingNodes.append(self.NID) 
                return True
            self.zone = Zone([0] * Config.NUM_DIMENSIONS, \
                             copy.deepcopy(Config.SIZE_TORUS))
            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.pseudoLoc, inOveraly), node)
        assert(result), 'Unable to send msg --> ' + str(self.NID) + ' ' + \
                                                                    str(node)
        return result


    def recMsg(self, msg, source):
        '''
        Receive message from overlay neighbor
        @param msg: Overlay message
        @param source: Source node 
        '''
        if msg.msgType == 'Join': self._handleJoin(msg)
        elif msg.msgType == 'JoinRply': self._handleJoinReply(msg, source)            
        elif msg.msgType == 'RemNbr': #Remove old neighbor
            if self._FIB.remove(msg.NID) == False:
                print 'CANNode::recMsg Unable to remove neighbor at', self.NID
        elif msg.msgType == 'AddNbr':
            if self.NID != msg.NID: 
                if self._FIB.put(msg.NID, msg.loc, msg.zone) == False:
                    print 'CANNode::recMsg Unable to add neighbor at', self.NID
        elif msg.msgType == 'Rejoin':
            if self.zone is not None: 
                print 'CAN::recMsg Asked to rejoin while being zone controller'\
                 , self.NID, msg.NID, source
            else:
                PM.EVENTS.addEvent(PM.EVENTS.now + Config.JOIN_DELAY/2, \
                                   'REJOIN', self.NID, []) 
        else: 
            assert(False), '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 == 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:
                    newMsg = OverlayJoinReplyMsg(None, self._dimToSplit, \
                        [[self.NID, self.pseudoLoc, self.zone]])
                    assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, msg.NID)), \
                        'Unable to forward message at ' + str(self.NID)
                    if self._FIB.put(msg.NID, msg.loc, None) == False:
                        print 'CANNode::recMsg FIB full', self.NID
                    return
            
            newZone = self._splitZone(msg.loc)
            #Zone was not split or this's my 1st neighbor
            nbrs = []
            if (newZone is None) or (self._FIB.routingTableSize() == 0): 
                #Send reply directly
                newMsg = OverlayJoinReplyMsg(newZone, self._dimToSplit, \
                    [[self.NID, self.pseudoLoc, self.zone]])
                assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, msg.NID)), \
                            'Unable to forward message at ' + str(self.NID)
            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.NID, self.pseudoLoc, self.zone])
                newMsg = OverlayJoinReplyMsg(newZone, self._dimToSplit, nbrs[0])
                assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, msg.NID)), \
                    'Unable to send message at ' + str(self.NID)
                
                for i in nbrs[1]: #neighbors to remove
                    newMsg = OverlayUpdateMsg('RemNbr', self.NID, \
                                              self.pseudoLoc, self.zone)
                    assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, i)), \
                                'Unable to send message at ' + str(self.NID)
                
                for i in nbrs[2]: #neighbors to move & rejoin
                    newMsg = OverlayUpdateMsg('Rejoin', self.NID, None, None)
                    assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, i)), \
                                'Unable to send message at ' + str(self.NID)
                
                for i in nbrs[3]: #neighbors to Update
                    newMsg = OverlayUpdateMsg('AddNbr', self.NID, \
                                              self.pseudoLoc, self.zone)
                    assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, i)), \
                                'Unable to send message at ' + str(self.NID)
            #Add to my routing table
            if self.NID != msg.NID:
                if self._FIB.put(msg.NID, msg.loc, newZone) == False:
                    print 'CANNode::recMsg FIB full', self.NID
        else: #Not in my zone or I'm not a zone owner. Send to neighboring zone             
            assert(PM.UNDERLAY[self.NID].fwdMsg(msg, nextHop)), \
                'Unable to forward message at ' + str(self.NID) + '\t' + \
                str(nextHop) + '\t' + str(self._FIB._FIB) + '\t'+ str(self.zone)


    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._dimToSplit = msg._dimToSplit
        if self.zone is None: #Not a zone
            assert(len(msg.nbrs) == 1), \
                    'Many routing entries given to none zone controller'
            if self.NID != msg.nbrs[0][0]:
                if self._FIB.put(msg.nbrs[0][0], msg.nbrs[0][1], \
                                                    msg.nbrs[0][2]) == False:
                    print 'CANNode::_handleJoinReply FIB full', self.NID
        else: #Zone controller
            PM.BOOTSTRAP.append(self.NID) #Add as a bootstrap

            for i in range(len(msg.nbrs)): #Add new neighbors
                #Is adjacent zone?
                if self._FIB.isNbrZone(msg.nbrs[i][2], self.zone) == True:
                    if self.NID != msg.nbrs[i][0]: #Add if not me
                        if self._FIB.put(msg.nbrs[i][0], msg.nbrs[i][1], \
                                                msg.nbrs[i][2])== False:
                            print 'CANNode::_handleJoinReply FIB full', self.NID
                        else:#Inform each direct neighbor about me
                            if msg.nbrs[i][0] != source:
                                newMsg = OverlayUpdateMsg('AddNbr', self.NID,\
                                      self.pseudoLoc, self.zone)
                                assert(PM.UNDERLAY[self.NID].fwdMsg(newMsg, \
                                    msg.nbrs[i][0])), 'Unable to send msg at'\
                                        + str(self.NID)


    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: Modied areas
        '''
        result = []
        for i in areas:
            overlap = [False] * Config.NUM_DIMENSIONS 
            #Must overlap in all dimensions
            for j in range(Config.NUM_DIMENSIONS):
                if (self.zone.min[j] <= i[1][j] and \
                                self.zone.max[j] >= i[2][j]) or \
                                (self.zone.min[j] >= i[1][j] and \
                                self.zone.max[j] <= i[2][j]):
                    overlap[j] = True
            if False in overlap: result.append(i) #Not overlapping
        return result


    def _splitZone(self, newLoc):
        '''
        Try to split zone.
        @param newLoc: Location of new node
        @return: new zone if split, else None
        '''
        zoneMin = []
        zoneMax = []
    
        p = (self.zone.min[self._dimToSplit] + \
                  self.zone.max[self._dimToSplit]) / 2.0 #new mid point
        #If 2 nodes are in 2 slides, then split. Otherwise don't split
        if (newLoc[self._dimToSplit] > p and self.pseudoLoc[self._dimToSplit] \
                                                                        <= p):
            for i in range(Config.NUM_DIMENSIONS):
                if i == self._dimToSplit: zoneMin.append(p)
                else: zoneMin.append(self.zone.min[i])
                zoneMax.append(self.zone.max[i])
            self.zone.max[self._dimToSplit] = p
        elif (newLoc[self._dimToSplit] <= p and \
                                        self.pseudoLoc[self._dimToSplit] > p):
            for i in range(Config.NUM_DIMENSIONS):
                if i == self._dimToSplit: zoneMax.append(p)
                else: zoneMax.append(self.zone.max[i])
                zoneMin.append(self.zone.min[i])
            self.zone.min[self._dimToSplit] = p
        else: return None #New node same side as current node    
        
        #Zone was split. Axis to use during next split.
        self._dimToSplit = (self._dimToSplit + 1) % Config.NUM_DIMENSIONS
        return Zone(zoneMin, zoneMax)


    def getNextHop(self, loc):        
        '''
        Find next hop to forward message using greedy forwarding
        @param loc: Location to route to
        @return: [Next hop or -1 if not able to find a next hop] 
        ''' 
        #Reached required zone
        if (self.zone is not None) and (Distance.inZone(loc, self.zone) ==True):
            return self.NID
        else: return self._FIB.getNextHop(loc)
        
   
    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
        '''
        return [self.numOvJoinRequests] + self._FIB.getStat()
