import ctypes
import re


def ValidHandle(value):
    if value == 0:
        raise ctypes.WinError()
    return value


NULL = 0
HDEVINFO = ctypes.c_int
BOOL = ctypes.c_int
CHAR = ctypes.c_char
PCTSTR = ctypes.c_char_p
HWND = ctypes.c_uint
DWORD = ctypes.c_ulong
PDWORD = ctypes.POINTER(DWORD)
ULONG = ctypes.c_ulong
ULONG_PTR = ctypes.POINTER(ULONG)
# ~ PBYTE = ctypes.c_char_p
PBYTE = ctypes.c_void_p


class GUID(ctypes.Structure):
    _fields_ = [
        ("Data1", ctypes.c_ulong),
        ("Data2", ctypes.c_ushort),
        ("Data3", ctypes.c_ushort),
        ("Data4", ctypes.c_ubyte * 8),
    ]

    def __str__(self):
        data4 = "".join([f"{d:02x}" for d in self.Data4[:2]])
        data5 = "".join([f"{d:02x}" for d in self.Data4[2:]])
        return f"{{{self.Data1:08x}-{self.Data2:04x}-{self.Data3:04x}-{data4}-{data5}}}"


class SP_DEVINFO_DATA(ctypes.Structure):
    _fields_ = [
        ("cbSize", DWORD),
        ("ClassGuid", GUID),
        ("DevInst", DWORD),
        ("Reserved", ULONG_PTR),
    ]

    def __str__(self):
        return f"ClassGuid:{self.ClassGuid} DevInst:{self.DevInst}"


PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)


class SP_DEVICE_INTERFACE_DATA(ctypes.Structure):
    _fields_ = [
        ("cbSize", DWORD),
        ("InterfaceClassGuid", GUID),
        ("Flags", DWORD),
        ("Reserved", ULONG_PTR),
    ]

    def __str__(self):
        return f"InterfaceClassGuid:{self.InterfaceClassGuid} Flags:{self.Flags}"


PSP_DEVICE_INTERFACE_DATA = ctypes.POINTER(SP_DEVICE_INTERFACE_DATA)

PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p

SetupDiDestroyDeviceInfoList = ctypes.windll.setupapi.SetupDiDestroyDeviceInfoList
SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
SetupDiDestroyDeviceInfoList.restype = BOOL

SetupDiGetClassDevs = ctypes.windll.setupapi.SetupDiGetClassDevsA
SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
SetupDiGetClassDevs.restype = ValidHandle  # HDEVINFO

SetupDiEnumDeviceInterfaces = ctypes.windll.setupapi.SetupDiEnumDeviceInterfaces
SetupDiEnumDeviceInterfaces.argtypes = [
    HDEVINFO,
    PSP_DEVINFO_DATA,
    ctypes.POINTER(GUID),
    DWORD,
    PSP_DEVICE_INTERFACE_DATA,
]
SetupDiEnumDeviceInterfaces.restype = BOOL

SetupDiGetDeviceInterfaceDetail = (
    ctypes.windll.setupapi.SetupDiGetDeviceInterfaceDetailA
)
SetupDiGetDeviceInterfaceDetail.argtypes = [
    HDEVINFO,
    PSP_DEVICE_INTERFACE_DATA,
    PSP_DEVICE_INTERFACE_DETAIL_DATA,
    DWORD,
    PDWORD,
    PSP_DEVINFO_DATA,
]
SetupDiGetDeviceInterfaceDetail.restype = BOOL

SetupDiGetDeviceRegistryProperty = (
    ctypes.windll.setupapi.SetupDiGetDeviceRegistryPropertyA
)
SetupDiGetDeviceRegistryProperty.argtypes = [
    HDEVINFO,
    PSP_DEVINFO_DATA,
    DWORD,
    PDWORD,
    PBYTE,
    DWORD,
    PDWORD,
]
SetupDiGetDeviceRegistryProperty.restype = BOOL


GUID_CLASS_COMPORT = GUID(
    0x86E0D1E0,
    0x8089,
    0x11D0,
    (ctypes.c_ubyte * 8)(0x9C, 0xE4, 0x08, 0x00, 0x3E, 0x30, 0x1F, 0x73),
)

DIGCF_PRESENT = 2
DIGCF_DEVICEINTERFACE = 16
INVALID_HANDLE_VALUE = 0
ERROR_INSUFFICIENT_BUFFER = 122
SPDRP_HARDWAREID = 1
SPDRP_FRIENDLYNAME = 12
ERROR_NO_MORE_ITEMS = 259


def comports(available_only=True):
    """This generator scans the device registry for com ports and yields port, desc, hwid.
    If available_only is true only return currently existing ports."""
    flags = DIGCF_DEVICEINTERFACE
    if available_only:
        flags |= DIGCF_PRESENT
    g_hdi = SetupDiGetClassDevs(ctypes.byref(GUID_CLASS_COMPORT), None, NULL, flags)
    # ~ for i in range(256):
    for dwIndex in range(256):
        did = SP_DEVICE_INTERFACE_DATA()
        did.cbSize = ctypes.sizeof(did)

        if not SetupDiEnumDeviceInterfaces(
            g_hdi, None, ctypes.byref(GUID_CLASS_COMPORT), dwIndex, ctypes.byref(did)
        ):
            if ctypes.GetLastError() != ERROR_NO_MORE_ITEMS:
                raise ctypes.WinError()
            break

        dwNeeded = DWORD()
        # get the size
        if not SetupDiGetDeviceInterfaceDetail(
            g_hdi, ctypes.byref(did), None, 0, ctypes.byref(dwNeeded), None
        ):
            # Ignore ERROR_INSUFFICIENT_BUFFER
            if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
                raise ctypes.WinError()

        # allocate buffer
        class SP_DEVICE_INTERFACE_DETAIL_DATA_A(ctypes.Structure):
            _fields_ = [
                ("cbSize", DWORD),
                ("DevicePath", CHAR * (dwNeeded.value - ctypes.sizeof(DWORD))),
            ]

            def __str__(self):
                return f"DevicePath:{self.DevicePath}"

        idd = SP_DEVICE_INTERFACE_DETAIL_DATA_A()
        idd.cbSize = 5
        devinfo = SP_DEVINFO_DATA()
        devinfo.cbSize = ctypes.sizeof(devinfo)
        if not SetupDiGetDeviceInterfaceDetail(
            g_hdi,
            ctypes.byref(did),
            ctypes.byref(idd),
            dwNeeded,
            None,
            ctypes.byref(devinfo),
        ):
            raise ctypes.WinError()

        # hardware ID
        szHardwareID = ctypes.create_string_buffer("\0" * 250)
        if not SetupDiGetDeviceRegistryProperty(
            g_hdi,
            ctypes.byref(devinfo),
            SPDRP_HARDWAREID,
            None,
            ctypes.byref(szHardwareID),
            ctypes.sizeof(szHardwareID) - 1,
            None,
        ):
            # Ignore ERROR_INSUFFICIENT_BUFFER
            if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
                raise ctypes.WinError()

        # friendly name
        szFriendlyName = ctypes.create_string_buffer("\0" * 250)
        if not SetupDiGetDeviceRegistryProperty(
            g_hdi,
            ctypes.byref(devinfo),
            SPDRP_FRIENDLYNAME,
            None,
            ctypes.byref(szFriendlyName),
            ctypes.sizeof(szFriendlyName) - 1,
            None,
        ):
            # Ignore ERROR_INSUFFICIENT_BUFFER
            if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
                raise ctypes.WinError()
        port_name = re.search(r"\((.*)\)", szFriendlyName.value).group(1)
        if len(port_name) > 4:
            port_name = "\\\\.\\" + port_name
        yield port_name, szFriendlyName.value, szHardwareID.value

    SetupDiDestroyDeviceInfoList(g_hdi)


if __name__ == "__main__":
    import serial

    for port, desc, hwid in comports():
        print(f"{port}: {desc} ({hwid})")
        print(" " * 10, serial.Serial(port))  # test open

    # list of all ports the system knows
    print("-" * 60)
    for port, desc, hwid in comports(False):
        print(f"{port:-10s}: {desc} ({hwid})")
