import threading
import functools

##################################################

class GuardState(object):
    """Guardian system state machine state.

    Execution model:

    This type of state is known as a "Moore" state: the primary action
    is executed when the state is entered (main() method).

    The main() method is executed once upon entering the state, after
    which the run() method is executed in a loop.  The return values
    of both methods are interpreted the same, as follows:

    If the return value of either method is True, the state will
    "complete" (status: DONE).  If the state is not the requested
    state the system will immediately transition to the target state.
    If the state is equal to the requested state the run method will
    be executed.

    If the return value is a string it will be interpreted as a state
    name and the system will transition immediately to the new state.
    This is known as a "jump" transition.

    If the return value is None or False, the run() method is
    executed.  NOTE: if unspecified, functions return None by default.

    State properties (user specified):

    request: indicates whether or not this state is requestable by the
    user.  Default: True

    goto: if True will cause edges to be added to this state from all
    states in the system graph.  Default: False

    index: numeric index for state.  must be a positive definite
    number.  If not specified, a negative index will be automatically
    assigned.

    State objects:

    timer: TimerManager object, used to count down a specified amount
    of time.

    >>> self.timer['foo'] = 3
    >>> self.timer['foo']
    False
    >>> time.sleep(3)
    >>> self.timer['foo']
    True

    The environment of state method execution includes a couple of
    additional "built-in" functions:

    log: write to state the system logger:

    >>> log("doing something")

    ezca: Ezca EPICS channel access:

    >>> ezca['A'] = 3

    """
    def __init__(self, logfunc=None):
        self._logfunc = logfunc
        self.timer = TimerManager(logfunc=logfunc)

    @property
    def name(self):
        return self.__class__.__name__

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.name)

    def __str__(self):
        # use base name
        return "<GuardState %r>" % (self.name)

    ##########

    def log(self, message):
        if self._logfunc:
            self._logfunc(message)
        else:
            print(message)

    ##########
    ##########
    # user overridable functions/variables

    request = True
    """Is this state "requestable"."""

    goto = False
    """Is this a "goto" state.

    If specified as :type int, the value will be used as the edge
    weight for all goto edges to this state.
    """

    redirect = True
    """Can this state be "redirected".

    If False the state is "protected" and redirects away from this
    state are ignored while the state is returning False.

    """

    ##########
    def main(self):
        """state MAIN method.

        This method is executed once immediately upon entering state.

        See "Execution model" above for more info.

        """
        return False

    ##########
    def run(self):
        """state RUN method.

        This method is executed in a loop if the main() method returns
        False or None.

        See "Execution model" above for more info.

        """
        return True

    ##########
    ##########

    def destroy(self):
        self.timer.destroy()
        # FIXME: anything else?

##################################################

class TimerError(Exception):
    pass

class Timer(object):
    """Simple single timer.

    Once initialized with a time in seconds, the done property returns
    False until the timeout, after which done returns True.

    """
    def __init__(self, time, name=None, logfunc=None):
        self._name = name
        self._logfunc = logfunc
        self._done = False
        self._timer = threading.Timer(time, self._stop)
        self._timer.daemon = True
        self._timer.start()

    def _stop(self):
        self._done = True
        if self._logfunc and self._name:
            self._logfunc("timer['%s'] done" % self._name)

    @property
    def done(self):
        """Return True if timeout reached, False otherwise."""
        return self._done

    def cancel(self):
        """Cancel timer."""
        self._timer.cancel()

class TimerManager(object):
    """Manage named timers.

    Setitem initializes and starts a named timer.
    Getitem returns timeout status.

    """
    def __init__(self, logfunc=None):
        self._logfunc = logfunc
        self._timers = {}

    def __setitem__(self, name, time):
        """Set named timer for seconds."""
        if name in self._timers:
            self._timers[name].cancel()
        self._timers[name] = Timer(time, name=name, logfunc=self._logfunc)
        if self._logfunc:
            self._logfunc("timer['%s'] = %s" % (name, time))

    def __getitem__(self, name):
        """Return True if named timer done, False otherwise."""
        if name not in self._timers:
            raise TimerError("timer '%s' not initialized." % name)
        return self._timers[name].done

    def destroy(self):
        """Destroy all timer objects."""
        for timer in self._timers.values():
            timer.cancel()

##################################################

class GuardStateDecorator(object):
    """GuardState method decorator base class.

    GuardStateDecorator is a function decorator designed for
    GuardState methods.  It executes "check" code before and/or after
    the function being wrapped.  It has three user-overrideable
    methods, *pre_exec*, *pre_return_exec*, and *post_exec*.  If any
    of these methods return a value that is not None, those return
    values will be returned from the wrapped function call.  See the
    individual method help for more information.

    Example: The following would cause the FOO.run() method to return
    'TRIPPED' immediately if the 'WD' channel equals 0.  The rest of
    the FOO.run() method will not be executed:

    class check_wd(GuardStateDecorator):
        def pre_exec(self):
            if ezca['WD'] == 0:
                return 'TRIPPED'

    class FOO(GuardState):
        @check_wd
        def run(self):
            ...

    """
    def __init__(self, func):
        functools.wraps(func)(self)
        self.func = func

    def __get__(self, state_obj, owner_class=None):
        return functools.partial(self.__call__, state_obj)

    def __call__(self, state_obj, *args, **kwargs):
        pre_exec_return = self.pre_exec()
        if pre_exec_return is not None:
            return pre_exec_return

        main_return = self.func.__call__(state_obj, *args, **kwargs)

        pre_return_exec_return = self.pre_return_exec()
        if pre_return_exec_return is not None:
            return pre_return_exec_return

        if main_return is not None:
            return main_return

        post_exec_return = self.post_exec()
        if post_exec_return is not None:
            return post_exec_return

        return main_return

    # FIXME: should these all be staticmethods?

    def pre_exec(self):
        """Pre-execution function.

        Executed before the wrapped function.  If this function's
        return value is not None, its return value will be returned
        immediately and none of the other functions will be executed.

        """
        pass

    def pre_return_exec(self):
        """Pre-return function.

        Executed after the wrapped function, but before inspection of
        the wrapped function's return value.  If this function's
        return value is not None, its return value will be returned
        immediately, the wrapped function's return value will be
        ignored, and *post_exec* will not be executed.

        """
        pass

    def post_exec(self):
        """Post-execution function.

        Executed after the wrapped function.  If none of the previous
        methods, or the wrapped function, have return values that are
        not None, this method will be executed, and its return value
        will be used if it is not None.

        """
        pass
