/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                                                   */
/* file: main.c                                                      */
/*                                                                   */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                                                   */
/* description: Example CLI application for basic hub functionality. */
/*                                                                   */
/*                                                                   */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *   */
/*                                                                   */
/* Copyright (c) 2023 Acroname Inc. - All Rights Reserved            */
/*                                                                   */
/* This file is part of the BrainStem release. See the license.txt   */
/* file included with this package or go to                          */
/* https://acroname.com/software/brainstem-development-kit           */
/* for full license details.                                         */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

///////////////////////////////////////////////////////////////////////////////
// Includes
///////////////////////////////////////////////////////////////////////////////

#include <iostream>
#include <iomanip>
#include "BrainStem2/BrainStem-all.h"

//The command line arguments are parsed using the open source cxxopts.hpp header
//implementation. The original source of this file can be found at:
//https://github.com/jarro2783/cxxopts
//FYI: Windows Users std::min() and std::max() are used by cxxopts.hpp.  This 
//     can cause an issues when including windows.h which defines these as macros.
//     To combat this I added NOMINMAX to the preprocessor defines for this project.
//     Debug -> BrainStem2Example Properties -> Configuration Properties -> C/C++ -> preprocessor
#include "cxxopts.hpp"


///////////////////////////////////////////////////////////////////////////////
// Private Macros
///////////////////////////////////////////////////////////////////////////////
// Platform specific line feed.
#if defined(_MSC_VER)
#define _LF             "\r\n"
#define LF_LEN          2
#else
#define _LF             "\n"
#define LF_LEN          1
#endif


///////////////////////////////////////////////////////////////////////////////
// Private Constants
///////////////////////////////////////////////////////////////////////////////
static const uint8_t MIN_MAJOR = 2;
static const uint8_t MIN_MINOR = 10;
static const uint8_t MIN_PATCH = 0;


static const char* EXAMPLES_COMMANDS = _LF
"Examples:" _LF
"Note: When --serial is NOT used, the application will default to the first Acroname Hub found" _LF
"\"AcronameHubCLI --ports 0 --enable 0\"                     - Disables Port 0" _LF
"\"AcronameHubCLI --ports 0 --enable 1\"                     - Enables Port 0" _LF
"\"AcronameHubCLI --ports=0,1,2 --enable 0\"                 - Disables Ports 0, 1 and 2" _LF
"\"AcronameHubCLI --ports 1 --enable 1 --serial FEEDBEEF\"   - Enables port 1 for device 0xFEEDBEEF" _LF
"\"AcronameHubCLI --ports 1 --enable 1 --data\"              - Enables port 1 data lines only" _LF
"\"AcronameHubCLI --ports 1 --enable 0 --data\"              - Disables port 1 data lines only" _LF
"\"AcronameHubCLI --ports 2 --enable 1 --power\"             - Enables port 2 power lines only" _LF
"\"AcronameHubCLI --ports 2 --enable 0 --power\"             - Disables port 2 power lines only" _LF
"\"AcronameHubCLI --ports 3 --voltage --current\"            - Gets the voltage and current of port 3" _LF
;


///////////////////////////////////////////////////////////////////////////////
// Private Types
///////////////////////////////////////////////////////////////////////////////

typedef enum PORT_ACTIONS : uint8_t {
    kPORT_ACTIONS_ENABLE = 0,
    kPORT_ACTIONS_TOGGLE,
    kPORT_ACTIONS_DATA,
    kPORT_ACTIONS_UNKNOWN,
    kPORT_ACTIONS_LAST
} PORT_ACTIONS_t;


typedef struct CLIObject {
    std::shared_ptr<Acroname::BrainStem::Module> stem = nullptr;
    std::shared_ptr<cxxopts::Options> options = nullptr;
    std::shared_ptr<linkSpec> spec = nullptr;
    
    unsigned long serialNumber = 0;
    std::vector<int> ports;
    int enable = 0;
    int upstream = 0;
    bool power = true;
    bool data = true;
    PORT_ACTIONS_t portsAction = kPORT_ACTIONS_UNKNOWN;
    cxxopts::ParseResult result;
} CLIObject_t;


///////////////////////////////////////////////////////////////////////////////
// Private Function Prototypes
///////////////////////////////////////////////////////////////////////////////
static void _config_cxxopts(CLIObject_t& cliObj);

static bool _parse(CLIObject_t& cliObj, int argc, char* argv[]);
static bool _parse_serial(CLIObject_t& cliObj);
static bool _parse_port(CLIObject_t& cliObj);
static bool _parse_upstream(CLIObject_t& cliObj);

//If (int < 0) A program error has occurred.
//If (int > 0) A BrainStem error has occurred.
//If (int == 0) Success.
static int _process(CLIObject_t& cliObj);
static int _process_createStem(CLIObject_t& cliObj);
static aErr _process_reset(CLIObject_t& cliObj);
static aErr _process_ports(CLIObject_t& cliObj);
static aErr _process_upstream(CLIObject_t& cliObj);
static aErr _process_verbose(CLIObject_t& cliObj);

static aErr _portAction_toggle(CLIObject_t& cliObj, const int& port);
static aErr _portAction_enable(CLIObject_t& cliObj, const int& port);
static aErr _portAction_voltage(CLIObject_t& cliObj, const int& port);
static aErr _portAction_current(CLIObject_t& cliObj, const int& port);

static int _translate_UpstreamPort(uint8_t model, int providedPort);

static aErr _kickConnection(CLIObject_t& cliObj);

static void _verbose_printMode_USBHub3c(const uint32_t& mode, const int& port, const bool& showHeader = false);
static void _verbose_printMode_USBHub3p(const uint32_t& mode, const int& port, const bool& showHeader = false);
static void _verbose_printMode_USBHub2x4(const uint32_t& mode, const int& port, const bool& showHeader = false);
static std::string _verbose_printMode_decodeModeString(const uint32_t& mode);
static std::string _verbose_upstreamPortNumber(const uint32_t& model, const uint8_t& upstreamPort);

static bool _checkVersion(std::shared_ptr<Acroname::BrainStem::Module> stem);


///////////////////////////////////////////////////////////////////////////////
// Public Function Implementations
///////////////////////////////////////////////////////////////////////////////

int main(int argc, char* argv[])
{
    CLIObject_t cliObj;
    aErr err = aErrNone;

    try
    {
        _config_cxxopts(cliObj);
        
        //Parse options/arguments.
        if(! _parse(cliObj, argc, argv)) { return -1; }
        
        //Process options/arguments.
        int success = _process(cliObj);
        if((success >= 0)               &&
           (success <= aErrUnknown))
        {
            err = (aErr)success;
        }
        else { return success; }
        
    }
    catch (const cxxopts::OptionException& e) {
        std::cerr << "Error parsing options: " << e.what() << std::endl;
        std::cerr << cliObj.options->help() << std::endl;
        return -2;
    }
    catch (...) {
        std::cerr << "Unknown Exception Caught: " << std::endl;
        std::cerr << cliObj.options->help() << std::endl;
        return -3;
    }
    
    return err;
}


static void
_config_cxxopts(CLIObject_t& cliObj) {
    //Create cxxopts options object
    cliObj.options.reset(new cxxopts::Options("AcronameHubCLI.exe", "Acroname Programmable Hub Command Line Interface"));

    cliObj.options->add_options()
        ("h, help", "Prints this help message")
        ("p, ports", "Port(s) in which a \"Port Action\" will be executed on. The default action is enable. This can be a comma-separated list.", cxxopts::value<std::vector<int>>())
        ("e, enable", "0 = Disable; 1 = Enable (Port Action) ", cxxopts::value<int>()->default_value("1"))
        ("t, toggle", "Toggle port(s) between enabled and disabled. (Port Action) ")
        ("b, voltage", "Reports the voltage of a given port(s) (Port Action)")
        ("c, current", "Reports the current of a given port(s) (Port Action)")
        ("w, power", "Only apply enable or toggle actions to port power lines")
        ("d, data", "Only apply enable or toggle actions to port data lines")
        ("u, upstream", "Manually select an upstream port", cxxopts::value<int>())
        ("a, auto", "Enable automatic upstream port selection. If this option is present, the -u/--upstream option is ignored.")
        ("s, serial", "Hub serial number in hexadecimal representation. Use 0 for automatic discovery.", cxxopts::value<std::string>()->default_value("0"))
        ("v, verbose", "Report port states after processing other options")
        ("r, reset", "Reset the hub. All port options are ignored.")
        ("x, examples", "Displays a list of common examples");
}

///////////////////////////////////////////////////////////////////////////////
// Private Function Implementations
///////////////////////////////////////////////////////////////////////////////

static bool
_parse(CLIObject_t& cliObj, int argc, char* argv[]) {
    cliObj.result = cliObj.options->parse(argc, argv);

    //Print arguments, if non were provided print the usage.
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    std::cout << "Arguments: ";
    for (int x = 0; x < argc; x++) { std::cout << argv[x] << " "; }
    std::cout << std::endl;

    if(argc == 1) { //First argument is always the app path.
        std::cout << "No arguments were provided. Printing usage" << std::endl;
        std::cout << cliObj.options->help() << std::endl;
        std::cout << EXAMPLES_COMMANDS << std::endl;
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Help
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    if (cliObj.result.count("help")) {
        std::cout << cliObj.options->help() << std::endl;
        std::cout << EXAMPLES_COMMANDS << std::endl;
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Help
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    if (cliObj.result.count("examples")) {
        std::cout << EXAMPLES_COMMANDS << std::endl;
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Serial
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    if(! _parse_serial(cliObj)) {
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Port
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    if(! _parse_port(cliObj)) {
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Enable - We need to check the value is within range.
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    cliObj.enable = cliObj.result["enable"].as<int>();
    if ((1 != cliObj.enable) &&
        (0 != cliObj.enable))
    {
        std::cerr << "Incorrect value for enable" << std::endl;
        std::cerr << "Acceptable values are 0 for false and 1 for true" << std::endl;
        std::cerr << cliObj.options->help() << std::endl;
        return false;
    }
    /// End Parse Enable/////////////////////////////////////////////////////////////////

    //Parser - Toggle
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    //Nothing specific needs to be handled or tested. Option will be checked in "work".
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Upstream - We need to check the value is within range.
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    if(! _parse_upstream(cliObj)) {
        return false;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Power and Data
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // act on both power and data by default (both flags present or both flags absent)
    cliObj.power = true;
    cliObj.data = true;
    if (cliObj.result.count("power") && !cliObj.result.count("data")) {
        // only power flag present: only act on power lines
        cliObj.power = true;
        cliObj.data = false;
    }
    if (cliObj.result.count("data") && !cliObj.result.count("power")) {
        // only data flag present: only act on power lines
        cliObj.power = false;
        cliObj.data = true;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////

    //Parser - Auto
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    //Nothing specific needs to be handled or tested. Option will be checked in "work".
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    return true;
}


static bool
_parse_serial(CLIObject_t& cliObj) {
    if (cliObj.result.count("serial")) {
        cliObj.serialNumber = std::stoul(cliObj.result["serial"].as<std::string>(), nullptr, 16);
        if (cliObj.serialNumber == 0) {
            cliObj.spec.reset(aDiscovery_FindFirstModule(USB, LOCALHOST_IP_ADDRESS));
        }
        else {
            cliObj.spec.reset(aDiscovery_FindModule(USB,
                                                    (uint32_t)cliObj.serialNumber,
                                                    LOCALHOST_IP_ADDRESS));
        }
        
        //Ensure that we found something.
        if (cliObj.spec == NULL) {
            std::cerr << "Could not find any BrainStem Devices" << std::endl;
            std::cerr << cliObj.options->help() << std::endl;
            return false;
        }
    }
    else {
        //No serial number was provided. Use the first thing we find.
        cliObj.spec.reset(aDiscovery_FindFirstModule(USB, LOCALHOST_IP_ADDRESS));
        if (cliObj.spec == NULL) {
            std::cerr << "Could not find any BrainStem Devices" << std::endl;
            std::cerr << cliObj.options->help() << std::endl;
            return false;
        }
    }

    if ((cliObj.spec->model != aMODULE_TYPE_USBHub3p)  &&
        (cliObj.spec->model != aMODULE_TYPE_USBHub2x4) &&
        (cliObj.spec->model != aMODULE_TYPE_USBHub3c))
    {
        std::cerr << "The device that was found is not a hub. Model: " << aDefs_GetModelName(cliObj.spec->model) << std::endl;
        std::cerr << cliObj.options->help() << std::endl;
        return false;
    }
    
    return true;
}


static bool 
_parse_port(CLIObject_t& cliObj) {
    if (cliObj.result.count("ports")) {
        
        if (cliObj.result.count("enable")) {
            cliObj.portsAction = kPORT_ACTIONS_ENABLE;
        }
        else if(cliObj.result.count("toggle")) {
            cliObj.portsAction = kPORT_ACTIONS_TOGGLE;
        }
        else if((cliObj.result.count("voltage")) ||
                (cliObj.result.count("current")))
        {
            cliObj.portsAction = kPORT_ACTIONS_DATA;
        }
        else {
            //This should preserve backwards compatibility.
            //Specifically AcronameHubCLIE.exe --ports 1 -> This enables the port.
            //ie. (--enable not required).
            cliObj.portsAction = kPORT_ACTIONS_ENABLE;
        }
        
        //We need to check the range based on the device type
        cliObj.ports = cliObj.result["ports"].as<std::vector<int>>();
        for (const auto& port : cliObj.ports) {
            if ((cliObj.spec->model == aMODULE_TYPE_USBHub3p) ||
                (cliObj.spec->model == aMODULE_TYPE_USBHub3c))
            {
                if ((port > 7) ||
                    (port < 0))
                {
                    std::cerr << "Incorrect port value: " << int(port) << std::endl;
                    std::cerr << "The " << aDefs_GetModelName(cliObj.spec->model) << " ports range from 0-7" << std::endl;
                    std::cerr << cliObj.options->help() << std::endl;
                    return false;
                }
            }
            else if (cliObj.spec->model == aMODULE_TYPE_USBHub2x4) {
                if ((port > 3) ||
                    (port < 0))
                {
                    std::cerr << "Incorrect port value: " << int(port) << std::endl;
                    std::cerr << "The " << aDefs_GetModelName(cliObj.spec->model) << " ports range from 0-3" << std::endl;
                    std::cerr << cliObj.options->help() << std::endl;
                    return false;
                }
            }//end model check.
        }//end ports for loop
    }// end "ports" argument
    else {
        //These parameters require --ports.
        //Notify the user and exit.
        std::string s = "";
        if     (cliObj.result.count("enable"))  { s = "enable";     }
        else if(cliObj.result.count("toggle"))  { s = "toggle";     }
        else if(cliObj.result.count("voltage")) { s = "voltage";    }
        else if(cliObj.result.count("current")) { s = "current";    }
        
        if(s.size()) {
            std::cout << "Parameter: \"" << s << "\" must accompany the \"ports\" parameter" << std::endl;
            std::cerr << cliObj.options->help() << std::endl;
            return false;
        }
    }
    
    return true;
}


static bool 
_parse_upstream(CLIObject_t& cliObj) {
    if (cliObj.result.count("upstream")) {
        bool handleError = false;
        
        cliObj.upstream = cliObj.result["upstream"].as<int>();
        if (cliObj.spec->model == aMODULE_TYPE_USBHub3c) {
            if(cliObj.upstream >= 6) {
                handleError = true;
            }
        }
        else {
            if ((cliObj.upstream != 1) &&
                (cliObj.upstream != 0))
            {
                handleError = true;
            }
        }
        
        if(handleError) {
            std::cerr << "Incorrect value for upstream" << std::endl;
            if (cliObj.spec->model == aMODULE_TYPE_USBHub3c) {
                std::cerr << "Acceptable values 0-5" << std::endl;
            }
            else {
                std::cerr << "Acceptable values are 0 for UP0 and 1 for UP1" << std::endl;
            }
            std::cerr << cliObj.options->help() << std::endl;
            return false;
        }
    }
    
    return true;
}


static int
_process(CLIObject_t& cliObj) {
    aErr err = aErrNone;
    
    int success = _process_createStem(cliObj);
    if(0 != success) { return success; }

    err = cliObj.stem->connectFromSpec(*cliObj.spec);
    if (aErrNone != err) {
        std::cerr << "Error connecting to the device. Error: " << err << std::endl;
        return err;
    }
    else {
        std::cout << "Sucessfully connected to " << aDefs_GetModelName(cliObj.spec->model);
        auto prevState = std::cout.flags();
        std::cout << " SN: 0x" << std::setfill('0') << std::setw(8) << std::hex << std::uppercase << cliObj.spec->serial_num << std::endl;
        std::cout.flags(prevState);
    }
    
    if (cliObj.result.count("reset")) {
        return _process_reset(cliObj);
    }
    
    if (aErrNone == err) { err = _process_ports(cliObj); }
    if (aErrNone == err) { err = _process_upstream(cliObj); }
    if (aErrNone == err) { err = _process_verbose(cliObj); }

    if(cliObj.stem->isConnected()) { cliObj.stem->disconnect(); }
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    return (int)err;
}


static int
_process_createStem(CLIObject_t& cliObj) {
    if (cliObj.spec->model == aMODULE_TYPE_USBHub3p) {
        cliObj.stem.reset(new Acroname::BrainStem::Module(aUSBHUB3P_MODULE, true, aMODULE_TYPE_USBHub3p));
    }
    else if (cliObj.spec->model == aMODULE_TYPE_USBHub2x4) {
        cliObj.stem.reset(new Acroname::BrainStem::Module(aUSBHUB2X4_MODULE, true, aMODULE_TYPE_USBHub2x4));
    }
    else if (cliObj.spec->model == aMODULE_TYPE_USBHub3c) {
        cliObj.stem.reset(new Acroname::BrainStem::Module(aUSBHUB3C_MODULE, true, aMODULE_TYPE_USBHub3c));
    }
    else {
        std::cerr << "Error: device object not created" << std::endl;
        return -5;
    }
    
    return 0;
}


static aErr
_process_reset(CLIObject_t& cliObj) {
    std::cout << "Resetting device" << std::endl;
    if(cliObj.stem->isConnected()) {
        Acroname::BrainStem::SystemClass system;
        system.init(cliObj.stem.get(), 0);
        system.reset();
        return aErrNone;
    }
    else { return aErrConnection; }
}


static aErr
_process_ports(CLIObject_t& cliObj) {
    aErr err = aErrNone;
    
    //Loop through provided ports.
    for (const auto& port : cliObj.ports) {
        switch(cliObj.portsAction) {
            case kPORT_ACTIONS_ENABLE:
                err = _portAction_enable(cliObj, port);
                break;
                
            case kPORT_ACTIONS_TOGGLE:
                err = _portAction_toggle(cliObj, port);
                break;
                
            case kPORT_ACTIONS_DATA:
                if(aErrNone != err) { break; }
                if(cliObj.result.count("voltage")) {
                    err = _portAction_voltage(cliObj, port);
                }
                
                if(aErrNone != err) { break; }
                if(cliObj.result.count("current")) {
                    err = _portAction_current(cliObj, port);
                }
                
                break;

            default:
                //Given the current logic we shouldn't get here.
                cliObj.portsAction = kPORT_ACTIONS_UNKNOWN;
                std::cout << "Unknown Action" << std::endl;
                return aErrUnknown;
        }
        
        if(aErrNone != err) { break; }
    } // for (const auto& port : ports)
    
    return err;
}

static int _translate_UpstreamPort(uint8_t model, int providedPort) {
    
    switch(model) {
        case aMODULE_TYPE_USBHub3p:
            if(providedPort == 0) {
                return aUSBHub3p::PORT_ID_t::kPORT_ID_UP0;
            }
            else if(providedPort == 1) {
                return aUSBHub3p::PORT_ID_t::kPORT_ID_UP1;
            }
        case aMODULE_TYPE_USBHub2x4:
            if(providedPort == 0) {
                return aUSBHub2x4::PORT_ID_t::kPORT_ID_UP0;
            }
            else if(providedPort == 1) {
                return aUSBHub2x4::PORT_ID_t::kPORT_ID_UP1;
            }
//        case aMODULE_TYPE_USBHub3c: //Nothing to translate
    }
    
    return providedPort;
}

static aErr
_process_upstream(CLIObject_t& cliObj) {
    if(! _checkVersion(cliObj.stem)) { return aErrVersion; }
    
    Acroname::BrainStem::USBSystemClass usbSystem;
    usbSystem.init(cliObj.stem.get(), 0);
    
    aErr err = aErrNone;
    if (cliObj.result.count("auto")) {
        err = usbSystem.setDataRoleBehavior(usbsystemDataBehavior_PortPriority);
        if (aErrTimeout == err) { err = _kickConnection(cliObj); }
        
        if(aErrNone != err) {
            std::cerr << "Error (" << err << ") " << "changing the data role to auto (Port Priority)." << std::endl;
            return err;
        }
        
        std::cout << "Automatic upstream selection configured successfully." << std::endl;
    }
    else if (cliObj.result.count("upstream")) {
        err = usbSystem.setDataRoleBehavior(usbsystemDataBehavior_HardCoded);
        if (aErrTimeout == err) { err = _kickConnection(cliObj); }
        
        if(aErrNone != err) {
            std::cerr << "Error (" << err << ") " << "changing the data role to fixed (Hard Coded)." << std::endl;
            return err;
        }
        
        err = usbSystem.setUpstream(_translate_UpstreamPort(cliObj.spec->model, cliObj.upstream));
        if (aErrTimeout == err) { err = _kickConnection(cliObj); }
        
        if(aErrNone != err) {
            std::cerr << "There was an error (" << err << ") " << "changing the upstream port." << std::endl;
            std::cerr << "If you changed the upstream connection away from this machine and you have no control port connection then this is expected." << std::endl;
            return err;
        }
        
        std::cout << "Upstream successfully set to port: " << cliObj.upstream << std::endl;
    }
    
    return err;
}


static aErr
_process_verbose(CLIObject_t& cliObj) {
    aErr err = aErrNone;
    
    if(cliObj.result.count("verbose")) {
        if(! _checkVersion(cliObj.stem)) { return aErrVersion; }
        
        Acroname::BrainStem::USBSystemClass usbSystem;
        usbSystem.init(cliObj.stem.get(), 0);
        
        uint8_t upstreamPort = 0;
        err = usbSystem.getUpstream(&upstreamPort);
        if(aErrNone != err) {
            std::cerr << "Error (" << err << ") " << "getting the upstream port." << std::endl;
            return err;
        }

        std::cout << "Active Upstream: " << _verbose_upstreamPortNumber(cliObj.spec->model, upstreamPort) << std::endl;
        
        
        //Iterate up to a large value of ports to support all products.
        //We will break out when we get aErrIndexRange as an error code
        for(int port = 0; port < 20; port++) {
            Acroname::BrainStem::PortClass portClass;
            portClass.init(cliObj.stem.get(), port);
            
            uint32_t mode = 0;
            err = portClass.getMode(&mode);
            
            //Once we hit an invalid index bail.
            if(aErrIndexRange == err) {
                return aErrNone; //Overwrite the error since we expect this case.
            }
            else if(aErrUnimplemented == err) {
                //Some ports do not support this API.
                //For instance: Ports: Up0, Up1, DownA and Control on the USBHub3p.
                continue;
            }
            else if(aErrNone != err) {
                std::cerr << "Error (" << err << ") " << "getting the mode." << std::endl;
                return err;
            }
            
            bool printHeader = (0 == port);
            
            switch (cliObj.spec->model) {
                case aMODULE_TYPE_USBHub3c:
                    _verbose_printMode_USBHub3c(mode, port, printHeader);
                    break;
                    
                case aMODULE_TYPE_USBHub3p:
                    _verbose_printMode_USBHub3p(mode, port, printHeader);
                    break;
                    
                case aMODULE_TYPE_USBHub2x4:
                    _verbose_printMode_USBHub2x4(mode, port, printHeader);
                    break;
                    
                default: break;
            }//end switch
        }//end for loop (ports)
    }
    
    return err;
}


static aErr
_portAction_toggle(CLIObject_t& cliObj, const int& port) {

    if(! _checkVersion(cliObj.stem)) { return aErrVersion; }
    
    Acroname::BrainStem::PortClass portClass;
    portClass.init(cliObj.stem.get(), port);
    
    aErr err = aErrNone;
    uint8_t vbusState = 0;
    uint8_t dataState = 0;
    
    err = portClass.getDataEnabled(&dataState);
    if(aErrNone != err) {
        std::cerr << "Error (" << err << ") getting port: " << int(port) <<  " data state" << std::endl;
        return err;
    }
    
    err = portClass.getPowerEnabled(&vbusState);
    if(aErrNone != err) {
        std::cerr << "Error (" << err << ") getting port: " << int(port) <<  " power state" << std::endl;
        return err;
    }
    
    //Apply Data Settings
    if (cliObj.data) {
        err = portClass.setDataEnabled(! dataState);
        if (aErrNone != err) {
            std::cerr << "Error (" << err << ") toggling port: " << int(port) << " data" << std::endl;
            return err;
        }
    }
        
    //Apply Power Settings
    if (cliObj.power) {
        err = portClass.setPowerEnabled(! vbusState);
        if (aErrNone != err) {
            std::cerr << "Error (" << err << ") toggling port: " << int(port) << " power" << std::endl;
            return err;
        }
    }
        
    std::cout << "Port: " << int(port) << " was sucessfully toggled" << std::endl;
    
    return err;
}


static aErr
_portAction_enable(CLIObject_t& cliObj, const int& port)
{
    if(! _checkVersion(cliObj.stem)) { return aErrVersion; }
    
    Acroname::BrainStem::PortClass portClass;
    portClass.init(cliObj.stem.get(), port);
    
    aErr err = aErrNone;
    
    if ((cliObj.power) &&
        (cliObj.data))
    {
        err = portClass.setEnabled(cliObj.enable);
    }
    else {
        if (cliObj.power)  { err = portClass.setPowerEnabled(cliObj.enable); }
        if (cliObj.data)   { err = portClass.setDataEnabled(cliObj.enable); }
    }
    
    if (err != aErrNone) {
        std::cerr << "There was an error (" << err << ") " << (cliObj.enable ? "enabling" : "disabling") << " port " << int(port) << std::endl;
    }
    else {
        std::cout << "Port: " << int(port) << " was sucessfully " << (cliObj.enable ? "enabled" : "disabled") << std::endl;
    }
    
    return err;
}


static aErr
_portAction_voltage(CLIObject_t& cliObj, const int& port) {
    aErr err = aErrNone;

    if(! _checkVersion(cliObj.stem)) { return aErrVersion; }
    
    Acroname::BrainStem::PortClass p;
    p.init(cliObj.stem.get(), port);
    
    int32_t voltage = 0;
    err = p.getVbusVoltage(&voltage);
    
    std::cout << "Port: " << int(port) << " Voltage: " << double(voltage)/1000000.0 << " VDC" << std::endl;
    
    return err;
}


static aErr
_portAction_current(CLIObject_t& cliObj, const int& port) {
    aErr err = aErrNone;
    
    if(! _checkVersion(cliObj.stem)) { return aErrVersion; }

    Acroname::BrainStem::PortClass p;
    p.init(cliObj.stem.get(), port);
    
    int32_t current = 0;
    err = p.getVbusCurrent(&current);
    
    std::cout << "Port: " << int(port) << " Current: " << double(current)/1000000.0 << " A" << std::endl;
    
    return err;
}


static bool
_checkVersion(std::shared_ptr<Acroname::BrainStem::Module> stem) {

    //Cached variables in order to reduce calls to getVersion
    static std::weak_ptr<Acroname::BrainStem::Module> _stem;
    static uint8_t major = 0;
    static uint8_t minor = 0;
    static uint8_t patch = 0;
    
    auto s = _stem.lock();
    if((nullptr == s)           ||
       (s.get() != stem.get()))
    {
        Acroname::BrainStem::SystemClass sys;
        sys.init(stem.get(), 0);
        uint32_t build = 0;
        aErr err = sys.getVersion(&build);
        if(aErrNone == err) {
            major = aVersion_ParseMajor(build);
            minor = aVersion_ParseMinor(build);
            patch = aVersion_ParsePatch(build);
            _stem = stem;
        }
    }
    
    if(aVersion_IsAtLeastCompare(major, minor, patch, 2, 10, 0)) {
        return true;
    }
    else {
        std::cout << "Current firmware version: " 
            << int(major) << "." << int(minor) << "." << int(patch) << std::endl;
        std::cout << "Minimum firmware version required: "
            << int(MIN_MAJOR) << "." << int(MIN_MINOR) << "." << int(MIN_PATCH) << std::endl;
        std::cout << "Please upgrade you firmware via HubTool or Updater" << std::endl;
        return false;
    }
}


static aErr
_kickConnection(CLIObject_t& cliObj) {
    cliObj.stem->disconnect();
    cliObj.stem->reconnect();
    return (cliObj.stem->isConnected()) ? (aErrNone) : (aErrConnection);
}


static void
_verbose_printMode_USBHub3c(const uint32_t& mode, const int& port, const bool& showHeader) {
    int power = (mode & (1 << portPortMode_powerEnabled_Bit)) >> portPortMode_powerEnabled_Bit;
    int hs1 = (mode & (1 << portPortMode_HS1Enabled_Bit)) >> portPortMode_HS1Enabled_Bit;
    int hs2 = (mode & (1 << portPortMode_HS2Enabled_Bit)) >> portPortMode_HS2Enabled_Bit;
    int ss1 = (mode & (1 << portPortMode_SS1Enabled_Bit)) >> portPortMode_SS1Enabled_Bit;
    int ss2 = (mode & (1 << portPortMode_SS2Enabled_Bit)) >> portPortMode_SS2Enabled_Bit;
    int cc1 = (mode & (1 << portPortMode_CC1Enabled_Bit)) >> portPortMode_CC1Enabled_Bit;
    int cc2 = (mode & (1 << portPortMode_CC2Enabled_Bit)) >> portPortMode_CC2Enabled_Bit;
    int vconn1 = (mode & (1 << portPortMode_Vconn1Enabled_Bit)) >> portPortMode_Vconn1Enabled_Bit;
    int vconn2 = (mode & (1 << portPortMode_Vconn2Enabled_Bit))  >> portPortMode_Vconn2Enabled_Bit;
    int pwrMode = (mode & (portPortMode_portPowerMode_Mask << portPortMode_portPowerMode_Offset)) >> portPortMode_portPowerMode_Offset;
    
    if(showHeader) {
        std::cout << "Port Power HS1 HS2 SS1 SS2 CC1 CC2 VConn1 VConn2 Power-Mode" << std::endl;
    }
    
    std::cout << std::setfill(' ') << std::setw(4) << port;
    std::cout << std::setfill(' ') << std::setw(6) << power;
    std::cout << std::setfill(' ') << std::setw(4) << hs1;
    std::cout << std::setfill(' ') << std::setw(4) << hs2;
    std::cout << std::setfill(' ') << std::setw(4) << ss1;
    std::cout << std::setfill(' ') << std::setw(4) << ss2;
    std::cout << std::setfill(' ') << std::setw(4) << cc1;
    std::cout << std::setfill(' ') << std::setw(4) << cc2;
    std::cout << std::setfill(' ') << std::setw(7) << vconn1;
    std::cout << std::setfill(' ') << std::setw(7) << vconn2;
    
    std::cout << std::setfill(' ') << std::setw(11) << _verbose_printMode_decodeModeString(pwrMode);
    std::cout << std::endl;
}


static void
_verbose_printMode_USBHub3p(const uint32_t& mode, const int& port, const bool& showHeader) {
    int power = (mode & (1 << portPortMode_powerEnabled_Bit)) >> portPortMode_powerEnabled_Bit;
    int hs = (mode & (1 << portPortMode_HS1Enabled_Bit)) >> portPortMode_HS1Enabled_Bit;
    int ss = (mode & (1 << portPortMode_SS1Enabled_Bit)) >> portPortMode_SS1Enabled_Bit;
    int pwrMode = (mode & (portPortMode_portPowerMode_Mask << portPortMode_portPowerMode_Offset)) >> portPortMode_portPowerMode_Offset;
    
    if(showHeader) {
        std::cout << "Port Power HS SS Power-Mode" << std::endl;
    }
    
    std::cout << std::setfill(' ') << std::setw(4) << port;
    std::cout << std::setfill(' ') << std::setw(6) << power;
    std::cout << std::setfill(' ') << std::setw(3) << hs;
    std::cout << std::setfill(' ') << std::setw(3) << ss;
    
    std::cout << std::setfill(' ') << std::setw(11) << _verbose_printMode_decodeModeString(pwrMode);
    std::cout << std::endl;
}


static void
_verbose_printMode_USBHub2x4(const uint32_t& mode, const int& port, const bool& showHeader) {
    int power = (mode & (1 << portPortMode_powerEnabled_Bit)) >> portPortMode_powerEnabled_Bit;
    int hs = (mode & (1 << portPortMode_HS1Enabled_Bit)) >> portPortMode_HS1Enabled_Bit;
    int pwrMode = (mode & (portPortMode_portPowerMode_Mask << portPortMode_portPowerMode_Offset)) >> portPortMode_portPowerMode_Offset;
    
    if(showHeader) {
        std::cout << "Port Power HS Power-Mode" << std::endl;
    }
    
    std::cout << std::setfill(' ') << std::setw(4) << port;
    std::cout << std::setfill(' ') << std::setw(6) << power;
    std::cout << std::setfill(' ') << std::setw(3) << hs;
    
    std::cout << std::setfill(' ') << std::setw(11) << _verbose_printMode_decodeModeString(pwrMode);
    std::cout << std::endl;
}


static std::string 
_verbose_printMode_decodeModeString(const uint32_t& mode) {
    switch (mode) {
        case portPowerMode_none_Value:      return "NONE";
        case portPowerMode_sdp_Value:       return "SDP";
        case portPowerMode_cdp_dcp_Value:   return "CDP/DCP";
        case portPowerMode_qc_Value:        return "QC";
        case portPowerMode_pd_Value:        return "PD";
        case portPowerMode_ps_Value:        return "PS";
        default:                            return "Unknown";
    }
}


static std::string
_verbose_upstreamPortNumber(const uint32_t& model, const uint8_t& upstreamPort) {
    std::string s = "(Port: " + std::to_string(int(upstreamPort)) + ")";
    switch (model) {
        case aMODULE_TYPE_USBHub3c:
            return std::to_string(int(upstreamPort));
            
        case aMODULE_TYPE_USBHub3p:
            if     (9 == upstreamPort)  { return "Up0 " + s; }
            else if(10 == upstreamPort) { return "Up1 " + s; }
            else { return "USBHub3p: Invalid upstream port number"; }
            
        case aMODULE_TYPE_USBHub2x4:
            if     (4 == upstreamPort) { return "0 " + s; }
            else if(5 == upstreamPort) { return "1 " + s; }
            else { return "USBHub2x4: Invalid upstream port number"; }
            
        default: 
            return "Unsupported model: " + std::to_string(int(model));
    }//end switch
}
