#ifdef _MSC_VER // is Microsoft compiler?
#   if _MSC_VER < 1300  // is 'old' VC 6 compiler?
#       pragma warning( disable : 4786 ) // 'identifier was truncated to '255' characters in the debug information'
#   endif // #if _MSC_VER < 1300
#   include <windows.h>
#endif // #ifdef _MSC_VER
#include <iostream>
#include <apps/Common/exampleHelper.h>
#include <mvIMPACT_CPP/mvIMPACT_acquire.h>

// CLI application that can be used to update a GenICam device using the Firmware Updater API.
// At the moment only updating from a local FW version e.g. as used by mvBlueNAOS is explicitly supported.

using namespace mvIMPACT::acquire;
using namespace std;

//-----------------------------------------------------------------------------
enum DeviceFamily
//-----------------------------------------------------------------------------
{
    dfMvBlueCOUGAR,
    dfMvBlueFOX3,
    dfMvBlueNAOS,
    dfMvBlueFOX,
    dfUnknown
};

//-----------------------------------------------------------------------------
class MyFirmwareUpdater : public mvIMPACT::acquire::FirmwareUpdater
//-----------------------------------------------------------------------------
{
public:
    explicit MyFirmwareUpdater( mvIMPACT::acquire::Device* pDev, bool boForceOverideSameVersion = false, bool boForceDowngrade = false, bool boForceSameVersion = false, bool boForceBreakingChange = false ) : mvIMPACT::acquire::FirmwareUpdater( pDev, boForceOverideSameVersion, boForceDowngrade, boForceSameVersion, boForceBreakingChange ) {}
    virtual int onErrorMessage( const double timeElapsed_s ) const
    {
        cout << "Error: " << statusMessage() << "@" << timeElapsed_s << " s" << endl;
        return fuaCancel;
    }
    virtual int onErasingFlash( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Erasing - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onUnzippingFirmwareArchive( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Unzipping firmware archive - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onUpdatingBootProgrammer( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Updating boot programmer - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onUploadingImage( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Uploading - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onRebooting( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Rebooting - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onHardResetRequired( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Rebooting - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl
             << "Manual hard reset required." << endl;
        return fuaCancel;
    }
    virtual int onSavingUserSets( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Saving sets - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
    virtual int onLoadingUserSets( const int currentProgress_pc, const double timeElapsed_s ) const
    {
        cout << "Loading sets - Update progress: " << currentProgress_pc << ", time " << timeElapsed_s << " s" << endl;
        return fuaContinue;
    }
};

//-----------------------------------------------------------------------------
bool IsGigEVisionDevice( const Device* pDev )
//-----------------------------------------------------------------------------
{
    PropertyI64 deviceIPAddress;
    DeviceComponentLocator locator( pDev->hDev() );
    if( locator.bindComponent( deviceIPAddress, "DeviceIPAddress" ) && ( deviceIPAddress.readS() != "Unavailable" ) )
    {
        return true;
    }
    return false;
}

//-----------------------------------------------------------------------------
bool IsPCIeDevice( const Device* pDev )
//-----------------------------------------------------------------------------
{
    PropertyI64 interfaceDisplayName;
    DeviceComponentLocator locator( pDev->hDev() );
    if( locator.bindComponent( interfaceDisplayName, "InterfaceDisplayName" ) && ( interfaceDisplayName.readS() == "PCIe Interface" ) )
    {
        return true;
    }
    return false;
}

//-----------------------------------------------------------------------------
DeviceFamily GetDeviceFamily( const Device* pDev )
//-----------------------------------------------------------------------------
{
    if( ( pDev->family.readS() == "mvBlueCOUGAR" || pDev->family.readS() == "BVS CA" ) && IsGigEVisionDevice( pDev ) )
    {
        return dfMvBlueCOUGAR;
    }
    else if( pDev->family.readS() == "mvBlueFOX3" || pDev->family.readS() == "BVS CA" )
    {
        return dfMvBlueFOX3;
    }
    else if( ( pDev->family.readS() == "mvBlueNAOS" || pDev->family.readS() == "BVS CA" ) && IsPCIeDevice( pDev ) )
    {
        return dfMvBlueNAOS;
    }
    else if( pDev->family.readS() == "mvBlueFOX" )
    {
        return dfMvBlueFOX;
    }
    return dfUnknown;
}

//-----------------------------------------------------------------------------
bool GetLatestLocalFirmwareVersion( const Device* pDev, string& latestFirmwareVersion )
//-----------------------------------------------------------------------------
{
    const string currentFirmwareVersion = pDev->firmwareVersion.readS();
    PropertyI64 firmwareVersionLatest;
    DeviceComponentLocator locator( pDev->hDev() );
    if( !locator.bindComponent( firmwareVersionLatest, "FirmwareVersionLatest" ) )
    {
        return false;
    }
    latestFirmwareVersion = firmwareVersionLatest.readS();
    return ( currentFirmwareVersion == latestFirmwareVersion );
}

//-----------------------------------------------------------------------------
TDMR_ERROR updateBlueCOUGAROrBlueFOX3( Device* pDev, const string& pathToFirmwareArchive, bool boForceOverideSameVersion, bool boForceDowngrade, bool boForceSameVersion, bool boForceBreakingChange, string& status )
//-----------------------------------------------------------------------------
{
    TDMR_ERROR result = DMR_NO_ERROR;
    MyFirmwareUpdater fwu( pDev, boForceOverideSameVersion, boForceDowngrade, boForceSameVersion, boForceBreakingChange );

    result = fwu.update( pathToFirmwareArchive );
    if( result == DMR_NO_ERROR )
    {
        result = fwu.verifyFirmwareChecksum( pDev, pathToFirmwareArchive, nullptr );
        if( result != DMR_NO_ERROR )
        {
            cout << "Firmware checksum verification: " << fwu.statusMessage() << endl;
        }
        else
        {
            cout << "Firmware checksum verified successful! " << endl;
        }
    }
    if( result != DMR_NO_ERROR )
    {
        status = fwu.statusMessage();
    }
    return result;
}

//-----------------------------------------------------------------------------
TDMR_ERROR updateBlueFOX( Device* pDev, const string& version, string& status )
//-----------------------------------------------------------------------------
{
    TDMR_ERROR result = DMR_NO_ERROR;
    MyFirmwareUpdater fwu( pDev );

    if( !version.empty() )
    {
        // use specific version if specified
        fwu.firmwareVersionToUpload.writeS( version );
    }
    cout << "Updating device " << pDev->serial.read() << " from FW version "
         << pDev->firmwareVersion.readS()
         << " to " << fwu.firmwareVersionToUpload.readS() << "." << endl;
    result = fwu.update();
    if( result != DMR_NO_ERROR )
    {
        status = fwu.statusMessage();
    }
    return result;
}

//-----------------------------------------------------------------------------
TDMR_ERROR updateBlueNAOS( Device* pDev, const bool boForce, string& status )
//-----------------------------------------------------------------------------
{
    string latestFirmwareVersion;
    if( GetLatestLocalFirmwareVersion( pDev, latestFirmwareVersion ) )
    {
        if( !boForce )
        {
            cout << "Skipping device " << pDev->serial.read() << " because it already uses the current FW version ("
                 << pDev->firmwareVersion.readS() << ")." << endl;
            return DMR_NO_ERROR;
        }
        cout << "Device " << pDev->serial.read() << " already uses the current FW version ("
             << pDev->firmwareVersion.readS() << ")." << endl
             << "Not skipping because 'force' specified'." << endl;
    }

    cout << "Updating device " << pDev->serial.read() << " from FW version "
         << pDev->firmwareVersion.readS()
         << " to " << latestFirmwareVersion << "." << endl;
    TDMR_ERROR result = DMR_NO_ERROR;
    MyFirmwareUpdater fwu( pDev );
    result = fwu.update();
    if( result != DMR_NO_ERROR )
    {
        status = fwu.statusMessage();
    }
    return result;
}

//-----------------------------------------------------------------------------
// This function will allow to select devices that support the Firmware API
// as the code of the example relies on these feature in the code.
bool isDeviceSupportedBySample( const Device* const pDev )
//-----------------------------------------------------------------------------
{
    return GetDeviceFamily( pDev ) != dfUnknown;
}

//-----------------------------------------------------------------------------
void displayCommandLineOptions( void )
//-----------------------------------------------------------------------------
{
    cout << "Available command line parameters:" << endl
         << " General:" << endl
         << "  'serial'              or 's' to specify the serial number of the device to use (if not specified the user will be asked to select a device)" << endl
         << "  'unattended'          or 'u' to specify no questions asked, if possible" << endl
         << "  'force'               or 'f' to specify force update even if FW is already up-to-date" << endl
         << "  'help'                or 'h' displays this help text" << endl
         << " mvBlueCOUGAR-(X/XD/XT) and mvBlueFOX3:" << endl
         << "  'forceDowngrade'      or 'd' to force an update even if the specified FW is older than the version already installed" << endl
         << "  'keepUserSets'        or 'k' if UserSets stored on the device should still be present after the update" << endl
         << "  'forceBreakingChange' or 'b' if the device should be updated even if the interface might be different afterwards" << endl
         << "  'pathToFWArchive'     or 'p' to specify the firmware archive which should be used to update the device" << endl
         << " mvBlueFOX:" << endl
         << "  'version'             or 'v' to specify the version that should be used to update the device" << endl

         << endl
         << "USAGE EXAMPLES:" << endl
         << "  FirmwareUpdate s=BN* f=1" << endl
         << "  FirmwareUpdate s=GX* p=\"path to firmware archive\" k=0" << endl
         << "  FirmwareUpdate s=BF* v=49" << endl << endl;
}

//-----------------------------------------------------------------------------
int main( int argc, char* argv[] )
//-----------------------------------------------------------------------------
{
    DeviceManager devMgr;
    Device* pDev = nullptr;
    bool boUnattended = false;
    bool boForceSameVersion = false;
    bool boForceDowngrade = false;
    bool boKeepUserSets = true;
    bool boForceBreakingChange = false;
    string version;
    string pathToFirmwareArchive;

    // scan command line
    if( argc > 1 )
    {
        bool boInvalidCommandLineParameterDetected = false;
        for( int i = 1; i < argc; i++ )
        {
            const string param( argv[i] );
            const string::size_type keyEnd = param.find_first_of( "=" );
            if( ( keyEnd == string::npos ) || ( keyEnd == param.length() - 1 ) )
            {
                cout << "Invalid command line parameter: '" << param << "' (ignored)." << endl;
                boInvalidCommandLineParameterDetected = true;
            }
            else
            {
                const string key = param.substr( 0, keyEnd );
                const string value = param.substr( keyEnd + 1 );
                if( ( key == "serial" ) || ( key == "s" ) )
                {
                    pDev = devMgr.getDeviceBySerial( value );
                }
                else if( ( key == "unattended" ) || ( key == "u" ) )
                {
                    boUnattended = ( atoi( value.c_str() ) != 0 );
                }
                else if( ( key == "force" ) || ( key == "f" ) )
                {
                    boForceSameVersion = ( atoi( value.c_str() ) != 0 );
                }
                else if( ( key == "forceDowngrade" ) || ( key == "d" ) )
                {
                    boForceDowngrade = ( atoi( value.c_str() ) != 0 );
                }
                else if( ( key == "keepUserSets" ) || ( key == "k" ) )
                {
                    boKeepUserSets = ( atoi( value.c_str() ) != 0 );
                }
                else if( ( key == "forceBreakingChange" ) || ( key == "b" ) )
                {
                    boForceBreakingChange = ( atoi( value.c_str() ) != 0 );
                }
                else if( ( key == "version" ) || ( key == "v" ) )
                {
                    version = value;
                }
                else if( ( key == "pathToFWArchive" ) || ( key == "p" ) )
                {
                    pathToFirmwareArchive = value;
                }
                else if( ( key == "help" ) || ( key == "h" ) )
                {
                    displayCommandLineOptions();
                }
                else
                {
                    cout << "Invalid command line parameter: '" << param << "' (ignored)." << endl;
                    boInvalidCommandLineParameterDetected = true;
                }
            }
        }
        if( boInvalidCommandLineParameterDetected )
        {
            displayCommandLineOptions();
        }
    }
    else
    {
        cout << endl << "No command line parameters specified." << endl << endl;
        displayCommandLineOptions();
    }

    if( pDev == nullptr )
    {
        pDev = getDeviceFromUserInput( devMgr, isDeviceSupportedBySample );
    }
    if( pDev == nullptr )
    {
        cout << "Unable to continue!" << endl;
        return 1;
    }
    const DeviceFamily deviceFamily = GetDeviceFamily( pDev );
    if( deviceFamily == dfUnknown )
    {
        cout << "The selected device is not supported by this sample. Unable to continue!" << endl;
        if( !boUnattended )
        {
            cout << "Press [ENTER] to end the application" << endl;
            cin.get();
        }
        return 1;
    }

    if( !boUnattended )
    {
        cout << "Press [ENTER] to update the device." << endl;
        cin.get();
    }
    cout << "Updating " << pDev->serial.read() << "..." << endl;

    string status;
    TDMR_ERROR updateResult = DMR_NO_ERROR;
    try
    {
        switch( deviceFamily )
        {
        case dfMvBlueCOUGAR:
        case dfMvBlueFOX3:
            updateResult = updateBlueCOUGAROrBlueFOX3( pDev, pathToFirmwareArchive, boForceSameVersion, boForceDowngrade, boKeepUserSets, boForceBreakingChange, status );
            break;
        case dfMvBlueFOX:
            updateResult = updateBlueFOX( pDev, version, status );
            break;
        case dfMvBlueNAOS:
            updateResult = updateBlueNAOS( pDev, boForceSameVersion, status );
            break;
        case dfUnknown:
            // should never get here because it is handled above
            break;
        }
    }
    catch( const ImpactAcquireException& e )
    {
        cout << "An error occurred while updating the firmware of the device " << pDev->serial.read()
             << "(error code: " << e.getErrorCodeAsString() << ")." << endl;
        // reading out potential issues
        cout << "Status: " << status << endl;
    }
    cout << "Result code after updating: " << updateResult << "(" << ImpactAcquireException::getErrorCodeAsString( updateResult ) << ")" << endl;

    // update device list explicitly otherwise it contains illegal pointers to the "old" device (mvBlueNAOS)
    devMgr.updateDeviceList();
    if( !boUnattended )
    {
        cout << "Press [ENTER] to end the application" << endl;
        cin.get();
    }
    return updateResult == DMR_NO_ERROR ? 0 : 1;
}
