//
// FrameL <-> FrameCPP Multi-Version I/O Compatibility Test
//
// Tests I/O compatibility across all frame specification versions (3, 4, 6, 7,
// 8, 9) by having FrameL write in each historic version and FrameCPP read them
// back, and vice versa, to ensure complete bidirectional compatibility.
//

#include <FrameL.h> // FrameL C library
#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <cstring>
#include <cmath>
#include <sstream>

#include "framecpp/IFrameStream.hh"
#include "framecpp/OFrameStream.hh"
#include "framecpp/FrameH.hh"
#include "framecpp/FrVect.hh"
#include "framecpp/FrAdcData.hh"
#include "framecpp/FrRawData.hh"
#include "framecpp/Dimension.hh"
#include "ldastoolsal/gpstime.hh"

//=============================================================================
// Test configuration
//=============================================================================
struct TestConfig
{
    // FrameL write → FrameCPP read: Skip v3/v4 due to known incompatibilities
    //
    // KNOWN ISSUES (FrameL v3/v4 → FrameCPP read):
    // - Class ID mapping failure: "Unable to map from stream class id (0)"
    // - Class ID 0 should never appear in valid frame files
    // - Suggests frameCPP reading from wrong file offset when parsing FrameL v3/v4
    // - Error occurs at position 40 (right after frame header)
    //
    // IMPACT:
    // - FrameCPP cannot parse FrameL-written v3/v4 files
    // - FrameCPP OFrameFStream crashes when writing v3/v4 (unsupported)
    // - v9 not supported by FrameL for reading
    //
    // Testing only FrameL-compatible versions (6, 7, 8) in both directions
    std::vector< int > framel_versions = { 6, 7, 8 };
    std::vector< int > framecpp_versions = { 6, 7, 8 };

    // Test data parameters
    static constexpr int          TEST_NDATA = 100;
    static constexpr double       TEST_SAMPLE_RATE = 16384.0;
    static constexpr int          TEST_RUN = 12345;
    static constexpr int          TEST_FRAME_NUM = 42;
    static constexpr unsigned int TEST_GPS_TIME = 1000000000;
    static constexpr unsigned int TEST_GPS_NSEC = 500000000;

    std::string
    GetFrameLFilename( int version ) const
    {
        std::ostringstream oss;
        oss << "test_framel_v" << version << ".gwf";
        return oss.str( );
    }

    std::string
    GetFrameCPPFilename( int version ) const
    {
        std::ostringstream oss;
        oss << "test_framecpp_v" << version << ".gwf";
        return oss.str( );
    }
};

//=============================================================================
// Helper: Create test data vector
//=============================================================================
std::vector< double >
CreateTestData( int nData )
{
    std::vector< double > data( nData );
    for ( int i = 0; i < nData; i++ )
    {
        data[ i ] = std::sin( i * 0.1 ) * 100.0 + i * 0.5;
    }
    return data;
}

//=============================================================================
// Helper: Compare data vectors
//=============================================================================
bool
CompareData( const std::vector< double >& original,
             const double*                read_data,
             int                          nData,
             double                       tolerance = 1e-9 )
{
    if ( nData != (int)original.size( ) )
    {
        std::cerr << "  ✗ Data size mismatch: expected " << original.size( )
                  << ", got " << nData << "\n";
        return false;
    }

    int mismatches = 0;
    for ( int i = 0; i < nData; i++ )
    {
        double diff = std::fabs( original[ i ] - read_data[ i ] );
        if ( diff > tolerance )
        {
            if ( mismatches < 5 ) // Report first 5 mismatches
            {
                std::cerr << "  ✗ Data mismatch at index " << i << ": expected "
                          << original[ i ] << ", got " << read_data[ i ]
                          << ", diff=" << diff << "\n";
            }
            mismatches++;
        }
    }

    if ( mismatches > 0 )
    {
        std::cerr << "  ✗ Total data mismatches: " << mismatches << " / "
                  << nData << "\n";
        return false;
    }

    return true;
}

//=============================================================================
// Test: FrameL writes version V, FrameCPP reads
//=============================================================================
bool
TestFrameLWrite_FrameCPPRead( int version, const TestConfig& config )
{
    std::cout << "\n--- Testing FrameL v" << version
              << " write -> FrameCPP read ---\n";

    std::string           filename = config.GetFrameLFilename( version );
    std::vector< double > test_data = CreateTestData( TestConfig::TEST_NDATA );

    // Set FrameL version
    FrLibSetVersion( version );
    std::cout << "  Set FrameL to write version " << version << "\n";

    // Create frame with FrameL
    FrameH* frame = FrameNew( const_cast< char* >( "TestFrame" ) );
    if ( !frame )
    {
        std::cerr << "  ✗ Failed to create FrameL frame\n";
        return false;
    }

    frame->run = TestConfig::TEST_RUN;
    frame->frame = TestConfig::TEST_FRAME_NUM;
    frame->GTimeS = TestConfig::TEST_GPS_TIME;
    frame->GTimeN = TestConfig::TEST_GPS_NSEC;
    frame->dt = 1.0;

    // Create FrAdcData
    FrAdcData* adc = FrAdcDataNew( frame,
                                   const_cast< char* >( "TestADC" ),
                                   TestConfig::TEST_SAMPLE_RATE,
                                   TestConfig::TEST_NDATA,
                                   -64 );
    if ( !adc || !adc->data || !adc->data->dataD )
    {
        std::cerr << "  ✗ Failed to create FrameL ADC data\n";
        FrameFree( frame );
        return false;
    }

    // Fill test data
    std::memcpy( adc->data->dataD,
                 test_data.data( ),
                 TestConfig::TEST_NDATA * sizeof( double ) );

    // Write frame
    FrFile* oFile = FrFileONew( filename.c_str( ), 0 ); // no compression
    if ( !oFile )
    {
        std::cerr << "  ✗ Failed to create FrameL output file\n";
        FrameFree( frame );
        return false;
    }

    int write_result = FrameWrite( frame, oFile );
    FrFileOEnd( oFile );
    FrameFree( frame );

    if ( write_result != FR_OK )
    {
        std::cerr << "  ✗ FrameL write failed with error: " << write_result
                  << "\n";
        return false;
    }

    std::cout << "  ✓ FrameL wrote v" << version << " frame successfully\n";

    // Read with FrameCPP
    try
    {
        FrameCPP::IFrameFStream istream( filename.c_str( ) );
        auto                    read_frame = istream.ReadNextFrame( );

        if ( !read_frame )
        {
            std::cerr << "  ✗ FrameCPP failed to read frame\n";
            return false;
        }

        std::cout << "  ✓ FrameCPP read frame successfully\n";

        // Verify metadata
        bool metadata_ok = true;
        if ( read_frame->GetRun( ) != TestConfig::TEST_RUN )
        {
            std::cerr << "  ✗ Run mismatch: expected " << TestConfig::TEST_RUN
                      << ", got " << read_frame->GetRun( ) << "\n";
            metadata_ok = false;
        }
        if ( read_frame->GetFrame( ) != TestConfig::TEST_FRAME_NUM )
        {
            std::cerr << "  ✗ Frame number mismatch: expected "
                      << TestConfig::TEST_FRAME_NUM << ", got "
                      << read_frame->GetFrame( ) << "\n";
            metadata_ok = false;
        }
        if ( read_frame->GetGTime( ).GetSeconds( ) !=
             TestConfig::TEST_GPS_TIME )
        {
            std::cerr << "  ✗ GPS time mismatch: expected "
                      << TestConfig::TEST_GPS_TIME << ", got "
                      << read_frame->GetGTime( ).GetSeconds( ) << "\n";
            metadata_ok = false;
        }

        // Access ADC data directly from the frame we just read
        if ( !read_frame->GetRawData( ) ||
             read_frame->GetRawData( )->RefFirstAdc( ).empty( ) )
        {
            std::cerr << "  ✗ No ADC data in frame\n";
            return false;
        }

        auto adc_data = read_frame->GetRawData( )->RefFirstAdc( )[ 0 ];
        if ( !adc_data )
        {
            std::cerr << "  ✗ ADC data pointer is null\n";
            return false;
        }

        if ( adc_data->RefData( ).empty( ) )
        {
            std::cerr << "  ✗ ADC RefData() is empty\n";
            return false;
        }

        auto vect = adc_data->RefData( )[ 0 ];
        if ( vect->GetNData( ) != TestConfig::TEST_NDATA )
        {
            std::cerr << "  ✗ ADC data size mismatch: expected "
                      << TestConfig::TEST_NDATA << ", got " << vect->GetNData( )
                      << "\n";
            return false;
        }

        // Compare data
        const double* read_data =
            reinterpret_cast< const double* >( vect->GetData( ).get( ) );
        if ( !CompareData( test_data, read_data, TestConfig::TEST_NDATA ) )
        {
            return false;
        }

        if ( metadata_ok )
        {
            std::cout << "  ✓ All metadata and data verified successfully\n";
        }

        return metadata_ok;
    }
    catch ( const std::exception& e )
    {
        std::cerr << "  ✗ FrameCPP exception: " << e.what( ) << "\n";
        return false;
    }
}

//=============================================================================
// Test: FrameCPP writes version V, FrameL reads
//=============================================================================
bool
TestFrameCPPWrite_FrameLRead( int version, const TestConfig& config )
{
    std::cout << "\n--- Testing FrameCPP v" << version
              << " write -> FrameL read ---\n";

    std::string           filename = config.GetFrameCPPFilename( version );
    std::vector< double > test_data = CreateTestData( TestConfig::TEST_NDATA );

    // Create frame with FrameCPP
    try
    {
        auto frame = boost::make_shared< FrameCPP::FrameH >( );
        frame->SetName( "TestFrame" );
        frame->SetRun( TestConfig::TEST_RUN );
        frame->SetFrame( TestConfig::TEST_FRAME_NUM );
        frame->SetGTime( LDASTools::AL::GPSTime( TestConfig::TEST_GPS_TIME,
                                                 TestConfig::TEST_GPS_NSEC ) );
        frame->SetDt( 1.0 );

        // Create FrRawData container
        auto rawData = boost::make_shared< FrameCPP::FrRawData >( );
        frame->SetRawData( rawData );

        // Create FrAdcData
        auto adc = boost::make_shared< FrameCPP::FrAdcData >(
            "TestADC",                        // name
            0,                                // channelGroup
            0,                                // channelNumber
            -64,                              // nBits (double precision)
            TestConfig::TEST_SAMPLE_RATE );   // sampleRate

        // Create dimension
        FrameCPP::Dimension dim( TestConfig::TEST_NDATA,
                                 1.0 / TestConfig::TEST_SAMPLE_RATE,
                                 "sample",
                                 0.0 );

        auto vect = boost::make_shared< FrameCPP::FrVect >(
            "TestADC", FrameCPP::FrVect::FR_VECT_8R, 1, &dim, "counts" );

        // Copy data into FrVect
        auto data = vect->GetData( );
        if ( !data )
        {
            std::cerr << "  ✗ FrVect::GetData() returned null pointer for version "
                      << version << "\n";
            return false;
        }
        std::memcpy( data.get( ),
                     test_data.data( ),
                     TestConfig::TEST_NDATA * sizeof( double ) );
        vect->SetUnitY( "counts" );

        adc->RefData( ).append( vect );
        rawData->RefFirstAdc( ).append( adc );

        // Write with FrameCPP
        FrameCPP::OFrameFStream ostream( filename.c_str( ), version );
        ostream.WriteFrame( frame );

        std::cout << "  ✓ FrameCPP wrote v" << version
                  << " frame successfully\n";
    }
    catch ( const std::exception& e )
    {
        std::cerr << "  ✗ FrameCPP write exception: " << e.what( ) << "\n";
        return false;
    }

    // Read with FrameL
    FrFile* iFile = FrFileINew( filename.c_str( ) );
    if ( !iFile )
    {
        std::cerr << "  ✗ Failed to open file with FrameL\n";
        return false;
    }

    FrameH* frame = FrameRead( iFile );
    FrFileIEnd( iFile );

    if ( !frame )
    {
        std::cerr << "  ✗ FrameL failed to read frame\n";
        return false;
    }

    std::cout << "  ✓ FrameL read frame successfully\n";

    // Verify metadata
    bool metadata_ok = true;
    if ( frame->run != TestConfig::TEST_RUN )
    {
        std::cerr << "  ✗ Run mismatch: expected " << TestConfig::TEST_RUN
                  << ", got " << frame->run << "\n";
        metadata_ok = false;
    }
    if ( frame->frame != TestConfig::TEST_FRAME_NUM )
    {
        std::cerr << "  ✗ Frame number mismatch: expected "
                  << TestConfig::TEST_FRAME_NUM << ", got " << frame->frame
                  << "\n";
        metadata_ok = false;
    }
    if ( frame->GTimeS != TestConfig::TEST_GPS_TIME )
    {
        std::cerr << "  ✗ GPS time mismatch: expected "
                  << TestConfig::TEST_GPS_TIME << ", got " << frame->GTimeS
                  << "\n";
        metadata_ok = false;
    }

    // Find and verify ADC data
    FrAdcData* adc = FrAdcDataFind( frame, const_cast< char* >( "TestADC" ) );
    if ( !adc || !adc->data || !adc->data->dataD )
    {
        std::cerr << "  ✗ Failed to find ADC data\n";
        FrameFree( frame );
        return false;
    }

    // Compare data
    if ( !CompareData( test_data, adc->data->dataD, adc->data->nData ) )
    {
        FrameFree( frame );
        return false;
    }

    if ( metadata_ok )
    {
        std::cout << "  ✓ All metadata and data verified successfully\n";
    }

    FrameFree( frame );
    return metadata_ok;
}

//=============================================================================
// Main test runner
//=============================================================================
int
main( int argc, char* argv[] )
{
    std::cout << "========================================\n";
    std::cout << "FrameL <-> FrameCPP Multi-Version I/O Compatibility Test\n";
    std::cout << "========================================\n";

    TestConfig config;
    int        total_tests = 0;
    int        passed_tests = 0;
    int        failed_tests = 0;

    // Test matrix: FrameL writes (versions 3,4,6,7,8) -> FrameCPP reads
    std::cout << "\n========================================\n";
    std::cout << "PART 1: FrameL Write -> FrameCPP Read\n";
    std::cout << "========================================\n";

    for ( int version : config.framel_versions )
    {
        total_tests++;
        if ( TestFrameLWrite_FrameCPPRead( version, config ) )
        {
            passed_tests++;
            std::cout << "✓ PASSED: FrameL v" << version << " -> FrameCPP\n";
        }
        else
        {
            failed_tests++;
            std::cout << "✗ FAILED: FrameL v" << version << " -> FrameCPP\n";
        }
    }

    // Test matrix: FrameCPP writes (versions 3,4,6,7,8,9) -> FrameL reads
    // Note: FrameL may not be able to read version 9
    std::cout << "\n========================================\n";
    std::cout << "PART 2: FrameCPP Write -> FrameL Read\n";
    std::cout << "========================================\n";

    for ( int version : config.framecpp_versions )
    {
        total_tests++;
        if ( TestFrameCPPWrite_FrameLRead( version, config ) )
        {
            passed_tests++;
            std::cout << "✓ PASSED: FrameCPP v" << version << " -> FrameL\n";
        }
        else
        {
            failed_tests++;
            std::cout << "✗ FAILED: FrameCPP v" << version << " -> FrameL\n";
        }
    }

    // Summary
    std::cout << "\n========================================\n";
    std::cout << "TEST SUMMARY\n";
    std::cout << "========================================\n";
    std::cout << "Total tests:  " << total_tests << "\n";
    std::cout << "Passed:       " << passed_tests << " ✓\n";
    std::cout << "Failed:       " << failed_tests << " ✗\n";
    std::cout << "Success rate: " << std::fixed << std::setprecision( 1 )
              << ( 100.0 * passed_tests / total_tests ) << "%\n";
    std::cout << "========================================\n";

    if ( failed_tests == 0 )
    {
        std::cout << "\n✓ ALL COMPATIBILITY TESTS PASSED\n";
        std::cout.flush();
        _exit( 0 );
    }
    else
    {
        std::cout << "\n✗ SOME COMPATIBILITY TESTS FAILED\n";
        std::cout.flush();
        _exit( 1 );
    }
}
