#-------------------------------------------------------------------------------
# Name:          Enzyme Vision - Perform Measurements
# Purpose:       Performs the experiments according to the experiment list accessing pumps to set flow velocities, spectrometer and calibration for NADH measurement
# Authors:       Leon H, Jonathan G, Eric N
#
# Last Version:  19.10.2022
# Copyright:     (c) Institute of Technical Biocatalysis TUHH
# Licence:       GNU GPL-v3 
#-------------------------------------------------------------------------------
from InputManager_2022_05_24 import InputManager
from OutputManager_2022_05_29_2 import OutputManager
from PumpInterface_2022_05_24 import PumpInterface
from ExperimentManager_2022_08_23 import ExperimentManager
from MailManager_2022_08_18 import MailManager
import time,json,os,pickle
from spectrometer import Spectrometer
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matlab.engine
import random

# for testing purposes only
from multiprocessing import Process, Queue


class Measurements:

    def __init__(self,CMQueue,MCQueue,InputManager) -> None:

        ### Variables for the communication
        #self.number = 0

        self.CMQueue = CMQueue
        self.MCQueue = MCQueue

        self.InputManager = InputManager
        self.AdminValues = self.InputManager.getAdminValues()
        self.startValues = self.InputManager.getStartValues()
        self.RunParameters = self.InputManager.getAllRunParameters()
        self.PumpMode = self.InputManager.getPumpMode()
        self.ControlMode = self.InputManager.getControlMode()
        self.IntermediatePurge = self.InputManager.getIntermediatePurge()
        self.PumpInterface = PumpInterface(self.InputManager)
        #self.MailManager = MailManager

        ### Create OutputManager
        self.OutputManager = OutputManager(self.startValues.OutputPath,self.PumpMode)
        self.paths = self.OutputManager.getPaths()

        ### Create ExperimentManager
        self.ExperimentManager = ExperimentManager(self.InputManager)
        #self.ExperimentManager.createBaseStep()
        self.ExperimentManager.createStartingExperiments()
        self.ListExperiments = self.ExperimentManager.getExperiments()

        # TODO: implement generally for everything?
        if self.PumpMode == 1:
            self.nPumps = 1
        elif self.PumpMode == 2:
            self.nPumps = 2
        elif self.PumpMode == 3:
            self.nPumps = 4

        print('Setup:')
        self.startValues.print()

        ### Initialize basic variables
        self.currentExperiment = 0
        ### Variables for the DOE
        self.eig_Exp = 0
        self.bestModel = 0
        
        self.ReactorVolume = self.startValues.ReactorVolume
        self.VelocityControlled = False
        self.CuvetteLength = 0
        self.Wavelength = self.AdminValues.Wavelength
        self.WvLngthSpread = self.AdminValues.WvLngthSpread

        ### Starts the matlab engine
        self.matlabPath = self.AdminValues.MainFolder + '\\Matlab'
        self.eng = matlab.engine.start_matlab()
        oldpath = self.eng.cd(self.matlabPath)
        print('Matlab Directory changed from %s to %s'%(oldpath,self.matlabPath))
        self.eng.testMatlab(nargout=0)

        ### Initialize the DOE
        fittingTool = 2
        oed_mode = 1
        self.DOE = self.eng.DesignOfExperiments(fittingTool,oed_mode)
        self.eng.setResultPath(self.DOE,self.paths['Results'],nargout=0)
        self.eng.setMaxExperiments(self.DOE,self.AdminValues.MaxExperiments,nargout=0)
        self.eng.setDecayRate(self.DOE,self.AdminValues.DecayRate,nargout=0)
        self.eng.activateDiary(self.DOE,nargout=0)
        
        ### Konfiguriert das Spektrometer
        self.spec = Spectrometer.from_serial_number("HR4C3815")
        self.spec.integration_time_micros(100000)  # 0.1 seconds
        
        self.get_calibration_data()

        self.timePassed = 0
        self.initTime = time.time()
        self.run()

    def get_calibration_data(self):
        '''
        Impot the calibration of the spectrometer
        '''
        path = '/'.join((self.AdminValues.MainFolder,self.startValues.calibrationTitle+'.json'))
        #print(path)
        with open(path,'rb') as fp:
            calibration_dict = json.load(fp)   # load the calibration dict from the json file
        
        # Initialize calibration values
        self.SLOPE = calibration_dict['slope']
        self.INTERCEPT = calibration_dict['intercept']
        self.BASELINEINTENSITIES = np.array(calibration_dict['baseline'])

        if self.AdminValues.Wavelength == 'Auto':
            self.Wavelength = float(calibration_dict['Wavelength'])
        
        if self.AdminValues.WvLngthSpread == 'Auto':
            self.WvLngthSpread = float(calibration_dict['WvLngthSpread'])

    def setVelocity(self,experiment):
        '''
        TODO: überarbeiten
        Helper function to send the desired velocity to the controller.
        '''
        flowRates = str(experiment['TotalFlowrate'])
        if self.PumpMode > 1:
            flowRates += ','+str(experiment['FlowrateS1'])+','+str(experiment['FlowrateS2'])  
        if self.PumpMode > 2:
            flowRates += ','+str(experiment['FlowrateS3'])+','+str(experiment['FlowrateS4'])

        if not self.VelocityControlled:
            msg = '1,'
            self.VelocityControlled = True
        else:
            msg = '2,'

        msg += flowRates    

        print("Type, Total Flowrate, Flowrate S1, Flowrate S2, Flowrate S3, Flowrate S4")      
        print(msg)
        self.sendToController(msg)

        self.PumpInterface.runExperiment(experiment)

    def sendToController(self,msg):
        '''
        Helper function to send data to the controller.
        '''
        self.MCQueue.put(msg)

    def receiveFromController(self):
        '''
        Helper function to receive data from the controller.
        '''
        msg = self.CMQueue.get()
        return msg

    def send_and_receiveController(self,msg):
        '''
        Function to send a message to the controller and 
        wait for the response.
        Could possibly lead to an infinte loop.
        '''
        self.sendToController(msg)
        time.sleep(0.1)
        HasReturned = False
        while not HasReturned:
            
            if not self.CMQueue.empty():
                ReturnMsg = self.receiveFromController()
                if ReturnMsg == None or (type(ReturnMsg)==list and len(list = 0)):
                    continue
                else:
                    HasReturned = True
            else:
                time.sleep(0.05)
        
        return ReturnMsg

    def runspectrometer(self):
        '''
        Collect the wavelengths and intensities from the spectrometer 
        and calculate the corresponding Absorbance array.
        Filter for the desired wavelength.
        '''
        ### Get the wavelengths and intensities
        wavelengths = self.spec.wavelengths()
        intensities = self.spec.intensities()
        
        #convert intensity to absorbance (This is the natural logarithm not the decadic one)
        Absorbance_array = np.log(self.BASELINEINTENSITIES/intensities)

        #if any(Absorbance_array<0):
        #    raise ValueError('Absorbance cannot be negative')
        
        #reading the maximum absorbance at the defined wavelength 
        #specwavelength = eval(input("wavelength of NADH [nm]: "))
        specwavelength = self.Wavelength
        wavelengthSpread = self.WvLngthSpread
        # picks the Absorbances in the specified range of wavelengths
        specAb = Absorbance_array[(wavelengths>specwavelength-wavelengthSpread) & (wavelengths<specwavelength+wavelengthSpread)] #return an array of a wavelength between +-10 of specific wavelength
        # identifies the maximum absorbance within that range and assumes its the absorbance related to the concentration of NADH
        maxAb = specAb.max()
        return Absorbance_array, maxAb, wavelengths

    def measureAbsorbance(self,experiment):
        '''
        One of the main function called to perform the actual measurement.
        '''
        #Initial variables
        list_of_Absorbance = []
        stack_maxAb = []
        stack_cNADH = []
        stack_initialtime = []
        list_temps = []
        list_devTemps = []
        list_flowRates = []
        list_devFlowRates = []
        
        # create the Spectrometer
        ResidenceTime = experiment['ResidenceTime']      

        self.inStep = False

        plt.ion() # command for matplotlib to allow interactive plots
    
        print('Running Spectrometer')
            
        stop_condition = False
        starttime = time.time()
        i = 0
        #counter = 0
        #iterate until the stop condition is fullfilled
        while not stop_condition:
            #run and stack data
            
            measurement_time = time.time() # Get the current time
            if experiment['isStep']:
                self.inStep = self.runStepExperiment(experiment,self.inStep,starttime,measurement_time)

            Absorbance_array, maxAb, wavelengths = self.runspectrometer() #Function call
            ControllerMsg = self.send_and_receiveController('4,0')
            ControlerMsgSplit = ControllerMsg.split(',')
            SteadyFlag = int(ControlerMsgSplit[0])
            FlowRate = float(ControlerMsgSplit[1])
            devFlowRate = float(ControlerMsgSplit[2])
            Temperature = float(ControlerMsgSplit[3])
            devTemperature = float(ControlerMsgSplit[4])
            

            if SteadyFlag == 1:
                ## Append the values to the created lists
                list_of_Absorbance.append(Absorbance_array.tolist())
                stack_maxAb.append(maxAb)
                initialtime = time.time()-starttime
                stack_initialtime.append(initialtime)
                list_flowRates.append(FlowRate)
                list_devFlowRates.append(devFlowRate)
                list_temps.append(Temperature)
                list_devTemps.append(devTemperature)
                
                ## cutoff value
                # TODO: Reinheit als Variable
                if maxAb >=self.INTERCEPT:
                    cNADH = (maxAb-self.INTERCEPT)/self.SLOPE
                else:
                    cNADH=0

                #append values
                stack_cNADH.append(cNADH)

                ## 'Interactive' plotting is not completely trivial
                ## Basically an object containing the data to show is created and then is updated
                ## Hopefully a fully functioning GUI will be implemented

                if i == 0:
                    experiment['FigureCnv'], axConc = plt.subplots()
                    axFlow = axConc.twinx()
                    (line_gui,) = axConc.plot(stack_initialtime,stack_cNADH,label='cNADH')
                    (lineFlow,) = axFlow.plot(stack_initialtime,list_flowRates,'r',label='Flowrate')
                    axConc.set_xlabel('time (sec)')
                    axConc.set_ylabel('cNADH (mM)')
                    axFlow.set_ylabel('Flow Rate ($\mu$L/min)')
                    axConc.grid(True)
                    axConc.set_autoscale_on(True)
                    axFlow.set_autoscale_on(True)
                    axConc.autoscale_view(True,True,True)
                    axFlow.autoscale_view(True,True,True)
                    experiment['FigureCnv'].legend()
                    experiment['FigureCnv'].show()
                    experiment['wavelengths'] = wavelengths.tolist()
                else:
                    line_gui.set_xdata(stack_initialtime)
                    lineFlow.set_xdata(stack_initialtime)
                    line_gui.set_ydata(stack_cNADH)
                    lineFlow.set_ydata(list_flowRates)
                    axConc.relim()
                    axFlow.relim()
                    axConc.autoscale_view(True,True,True)
                    axFlow.autoscale_view(True,True,True)
                    experiment['FigureCnv'].canvas.draw()
                    experiment['FigureCnv'].canvas.flush_events()

                i+=1


                sleeptime = self.AdminValues.MeasureInterval-(time.time()-measurement_time)    # The sleep time is the time that should be between measurements minus the time needed to come to this point
                if sleeptime >0:
                    time.sleep(sleeptime)                                #interval time between each measurement (sec)
            else:
                time.sleep(2)

            if len(stack_cNADH)>4:   #get the average concentration when measure more than 4 times
                average = np.average(stack_cNADH[-5:]) 
            else:
                continue

            #condition to collect the data at steady state --> concentration does not change within the range 95-105%
            minAvg = (100-self.AdminValues.StopAccuracy)/100
            maxAvg = (100+self.AdminValues.StopAccuracy)/100

            stop_condition = ((time.time() >= ((self.AdminValues.TimeSecurity*60*ResidenceTime)+starttime)) 
            and len(stack_cNADH)>5 )
            #and (all(minAvg*average <= stack_cNADH[-5:]) 
            #and all(maxAvg*average >= stack_cNADH[-5:])))

            # Update experiment dictionary
            if stop_condition:
                experiment['Absorbances'] = list_of_Absorbance
                experiment['maxAb'] = stack_maxAb
                experiment['times'] = stack_initialtime
                experiment['cNADH'] = stack_cNADH
                experiment['average_final_cNADH'] = float(np.average(stack_cNADH[-5:]))
                experiment['Timestamp']= time.time()
                experiment['FlowRates'] = list_flowRates
                experiment['DeviationFlowRates'] = list_devFlowRates
                experiment['Temperatures'] = list_temps
                experiment['DeviationTemperatures'] = list_devTemps

        #if counter == 0:
        #    print("No concentration meassured -> No output absorbance image generated")
        
        plt.close(experiment['FigureCnv'])

        print('Finished Absorbance Measurement')

        experiment['avgAb'] = np.average(stack_maxAb[-5:])
        if self.PumpMode == 1:
            experiment['conversion'] = experiment['average_final_cNADH']/experiment['StockConc']
        else:
            experiment['conversion'] = experiment['average_final_cNADH']/experiment['TargetConcentrationS1']
        # TODO: check if the factor 60 is needed
        experiment['STY'] = experiment['average_final_cNADH']/(experiment['ResidenceTime']*60)

    def shutdown(self):
        '''
        Function to shut down the whole system.
        '''
        print('shutdown Measurement')
        self.MCQueue.put('3,0')
        self.PumpInterface.stopPumps()
        self.spec.close()
        self.eng.saveFigures(self.DOE,nargout=0)
        self.eng.deactivateDiary(self.DOE,nargout=0)

    def outputExperiment(self,experiment):
        self.OutputManager.outputExperiment(experiment)

    def runExperiment(self,experiment):
        print('New Experiment: No.%i'%(self.currentExperiment+1))

        self.ExperimentManager.setTimePassed(self.timePassed)
        self.ExperimentManager.calcFlowrates(experiment)

        self.setVelocity(experiment)
        self.measureAbsorbance(experiment)
        self.outputExperiment(experiment)
        experiment['Done'] = True
        print('\n')

        if self.IntermediatePurge:
            self.PumpInterface.shortPurge()

    def runBaseExperiments(self):
        '''
        Main Loop that iterates through the original experiments.
        '''

        for i in range(self.currentExperiment,len(self.ListExperiments)):
            
        
            ''' this might cause issues with the base experiments,
                if an experiment using the maximum concentrations is used '''
            self.timePassed = time.time() - self.initTime
            print('Time passed: %f s\n'%self.timePassed)
            experiment = self.ListExperiments[i]
            self.runExperiment(experiment)
            self.addToDoE(experiment)
            self.currentExperiment += 1  

    def runStepExperiment(self,experiment,inStep,starttime,measurement_time):
        resTime = experiment['Step']['ResidenceTime']
        if not(inStep) and ((measurement_time-starttime) > 180) and ((measurement_time-starttime) < (180+5)):
            self.setVelocity(experiment['Step'])
            inStep = True
        elif inStep and (measurement_time-starttime) > (180+5):
            self.setVelocity(experiment)
            inStep = False
        return inStep

    def addToDoE(self,experiment):
        ''' helper function for adding an experiment to the matlab DOE '''
        s1 = float(experiment['TargetConcentrationS1'])
        s2 = float(experiment['TargetConcentrationS3'])
        tres = float(experiment['ResidenceTime'])
        cNADH = float(experiment['average_final_cNADH'])
        #cNADH = float(random.random()*2)
        #cNADH = float(fakeCNADH[self.currentExperiment])
        self.eng.addExperiments(self.DOE,s1,s2,tres,cNADH,nargout=0)

    def run(self):
        '''
        Loop that creates new experiments while the (matlab) stop conditions havent been met.
        '''
        try:
            self.runBaseExperiments()
            self.eng.tryRunDOE(self.DOE,nargout=0)
            continue_OED = self.eng.checkStopConditions(self.DOE)
            self.eng.outputJSON(self.DOE,continue_OED,nargout=0)
            while continue_OED:
                self.timePassed = time.time() - self.initTime
                print('Time passed: %f s'%self.timePassed)
                self.eng.setTimePassed(self.DOE,float(self.timePassed),nargout=0)
                newExp = self.eng.tryCalcNextExperiment(self.DOE)[0]
                #print(newExp)
                self.ExperimentManager.addNewExperiment(newExp[2],newExp[0],newExp[1])
                for i in range(self.currentExperiment,len(self.ListExperiments)):
                    experiment = self.ListExperiments[i]
                    self.runExperiment(experiment)
                    self.addToDoE(experiment)
                    self.currentExperiment += 1
                self.eng.tryRunDOE(self.DOE,nargout=0)
                continue_OED = self.eng.checkStopConditions(self.DOE)
                self.eng.outputJSON(self.DOE,continue_OED,nargout=0)

            #self.MailManager.sendFinishMail()
            self.shutdown()
        except:
            print('ERROR!')
            #self.MailManager.sendErrorMail()
            self.shutdown()
        self.PumpInterface.purge()

        
if __name__ == '__main__':
    """ Tests the InputManager by starting it and printing all
        the admin values and ResTimes """
    CMQueue = Queue() # Queue Controller -> Measurements
    MCQueue = Queue() # Queue Measurements -> Controller
    test1 = InputManager()
    test2 = Measurements(CMQueue,MCQueue,test1)
    #test2.setVelocity(test2.ListExperiments[0])
    #test2.outputCSV(test2.ListExperiments[0])
