import os
import time
import traceback
import datetime
from PyQt6 import QtWidgets
import PyQt6.QtCore as QtCore
import DevicePackages.defaultConfigForDevices as defaultConfigForDevices
from DevicePackages.ControlBoardPackage.ControlBoardConnectionModule import ConnectionClass
from DevicePackages.ControlBoardPackage.ControlBoardTestsModule import DeviceTestsClass
from DevicePackages.ControlBoardPackage.testClassFor80pinCardModule import AllPins
from DevicePackages.DeviceStatusModule import DeviceStatusClass
from DevicePackages.ControlBoardPackage.ControlBoardGUI import Ui_ControlBoardwidget
from LowLevelWidgetsPackage.Relay8Package.Relay8Module import Relay8Class
from LowLevelWidgetsPackage.Relay16Package.Relay16Module import Relay16Class
from LowLevelWidgetsPackage.Relay32Package.Relay32Module import Relay32Class
from LowLevelWidgetsPackage.Ext80pinIOCardPackage.Ext80pinIOCard import Ext80pinIOCardClass
from LowLevelWidgetsPackage.VisaControlPackage.visaControlModule import VisaControlClass
from LowLevelWidgetsPackage.DeviceStatusIndicatorPackage.DeviceStatusIndicatorModule import DeviceStatusIndicatorClass

class ControlBoardClass(QtWidgets.QWidget):
    def __init__(self,log:defaultConfigForDevices.LOGGER = defaultConfigForDevices.LOGGER("INFO"),\
        config_dictionary:dict = defaultConfigForDevices.defaultConfig,\
        *args, **kwargs):#args = multiple input variables, **kvargs multiple keyword variables (dictionaries, touples etc.)
    ################ INIT Function ##################
        super().__init__(*args, **kwargs)#super inherits all methods and properties from its parent  (QWIDGET classes)
        #Load UI forms creaated in QTdDesigner
        self.ui = Ui_ControlBoardwidget() #UI form generated by UI designe
        self.ui.setupUi(self) #setup UI form
        self.log = log #log class
        self.config_dictionary = config_dictionary #config dictionary
        self.log.log(f"{self.config_dictionary['DEVICENAME']} :: STARTING Control Board Class","UNNECESSARY")
        self.ui.la_device_name.setText(f"{self.config_dictionary['DEVICENAME']} CONTROL PANNEL")
        self.ConnectionClass = ConnectionClass(log=self.log,config_dictionary=self.config_dictionary)
        self.CardTestPins = AllPins(log=self.log,ConnectionClass=self.ConnectionClass)
        self.DeviceStatusClass = DeviceStatusClass(log=self.log)
        self._name = self.config_dictionary["DEVICENAME"]
        
        #DEVICE STATUS CLASS SETTINGS --------------------------------------------------
        self.DeviceStatusIndicatorWidget = DeviceStatusIndicatorClass(log=self.log,
                device_config=self.config_dictionary,
                device_status_class= self.DeviceStatusClass)
        self.ui.tab_device_status.layout().replaceWidget(self.ui.pb_widget_placeholder_device_status,self.DeviceStatusIndicatorWidget)
        self.ui.pb_widget_placeholder_device_status.deleteLater()
        self.ui.tab_device_status.layout().update()

        #change name after device status indicator widget is created
        self.DeviceStatusClass.device_name = self.config_dictionary["DEVICENAME"]

        #SIGNAL HANDLERS --------------------------------------------------
        self.ConnectionClass.signals.error_signal.connect(self._errorSignalHandler)

        if self.config_dictionary["CONNECTATSTARTUP"]:
            self.DeviceStatusClass.last_startUp_time = datetime.datetime.now()
            try:
                self._connect(ignore_wrong_setup=True)
            except Exception:
                self.log.log(f"{self.config_dictionary['DEVICENAME']} {traceback.format_exc()}","ERROR")
                self.DeviceStatusClass.last_fault_description = "Connection failed"   
                self.DeviceStatusClass.connection_status = self.DeviceStatusClass.STATE.ERROR
                self.DeviceStatusClass.test_status = self.DeviceStatusClass.STATE.ERROR
                self.DeviceStatusClass.last_fault_time = datetime.datetime.now()

            self.log.log("\n" + self.DeviceStatusClass.printStatus(),"INFO")
        self.DeviceTestsClass = DeviceTestsClass(config_dictionary=self.config_dictionary,log=self.log,ConnectionClass=self.ConnectionClass)
    
        #VISA WIDGET SETTINGS --------------------------------------------------
        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.VisaWidget = VisaControlClass(log=self.log, ConnectionClass=self.ConnectionClass,\
                DeviceStatusClass= self.DeviceStatusClass)

            self.ui.tab_visa.layout().replaceWidget(self.ui.pb_widget_placeholder_visa,self.VisaWidget)
            self.ui.pb_widget_placeholder_visa.deleteLater()
            self.ui.tab_visa.layout().update()

        
        #RELAY WIDGET SETTINGS --------------------------------------------------
        self._widget_placeholder_EXT_1 = self.ui.pb_widget_placeholder_EXT_1 #Points to the widget which will be replace by selected relay widget
        self._widget_placeholder_EXT_2 = self.ui.pb_widget_placeholder_EXT_2 #Points to the widget which will be replace by selected relay widget
        self._widget_placeholder_EXT_3 = self.ui.pb_widget_placeholder_EXT_3 #Points to the widget which will be replace by selected relay widget

        self.ui.cb_EXT1.currentTextChanged.connect(self._addRelayWidget) #relay card size selection
        self.ui.cb_EXT2.currentTextChanged.connect(self._addRelayWidget) #relay card size selection
        self.ui.cb_EXT3.currentTextChanged.connect(self._addRelayWidget) #relay card size selection

        #EXTERNAL 80IOCARD -> Connected to the 50pin connector
        self.Io80PinCard = Ext80pinIOCardClass(log=self.log)
        self.Io80PinCard.setName("80_PIN_IO_CARD_1")
        self.Io80PinCard.setCardNumber(1)
        self.ui.tab_80_pin_IO_card.layout().replaceWidget(self.ui.pb_placeholder_for_80_pin_IO_CARD, self.Io80PinCard)
        self.ui.pb_placeholder_for_80_pin_IO_CARD.deleteLater()
        self.Io80PinCard.signals.pin_changed_from_widget.connect(self._update80IoCard)
        self.Io80PinCard.signals.pin_voltage_request_from_widget.connect(self._meas80IoCardPinVoltage)
        self.Io80PinCard.signals.pin_input_request_from_widget.connect(self._meas80IoCardPinInputState)
        self.Io80PinCard.signals.measure_all_voltage_request_from_widget.connect(self._meas80IoCardAllVoltages)
        self.Io80PinCard.signals.set_dac_request_from_widget.connect(self._set80IoCardDac)
        self.Io80PinCard.signals.read_config_request_from_widget.connect(self._read80IoCardConfig)
        self.Io80PinCard.signals.measure_all_inputs_request_from_widget.connect(self._measure80IoCardAllInputs)

        #to ensure correct scaling of tabs
        self.ui.tabWidget.minimumSizeHint = self.tabMinimumSizeHint
        self.ui.tabWidget.sizeHint = self.tabSizeHint
        self.ui.tabWidget.currentChanged.connect(self.ui.tabWidget.updateGeometry)
        
    @QtCore.pyqtSlot(int,float,str)
    def _set80IoCardDac(self,card_number,voltage,sender_name):
        self.log.log(f"{self.config_dictionary['DEVICENAME']} DAC Request from {sender_name}: cardNumber:{card_number},to set {voltage}V","SIGNAL")
        self.DeviceTestsClass.doTest(f"80PINIOCARD SET DAC {voltage}")
    
    @QtCore.pyqtSlot(int,str)
    

    def _measure80IoCardAllInputs(self,card_number,sender_name):
        start_time = time.time()
        self.log.log(f"{self.config_dictionary['DEVICENAME']} MEASURE ALL INPUTS Request from {sender_name}: cardNumber:{card_number}","SIGNAL")
        self.DeviceTestsClass.doTest(f"80PINIOCARD READ MUX MUXES_1TO40 ALL")
        self.DeviceTestsClass.doTest(f"80PINIOCARD READ MUX MUXES_41TO80 ALL")
        for pins in range(80):
            if pins < 40:
                self.Io80PinCard.getPinWidget(pins).signals.input_to_widget.emit(self.CardTestPins.MULTIPLEXERS.MUXES_1TO40.getPinStateFromMultiplexerValue(pins+1), self._name)
            else:
                pins = pins - 40
                self.Io80PinCard.getPinWidget(pins+40).signals.input_to_widget.emit(self.CardTestPins.MULTIPLEXERS.MUXES_41TO80.getPinStateFromMultiplexerValue(pins+1), self._name)
        print("--- %s seconds ---" % (time.time() - start_time))

    @QtCore.pyqtSlot(int,str)
    def _read80IoCardConfig(self,card_number,sender_name):
        status,message = self.CardTestPins.askForPinStatesFromHardware()
        self.log.log(f"{self.config_dictionary['DEVICENAME']} MESSAGE FROM PINSTATES: {message}","DEBUG")
        if status:
            for pins in range(80):
                control_widget =  self.Io80PinCard.getPinWidget(pins)
                control_widget.signals.impedance_to_widget.emit(int(self.CardTestPins.list_of_All_pins[pins].IMPEDANCE.replace("LOW","0").replace("HIGH","1")), self._name)
                control_widget.signals.output_to_widget.emit(int(self.CardTestPins.list_of_All_pins[pins].OUTPUT.replace("LOW","0").replace("HIGH","1")), self._name)
        else:
            self.log.log(f"{self.config_dictionary['DEVICENAME']} ERROR IN LOADING DATA: {message}","ERROR")



    def _meas80IoCardAllVoltages(self,card_number,sender_name):
        start_time = time.time()
        self.log.log(f"{self.config_dictionary['DEVICENAME']} voltageMeasurement Request from {sender_name}: cardNumber:{card_number}","SIGNAL")
        self.CardTestPins.askForAllVoltages()
        for i in range(80):
            self.Io80PinCard.getPinWidget(i).signals.voltage_to_widget.emit(self.CardTestPins.list_of_All_pins[i].VOLTAGE, self._name)
        print("--- %s seconds ---" % (time.time() - start_time))

    @QtCore.pyqtSlot(int,int,str)
    def _meas80IoCardPinVoltage(self,card_number,pin_number,sender_name): 
        self.log.log(f"{self.config_dictionary['DEVICENAME']} voltageMeasurement Request from {sender_name}: cardNumber:{card_number}, pinNumber: {pin_number}","SIGNAL")
        self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: 80PINIOCARD MEASURE VOLTAGE {pin_number +1}", "INFO")
        status,response = self.DeviceTestsClass.doTest(f"80PINIOCARD MEASURE VOLTAGE {pin_number + 1}")
        if status != "FAIL":
            result = float(response.split(":")[-1])
            self.CardTestPins.list_of_All_pins[pin_number].fetchVoltage(result)
            self.Io80PinCard.getPinWidget(pin_number).signals.voltage_to_widget.emit(result, self._name)
        else:
            self.Io80PinCard.getPinWidget(pin_number).signals.voltage_to_widget.emit(-1, self._name)

    @QtCore.pyqtSlot(int,int,str)
    def _meas80IoCardPinInputState(self,card_number,pin_number,sender_name): 
        self.log.log(f"{self.config_dictionary['DEVICENAME']} inputState Request from {sender_name}: cardNumber:{card_number}, pinNumber: {pin_number}","SIGNAL")
        self.Io80PinCard.getPinWidget(pin_number).signals.input_to_widget.emit(1, self._name)
        self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: 80PINIOCARD READ PIN_INPUT {pin_number +1 }", "INFO")
        status,response = self.DeviceTestsClass.doTest(f"80PINIOCARD READ PIN_INPUT {pin_number + 1}")
        if status:
            result = float(response.split(":")[-1])
            self.Io80PinCard.getPinWidget(pin_number).signals.input_to_widget.emit(int(result), self._name)
        else:
            self.Io80PinCard.getPinWidget(pin_number).signals.input_to_widget.emit(-1, self._name)

    @QtCore.pyqtSlot(str,int,int,bool,bool,str) 
    def _update80IoCard(self,to_set,card_number,pin_number,output,impedance,sender_name): #toSet(OUTPUT|IMPEDANCE),cardNumber,PinNumber,output,impendance,sender_name
        self.log.log(f"{self.config_dictionary['DEVICENAME']} SIGNAL FROM {sender_name} : {card_number},{pin_number},{output},{impedance}","SIGNAL")
        if to_set == "OUTPUT":
            command = f"80PINIOCARD SET PIN {pin_number +1 } OUTPUT {str(output).replace('True','HIGH').replace('False','LOW')}"
            self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: {command}", "INFO")
            self.DeviceTestsClass.doTest(command)

        elif to_set == "IMPEDANCE":
            command = f"80PINIOCARD SET PIN {pin_number + 1} IMPEDANCE {str(impedance).replace('True','HIGH').replace('False','LOW')}"
            self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: {command}", "INFO")
            self.DeviceTestsClass.doTest(command)

        elif to_set == "BOTH":
            command = f"80PINIOCARD SET PIN {pin_number + 1} OUTPUT {str(output).replace('True','HIGH').replace('False','LOW')}"
            self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: {command}", "INFO")
            self.DeviceTestsClass.doTest(command)
            command = f"80PINIOCARD SET PIN {pin_number + 1} IMPEDANCE {str(impedance).replace('True','HIGH').replace('False','LOW')}"
            self.log.log(f"{self.config_dictionary['DEVICENAME']} RUNNING command: {command}", "INFO")
            self.DeviceTestsClass.doTest(command)


    @QtCore.pyqtSlot(str,object)
    def _errorSignalHandler(self,error_message,sender_object):
        self.log.log(f"{self.config_dictionary['DEVICENAME']}: ERROR FROM{sender_object.__class__.__name__} : {error_message}","ERROR")
        self.DeviceStatusClass.last_fault_description = error_message
        self.DeviceStatusClass._last_fault_time = datetime.datetime.now()
    
    @QtCore.pyqtSlot(list,str)
    def _relayWidgetSignal(self,signal_list:list,name:str):
        self.log.log(f"{self.config_dictionary['DEVICENAME']} Relay Widget Signal","INFO")
        self.log.log(f"{self.config_dictionary['DEVICENAME']} Name: " + str(name),"INFO")
        self.log.log(f"{self.config_dictionary['DEVICENAME']} Signal List: " + str(signal_list),"INFO")

        command = f"SETEXTPORT;{name[-1]};"
        for vaules in signal_list:
            command += str(vaules).replace("True","1").replace("False","0") + ";"
        command = command[:-1]#remove last ";"

        self.log.log(f"{self.config_dictionary['DEVICENAME']} -Writing: {command}","INFO")

        status,response = self.ConnectionClass.queryMessage(command)
        self.log.log(f"{self.config_dictionary['DEVICENAME']} -Reading: {response}","INFO")

    def _addRelayWidget(self):
        """Adds RELAY widgets to the layout"""
        if self.sender().currentText() == "RELAY 8":
            widget_selection = Relay8Class(log=self.log) #serves as reference to last selected widget at EXT PORT
        elif self.sender().currentText() == "RELAY 16":
            widget_selection = Relay16Class(log=self.log) #serves as reference to last selected widget at EXT PORT
        elif self.sender().currentText() == "RELAY 32":
            widget_selection = Relay32Class(log=self.log) #serves as reference to last selected widget at EXT PORT

        widget_selection.signals.checkbox_updated.connect(self._relayWidgetSignal) #signal to send signals from relay widgets
        #Update ext ports accordingly to selected relay widget size
        if self.sender() == self.ui.cb_EXT1:
            widget_selection.setName("EXT_1") #set name of widget. In future this will be used to identify the widget when it sends a signal
            self.ui.EXT_1_LAYOUT.layout().replaceWidget(self._widget_placeholder_EXT_1,widget_selection) #replace widget reference with new widget
            self._widget_placeholder_EXT_1.deleteLater()
            self.ui.EXT_1_LAYOUT.layout().update()
            self._widget_placeholder_EXT_1 = widget_selection #save reference to placeholder widget

        if self.sender() == self.ui.cb_EXT2:
            widget_selection.setName("EXT_2")#set name of widget. In future this will be used to identify the widget when it sends a signal
            self.ui.EXT_2_LAYOUT.layout().replaceWidget(self._widget_placeholder_EXT_2,widget_selection) 
            self._widget_placeholder_EXT_2.deleteLater()
            self.ui.EXT_2_LAYOUT.layout().update()
            self._widget_placeholder_EXT_2 = widget_selection
            

        if self.sender() == self.ui.cb_EXT3:
            widget_selection.setName("EXT_3")#set name of widget. In future this will be used to identify the widget when it sends a signal
            self.ui.EXT_3_LAYOUT.layout().replaceWidget(self._widget_placeholder_EXT_3,widget_selection)
            self._widget_placeholder_EXT_3.deleteLater()
            self.ui.EXT_3_LAYOUT.layout().update()
            self._widget_placeholder_EXT_3 = widget_selection

    def _connect(self, ignore_wrong_setup:bool = False) -> bool:
        """ little bit customized connect function from connection class"""
        error_description = ""
        error_flag = False
        status,response = self.ConnectionClass.connectionSetup()
        if status == False: #setup connection parameters
            error_description = "Connection setup failed"
            error_flag = True

        #someteimes visa cannot setup connecion succesfully, but still can be connected.
        #ignore_wrong_setup is used to ignore this error and try to connect to device anyway
        if error_flag == True and ignore_wrong_setup == False:
            self.DeviceStatusClass.last_fault_description = error_description   
            self.DeviceStatusClass.connection_status = self.DeviceStatusClass.STATE.ERROR
            self.DeviceStatusClass.test_status = self.DeviceStatusClass.STATE.ERROR
            self.DeviceStatusClass.last_fault_time = datetime.datetime.now()
            return False

        status,response = self.ConnectionClass.connect()
        if status == False: #connection was succesful
            error_description = "Connection failed"
            error_flag = True

        else: #someteimes visa cannot setup connecion succesfully, but still can be connected
            error_flag = False

        if self.ConnectionClass.blockingSelfCheck() == False:
            error_description = "Self check failed"
            error_flag = True

        if error_flag == False:
            self.DeviceStatusClass.connection_status = self.DeviceStatusClass.STATE.CONNECTED
            self.DeviceStatusClass.test_status = self.DeviceStatusClass.STATE.READY
            self.DeviceStatusClass.last_self_check_time = datetime.datetime.now()
            return True

        else:
            self.DeviceStatusClass.last_fault_description = error_description   
            self.DeviceStatusClass.connection_status = self.DeviceStatusClass.STATE.ERROR
            self.DeviceStatusClass.test_status = self.DeviceStatusClass.STATE.ERROR
            self.DeviceStatusClass.last_fault_time = datetime.datetime.now()
            return False

    def close(self) -> bool:
        self.ConnectionClass.disconnect()
        self.DeviceStatusClass.connection_status = self.DeviceStatusClass.STATE.DISCONNECTED
        self.DeviceStatusClass.test_status = self.DeviceStatusClass.STATE.UNKNOWN
        return super().close()

    #to ensure correct scaling of tabs
    def tabMinimumSizeHint(self):
        return self.ui.tabWidget.sizeHint()
    #to ensure correct scaling of tabs
    def tabSizeHint(self):
        current = self.ui.tabWidget.currentWidget()
        if not current:
            return super().sizeHint()
        return current.sizeHint()



if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = ControlBoardClass(QtWidgets.QWidget)
    window.show()
    app.exec()