#
# Copyright 2007 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# GNU Radio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Radio; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#

from runtime_swig import top_block_swig, \
    top_block_wait_unlocked, top_block_run_unlocked, \
    top_block_start_unlocked, top_block_stop_unlocked

#import gnuradio.gr.gr_threading as _threading
import gr_threading as _threading

#
# There is no problem that can't be solved with an additional
# level of indirection...
#
# This kludge allows ^C to interrupt top_block.run and top_block.wait
#
# The problem that we are working around is that Python only services
# signals (e.g., KeyboardInterrupt) in its main thread.  If the main
# thread is blocked in our C++ version of wait, even though Python's
# SIGINT handler fires, and even though there may be other python
# threads running, no one will know.  Thus instead of directly waiting
# in the thread that calls wait (which is likely to be the Python main
# thread), we create a separate thread that does the blocking wait,
# and then use the thread that called wait to do a slow poll of an
# event queue.  That thread, which is executing "wait" below is
# interruptable, and if it sees a KeyboardInterrupt, executes a stop
# on the top_block, then goes back to waiting for it to complete.
# This ensures that the unlocked wait that was in progress (in the
# _top_block_waiter thread) can complete, release its mutex and back
# out.  If we don't do that, we are never able to clean up, and nasty
# things occur like leaving the USRP transmitter sending a carrier.
#
# See also top_block.wait (below), which uses this class to implement
# the interruptable wait.
#
class _top_block_waiter(_threading.Thread):
    def __init__(self, tb):
        _threading.Thread.__init__(self)
        self.setDaemon(1)
        self.tb = tb
        self.event = _threading.Event()
        self.start()

    def run(self):
        top_block_wait_unlocked(self.tb)
        self.event.set()

    def wait(self):
        try:
            while not self.event.isSet():
                self.event.wait(0.100)
        except KeyboardInterrupt:
            self.tb.stop()
            self.wait()


#
# This hack forces a 'has-a' relationship to look like an 'is-a' one.
#
# It allows Python classes to subclass this one, while passing through
# method calls to the C++ class shared pointer from SWIG.
#
# It also allows us to intercept method calls if needed.
#
# This allows the 'run_locked' methods, which are defined in gr_top_block.i,
# to release the Python global interpreter lock before calling the actual
# method in gr_top_block
#
class top_block(object):
    """
    Top-level hierarchical block representing a flow-graph.

    This is a python wrapper around the C++ implementation to allow
    python subclassing.
    """
    def __init__(self, name="top_block"):
	self._tb = top_block_swig(name)

    def __getattr__(self, name):
        if not hasattr(self, "_tb"):
            raise RuntimeError("top_block: invalid state--did you forget to call gr.top_block.__init__ in a derived class?")
	return getattr(self._tb, name)

    def start(self, max_noutput_items=10000000):
    	top_block_start_unlocked(self._tb, max_noutput_items)

    def stop(self):
    	top_block_stop_unlocked(self._tb)

    def run(self, max_noutput_items=10000000):
        self.start(max_noutput_items)
        self.wait()

    def wait(self):
        _top_block_waiter(self._tb).wait()


    # FIXME: these are duplicated from hier_block2.py; they should really be implemented
    # in the original C++ class (gr_hier_block2), then they would all be inherited here

    def connect(self, *points):
        '''connect requires one or more arguments that can be coerced to endpoints.
        If more than two arguments are provided, they are connected together successively.
        '''
        if len (points) < 1:
            raise ValueError, ("connect requires at least one endpoint; %d provided." % (len (points),))
	else:
	    if len(points) == 1:
		self._tb.primitive_connect(points[0].to_basic_block())
	    else:
		for i in range (1, len (points)):
        	    self._connect(points[i-1], points[i])

    def msg_connect(self, src, srcport, dst, dstport):
        self.primitive_msg_connect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);
    
    def msg_disconnect(self, src, srcport, dst, dstport):
        self.primitive_msg_disconnect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);

    def _connect(self, src, dst):
        (src_block, src_port) = self._coerce_endpoint(src)
        (dst_block, dst_port) = self._coerce_endpoint(dst)
        self._tb.primitive_connect(src_block.to_basic_block(), src_port,
                                   dst_block.to_basic_block(), dst_port)

    def _coerce_endpoint(self, endp):
        if hasattr(endp, 'to_basic_block'):
            return (endp, 0)
        else:
            if hasattr(endp, "__getitem__") and len(endp) == 2:
                return endp # Assume user put (block, port)
            else:
                raise ValueError("unable to coerce endpoint")

    def disconnect(self, *points):
        '''disconnect requires one or more arguments that can be coerced to endpoints.
        If more than two arguments are provided, they are disconnected successively.
        '''
        if len (points) < 1:
            raise ValueError, ("disconnect requires at least one endpoint; %d provided." % (len (points),))
        else:
            if len(points) == 1:
                self._tb.primitive_disconnect(points[0].to_basic_block())
            else:
                for i in range (1, len (points)):
                    self._disconnect(points[i-1], points[i])

    def _disconnect(self, src, dst):
        (src_block, src_port) = self._coerce_endpoint(src)
        (dst_block, dst_port) = self._coerce_endpoint(dst)
        self._tb.primitive_disconnect(src_block.to_basic_block(), src_port,
                                      dst_block.to_basic_block(), dst_port)

