//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools frameCPP is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

//
//    Framecmp compares the data portion of two frames.
//

#include <iostream>
#include <iomanip>
#include <map>
#include <sstream>
#include <string>

#include <boost/program_options.hpp>
#include <boost/shared_ptr.hpp>

#include "framecpp/FrameH.hh"
#include "framecpp/FrVect.hh"
#include "framecpp/FrRawData.hh"
#include "framecpp/FrAdcData.hh"
#include "framecpp/FrProcData.hh"
#include "framecpp/GPSTime.hh"
#include "framecpp/IFrameStream.hh"

namespace po = boost::program_options;

using namespace std;
using FrameCPP::FrAdcData;
using FrameCPP::FrameH;
using FrameCPP::FrProcData;
using FrameCPP::FrRawData;
using FrameCPP::FrVect;
using FrameCPP::GPSTime;
using FrameCPP::IFrameFStream;

typedef boost::shared_ptr< FrameCPP::FrVect > frvect_pointer;
typedef FrameH::dt_type                       Interval;

static int            verbose = 0;
static frvect_pointer vnull;

typedef pair< frvect_pointer, frvect_pointer > ListData;
typedef map< std::string, ListData >           ScanList;
typedef ScanList::iterator                     ScanIter;

static bool
Almost( const GPSTime& t1, const GPSTime& t2, GPSTime::seconds_type dT = 1 )
{
    GPSTime::seconds_type     dS;
    GPSTime::nanoseconds_type dNS;
    if ( t1 >= t2 )
    {
        dS = t1.GetSeconds( ) - t2.GetSeconds( );
        if ( dS > 1 )
        {
            return false;
        }
        dNS = NANOSECOND_INT_MULTIPLIER * dS + t1.GetNanoseconds( ) -
            t2.GetNanoseconds( );
        if ( dNS > dT )
        {
            return false;
        }
    }
    else
    {
        dS = t2.GetSeconds( ) - t1.GetSeconds( );
        if ( dS > 1 )
        {
            return false;
        }
        dNS = NANOSECOND_INT_MULTIPLIER * dS + t2.GetNanoseconds( ) -
            t1.GetNanoseconds( );
        if ( dNS > dT )
        {
            return false;
        }
    }
    return true;
}

//======================================  Dump data from two FrVects
void
dump( frvect_pointer v,
      int            ov = 0,
      frvect_pointer x = vnull,
      int            ox = 0,
      int            nByte = 0,
      bool           diff_only = true )
{
    int nData = v->GetNData( );
    if ( nByte )
    {
        switch ( v->GetType( ) )
        {
        case FrVect::FR_VECT_2S:
            nData = nByte / 2;
            break;
        case FrVect::FR_VECT_4S:
        case FrVect::FR_VECT_4U:
            nData = nByte / 4;
            break;
        case FrVect::FR_VECT_4R:
            nData = nByte / 4;
            break;
        case FrVect::FR_VECT_8R:
            nData = nByte / 8;
            break;
        default:
            return;
        }
    }
    for ( int i = 0; i < nData; i += 8 )
    {
        int nMax = i + 8;
        if ( nMax > nData )
            nMax = nData;

        //------------------------------  Test for differences
        const char* d = ( (const char*)v->GetData( ).get( ) ) + ov;
        bool        diff = false;
        if ( x )
        {
            const char* dx = ( (const char*)x->GetData( ).get( ) ) + ox;
            switch ( v->GetType( ) )
            {
            case FrVect::FR_VECT_2S:
                for ( int j = i; j < nMax; j++ )
                {
                    diff |= *( (short*)d + j ) != *( (short*)dx + j );
                }
                break;
            case FrVect::FR_VECT_4S:
            case FrVect::FR_VECT_4U:
                for ( int j = i; j < nMax; j++ )
                {
                    diff |= *( (int*)d + j ) != *( (int*)dx + j );
                }
                break;
            case FrVect::FR_VECT_4R:
                for ( int j = i; j < nMax; j++ )
                {
                    diff |= *( (float*)d + j ) != *( (float*)dx + j );
                }
                break;
            case FrVect::FR_VECT_8R:
                for ( int j = i; j < nMax; j++ )
                {
                    diff |= *( (double*)d + j ) != *( (double*)dx + j );
                }
                break;
            default:
                return;
            }
            if ( diff )
                cout << "|";
            else if ( !diff_only )
                cout << " ";
        }

        //------------------------------  Dump the values
        if ( diff || !diff_only )
        {
            cout << setw( 6 ) << i;
            for ( int j = i; j < nMax; j++ )
            {
                switch ( v->GetType( ) )
                {
                case FrVect::FR_VECT_2S:
                    cout << setw( 9 ) << *( (short*)d + j );
                    break;
                case FrVect::FR_VECT_4S:
                case FrVect::FR_VECT_4U:
                    cout << setw( 9 ) << *( (int*)d + j );
                    break;
                case FrVect::FR_VECT_4R:
                    cout << setw( 9 ) << *( (float*)d + j );
                    break;
                case FrVect::FR_VECT_8R:
                    cout << setw( 9 ) << *( (double*)d + j );
                    break;
                default:
                    return;
                }
            }
            cout << endl;
        }
    }
}

class CmpStream
{
public:
    CmpStream( const char* Filename );

    const std::string& Filename( ) const;

    GPSTime GetGTime( ) const;

    Interval GetDt( ) const;

    FrameH::procData_type& RefProcData( );

    FrameH::rawData_type GetRawData( );

    void ReadNextFrame( );

    inline operator bool( ) const
    {
        if ( verbose > 3 )
        {
            cerr << "frame_count: " << frame_count << std::endl;
        }
        if ( frame_count == FRAME_COUNT_FINISHED )
        {
            return false;
        }
        return true;
    }

private:
    static const INT_4S         FRAME_COUNT_UNKNOWN = -2;
    static const INT_4S         FRAME_COUNT_FINISHED = -1;
    IFrameFStream               stream;
    std::string                 filename;
    IFrameFStream::frame_h_type frameh;
    INT_4S                      frame_count;
};

inline CmpStream::CmpStream( const char* Filename )
    : stream( Filename ), filename( Filename )
{
    try
    {
        frame_count = stream.GetNumberOfFrames( );
    }
    catch ( ... )
    {
        frame_count = FRAME_COUNT_UNKNOWN;
    }
    ReadNextFrame( );
}

inline const std::string&
CmpStream::Filename( ) const
{
    return filename;
}

inline GPSTime
CmpStream::GetGTime( ) const
{
    return frameh->GetGTime( );
}

inline Interval
CmpStream::GetDt( ) const
{
    return frameh->GetDt( );
}

inline FrameH::rawData_type
CmpStream::GetRawData( )
{
    return frameh->GetRawData( );
}

inline FrameH::procData_type&
CmpStream::RefProcData( )
{
    return frameh->RefProcData( );
}

inline void
CmpStream::ReadNextFrame( )
{
    if ( frame_count > 0 )
    {
        frameh = stream.ReadNextFrame( );
        --frame_count;
    }
    else if ( frame_count == FRAME_COUNT_UNKNOWN )
    {
        try
        {
            frameh = stream.ReadNextFrame( );
        }
        catch ( ... )
        {
            frame_count = FRAME_COUNT_FINISHED;
        }
    }
    else if ( frame_count == 0 )
    {
        --frame_count;
    }
}

//======================================  Frame comparison main function
int
main( int argc, const char* argv[] )
{

    try
    {
        FrameCPP::Initialize( );

        bool exact = false;
        bool syntax = false;
        bool dumpall = false;

        std::ostringstream d;

        d << "Command Syntax:" << std::endl
          << argv[ 0 ] << " [option [option...]] <file1> <file2>";

        po::options_description desc( d.str( ) );
        /* clang-format off */
        desc.add_options( )
          ( "help,h", "Help screen" )
          ( "exact,e", "Exact match" )
          ( "dump-all,a", "dump all data" )
          ( "verbose,v", "Turn up verbocity" )
          ;
        /* clang-format on */

        po::variables_map vm;
        po::store( po::command_line_parser( argc, argv ).options( desc ).run( ),
                   vm );

        if ( vm.count( "help" ) )
        {
            std::cout << desc;
            return ( 0 );
        }
        if ( vm.count( "exact" ) )
        {
            exact = true;
        }
        if ( vm.count( "dump-all" ) )
        {
            dumpall = true;
        }
        verbose = vm.count( "verbose" );

        if ( syntax || argc < 3 || *argv[ argc - 2 ] == '-' ||
             *argv[ argc - 1 ] == '-' )
        {
            cerr << "Command Syntax: " << endl;
            cerr << "framecmp [-v] [-e] [-a] <file1> <file2>" << endl;
            return 1;
        }

        //---------------------------------------------------------------
        // Setup to read each file sequentially
        //---------------------------------------------------------------
        CmpStream C1( argv[ argc - 2 ] );
        CmpStream C2( argv[ argc - 1 ] );

        while ( C1 && C2 )
        {
            //------------------------------  Check frames have same start time
            GPSTime  time1 = C1.GetGTime( );
            GPSTime  time2 = C2.GetGTime( );
            Interval dT1 = C1.GetDt( );
            Interval dT2 = C2.GetDt( );
            GPSTime  stop1 = time1 + dT1;
            GPSTime  stop2 = time2 + dT2;
            if ( verbose > 1 )
            {
                cout << "Frame times, File 1: " << time1.GetSeconds( ) << ":"
                     << dT1 << " File2: " << time2.GetSeconds( ) << ":" << dT2
                     << endl;
            }

            GPSTime tStart = ( time2 > time1 ) ? time2 : time1;
            GPSTime tStop = ( stop1 < stop2 ) ? stop1 : stop2;

            if ( exact && ( dT1 != dT2 || Almost( time1, time2 ) ) )
            {
                cout << "Exact comparison not possible with different sized "
                        "frames"
                     << endl;
                return 2;
            }
            if ( tStart > tStop || Almost( tStart, tStop ) )
            {
                if ( stop1 < stop2 )
                {
                    cout << "Skipping frame from file 1." << endl << flush;
                    C1.ReadNextFrame( );
                    continue;
                }
                else
                {
                    cout << "Skipping frame from file 2." << endl << flush;
                    C2.ReadNextFrame( );
                    continue;
                }
            }

            if ( verbose )
            {
                cout << "Comparing file: " << C1.Filename( )
                     << " with file: " << C2.Filename( ) << endl
                     << flush;
            }

            //------------------------------  Build a list of channel names
            ScanList             al;
            FrameH::rawData_type raw( C1.GetRawData( ) );
            if ( !raw )
            {
                if ( verbose )
                {
                    cout << "File 1 has no raw data." << endl << flush;
                }
            }
            else
            {
                for ( FrRawData::const_firstAdc_iterator
                          adcIter = raw->RefFirstAdc( ).begin( ),
                          adcLast = raw->RefFirstAdc( ).end( );
                      adcIter != adcLast;
                      adcIter++ )
                {
                    FrRawData::const_firstAdc_iterator::value_type ap(
                        *adcIter );
                    if ( verbose > 3 )
                    {
                        cout << "Prep Adc channel " << ap->GetName( )
                             << " from file 1" << endl
                             << flush;
                    }
                    al[ ap->GetName( ) ] =
                        ListData( ap->RefData( ).front( ), vnull );
                    if ( verbose > 3 )
                    {
                        cout << "Add channel " << ap->GetName( )
                             << " from file 1" << endl
                             << flush;
                    }
                }
            }
            for ( FrameH::const_procData_iterator
                      i = C1.RefProcData( ).begin( ),
                      last = C1.RefProcData( ).end( );
                  i != last;
                  i++ )
            {
                if ( verbose > 3 )
                {
                    cout << "Prep Proc channel " << ( *i )->GetName( )
                         << " from file 1" << endl
                         << flush;
                }
                al[ ( *i )->GetName( ) ] =
                    ListData( ( *i )->RefData( ).front( ), vnull );
                if ( verbose > 3 )
                {
                    cout << "Add channel " << ( *i )->GetName( )
                         << " from file 1" << endl
                         << flush;
                }
            }

            if ( verbose > 3 )
            {
                cout << "Preparing to read from 2" << endl << flush;
            }
            raw = C2.GetRawData( );
            if ( !raw )
            {
                cout << "File 2 has no raw data." << endl << flush;
            }
            else
            {
                for ( FrRawData::const_firstAdc_iterator
                          adcIter = raw->RefFirstAdc( ).begin( ),
                          adcLast = raw->RefFirstAdc( ).end( );
                      adcIter != adcLast;
                      adcIter++ )
                {
                    FrRawData::const_firstAdc_iterator::value_type ap(
                        *adcIter );
                    string   name = ap->GetName( );
                    ScanIter li = al.find( name );
                    if ( li == al.end( ) )
                    {
                        al[ name ] = ListData( vnull, ap->RefData( ).front( ) );
                    }
                    else
                    {
                        li->second.second = ap->RefData( ).front( );
                    }
                    if ( verbose > 3 )
                    {
                        cout << "Add channel " << name << " from file 2" << endl
                             << flush;
                    }
                }
            }
            for ( FrameH::const_procData_iterator
                      i = C2.RefProcData( ).begin( ),
                      last = C2.RefProcData( ).end( );
                  i != last;
                  i++ )
            {
                string   name = ( *i )->GetName( );
                ScanIter li = al.find( name );
                if ( li == al.end( ) )
                {
                    al[ name ] = ListData( vnull, ( *i )->RefData( ).front( ) );
                }
                else
                {
                    li->second.second = ( *i )->RefData( ).front( );
                }
                if ( verbose > 3 )
                {
                    cout << "Add channel " << name << " from file 2" << endl
                         << flush;
                }
            }

            //------------------------------  Scan
            int nMiss1 = 0;
            int nMiss2 = 0;
            int nTotChan = 0;
            int nCmpFail = 0;
            for ( ScanIter li = al.begin( ); li != al.end( ); li++ )
            {
                nTotChan++;
                if ( !li->second.first )
                {
                    if ( verbose > 1 )
                        cout << " Channel: " << li->first << " not in frame 1"
                             << endl
                             << flush;
                    nMiss1++;
                }
                else if ( !li->second.second )
                {
                    if ( verbose > 1 )
                        cout << " Channel: " << li->first << " not in frame 2"
                             << endl
                             << flush;
                    nMiss2++;
                }
                else
                {
                    frvect_pointer vec1( li->second.first );
                    frvect_pointer vec2( li->second.second );
                    if ( !exact ||
                         vec1->GetCompress( ) != vec2->GetCompress( ) )
                    {
                        vec1->Uncompress( );
                        vec2->Uncompress( );
                    }

                    int b1off = 0, b2off = 0;
                    int nbyt1 = vec1->GetNBytes( );
                    int nbyt2 = vec2->GetNBytes( );
                    if ( !exact )
                    {
                        b1off =
                            int( nbyt1 * double( tStart - time1 ) / dT1 + 0.5 );
                        nbyt1 =
                            int( nbyt1 * double( tStop - time1 ) / dT1 + 0.5 ) -
                            b1off;
                        b2off =
                            int( nbyt2 * double( tStart - time2 ) / dT2 + 0.5 );
                        nbyt2 =
                            int( nbyt2 * double( tStop - time2 ) / dT2 + 0.5 ) -
                            b2off;
                    }
                    if ( nbyt1 != nbyt2 )
                    {
                        cout << "Data length differs for Channel: " << li->first
                             << " File 1 length: " << nbyt1
                             << " File 2 length: " << nbyt2 << endl
                             << flush;
                        nCmpFail++;
                        continue;
                    }
                    if ( verbose > 3 )
                    {
                        cout << "Comparing channel " << li->first
                             << " offsets = " << b1off << "," << b2off
                             << " length = " << nbyt1 << endl
                             << flush;
                    }
                    if ( memcmp( &( vec1->GetData( )[ 0 ] ) + b1off,
                                 &( vec2->GetData( )[ 0 ] ) + b2off,
                                 nbyt1 ) )
                    {
                        cout << "Data for Channel: " << li->first << " differs."
                             << endl;
                        nCmpFail++;
                        if ( verbose > 1 )
                        {
                            cout << "Data at " << tStart << " from file "
                                 << C1.Filename( ) << endl
                                 << flush;
                            dump( vec1, b1off, vec2, b2off, nbyt1, !dumpall );
                            cout << "Data at " << tStart << " from file "
                                 << C2.Filename( ) << endl
                                 << flush;
                            dump( vec2, b2off, vec1, b1off, nbyt1, !dumpall );
                        }
                    }
                }
            }

            //------------------------------  Print statistics
            if ( verbose )
            {
                cout << "Frame comparison statistics: " << endl
                     << "Combined channels found:       " << nTotChan << endl
                     << "Channels missing from frame 1: " << nMiss1 << endl
                     << "Channels missing from frame 2: " << nMiss2 << endl
                     << "Comparison failures:           " << nCmpFail << endl
                     << flush;
            }
            else
            {
                cout << "GPS: " << time1 << " chans-combined: " << nTotChan
                     << " miss-1: " << nMiss1 << " miss-2: " << nMiss2
                     << " failed: " << nCmpFail << endl
                     << flush;
            }

            //------------------------------  Go On to the next frames.
            if ( tStop == time1 + dT1 )
            {
                C1.ReadNextFrame( );
            }
            if ( tStop == time2 + dT2 )
            {
                C2.ReadNextFrame( );
            }
        }
    }
    catch ( const std::exception& Except )
    {
        std::cerr << Except.what( );
        return 1;
    }
    return 0;
}
