#!/usr/bin/python
# coding: iso-8859-15

## @brief Klassen zur Bereitstellung von Zeitgebern.
class hsl20_4_timer:

    ## @brief Alle Methoden des Timer-Objekts.
    ## @details
    ## Ein Beispiel für die Umsetzung eines @b Timers liegt bei: @e 10708_MyTimer.py @n@n
    ## Der Beispiel-Code erstellt beim ersten Eingangstelegramm an Eingang 1 oder 2 einen Timer und startet ihn.
    ## Bei jedem weiteren Telegramm wird der Timer erneut gestartet. @n
    ## Somit löst der Timer erst aus, wenn innerhalb von der an Eingang 3 definierten Zeit kein weiteres Telegramm am Eingang eintrifft.
    ## Wird auf Eingang 4 eine 1 gesendet, wird ein laufender Timer gestoppt. @n
    ##
    ## @note Die Ausführungszeit kann von der definierten Zeit abweichen.
    ## @note Die Klasse arbeitet asynchron. Keine der angebotenen Methoden blockiert.
    ## @if OLD_CHANGELOG
    ## @chlg04 Beispielcode neu erstellt.
    ## @endif
    class Timer:

        ## Konstruktor
        ##
        ## @warning Diese Klasse sollte nicht direkt instanziert werden.
        def __init__(self, framework):
            ## @cond NO_DOC
            self._time = None
            self._callback = None
            self._arguments = None
            self._framework = framework
            self._running = False
            ## @endcond

        ## Setzt die Zeit und die Callback-Methode, die nach Start des Timers aufgerufen werden soll.
        ## @param timeout @e int @n Zeit in ms
        ## @param callback @e function @n Callback
        ## @param arguments @e tuple @n Optional. Die Elemente des Tupels werden der Callback-Methode beim Aufruf als Parameter übergeben.
        ## @throws RuntimeError Wird diese Methode aufgerufen während der Timer bereits läuft, wird eine RuntimeError-Exception ausgelöst.
        ## @if OLD_CHANGELOG
        ## @chlg03 Parameter @e arguments angepasst und throws @e RuntimeError hinzugefügt
        ## @endif
        def set_timer(self, timeout, callback, arguments=()):
            ## @cond NO_DOC
            if(self._running):
                raise RuntimeError("Timer is already running!")
            else:
                self._time = timeout
                self._callback = callback
                self._arguments = arguments
            ## @endcond

        ## Hält den Timer an.
        def stop(self):
            ## @cond NO_DOC
            hsl20_4.Framework._timer_thread.delete_switchpoints(self) #Schaltpunkt entfernen
            self._running=False
            ## @endcond

        ## Startet den Timer.
        ## @note Sollte der Timer bereits laufen, wird er angehalten und neu gestartet.
        ## @throws AttributeError Wird ausgelöst, wenn noch kein Callback oder kein Timeout definiert wurde.
        ## @if OLD_CHANGELOG
        ## @chlg03 Beschreibung von throws @e AttributeError angepasst
        ## @endif
        def start(self):
            ## @cond NO_DOC
            if self._time == None:
                raise AttributeError("no timeout")
            if self._callback == None:
                raise AttributeError("no callback")
            if self._running:
                self.stop()
            self._running=True
            hsl20_4.Framework._timer_thread.add_switchpoint(self, self._time)
            ## @endcond
        
        ## @cond NO_DOC
        # Zeit ist abgelaufen. __timer_execute wird im Kontext-Thread aufgerufen.        
        def _on_timer_elapsed(self):
            self._framework._run_in_context_thread(self.__timer_execute)
            
        def __timer_execute(self):
             # Wurde der Timer inzwischen gestoppt?
            if(self._running):
                self._running=False
                self._callback(*self._arguments)
        ## @endcond


    ## @brief Alle Methoden des Intervall-Objekts.
    ## Ein Beispiel für die Umsetzung eines @b Intervall-Timers liegt bei: @e 10709_MyIntervaltimer.py @n@n
    ## Wird eine 1 auf Eingang 1 gesendet, wird ein Intervall-Timer erzeugt und gestartet, der in einem Intervall, dessen Länge (in Sekunden) über Eingang 2 bestimmt wird,
    ## auslöst und einen Ausgang mit dem Wert '1' beschreibt. @n
    ## Durch Senden einer '0' an Eingang 1 kann der Timer gestoppt werden.
    ## @note Die Ausführungszeit kann von der definierten Zeit abweichen.
    ## @note Die Klasse arbeitet asynchron. Keine der angebotenen Methoden blockiert.
    ## @if OLD_CHANGELOG
    ## @chlg04 Beispielcode neu erstellt.
    ## @endif
    class Interval:

        ## Konstruktor
        ##
        ## @warning Diese Klasse sollte nicht direkt instanziert werden.
        def __init__(self, framework):
            ## @cond NO_DOC
            self._time = None
            self._callback = None
            self._arguments = None
            self._framework = framework
            self._running = False
            self._is_executing = False
            ## @endcond

        ## Setzt die Intervall-Zeit und die Callback-Methode, die nach Start des Intervall-Timers aufgerufen werden soll.
        ## @note Sollte zum Ausführungszeitpunkt eines Intervall-Timers (also am Ende eines Intervalls) die am Ende des @b vorausgehenden Intervalls ausgelöste
        ## Aktion noch nicht beendet sein, wird der @b neue Vorgang übersprungen!
        ## @param time @e int @n Zeit in ms
        ## @param callback @n Nach Ablauf von @e time wird die Callback-Methode aufgerufen.
        ## @param arguments @e tuple @n Optional. Die Elemente des Tupels werden der Callback-Methode beim Aufruf als Parameter übergeben.
        ## @throws RuntimeError Wird diese Methode aufgerufen während der Intervall-Timer bereits läuft, wird eine RuntimeError-Exception ausgelöst.
        ## @if OLD_CHANGELOG
        ## @chlg03 Parameter @e arguments und throws @e RuntimeError hinzugefügt
        ## @endif
        def set_interval(self, time, callback, arguments=()):
            ## @cond NO_DOC
            if(self._running):
                raise RuntimeError("Interval is already running!")
            else:
                self._time = time
                self._callback = callback
                self._arguments = arguments
            ## @endcond

        ## Hält den Intervall-Timer an.
        def stop(self):
            ## @cond NO_DOC
            self._running=False
            self._is_executing = False
            hsl20_4.Framework._timer_thread.delete_switchpoints(self) #Schaltpunkt entfernen
            ## @endcond

        ## Startet den Intervall-Timer. Das Callback wird zum ersten Mal nach der in der Methode @e set_interval definierten Zeit aufgerufen.
        ## @note Sollte der Intervall-Timer bereits laufen, wird er angehalten und neu gestartet.
        ## @throws AttributeError Wird ausgelöst, wenn noch kein Callback oder kein Timeout definiert wurde.
        ## @if OLD_CHANGELOG
        ## @chlg03 Beschreibung von throws @e AttributeError angepasst
        ## @chlg04 Beschreibung angepasst
        ## @endif
        def start(self):
            ## @cond NO_DOC
            if self._time == None:
                raise AttributeError("no timeout")
            if self._callback == None:
                raise AttributeError("no callback")
            if self._running:
                self.stop()
            self._running=True
            hsl20_4.Framework._timer_thread.add_switchpoint(self, self._time)
            ## @endcond
                
        ## @cond NO_DOC
        # Zeit ist abgelaufen. __timer_execute wird im Kontext-Thread aufgerufen.        
        def _on_timer_elapsed(self):
            self._framework._run_in_context_thread(self.__timer_execute)
            if(self._running):
                hsl20_4.Framework._timer_thread.add_switchpoint(self, self._time)
            
        def __timer_execute(self):
            if(self._running and not self._is_executing):            
                self._is_executing = True
                self._callback(*self._arguments)
                self._is_executing = False
        ## @endcond
   
    ## @cond NO_DOC
    # Wird global einmal instanziiert und verwaltet in einem eigenem Thread das Auslösen aller Schaltpunkte/Timer.            
    class _TimerThread:
    
        ## Max. Einträge der _TimerThread-Queue  
        _TIMER_THREAD_QUEUE_MAX_SIZE = 0 # 0 = keine Beschränkung der Queue
    
        def __init__(self):
            #eingetragene Schaltpunkte (Timerobjekt, Zeitpunkt)
            self._switchpoint_list=[] 
            # Lock für den Zugriff auf _switchpoint_list
            self._lock = thread.allocate_lock() 
            self._queue = Queue.Queue(hsl20_4_timer._TimerThread._TIMER_THREAD_QUEUE_MAX_SIZE)
            self._thread_id = thread.start_new_thread(self.__thread_queue_consumer,())
            hsl20_4.Framework._get_global_debug_section()._internal_register(self.__on_debug)
        
        # Alle Schaltpunkte einer Instanz löschen. Darf nicht in _on_timer_elapsed() aufgerufen werden.  
        def delete_switchpoints(self, timer_instance):
            with self._lock:
                self._switchpoint_list=filter(lambda x:x[0]!=timer_instance, self._switchpoint_list)
        
        # Schaltpunkt hinzufügen    
        def add_switchpoint(self, timer_instance, time_in_ms):
            self._queue.put_nowait((timer_instance, (time_in_ms/1000.0) + time.time()))
        
        # Timer-Thread: Hängt an der Queue und löst bei erreichen der Zeit einen Schalpunkt aus    
        def __thread_queue_consumer(self):
            while(True):
                timeout=None
                with self._lock:
                    if(len(self._switchpoint_list)>0): 
                        #Nächsten Zeitpunkt berechnen
                        timeout=self._switchpoint_list[0][1]-time.time()
                switchpoint_data = None
                #Nur blockieren wenn der Schaltzeitpunkt von self._switchpoint_list[0] noch nicht erreicht ist
                if(timeout==None or timeout>0): 
                    try:
                        #blockieren und timeout Sekunden warten
                        switchpoint_data = self._queue.get(True, timeout) 
                    except Queue.Empty:
                        pass
                #Neuer Eintrag in Queue                        
                if(switchpoint_data!=None): 
                    with self._lock:
                        self._switchpoint_list.append(switchpoint_data)
                        #Sortierten: niedrigster TS ist [0]
                        self._switchpoint_list.sort(lambda x,y: cmp(x[1], y[1])) 
                with self._lock:
                    #Prüfen ob irgendwo die Zeit abgelaufen ist
                    for i in range(len(self._switchpoint_list)): #Maximal len_switchpoint_list Durchläufe der Schleife
                        if(len(self._switchpoint_list)==0):
                            break                    
                        nearest_switchpoint=self._switchpoint_list[0]                    
                        if(nearest_switchpoint[1]<=time.time()):
                            del self._switchpoint_list[0]
                            nearest_switchpoint[0]._on_timer_elapsed()                        
                        else:                                 
                            break
                if(switchpoint_data!=None):
                    #Optional
                    self._queue.task_done() 
                    
        def __on_debug(self):
            result = {}
            with self._lock:
                list_copy=self._switchpoint_list[:]
            result["TIMER IN LIST"] = str(len(list_copy))
            #TODO: Infos über Listeneinträge ausgeben
            return (result,None)
            
    ## @endcond
