//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2022 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
//

#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>

#include <cstdint>
#include <memory>

#include <boost/chrono.hpp>
#include <boost/range/irange.hpp>

#include "framecpp/Common/CheckSum.hh"

bool init_ramped_buffer( );

using FrameCPP::Common::CheckSum;
using FrameCPP::Common::CheckSumCRC;
typedef FrameCPP::Common::CheckSumCRC::crc_type cksum;

static const cksum::value_type expected_value_zero_0( 4294967295 );
static const cksum::value_type expected_value_zero_4096( 3018728591 );
static const cksum::value_type expected_value_zero_8589934592( 4135437457 );
static const cksum::value_type expected_value_ramped_4096( 300014538 );

uint8_t ramped_buffer[ 256 ];
bool    ramped_buffer_initted = init_ramped_buffer( );

template < typename CRC >
void
process_buffer( CRC& Accumulator, const uint8_t* Buffer, size_t BufferSize );

template <>
inline void
process_buffer< CheckSumCRC >( CheckSumCRC&   Accumulator,
                               const uint8_t* Buffer,
                               size_t         BufferSize )
{
    Accumulator.calc( Buffer, BufferSize );
}

template < typename CRC >
inline void
process_buffer( CRC& Accumulator, const uint8_t* Buffer, size_t BufferSize )
{
    Accumulator.process_bytes( Buffer, BufferSize );
}

template < typename CRC >
inline auto
process_n_buffers( CRC&           Accumulator,
                   const uint8_t* Buffer,
                   size_t         BufferSize,
                   size_t         N )
    -> decltype( boost::chrono::process_real_cpu_clock::now( ) -
                 boost::chrono::process_real_cpu_clock::now( ) )
{
    auto start = boost::chrono::process_real_cpu_clock::now( );

    for ( auto i = size_t( 0 ); i < N; i++ )
    {
        process_buffer( Accumulator, Buffer, BufferSize );
    }
    return ( boost::chrono::process_real_cpu_clock::now( ) - start );
}

bool
init_ramped_buffer( )
{
    for ( auto pos : boost::irange( size_t( 0 ), sizeof( ramped_buffer ) ) )
    {
        ramped_buffer[ pos ] = pos;
    }
    return ( true );
}

BOOST_AUTO_TEST_CASE( test_checksum_constructor )
{
    {
        static const auto expected_value( expected_value_ramped_4096 );
        CheckSumCRC       test_checksum( expected_value );

        BOOST_CHECK( test_checksum.value( ) == expected_value );
    }
}

BOOST_AUTO_TEST_CASE( test_checksum_zero_bytes )
{
    static const auto expected_value( expected_value_zero_0 );

    cksum       test_cksum;
    CheckSumCRC test_checksum;

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );
}

BOOST_AUTO_TEST_CASE( test_checksum_4096_bytes )
{
    static const auto expected_value( expected_value_zero_4096 );

    cksum       test_cksum;
    CheckSumCRC test_checksum;

    uint8_t zero_buffer[ 1024 ];
    std::fill( zero_buffer, &( zero_buffer[ sizeof( zero_buffer ) ] ), 0 );

    for ( auto i = 0; i < 4; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );
}

BOOST_AUTO_TEST_CASE( test_checksum_4096_bytes_2_stages )
{
    static const auto expected_value( expected_value_zero_4096 );

    uint8_t zero_buffer[ 1024 ];
    std::fill( zero_buffer, &( zero_buffer[ sizeof( zero_buffer ) ] ), 0 );

    cksum       test_cksum_first;
    CheckSumCRC test_checksum_first;

    for ( auto i = 0; i < 2; i++ )
    {
        test_cksum_first.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum_first.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    cksum                       test_cksum{ test_cksum_first };
    CheckSum::self_ptr_type test_checksum(
        test_checksum_first.Clone( ) );

    for ( auto i = 2; i < 4; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum->calc( zero_buffer, sizeof( zero_buffer ) );
    }

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum->value( ) == expected_value );
}

BOOST_AUTO_TEST_CASE( test_checksum_4096_bytes_reset )
{
    static const auto expected_value( expected_value_zero_4096 );

    cksum       test_cksum;
    CheckSumCRC test_checksum;

    uint8_t zero_buffer[ 1024 ];
    std::fill( zero_buffer, &( zero_buffer[ sizeof( zero_buffer ) ] ), 0 );

    for ( auto i = 0; i < 8; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    test_cksum.reset( );
    test_checksum.Reset( );

    for ( auto i = 0; i < 4; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );

    test_cksum.reset( );
    test_checksum.Reset( );

    std::ofstream ramped_4096( "ramped_4096.dat", std::ofstream::out );
    for ( auto buffer_count = size_t( 0 );
          buffer_count < size_t( 4096 / sizeof( ramped_buffer ) );
          buffer_count++ )
    {
        ramped_4096.write( reinterpret_cast< const char* >( ramped_buffer ),
                           sizeof( ramped_buffer ) );
        test_cksum.process_bytes( ramped_buffer, sizeof( ramped_buffer ) );
        test_checksum.calc( ramped_buffer, sizeof( ramped_buffer ) );
    }
    ramped_4096.close( );

    BOOST_CHECK( test_cksum.checksum( ) == expected_value_ramped_4096 );
    BOOST_CHECK( test_checksum.value( ) == expected_value_ramped_4096 );
}

BOOST_AUTO_TEST_CASE( test_checksum_4096_bytes_no_extend )
{
    static const auto expected_value( expected_value_zero_4096 );

    cksum       test_cksum;
    CheckSumCRC test_checksum;

    uint8_t zero_buffer[ 1024 ];
    std::fill( zero_buffer, &( zero_buffer[ sizeof( zero_buffer ) ] ), 0 );

    for ( auto i = 0; i < 4; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );

    for ( auto i = 0; i < 4; i++ )
    {
        test_cksum.process_bytes( zero_buffer, sizeof( zero_buffer ) );
        test_checksum.calc( zero_buffer, sizeof( zero_buffer ) );
    }

    BOOST_CHECK( test_cksum.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );
}

BOOST_AUTO_TEST_CASE( test_checksum_8589934592_bytes )
{
    using namespace boost::chrono;
    using namespace FrameCPP::Common;
    // Test that can calculate checksums for streams greater
    // thsn 2^32 bytes in length.

    static const auto expected_value( expected_value_zero_8589934592 );
    static seconds    expected_maximum_time( 25 );

    cksum_boost test_cksum_boost;
    cksum_ldas  test_cksum_ldas;
    ;
    CheckSumCRC test_checksum;

    uint8_t zero_buffer[ 65536 ];
    std::fill( zero_buffer, &( zero_buffer[ sizeof( zero_buffer ) ] ), 0 );
    auto range_end = size_t( 8589934592L / sizeof( zero_buffer ) );

    auto cksum_boost_time = duration_cast< seconds >( process_n_buffers(
        test_cksum_boost, zero_buffer, sizeof( zero_buffer ), range_end ) );
    auto cksum_ldas_time = duration_cast< seconds >( process_n_buffers(
        test_cksum_ldas, zero_buffer, sizeof( zero_buffer ), range_end ) );
    auto CheckSumCRC_time = duration_cast< seconds >( process_n_buffers(
        test_checksum, zero_buffer, sizeof( zero_buffer ), range_end ) );

    BOOST_CHECK( test_cksum_boost.checksum( ) == expected_value );
    BOOST_CHECK( test_cksum_ldas.checksum( ) == expected_value );
    BOOST_CHECK( test_checksum.value( ) == expected_value );

    BOOST_WARN_MESSAGE( cksum_boost_time < expected_maximum_time,
                        "Time to execute cksum_boost calculation: "
                            << cksum_boost_time << " [Upper limit: "
                            << expected_maximum_time << " ]" );
    BOOST_CHECK_MESSAGE( cksum_ldas_time < expected_maximum_time,
                         "Time to execute cksum_ldas calculation: "
                             << cksum_ldas_time << " [Upper limit: "
                             << expected_maximum_time << " ]" );
    BOOST_CHECK_MESSAGE( CheckSumCRC_time < expected_maximum_time,
                         "Time to execute CheckSumCRC calculation: "
                             << CheckSumCRC_time << " [Upper limit: "
                             << expected_maximum_time << " ]" );
}
