//
// 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
//

#if HAVE_CONFIG_H
#include <framecpp_config.h>
#endif /* HAVE_CONFIG_H */

#include <iomanip>
#include <memory>
#include <sstream>

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

#include <boost/pointer_cast.hpp>
#include <boost/scoped_array.hpp>
#include <boost/shared_ptr.hpp>

#include "ldastoolsal/fstream.hh"

#include "framecpp/Common/FrameH.hh"

#include "framecpp/FrameH.hh"
#include "framecpp/GPSTime.hh"

#include "framecpp/FrameCPP.hh"
#include "framecpp/FrAdcData.hh"
#include "framecpp/FrRawData.hh"
#include "framecpp/FrVect.hh"
#include "framecpp/OFrameStream.hh"

#if 0
using std::filebuf;
using std::ofstream;
#else
using LDASTools::AL::filebuf;
using LDASTools::AL::ofstream;
#endif

using namespace FrameCPP;

typedef Common::FrameBuffer< filebuf >   FrameBuffer;
typedef Common::FrameH                   frameh_type;
typedef boost::shared_ptr< frameh_type > build_frame_type;

static const INT_4U            CHANNEL_COUNT = 64;
static const FrameCPP::GPSTime START_TIME( 913423140, 0 );
static const INT_2U            ULEAPS = START_TIME.GetLeapSeconds( );
static const REAL_8            DT = 60.0;

static build_frame_type build_frame( version_type Version,
                                     INT_4U       ChannelCount );
void                    make_file( INT_4U ChannelCount );

bool
do_init( )
{
    FrameCPP::Initialize( );
    return ( 1 );
}

static const bool HAS_BEEN_INITIALIZED = do_init( );

BOOST_AUTO_TEST_CASE( SixtyK )
{
    try
    {
        int power = 0;

        for ( INT_4U count = 1; count <= CHANNEL_COUNT; count = 1 << ++power )
        {
            make_file( count );
        }
    }
    catch ( const std::exception& E )
    {

        BOOST_TEST_MESSAGE( "Caught exception" << E.what( ) );
        BOOST_CHECK( false );
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( "Caught an unknown exception" );
        BOOST_CHECK( false );
    }
}

void
make_file( INT_4U ChannelCount )
{
    try
    {
        int frame_spec = FRAME_SPEC_CURRENT;

        FrameCPP::GPSTime start;
        FrameCPP::GPSTime finish;

        start.Now( );
        INT_4U OBUFFER_SIZE = ( 10 * 1024 * 1024 );
        boost::scoped_array< FrameBuffer::char_type > obuffer(
            new FrameBuffer::char_type[ OBUFFER_SIZE ] );

        //-------------------------------------------------------------------
        // Construct a frame
        //-------------------------------------------------------------------
        boost::shared_ptr< frameh_type > frameh(
            build_frame( frame_spec, ChannelCount ) );

        finish.Now( );
        BOOST_TEST_MESSAGE( "Time to create frame with "
                            << ChannelCount << "k channels:"
                            << " [dt: " << ( finish - start )
                            << " Percent of Real Time: "
                            << ( ( finish - start ) * 100 / DT ) << "]" );

        //-------------------------------------------------------------------
        // Constuct output frame name
        //-------------------------------------------------------------------
        std::ostringstream filename;
        filename << "Z-SixtyK_" << ChannelCount << "-"
                 << START_TIME.GetSeconds( ) << "-" << int( DT ) << ".gwf";
        //-------------------------------------------------------------------
        // Time writing of the frame
        //-------------------------------------------------------------------
        start.Now( );

        std::unique_ptr< FrameBuffer > obuf( new FrameBuffer( std::ios::out ) );

        obuf->pubsetbuf( obuffer.get( ), OBUFFER_SIZE );
        obuf->open( filename.str( ).c_str( ),
                    std::ios::out | std::ios::binary );

        OFrameStream ofs( obuf.release( ) );

        ofs.DisableTOC( );

        ofs.WriteFrame( frameh,
                        FrameCPP::FrVect::ZERO_SUPPRESS_OTHERWISE_GZIP,
                        FrameCPP::FrVect::DEFAULT_GZIP_LEVEL );
        ofs.Close( );

        finish.Now( );
        BOOST_TEST_MESSAGE(
            "Writing of frame with "
            << ChannelCount << "k channels completed in less than real time"
            << "( dt: " << DT << " vs write time: " << ( finish - start )
            << " [ " << ( ( finish - start ) * 100 / DT ) << "% ])" );
        BOOST_CHECK( ( finish - start ) < DT );
        BOOST_TEST_MESSAGE( "Time to write file: "
                            << ( finish - start ) << " Percent of Real Time: "
                            << ( ( finish - start ) * 100 / DT ) );
    }
    catch ( const std::exception& E )
    {
        BOOST_TEST_MESSAGE( "Caught an exception: " << E.what( ) );
        BOOST_CHECK( false );
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( "Caught an unknown exception" );
        BOOST_CHECK( false );
    }
}

//=======================================================================
// Local functions
//=======================================================================

static void add_channels( version_type     Version,
                          build_frame_type Frame,
                          INT_2U           padding,
                          INT_4U           ChannelCount );

static build_frame_type create_frameh( version_type Version );

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
template < typename RawData_type >
void add_channel( RawData_type RawData, INT_2U Padding, INT_4U ChannelOffset );

template <>
void
add_channel( FrameCPP::FrameH::rawData_type RawData,
             INT_2U                         Padding,
             INT_4U                         ChannelOffset )
{
    using namespace FrameCPP::Version_9;
    using FrameCPP::Universal::FrVect;

    std::ostringstream name;
    name << "Z1:Channel" << std::setw( Padding ) << std::setfill( '0' )
         << ChannelOffset;
    std::vector< REAL_4 > d( size_t( DT ), 0.0 );

    FrVect::dimension_type dim( d.size( ) );
    FrVect                 v( name.str( ), 1, &dim, &d[ 0 ] );
    FrAdcData              a( name.str( ), /* name */
                 0,
                 ChannelOffset, /* group, channel */
                 32, /* nBits */
                 1, /* sampleRate */
                 0, /* bias */
                 1, /* slope */
                 "NONE", /* units */
                 0, /* fShift */
                 0, /* timeOffset */
                 0, /* dataValid */
                 0 /* phase */
    );
    // a.RefData( ).append( &v, false /* allocate */, false /* own */ );
    a.RefData( ).append( v );
    RawData->RefFirstAdc( ).append( a );
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
template < typename RawData_type >
void
add_adc_channels( RawData_type& RawData, INT_2U Padding, INT_4U ChannelCount )
{
    for ( INT_4U cc = 0; cc < ChannelCount; ++cc )
    {
        add_channel( RawData, Padding, cc );
    }
}
//-----------------------------------------------------------------------
// Creation of the header
//-----------------------------------------------------------------------
template < int >
build_frame_type frameh( );

template <>
build_frame_type
frameh< 8 >( )
{
    using namespace FrameCPP::Universal;
    using FrameCPP::FrameH;
    using FrameCPP::FrRawData;

    boost::shared_ptr< FrameH > frameh(
        new FrameH( "SixtyK", 0, 0, START_TIME, DT ) );
    boost::shared_ptr< FrRawData > r( new FrRawData( "SixtyK" ) );
    frameh->SetRawData( r );
    return boost::dynamic_pointer_cast< frameh_type >( frameh );
}

template <>
build_frame_type
frameh< 9 >( )
{
    using namespace FrameCPP::Version_9;
    using FrameCPP::FrameH;
    using FrameCPP::FrRawData;

    boost::shared_ptr< FrameH > frameh(
        new FrameH( "SixtyK", 0, 0, START_TIME, DT ) );
    boost::shared_ptr< FrRawData > r( new FrRawData( "SixtyK" ) );
    frameh->SetRawData( r );
    return boost::dynamic_pointer_cast< frameh_type >( frameh );
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------

static void
add_channels( version_type     Version,
              build_frame_type Frame,
              INT_2U           Padding,
              INT_4U           ChannelCount )
{
    switch ( Version )
    {
    case 8:
    {
        using namespace FrameCPP::Version_9;
        using FrameCPP::FrameH;

        FrameH* f = dynamic_cast< FrameH* >( Frame.get( ) );
        if ( f )
        {
            FrameH::rawData_type r = f->GetRawData( );
            if ( r )
            {
                add_adc_channels( r, Padding, ChannelCount );
            }
        }
    }
    break;
    default:
        break;
    }
}

static build_frame_type
build_frame( version_type Version, INT_4U ChannelCount )
{
    build_frame_type retval;

    retval = create_frameh( Version );

    const INT_4U cc_last = ChannelCount * 1024;
    int          padding = 0;

    for ( INT_4U cc_cur = cc_last; cc_cur > 10; cc_cur /= 10 )
    {
        ++padding;
    }

    add_channels( Version, retval, padding, cc_last );
    return retval;
}

static build_frame_type
create_frameh( version_type Version )
{
    build_frame_type retval;
    switch ( Version )
    {
    case 8:
        retval = frameh< 8 >( );
        break;
    case 9:
        retval = frameh< 9 >( );
        break;
    default:
    {
        std::ostringstream msg;

        msg << "create_frameh does not support version " << (int)Version
            << " of the frame specification";
        throw std::range_error( msg.str( ) );
    }
    break;
    }
    return retval;
}
