# -*- coding: iso-8859-1 -*-
"""Class for periodic activities

Copyright © 2005-2012, Anders Andersen, University of Tromsų, Norway.
See http://www.cs.uit.no/~aa/dist/tools/noop/COPYING (../COPYING) for
details.


Periodic activities
===================

The activity class provides a thread that can run as a periodic
activity (wakes up and does a function call every given period).  The
activity is created with the `Activity` class where the constructor is
provided the period (integer or float), the function, optionally
arguments or an argument generator for the function, and an optionally
period type.  The period type can either be `StrictPeriodic`
(default), `StartOfCall` or `EndOfCall`.  `StrictPeriodic` will try to
adjust for any drift (this means that after N periods with period
value t, the next function call will begin at N x t).  `StartOfCall`
does not care about history, but tries to start the next function call
as close as possible to the length of one period later than the
previous method call started.  The `EndOfCall` ignores the time used
by the function call and starts the next function call as close as
possible to the length of one period after the previous method call
ended.

Usage
-----

An actvity is created using the `Activity` class.  If we want to
create a `StrictPeriodic` activity calling the function `f` every 3rd
second (with no arguments) we call the constructor in this way:

>>> a = Activity(3.0, f)

The activity does not start until we explicit starts it:

>>> a.start()

We can later pause the activity, change the period, and restart it:

>>> a.pause()
>>> a.period = 2.4
>>> a.start()

Finally we can stop the activity (a stopped activity can not be restarted):

>>> a.stop()

If the running activity is performing its function the
`start`, `pause` and `stop` methods are
blocked until this function finnish (or fails).  (A side effect of
this is that you can use `start` on a running activity to
block your control thread until the current iteration of performing
the function is finnished.  It is not obvious that this can be used
for anything useful, because you do not know which iteration you are
waiting for or even if the first function call has started yet, and
the next iteration might start before you are able to do anything.)

It is also possible to wait for an activity to stop or pause.  An
activity can be stopped by an exception (see Error handling below),
but the exception can be intentional (like the function raising an
exception when is has no more to do or the argument generator raising
an exception when no more arguments for the function can be produced).
The `wait` method will block if the activity is running, and
it will continue to be blocked until the activity is stopped (or
paused).

As seen above, the period can be changed by giving a new value to the
`period` attribute of the `Activity` object.  You can also change the
function (the `function` attribute), the arguments (the `args` and
`kw` attributes), the agrument generator (the `arggen` attribute), and
the period type (the `type` attribute).  This should only be done
before the activity is started or when the activity is paused.  The
`arggen` attribute has precedence over the `args` and `kw` attributes.
This means that if the `arggen` attribute has a value different from
`None`, the generator given in `arggen` will be used to get the
arguments to the function and any values of `args` and `kw` will be
ignored.

We can also create more complex activities.  Activity `b` runs the
function `g` every 1.3 second in an `StartOfCall` fassion.  Every call
to `g` is provided the arguments `(42, 6.48074)`:

>>> b = Activity(1.3, g, args=(42, 6.48074), type=StartOfCall)

Activity `c` runs the function `h` every 0.23 second in an `EndOfCall`
fassion.  Every call to `h` gets its argument from the generator `e`
(in Python, a generator is created by calling a function that contains
a `yield` statement):

>>> c = Activity(0.23, h, arggen=e, type=EndOfCall)

The `Activity` class only supports generators for dynamic arguments to
the function, and the generator has to return a two-tuple containing
an argument tuple and a keyword dictionary (named arguments).  This
might not fit your usage, but it is simple to wrap any function call
as a generator, or create your own generator.  A generator provides
the `__next__` operation, and if you create a class with that method
(no arguments) and make an instance of it, you have a simple
generator.  The following is an example of a such a generator (that is
not a Python generator) that returns a line from a text file at every
call to `next`:

>>> class NextLine:
>>>     def __init__(self, filename):
>>>         self.f = open(filename)
>>>     def __next__(self):
>>>         line = self.f.readline()
>>>         if line: return ((line,), {})
>>>         else: raise StopIteration

The activity `c` can now be created with this generator:

>>> c = Activity(0.23, h, arggen=NextLine('doc.txt'), type=EndOfCall)

Error handling
--------------

Errors (exceptions) in a running activity will stop the activity, and
information about why it stopped will be saved in the attributes
`exctype`, `excmsg` and `excinfo`.  `exctype` is the exception that
stopped the activity or `None` if no errors have occured.  `excmsg` is
the message given when the exception was raised.  `excinfo` is a
message generated by the `Activity` object when the error occured.
This message says when the error occured.  It is two possible
situations when such errors occur.  The first is when the argument
generator was called (see module attribute `_arggen_error` and
`_arggen_stopped`), and the second is when the function was called
(see module attribute `_function_error`).

Activity is a thread
--------------------

Since an activity object is a thread object, you can also use the
thread methods provided by the Thread class in the standard Python
threading module.  This includes methods like `isAlive` and `join`.
See the Python Library Reference for more details.

More examples
-------------

More examples are included in the 'noop/core/examples' directory of
the noop distribution.

"""
__docformat__ = "restructuredtext"


# CVS/RCS information

_file     = r"$RCSfile: activity.py,v $"
_revision = r"$Revision: 2.7 $"
_date     = r"$Date: 2012-10-08 19:49:26 $"
_state    = r"$State: Exp $"
_author   = r"$Author: aa $"

_log      = r"""

$Log: activity.py,v $
Revision 2.7  2012-10-08 19:49:26  aa
Added copyright notice.

Revision 2.6  2011-09-18 18:19:39  aa
Cleaned up signatures.

Revision 2.5  2010-10-05 14:49:42  aa
Updated to match latest version of signature (replaced `any` with
`whatever` and removed the `exceptions` decorator.

Revision 2.4  2010/01/07 10:58:59  aa
Added new type of signature/exceptions specifications.

Revision 2.3  2009/12/02 18:05:30  aa
Added a new error/status message when generator stops (StopIteration
raised).

Revision 2.2  2009/12/02 17:44:17  aa
Corrected to the new generator syntax in Python 3.

Revision 2.1  2009/12/02 16:56:03  aa
Corrected type names to Python 3 names.

Revision 2.0  2009/12/02 15:44:16  aa
Moved to Python 3 syntax.

Revision 1.8  2005/11/30 10:58:51  aa
Added more documentation, renamed methods (removed Activity from the
method names), made some attributes private, ensured that the `start`,
`pause` and `stop` methods are blocked until an ongoing function has
terminated (this also corrected the implementation of `wait`), and
made it more explicit that an activity object is a thread object.

Revision 1.7  2005/11/29 19:32:48  aa
Added comments and usage text + error handling and `wait Activity`.

Revision 1.6  2005/11/01 19:51:01  aa
RST stuff removed.

Revision 1.5  2005/10/26 08:46:38  aa
Fixed problem with StrictPeriodic and paused activity.

Revision 1.4  2005/10/25 22:22:10  aa
Now activity supports 3 types of periods and function arguments.

Revision 1.3  2004/11/23 21:14:08  aa
Accept both Integer and Float as a period for the constructor.

Revision 1.2  2004/10/22 04:11:28  aa
Some minor updates (not important).

Revision 1.1  2004/10/21 04:03:45  aa
Initial suggestion for a periodic activity thread.

"""


# Load some standard python libraries
from types import *
import threading
import time
import sys


# Load noop libraries (if available)
try:
    from noop.core.signature import signature, one, opt, watever
except ImportError:
    def signature(*args, **kw): return lambda f: f
    class one:
        def __init__(*args): pass
    opt = one; whatever = one()


# Different types of periodes
StrictPeriodic = 1
StartOfCall = 2
EndOfCall = 3


# Error/status messages
_arggen_error = "Argument generator failed"
_arggen_stopped = "Argument generator stopped"
_function_error = "Function call failed"


class Activity(threading.Thread):
    """An periodic activity

    A periodic activity than can be started and paused (and finally
    stopped).  The user controls the activity with the methods
    `start`, `pause` and `stop`.  The user can
    block waiting for the activity to stop or pause with the
    `wait` method.  Be aware that the current implementation
    is only thread-safe for one controlling thread (only one thread
    should control the activity using the methods listed above).  The
    `Activity` class is implemented using the `Thread` class of the
    Python `threading` module.  If the activity is paused, the
    following attributes of the object can be modified: `period`,
    `function`, `args`, `kw`, `arggen` and `type`.  Their meanings are
    the same as the arguments to the constructor with the same names.

    """

    @signature(exc=[])
    def __init__(self,
                 period: one(int, float),
                 function: FunctionType, 
                 args: opt(tuple) = (), 
                 kw: opt({str: any}) = {}, 
                 arggen: opt(GeneratorType) = None,
                 type: opt(int) = StrictPeriodic):
        """Initialise the activity

        Save information about the periodic activity and initialise
        the thread (do not start it).  The implementation uses three
        semaphores `_runsem`, `_pausesem` and `_waitsem`.  The
        `_runsem` semaphore is used to block the activity before it is
        started.  The `_pausesem` semaphore is used to block the
        activity when it is paused.  The `_waitsem` semaphore is used
        to block the caller of the `wait` method until to
        activity is stopped (or paused).
        
        """
        self.period = period
        self.function = function
        self.args = args
        self.kw = kw
        self.arggen = arggen
        self.type = type
        self._runs = self._pause = self._stopped = 0
        self.exctype = None
        self.excmsg = self.excinfo = ""
        self._runsem = threading.Semaphore()
        self._pausesem = threading.Semaphore()
        self._waitsem = threading.Semaphore()
        threading.Thread.__init__(self)

    @signature(exc=[])
    def run(self):
        """The actual thread

        This is the thread doing the periodic activity (the function).
        This method is not used by the user.  It is called from the
        `threading.Thread` class.  In the `StrictPeriodic` version of
        the thread, the time to the next call is always the previous
        call (planned) plus the period.  In the `StartOfCall` version
        of the thread, the time to the next call is when the previous
        call actually started plus the period.  In he `EndOfCall`
        version of the thread, the time to the next call is when the
        previous call ended plus the period.

        """
        
        # This goes on forever (or until run is unset)
        self.nextcall = time.time()
        while True:

            # Pre fetch arguments
            if self.arggen:
                try:
                    (a, k) = next(self.arggen)
                except Exception:
                    self.exctype, self.excmsg = sys.exc_info()[:2]
                    if self.exctype == StopIteration:
                        self.excinfo = _arggen_stopped
                    else:
                        self.excinfo = _arggen_error
                    self.stop()
                    break
            else:
                (a, k) = self.args, self.kw	# Arguments can be changed

            # Check period (and sleep if we are not ready yet)
            if time.time() < self.nextcall:
                try:
                    time.sleep(self.nextcall - time.time())
                except IOError:
                    pass	# Catch negative values to sleep()

            # Pause the thread?
            self._pausesem.acquire()	# Activity blocked here if it is paused
            self._pausesem.release()

            # Read the run attribute and terminate if run is unset
            self._runsem.acquire()
            if not self._runs:
                self._runsem.release()	# Activity is stopped
                break

            # Calculate next call (StartOfCall)
            if self.type == StartOfCall:
                self.nextcall = time.time() + self.period

            # Perform the function
            try:
                self.function(*a, **k)
            except Exception:
                self.exctype, self.excmsg = sys.exc_info()[:2]
                self.excinfo = _function_error
                self.stop()
                self._runsem.release()
                break
            else:
                self._runsem.release()

            # Calculate next call (StrictPeriodic or EndOfCall)
            if self.type == StrictPeriodic:
                self.nextcall += self.period
            elif self.type == EndOfCall:
                self.nextcall = time.time() + self.period

    @signature(exc=[RuntimeError])
    def start(self):
        """Start activity

        Start the periodic activity if it is not started yet or if it
        is paused.  Raises a RuntimeError of you try to start a
        stopped activity.

        """

        # You can not start a stopped activity
        if self._stopped:
            raise RuntimeError("Can not start a stopped activity")

        # If it is not running, start it
        self._runsem.acquire()
        if not self._runs:
            self._runs = 1
            threading.Thread.start(self)

        # If it is paused, restart it
        elif self._pause:
            self._pause = 0
            if self.nextcall < time.time():
                self.nextcall = time.time()	# Do not catch up with lost time
            self._pausesem.release()

        # Wait should wait for a running activity to stop or pause
        self._waitsem.acquire()
        self._runsem.release()

    @signature(exc=[])
    def pause(self):
        """Pause activity

        Pause the activity if it is running and not allready paused.

        """

        # Is it running?
        self._runsem.acquire()
        if self._runs:

            # Is it allready paused
            if not self._pause:

                # Make thread block (with semaphore) and mark it paused
                self._pausesem.acquire()
                self._pause = 1

                # Wait should not wait for a paused activity
                self._waitsem.release()
                
        self._runsem.release()

    @signature(exc=[])
    def wait(self):
        """Wait for activity to stop

        This call blocks until the activity are stopped or paused.  If
        the activity is not running, the call returns immediately.

        """

        # Block it activity is running
        self._waitsem.acquire()

        # Reset semaphore
        self._waitsem.release()
        
    @signature(exc=[])
    def stop(self):
        """Stop activity

        Stop the activity.  The activity can not be started again, and
        is probarly not of any value after this call is done.  It is
        possible to check if an activity has stopped with no error (by
        calling this method) or by an exception generated by the
        function call or a call to the argument generator (see
        `arggen` attribute).  If the `exctype` attribute of the
        activity object is `None`, then the activity has stopped with
        no errors.  Otherwise the `exctype` attribute value is the
        exception that stopped the activity, and the `excmsg` and
        `excinfo` attributes contains information about this
        exception.

        """

        # Mark this activity as stopped
        self._stopped = 1

        # Is it running?  Stop it
        self._runsem.acquire()
        if self._runs:
            self._runs = 0
            self._runsem.release()

            # Is it paused?  Unblock it (so it can stop)
            if self._pause:
                self._pause = 0
                self._pausesem.release()

            # Unblock any `wait` calls
            self._waitsem.release()
                
        else:
            self._runsem.release()
