#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
#endif // #ifdef _MSC_VER
#include <windows.h>
#include <process.h>
#include <iostream>
#include <set>
#include <vector>
#include <apps/Common/exampleHelper.h>
#include <mvIMPACT_CPP/mvIMPACT_acquire.h>
#include <mvDisplay/Include/mvIMPACT_acquire_display.h>

using namespace std;
using namespace mvIMPACT::acquire;
using namespace mvIMPACT::acquire::display;

static bool s_boTerminated = false;

//-----------------------------------------------------------------------------
struct DisplayInfo
//-----------------------------------------------------------------------------
{
    ImageDisplayWindow displayWindow;
    // we always have to keep one frame locked as the display module might want to repaint the image e.g. during sizing, thus we
    // cannot free it unless we have a assigned the display to a new buffer.
    int lastRequestNr;
    unsigned int framesCaptured;
    explicit DisplayInfo( const std::string& windowTitle ) : displayWindow( windowTitle ), lastRequestNr( INVALID_ID ), framesCaptured( 0 ) {}
};

//-----------------------------------------------------------------------------
struct ThreadParameter
//-----------------------------------------------------------------------------
{
    Device*              pDev;
    vector<DisplayInfo*> displayData;
    int                  sensorHeadCount;
    ThreadParameter( Device* p, const int shc ) : pDev( p ), sensorHeadCount( shc ) {}
};

//-----------------------------------------------------------------------------
class CTime
//-----------------------------------------------------------------------------
{
    //-----------------------------------------------------------------------------
    struct CTimeImpl
    //-----------------------------------------------------------------------------
    {
        LARGE_INTEGER frequency;
        LARGE_INTEGER start;
        LARGE_INTEGER end;
        CTimeImpl()
        {
            QueryPerformanceFrequency( &frequency );
            QueryPerformanceCounter( &start );
        }
    };
    struct CTimeImpl* m_pImpl;
public:
    /// \brief Constructs a new time measurement object
    ///
    /// This constructs the new object and starts a new measurement.
    ///
    /// \sa
    /// <b>CTime::start()</b>
    explicit CTime()
    {
        m_pImpl = new CTimeImpl();
    }
    ~CTime()
    {
        delete m_pImpl;
    }
    /// \brief Starts a new time measurement.
    ///
    /// Call <b>CTime::elapsed()</b> to get the timespan in sec. after the last call
    /// to <b>CTime::start()</b>.
    void start( void )
    {
        QueryPerformanceCounter( &( m_pImpl->start ) );
    }
    /// \brief Returns the elapsed time in sec.
    ///
    /// Returns the elapsed time in sec. after the last call to <b>CTime::start()</b>
    double elapsed( void )
    {
        QueryPerformanceCounter( &( m_pImpl->end ) );
        return static_cast<double>( m_pImpl->end.QuadPart - m_pImpl->start.QuadPart ) / m_pImpl->frequency.QuadPart;
    }
    /// \brief Returns the elapsed time in sec. and sets this time to the current time
    ///
    /// Returns the elapsed time in sec. after the last call to <b>CTime::start()</b>
    /// or <b>CTime::restart()</b> and sets this time to the current time.
    double restart( void )
    {
        QueryPerformanceCounter( &( m_pImpl->end ) );
        double result = static_cast<double>( m_pImpl->end.QuadPart - m_pImpl->start.QuadPart ) / m_pImpl->frequency.QuadPart;
        m_pImpl->start = m_pImpl->end;
        return result;
    }
};

//-----------------------------------------------------------------------------
bool setupHRTC( Device* pDev, int frameRate_Hz, int exposureTime_us, int sensorHeadCount )
//-----------------------------------------------------------------------------
{
    cout << "Trying to capture at " << frameRate_Hz << " frames per second. Please make sure the device can deliver this frame rate." << endl;

    int frametime_us = static_cast<int>( 1000000.0 * ( 1.0 / static_cast<double>( frameRate_Hz ) ) );
    const int TRIGGER_PULSE_WIDTH_us = 100;
    if( frametime_us < 2 * TRIGGER_PULSE_WIDTH_us )
    {
        cout << "frame rate too high (" << frameRate_Hz << "). Will use 5 Hz." << endl;
        frametime_us = 200000;
    }

    if( exposureTime_us > ( frametime_us - 2 * TRIGGER_PULSE_WIDTH_us ) )
    {
        cout << "exposure time too high(" << exposureTime_us << "). Will use " << frametime_us - 2 * TRIGGER_PULSE_WIDTH_us << " instead" << endl;
        exposureTime_us = frametime_us - 2 * TRIGGER_PULSE_WIDTH_us;
    }

    {
        CameraSettingsBlueCOUGAR csbd( pDev );
        csbd.expose_us.write( exposureTime_us );
        // define a HRTC program that results in a define image frequency
        // the hardware real time controller shall be used to trigger an image
        csbd.triggerSource.write( ctsRTCtrl );
        csbd.triggerMode.write( ctmOnFallingEdge );
    }

    IOSubSystemCommon iossc( pDev );
    // error checks
    if( iossc.RTCtrProgramCount() == 0 )
    {
        // no HRTC controllers available
        cout << "This device (" << pDev->product.read() << ") doesn't support HRTC" << endl;
        return false;
    }

    RTCtrProgram* pRTCtrlprogram = iossc.getRTCtrProgram( 0 );
    if( !pRTCtrlprogram )
    {
        // this only should happen if the system is short of memory
        cout << "Error! No valid program. Short of memory?" << endl;
        return false;
    }

    // start of the program
    // we need 5 steps for the program
    pRTCtrlprogram->setProgramSize( 5 );

    // wait a certain amount of time to achieve the desired frequency
    int progStep = 0;
    RTCtrProgramStep* pRTCtrlStep = pRTCtrlprogram->programStep( progStep++ );
    pRTCtrlStep->opCode.write( rtctrlProgWaitClocks );
    pRTCtrlStep->clocks_us.write( frametime_us - exposureTime_us );

    // trigger both sensor heads
    pRTCtrlStep = pRTCtrlprogram->programStep( progStep++ );
    pRTCtrlStep->opCode.write( rtctrlProgTriggerSet );
    int i = 0;
    for( i = 0; i < sensorHeadCount; i++ )
    {
        pRTCtrlStep->sensorHeads.write( digioOn, i );
    }

    // high time for the trigger signal (should not be smaller than 100 us)
    pRTCtrlStep = pRTCtrlprogram->programStep( progStep++ );
    pRTCtrlStep->opCode.write( rtctrlProgWaitClocks );
    pRTCtrlStep->clocks_us.write( exposureTime_us );

    // end trigger signal
    pRTCtrlStep = pRTCtrlprogram->programStep( progStep++ );
    pRTCtrlStep->opCode.write( rtctrlProgTriggerReset );
    for( i = 0; i < sensorHeadCount; i++ )
    {
        pRTCtrlStep->sensorHeads.write( digioOff, i );
    }

    // restart the program
    pRTCtrlStep = pRTCtrlprogram->programStep( progStep++ );
    pRTCtrlStep->opCode.write( rtctrlProgJumpLoc );
    pRTCtrlStep->address.write( 0 );

    // start the program
    pRTCtrlprogram->mode.write( rtctrlModeRun );

    // Now this device will deliver synchronous images at exactly the desired frequency
    // when it is constantly feed with image requests and the device can deliver
    // images at this frequency.

    return true;
}

//-----------------------------------------------------------------------------
unsigned int __stdcall liveThread( void* pData )
//-----------------------------------------------------------------------------
{
    ThreadParameter* pThreadParameter = reinterpret_cast<ThreadParameter*>( pData );
    unsigned int cnt = 0;

    if( !pThreadParameter->pDev->isOpen() )
    {
        cout << "Initialising the device. This might take some time..." << endl;
        try
        {
            pThreadParameter->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 " << pThreadParameter->pDev->serial.read()
                 << "(error code: " << e.getErrorCodeAsString() << ")." << endl
                 << "Press [ENTER] to end the application..." << endl;
            cin.get();
            return 0;
        }
    }

    vector<HDISP>::size_type displayCount = pThreadParameter->displayData.size();

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

    // make sure enough requests are available
    SystemSettings ss( pThreadParameter->pDev );
    ss.requestCount.write( static_cast<int>( pThreadParameter->displayData.size()*fi.requestCount() ) );
    // pre-fill 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.

    Connector connector( pThreadParameter->pDev );
    // prepare all sensor heads
    ImageRequestControl irc( pThreadParameter->pDev );
    irc.mode.write( ircmTrial );
    for( int i = 0; i < pThreadParameter->sensorHeadCount; i++ )
    {
        connector.videoChannel.write( i );
        fi.imageRequestSingle( &irc );
        int trialRequestNr = fi.imageRequestWaitFor( -1 );
        if( !fi.isRequestNrValid( trialRequestNr ) )
        {
            cout << "An error occurred while preparing device " << pThreadParameter->pDev->serial.read()
                 << "(error code: " << ImpactAcquireException::getErrorCodeAsString( trialRequestNr ) << ")."
                 << "Press [ENTER] to end the application..." << endl;
            cin.get();
            return 0;
        }
        // unlock this request to make it usable for the driver again
        fi.imageRequestUnlock( trialRequestNr );
    }

    // now all the devices sensor heads have been setup to use the current settings.
    // We can now request 'real' frames
    irc.mode.write( ircmManual );
    int input = 0;
    do
    {
        connector.videoChannel.write( input );
        input = ( input + 1 ) % pThreadParameter->sensorHeadCount;
    }
    while( fi.imageRequestSingle( &irc ) == DMR_NO_ERROR );

    // Start the acquisition manually if this was requested(this is to prepare the driver for data capture and tell the device to start streaming data)
    if( pThreadParameter->pDev->acquisitionStartStopBehaviour.read() == assbUser )
    {
        TDMR_ERROR result = DMR_NO_ERROR;
        if( ( result = static_cast<TDMR_ERROR>( fi.acquisitionStart() ) ) != DMR_NO_ERROR )
        {
            cout << "'FunctionInterface.acquisitionStart' returned with an unexpected result: " << result
                 << "(" << ImpactAcquireException::getErrorCodeAsString( result ) << ")" << endl;
        }
    }

    // run thread loop
    const Request* pRequest = 0;
    const unsigned int timeout_ms = 2500;
    int requestNr = INVALID_ID;
    CTime timer;
    while( !s_boTerminated )
    {
        // wait for results from the default capture queue
        requestNr = fi.imageRequestWaitFor( timeout_ms );
        if( fi.isRequestNrValid( requestNr ) )
        {
            pRequest = fi.getRequest( requestNr );
            int videoChannel = pRequest->infoVideoChannel.read();
            if( pRequest->isOK() )
            {
                ++pThreadParameter->displayData.at( videoChannel )->framesCaptured;
                ImageDisplay& display = pThreadParameter->displayData[videoChannel]->displayWindow.GetImageDisplay();
                display.SetImage( pRequest );
                display.Update();
                if( fi.isRequestNrValid( pThreadParameter->displayData[videoChannel]->lastRequestNr ) )
                {
                    // this image is no longer needed by the display...
                    fi.imageRequestUnlock( pThreadParameter->displayData[videoChannel]->lastRequestNr );
                }
                pThreadParameter->displayData[videoChannel]->lastRequestNr = requestNr;
            }
            else
            {
                cout << "Error: " << pRequest->requestResult.readS() << endl;
                fi.imageRequestUnlock( requestNr );
            }

            // send a new image request into the capture queue
            connector.videoChannel.write( videoChannel );
            fi.imageRequestSingle( &irc );

            ++cnt;
            // here we can display some statistical information every 100th image
            if( cnt % 100 == 0 )
            {
                cout << "Info from " << pThreadParameter->pDev->serial.read()
                     << ": " << statistics.errorCount.name() << ": " << statistics.errorCount.readS()
                     << ", " << statistics.framesIncompleteCount.name() << ": " << statistics.framesIncompleteCount.readS()
                     << ", " << statistics.missingDataAverage_pc.name() << ": " << statistics.missingDataAverage_pc.readS();
                for( vector<HDISP>::size_type i = 0; i < displayCount; i++ )
                {
                    cout << ", channel[" << i << "]: " << pThreadParameter->displayData[i]->framesCaptured << " frames captured";
                }
                cout << ", overall frame rate: " << static_cast<double>( cnt ) / timer.elapsed() << " fps";
                cout << endl;
            }
        }
        else
        {
            // If the error code is -2119(DEV_WAIT_FOR_REQUEST_FAILED), the documentation will provide
            // additional information under TDMR_ERROR in the interface reference
            cout << "imageRequestWaitFor failed (" << requestNr << ", " << ImpactAcquireException::getErrorCodeAsString( requestNr ) << ")"
                 << ", timeout value too small?" << endl;
        }
    }

    // Stop the acquisition manually if this was requested
    if( pThreadParameter->pDev->acquisitionStartStopBehaviour.read() == assbUser )
    {
        TDMR_ERROR result = DMR_NO_ERROR;
        if( ( result = static_cast<TDMR_ERROR>( fi.acquisitionStop() ) ) != DMR_NO_ERROR )
        {
            cout << "'FunctionInterface.acquisitionStop' returned with an unexpected result: " << result
                 << "(" << ImpactAcquireException::getErrorCodeAsString( result ) << ")" << endl;
        }
    }

    for( vector<HDISP>::size_type j = 0; j < displayCount; j++ )
    {
        // stop the display from showing freed memory
        pThreadParameter->displayData[j]->displayWindow.GetImageDisplay().RemoveImage();
        // free buffer that is locked for the display
        if( fi.isRequestNrValid( pThreadParameter->displayData[j]->lastRequestNr ) )
        {
            fi.imageRequestUnlock( pThreadParameter->displayData[j]->lastRequestNr );
        }
    }

    // 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( fi.isRequestNrValid( requestNr ) )
    {
        fi.imageRequestUnlock( requestNr );
    }
    // clear all queues
    fi.imageRequestReset( 0, 0 );
    return 0;
}

//-----------------------------------------------------------------------------
bool isDeviceSupportedBySample( const Device* const pDev )
//-----------------------------------------------------------------------------
{
    return ( match( pDev->product.read(), string( "mvBlueLYNX-M7" ), '*' ) == 0 );
}

//-----------------------------------------------------------------------------
int main( int argc, char* argv[] )
//-----------------------------------------------------------------------------
{
    cout << "This sample is meant for mvBlueLYNX-M7 devices only. Other devices might be installed" << endl
         << "but won't be recognized by the application." << endl
         << endl;

    int exposureTime_us = 20000;
    int frameRate_Hz = 5;
    int width = -1;
    int height = -1;
    int interPacketDelay = 25;

    // scan command line
    if( argc > 1 )
    {
        for( int i = 1; i < argc; i++ )
        {
            const string param( argv[i] );
            string::size_type keyEnd = param.find_first_of( "=" );
            if( ( keyEnd == string::npos ) || ( keyEnd == param.length() - 1 ) )
            {
                cout << "Invalid command line parameter: '" << param << "' (ignored)." << endl;
            }
            else
            {
                const string key = param.substr( 0, keyEnd );
                const string value = param.substr( keyEnd + 1 );
                if( ( key == "exposureTime" ) || ( key == "et" ) )
                {
                    exposureTime_us = static_cast<int>( atoi( value.c_str() ) );
                }
                else if( ( key == "frameRate" ) || ( key == "fr" ) )
                {
                    frameRate_Hz = static_cast<int>( atoi( value.c_str() ) );
                }
                else 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 if( key == "scpd" )
                {
                    interPacketDelay = static_cast<int>( atoi( value.c_str() ) );
                }
                else
                {
                    cout << "Invalid command line parameter: '" << param << "' (ignored)." << endl;
                }
            }
        }
    }
    else
    {
        cout << "No command line parameters specified. Available parameters:" << endl
             << "  'frameRate' or 'fr' to specify the frame rate(frames per second per sensor head) of the resulting data stream" << endl
             << "  'exposureTime' or 'et' to specify the exposure time per frame in us" << endl
             << "  'width' or 'w' to specify the width of the AOI" << endl
             << "  'height' or 'h' to specify the height of the AOI" << endl
             << "  'scpd' to specify the inter-packet delay for the GigE Vision stream transmission" << endl
             << endl
             << "USAGE EXAMPLE:" << endl
             << "  SynchronousCaptureMultipleInputs et=5000 frameRate=5" << endl << endl;
    }

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

    try
    {
        cout << "Please note, that this sample (depending on the selected frame rate and resolution) might require a lot" << endl
             << "of network bandwidth, thus to achieve optimal results, it's crucial to have" << endl
             << "  - a good, reliable network controller (we recommend the Intel PRO/1000 series)" << endl
             << "  - the latest driver for the network controller installed" << endl
             << "  - jumbo frames enabled and the receive and transmit buffer for the network controller set to max. values" << endl
             << "  - the InterfaceMTU on the mvBlueLYNX-M7 set to its maximum value" << endl
             << endl
             << "In case of 'rrFrameIncomplete' errors the reason is most certainly to be found in one of"
             << "the requirements listed above not being met."
             << endl;

        cout << "Will try to synchronize sensor heads(" << frameRate_Hz << " fps per head with " << exposureTime_us << "us exposure time per frame) now" << endl
             << "During this operation the device will be initialised. This might take some time..." << endl;

        Connector connector( pDev );
        CameraSettingsBlueCOUGAR cs( pDev );
        try
        {
            if( width != -1 )
            {
                cs.aoiWidth.write( width );
            }
            if( height != -1 )
            {
                cs.aoiHeight.write( height );
            }
        }
        catch( const ImpactAcquireException& e )
        {
            cout << "Failed to set up AOI: " << e.getErrorString() << "(" << e.getErrorCodeAsString() << ")" << endl;
        }
        const int sensorHeadCount = connector.videoChannel.read( plMaxValue ) + 1;
        setupHRTC( pDev, frameRate_Hz, exposureTime_us, sensorHeadCount );

        ThreadParameter threadParam( pDev, sensorHeadCount );
        DeviceComponentLocator locator( pDev, dltSystemSettings );
        PropertyI64 gevStreamChannelSelector;
        locator.bindComponent( gevStreamChannelSelector, "GevStreamChannelSelector" );
        PropertyI64 gevSCPD;
        locator.bindComponent( gevSCPD, "GevSCPD" );
        for( int i = 0; i < sensorHeadCount; i++ )
        {
            gevStreamChannelSelector.write( i );
            gevSCPD.write( interPacketDelay );
            // initialise display windows
            // IMPORTANT: It's NOT save to create multiple display windows in multiple threads!!!
            ostringstream oss;
            oss << "mvIMPACT_acquire sample, Device " << pDev->serial.read() << ", sensor head " << i;
            threadParam.displayData.push_back( new DisplayInfo( oss.str() ) );
        }

        // start the execution of the 'live' thread.
        cout << "Press [ENTER] to end the application" << endl;
        unsigned int dwThreadID;
        HANDLE hThread = ( HANDLE )_beginthreadex( 0, 0, liveThread, ( LPVOID )( &threadParam ), 0, &dwThreadID );
        cin.get();
        s_boTerminated = true;
        WaitForSingleObject( hThread, INFINITE );
        CloseHandle( hThread );
        hThread = 0;
        vector<HDISP>::size_type displayCount = threadParam.displayData.size();
        for( vector<HDISP>::size_type j = 0; j < displayCount; j++ )
        {
            delete threadParam.displayData[j];
            threadParam.displayData[j] = 0;
        }
    }
    catch( const ImpactAcquireException& e )
    {
        // this e.g. might happen if the same device is already opened in another process...
        cout << "An error occurred while configuring device " << pDev->serial.read()
             << "(error code: " << e.getErrorCodeAsString() << ")." << endl
             << "Press [ENTER] to end the application..." << endl;
        cin.get();
        return 1;
    }
    return 0;
}
