import PyQt5.QtWidgets as Qt# QApplication, QWidget, QMainWindow, QPushButton, QHBoxLayout
import PyQt5.QtGui as QtGui
import PyQt5.QtCore as QtCore
import logging
import sys
from os import path
import argparse
from pySRSLockin.driver import SRS_Lockin
from pySRSLockin.plots import PlotObject
from pySRSLockin.abstract_interface import abstract_interface

graphics_dir = path.join(path.dirname(__file__), 'graphics')

class interface(abstract_interface):
    output = {'X':0,'Y':0,'mag':0,'theta':0} #We define this also as class variable, to make it possible to see which data is produced by this interface without having to create an object

    def __init__(self, **kwargs):
        self.output = {'X':0,'Y':0,'mag':0,'theta':0} #We define this also as class variable, to make it possible to see which data is produced by this interface without having to create an object
        self.continuous_read = False #When this is set to True, the data from device are acquired continuosly at the rate set by self.refresh_time
        self.refresh_time = 0.2 #default refresh rate, in seconds
        self.stored_data = {'X':[],'Y':[],'mag':[],'theta':[]}  # List used to store recorded data
        self.plot_window = None # QWidget object of the widget (i.e. floating window) that will contain the plot
        self.plot_object = None # PlotObject object of the plot where self.store_powers is plotted
        self.connected_device_name = ''
        
        self.instrument = SRS_Lockin() 
        super().__init__(**kwargs)

    ### Functions to create GUI

    def create_gui(self,parent,plot=False): 
        '''
            parent, QWidget        
                a QWidget (or QMainWindow) object that will be the parent for the gui of this device.
                When this script is ran on its own (i.e. not within another gui) self.mainwindow and self.parent are actually the same object
            plot, boolean
                If set true, the GUI also generates a plot object (and a button to show/hide the plot) to plot the content of the self.stored_data object
        '''
        super().create_gui(parent)
        self.widgets_enabled_when_connected = []     #The widgets in this list will only be enabled when the interface has succesfully connected to a powermeter
        self.widgets_enabled_when_disconnected = []  #The widgets in this list will only be enabled when the interface is not connected to a powermeter
        
        hbox1 = Qt.QHBoxLayout()
        self.label_DeviceList = Qt.QLabel("Devices: ")
        self.combo_Devices = Qt.QComboBox()
        #self.combo_Devices.setFixedWidth(300)
        self.button_RefreshDeviceList = Qt.QPushButton("")
        self.button_RefreshDeviceList.setIcon(QtGui.QIcon(path.join(graphics_dir,'refresh.png')))
        self.button_RefreshDeviceList.clicked.connect(self.click_button_refresh_list_devices)
        hbox1.addWidget(self.label_DeviceList)
        hbox1.addWidget(self.combo_Devices,stretch=1)
        hbox1.addWidget(self.button_RefreshDeviceList)
        #hbox1.addStretch(1)

        hbox2 = Qt.QHBoxLayout()
        self.button_ConnectDevice = Qt.QPushButton("Connect")
        self.button_ConnectDevice.clicked.connect(self.click_button_connect_disconnect)
        self.button_SetAutoScale = Qt.QPushButton("Set Auto Scale")
        self.button_SetAutoScale.clicked.connect(self.click_button_set_auto_scale)
        self.label_TimeConstants = Qt.QLabel("Time constant (s): ")
        self.combo_TimeConstants = Qt.QComboBox()
        self.combo_TimeConstants.activated.connect(self.click_combo_time_constants)

        self.label_Sensitivities = Qt.QLabel("Sensitivity (V): ")
        self.combo_Sensitivities = Qt.QComboBox()
        self.combo_Sensitivities.activated.connect(self.click_combo_sensitivities)

        hbox2.addWidget(self.button_ConnectDevice)
        hbox2.addWidget(self.button_SetAutoScale)
        hbox2.addWidget(self.label_TimeConstants)
        hbox2.addWidget(self.combo_TimeConstants,stretch=1)
        hbox2.addWidget(self.label_Sensitivities)
        hbox2.addWidget(self.combo_Sensitivities,stretch=1)
        #hbox2.addStretch(1)

        hbox3 = Qt.QHBoxLayout()
        hbox3_vbox1 = Qt.QVBoxLayout()
        
        self.button_StartPauseReading = Qt.QPushButton("")
        self.button_StartPauseReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'play.png')))
        self.button_StartPauseReading.setToolTip('Start or pause the reading from the device. The previous data points are not discarded when pausing.') 
        self.button_StartPauseReading.clicked.connect(self.click_button_StartPauseReading)
        if plot:
            self.button_StopReading = Qt.QPushButton("")
            self.button_StopReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'stop.png')))
            self.button_StopReading.setToolTip('Stop the reading from the device. All previous data points are discarded.') 
            self.button_StopReading.clicked.connect(self.click_button_StopReading)
        else:
            self.button_StopReading = None
        hbox3_vbox1_hbox1 = Qt.QHBoxLayout()
        hbox3_vbox1_hbox1.addWidget(self.button_StartPauseReading)  
        if plot:
            hbox3_vbox1_hbox1.addWidget(self.button_StopReading)
        
        
        self.label_RefreshTime = Qt.QLabel("Refresh time (s): ")
        self.label_RefreshTime.setToolTip('Specifies how often data is read from the device (Minimum value = 0.001 s).') 
        self.edit_RefreshTime  = Qt.QLineEdit()
        self.edit_RefreshTime.setText(f"{self.refresh_time:.3f}")
        self.edit_RefreshTime.setToolTip('Specifies how often data is read from the device (Minimum value = 0.001 s).') 
        self.edit_RefreshTime.returnPressed.connect(self.press_enter_refresh_time)
        self.edit_RefreshTime.setAlignment(QtCore.Qt.AlignRight)
        self.edit_RefreshTime.setMaximumWidth(50)
        hbox3_vbox1_hbox2 = Qt.QHBoxLayout()
        hbox3_vbox1_hbox2.addWidget(self.label_RefreshTime)  
        hbox3_vbox1_hbox2.addWidget(self.edit_RefreshTime,stretch=1)

        hbox3_vbox1.addLayout(hbox3_vbox1_hbox1)
        hbox3_vbox1.addLayout(hbox3_vbox1_hbox2)


        fontLabel = QtGui.QFont("Times", 10,QtGui.QFont.Bold)
        fontEdit = QtGui.QFont("Times", 10,QtGui.QFont.Bold)

        self.label_Output = dict()    
        self.edit_Output = dict() 
        self.widget_Output = dict()     
        for k in self.output.keys():		
            self.label_Output[k] = Qt.QLabel(f"{k}: ")
            self.label_Output[k].setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
            self.label_Output[k].setFont(fontLabel)
            self.edit_Output[k] = Qt.QLineEdit()
            self.edit_Output[k].setFont(fontEdit)
            self.edit_Output[k].setMinimumWidth(60)
            self.edit_Output[k].setAlignment(QtCore.Qt.AlignRight)
            self.edit_Output[k].setReadOnly(True)
            hbox_temp = Qt.QHBoxLayout()
            hbox_temp.addWidget(self.label_Output[k])
            hbox_temp.addWidget(self.edit_Output[k],stretch=1)
            self.widget_Output[k] = Qt.QWidget()
            self.widget_Output[k].setLayout(hbox_temp)

        self.layout_ContainerOutputs = Qt.QGridLayout()
        self.layout_ContainerOutputs.addWidget(self.widget_Output['X'], 0, 0)
        self.layout_ContainerOutputs.addWidget(self.widget_Output['Y'], 1, 0)
        self.layout_ContainerOutputs.addWidget(self.widget_Output['mag'], 0, 1)
        self.layout_ContainerOutputs.addWidget(self.widget_Output['theta'], 1, 1)
        self.layout_ContainerOutputs.setSpacing(0)
        self.layout_ContainerOutputs.setContentsMargins(0, 0, 0, 0)
        self.widget_ContainerOutputs = Qt.QWidget()
        self.widget_ContainerOutputs.setLayout(self.layout_ContainerOutputs)
        #self.button_ShowHidePlot = Qt.QPushButton("Show/Hide Plot")
        #self.button_StopReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'stop.png')))
        #self.button_ShowHidePlot.setToolTip('Show/Hide Plot.') 
        #self.button_ShowHidePlot.clicked.connect(self.click_button_ShowHidePlot)

        hbox3.addLayout(hbox3_vbox1)
        #hbox3.addStretch(1)
        hbox3.addWidget(self.widget_ContainerOutputs,stretch=1)
        #hbox3.addWidget(self.button_ShowHidePlot)
        #hbox3.addStretch(1)
        #hbox3.setAlignment(QtCore.Qt.AlignTop)
                
        vbox = Qt.QVBoxLayout()
        vbox.addLayout(hbox1)  
        vbox.addLayout(hbox2)  
        vbox.addLayout(hbox3)  
        vbox.addStretch(1)

        self.parent.setLayout(vbox) #This line makes sure that all widgest defined so far are assigned to the widget defines in self.parent
        
        self.parent.resize(self.parent.minimumSize())

        self.widgets_enabled_when_connected = [self.button_SetAutoScale, 
                                               self.combo_TimeConstants , 
                                               self.label_Sensitivities,
                                               self.combo_Sensitivities,
                                               self.button_StartPauseReading,
                                               self.button_StopReading]

        self.widgets_enabled_when_disconnected = [self.combo_Devices , 
                                                  self.button_RefreshDeviceList]

        self.click_button_refresh_list_devices()    #By calling this method, as soon as the gui is created we also look for devices
        self.set_disconnected_state()               #When GUI is created, all widgets are set to the "Disconnected" state
        return self

    def create_plot(self):
        '''
        This function creates an additional (separated) window with a pyqtgraph object, which plots the contents of self.stored_powers
        '''
        self.plot_window = Qt.QWidget() #This is the widget that will contain the plot. Since it does not have a parent, the plot will be in a floating (separated) window
        self.plot_object = PlotObject(self.app, self.mainwindow, self.plot_window)
        styles = {"color": "#fff", "font-size": "20px"}
        self.plot_object.graphWidget.setLabel("left", "Power", **styles)
        self.plot_object.graphWidget.setLabel("bottom", "Acqusition #", **styles)
        self.plot_window.setWindowTitle(f"Powermeter: {self.connected_device_name}")
        self.plot_window.show()
        
### END functions to create GUI

### GUI Events Functions

    def click_button_refresh_list_devices(self):
        '''
        Get a list of all device connected to the computer, by using the method ListDevices(). For each device we obtain its identity and its address.
        For each device, we create the string "identity -->  address" and we add the string to the corresponding combobox
        '''
        #First we empty the combobox
        self.combo_Devices.clear()
        
        #Then we read the list of devices
        self.logger.info(f"Looking for devices...") 
        list_valid_devices = self.instrument.list_devices()
        if(len(list_valid_devices)>0):
            list_IDNs_and_devices = [dev[1] + " --> " + dev[0] for dev in list_valid_devices] 
            #list_IDNs_and_devices = [i for i in list_IDN] 
            self.combo_Devices.addItems(list_IDNs_and_devices)  
        self.logger.info(f"Found {len(list_valid_devices)} devices.") 

    def click_button_connect_disconnect(self):
        ''' 
        This function establishes the connection to the device selected in the combobox list, or disconnects the device, depending on the value of the variable self.instrument.connected .   
        If self.instrument.connected == False, then the device is not connected, and we attempt connection
        If self.instrument.connected == True, then the device is connected, and we attempt disconnection
        '''
        
        if(self.instrument.connected == False): # We attenpt connection       
            self.connect_device()
        elif(self.instrument.connected == True): # We attenpt disconnection
            self.disconnect_device()
            
    def click_button_set_auto_scale(self):
        ID = self.instrument.set_auto_scale()
        if ID==0:
            self.logger.error(f"It was not possible to set 'auto scale'. This device model might not support it.")
        else:
            self.logger.info(f"'auto scale' set correctly.")

    def click_combo_time_constants(self):
        self.set_time_constant(self.combo_TimeConstants.currentIndex())

    def click_combo_sensitivities(self):
        self.set_sensitivity(self.combo_Sensitivities.currentIndex())
       
    def click_button_StartPauseReading(self): 
        if(self.continuous_read == False):
            self.start_reading()
        elif (self.continuous_read == True):
            self.pause_reading()
        return

    def click_button_StopReading(self):
        self.stop_reading()

    def press_enter_refresh_time(self):
        refresh_time = self.edit_RefreshTime.text()
        try: 
            refresh_time = float(refresh_time)
            if self.refresh_time == refresh_time: #in this case the number in the refresh time edit box is the same as the refresh time currently stored
                return True
        except ValueError:
            self.logger.error(f"The refresh time must be a valid number.")
            self.edit_RefreshTime.setText(f"{self.refresh_time:.3f}")
            return False
        if refresh_time < 0.001:
            self.logger.error(f"The refresh time must be positive and >= 1ms.")
            self.edit_RefreshTime.setText(f"{self.refresh_time:.3f}")
            return False
        self.logger.info(f"The refresh time is now {refresh_time} s.")
        self.refresh_time = refresh_time
        self.edit_RefreshTime.setText(f"{self.refresh_time:.3f}")
        return True

    def click_button_ShowHidePlot(self):
        self.plot_window.setHidden(not self.plot_window.isHidden())
        #self.plot_frame.activate()
        #self.parent.resize(self.parent.minimumSizeHint())

### END GUI Events Functions

### Functions to talk with low-level driver

    def connect_device(self):
        device_full_name = self.combo_Devices.currentText() # Get the device name from the combobox
        if(device_full_name==''): # Check  that the name is not empty
            self.logger.error("No valid device has been selected")
            return
        self.set_connecting_state()
        device_name = device_full_name.split(' --> ')[1].lstrip() # We extract the device address from the device name
        self.logger.info(f"Connecting to device {device_name}...")
        try:
            (Msg,ID) = self.instrument.connect_device(device_name) # Try to connect by using the method ConnectDevice of the device object
        except Exception as e:
            self.logger.error(f"Some error occurred: {e}")
            return
        if(ID==1):  #If connection was successful
            self.logger.info(f"Connected to device {device_name}.")
            self.connected_device_name = device_name
            self.set_connected_state()
            self.populate_combo_time_constants()
            self.read_time_constant()
            self.populate_combo_sensitivities()
            self.read_sensitivity()
            self.start_reading()
        else: #If connection was not successful
            self.logger.error(f"Error: {Msg}")
            self.set_disconnected_state()

    def disconnect_device(self):
        self.logger.info(f"Disconnecting from device {self.connected_device_name}...")
        try:
            (Msg,ID) = self.instrument.disconnect_device()
        except Exception as e:
            self.logger.error(f"Some error occurred: {e}")
            return
        if(ID==1): # If disconnection was successful
            self.logger.info(f"Disconnected from device {self.connected_device_name}.")
            self.continuous_read = 0 # We set this variable to 0 so that the continuous reading from the device will stop
            self.set_disconnected_state()
        else: #If disconnection was not successful
            self.logger.error(f"Error: {Msg}")

    def set_disconnected_state(self):
        self.disable_widget(self.widgets_enabled_when_connected)
        self.enable_widget(self.widgets_enabled_when_disconnected)
        self.button_ConnectDevice.setText("Connect")

    def set_connecting_state(self):
        self.disable_widget(self.widgets_enabled_when_connected)
        self.enable_widget(self.widgets_enabled_when_disconnected)
        self.button_ConnectDevice.setText("Connecting...")

    def set_connected_state(self):
        self.enable_widget(self.widgets_enabled_when_connected)
        self.disable_widget(self.widgets_enabled_when_disconnected)
        self.button_ConnectDevice.setText("Disconnect")
        #self.read_wavelength()
        #self.read_status_power_autorange()
        #self.read_power_range()
        #If a self.plot_object was created, update window title with device name and vertical axis of plot with current Power units
        if self.plot_object: 
            #self.plot_object.graphWidget.setLabel("left", f"Power [{self.instrument._power_units}]")
            self.plot_window.setWindowTitle(f"Powermeter: {self.connected_device_name}")

    def set_pause_state(self):
        self.button_StartPauseReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'play.png')))
    def set_reading_state(self):
        self.button_StartPauseReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'pause.png')))
    def set_stopped_state(self):    
        self.button_StartPauseReading.setIcon(QtGui.QIcon(path.join(graphics_dir,'play.png')))

    def populate_combo_time_constants(self):
        self.logger.info(f"Reading all possible values of time constants from device...")
        self.time_constants = self.instrument.time_constants
        self.combo_TimeConstants.clear() #First we empty the combobox
        if len(self.time_constants)>0:
                self.combo_TimeConstants.addItems([str(tc) for tc in self.time_constants])  
        self.logger.info(f"The device supports {len(self.time_constants)} different time constants.") 
        
    def populate_combo_sensitivities(self):
        self.logger.info(f"Reading all possible values of sensitivities from device...")
        self.sensitivities = self.instrument.sensitivities
        self.combo_Sensitivities.clear() #First we empty the combobox
        if len(self.sensitivities)>0:
                self.combo_Sensitivities.addItems([str(s) for s in self.sensitivities])  
        self.logger.info(f"The device supports {len(self.sensitivities)} different sensitivities.") 

    def read_time_constant(self):
        self.logger.info(f"Reading current time constant from device {self.connected_device_name}...") 
        self.time_constant = self.instrument.time_constant
        if self.time_constant == None:
            self.logger.error(f"An error occurred while reading the time constant from this device.")
            return
        self.combo_TimeConstants.setCurrentIndex(self.time_constant)
        self.logger.info(f"Current time constant is {self.time_constants[self.time_constant]} (index={self.time_constant})...") 
        
    def set_time_constant(self,tc): 
        if tc == self.time_constant:
            return
        self.logger.info(f"Setting time constant to {self.time_constants[tc]}  (index={tc})...")
        try: 
            self.instrument.time_constant = tc #set time constant
            tcnew = self.instrument.time_constant #read again time constant to confirm that it changed
            if tcnew == tc:
                self.logger.info(f"Time constant changed to {self.time_constants[tc]} (old value was {self.time_constants[self.time_constant]}).")
                self.time_constant = tc
            else:
                self.logger.error(f"The driver did not raise any error, but it was not possible to change the time constant.")
        except Exception as e:
            self.logger.error(f"An error occurred while setting the time constant: {e}")
            return False
        return True
    
    def read_sensitivity(self):
        self.logger.info(f"Reading current sensitivity from device {self.connected_device_name}...") 
        self.sensitivity = self.instrument.sensitivity
        if self.sensitivity == None:
            self.logger.error(f"An error occurred while reading the sensitivity from this device.")
            return
        self.combo_Sensitivities.setCurrentIndex(self.sensitivity)
        self.logger.info(f"Current sensitivity is {self.sensitivities[self.sensitivity]} (index={self.sensitivity})...") 
        
    def set_sensitivity(self,s): 
        if s == self.sensitivity:
            return
        self.logger.info(f"Setting sensitivity to {self.sensitivities[s]}  (index={s})...")
        try: 
            self.instrument.sensitivity = s #set sensitivity
            snew = self.instrument.sensitivity #read again sensitivity to confirm that it changed
            if snew == s:
                self.logger.info(f"Sensitivity changed to {self.sensitivities[s]} (old value was {self.sensitivities[self.sensitivity]}).")
                self.sensitivity = s
            else:
                self.logger.error(f"The driver did not raise any error, but it was not possible to change the sensitivity.")
        except Exception as e:
            self.logger.error(f"An error occurred while setting the sensitivity: {e}")
            return False
        return True
   
    def start_reading(self):
        if(self.instrument.connected == False):
            self.logger.error(f"No device is connected.")
            return
        self.logger.info(f"Updating refresh time before starting reading...")
        if not(self.press_enter_refresh_time()): #read the current value in the refresh_time textbox, and validates it. The function returns True/False if refresh_time was valid
            return
        
        self.set_reading_state() # Change some widgets

        self.continuous_read = True #Until this variable is set to True, the function Update will be repeated continuosly 
        self.logger.info(f"Starting reading from device {self.connected_device_name}...")
        # Call the function self.Update(), which will do some suff (read data from device and store it in a global variable) and then call itself continuosly until the variable self.ContinuousRead is set back to 0
        self.update()
        return
 
    def pause_reading(self):
        #Sets self.ContinuousRead to 0 (this will force the function Update() to stop calling itself)
        self.continuous_read = False
        self.logger.info(f"Paused reading from device {self.connected_device_name}.")
        self.set_pause_state() # Change some widgets
        return

    def stop_reading(self):
        #Sets self.ContinuousRead to 0 (this will force the function Update() to stop calling itself)
        self.continuous_read = False
        self.stored_powers = []
        self.update() #We call one more time the self.Update() function to make sure plots is cleared. Since self.continuous_read is already set to False, Update() will not acquire data anymore
        self.logger.info(f"Stopped reading from device {self.connected_device_name}. All stored data have been deleted.")
        self.set_stopped_state() # Change some widgets
        # ...
        return
        
    def update(self):
        '''
        This routine reads continuosly the data from the device and store the values
        If we are continuosly acquiring (i.e. if self.ContinuousRead = 1) then:
            1) Reads data from device object and stores it in the self.output dictionary
            2) Update the value of the corresponding edits widgets in the GUI
            3) Call itself after a time given by self.refresh_time
        '''
        #self.plot_object.data.setData(list(range(1, len(self.stored_powers)+1)), self.stored_powers) #This line is executed even when self.continuous_read == False, to make sure that plot gets cleared when user press the stop button
        if(self.continuous_read == True):
            (X,Y,mag,theta) = self.instrument.X,self.instrument.Y,self.instrument.mag,self.instrument.theta
            self.output['X'] = X
            self.output['Y'] = Y
            self.output['mag'] = mag
            self.output['theta'] = theta
            for k in self.output.keys():	
                self.stored_data[k].append(self.output[k])

            super().update()

            for k in self.output.keys():
                try:
                    self.edit_Output[k].setText(f"{self.output[k]:.2e}")
                except:
                    self.edit_Output[k].setText(f"nan")
            QtCore.QTimer.singleShot(int(self.refresh_time*1e3), self.update)
           
        return

    def close(self):
        if (self.instrument.connected == True):
            self.disconnect_device()
    

class MainWindow(Qt.QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(__package__)
        # Set the central widget of the Window.
        # self.setCentralWidget(self.container)
    def closeEvent(self, event):
        if self.child:
            self.child.close()

def main():
    parser = argparse.ArgumentParser(description = "",epilog = "")
    parser.add_argument("-s", "--decrease_verbose", help="Decrease verbosity.", action="store_true")
    args = parser.parse_args()

    app = Qt.QApplication(sys.argv)
    window = MainWindow()
    Interface = interface(app=app,mainwindow=window) #In this case window is both the MainWindow and the parent of the gui
    Interface.verbose = not(args.decrease_verbose)
    app.aboutToQuit.connect(Interface.close) 
    Interface.create_gui(window,plot=False)
    #Interface.set_trigger(lambda : print('test'),delay=1000)
    window.show()
    app.exec()# Start the event loop.

if __name__ == '__main__':
    main()
