#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import string
import logging
import time

import usb.core
import usb.util
import usb.backend.libusb1
import serial.tools.list_ports
import paths
import config


class USBDetector:

    @staticmethod
    def format_vid_or_pid(vid_or_pid):
        if vid_or_pid:
            return hex(vid_or_pid)[2:].zfill(4).upper()

    def detect_devices(self):
        retries = 5
        while retries: # the retries is for mac, because it can suddenly refuse to detect usb devices
            try:
                backend_from_our_directory = usb.backend.libusb1.get_backend(find_library=paths.get_libusb_path)
                devices = usb.core.find(find_all=True, backend=backend_from_our_directory)
            except Exception:
                logging.getLogger(self.__class__.__name__).exception("Exception in pyusb: ")
                devices = None
            if not devices:
                message = "Warning: no USB devices detected. This is an error. Detection retries left: %d" % retries
                logging.getLogger(self.__class__.__name__).warning(message)
                time.sleep(0.5)
                retries -= 1
            else:
                return devices
        else:
            return []

    def get_printers_list(self, ignore_profiles=False):
        try:
            serial_ports = serial.tools.list_ports.comports()
        except Exception as e:
            logging.getLogger(self.__class__.__name__).warning(e)
            return []
        unused_serial_ports = [x for x in serial_ports if x[2] != "n/a"]
        printers = []
        forced_types = config.get_settings().get('forced_types', {})
        for dev in self.detect_devices():
            VID = USBDetector.format_vid_or_pid(dev.idVendor) #cuts "0x", fill with zeroes if needed, doing case up
            PID = USBDetector.format_vid_or_pid(dev.idProduct)
            profile = self.find_profile_by_vid_pid(VID, PID)
            if profile or ignore_profiles: 
                SNR = None
                # Warning! Crazy serial number can lead wrong printers profile 
                if not profile or not profile.get('crazy_serial_number'):
                    try:
                        SNR = usb.util.get_string(dev, dev.iSerialNumber)
                    except:
                        logger = logging.getLogger(self.__class__.__name__)
                        logger.error("Error getting printer's serial number")
                    else:
                        if SNR:
                            for symbol in SNR:
                                if not symbol in string.printable:
                                    logger = logging.getLogger(self.__class__.__name__)
                                    message = "Non printable character '%s ' in serial number of device - %s:%s"
                                    logger.error(message % (str(symbol), VID, PID))
                                    SNR = None
                                    break
                usb_info = {'VID': VID, 'PID': PID, 'SNR': SNR}
                if SNR:
                    usb_info['COM'] = self.get_serial_port_name(unused_serial_ports, VID, PID, SNR)
                forced_type = forced_types.get(VID + ":" + PID)
                if forced_type:
                    usb_info['forced_type'] = forced_type
                    #logger = logging.getLogger(self.__class__.__name__)
                    #logger.info("Forcing printer type selection for %s:%s = %s" % (VID, PID, forced_type))
                printers.append(usb_info)
        for printer in printers:
            if not printer.get('COM'):
                printer['COM'] = self.get_serial_port_name(unused_serial_ports, printer['VID'], printer['PID'])
            if not printer['SNR']:
                printer['SNR'] = self.get_snr_by_serial_port_name(printer['COM'], serial_ports)
        self.modify_identical_printers_id(printers)
        return printers

    def get_snr_by_serial_port_name(self, serial_port_name, all_serial_ports):
        for port in all_serial_ports:
            if port.device == serial_port_name:
                return port.serial_number

    def get_serial_port_name(self, unused_serial_ports, vid, pid, snr=None):
        for port in unused_serial_ports:
            if vid == USBDetector.format_vid_or_pid(port.vid) and pid == USBDetector.format_vid_or_pid(port.pid):
                if snr and not snr == port.serial_number.upper():
                    continue
                unused_serial_ports.remove(port)
                return port.device

    def find_profile_by_vid_pid(self, vid, pid):
        profiles = config.get_profiles()
        if profiles:
            for profile in profiles:
                if [vid, pid] in profile['vids_pids']:
                    return profile

    # find printers with same VID, PID, SNR and include COM name in their SNRs
    def modify_identical_printers_id(self, printers):
        for checked_printer in printers:
            for printer in printers:
                if printer['VID'] == checked_printer['VID'] and printer['PID'] == checked_printer['PID'] and\
                   printer['SNR'] == checked_printer['SNR'] and printer['COM'] != checked_printer['COM']:
                      self.update_duplicate_printer_info(printer)
                      self.update_duplicate_printer_info(checked_printer)

    def update_duplicate_printer_info(self, printer):
        printer['ID_CLONE_WARNING'] = True
        if printer['COM']:
            if not printer['SNR']:
                printer['SNR'] = ''
            printer['SNR'] = printer['SNR'] + printer['COM'].split("/")[-1]


if __name__ == '__main__':
    detector = USBDetector()
    printers = detector.get_printers_list(ignore_profiles=True)
    print("\nAll devices:")
    for printer in printers:
        print(printer)
    printers = detector.get_printers_list()
    print("\nMost likely printers:")
    for printer in printers:
        print(printer)
    printers = [x for x in printers if x.get('COM')]
    print("\nDevices with serial port:")
    for printer in printers:
        print(printer)
