
import traceback
from LowLevelWidgetsPackage.defaultConfigForWidgets import LOGGER
from LowLevelWidgetsPackage.Ext80pinIOCardPackage.Ext80pinIOCard import Ext80pinIOCardClass
from TestsRelatedPackage.TestTemplatePackage.TestTemplateModule import TestTemplateClass
from TestsRelatedPackage.TestOverviewPackage.TestOverviewModule import AllGroupsOverviewClass, _TestResultClass, _CommandResultClass
from TestsRelatedPackage.TestResultWidgetsPackage.DefaultTestWidgetPromptPackage.DefaultTestWidgetPromptModule import DefaultTestWidgetPromptClass
from DevicePackages.SiglentSPD3303XEPackage.siglentSPD3303XEModule import SiglentSPD3303XEClass
from DevicePackages.GwinstekGPP4323Package.GwinstekGPP4323Module import GwinstekGPP4323Class
from DevicePackages.ArrigoPackage.ArrigoModule import ArrigoClass
from DevicePackages.ControlBoardPackage.ControlBoardModule import ControlBoardClass
from mainGUI import Ui_MainWindow
from PyQt6 import QtWidgets,QtCore,QtGui
import datetime
import yaml
import sys
from TestsRelatedPackage.TestParserPackage.TestParserModule import TestParserClass
import matplotlib
matplotlib.use('QtAgg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from matplotlib.axes import Axes
import numpy as np


class MplCanvas(FigureCanvasQTAgg):
    signal_change_test_result = QtCore.pyqtSignal(str,str,str)
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes:Axes = self.fig.add_subplot(111)
        super(MplCanvas, self).__init__(self.fig)
        self.number_of_y_ticks = 0
        self.number_of_x_ticks = 0
        self._custom_x_ticsk_flag = 0
        self._custom_y_ticsk_flag = 0
        self.y1_marker = "-b"

    def makeInitialPlot(self):
        self.axes.plot([0,1],[0,1], self.y1_marker)
        
    def setNumberOfYTicks(self,number_of_y_ticks):
        self.number_of_y_ticks = number_of_y_ticks
        ymin, ymax = self.axes.get_ylim()

        self.axes.set_yticks(np.round(np.linspace(ymin, ymax, number_of_y_ticks), 2))
        self._custom_y_ticsk_flag =1

    def setNumberOfXTicks(self,number_of_x_ticks):
        self.number_of_x_ticks = number_of_x_ticks
        xmin, xmax = self.axes.get_xlim()
        self.axes.set_xticks(np.round(np.linspace(xmin, xmax, number_of_x_ticks), 2))
        self._custom_x_ticsk_flag = 1

    def updateGraphData(self,x,y):
        self.axes.lines
        self.axes.lines[0].set_data(x,y)
        self.axes.relim()                  # recompute the data limits
        self.axes.autoscale_view()         # automatic axis scaling
        if self._custom_x_ticsk_flag:
            self.setNumberOfXTicks(self.number_of_x_ticks)
        if self._custom_y_ticsk_flag:
            self.setNumberOfYTicks(self.number_of_y_ticks)
        self.fig.canvas.flush_events()
        
    def loadDefaults(self):
        ...

    def setTestResult(self, result):
        ...
    
    def setTestTime(self, time):
        ...


class MainApplicationClass(QtWidgets.QMainWindow):
    def __init__(self,*args,**kwargs) -> None:
        super().__init__(*args,**kwargs)
        with open("tests.yaml","r") as file:
            self.YAML_tests = yaml.load(file, Loader=yaml.FullLoader)

        with open("hardwareConfig.yaml","r") as file:
            self.YAML_hardware = yaml.load(file, Loader=yaml.FullLoader)

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.log = LOGGER("INFO")
        self.dict_of_supported_tests = {}

        #LOAD DEVICE RELATED CONTENT
        #Control Board
        self.ControlBoard = ControlBoardClass(log=self.log, config_dictionary = self.YAML_hardware["CONTROL_BOARD"])
        self.ui.device_widgets_layout.layout().replaceWidget(self.ui.placeholder_for_control_board, self.ControlBoard)
        self.dict_of_supported_tests[self.YAML_hardware["CONTROL_BOARD"]["PREFIXFORTESTS"]] = self.ControlBoard.DeviceTestsClass.doTest

        self.ui.placeholder_for_control_board.setParent(None)

        #ARRIGO
        self.Arrigo = ArrigoClass(log=self.log, config_dictionary = self.YAML_hardware["ARRIGO"])
        self.ui.device_widgets_layout.layout().replaceWidget(self.ui.pb_placeholder_for_Arrigo_control, self.Arrigo)
        self.dict_of_supported_tests[self.YAML_hardware["ARRIGO"]["PREFIXFORTESTS"]] = self.Arrigo.DeviceTestsClass.doTest

        self.ui.pb_placeholder_for_Arrigo_control.setParent(None)

        #PSU SIGLENT
        self.SiglentPSU = SiglentSPD3303XEClass(log=self.log, config_dictionary = self.YAML_hardware["SIGLENTSPD3303XE"])
        self.ui.device_widgets_layout.layout().replaceWidget(self.ui.placeholder_for_Siglent_PSU, self.SiglentPSU )
        self.ui.placeholder_for_Siglent_PSU.setParent(None)
        self.dict_of_supported_tests[self.YAML_hardware["SIGLENTSPD3303XE"]["PREFIXFORTESTS"]] = self.SiglentPSU.DeviceTestsClass.doTest

        #PSU GWINSTEK
        self.GwinstekPSU = GwinstekGPP4323Class(log=self.log, config_dictionary = self.YAML_hardware["GWINSTEKGPP4323"])
        self.ui.device_widgets_layout.layout().replaceWidget(self.ui.placeholder_for_GWINSTEK_PSU, self.GwinstekPSU )
        self.ui.placeholder_for_GWINSTEK_PSU.setParent(None)
        self.dict_of_supported_tests[self.YAML_hardware["GWINSTEKGPP4323"]["PREFIXFORTESTS"]] = self.GwinstekPSU.DeviceTestsClass.doTest

        #SCOPE
        self.SiglentScope = SiglentSPD3303XEClass(log=self.log, config_dictionary = self.YAML_hardware["SIGLENTSDS1104XE"])
        self.ui.device_widgets_layout.layout().replaceWidget(self.ui.placeholder_for_scope, self.SiglentScope )
        self.ui.placeholder_for_scope.setParent(None)
        self.dict_of_supported_tests[self.YAML_hardware["SIGLENTSDS1104XE"]["PREFIXFORTESTS"]] = self.SiglentScope.DeviceTestsClass.doTest

        #LOAD TEST RELATED CONTENT
        self.AllTests = AllGroupsOverviewClass(log=self.log, yaml_file=self.YAML_tests)
        self._loadTestWidgets()#overwrite this function to load custom widgets
        self.TestTemplateWidget = TestTemplateClass(log = self.log, AllGroupsOverviewClass=self.AllTests) 
        self.ui.tab_tests.layout().replaceWidget(self.ui.pb_placeholder_for_tests_widget,self.TestTemplateWidget)
        self.ui.pb_placeholder_for_tests_widget.setParent(None)
        #self.TestTemplateWidget.setSizePolicy  setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetFixedSize)

        #TEST WIDGET OVERVIEW SIGNALS
        self.TestTemplateWidget.signals.run_command_request.connect(self._commonRequestForTestCallback_object_str)    
        self.TestTemplateWidget.signals.run_test_request.connect(self._commonRequestForTestCallback_object_str)
        self.TestTemplateWidget.signals.run_group_request.connect(self._commonRequestForTestCallback_str_str)
        self.thread_pool = QtCore.QThreadPool()

        #TRIALS
        #self.singleIOWidget = Ext80pinIOCardClass(log = self.log)
        #self.ui.tabTrial.layout().addWidget(self.singleIOWidget)
        #self.singleIOWidget.signals.impedance_from_widget.connect(self._impedance_changed)
        #self.singleIOWidget.signals.output_from_widget.connect(self._output_changed)
        
    #Interconnection between widgets --> main app --> test parser and vice versa
    #use main function as a middleman
    @QtCore.pyqtSlot(bool,str)
    def _impedance_changed(self, impedance, sender_name):
        self.log.log(f"MainApplicationClass:  Received -> impedance changed to {impedance} from {sender_name}", "SIGNAL")

    @QtCore.pyqtSlot(bool,str)
    def _output_changed(self, output, sender_name):
        self.log.log(f"MainApplicationClass:  Received -> Output changed to {output} from {sender_name}", "SIGNAL")


    @QtCore.pyqtSlot(str, str)
    def _commonRequestForTestCallback_str_str(self, group:str,source:str):
        """ if widget asks for test to be run -> this function is called Test can be group name, test or specific test
        This funtion runs all tests in a thread and set Signals to update the GUI"""
        self.log.log("MainApplicationClass:  RECIEVED REQUEST FOR GROUP", "DEBUG")
        
        worker = TestParserClass(log = self.log, function_dictionary= self.dict_of_supported_tests, AllGroups= self.AllTests, test = group)
        self._connectWorkerSignals(worker)
        self.thread_pool.start(worker)

    @QtCore.pyqtSlot(object, str)
    def _commonRequestForTestCallback_object_str(self, test:_TestResultClass|_TestResultClass,source:str):
        """ if widget asks for test to be run -> this function is called Test can be group name, test or specific test
        This funtion runs all tests in a thread and set Signals to update the GUI"""
        self.log.log("MainApplicationClass: RECIEVED REQUEST FOR TEST", "DEBUG")
        worker = TestParserClass(log = self.log, function_dictionary= self.dict_of_supported_tests, AllGroups= self.AllTests, test = test)
        self._connectWorkerSignals(worker)
        self.thread_pool.start(worker)

        

    def _connectWorkerSignals(self,worker:TestParserClass):
        worker.signals.signal_command_started.connect(self._CommandStarted)
        worker.signals.signal_command_finished.connect(self._CommandFinished)
        worker.signals.signal_test_started.connect(self._testStarted)
        worker.signals.signal_test_finished.connect(self._testFinished)
        worker.signals.signal_test_group_started.connect(self._groupTestStarted)
        worker.signals.signal_test_group_finished.connect(self._groupTestFinished)

        
    
    #WIDGETS SIGNALS UPDATE
    @QtCore.pyqtSlot(object, str)
    def _CommandStarted(self, command:_CommandResultClass, soruce:str):
        self.log.log(f"MainApplicationClass: RECIEVED Information that SPECIFIC TEST started: {command.command} from {soruce}", "SIGNAL")
        self.TestTemplateWidget.signals.run_command_started_response.emit(command, "MAINAPP")

    @QtCore.pyqtSlot(object, str)
    def _CommandFinished(self, command:_CommandResultClass, source:str):
        self.log.log(f"MainApplicationClass: RECIEVED Information that SPECIFIC TEST finished: {command.command} from {source}", "SIGNAL")
        self.TestTemplateWidget.signals.run_command_finished_response.emit(command, "MAINAPP")

    @QtCore.pyqtSlot(object, str)
    def _testStarted(self, test:_TestResultClass, source:str):
        self.log.log(f"MainApplicationClass: RECIEVED Information that TEST started: {test.name} from {source}", "SIGNAL")
        self.TestTemplateWidget.signals.run_test_started_response.emit(test, "MAINAPP")
    
    @QtCore.pyqtSlot(object, str)
    def _testFinished(self, test:_TestResultClass, source:str):
        self.log.log(f"MainApplicationClass: RECIEVED Information that TEST finished: {test.name} from {source}", "SIGNAL")
        self.TestTemplateWidget.signals.run_test_finished_response.emit(test, "MAINAPP")


    @QtCore.pyqtSlot(str, str)
    def _groupTestStarted(self, group:str, source:str):
        self.log.log(f"MainApplicationClass: Group {group} started -> SIGNAL IS NOT CONNECTED, source: {source}", "SIGNAL")

    @QtCore.pyqtSlot(str, str)
    def _groupTestFinished(self, group:str, source:str):
        self.log.log(f"MainApplicationClass: RECIEVED Information that GROUP finished: {group} from {source}", "SIGNAL")
        self.TestTemplateWidget.signals.run_group_finished_response.emit(group, "MAINAPP")

    
    def _loadTestWidgets(self):
        """In this function you can overwrite the default test widget with your own custom widget"""
        for keys in self.AllTests.groups_dict:
            for tests in self.AllTests.groups_dict[keys].tests: #for each test
                for widgets in self.YAML_tests["WIDGETS"]: #look if it has its own widget
                    if tests.name == widgets["TEST"]: #if found smthing
                        match widgets["WIDGET"]: #if widget name is:
                            case "GRAPH":
                                self.log.log(f"MainApplicationClass: Loading custom widget for test {tests.name}", "INFO")
                                widget_plot = MplCanvas()
                                
                                widget_plot.axes.grid(which="both")
                                widget_plot.axes.set_title(widgets["TITLE"])
                                widget_plot.axes.set_xlabel(widgets["X_LABEL"])
                                widget_plot.axes.set_ylabel(widgets["Y_LEFT_LABEL"])
                                
                                if "NUMBER:OF_X_TICKS" in widgets:
                                    widget_plot.setNumberOfXTicks(int(widgets["NUMBER_OF_X_TICKS"]))
                                if "NUMBER:OF_Y_TICKS" in widgets:
                                    widget_plot.setNumberOfYTicks(int(widgets["NUMBER_OF_Y_TICKS"]))

                                if "MARKER_Y1_LEFT" in widgets:
                                    widget_plot.y1_marker = widgets["MARKER_Y1_LEFT"]

                                tests.result_widget = widget_plot
                                tests.result_widget.makeInitialPlot()
                            
                            case "DEFAULT":
                                self.log.log(f"MainApplicationClass: Loading default for test {tests.name}", "INFO")
                                tests.result_widget = DefaultTestWidgetPromptClass(tests, self.log)
                                tests.result_widget.ui.la_promt_label.setText(widgets["TITLE"])
                            
                            case _:
                                self.log.log(f"MainApplicationClass: TestWidget not found -> Loading default for test {tests.name}", "WARNING")
                                tests.result_widget = DefaultTestWidgetPromptClass(tests, self.log)    
                        break           
                else: #if no widget is found load default widget
                    self.log.log(f"MainApplicationClass: TestWidget not found -> Loading default for test {tests.name}", "WARNING")
                    tests.result_widget = DefaultTestWidgetPromptClass(tests, self.log)
                    #tests.result_widget.signal_change_test_result.connect(self._changeTestResultSignalCallback)




    def _unsuccesfullClose(self, message):
        self.log.log(message, "CRITICAL")
        PopUp = QtWidgets.QMessageBox(text = message)
        PopUp.exec()

    def closeEvent(self, event) -> None:
        fault_flag = False
        message = ""
        try:
            if self.SiglentPSU.ConnectionClass.config_dictionary["COMMUNICATION"] == "VISA":
                self.SiglentPSU.ConnectionClass._visa_instrument.timeout = 50
        except Exception as e:
            self.log.log(f"CLOSING EVENT: {traceback.format_exc()}", "CRITICAL")
            fault_flag = True
            message += str(e) + "\n"


        status,result = self.SiglentPSU.DeviceTestsClass.doTest("CH1 SET OUTPUT OFF") 
        if status == False:
            message += "Could not turn off CH1 on PSU\n"
            fault_flag = True
        status,result = self.SiglentPSU.DeviceTestsClass.doTest("CH2 SET OUTPUT OFF") 
        if status == False:
            message += "Could not turn off CH2 on PSU\n"
            fault_flag = True
        
        status,result = self.SiglentPSU.DeviceTestsClass.doTest("CH3 SET OUTPUT OFF") 
        if status == False:
            message += "Could not turn off CH2 on PSU\n"
            fault_flag = True

        if fault_flag:
            self._unsuccesfullClose(f"{message}\n\nPlease Check the power supply manualy" )

        self.SiglentScope.ConnectionClass.disconnect()
        self.Arrigo.ConnectionClass.disconnect()
        self.SiglentPSU.ConnectionClass.disconnect()        
        event.accept()
        
if __name__ == "__main__":
    try:
        app = QtWidgets.QApplication(sys.argv)    
        with open("error.txt", "a") as f:
            window = MainApplicationClass()
            window.show()
            app.exec()
    except:
        traceback.print_exc()
        with open("error.txt", "w") as f:
            f.write(f"\n\n\nERROR HAPPEND AT TIME: {datetime.datetime.now()}\n")
            f.write(traceback.format_exc())








    
