
from DevicePackages.defaultConfigForDevices import LOGGER, defaultConfig
import pyvisa
import traceback
import time
from PyQt6 import QtCore
from typing import Tuple

def checkIfBussy(func):
    """If hardware is doing something, return BUSSY, else return function result -> This Function is used as decorator"""
    def wrapper(*args, **kwargs):
        #args[0] is self from Connection Class
        if args[0].busy_flag == False:
            args[0].busy_flag = True
            result = func(*args, **kwargs)
            args[0].busy_flag = False
            return result
        else:
            args[0].log.log(f"{args[0].config_dictionary['DEVICENAME']} - Hardware is bussy, try again later","ERROR")
            return "BUSSY"
    return wrapper

class signalsClass(QtCore.QObject):
    error_signal = QtCore.pyqtSignal(str,object) #whenever error ocurs in connection class, this signal is emmiterd

class ConnectionClass():
    """Connection Class provides functions for low level communication with device. It is used by Device Class to communicate with device.
    For proper working, you mus specify config_dictionary. Config dictionary is usually contained in hardwareConfig.yaml file. For example
    hardwareConfig.yaml file for Gwinstek GPP4323 Power Supply may look like this:

    GWINSTEKGPP4323:
        DEVICENAME : "GWINSTEKPSU" #this name is used in debug logs, some widgets and etc.
        COMMUNICATION: "VISA" #in case of using visa coomunication, you must specify VISA string
        #COMMUNICATION: "VIRTUAL" #device also can be used in virtual mode, in this case, all functions will return "VIRTUAL" as result
        VISASTRING : 'TCPIP0::192.168.1.60::1026::SOCKET' #VISA string for TCPIP connection
        #VISASTRING : 'ASRL9::INSTR' #VISA string for serial connection
        BAUDRATE: 115200 #Irelevant for TCIP IP..
        TIMEOUT: "1000"
        QUERYDELAY: 0.01 #automatic delay between queries
        PREFIXFORTESTS: "GWINSTEKPSU" #prefix for tests, used in test class
        SELF_CHECK_PERIOD: 5000, #provided by main function only -> not implemented yet
        SELF_CHECK_COMMAND: "*IDN?" #this command is sent to the device whenever self check is performed
        SELF_CHECK_RESPONSE: "GW Instek,GPP-4323,SN:GEW866115,V1.19" #check if this command is returned in selfcheck response
        READTERMINATION: "\n" #read termination character
        WRITETERMINATION: "\n" #write termination character
        CONNECTATSTARTUP: True #if true, connection is established at startup
        NEEDSTOBECONNECTED: False #if true, Program will not let user do any tests until connection is established
    
    This file is then parsed into dictionary, which looks like this:
    config_dict{
        "DEVICENAME" : "GWINSTEKPSU",
        "COMMUNICATION": "VISA",
        "VISASTRING" : 'TCPIP0::192.168.1.60::1026::SOCKET'
        ...
        ...
        ..
    }
    """
    def __init__(self,log:LOGGER = LOGGER("ALL"),\
        config_dictionary:dict = defaultConfig,*args, **kwargs):#args = multiple input variables, **kvargs multiple keyword variables (dictionaries, touples etc.)
    ################ INIT Function ##################
        super().__init__(*args, **kwargs)
        self.log = log
        self.signals = signalsClass()
        self.config_dictionary = config_dictionary
        self.log.log(f" {self.config_dictionary['DEVICENAME']} - STARTING CONNECTION CLASS","UNNECESSARY")
        self.log.log(f" OBJECT: - {self}","UNNECESSARY")
        self._visa_instrument = None
        self._visa_RM = None
        self.busy_flag = False #to prevent multiple attempts to communicate with hardware in the same time from another thread
        self._last_exception = "UNKNOWN"


    def _communicationNotSupported(self):
        """If communication type is not supported, log error and emit error signal"""
        self.log.log(f"{self.config_dictionary['DEVICENAME']} - Communication type not supported","ERROR")
        self._last_exception = "Communication type not supported"
        self.signals.error_signal.emit(self._last_exception,self)

    def _handleResult(self, status,result):
        if status == True or result == "VIRTUAL":
            if result == None or result == "":
                self.log.log(f"Status is True, but result: {result} is empty","WARNING")
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - Operation successful","UNNECESSARY")
        
        else:
            self.log.log(f"{self.config_dictionary['DEVICENAME']}: SENDING ERROR SIGNAL - Operation failed","ERROR")
            self._last_exception = str(result)
            self.signals.error_signal.emit(self._last_exception,self)

    def _exceptionTraceback(self, e):
        self.log.log(f"{self.config_dictionary['DEVICENAME']} - {traceback.format_exc()}","ERROR",oneline=False)
        return  str(e)

    @checkIfBussy
    def connectionSetup(self):
        """ Takes data from config_dictionary and sets up connection to device accordingly -> this function does not connect
        to device, only sets up connection, to connect to device, use connect function"""
        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - connectionSetup as VISA device","HARDWARE")
            status,result = self._visaSetup()
            self._handleResult(status, result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - connectionSetup as virtual device","WARNING")
            return True,"VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def connect(self):
        """ Connect to device according to config_dictionary -> before this function is called, connectionSetup must be called"""
        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - connect to VISA device","HARDWARE")
            status,result = self._visaConnect()
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - connection as virtual device","WARNING")
            return True, "VIRTUAL"
        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def disconnect(self):
        """disconnect from device"""
        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - disconnect from VISA device","HARDWARE")
            status,result = self._visaDisconnect()
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - disconnecting virtual device","WARNING")
            return True, "VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def sendMessage(self,command:str) -> Tuple[bool, str] :
        """Send message to device, the message is send via interface specified in config_dictionary
        
        INPUT: command: str -> message to be sent to device without termination characters,
        termination characters are added automatically according to config_dictionary
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message, which was sent to the device,
        in case that status is True and device is set to VIRTUAL mode, result always returns "VIRTUAL"
        in case that status is False, result is the error message"""

        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - sendMessage {command}","HARDWARE")
            status,result = self._visaSendMessage(command)
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - sending virtual message","WARNING")
            return True, "VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def readMessage(self) -> Tuple[bool, str]:
        """Read message from device, the message is read via interface specified in config_dictionary

        RETURNS: status :bool, result: str
        in case that status is True, result is the message, which was read from the device,
        in case that status is True and device is set to VIRTUAL mode, result always returns "VIRTUAL"
        in case that status is False, result is the error message"""

        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - read Message from VISA device","HARDWARE")
            status,result = self._visaReadMessage()
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - reading virtual message","WARNING")
            return True, "VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def queryMessage(self,command:str) -> Tuple[bool, str]:
        """Send message to device and read response, the message is send via interface specified in config_dictionary
        
        INPUT: command: str -> message to be sent to device without termination characters,
        termination characters are added automatically according to config_dictionary
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message, which was read from the device,
        in case that status is True and device is set to VIRTUAL mode, result always returns "VIRTUAL"
        in case that status is False, result is the error message"""

        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - queryMessage: {command}","HARDWARE")
            status,result = self._visaQueryMessage(command)
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - querying virtual message","WARNING")
            return True, "VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"

    @checkIfBussy
    def blockingSelfCheck(self):
        """Send SELF_CHECK_COMMAND from config_dictionary and check if response match the  SELF_CHECK_RESPONSE from config_dictionary
        This function is blocking, it waits for the response from the device
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message, which was read from the device,
        in case that status is True and device is set to VIRTUAL mode, result always returns "VIRTUAL
        in case that status is False, result is the error message"""

        if self.config_dictionary["COMMUNICATION"] == "VISA":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - blocking SelfCheck VISA device","HARDWARE")
            status,result = self._visaBlockingSelfCheck()
            self._handleResult(status,result)
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - status:{status}, result: {result}","HARDWARE")
            return status,result

        elif self.config_dictionary["COMMUNICATION"] == "VIRTUAL":
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - blocking self check virtual device","WARNING")
            return True, "VIRTUAL"

        else:
            self._communicationNotSupported()
            return False, "Not Supported"
    
    def _visaBlockingSelfCheck(self) -> Tuple[bool, str]:
        """provides blocking self check for VISA devices - see blockingSelfCheck for more info"""
        try:
            self.log.log(f"{self.config_dictionary['DEVICENAME']} - Blocking self check","UNNECESSARY")
            if self._visa_instrument == None:
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - Visa instrument not connected","ERROR")
                return False, "Visa instrument not connected"

            self._visa_instrument.clear()
            status, message = self._visaQueryMessage(self.config_dictionary["SELF_CHECK_COMMAND"])
            if message in self.config_dictionary['SELF_CHECK_RESPONSE']:
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - Blocking self check -> PASS","UNNECESSARY")
                return True, message

            else:
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - Blocking self check failed with message: {message}","ERROR")
                return False, f"Blocking self check failed with message: {message}"
        
        except Exception as e:
            return False, self._exceptionTraceback(e)

    def _visaSetup(self) -> Tuple[bool, str]:
        """Look at avaiable VISA resources and compare them with the one in the config file
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the just informative message
        in case that status is False, result is the error message"""
        self.log.log(f" {self.config_dictionary['DEVICENAME']} - Visa SETUP","INFO")
        self._visa_RM = pyvisa.ResourceManager()
        for resources in self._visa_RM.list_resources():
            if self.config_dictionary['VISASTRING'] in resources:
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - Visa resource found","INFO")
                return True, "Visa resource found"

        #Sometimes visa device is not in the list of resources, this does not necessarily mean that, you cannot connect to the device,
        #list of resources is similar to the autodiscovery devices, but it does not always work.
        #if device is not in the list you can still try to connect to it using VISA string -> Connection is done in self._visaConnect(), which is called in self.connect()
        self.log.log(f"{self.config_dictionary['DEVICENAME']} Avaiable resources: {self._visa_RM.list_resources()}","INFO")
        self.log.log(f"{self.config_dictionary['DEVICENAME']} If the resource is not found you can still try to connect!","WARNING")
        return False , f"Resource {self.config_dictionary['VISASTRING']} is not found. TCP/IP connections are sometimes not listed.. You can still try to connect!"
        
    def _visaConnect(self) -> Tuple[bool, str]:
        """Try to connect to the visa resource with settings from self.config_dictionary
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the just informative message
        in case that status is False, result is the error message"""
        try:
            if self._visa_RM == None: # in case that someone forget to call self._visaSetup() or device was not found try to setup connection once again
                self._visaSetup()

            self.log.log(f"{self.config_dictionary['DEVICENAME']} - Visa Connecting to {self.config_dictionary['VISASTRING']}","HARDWARE")
            self._visa_instrument:pyvisa.Resource = self._visa_RM.open_resource(self.config_dictionary['VISASTRING'])

            if "BAUDRATE" not in self.config_dictionary: #Connection is propably TCP/IP or someone forgot to specify BAUDRATE in config file
                self._visa_instrument.baud_rate = 115200 #try to use this baudrate -> ignored in TCP/IP connection
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - Baudrate not specified in config file, using default 115200","WARNING")
                self.log.log(f"{self.config_dictionary['DEVICENAME']} - If Visa String is not using baud rate, this setting will be ignored","WARNING")
            else:
                self._visa_instrument.baud_rate = self.config_dictionary['BAUDRATE']
            self._visa_instrument.query_delay = float(self.config_dictionary['QUERYDELAY']) #automatic delay between queries
            self._visa_instrument.read_termination = self.config_dictionary['READTERMINATION'] #termination character used in queries and readings
            self._visa_instrument.write_termination = self.config_dictionary['WRITETERMINATION'] #termination character used in writing
            self._visa_instrument.timeout = float(self.config_dictionary['TIMEOUT'])
            time.sleep(0.1)#give instrument some time
            self._visa_instrument.clear()#clear buffers
            return True , "Visa connected"
                
        except Exception as e:
            return False, self._exceptionTraceback(e)


    def _visaDisconnect(self) -> Tuple[bool, str]:
        """Disconnect from the visa resource
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the just informative message
        in case that status is False, result is the error message"""
        self.log.log(f"{self.config_dictionary['DEVICENAME']} - Disconnecting from VISA","HARDWARE")
        try:
            self._visa_instrument.close()
            self._visa_RM.close()
            self._visa_instrument = None
            self._visa_RM = None
            return True , "Visa disconnected"
        except Exception as e:
            return False, self._exceptionTraceback(e)

    def _visaSendMessage(self,command:str) -> Tuple[bool, str]:
        """Send a command to the visa resource
        
        INPUT: command: str -> message to be sent to device without termination characters,
        termination characters are added automatically according to config_dictionary
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message "Sent to visa: {command_which_is_send_to_the_device}",
        in case that status is False, result is the error message"""
        try:
            self._visa_instrument.write(command)
            return True , f"Sent to visa: {command}"
        except Exception as e:
            return False, self._exceptionTraceback(e)


    def _visaReadMessage(self):
        """Read a message from the visa resource
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message which was read from the device,
        in case that status is False, result is the error message"""

        try:
            return True, self._visa_instrument.read()
        except Exception as e:
            return False, self._exceptionTraceback(e)

    def _visaQueryMessage(self,command:str):
        """Send a query to the visa resource
        
        INPUT: command: str -> message to be sent to device without termination characters,
        termination characters are added automatically according to config_dictionary
        
        RETURNS: status :bool, result: str
        in case that status is True, result is the message which was read from the device,
        in case that status is False, result is the error message"""
        try:
            return True,self._visa_instrument.query(command)
        except Exception as e:
            return False, self._exceptionTraceback(e)