#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <thread>
#include <apps/Common/exampleHelper.h>
#include <mvIMPACT_CPP/mvIMPACT_acquire_GenICam.h>
#include <mvIMPACT_CPP/mvIMPACT_acquire_GenICam_CustomCommands.h>
#ifdef _WIN32
#   include <mvDisplay/Include/mvIMPACT_acquire_display.h>
using namespace mvIMPACT::acquire::display;
#   define USE_DISPLAY
#else
#   include <stdio.h>
#   include <unistd.h>
#endif // #ifdef _WIN32

using namespace std;
using namespace mvIMPACT::acquire;

//=============================================================================
//================= Data type definitions =====================================
//=============================================================================
//-----------------------------------------------------------------------------
struct SequencerSetParameter
//-----------------------------------------------------------------------------
{
    const int64_type setNr_;
    const int64_type sequencerSetNext_;
    const double exposureTime_us_;
    explicit SequencerSetParameter( const int64_type setNr, const int64_type sequencerSetNext, const double exposureTime_us ) :
        setNr_( setNr ), sequencerSetNext_( sequencerSetNext ), exposureTime_us_( exposureTime_us ) {}
};

//-----------------------------------------------------------------------------
struct ThreadParameter
//-----------------------------------------------------------------------------
{
    Device* pDev;
    GenICam::AcquisitionControl ac;
    GenICam::SequencerControl sc;
    GenICam::CustomCommandGenerator ccg;
    int width;
    int height;

#ifdef USE_DISPLAY
    ImageDisplayWindow displayWindow;
#endif // #ifdef USE_DISPLAY
    explicit ThreadParameter( Device* p, int w, int h ) : pDev( p ), ac( p ), sc( p ), ccg( p ), width( w ), height( h )
#ifdef USE_DISPLAY
        // initialise display window
        // IMPORTANT: It's NOT save to create multiple display windows in multiple threads!!!
        , displayWindow( "mvIMPACT_acquire sample, Device " + pDev->serial.read() )
#endif // #ifdef USE_DISPLAY
    {}
    ThreadParameter( const ThreadParameter& src ) = delete;
    ThreadParameter& operator=( const ThreadParameter& rhs ) = delete;
};

//=============================================================================
//================= static variables ==========================================
//=============================================================================
static bool s_boTerminated = false;
static array<SequencerSetParameter, 6> s_SequencerData =
{
    SequencerSetParameter( 0, 1,  5000. ), // Set 0: Capture 1 frame with an exposure time of  5000 us, then jump to set 1
    SequencerSetParameter( 1, 2, 10000. ), // Set 1: Capture 1 frame with an exposure time of 10000 us, then jump to set 2
    SequencerSetParameter( 2, 3, 15000. ), // Set 2: Capture 1 frame with an exposure time of 15000 us, then jump to set 3
    SequencerSetParameter( 3, 4, 20000. ), // Set 3: Capture 1 frame with an exposure time of 20000 us, then jump to set 4
    SequencerSetParameter( 4, 5, 25000. ), // Set 4: Capture 1 frame with an exposure time of 25000 us, then jump to set 5
    SequencerSetParameter( 5, 0, 30000. )  // Set 5: Capture 1 frame with an exposure time of 30000 us, then jump back to set 0
};

static const array<double, 6> s_SequencerExposureDataForRuntimeChange = { 50000., 60000., 70000., 80000., 90000., 100000. };

//=============================================================================
//================= function declarations =====================================
//=============================================================================
static void configureDevice( Device* pDev );
static bool isDeviceSupportedBySample( const Device* const pDev );
static void liveThread( ThreadParameter* pTP );

//=============================================================================
//================= implementation ============================================
//=============================================================================
// Calls the function bound to an mvIMPACT::acquire::Method object and displays
// an error message if the function call did fail.
void checkedMethodCall( Device* pDev, Method& method )
//-----------------------------------------------------------------------------
{
    const TDMR_ERROR result = static_cast<TDMR_ERROR>( method.call() );
    if( result != DMR_NO_ERROR )
    {
        std::cout << "An error was returned while calling function '" << method.displayName() << "' on device " << pDev->serial.read()
                  << "(" << pDev->product.read() << "): " << ImpactAcquireException::getErrorCodeAsString( result ) << endl;
    }
}

//-----------------------------------------------------------------------------
// Configures all the stuff that needs to be done only once. All the stuff related
// to setting up the actual sequencer could be called multiple times whenever an
// application gets re-configured. This is not the case here, but the code has been
// split in order to logically group what belongs together.
//
// Whenever 'conditionalSetEnumPropertyByString' or 'conditionalSetProperty' is
// not used here the stuff MUST succeed as otherwise when the device doesn't allow
// this feature the whole example does not work!
void configureDevice( Device* pDev )
//-----------------------------------------------------------------------------
{
    try
    {
        // Restore the factory default first in order to make sure nothing is incorrectly configured
        GenICam::UserSetControl usc( pDev );
        conditionalSetEnumPropertyByString( usc.userSetSelector, "Default" );
        const TDMR_ERROR result = static_cast<TDMR_ERROR>( usc.userSetLoad.call() );
        if( result != DMR_NO_ERROR )
        {
            std::cout << "An error occurred while restoring the factory default for device " << pDev->serial.read()
                      << "(error code: " << ImpactAcquireException::getErrorCodeAsString( result ) << ")." << endl;
        }

        // Auto exposure or an open shutter will not be helpful for this example thus switch it off if possible.
        GenICam::AcquisitionControl acqc( pDev );
        conditionalSetEnumPropertyByString( acqc.exposureMode, "Timed" );
        conditionalSetEnumPropertyByString( acqc.exposureAuto, "Off" );

        // Auto gain will not be helpful for this example either thus switch it off if possible.
        GenICam::AnalogControl ac( pDev );
        if( ac.gainSelector.isValid() )
        {
            // There might be more than a single 'Gain' as a 'GainSelector' is present. Iterate over all
            // 'Gain's that can be configured and switch off every 'Auto' feature detected.
            vector<string> validGainSelectorValues;
            ac.gainSelector.getTranslationDictStrings( validGainSelectorValues );
            for( const auto& gainSelectorValue : validGainSelectorValues )
            {
                conditionalSetEnumPropertyByString( ac.gainSelector, gainSelectorValue );
                conditionalSetEnumPropertyByString( ac.gainAuto, "Off" );
            }
        }
        else
        {
            // There is just a single 'Gain' turn off the 'Auto' feature if supported.
            conditionalSetEnumPropertyByString( ac.gainAuto, "Off" );
        }

        // Chunk mode is needed in order to get back all the information needed to properly check
        // if an image has been taken using the desired parameters.
        GenICam::ChunkDataControl cdc( pDev );
        cdc.chunkModeActive.write( bTrue );

        // We want to act fast, thus if e.g. Bayer-images arrive in the system do NOT convert them on the fly as depending
        // on the device speed the host system might be too slow deal with the amount of data
        ImageProcessing ip( pDev );
        ip.colorProcessing.write( cpmRaw );
        if( ip.tapSortEnable.isValid() )
        {
            ip.tapSortEnable.write( bFalse );
        }
    }
    catch( const ImpactAcquireException& e )
    {
        // This e.g. might happen if the same device is already opened in another process...
        std::cout << "An error occurred while configuring the device " << pDev->serial.read()
                  << "(error code: " << e.getErrorCodeAsString() << ")." << endl
                  << "Press [ENTER] to end the application..." << endl;
        cin.get();
        exit( 1 );
    }
}

//-----------------------------------------------------------------------------
void displayCommandLineOptions( void )
//-----------------------------------------------------------------------------
{
    cout << "Available parameters:" << endl
         << "  'width' or 'w' to specify the width of the ROI that will be used in sequencer sets" << endl
         << "  'height' or 'h' to specify the height of the ROI that will be used in sequencer sets" << endl
         << endl
         << "USAGE EXAMPLE:" << endl
         << "  GenICamSequencerParameterChangeAtRuntime width=200 height = 200" << endl << endl;
}

//-----------------------------------------------------------------------------
// Configures a single 'SequencerSet' so that 'X' frames are captured using a
// certain exposure time and afterwards another sets will be used.
void configureSequencerSet( ThreadParameter* pTP, const SequencerSetParameter& ssp )
//-----------------------------------------------------------------------------
{
    pTP->sc.sequencerSetSelector.write( ssp.setNr_ );
    pTP->ac.exposureTime.write( ssp.exposureTime_us_ );
    pTP->sc.sequencerPathSelector.write( 0LL );
    pTP->sc.sequencerTriggerSource.writeS( "ExposureEnd" );
    pTP->sc.sequencerSetNext.write( ssp.sequencerSetNext_ );
    checkedMethodCall( pTP->pDev, pTP->sc.sequencerSetSave );
}

//-----------------------------------------------------------------------------
// This function will configure the sequencer on the device to take a sequence of
// 'X' images with different exposure times. To change the sequence edit the
// 's_SequencerData' data array and recompile the application.
void configureSequencer( ThreadParameter* pTP )
//-----------------------------------------------------------------------------
{
    try
    {
        pTP->sc.sequencerMode.writeS( "Off" );
        pTP->sc.sequencerConfigurationMode.writeS( "On" );
        pTP->sc.sequencerFeatureSelector.writeS( "ExposureTime" );
        pTP->sc.sequencerFeatureEnable.write( bTrue );
        for_each( s_SequencerData.begin(), s_SequencerData.end(), [pTP]( const SequencerSetParameter & sequencerSetParameter )
        {
            configureSequencerSet( pTP, sequencerSetParameter );
        } );
        pTP->sc.sequencerSetStart.write( 0 );
        pTP->sc.sequencerConfigurationMode.writeS( "Off" );
        pTP->sc.sequencerMode.writeS( "On" );
    }
    catch( const ImpactAcquireException& e )
    {
        std::cout << "An error occurred while setting up the sequencer for device " << pTP->pDev->serial.read()
                  << "(error code: " << e.getErrorCodeAsString() << ")." << endl;
        s_boTerminated = true;
    }
}

//-----------------------------------------------------------------------------
// This function will configure the sequencer on the device at runtime without
// stopping the acquisition of the sequencer program. This approach is much faster
// than the usual way.
void configureSequencerAtRuntime( ThreadParameter* pTP, const bool boApplyOriginalData )
//-----------------------------------------------------------------------------
{
    try
    {
        if( boApplyOriginalData )
        {
            for( size_t i = 0; i < s_SequencerData.size(); i++ )
            {
                cout << "Will set the exposure time of sequencer set " << i << " to " << s_SequencerData[i].exposureTime_us_ << "us." << endl;
                pTP->ccg.queueSequencerSetValueModification( static_cast<int64_type>( i ), sspExposureTime, s_SequencerData[i].exposureTime_us_ );
                pTP->ccg.queueSequencerSetValueModification( static_cast<int64_type>( i ), sspExposureTime, s_SequencerData[i].exposureTime_us_ );
            }
        }
        else
        {
            for( size_t i = 0; i < s_SequencerExposureDataForRuntimeChange.size(); i++ )
            {
                cout << "Will set the exposure time of sequencer set " << i << " to " << s_SequencerExposureDataForRuntimeChange[i] << "us." << endl;
                pTP->ccg.queueSequencerSetValueModification( static_cast<int64_type>( i ), sspExposureTime, s_SequencerExposureDataForRuntimeChange[i] );
            }
        }
        // Set random OffsetX and OffsetY for partial AOIs. Partial AOIs can be set with command line parameters 'width' and 'height'.
        // Use random sequencer set number
        int sequencerSetNumber = rand() % s_SequencerData.size();

        GenICam::ImageFormatControl ifc( pTP->pDev );
        if( pTP->width != 0 )
        {
            int64_t offsetX = rand() % ( ifc.sensorWidth.read() - pTP->width );
            offsetX -= offsetX % ifc.offsetX.getStepWidth();
            cout << "Will set OffsetX of sequencer set " << sequencerSetNumber << " to " << offsetX << "." << endl;
            pTP->ccg.queueSequencerSetValueModification( static_cast<int64_type>( sequencerSetNumber ), sspOffsetX, offsetX );
        }
        if( pTP->height != 0 )
        {
            int64_t offsetY = rand() % ( ifc.sensorHeight.read() - pTP->height );
            offsetY -= offsetY % ifc.offsetY.getStepWidth();
            cout << "Will set OffsetY of sequencer set " << sequencerSetNumber << " to " << offsetY << "." << endl;
            pTP->ccg.queueSequencerSetValueModification( static_cast<int64_type>( sequencerSetNumber ), sspOffsetY, offsetY );
        }

        int sendCommandBufferResult = pTP->ccg.sendCommandBuffer();
        if( sendCommandBufferResult != DMR_NO_ERROR )
        {
            cout << "sendCommandBuffer failed(error code: " << sendCommandBufferResult << "(" << ImpactAcquireException::getErrorCodeAsString( sendCommandBufferResult ) << "))!" << endl;
        }
    }
    catch( const ImpactAcquireException& e )
    {
        std::cout << "An error occurred while setting up the sequencer for device " << pTP->pDev->serial.read()
                  << "(error code: " << e.getErrorCodeAsString() << ")." << endl;
        s_boTerminated = true;
    }
}

//-----------------------------------------------------------------------------
// This function will allow to select devices that support the GenICam interface
// layout(these are devices, that claim to be compliant with the GenICam standard)
// and that are bound to drivers that support the user controlled start and stop
// of the internal acquisition engine. Other devices will not be listed for
// selection as the code of the example relies on these features in the code.
bool isDeviceSupportedBySample( const Device* const pDev )
//-----------------------------------------------------------------------------
{
    if( !pDev->interfaceLayout.isValid() &&
        !pDev->acquisitionStartStopBehaviour.isValid() )
    {
        return false;
    }

    vector<TDeviceInterfaceLayout> availableInterfaceLayouts;
    pDev->interfaceLayout.getTranslationDictValues( availableInterfaceLayouts );
    return find( availableInterfaceLayouts.begin(), availableInterfaceLayouts.end(), dilGenICam ) != availableInterfaceLayouts.end();
}


//-----------------------------------------------------------------------------
void liveThread( ThreadParameter* pTP )
//-----------------------------------------------------------------------------
{
    // Set width and height according to command line parameters
    GenICam::ImageFormatControl ifc( pTP->pDev );
    if( pTP->width != 0 )
    {
        ifc.width.write( pTP->width );
    }
    if( pTP->height != 0 )
    {
        ifc.height.write( pTP->height );
    }

    // establish access to the statistic properties
    Statistics statistics( pTP->pDev );
    // create an interface to the device found
    FunctionInterface fi( pTP->pDev );
    configureSequencer( pTP );

    // Send all requests to the capture queue. There can be more than 1 queue for some devices, but for this sample
    // we will work with the default capture queue. If a device supports more than one capture or result
    // queue, this will be stated in the manual. If nothing is mentioned about it, the device supports one
    // queue only. This loop will send all requests currently available to the driver. To modify the number of requests
    // use the property mvIMPACT::acquire::SystemSettings::requestCount at runtime or the property
    // mvIMPACT::acquire::Device::defaultRequestCount BEFORE opening the device.
    TDMR_ERROR result = DMR_NO_ERROR;
    while( ( result = static_cast<TDMR_ERROR>( fi.imageRequestSingle() ) ) == DMR_NO_ERROR ) {};
    if( result != DEV_NO_FREE_REQUEST_AVAILABLE )
    {
        cout << "'FunctionInterface.imageRequestSingle' returned with an unexpected result: " << result
             << "(" << ImpactAcquireException::getErrorCodeAsString( result ) << ")" << endl;
    }

    manuallyStartAcquisitionIfNeeded( pTP->pDev, fi );
    // run thread loop
    mvIMPACT::acquire::Request* pRequest = nullptr;
    // we always have to keep at least 2 images as the displayWindow module might want to repaint the image, thus we
    // can free it unless we have a assigned the displayWindow to a new buffer.
    mvIMPACT::acquire::Request* pPreviousRequest = nullptr;
    const unsigned int timeout_ms = {500};
    bool boApplyOriginalData = false;
    unsigned int cnt = {0};
    while( !s_boTerminated )
    {
        // wait for results from the default capture queue
        int requestNr = fi.imageRequestWaitFor( timeout_ms );
        pRequest = fi.isRequestNrValid( requestNr ) ? fi.getRequest( requestNr ) : nullptr;
        if( pRequest != nullptr )
        {
            if( pRequest->isOK() )
            {
                // within this scope we have a valid buffer of data that can be an image or any other chunk of data.
                ++cnt;
                // here we can display some statistical information every 100th image
                if( cnt % 100 == 0 )
                {
                    cout << "Info from " << pTP->pDev->serial.read()
                         << ": " << statistics.framesPerSecond.name() << ": " << statistics.framesPerSecond.readS()
                         << ", " << statistics.errorCount.name() << ": " << statistics.errorCount.readS()
                         << ", " << statistics.captureTime_s.name() << ": " << statistics.captureTime_s.readS() << endl;
                    configureSequencerAtRuntime( pTP, boApplyOriginalData );
                    boApplyOriginalData = !boApplyOriginalData;
                }
                cout << "Image captured" << ": " << pRequest->imageOffsetX.read() << "x" << pRequest->imageOffsetY.read()
                     << "@" << pRequest->imageWidth.read() << "x" << pRequest->imageHeight.read() << ". SequencerSetActive: " << pRequest->chunkSequencerSetActive.read() << ", exposure time: " << pRequest->chunkExposureTime.read() << endl;
#ifdef USE_DISPLAY
                pTP->displayWindow.GetImageDisplay().SetImage( pRequest );
                pTP->displayWindow.GetImageDisplay().Update();
#endif // #ifdef USE_DISPLAY
            }
            if( pPreviousRequest )
            {
                // this image has been displayed thus the buffer is no longer needed...
                pPreviousRequest->unlock();
            }
            pPreviousRequest = pRequest;
            // send a new image request into the capture queue
            fi.imageRequestSingle();
        }
        //else
        //{
        // Please note that slow systems or interface technologies in combination with high resolution sensors
        // might need more time to transmit an image than the timeout value which has been passed to imageRequestWaitFor().
        // If this is the case simply wait multiple times OR increase the timeout(not recommended as usually not necessary
        // and potentially makes the capture thread less responsive) and rebuild this application.
        // Once the device is configured for triggered image acquisition and the timeout elapsed before
        // the device has been triggered this might happen as well.
        // The return code would be -2119(DEV_WAIT_FOR_REQUEST_FAILED) in that case, the documentation will provide
        // additional information under TDMR_ERROR in the interface reference.
        // If waiting with an infinite timeout(-1) it will be necessary to call 'imageRequestReset' from another thread
        // to force 'imageRequestWaitFor' to return when no data is coming from the device/can be captured.
        // cout << "imageRequestWaitFor failed (" << requestNr << ", " << ImpactAcquireException::getErrorCodeAsString( requestNr ) << ")"
        //   << ", timeout value too small?" << endl;
        //}
    }
    manuallyStopAcquisitionIfNeeded( pTP->pDev, fi );

#ifdef USE_DISPLAY
    // stop the display from showing freed memory
    pTP->displayWindow.GetImageDisplay().RemoveImage();
#endif // #ifdef USE_DISPLAY

    // In this sample all the next lines are redundant as the device driver will be
    // closed now, but in a real world application a thread like this might be started
    // several times an then it becomes crucial to clean up correctly.

    // free the last potentially locked request
    if( pRequest != nullptr )
    {
        pRequest->unlock();
    }
    // clear all queues
    fi.imageRequestReset( 0, 0 );
}

//-----------------------------------------------------------------------------
int main( int argc, char* argv[] )
//-----------------------------------------------------------------------------
{
    int width = { 0 };
    int height = { 0 };

    bool boInvalidCommandLineParameterDetected = false;
    // scan command line
    if( argc > 1 )
    {
        for( int i = 1; i < argc; i++ )
        {
            const string param( argv[i] );
            const auto 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 == "width" ) || ( key == "w" ) )
                {
                    width = static_cast<int>( atoi( value.c_str() ) );
                }
                else if( ( key == "height" ) || ( key == "h" ) )
                {
                    height = static_cast<int>( atoi( value.c_str() ) );
                }
                else
                {
                    cout << "Invalid command line parameter: '" << param << "' (ignored)." << endl;
                    boInvalidCommandLineParameterDetected = true;
                }
            }
        }
        if( boInvalidCommandLineParameterDetected )
        {
            displayCommandLineOptions();
        }
    }
    else
    {
        cout << "No command line parameters specified." << endl;
        cout << "Full AOI will be used and no randomly changing of OffsetX/OffsetY will happen." << endl;
        displayCommandLineOptions();
    }

    DeviceManager devMgr;
    Device* pDev = getDeviceFromUserInput( devMgr, isDeviceSupportedBySample );
    if( pDev == nullptr )
    {
        cout << "Could not obtain a valid pointer to a device. Unable to continue!";
        cout << "Press [ENTER] to end the application" << endl;
        cin.get();
        return 1;
    }

    try
    {
        cout << "Initialising the device. This might take some time..." << endl << endl;
        pDev->interfaceLayout.write( dilGenICam ); // This is also done 'silently' by the 'getDeviceFromUserInput' function but your application needs to do this as well so state this here clearly!
        pDev->open();
    }
    catch( const ImpactAcquireException& e )
    {
        // this e.g. might happen if the same device is already opened in another process...
        cout << "An error occurred while opening device " << pDev->serial.read()
             << "(error code: " << e.getErrorCodeAsString() << ")." << endl
             << "Press [ENTER] to end the application..." << endl;
        cin.get();
        return 1;
    }

    try
    {
        configureDevice( pDev );
        // start the execution of the 'live' thread.
        cout << "Press [ENTER] to end the application" << endl;
        ThreadParameter threadParam( pDev, width, height );
        thread myThread( liveThread, &threadParam );
        cin.get();
        s_boTerminated = true;
        myThread.join();
    }
    catch( const ImpactAcquireException& e )
    {
        // this e.g. might happen if the same device is already opened in another process...
        cout << "An error occurred while setting up device " << pDev->serial.read()
             << "(error code: " << e.getErrorCodeAsString() << ")." << endl
             << "Press [ENTER] to end the application..." << endl;
        cin.get();
        return 1;
    }
    return 0;
}
