'''
.. module BufferMan of picoDAQ

.. author: Guenter Quast <guenter.quast@online.de>
'''

#
from __future__ import print_function, division, unicode_literals
from __future__ import absolute_import

# - class BufferMan
import numpy as np, sys, time, threading

from multiprocessing import Queue, Process, Array
from multiprocessing.sharedctypes import RawValue, RawArray

from .mpBufManCntrl import *
from .mpOsci import *


class BufferMan(object):
    '''
    A simple Buffer Manager

    Calls rawDAQproducer() to receive data from hardware device
    and stores them in the classes buffer space. Data are served
    from buffer to obligatory consumers (i.e. data qcquisition
    is paused until all consumcers are done) and random consumers
    (receive an event copy, data acquisition continues regardless
    of the consumers' progress)
    '''

    def __init__(self, BMdict, DevConf):
        '''Args:  configuration dictionary
        Device Class
        '''

        # read configuration dictionary
        if "NBuffers" in BMdict:
            self.NBuffers = BMdict["NBuffers"]  # number of buffers
        else:
            self.NBuffers = 16
        if "BMmodules" in BMdict:
            self.BMmodules = BMdict["BMmodules"]  # display modules to start
        else:
            self.BMmodules = []
        if "LogFile" in BMdict:
            self.LogFile = BMdict["LogFile"]
        else:
            self.LogFile = None
        self.flog = None  # file not yet open
        if "verbose" in BMdict:
            self.verbose = BMdict["verbose"]
        else:
            self.verbose = 1  # print (detailed) info if >0
        if "logTime" in BMdict:
            self.logTime = BMdict["logTime"]  # time between logging entries
        else:
            self.logTime = 60  # logging information once per 60 sec

        # read device congiguration and set up Buffer space
        self.DevConf = DevConf
        self.NChannels = DevConf.NChannels  # number of channels in use
        self.NSamples = DevConf.NSamples  # number of samples
        self.TSampling = DevConf.TSampling  # sampling interval

        # function collecting data from hardware device
        self.rawDAQproducer = DevConf.acquireDataBM

        # data structure for BufferManager in shared c-type memory ...
        self.CBMbuf = RawArray('f', self.NBuffers * self.NChannels * self.NSamples)
        self.CtimeStamp = RawArray('f', self.NBuffers)
        self.CtrigStamp = RawArray('i', self.NBuffers)
        #  ... and map to numpy arrays
        self.BMbuf = np.frombuffer(self.CBMbuf, 'f').reshape(self.NBuffers, self.NChannels, self.NSamples)
        self.timeStamp = np.frombuffer(self.CtimeStamp, 'f')
        self.trigStamp = np.frombuffer(self.CtrigStamp, 'f')

        self.ibufr = RawValue('i', -1)  # read index, synchronization with producer

        # global variables for producer statistics
        self.Ntrig = RawValue('i', 0)  # count number of readings
        self.Ttrig = RawValue('f', 0.0)  # time of last event
        self.Tlife = RawValue('f', 0.0)  # DAQ lifetime
        self.readrate = RawValue('f', 0.0)  # current rate
        self.lifefrac = RawValue('f', 0.0)  # current life-time
        self.BMT0 = RawValue('d', 0.0)  # time of run-start

        # set up variables for Buffer Manager status and accounting
        self.tPause = 0.0  # time when last paused
        self.dTPause = 0.0  # total time spent in paused state
        self.ACTIVE = RawValue('b', 0)
        self.RUNNING = RawValue('b', 0)
        self.STOPPED = False

        # queues ( multiprocessing Queues for communication with sub-processes)
        self.prod_Que = Queue(self.NBuffers)  # acquireData <-> manageDataBuffer
        self.request_Ques = []  # consumer request to manageDataBuffer
        # 0:  request event pointer, obligatory consumer
        # 1:  request event data, random consumer
        # 2:  request event data, obligatoray consumer
        self.consumer_Ques = []  # data from manageDataBuffer to consumer

        # multiprocessing Queues for data transfer to subprocesses
        self.mpQues = []
        self.BMInfoQue = None

        self.BMlock = threading.Lock()
        self.logQ = None

        # keep track of sub-processes started by BufferManager
        self.procs = []  # list of sub-processes started by BufferMan
        self.thrds = []  # list of sub-processes started by BufferMan

    # -- end of __init__()

    # -- the raw data procuder
    #   must run as a thread in the same process that initialized the hardware device
    def acquireData(self):
        '''
         Procucer Thread

           - collects and stores data in buffers
           - provides all acquired data to manageDataBufer
           - count number of events and calculate life time

           Arg: funtion handling data acquisition from device

        Communicates with consumer via multiprocessing.Queue()

        '''
        #    self.prlog('*==* BufMan:  !!! acquireData starting')
        tlife = 0.0

        ni = 0  # temporary variable
        ts = time.time()

        ibufw = -1  # buffer index
        while self.ACTIVE.value:
            # sample data from Picoscope handled by instance ps
            ibufw = (ibufw + 1) % self.NBuffers  # next write buffer
            while ibufw == self.ibufr.value:  # wait for consumer done with this buffer
                if not self.ACTIVE.value:
                    if self.verbose:
                        self.prlog('*==* BufMan.acquireData()  ended')
                    return
                time.sleep(0.0005)
            #
            while not self.RUNNING.value:  # wait for running status
                if not self.ACTIVE.value:
                    if self.verbose:
                        self.prlog('*==* BufMan.acquireData()  ended')
                    return
                time.sleep(0.01)

            # data acquisition from hardware
            e = self.rawDAQproducer(self.BMbuf[ibufw])
            if e == None:
                if self.verbose:
                    self.prlog('*==* BufMan.acquireData()  ended')
                return
            ttrg, tl = e
            tlife += tl
            self.Tlife.value += tl
            ttrg -= self.BMT0.value
            self.timeStamp[ibufw] = ttrg  # store time when data became ready
            self.Ttrig.value = ttrg
            self.Ntrig.value += 1
            self.trigStamp[ibufw] = self.Ntrig.value
            self.prod_Que.put(ibufw)

            # wait for free buffer
            while self.prod_Que.qsize() == self.NBuffers:
                if not self.ACTIVE.value:
                    if self.verbose:
                        self.prlog('*==* BufMan.acquireData()  ended')
                    return
                time.sleep(0.0005)

            # calculate life time and read rate
            if (self.Ntrig.value - ni) == 10:
                dt = time.time() - ts
                ts += dt
                self.readrate.value = (self.Ntrig.value - ni) / dt
                self.lifefrac.value = (tlife / dt) * 100.0
                tlife = 0.0
                ni = self.Ntrig.value
        # --- end while
        if self.verbose:
            self.prlog('*==* BufMan.acquireData()  ended')
        return

    # -- end def acquireData

    # -- the central Buffer Manager - runs as a thread or sub-process
    def manageDataBuffer(self):
        '''main Consumer Thread

        - receive data from procuder (acquireData):
        - provide all events for analysis to "obligatory" consumers
        - provide subset of events to "random" consumers (picoVMeter, oscilloscope)

        '''
        t0 = time.time()
        n0 = 0
        n = 0
        while self.ACTIVE.value:
            while self.prod_Que.empty():  # wait for pointer to data in producer queue
                if not self.ACTIVE.value:
                    if self.verbose:
                        self.prlog('*==* BufMan ended')
                    return
                time.sleep(0.0005)
            self.ibufr.value = self.prod_Que.get()
            evNr = self.trigStamp[self.ibufr.value]
            evTime = self.timeStamp[self.ibufr.value]

            # check if other threads or sup-processes request data
            #     next request treated as "done" for obligatory consumers
            l_obligatory = []
            if len(self.request_Ques):
                for i, Q in enumerate(self.request_Ques):
                    if not Q.empty():
                        req = Q.get()
                        if req == 0:  # return poiner to Buffer
                            self.consumer_Ques[i].put(self.ibufr.value)
                            l_obligatory.append(i)
                        elif req == 1:  # return a copy of data
                            self.consumer_Ques[i].put((evNr, evTime, self.BMbuf[self.ibufr.value]))
                        elif req == 2:  # return copy and mark as obligatory
                            self.consumer_Ques[i].put((evNr, evTime, BMbuf[self.ibufr.value]))
                            l_obligatory.append(i)
                        else:
                            self.prlog('!=! manageDataBuffer: invalid request mode', req)
                            sys.exit(1)

            # provide data via a mp-Queue at lower priority
            if len(self.mpQues):
                ###                 only if Buffer is not full
                #      if len(self.mpQues) and self.prod_Que.qsize() <= self.NBuffers/2 :
                for Q in self.mpQues:
                    if Q.empty():  # put an event in the Queue
                        Q.put((evNr, evTime, self.BMbuf[self.ibufr.value]))

            # wait until all obligatory consumers are done
            if len(l_obligatory):
                while self.ACTIVE.value:
                    done = True
                    for i in l_obligatory:
                        if self.request_Ques[i].empty():
                            done = False
                    if done:
                        break
                    if not self.ACTIVE.value:
                        if self.verbose:
                            self.prlog('*==* BufMan ended')
                        return
                    time.sleep(0.0005)
            #  signal to producer that all consumers are done with this event
            self.ibufr.value = -1

            # print event rate
            n += 1
            if time.time() - t0 >= self.logTime:
                t0 = time.time()
                if self.verbose:
                    self.prlog('evt %i:  rate: %.3gHz   life: %.2f%%' % (n, self.readrate.value, self.lifefrac.value))
                if evNr != n:
                    self.prlog("!!! manageDataBuffer error: ncnt != Ntrig: %i, %i" % (n, evNr))
        #   - end while ACTIVE
        if self.verbose:
            self.prlog('*==* BufMan ended')
        return

    # -- end def manageDataBuffer()

    # -- helper functions for interaction with BufferManager

    def BMregister(self):
        '''
        register a client to Buffer Manager

        Returns: client index
        '''

        self.BMlock.acquire()  # called by many processes, needs protection ...
        self.request_Ques.append(Queue(1))
        self.consumer_Ques.append(Queue(1))
        client_index = len(self.request_Ques) - 1
        self.BMlock.release()

        if self.verbose:
            self.prlog("*==* BMregister: new client id=%i" % client_index)
        return client_index

    def BMregister_mpQ(self):
        #   multiprocessing Queue
        '''
        register a subprocess to Buffer Manager

        copy of data will be transferred via a multiprocess Queue

        Returns: client index
                 multiprocess Queue
        '''

        self.mpQues.append(Queue(1))
        cid = len(self.mpQues) - 1

        if self.verbose:
            self.prlog("*==* BMregister_mpQ: new subprocess client id=%i" % cid)
        return cid, self.mpQues[-1]

    # -- encapsulates data access for obligatory and random clients
    def getEvent(self, client_index, mode=1):
        '''
        request event from Buffer Manager

          Arguments:

            client_index client:  index as returned by BMregister()
            mode:   0: event pointer (olbigatory consumer)
                    1: copy of event data (random consumer)
                    2: copy of event (olbigatory consumer)

          Returns:

            event data
        '''

        self.request_Ques[client_index].put(mode)
        cQ = self.consumer_Ques[client_index]
        while cQ.empty():
            if not self.ACTIVE.value:
                return
            time.sleep(0.0005)
        # self.prlog('*==* getEvent: received event %i'%evNr)
        if mode != 0:  # received copy of the event data
            return cQ.get()
        else:  # received pointer to event buffer
            ibr = cQ.get()
            evNr = self.trigStamp[ibr]
            evTime = self.timeStamp[ibr]
            evData = self.BMbuf[ibr]
            return evNr, evTime, evData

    # -- Run control fuctions
    # set-up Buffer Manager processes
    def start(self):
        if self.verbose > 1:
            self.prlog('*==* BufferMan  starting acquisition threads')
        self.ACTIVE.value = True
        self.runStarted = False

        thr_acquireData = threading.Thread(target=self.acquireData)
        thr_acquireData.setName('acquireData')
        thr_acquireData.daemon = True
        thr_acquireData.start()
        self.thrds.append(thr_acquireData)
        # test: try as sub-process
        #    prc_acquireData=Process(name='acquireData', target=self.acquireData)
        #    prc_cquireData.start()

        #    thr_manageDataBuffer=threading.Thread(target=self.manageDataBuffer)
        #    thr_manageDataBuffer.setName('manageDataBuffer')
        #    thr_manageDataBuffer.daemon=True
        #    thr_manageDataBuffer.start()

        # start manageDataBuffer as the last sub-process in run(),
        #   connects daq producer and clients)
        self.start_manageDataBuffer = True

        # BufferMan Info and control always started
        self.logQ = Queue()
        maxBMrate = 450.0
        self.BMIinterval = 1000.0  # update interval in ms
        self.procs.append(
            Process(
                name='BufManCntrl',
                target=mpBufManCntrl,
                args=(
                    self.getBMCommandQue(),
                    self.logQ,
                    self.getBMInfoQue(),
                    #                  cmdQ             BM_logQue       BM_InfoQue
                    maxBMrate,
                    self.BMIinterval,
                ),
            )
        )
        #             max_rate   update_interval

        # waveform display
        if 'mpOsci' in self.BMmodules:
            OScidx, OSmpQ = self.BMregister_mpQ()
            self.procs.append(
                Process(name='Osci', target=mpOsci, args=(OSmpQ, self.DevConf.OscConfDict, 100.0, 'event rate'))
            )
        #                                            interval
        # start BufferMan background processes
        for prc in self.procs:
            #      prc.deamon = True
            prc.start()
            if self.verbose:
                print('      BufferMan: starting process ', prc.name, ' PID =', prc.pid)

    #  end start()

    # start run - this must not be started before all clients have registred
    def run(self):
        # start manageDataBuffer process and initialize run
        if self.runStarted:
            self.prlog('*==* run already started - do nothing')
            return

        tstart = time.time()
        if self.LogFile:
            datetime = time.strftime('%y%m%d-%H%M', time.localtime(tstart))
            self.flog = open(self.LogFile + '_' + datetime + '.log', 'w', 1)

        if self.verbose:
            self.prlog('*==* BufferMan T0')
        self.BMT0.value = tstart
        if self.verbose:
            self.prlog('*==* BufferMan start running')

        if self.start_manageDataBuffer:  # delayed start of manageDataBuffer
            self.procs.append(Process(name='manageDataBuffer', target=self.manageDataBuffer))
            self.procs[-1].start()
            self.start_manageDataBuffer = False
            if self.verbose:
                print('      BufferMan: starting process ', self.procs[-1].name, ' PID =', self.procs[-1].pid)

        self.runStarted = True
        self.RUNNING.value = True

    # pause data acquisition - RUNNING flag evaluated by raw data producer
    def pause(self):
        if not self.RUNNING.value:
            print('*==* BufferMan: Pause command recieved, but not running')
            return
        if self.STOPPED:
            print('*==* BufferMan: Pause from Stopped state not possible')
            return
        if self.verbose:
            self.prlog('*==* BufferMan  pause')
        self.RUNNING.value = False
        self.tPause = time.time()
        self.readrate.value = 0.0

    # resume data acquisition
    def resume(self):
        if self.RUNNING.value:
            print('*==* BufferMan: Resume command recieved, but already running')
            return
        if self.STOPPED:
            print('*==* BufferMan: Resume from Stopped state not possible')
            return

        if self.verbose:
            self.prlog('*==* BufferMan  resume')
        self.RUNNING.value = True
        self.dTPause += time.time() - self.tPause
        self.tPause = 0.0

    def setverbose(self, vlevel):
        self.verbose = vlevel

    # functions for Buffer Manager control
    def execCommand(self, c):
        if c == 'P':
            self.pause()
        elif c == 'R':
            self.resume()
        elif c == 'S':
            self.stop()
        elif c == 'E':
            self.end()

    def kbdin(self):
        '''
        read keyboard input, run as backround-thread to aviod blocking
        '''
        # 1st, remove pyhton 2 vs. python 3 incompatibility for keyboard input
        if sys.version_info[:2] <= (2, 7):
            get_input = raw_input
        else:
            get_input = input
        # keyboard input as thread

        while self.ACTIVE.value:
            self.kbdtxt = get_input(30 * ' ' + 'type -> E(nd), P(ause), S(top) or R(esume) + <ret> ')

    def kbdCntrl(self):
        '''
        Control Buffer Manager via keyboard (stdin)
        '''

        self.kbdtxt = ''
        # set up a thread to read from keyboad without blocking
        kbd_thrd = threading.Thread(target=self.kbdin)
        kbd_thrd.daemon = True
        kbd_thrd.start()
        self.thrds.append(kbd_thrd)

        while self.ACTIVE.value:
            if len(self.kbdtxt):
                cmd = self.kbdtxt
                self.kbdtxt = ''
                self.execCommand(cmd)
            time.sleep(0.1)

    def readCommands_frQ(self, cmdQ):
        # a thread to receive control commands via a mp-Queue
        while self.ACTIVE.value:
            cmd = cmdQ.get()
            self.execCommand(cmd)
            time.sleep(0.1)

    def getBMCommandQue(self):
        '''multiprocessing Queue for commands

        starts a background process to read BMCommandQue

        Returns: multiprocess Queue
        '''

        self.BMCommandQue = Queue(1)
        # start a background thread for reporting
        thr_BMCommandQ = threading.Thread(target=self.readCommands_frQ, args=(self.BMCommandQue,))
        thr_BMCommandQ.daemon = True
        thr_BMCommandQ.setName('BMreadCommand')
        thr_BMCommandQ.start()
        self.thrds.append(thr_BMCommandQ)

        if self.verbose:
            self.prlog("*==* BMCommandQue enabled")
        return self.BMCommandQue

    # collect status information - actual and integrated
    def getStatus(self):
        '''Returns:
        tuple: Running status, number of events,
               time of last event, rate, life fraction and buffer level
        '''
        bL = (self.prod_Que.qsize() * 100) / self.NBuffers
        stat = self.RUNNING.value
        if self.tPause != 0.0:
            t = self.tPause
        else:
            t = time.time()
        return (
            stat,
            t - self.BMT0.value - self.dTPause,
            self.Ntrig.value,
            self.Ttrig.value,
            self.Tlife.value,
            self.readrate.value,
            self.lifefrac.value,
            bL,
        )

    # mp-Qeueu for information, starts getStatus as thread
    def getBMInfoQue(self):
        '''multiprocessing Queue for status information

        starts a background process to fill BMInfoQue

        Returns: multiprocess Queue
        '''

        self.BMInfoQue = Queue(1)
        # start a background thread for reporting
        thr_BMInfoQ = threading.Thread(target=self.reportStatus, args=(self.BMInfoQue,))
        thr_BMInfoQ.daemon = True
        thr_BMInfoQ.setName('BMreportStatus')
        thr_BMInfoQ.start()
        self.thrds.append(thr_BMInfoQ)

        if self.verbose:
            self.prlog("*==* BMInfoQue enabled")
        return self.BMInfoQue

    def reportStatus(self, Q):
        '''report Buffer manager staus to a multiprocessing Queue'''
        while not self.STOPPED:
            if Q is not None and Q.empty():
                Q.put(self.getStatus())
            time.sleep(self.BMIinterval / 2000.0)  # ! convert from ms to s

    def prlog(self, m):
        '''send a Message, to screen or to LogQ'''
        t = time.time() - self.BMT0.value  # add time stamp to message
        s = '%.2f ' % (t) + m
        if self.logQ is None:
            print(s)
        else:
            if self.logQ.empty():
                self.logQ.put(s)
            else:
                print(s)
        if self.flog != None:
            print(s, file=self.flog)

    # prints end-of-run summary
    def print_summary(self):
        datetime = time.strftime('%y%m%d-%H%M', time.localtime(self.BMT0.value))
        if self.flog == None:
            self.flog = open('BMsummary_' + datetime + '.sum', 'w')
        self.prlog('Run Summary: started ' + datetime)
        self.prlog(
            '  Trun=%.1fs  Ntrig=%i  Tlife=%.1fs\n'
            % (self.TStop - self.BMT0.value - self.dTPause, self.Ntrig.value, self.Tlife.value)
        )
        self.flog.close()
        self.flog = None

    # put run in "stopped state" - BM processes remain active, no resume possible
    def stop(self):
        if not self.ACTIVE.value:
            print('*==* BufferMan: Stop command recieved, but not running')
            return

        if self.tPause != 0.0:  # stopping from paused state
            self.dTPause += time.time() - self.tPause
            self.tPause = 0.0
        if self.RUNNING.value:
            self.pause()
            self.tPause = 0.0

        self.TStop = time.time()
        self.STOPPED = True

        time.sleep(1.0)  # allow all events to propagate

        if self.verbose:
            self.prlog('*==* BufferMan ending - reached stopped state')
        self.print_summary()
        if self.verbose:
            print('\n *==* BufferMan ending, RunSummary written')
            print(
                '  Run Summary: Trun=%.1fs  Ntrig=%i  Tlife=%.1fs\n'
                % (self.TStop - self.BMT0.value - self.dTPause, self.Ntrig.value, self.Tlife.value)
            )

    # end BufferManager, stop all processes
    #       ACTIVE flag observed by all sub-processes
    def end(self):
        if not self.ACTIVE.value:
            print('*==* BufferMan: End command recieved, but not active')
            return

        if self.RUNNING.value:  # ending from RUNNING state, stop first
            self.stop()

        self.ACTIVE.value = False
        time.sleep(0.3)

        # stop all sub-processes
        for prc in self.procs:
            if self.verbose:
                print('    BufferMan: terminating ' + prc.name)
            prc.terminate()
        time.sleep(0.3)

    def __del__(self):
        self.RUNNING.value = False
        self.ACTIVE.value = False


# - end class BufferMan
