class HashrouteSymmetric(BaseStrategy):
    """Hashroute with symmetric routing
    """

    def __init__(self, topology, log_dir, scenario_id, params=None):
        '''
        Constructor
        '''
        super(HashrouteSymmetric, self).__init__(topology, log_dir, scenario_id)
        # map id of content to node with cache responsibility
        self.cache_assignment = self.assign_caches(topology, self.cache_size, replicas=1)
        self.stretch_logger = StretchLogger(path.join(log_dir, 'RESULTS_%s_STRETCH.txt' % scenario_id))
    
    def handle_event(self, time, event):
        # get all required data
        receiver = event['receiver']
        content = event['content']
        log = event['log']
        source = self.content_location[content]
        cache = self.cache_assignment[self.content_hash(content)]
        # handle (and log if required) actual request
        if log: self.log_transfer(time, receiver, cache, PACKET_TYPE_INTEREST, content)
        has_content = self.caches[cache].has_content(content)
        if has_content and log:
            self.cache_logger.log_cache_info(time, EVENT_CACHE_HIT, content, receiver, cache, source)
            self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content)
            return
        else:
            if log:
                self.log_transfer(time, cache, source, PACKET_TYPE_INTEREST, content)
                self.cache_logger.log_cache_info(time, EVENT_SERVER_HIT, content, receiver, cache, source)
                self.log_transfer(time, source, cache, PACKET_TYPE_DATA, content) # pass via cache
                self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content)
                optimal_path_len = len(self.shortest_path[source][receiver]) - 1
                actual_path_len = len(self.shortest_path[source][cache]) + len(self.shortest_path[cache][receiver]) - 2 
                self.stretch_logger.log_stretch_info(time, receiver, source, content, optimal_path_len, actual_path_len)
            self.caches[cache].store(content) # insert content
            return
    
    def close(self):
        super(HashrouteSymmetric, self).close()
        self.stretch_logger.close()
class HashrouteHybridSymmMCast(BaseStrategy):
    """
    Hashroute implementation with hybrid delivery of Data packets.
    
    In this implementation, the edge router receiving a DATA packet decides
    whether to deliver the packet using Multicast or asymmetric hashroute
    based on the total cost for delivering the Data to both cache and receiver
    in terms of hops.
    """

    def __init__(self, topology, log_dir, scenario_id, params=None):
        '''
        Constructor
        '''
        super(HashrouteHybridSymmMCast, self).__init__(topology, log_dir, scenario_id)
        # map id of content to node with cache responsibility
        self.cache_assignment = self.assign_caches(topology, self.cache_size, replicas=1)
        self.stretch_logger = StretchLogger(path.join(log_dir, 'RESULTS_%s_STRETCH.txt' % scenario_id))
        self.scenario_id = scenario_id
        self.symm_count = 0
        self.mcast_count = 0

    
    def handle_event(self, time, event):
        # get all required data
        receiver = event['receiver']
        content = event['content']
        log = event['log']
        source = self.content_location[content]
        cache = self.cache_assignment[self.content_hash(content)]
        # handle (and log if required) actual request 
        if log: self.log_transfer(time, receiver, cache, PACKET_TYPE_INTEREST, content)
        has_content = self.caches[cache].has_content(content)
        if has_content and log:
            self.cache_logger.log_cache_info(time, EVENT_CACHE_HIT, content, receiver, cache, source)
            self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content)
        else:
            if log:
                self.log_transfer(time, cache, source, PACKET_TYPE_INTEREST, content)
                self.cache_logger.log_cache_info(time, EVENT_SERVER_HIT, content, receiver, cache, source)
            if cache in self.shortest_path[source][receiver]:
                self.caches[cache].store(content) # insert content
                if log:
                    self.log_transfer(time, source, cache, PACKET_TYPE_DATA, content) # pass via cache
                    self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content) # pass via cache
                    path_len = len(self.shortest_path[source][receiver])
                    self.stretch_logger.log_stretch_info(time, receiver, source, content, path_len, path_len)
                return
            else:
                #do multicast here
                cache_path = self.shortest_path[source][cache]
                recv_path = self.shortest_path[source][receiver]
                
                # find what is the node that has to fork the DATA packet
                for i in range(1, min([len(cache_path), len(recv_path)])):
                    if cache_path[i] != recv_path[i]:
                        fork_node = cache_path[i-1]
                        break
                else: fork_node = cache
                
                optimal_path_len   = len(self.shortest_path[source][receiver])
                symmetric_path_len = len(self.shortest_path[source][cache]) + \
                                     len(self.shortest_path[cache][receiver]) - 2
                multicast_path_len = len(self.shortest_path[source][fork_node]) + \
                                     len(self.shortest_path[fork_node][cache]) + \
                                     len(self.shortest_path[fork_node][receiver]) - 3
                # insert content in cache
                self.caches[cache].store(content)
                # decide if use symmetric or multicast depending on total costs
                if symmetric_path_len < multicast_path_len: # use symmetric delivery
                    if log:
                        self.symm_count += 1
                        self.log_transfer(time, source, cache, PACKET_TYPE_DATA, content)
                        self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content)
                        self.stretch_logger.log_stretch_info(time, receiver, source, content, optimal_path_len, symmetric_path_len)
                else: # use multicast delivery
                    if log:
                        self.mcast_count += 1
                        self.log_transfer(time, source, fork_node, PACKET_TYPE_DATA, content)
                        self.log_transfer(time, fork_node, receiver, PACKET_TYPE_DATA, content)
                        self.log_transfer(time, fork_node, cache, PACKET_TYPE_DATA, content)
                        self.stretch_logger.log_stretch_info(time, receiver, source, content, optimal_path_len, multicast_path_len)
             

    def close(self):
        super(HashrouteHybridSymmMCast, self).close()
        self.stretch_logger.close()
class HashrouteHybridStretch(BaseStrategy):
    """
    Hashroute implementation with hybrid delivery of Data packets.
    
    In this strategy, if there is a cache miss, when DATA packets returns in
    the domain, the packet is multicasted, one copy being sent to the
    authoritative cache and the other to the receiver. If the cache is on the
    path from source to receiver, this strategy behaves as a normal asymmetric
    HashRoute
    """

    def __init__(self, topology, log_dir, scenario_id, params=None):
        '''
        Constructor
        '''
        super(HashrouteHybridStretch, self).__init__(topology, log_dir, scenario_id)
        # map id of content to node with cache responsibility
        params = {'max_stretch': 0.2}
        self.cache_assignment = self.assign_caches(topology, self.cache_size, replicas=1)
        self.max_stretch = nx.diameter(topology) * params['max_stretch']
        self.stretch_logger = StretchLogger(path.join(log_dir, 'RESULTS_%s_STRETCH.txt' % scenario_id))

    
    def handle_event(self, time, event):
        # get all required data
        receiver = event['receiver']
        content = event['content']
        log = event['log']
        source = self.content_location[content]
        cache = self.cache_assignment[self.content_hash(content)]
        # handle (and log if required) actual request 
        if log: self.log_transfer(time, receiver, cache, PACKET_TYPE_INTEREST, content)
        has_content = self.caches[cache].has_content(content)
        if has_content and log:
            self.cache_logger.log_cache_info(time, EVENT_CACHE_HIT, content, receiver, cache, source)
            self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content)
        else:
            if log:
                self.log_transfer(time, cache, source, PACKET_TYPE_INTEREST, content)
                self.cache_logger.log_cache_info(time, EVENT_SERVER_HIT, content, receiver, cache, source)
            if cache in self.shortest_path[source][receiver]:
                self.caches[cache].store(content) # insert content
                if log:
                    self.log_transfer(time, source, cache, PACKET_TYPE_DATA, content) # pass via cache
                    self.log_transfer(time, cache, receiver, PACKET_TYPE_DATA, content) # pass via cache
                    path_len = len(self.shortest_path[source][receiver])
                    self.stretch_logger.log_stretch_info(time, receiver, source, content, path_len, path_len)
                return
            else:
                #do multicast here
                cache_path = self.shortest_path[source][cache]
                recv_path = self.shortest_path[source][receiver]
                
                # find what is the node that has to fork the DATA packet
                for i in range(1, min([len(cache_path), len(recv_path)])):
                    if cache_path[i] != recv_path[i]:
                        fork_node = cache_path[i-1]
                        break
                else: fork_node = cache
                optimal_path_len = len(self.shortest_path[source][receiver]) - 1
                actual_path_len = len(self.shortest_path[source][fork_node]) + \
                                  len(self.shortest_path[fork_node][cache]) + \
                                  len(self.shortest_path[fork_node][receiver]) - 3
                # multicast to cache only if stretch is under threshold
                go_to_cache = ((actual_path_len - optimal_path_len) < self.max_stretch)
                if go_to_cache:
                    self.caches[cache].store(content)
                if log:
                    self.log_transfer(time, source, fork_node, PACKET_TYPE_DATA, content)
                    self.log_transfer(time, fork_node, receiver, PACKET_TYPE_DATA, content)
                    if go_to_cache:
                        self.log_transfer(time, fork_node, cache, PACKET_TYPE_DATA, content)
                        self.stretch_logger.log_stretch_info(time, receiver, source, content, optimal_path_len, actual_path_len)

    def close(self):
        super(HashrouteHybridStretch, self).close()
        self.stretch_logger.close()