from collections import Iterable

from simpy import Store

from .buffer import Buffer
from .core import TickProcess

def NodeFactory(router, **kwargs):
    def factory(network, nid):
        return Node(network, nid, router=router, **kwargs)
    return factory


class Node(TickProcess):
    ''''''
    def __init__(self, network, nid,
                 buffer_size=None, tick_time=1, router=None):
        ''''''
        super().__init__(tick_time)
        self.env = network.env
        self.network = network
        self.id = nid

        self.bandwidth = float('inf')

        self.buffer = Buffer(self.env, capacity=buffer_size)

        if router is None:
            router = routers['direct']
        self.router = router(self)

        self.start(self.env)

        self.transfer_queue = Store(self.env)

    def route_packet(self, packet):
        '''
        Applies the routing heuristic to packet and queues packet for
        transfer if requested

        Returns:
            True - if the packet should be deleted from the buffer
            False - otherwise
        '''

        # check if packet has expired
        if packet.expired(self.env.now):
            return True

        # ask the router what to do
        targets, reason, delete = self.router(packet)

        # check if there are targets to send to
        if targets is None:
            return False

        # allow for targets to be iterable or single item
        if not isinstance(targets, Iterable):
            targets = [targets]

        # add the packet to the transfer queue
        self.transfer_queue.put((packet, targets, reason))

        return delete

    def route_packets(self):
        '''
        Applies the routing heuristic to all packets in the buffer and
        queues transfers if requested. Additionally, removes packets
        from the buffer if the packet has exipred or if the router
        requests the packet to be removed.
        '''

        packets_to_delete = []

        for packet in self.buffer:
            delete = self.route_packet(packet)
            if delete:
                packets_to_delete.append(packet)

        for packet in packets_to_delete:
            self.buffer.remove(packet)

    def process(self, **kwargs):
        ''''''
        while True:
            packet, targets, reason = yield self.transfer_queue.get()

            failed = False

            # simulate transfer delay of packet
            delay = packet.size / self.bandwidth
            yield self.env.timeout(delay)

            success = []
            # simulates broadcast
            for target in targets:
                # try sending packet
                if self.send(target, packet):
                    success.append(target)
                    self.router.on_send_success(target, packet)
                else:
                    failed = True
                    self.router.on_send_failure(target, packet)

            packet.send(self, success, reason=reason)

            if failed:
                delete = self.route_packet(packet)
                if delete:
                    self.buffer.remove(packet)

    def send(self, target, packet):
        '''
        Instantly sends a packet to a target if they are connected.
        Returns:
            True - if successful
            False - otherwise
        '''
        if self.connected_to(target):
            return target.recv(packet)
        else:
            return False

    def recv(self, packet):
        '''
        Tells the node to recieve a packet.
        Returns:
            True - if the node was able to recieved the packet.
            False - otherwise, triggered by buffer being full.
        '''
        if packet.destination == self:
            packet.recv()
            return True
        else:
            return self.buffer.add(packet)

    @property
    def community(self):
        return self.network.community[self]

    def connected_to(self, other):
        return self.network[self][other]['state']

    @property
    def links(self):
        '''
        Returns a list of connected links.
        '''
        return {
            met: data
            for met, data in self.network[self].items()
            if self.connected_to(met)
        }

    def __repr__(self):
        return 'Node(id={})'.format(self.id)