# (c) Copyright 2010-2011, Synapse Wireless, Inc.


import pprint
import timeit
import binascii
import logging
import random

from snapconnect import snap


MCAST_GROUP = 0x100


class PollingFrameworkHelper(object):
    VERSION = "1.1.0"

    def __init__(self):
        self.current_sequence_number = random.randint(0, 0xFFFF)
        self.node_addresses = []
        self.node_data = {}
        self.log = logging.getLogger(__name__)
        self.start_time = 0
        self.get_data_all_start_time = 0
        self.snap_instance = None
        self._mcast_group = MCAST_GROUP
        self._ping_callback_func = None
        self._ping_deferred = None
        self.ping_wait_time = 5.0
        self._get_data_callback = None
        self._get_data_wait_time = 5.0
        self._get_data_deferred = None
        self._get_data_all_callback = None
        self._get_data_all_index = 0
        self._get_data_all_results = {}
        self._get_data_all_func_name = 'random'
        self._expected_responses = None

    def get_data(self, remote_address, remote_function_name, callback_function):
        """
        Executes the provided function on the remote node retrieving it's answer

        remote_addresss : The SNAP address of the remote node
        remote_function_name : The SNAPpy function name to call and retrieve
                               data from
        callback_function : A function to call when the node has responded
        """
        if self.snap_instance is None:
            self.log.error("No SNAP instance was given")
            return
        assert isinstance(self.snap_instance, snap.Snap)
        assert callable(callback_function)

        self.current_sequence_number += 1
        self._get_data_callback = callback_function
        self.snap_instance.mcast_rpc(self._mcast_group, 1, 'get_data', self.current_sequence_number, remote_address, remote_function_name)
        self._get_data_deferred = self.snap_instance.scheduler.schedule(self._get_data_wait_time, self._get_data_timeout)
        self.start_time = timeit.default_timer()

    def get_data_all_nodes(self, remote_function_name, callback_function):
        """
        Executes the provided function on all nodes retreived via a ping 
        and retrieves their answeres

        remote_function_name : The SNAPpy function name to call and retrieve
                               data from
        callback_function : A function to call when the nodes have responded
        """
        if self.snap_instance is None:
            self.log.error("No SNAP instance was given")
            return
        if not self.node_addresses:
            self.log.error("No SNAP nodes were discovered")
            return
        assert isinstance(self.snap_instance, snap.Snap)
        assert callable(callback_function)

        self._get_data_all_callback = callback_function
        self._get_data_all_index = 0
        self.get_data(self.node_addresses[self._get_data_all_index], remote_function_name, self._on_get_data_all_request)
        self._get_data_all_func_name = remote_function_name
        self.get_data_all_start_time = timeit.default_timer()

    def get_data_result(self, sequence_number, responding_address, response):
        """Called with the result of the get_data request"""
        if sequence_number == self.current_sequence_number:
        #if responding_address == self.node_addresses[self._get_data_all_index]:
            if callable(self._get_data_callback):
                self.snap_instance.scheduler.schedule(0, self._get_data_callback, responding_address, response)
            self._get_data_deferred.Stop()
            self._get_data_callback = None

    def _get_data_timeout(self):
        if callable(self._get_data_callback):
            self.snap_instance.scheduler.schedule(0, self._get_data_callback, None, None)
        self._get_data_callback = None

    def _on_get_data_all_request(self, address, response):
        if address is not None:
            self._get_data_all_results[address] = response
            self.log.info("Node %s responded with %s" % (binascii.hexlify(address), str(response)))

        self._get_data_all_index += 1
        if self._get_data_all_index >= len(self.node_addresses):
            if callable(self._get_data_all_callback):
                self._get_data_all_callback(self._get_data_all_results)
            return
        addr = self.node_addresses[self._get_data_all_index]
        self.get_data(addr, self._get_data_all_func_name, self._on_get_data_all_request)

    def on_get_data(self, sequence_number, addr, func_name):
        if sequence_number == self.current_sequence_number and self._get_data_deferred:
            # Restart wait timeout
            self._get_data_deferred.Stop()
            self._get_data_deferred = self.snap_instance.scheduler.schedule(self._get_data_wait_time, self._get_data_timeout)

    def on_rpA(self, sequence_number, addresses, node_sequence, data_func):
        if self._ping_deferred:
            self._ping_deferred.Stop()
            self._ping_deferred = self.snap_instance.scheduler.schedule(self.ping_wait_time, self._ping_timeout)

    #def on_rpc_sent(self, sent_id, success):
        #assert success
        #pass

    def _ping_timeout(self):
        cb = self._ping_callback_func
        self._ping_callback_func = None
        if callable(cb):
            cb()

    def start_ping(self, node_function, callback_function, expected_responses=None):
        """
        Initiates a ping to all nodes in your SNAPpy network running the 
        polling framework

        node_function : A function to call on the node
        callback_function :  A function to call when the ping has completed
        expected_responses : (Optional) The number of nodes expected to respond
            If this number of nodes responds we don't wait for the timeout
        """
        if self.snap_instance is None:
            self.log.error("No SNAP instance was given")
            return
        assert isinstance(self.snap_instance, snap.Snap)
        assert callable(callback_function)

        del self.node_addresses[:]
        self.node_data.clear()
        self._expected_responses = expected_responses
        self.current_sequence_number += 1
        if self.current_sequence_number > 0xFFFF:
            self.current_sequence_number = 1
        self._ping_callback_func = callback_function
        #self.snap_instance.mcast_rpc(self._mcast_group, 10, 'clear_ping')
        self.snap_instance.mcast_rpc(self._mcast_group, 1, 'proxy_ping', self.current_sequence_number, node_function)
        self._ping_deferred = self.snap_instance.scheduler.schedule(self.ping_wait_time, self._ping_timeout)
        self.start_time = timeit.default_timer()

    def tell_ping(self, sequence_number, responding_addresses, with_data):
        """Called with network address of nodes responding to a ping request"""
        if with_data:
            while responding_addresses:
                responding_addr = responding_addresses[:3]
                #print "responding_addr:", binascii.hexlify(responding_addr), "len:", binascii.hexlify(responding_addresses[3])
                data_len = ord(responding_addresses[3])
                data = responding_addresses[4:4+data_len]
                if responding_addr not in self.node_addresses:
                    self.node_addresses.append(responding_addr)
                    self.node_data[responding_addr] = data
                    self.log.info("Node %s responded with %s" % (binascii.hexlify(responding_addr), pprint.pformat(data)))
                else:
                    self.log.debug("Already saw %s" % (binascii.hexlify(responding_addr)))
                responding_addresses = responding_addresses[4+data_len:]
        else:
            for addr in [responding_addresses[i:i+3] for i in range(0, len(responding_addresses), 3)]:
                if addr not in self.node_addresses:
                    self.node_addresses.append(addr)
                    self.log.info("Node %s responded" % (binascii.hexlify(addr)))
                else:
                    self.log.debug("Already saw %s" % (binascii.hexlify(addr)))

        if self._ping_deferred:
            self._ping_deferred.Stop()
            if self._expected_responses is not None and len(self.node_addresses) == self._expected_responses:
                self._ping_timeout()
            else:
                self._ping_deferred = self.snap_instance.scheduler.schedule(self.ping_wait_time, self._ping_timeout)
