# License: Apache 2.0. See LICENSE file in root directory.
# Copyright(c) 2021 Intel Corporation. All Rights Reserved.

"""
Brainstem Acroname Hub

See documentation for brainstem here:
https://acroname.com/reference/api/python/index.html
"""

from rspy import log, device_hub
import time
import platform, re

if __name__ == '__main__':
    import os, sys, getopt
    def usage():
        ourname = os.path.basename( sys.argv[0] )
        print( 'Syntax: acroname [options]' )
        print( '        Control the acroname USB hub' )
        print( 'Options:' )
        print( '        --enable       Enable all ports' )
        print( '        --disable      Disable all ports' )
        print( '        --recycle      Recycle all ports' )
        print( '        --reset        Reset the acroname' )
        sys.exit(2)
    try:
        opts,args = getopt.getopt( sys.argv[1:], '',
            longopts = [ 'help', 'recycle', 'enable', 'disable', 'reset' ])
    except getopt.GetoptError as err:
        print( '-F-', err )   # something like "option -a not recognized"
        usage()
    if args or not opts:
        usage()
    # See the end of the file for all the option handling


try:
    import brainstem
except ModuleNotFoundError:
    log.d( 'no acroname library is available' )
    raise

class NoneFoundError( RuntimeError ):
    def __init__( self, message = None ):
        super().__init__( self, message  or  'no Acroname module found' )

class Acroname(device_hub.device_hub):



    def __init__(self):
        super().__init__()
        if discover() is None:  # raise an error if there is no hub connected
            raise NoneFoundError()
        self.hub = None
        self.all_hubs = None

    def connect(self,  reset = False, req_spec = None ):
        """
        Connect to the hub. Raises RuntimeError on failure
        :param reset: When true, the acroname will be reset as part of the connection process
        :param req_spec: Required spec to connect to.
        """
        if not self.hub:
            self.hub = brainstem.stem.USBHub3p()

        if req_spec:
            specs = [req_spec]
        else:
            specs = discover()

        spec = specs[0]
        result = self.hub.connectFromSpec( spec )
        if result != brainstem.result.Result.NO_ERROR:
            raise RuntimeError( "failed to connect to Acroname (result={})".format( result ))
        elif len(specs) > 1:
            log.d( 'connected to', spec )

        if reset:
            log.d("resetting Acroname...")
            result = self.hub.system.reset()
            # According to brainstem support:
            # Result error is expected, so we do not check it
            # * there is also a brainstem internal console print on Linux "error release -4"
            # Disconnection is needed after a reset command
            self.hub.disconnect()

            result = None
            for i in range(10):
                result = self.hub.connectFromSpec(spec)
                if result != brainstem.result.Result.NO_ERROR:
                    time.sleep(1)
                else:
                    log.d('reconnected')
                    return
            raise RuntimeError("failed to reconnect to Acroname (result={})".format(result))


    def is_connected(self):
        return self.hub is not None and self.hub.isConnected()


    def disconnect(self):
        if self.hub:
            self.hub.disconnect()
            del self.hub
            self.hub = None


    def all_ports(self):
        """
        :return: a list of all possible ports, even if currently unoccupied or disabled
        """
        return range(8)


    def ports(self):
        """
        :return: a list of all ports currently occupied (and enabled)
        """
        occupied_ports = []
        for port in self.all_ports():
            if self._port_power(port) > 0.0:
                occupied_ports.append( port )
        return occupied_ports


    def is_port_enabled(self,  port ):
        """
        query if the input port number is enabled through Acroname
        It doesn't mean the device is enumerated as sometimes if the FW crashed we can have a situation where the port is "Disconnected" and not "OK"
        :param port: port number;
        :return: True if Acroname enabled this port, False otherwise
        """

        return self.port_state( port ) != "Disabled"


    def port_state(self,  port ):
        if port < 0 or port > 7:
            raise ValueError( "port number must be [0-7]" )
        #
        status = self.hub.usb.getPortState( port )
        #log.d("getPortState for port", port ,"return", port_state_bitmask_to_string( status.value ))
        return self.port_state_bitmask_to_string( status.value )


    def port_state_bitmask_to_string(self,  bitmask ):
        """
        https://acroname.com/reference/devices/usbhub3p/entities/usb.html#usb-port-state
        Bit   |  Port State: Result Bitwise Description
        -----------------------------------------------
        0     |  USB Vbus Enabled - Port is Disabled/Enabled by the Acroname
        1     |  USB2 Data Enabled
        2     |  Reserved
        3     |  USB3 Data Enabled
        4:10  |  Reserved
        11    |  USB2 Device Attached
        12    |  USB3 Device Attached
        13:18 |  Reserved
        19    |  USB Error Flag
        20    |  USB2 Boost Enabled
        21:22 |  Reserved
        23    |  Device Attached
        24:31 |  Reserved
        """

        if bitmask == 0: # Port is disabled by Acroname
            return "Disabled"
        if bitmask == 11: # Port is enabled but no device was detected
            return "Disconnected"
        if bitmask > 100: # Normally we hope it will cover "Device Attached" use cases (Note, can also be turn on when 'USB Error Flag' is on...but we havn't seen that )
            return "OK"
        return "Unknown Error ({})".format( bitmask )


    def enable_ports(self,  ports = None, disable_other_ports = False, sleep_on_change = 0 ):
        """
        Set enable state to provided ports
        :param ports: List of port numbers; if not provided, enable all ports
        :param disable_other_ports: if True, the ports not in the list will be disabled
        :param sleep_on_change: Number of seconds to sleep if any change is made
        :return: True if no errors found, False otherwise
        """
        result = True
        changed = False
        for port in self.all_ports():
            #
            if ports is None or port in ports:
                if not self.is_port_enabled( port ):
                    #log.d( "enabling port", port)
                    action_result = self.hub.usb.setPortEnable( port )
                    if action_result != brainstem.result.Result.NO_ERROR:
                        result = False
                        log.e("Failed to enable port", port)
                    else:
                        changed = True
            #
            elif disable_other_ports:
                if self.is_port_enabled( port ):
                    #log.d("disabling port", port)
                    action_result = self.hub.usb.setPortDisable( port )
                    if action_result != brainstem.result.Result.NO_ERROR:
                        result = False
                    else:
                        changed = True
        #
        if changed and sleep_on_change:
            import time
            time.sleep( sleep_on_change )
        #
        return result


    def disable_ports(self,  ports = None, sleep_on_change = 0 ):
        """
        :param ports: List of port numbers; if not provided, disable all ports
        :param sleep_on_change: Number of seconds to sleep if any change is made
        :return: True if no errors found, False otherwise
        """
        result = True
        changed = False
        for port in self.all_ports():
            if ports is None or port in ports:
                if self.is_port_enabled( port ):
                        #log.d("disabling port", port)
                        action_result = self.hub.usb.setPortDisable( port )
                        if action_result != brainstem.result.Result.NO_ERROR:
                            result = False
                            log.e("Failed to disable port", port)
                        else:
                            changed = True
        if changed and sleep_on_change:
            import time
            time.sleep( sleep_on_change )
        #
        return result


    def set_ports_usb2(self,  portlist = None, timeout = 100e-3 ):
        """
        Set USB ports to USB2
        """
        if portlist is None:
            portlist = self.ports()
        #
        self.recycle_ports( portlist, timeout = timeout )
        #
        for port in portlist:
            self.hub.usb.setSuperSpeedDataEnable( port )
            self.hub.usb.setHiSpeedDataEnable( port )
            self.hub.usb.setSuperSpeedDataDisable( port )


    def set_ports_usb3(self,  portlist = None, timeout = 100e-3 ):
        """
        Set USB ports to support USB3
        """
        if portlist is None:
            portlist = self.ports()
        #
        self.recycle_ports( portlist, timeout = timeout )
        #
        for port in portlist:
            self.hub.usb.setSuperSpeedDataEnable( port )
            self.hub.usb.setHiSpeedDataEnable( port )
            self.hub.usb.setHiSpeedDataDisable( port )


    def _port_power(self,  port ):
        """
        """
        if port < 0 or port > 7:
            raise ValueError("port number can be only within 0 and 7 (inclusive)")
        #
        micro_volt = self.hub.usb.getPortVoltage( port )
        micro_curr = self.hub.usb.getPortCurrent( port )
        volt = float(micro_volt.value) / 10.0 ** 6
        amps = float(micro_curr.value) / 10.0 ** 6
        #
        return volt * amps

    if 'windows' in platform.system().lower():
        def get_port_by_location(self, usb_location):
            """
            """
            if usb_location:
                #
                # T265 locations look differently...
                match = re.fullmatch(r'Port_#(\d+)\.Hub_#(\d+)', usb_location, re.IGNORECASE)
                if match:
                    # We don't know how to get the port from these yet!
                    return None  # int(match.group(2))
                else:
                    split_location = [int(x) for x in usb_location.split('.')]
                    # lambda helper to return the last 2 non-zero numbers, used when connecting using an additional hub
                    # ex: laptop -> hub -> acroname
                    get_last_two_digits = lambda array: tuple(
                        reversed(list(reversed([i for i in array if i != 0]))[:2]))
                    # only the last two digits are necessary
                    first_index, second_index = get_last_two_digits(split_location)

                    return get_port_from_usb(first_index, second_index)
    else:

        def get_port_by_location(self, usb_location):
            """
            """
            if not self.all_hubs:
                self.all_hubs = set(device_hub.find_all_hubs('24ff'))  # 24ff is Acroname VendorID
            if usb_location:
                #
                # Devices connected through an acroname will be in one of two sub-hubs under the acroname main
                # hub. Each is a 4-port hub with a different port (4 for ports 0-3, 3 for ports 4-7):
                #     /:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 10000M
                #         |__ Port 2: Dev 2, If 0, Class=Hub, Driver=hub/4p, 5000M                       <--- ACRONAME
                #             |__ Port 3: Dev 3, If 0, Class=Hub, Driver=hub/4p, 5000M
                #                 |__ Port X: Dev, If...
                #                 |__ Port Y: ...
                #             |__ Port 4: Dev 4, If 0, Class=Hub, Driver=hub/4p, 5000M
                #                 |__ Port Z: ...
                # (above is output from 'lsusb -t')
                # For the above acroname at '2-2' (bus 2, port 2), there are at least 3 devices:
                #     2-2.3.X
                #     2-2.3.Y
                #     2-2.4.Z
                # Given the two sub-ports (3.X, 3.Y, 4.Z), we can get the port number.
                # NOTE: some of our devices are hubs themselves! For example, the SR300 will show as '2-2.3.2.1' --
                # we must start a known hub or else the ports we look at are meaningless...
                #
                for port in self.all_hubs:
                    if usb_location.startswith(port + '.'):
                        match = re.search(r'^(\d+)\.(\d+)', usb_location[len(port) + 1:])
                        if match:
                            return get_port_from_usb(int(match.group(1)), int(match.group(2)))

specs = None
def discover(retries = 0):
    """
    Return all Acroname module specs in a list. Raise NoneFoundError if one is not found!
    """
    global specs
    # see https://acroname.com/reference/_modules/brainstem/module.html#Module.discoverAndConnect
    if specs is None:
        log.d('discovering Acroname modules ...')
        try:
            log.debug_indent()
            for i in range(retries + 1):
                specs = brainstem.discover.findAllModules(brainstem.link.Spec.USB)
                if not specs:
                    time.sleep(1)
                else:
                    for spec in specs:
                        log.d( '...', spec )
                    break
            if not specs:
                raise NoneFoundError()
        finally:
            log.debug_unindent()

    return specs


def get_port_from_usb(first_usb_index, second_usb_index ):
    """
    Based on last two USB location index, provide the port number
    """
    acroname_port_usb_map = {(4, 4): 0,
                             (4, 3): 1,
                             (4, 2): 2,
                             (4, 1): 3,
                             (3, 4): 4,
                             (3, 3): 5,
                             (3, 2): 6,
                             (3, 1): 7,
                             }
    return acroname_port_usb_map[(first_usb_index, second_usb_index)]



if __name__ == '__main__':
    acroname = Acroname()
    for opt,arg in opts:
        if opt in ('--enable'):
            acroname.connect()
            acroname.enable_ports()   # so ports() will return all
        elif opt in ('--disable'):
            acroname.connect()
            acroname.disable_ports()
        elif opt in ('--recycle'):
            acroname.connect()
            acroname.enable_ports()   # so ports() will return all
            acroname.recycle_ports()
        elif opt in ('--reset'):
            acroname.connect( reset = True )