"""ST-Link/V2 USB communication
"""

import sys as _sys
import usb as _usb


class StlinkUsbError(Exception):
    """StlinkUsb general errors"""


class StlinkUsbException(Exception):
    """StlinkUsb general exception"""


class NoDeviceFoundException(StlinkUsbException):
    """Exception raised when no STLink device is connected"""


class MoreDevicesException(StlinkUsbException):
    """Exception raised when more devices was detected"""

    def __init__(self, devices):
        super().__init__("More than one device found.")
        self._serial_numbers = [dev.serial_no for dev in devices]

    @property
    def serial_numbers(self):
        """return list of serial numbers"""
        return self._serial_numbers


class StlinkUsbBase:
    """ST link comm base class"""
    ID_VENDOR = None
    ID_PRODUCT = None
    PIPE_OUT = None
    PIPE_IN = None
    DEV_NAME = None

    def __init__(self, dev):
        self._dev = dev

    @classmethod
    def find_all(cls):
        """return all devices with this idVendor and idProduct"""
        devices = []
        try:
            usb_devices = _usb.core.find(
                idVendor=cls.ID_VENDOR,
                idProduct=cls.ID_PRODUCT,
                find_all=True)
            for device in usb_devices:
                devices.append(cls(device))
        except _usb.core.NoBackendError as err:
            raise StlinkUsbException("USB Error: %s" % err)
        return devices

    @property
    def serial_no(self):
        """Return device serial number"""
        try:
            serial_no = self._dev.serial_number
        except ValueError:
            return None
        try:
            if serial_no.isalnum():
                return serial_no
            return ''.join(['%02X' % ord(c) for c in serial_no])
        except NotImplementedError:
            return None

    def compare_serial_no(self, serial_no):
        """Compare device serial no with selected serial number"""
        if self.serial_no.startswith(serial_no):
            return True
        if self.serial_no.endswith(serial_no):
            return True
        return False

    def write(self, data, timeout=200):
        """Write data to USB pipe"""
        try:
            count = self._dev.write(self.PIPE_OUT, data, timeout)
        except _usb.USBError as err:
            self._dev = None
            raise StlinkUsbException("USB Error: %s" % err)
        if count != len(data):
            raise StlinkUsbException("Error Sending data")

    def read(self, size, timeout=200):
        """Read data from USB pipe"""
        try:
            data = self._dev.read(self.PIPE_IN, size, timeout).tobytes()
        except _usb.USBError as err:
            self._dev = None
            raise StlinkUsbException("USB Error: %s" % err)
        return data

    def __del__(self):
        if self._dev is not None:
            self._dev.finalize()


class StlinkUsbV2(StlinkUsbBase):
    """ST-Link/V2 USB communication class"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x3748
    PIPE_OUT = 0x02
    PIPE_IN = 0x81
    DEV_NAME = "V2"


class StlinkUsbV21M(StlinkUsbBase):
    """ST-Link/V2-1 USB communication"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x374b
    PIPE_OUT = 0x01
    PIPE_IN = 0x81
    DEV_NAME = "V2-1"


class StlinkUsbV21(StlinkUsbBase):
    """ST-Link/V2-1 USB communication without mass storage"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x3752
    PIPE_OUT = 0x01
    PIPE_IN = 0x81
    DEV_NAME = "V2-1"


class StlinkUsbV3E(StlinkUsbBase):
    """ST-Link/V3 USB communication"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x374e
    PIPE_OUT = 0x01
    PIPE_IN = 0x81
    DEV_NAME = "V3E"


class StlinkUsbV3(StlinkUsbBase):
    """ST-Link/V3 USB communication"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x374f
    PIPE_OUT = 0x01
    PIPE_IN = 0x81
    DEV_NAME = "V3"


class StlinkUsbV3(StlinkUsbBase):
    """ST-Link/V3 USB communication without mass storage"""
    ID_VENDOR = 0x0483
    ID_PRODUCT = 0x3753
    PIPE_OUT = 0x01
    PIPE_IN = 0x81
    DEV_NAME = "V3"


class StlinkUsb:
    """ST-Link communication class"""
    STLINK_MAXIMUM_TRANSFER_SIZE = 6144
    _STLINK_CMD_SIZE = 16
    _COM_CLASSES = [
        StlinkUsbV2,
        StlinkUsbV21M,
        StlinkUsbV21,
        StlinkUsbV3E,
        StlinkUsbV3
    ]

    @classmethod
    def _find_all_devices(cls):
        devices = []
        for com_cls in cls._COM_CLASSES:
            devices.extend(com_cls.find_all())
        return devices

    @staticmethod
    def _filter_devices(devices, serial_no):
        filtered_devices = []
        for dev in devices:
            serial = dev.serial_no
            if serial and (serial.startswith(serial_no) or serial.endswith(serial_no)):
                filtered_devices.append(dev)
            else:
                del dev
        return filtered_devices

    def print_debug(self, msg, level=0):
        """Print info string"""
        if self._debug >= level:
            _sys.stderr.write(f"D: {msg}\n")

    def print_debug_data(self, msg, data, level=0):
        """Print info string with hexadecimal representation of data"""
        if self._debug >= level:
            if data is None:
                _sys.stderr.write(f"{msg}\n")
            else:
                _sys.stderr.write(
                    f"{msg}: {' '.join([f'{i:02x}' for i in data])}\n")

    def __init__(self, serial_no='', debug=0):
        self._dev = None
        self._debug = debug
        devices = StlinkUsb._find_all_devices()
        if serial_no:
            devices = StlinkUsb._filter_devices(devices, serial_no)
        if not devices:
            raise NoDeviceFoundException()
        if len(devices) > 1:
            raise MoreDevicesException(devices)
        self._dev = devices[0]

    @property
    def dev_name(self):
        """property with device name"""
        return self._dev.DEV_NAME

    def xfer(self, command, data=None, rx_length=0, timeout=200):
        """Transfer command between ST-Link

        Arguments:
            command: is an list of bytes with command (max 16 bytes)
            data: data will be sent after command
            rx_length: number of expected data to receive after command
                and data transfer
            timeout: maximum waiting time for received data in ms

        Return:
            received data

        Raises:
            StlinkUsbException
        """
        if not isinstance(command, bytes):
            raise StlinkUsbError("command is not type of bytes")
        self.print_debug_data("command", command, level=3)
        if len(command) > self._STLINK_CMD_SIZE:
            raise StlinkUsbError(
                "Error too many Bytes in command (maximum is %d Bytes)"
                % self._STLINK_CMD_SIZE)
        # pad to _STLINK_CMD_SIZE
        command += b'\x00' * (self._STLINK_CMD_SIZE - len(command))
        self.print_debug_data("USB:WR", command, level=4)
        self._dev.write(command, timeout)
        if data:
            if not isinstance(data, bytes):
                raise StlinkUsbError("data are not type of bytes")
            self.print_debug_data("USB:WR", data, level=4)
            self._dev.write(data, timeout)
        if rx_length:
            # minimum read length is 2 bytes
            data = self._dev.read(max(2, rx_length))
            self.print_debug_data("USB:RD", data, level=4)
            if len(data) != rx_length:
                data = data[:rx_length]
            return data
        return None
