import serial, serial.tools.list_ports
from distutils.version import LooseVersion
from . codes import CMDTYPE, LEDTYPE, SPIChipsets, BufferChipsets
from . import io
from ... drivers.return_codes import raise_error
from ... util import log, util
from ... project.importer import import_module

_DEVICES_BY_HARDWARE_ID = {}


def Devices(hardware_id, baudrate):
    devices = _DEVICES_BY_HARDWARE_ID.get(hardware_id)
    if devices:
        if devices.baudrate != baudrate:
            log.warning(
                'Expected baudrate %s but got %s', baudrate, devices.baudrate)
    else:
        devices = DevicesImpl(hardware_id, baudrate)
        _DEVICES_BY_HARDWARE_ID[hardware_id] = devices

    return devices


class DevicesImpl(object):
    """Manage a list of serial devices.

    :param str hardware_id: A valid USB VID:PID pair such as "1D50:60AB"
    :param int baudrate: Baud rate to connect to serial device
    """

    def __init__(self, hardware_id, baudrate):
        self.hardware_id = hardware_id
        self.baudrate = baudrate
        self.devices = None

    def find_serial_devices(self):
        """Scan and report all compatible serial devices on system.

        :returns: List of discovered devices
        """
        if self.devices is not None:
            return self.devices

        self.devices = {}
        hardware_id = "(?i)" + self.hardware_id  # forces case insensitive

        for ports in serial.tools.list_ports.grep(hardware_id):
            port = ports[0]
            try:
                id = self.get_device_id(port)
                ver = self._get_device_version(port)
            except:
                log.debug('Error getting device_id for %s, %s',
                          port, self.baudrate)
                if True:
                    raise
                continue

            if getattr(ports, '__len__', lambda: 0)():
                log.debug('Multi-port device %s:%s:%s with %s ports found',
                          self.hardware_id, id, ver, len(ports))
            if id < 0:
                log.debug('Serial device %s:%s:%s with id %s < 0',
                          self.hardware_id, id, ver)
            else:
                self.devices[id] = port, ver

        return self.devices

    def get_device(self, id=None):
        """Returns details of either the first or specified device

        :param int id: Identifier of desired device. If not given, first device
            found will be returned

        :returns tuple: Device ID, Device Address, Firmware Version
        """
        if id is None:
            if not self.devices:
                raise ValueError('No default device for %s' % self.hardware_id)
            id, (device, version) = sorted(self.devices.items())[0]

        elif id in self.devices:
            device, version = self.devices[id]

        else:
            error = 'Unable to find device with ID %s' % id
            log.error(error)
            raise ValueError(error)

        log.info("Using COM Port: %s, Device ID: %s, Device Ver: %s",
                 device, id, version)
        return id, device, version

    def error(self, fail=True, action=''):
        """
        SHOULD BE PRIVATE METHOD
        """
        e = 'There was an unknown error communicating with the device.'
        if action:
            e = 'While %s: %s' % (action, e)
        log.error(e)
        if fail:
            raise IOError(e)

    def set_device_id(self, dev, id):
        """Set device ID to new value.

        :param str dev: Serial device address/path
        :param id: Device ID to set
        """
        if id < 0 or id > 255:
            raise ValueError("ID must be an unsigned byte!")
        com, code, ok = io.send_packet(
            CMDTYPE.SETID, 1, dev, self.baudrate, 5, id)
        if not ok:
            raise_error(code)

    def get_device_id(self, dev):
        """Get device ID at given address/path.

        :param str dev: Serial device address/path
        :param baudrate: Baudrate to use when connecting (optional)
        """
        com, code, ok = io.send_packet(CMDTYPE.GETID, 0, dev, self.baudrate, 5)
        if code is None:
            self.error(action='get_device_id')
        return code

    def _get_device_version(self, dev):
        com, code, ok = io.send_packet(
            CMDTYPE.GETVER, 0, dev, self.baudrate, 0.5)
        return ok and io.read_byte(com) or 0
