from PySide6.QtWidgets import QComboBox, QSpinBox, QDoubleSpinBox
from PySide6.QtGui import QValidator
from PySide6.QtCore import Signal
import serial.tools.list_ports

MAX_UINT32 = (2**32)-1
MAX_UINT16 = (2**16)-1
MAX_UINT8 = (2**8)-1




class COMPortSelector(QComboBox):
    """
    Custom ComboBox widget for Serial COM Port selection
    """
    message = Signal(str)
    
    def __init__(self, *args, **kwargs):
        QComboBox.__init__(self, *args, **kwargs)
        self.setPlaceholderText("Click To Scan")
        self.setCurrentIndex(-1)
        self.noPortMessage = "No Ports Found"
        self.currentIndexChanged.connect(self.onIndexChange)


    def showPopup(self):
        """
        Custom implementation of showPopup function.
        """
        self.clear() # Clear all items
        ports = self.scanPorts()

        if not ports:
            self.insertItem(0, self.noPortMessage)
            self.message.emit("No Ports Found: Please connect RemoteGP via USB")
        else:
            index = 0
            for port in ports:
                self.insertItem(index, port)
                index = index + 1
                
        return super().showPopup()


    def scanPorts(self):
        """
        Scans available serial com ports and returns a list that matches VID: 0x16D0 PID: 0x13FB
        """
        ports = serial.tools.list_ports.comports()
        
        if not ports:
            return None
        
        validPorts = []
        for port in ports:
            # Filter for USB ports only
            if(("USB" in port.description.upper()) or ("USB" in port.hwid.upper())):
                try:
                    vid_pid_str = port.hwid.split("VID:PID=")[1].split(" ")[0]
                    vid, pid = vid_pid_str.split(":")
                    if((port.vid == 0x16D0) and (pid == "13FB")):
                        validPorts.append(port.device)
                except:
                    pass

        return validPorts
    

    def onIndexChange(self):
        """
        Called when current index changes.
        If no ports found, resets placehold message.
        """
        if(self.currentIndex() == -1):
            pass
        else:
            if(self.currentText() == self.noPortMessage):
                self.setCurrentIndex(-1)
                self.message.emit("No Ports Found: Please connect RemoteGP via USB")
            elif(self.currentText() == ""):
                pass
            else:
                self.message.emit(self.currentText() + " selected")



class CANIDSpinbox(QSpinBox):
    """
    Custom spinbox for displaying CAN ID entry
    """

    isExtendedID = False

    def __init__(self, *args, **kwargs):
        QSpinBox.__init__(self, *args, **kwargs)
        self.setDisplayIntegerBase(16)
        self.setPrefix("0x")
        self.setRange(0, int(0x7FF)) # Standard CAN

    def textFromValue(self, val):
        if(self.isExtendedID):
            hexFormat = f"{val:08X}"
        else:
            hexFormat = f"{val:03X}"
        return hexFormat
    
    def valueFromText(self, text):
        try:
            return int(text, 16)
        except ValueError:
            return 0
        
    def updateIDType(self, isExtended):
        if(isExtended):
            self.isExtendedID = True
            self.setRange(0, int(0x1FFFFFFF))
        else:
            self.isExtendedID = False
            self.setRange(0, int(0x7FF))
        
        self.refreshDisplay()

    def refreshDisplay(self):
        """
        Refreshes display
        """
        self.setValue(self.value())


class HexSpinBox(QSpinBox):
    """
    Displays SpinBox value as capital hexidecimal value

    byteFormat initialisation arguement decides number of leading zeroes.
    """
    byteFormat = 1

    def __init__(self, byteFormat=1, parent=None):
        super().__init__(parent)
        self.setDisplayIntegerBase(16)
        self.setPrefix("0x")
        self.byteFormat = byteFormat

    def textFromValue(self, val):
        if(self.byteFormat == 1):
            hexFormat = f"{val:02X}"
        elif(self.byteFormat == 2):
            hexFormat = f"{val:04X}"
        else:
            hexFormat = f"{val:08X}"
        return hexFormat
    
    def valueFromText(self, text):
        try:
            return int(text, 16)
        except ValueError:
            return 0


class HexSpinBox_1Byte(HexSpinBox):
    """
    Wrapper class for 1 byte HexSpinBox
    """
    def __init__(self, parent=None):
        super().__init__(byteFormat=1)


class HexDoubleSpinBox(QDoubleSpinBox):
    """
    Custom Double SpinBox to display hexadecimal values
    """
    byteFormat = 1 # Number of bytes to format text as

    def __init__(self, *args, **kwargs):
        QDoubleSpinBox.__init__(self, *args, **kwargs)
        self.setRange(0, MAX_UINT8)
        self.setDecimals(0)

    def textFromValue(self, value: float) -> str:
        # Convert the float value to an integer, then to a hex string
        integer_value = int(value)
        if(self.byteFormat == 1):
            hexFormat = f"{integer_value:02X}"
        elif(self.byteFormat == 2):
            hexFormat = f"{integer_value:04X}"
        else:
            hexFormat = f"{integer_value:08X}"

        return f"0x{hexFormat}"

    def valueFromText(self, text: str) -> float:
        # Convert the hex string to an integer, then to a float
        try:
            return float(int(text, 16))
        except ValueError:
            return 0.0  # Handle invalid input

    def validate(self, input: str, pos: int):
        # Remove 0x from input value if present
        if(input.startswith("0x")):
            na, hexVal = input.split("0x")
        else:
            hexVal = input

        # Allow hexadecimal characters (0-9, A-F)
        if all(c in "0123456789ABCDEFabcdef" for c in hexVal):
            return QValidator.Acceptable, input, pos
        return QValidator.Invalid, input, pos

    def updateByteLength(self, numBytes):
        self.byteFormat = numBytes
        if(numBytes == 1):
            self.setRange(0, MAX_UINT8)
        elif(numBytes == 2):
            self.setRange(0, MAX_UINT16)
        else:
            self.setRange(0, MAX_UINT32)
        self.refreshDisplay()

    def refreshDisplay(self):
        self.setValue(self.value())


class MultiplesSpinBox(QDoubleSpinBox):
    """
    Custom Double Spinbox to only only allow values equal to the Single Step setting
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.editingFinished.connect(self.check_value)
        

    def check_value(self):
        # Round to the nearest multiple of Single Step setting
        self.blockSignals(True)
        value = self.value()
        self.setValue(round(value / float(self.singleStep())) * float(self.singleStep()))
        self.blockSignals(False)


class MultiplesSpinBoxStandard(QSpinBox):
    """
    Custom Spinbox to only only allow values equal to the Single Step setting
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.editingFinished.connect(self.check_value)
        

    def check_value(self):
        # Round to the nearest multiple of Single Step setting
        self.blockSignals(True)
        value = self.value()
        self.setValue(round(value / float(self.singleStep())) * float(self.singleStep()))
        self.blockSignals(False)