# -*- coding: utf-8 -*-
#
# This file is part of EventGhost.
# Copyright © 2005-2018 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 2 of the License, or (at your option)
# any later version.
#
# EventGhost is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with EventGhost. If not, see <http://www.gnu.org/licenses/>.


import sys
import ctypes
from ctypes.wintypes import BOOL, DWORD

newdev = ctypes.windll.newdev
ERROR_IN_WOW64 = -536870347

# BOOL DiInstallDriverW(
#   HWND    hwndParent,
#   LPCWSTR InfPath,
#   DWORD   Flags,
#   PBOOL   NeedReboot
# )
DiInstallDriverW = newdev.DiInstallDriverW
DiInstallDriverW.restype = BOOL


def DiInstallDriver(InfPath):
    NeedReboot = BOOL()
    Flags = DWORD(0)
    InfPath = ctypes.create_unicode_buffer(InfPath)
    hwndParent = None

    if not DiInstallDriverW(hwndParent, InfPath, Flags, ctypes.byref(NeedReboot)):
        err = ctypes.GetLastError()

        if err == ERROR_IN_WOW64:
            return False
        else:
            return err
    return True


if __name__ == '__main__' and ctypes.sizeof(ctypes.c_void_p) == 8 and len(sys.argv) == 2:
    return_code = DiInstallDriver(sys.args[1])
    if return_code is True:
        return_code = 0

    elif return_code is False:
        return_code = ERROR_IN_WOW64
    sys.exit(return_code)


import codecs  # NOQA
import hashlib  # NOQA
import os  # NOQA
import Queue  # NOQA
import string  # NOQA
import threading  # NOQA
import wx  # NOQA
from ctypes import (
    addressof,
    byref,
    c_ubyte,
    cast,
    create_string_buffer,
    create_unicode_buffer,
    GetLastError,
    POINTER,
    sizeof,
    WinDLL,
    WinError,
    wstring_at,
)  # NOQA

from os.path import join, dirname  # NOQA

# Local imports
import eg  # NOQA
from eg.WinApi import IsWin64  # NOQA
from eg.WinApi.Dynamic import (
    CLSIDFromString,
    ERROR_NO_MORE_ITEMS,
    GUID,
    INVALID_HANDLE_VALUE,
    PBYTE,
)  # NOQA
from eg.WinApi.Dynamic.SetupApi import (
    DIGCF_ALLCLASSES,
    DIGCF_DEVICEINTERFACE,
    DIGCF_PRESENT,
    ERROR_INSUFFICIENT_BUFFER,
    PSP_DEVICE_INTERFACE_DETAIL_DATA,
    SetupDiBuildDriverInfoList,
    SetupDiEnumDeviceInfo,
    SetupDiEnumDeviceInterfaces,
    SetupDiEnumDriverInfo,
    SetupDiGetClassDevs,
    SetupDiGetDeviceInstallParams,
    SetupDiGetDeviceInterfaceDetail,
    SetupDiGetDeviceRegistryProperty,
    SetupDiSetDeviceInstallParams,
    SP_DEVICE_INTERFACE_DATA,
    SP_DEVICE_INTERFACE_DETAIL_DATA,
    SP_DEVINFO_DATA,
    SP_DEVINSTALL_PARAMS,
    SP_DRVINFO_DATA,
    SPDIT_COMPATDRIVER,
    SPDRP_HARDWAREID,
)  # NOQA
from eg.WinApi.IsAdmin import IsAdmin  # NOQA
from eg.WinApi.PipedProcess import ExecAs  # NOQA

DI_FLAGSEX_INSTALLEDDRIVER = 0x04000000
PUBYTE = POINTER(c_ubyte)

DRIVER_VERSION = "1.0.2.0"
DRIVER_PROVIDER = "EventGhost"
DRIVER_CLASS_GUID = "{FE050E98-31CD-47EA-AC39-CB143EF208B2}"
PLATFORM = "x64" if IsWin64() else "x86"
DOWNLOAD_ROOT = "http://www.eventghost.net/downloads/winusb/%s/" % PLATFORM
INSTALLATION_ROOT = join(
    eg.folderPath.ProgramData, "eventghost", "drivers", "winusb", PLATFORM
)


class StrWrapper(str):
    
    def __init__(self, value):
        try:
            str.__init__(self, value)
        except TypeError:
            str.__init__(self)
        
        self.md5 = ''
    
    def set_md5(self, value):
        self.md5 = value


dpinst = StrWrapper('dpinst.exe')
WinUSBCoInstaller2 = StrWrapper('WinUSBCoInstaller2.dll')
if IsWin64():
    dpinst.set_md5('aa0a91227631a09cd075d315646fb7a9')
    WinUSBCoInstaller2.set_md5('246900ce6474718730ecd4f873234cf5')
else:
    dpinst.set_md5('e6213cec602f332bf8e868b7b8bf2bb1')
    WinUSBCoInstaller2.set_md5('8e7b9f81e8823fee2d82f7de3a44300b')

if eg.WindowsVersion.Is10():
    WUDFUpdate = StrWrapper('WUDFUpdate_01011.dll')
    WdfCoInstaller = StrWrapper('WdfCoInstaller01011.dll')
    if IsWin64():
        WUDFUpdate.set_md5('d9b4bed45b1e6f83b05f5abeb86f7ec6')
        WdfCoInstaller.set_md5('d10864c1730172780c2d4be633b9220a')
        
    else:
        WUDFUpdate.set_md5('554901cd6380aa9ab26d3471f81ea7f9')
        WdfCoInstaller.set_md5('3d2a2d921135801835073451f002480f')

else:
    WUDFUpdate = StrWrapper('WUDFUpdate_01009.dll')
    WdfCoInstaller = StrWrapper('WdfCoInstaller01009.dll')
    if IsWin64():
        WUDFUpdate.set_md5('ebf9ee8a7671f3b260ed9b08fcee0cc5')
        WdfCoInstaller.set_md5('4da5da193e0e4f86f6f8fd43ef25329a')
    else:
        WUDFUpdate.set_md5('e1bbe9e3568cf54598e9a8d23697b67e')
        WdfCoInstaller.set_md5('a9970042be512c7981b36e689c5f3f9f')


class DriverTemplate(str):

    if eg.WindowsVersion.Is10():
        _wdf_framework_version = '01011'
    else:
        _wdf_framework_version = '01009'

    _template = (
        '; This file is automatically created by the EventGhost.\r\n'
        '; Don\'t edit this file directly.\r\n'
        '\r\n'
        '[Version]\r\n'
        'Signature="$$Windows NT$$"\r\n'
        'Class=HIDClass\r\n'
        'ClassGuid={745a17a0-74d3-11d0-b6fe-00a0c90f57da}\r\n'
        'Provider=%ProviderName%\r\n'
        'DriverVer=01/25/2010,____DRIVER_VERSION____\r\n'
        'DriverPackageDisplayName=%DisplayName%\r\n'
        '\r\n'
        '; ========== Manufacturer/Models sections ===========\r\n'
        '\r\n'
        '[Manufacturer]\r\n'
        '%ProviderName%=Remotes,NTx86,NTamd64\r\n'
        '\r\n'
        '[Remotes.NTx86]\r\n'
        '____REMOTES____\r\n'
        '[Remotes.NTamd64]\r\n'
        '____REMOTES____\r\n'
        '; ========== Global sections ===========\r\n'
        '\r\n'
        '[Install]\r\n'
        'Include=winusb.inf\r\n'
        'Needs=WINUSB.NT\r\n'
        '\r\n'
        '[Install.Services]\r\n'
        'Include=winusb.inf\r\n'
        'AddService=WinUSB,0x00000002,WinUSB_ServiceInstall\r\n'
        '\r\n'
        '[Install.Wdf]\r\n'
        'KmdfService=WINUSB, WinUsb_Install\r\n'
        '\r\n'
        '[Install.CoInstallers]\r\n'
        'AddReg=CoInstallers_AddReg\r\n'
        'CopyFiles=CoInstallers_CopyFiles\r\n'
        '\r\n'
        '[Install.HW]\r\n'
        'AddReg=Dev_AddReg\r\n'
        '\r\n'
        '[Dev_AddReg]\r\n'
        'HKR,,DeviceInterfaceGUIDs,0x10000,"{FE050E98-31CD-47EA-AC39-CB143EF208B2}"\r\n'
        'HKR,,"SystemWakeEnabled",0x00010001,1\r\n'
        '\r\n'
        '[WinUSB_Install]\r\n'
        'KmdfLibraryVersion=1.9\r\n'
        '\r\n'
        '[WinUSB_ServiceInstall]\r\n'
        'DisplayName=%WinUSB_SvcDesc%\r\n'
        'ServiceType=1\r\n'
        'StartType=3\r\n'
        'ErrorControl=1\r\n'
        'ServiceBinary=%12%\\WinUSB.sys\r\n'
        '\r\n'
        '[CoInstallers_AddReg]\r\n'
        'HKR,,CoInstallers32,0x00010000,"WdfCoInstaller____WDF_VERSION____,WdfCoInstaller",'
        '"WinUSBCoInstaller2.dll","WUDFUpdate_____WDF_VERSION____.dll"\r\n'
        '\r\n'
        '[CoInstallers_CopyFiles]\r\n'
        'WinUSBCoInstaller2.dll\r\n'
        'WdfCoInstaller____WDF_VERSION____.dll\r\n'
        '\r\n'
        '[DestinationDirs]\r\n'
        'CoInstallers_CopyFiles=11\r\n'
        '\r\n'
        '; ================= Source Media Section =====================\r\n'
        '\r\n'
        '[SourceDisksNames]\r\n'
        '1=%DISK_NAME%,,,\r\n'
        '\r\n'
        '[SourceDisksNames.amd64]\r\n'
        '1=%DISK_NAME%,,,\r\n'
        '\r\n'
        '[SourceDisksFiles]\r\n'
        'WinUSBCoInstaller2.dll=1\r\n'
        'WdfCoInstaller____WDF_VERSION____.dll=1\r\n'
        'WUDFUpdate_____WDF_VERSION____.dll=1\r\n'
        '\r\n'
        '; =================== Strings ===================\r\n'
        '\r\n'
        '[Strings]\r\n'
        'ProviderName="____DRIVER_PROVIDER____"\r\n'
        'WinUSB_SvcDesc="WinUSB Driver"\r\n'
        'DISK_NAME="My Install Disk"\r\n'
        'DisplayName="____DISPLAY_NAME____"\r\n'
        '____DEVICE_NAMES____'
    )

    def __init__(self, value=''):
        try:
            str.__init__(self, value)
        except TypeError:
            str.__init__(self)

        self._remotes = ''
        self._device_names = ''
        self._display_name = ''

    def __str__(self):
        template = self._template
        template = template.replace('_____WDF_VERSION____', self._wdf_framework_version)
        template = template.replace('____DRIVER_VERSION____', DRIVER_VERSION)
        template = template.replace('____REMOTES____', self._remotes)
        template = template.replace('____DRIVER_PROVIDER____', DRIVER_PROVIDER)
        template = template.replace('____DISPLAY_NAME____', self._display_name)
        template = template.replace('____DEVICE_NAMES____', self._device_names)

        return template

    def hardware_ids(self, hardware_ids):
        for i, hardware_id in enumerate(hardware_ids):
            self._remotes += '%Device{0}.DeviceDesc%=Install,{1}\r\n'.format(i, hardware_id)

    hardware_ids = property(fset=hardware_ids)

    def device_names(self, device_names):
        for i, device_name in enumerate(device_names):
            self._device_names += 'Device{0}.DeviceDesc="{1}"\r\n'.format(i, device_name)

    device_names = property(fset=device_names)

    def display_name(self, display_name):
        self._display_name = display_name

    display_name = property(fset=display_name)


class Text(eg.TranslatableStrings):
    dialogCaption = "EventGhost Plugin: %s"
    downloadMsg = (
        "EventGhost needs to download additional files before it "
        "can install the driver for the %s plugin.\n\n"
        "Do you want to start the download now?\n"
    )
    installMsg = (
        "You need to install the proper driver for this %s device.\n\n"
        "Should EventGhost start the driver installation for you now?"
    )
    restartMsg = (
        "EventGhost needs to restart, before it can use the new driver.\n\n"
        "Do you want to restart EventGhost now?"
    )
    downloadFailedMsg = (
        "The download failed!\n\nPlease try again later."
    )


class WinUsb(object):
    installQueue = Queue.Queue()
    installThreadLock = threading.Lock()
    installThread = None

    def __init__(self, plugin):
        self.plugin = plugin
        self.devices = []

    def AddDevice(
        self,
        name,
        hardwareId,
        guid,
        callback,
        dataSize=1,
        suppressRepeat=False
    ):
        device = self.Device(callback, dataSize, suppressRepeat)
        device.AddHardwareId(name, hardwareId)
        return device

    def CheckAddOnFiles(self):
        neededFiles = self.GetNeededFiles()
        if len(neededFiles) == 0:
            return True

        if not eg.CallWait(self.ShowDownloadMessage):
            return False

        stopEvent = threading.Event()
        wx.CallAfter(eg.TransferDialog, None, neededFiles, stopEvent)
        stopEvent.wait()
        neededFiles = self.GetNeededFiles()
        if neededFiles:
            eg.CallWait(
                wx.MessageBox,
                Text.downloadFailedMsg,
                caption=Text.dialogCaption % self.plugin.name,
                style=wx.OK | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP,
                parent=eg.document.frame
            )
            return False
        return True

    def CreateInf(self):
        template = DriverTemplate()

        infPath = join(INSTALLATION_ROOT, "driver.inf")

        if not os.path.exists(INSTALLATION_ROOT):
            os.makedirs(INSTALLATION_ROOT)

        hardwareIds = []
        names = []
        for device in self.devices:
            for hardwareId, name in device.hardwareIds:
                hardwareIds.append(hardwareId)
                names.append(name)

        template.hardware_ids = hardwareIds
        template.device_names = names
        template.display_name = self.plugin.name

        with codecs.open(infPath, "w", 'mbcs') as f:
            f.write(str(template))

        return infPath

    def Device(self, callback, dataSize=1, suppressRepeat=False):
        device = UsbDevice(self, callback, dataSize, suppressRepeat)
        self.devices.append(device)
        return device

    @staticmethod
    def GetDeviceHardwareId(hDevInfo, deviceInfoData):
        buffersize = DWORD(0)
        dataType = DWORD()
        if SetupDiGetDeviceRegistryProperty(
            hDevInfo,
            byref(deviceInfoData),
            SPDRP_HARDWAREID,
            None,
            None,
            0,
            byref(buffersize)
        ):
            raise WinError()
        err = GetLastError()
        if err == ERROR_INSUFFICIENT_BUFFER:
            hardwareId = create_unicode_buffer(buffersize.value / 2)
        else:
            raise WinError(err)
        if not SetupDiGetDeviceRegistryProperty(
            hDevInfo,
            byref(deviceInfoData),
            SPDRP_HARDWAREID,
            byref(dataType),
            cast(hardwareId, PBYTE),
            buffersize.value,
            byref(buffersize)
        ):
            raise WinError()
        return StripRevision(hardwareId.value.upper())

    @staticmethod
    def GetDevicePaths():
        classGuid = GUID()
        CLSIDFromString(DRIVER_CLASS_GUID, byref(classGuid))
        hDevInfo = SetupDiGetClassDevs(
            classGuid, None, None, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE
        )
        if hDevInfo == INVALID_HANDLE_VALUE:
            raise WinError()

        deviceInterfaceData = SP_DEVICE_INTERFACE_DATA()
        deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA)
        deviceInfoData = SP_DEVINFO_DATA()
        deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA)
        memberIndex = 0
        result = {}
        while True:
            if not SetupDiEnumDeviceInterfaces(
                hDevInfo,
                None,
                classGuid,
                memberIndex,
                byref(deviceInterfaceData)
            ):
                err = GetLastError()
                if err == ERROR_NO_MORE_ITEMS:
                    break
                else:
                    raise WinError(err)
            requiredSize = DWORD()
            SetupDiGetDeviceInterfaceDetail(
                hDevInfo,
                byref(deviceInterfaceData),
                None,
                0,
                byref(requiredSize),
                byref(deviceInfoData)
            )
            buf = create_string_buffer(requiredSize.value)
            pDiDetailData = cast(buf, PSP_DEVICE_INTERFACE_DETAIL_DATA)
            pDiDetailData.contents.cbSize = sizeof(
                SP_DEVICE_INTERFACE_DETAIL_DATA
            )
            SetupDiGetDeviceInterfaceDetail(
                hDevInfo,
                byref(deviceInterfaceData),
                pDiDetailData,
                requiredSize.value,
                byref(requiredSize),
                None
            )

            devicePath = wstring_at(addressof(pDiDetailData.contents) + 4)
            hardwareId = WinUsb.GetDeviceHardwareId(hDevInfo, deviceInfoData)
            result[hardwareId] = devicePath
            memberIndex += 1
        return result

    def GetNeededFiles(self):
        neededFiles = []

        for f in (
            dpinst,
            WinUSBCoInstaller2,
            WUDFUpdate,
            WdfCoInstaller,
        ):
            path = join(INSTALLATION_ROOT, f)
            if not os.path.exists(path):
                neededFiles.append((DOWNLOAD_ROOT + f, path))
                continue

            md5 = hashlib.md5()
            md5.update(open(path, "rb").read())

            if md5.hexdigest() != f.md5:
                neededFiles.append((DOWNLOAD_ROOT + f, path))

        return neededFiles

    @classmethod
    def InstallDriver(cls):
        while True:
            with cls.installThreadLock:
                if cls.installQueue.empty():
                    cls.installThread = None
                    return
            self = cls.installQueue.get()
            if wx.YES != eg.CallWait(
                wx.MessageBox,
                Text.installMsg % self.plugin.name,
                caption=Text.dialogCaption % self.plugin.name,
                style=wx.YES_NO | wx.ICON_QUESTION | wx.STAY_ON_TOP,
                parent=eg.document.frame
            ):
                continue

            if not self.CheckAddOnFiles():
                continue

            self.CreateInf()
            result = -1
            cmdLine = '"%s" /f /lm' % join(INSTALLATION_ROOT, "dpinst.exe")
            try:
                result = ExecAs(
                    "subprocess",
                    eg.WindowsVersion >= 'Vista' or not IsAdmin(),
                    "call",
                    cmdLine.encode('mbcs'),
                )
            except WindowsError as exc:
                # only silence "User abort"
                if exc.winerror != 1223:
                    raise

            if result == 1:
                eg.actionThread.Call(self.plugin.info.Start)
            #
            # infPath = self.CreateInf()
            #
            # res = DiInstallDriver(infPath)
            # if res is True:
            #     res = 0
            # elif res is False:
            #     exec_path = os.path.split(sys.executable)[0]
            #     exec_path = os.path.join(exec_path, 'py64.exe')
            #
            #     cmdLine = '"%s" "%s" "%s"'.format(exec_path, __file__, infPath)
            #     res = ExecAs(
            #         "subprocess",
            #         eg.WindowsVersion >= 'Vista' or not IsAdmin(),
            #         "call",
            #         cmdLine.encode('mbcs'),
            #     )
            #
            # if res == 0:
            #     eg.actionThread.Call(self.plugin.info.Start)

    @staticmethod
    def ListDevices():
        devices = {}
        guid = GUID()
        CLSIDFromString("{A5DCBF10-6530-11D2-901F-00C04FB951ED}", byref(guid))
        hDevInfo = SetupDiGetClassDevs(
            guid,
            "USB",  # Enumerator
            0,
            DIGCF_PRESENT | DIGCF_ALLCLASSES
        )
        if hDevInfo == INVALID_HANDLE_VALUE:
            raise WinError()
        deviceInfoData = SP_DEVINFO_DATA()
        deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA)
        driverInfoData = SP_DRVINFO_DATA()
        driverInfoData.cbSize = sizeof(SP_DRVINFO_DATA)
        deviceInstallParams = SP_DEVINSTALL_PARAMS()
        deviceInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS)

        i = 0
        while True:
            if not SetupDiEnumDeviceInfo(hDevInfo, i, byref(deviceInfoData)):
                err = GetLastError()
                if err == ERROR_NO_MORE_ITEMS:
                    break
                else:
                    raise WinError(err)
            i += 1
            hardwareId = WinUsb.GetDeviceHardwareId(hDevInfo, deviceInfoData)
            if hardwareId.startswith("USB\\ROOT_HUB"):
                continue
            driverInfoData.DriverVersion = 0
            SetupDiGetDeviceInstallParams(
                hDevInfo,
                byref(deviceInfoData),
                byref(deviceInstallParams)
            )
            deviceInstallParams.FlagsEx |= DI_FLAGSEX_INSTALLEDDRIVER
            SetupDiSetDeviceInstallParams(
                hDevInfo,
                byref(deviceInfoData),
                byref(deviceInstallParams)
            )
            SetupDiBuildDriverInfoList(
                hDevInfo,
                byref(deviceInfoData),
                SPDIT_COMPATDRIVER
            )
            if not SetupDiEnumDriverInfo(
                hDevInfo,
                byref(deviceInfoData),
                SPDIT_COMPATDRIVER,
                0,
                byref(driverInfoData)
            ):
                err = GetLastError()
                if err == ERROR_NO_MORE_ITEMS:
                    devices[hardwareId] = DeviceInfo(
                        name = "<unknown name>",
                        version = "",
                        hardwareId = hardwareId,
                        provider = "<unknown provider",
                    )
                    continue
                else:
                    raise WinError(err)
            version = driverInfoData.DriverVersion
            versionStr = "%d.%d.%d.%d" % (
                (version >> 48) & 0xFFFF,
                (version >> 32) & 0xFFFF,
                (version >> 16) & 0xFFFF,
                version & 0xFFFF
            )
            devices[hardwareId] = DeviceInfo(
                driverInfoData.Description,
                versionStr,
                hardwareId,
                driverInfoData.ProviderName,
            )
        return devices

    def ShowDownloadMessage(self):
        return wx.YES == wx.MessageBox(
            Text.downloadMsg % self.plugin.name,
            caption=Text.dialogCaption % self.plugin.name,
            style=wx.YES_NO | wx.ICON_QUESTION | wx.STAY_ON_TOP,
            parent=eg.document.frame
        )

    def ShowRestartMessage(self):
        res = wx.MessageBox(
            Text.restartMsg,
            caption=eg.APP_NAME,
            style=wx.YES_NO | wx.ICON_QUESTION | wx.STAY_ON_TOP,
            parent=eg.document.frame
        )
        if res == wx.YES:
            eg.app.Restart()

    def Start(self):
        installedHardware = self.ListDevices()
        for device in self.devices:
            for hardwareId, name in device.hardwareIds:
                if hardwareId in installedHardware:
                    break
            else:
                raise self.plugin.Exceptions.DeviceNotFound
            deviceInfo = installedHardware[hardwareId]
            if (
                deviceInfo.version != DRIVER_VERSION or
                deviceInfo.provider != DRIVER_PROVIDER or
                deviceInfo.name != name
            ):
                self.StartInstall()
                raise self.plugin.Exceptions.DriverNotFound
        for device in self.devices:
            device.Start()
    Open = Start

    def StartInstall(self):
        with self.installThreadLock:
            self.installQueue.put(self)
            if self.installThread is None:
                self.__class__.installThread = threading.Thread(
                    target=self.InstallDriver
                )
                self.installThread.start()

    def Stop(self):
        for device in self.devices:
            device.Stop()
    Close = Stop


class DeviceInfo(object):
    def __init__(self, name, version, hardwareId, provider):
        self.name = name
        self.version = version
        self.hardwareId = hardwareId
        self.provider = provider

    def __repr__(self):
        return "DeviceInfo(%r, %r, %r, %r)" % (
            self.name, self.version, self.hardwareId, self.provider
        )


class UsbDevice(object):
    dll = None

    def __init__(self, winUsb, callback, dataSize, suppressRepeat):
        self.winUsb = winUsb
        self.callback = callback
        self.dataSize = dataSize
        self.suppressRepeat = suppressRepeat
        self.threadId = None
        self.hardwareIds = []

    def AddHardwareId(self, name, *hardwareIds):
        for hardwareId in hardwareIds:
            hardwareId = StripRevision(hardwareId.upper())
            self.hardwareIds.append((hardwareId, name))
        return self

    def FindDevicePath(self):
        installedDevices = WinUsb.GetDevicePaths()
        for hardwareId, _ in self.hardwareIds:
            if hardwareId in installedDevices:
                return installedDevices[hardwareId]
        raise self.winUsb.plugin.Exceptions.DeviceNotFound

    def MsgHandler(self, dummyHwnd, dummyMsg, dummyWParam, lParam):
        dataArray = cast(lParam, PUBYTE)
        value = tuple(dataArray[i] for i in range(self.dataSize))
        try:
            self.callback(value)
        except:
            eg.PrintTraceback(source=self.winUsb.plugin.info.treeItem)
        return 1

    def Start(self):
        if self.dll is None:
            self.__class__.dll = WinDLL(
                join(eg.sitePackagesDir, "WinUsbWrapper.dll").encode('mbcs')
            )
        msgId = eg.messageReceiver.AddWmUserHandler(self.MsgHandler)
        devicePath = self.FindDevicePath()
        self.threadId = self.dll.Start(
            eg.messageReceiver.hwnd,
            msgId,
            devicePath,
            self.dataSize,
            int(self.suppressRepeat)
        )
        if not self.threadId:
            raise self.winUsb.plugin.Exceptions.DriverNotOpen

    def Stop(self):
        self.dll.Stop(self.threadId)
        self.threadId = None
        eg.messageReceiver.RemoveWmUserHandler(self.MsgHandler)


def StripRevision(hardwareId):
    return "&".join(
        part for part in hardwareId.split("&") if not part.startswith("REV_")
    )
