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

#include <algorithm>

#include "framecpp/Common/FrameCPPServices.hh"

#include "framecpp/Common/Description.hh"
#include "framecpp/Common/TOCInfo.hh"

#include "framecpp/Universal/STRING.hh"

#include "framecpp/Universal/impl/FrameSpec.hh"

#include "framecpp/Universal/FrSE.hh"
#include "framecpp/Universal/FrSH.hh"
#include "framecpp/Universal/FrTOCEvent.hh"

#include "../Common/FrTOCPrivate.hh"

using FrameCPP::Common::Description;
using FrameCPP::Common::FrameSpec;
using FrameCPP::Common::TOCInfo;

using FrameCPP::Universal::STRING;

namespace
{
    template < typename T >
    class IncrementOnExit
    {
    public:
        explicit IncrementOnExit( T& value ) : ref( value )
        {
        }
        ~IncrementOnExit( )
        {
            ++ref;
        }

        // Disable copy/move to avoid accidental behavior
        IncrementOnExit( const IncrementOnExit& ) = delete;
        IncrementOnExit& operator=( const IncrementOnExit& ) = delete;

    private:
        T& ref;
    };
} // namespace
namespace FrameCPP
{
    namespace Universal
    {
        namespace FrTOCImpl
        {
            FrTOCEventData::positionEvent_type
            FrTOCEventData::positionEvent( INT_4U             Frame,
                                           const std::string& Event ) const
            {
                // std::cerr << "DEBUG: positionEvent(" << Frame << ", " <<
                // Event
                //          << ")" << std::endl;
                const nameEvent_container_type::const_iterator event(
                    events.find( Event ) );
                //-----------------------------------------------------------------
                // Locate the event by name
                //-----------------------------------------------------------------
                if ( event == events.end( ) )
                {
                    //---------------------------------------------------------------
                    // Event name does not exist.
                    //---------------------------------------------------------------
                    std::ostringstream msg;

                    msg << "No FrEvent structures with the name '" << Event
                        << "'";
                    // std::cerr << "DEBUG: " << msg.str( ) << std::endl;
                    throw std::out_of_range( msg.str( ) );
                }
                //-----------------------------------------------------------------
                // Verify that the index exists
                //-----------------------------------------------------------------
                if ( Frame >= event->second.size( ) )
                {
                    std::ostringstream msg;

                    msg << "Request for frame " << Frame
                        << " exceeds the range of 0 through "
                        << ( event->second.size( ) - 1 );
                    throw std::out_of_range( msg.str( ) );
                }
                //-----------------------------------------------------------------
                // Return position information
                //-----------------------------------------------------------------
#if DEBUG
                std::cerr << "DEBUG: FrEvent file position: "
                          << event->second[ Frame ].positionEvent << std::endl;
#endif /* DEBUG */
                return event->second[ Frame ].positionEvent;
            }

            FrTOCEventData::positionEvent_type
            FrTOCEventData::positionEvent( const std::string& Event,
                                           INT_4U             Index ) const
            {
#if DEBUG
                std::cerr << "DEBUG: positionEvent(" << Event << ", " << Index
                          << ")" << std::endl;
#endif /* DEBUG */

                const nameEvent_container_type::const_iterator event(
                    events.find( Event ) );
                //-----------------------------------------------------------------
                // Locate the event by name
                //-----------------------------------------------------------------
                if ( event == events.end( ) )
                {
                    //---------------------------------------------------------------
                    // Event name does not exist.
                    //---------------------------------------------------------------
                    std::ostringstream msg;

                    msg << "No FrEvent structures with the name '" << Event
                        << "'";
#if DEBUG
                    std::cerr << "DEBUG: " << msg.str( ) << std::endl;
#endif /* DEBUG */
                    throw std::out_of_range( msg.str( ) );
                }
                //-----------------------------------------------------------------
                // Verify that the index exists
                //-----------------------------------------------------------------
                if ( Index >= event->second.size( ) )
                {
                    std::ostringstream msg;

                    msg << "Request for index " << Index
                        << " exceeds the range of 0 through "
                        << ( event->second.size( ) - 1 );
                    throw std::out_of_range( msg.str( ) );
                }
                //-----------------------------------------------------------------
                // Return position information
                //-----------------------------------------------------------------
#if DEBUG
                std::cerr << "DEBUG: FrEvent file position: "
                          << event->second[ Index ].positionEvent << std::endl;
#endif /* DEBUG */
                return event->second[ Index ].positionEvent;
            }

        } // namespace FrTOCImpl
    } // namespace Universal
} // namespace FrameCPP

//=======================================================================
//               C L A S S I C   I O
//=======================================================================
namespace FrameCPP
{
    namespace Universal
    {
        namespace FrTOCImpl
        {
            FrTOCEventClassicIO&
            FrTOCEventClassicIO::operator=( FrTOCEventClassicIO&& TOC )
            {
                static_cast< FrTOCEventData& >( *this ) =
                    static_cast< FrTOCEventData&& >( TOC );
                return *this;
            }

            Common::FrameSpec::size_type
            FrTOCEventClassicIO::Bytes( const Common::StreamBase& Stream ) const
            {
                auto frame_stream_version = StreamVersion( Stream );
                logger_log_debug(logger(), 10, "FrTOCEvent::Bytes: Starting calculation for stream version ", frame_stream_version);
                
                Common::FrameSpec::size_type retval{ 0 }; //

                if ( frame_stream_version >= 4 )
                {
                    if ( frame_stream_version >= 5 )
                    {
                        retval += //
                            sizeof( v5::nEventType_type ) //
                            ;
                        if ( frame_stream_version >= 8 )
                        {
                            retval += //
                                sizeof( v8::nTotalEvent_type ) //
                                ;
                        }
                        logger_log_debug(logger(), 20, "FrTOCEvent::Bytes: Added header bytes for v5+ format");
                    }
                    else
                    {
                        retval += //
                            sizeof( v4::nTrig_type ) //
                            ;
                        logger_log_debug(logger(), 20, "FrTOCEvent::Bytes: Added header bytes for v4 format");
                    }
                    //-----------------------------------------------------------------
                    // Iterate over each event
                    //-----------------------------------------------------------------
                    logger_log_debug(logger(), 30, "FrTOCEvent::Bytes: Processing ", events.size(), " event types");
                    for ( nameEvent_container_type::const_iterator
                              cur = events.begin( ),
                              last = events.end( );
                          cur != last;
                          ++cur )
                    {
                        retval += //
                            STRING::Bytes( cur->first ) //
                            + ( ( frame_stream_version >= 5 ) //
                                    ? sizeof( nEvent_type ) //
                                    : 0 //
                                ) //
                            + ( cur->second.size( ) * //
                                ( sizeof( v4::GTimeSTrig_type ) + //
                                  sizeof( v4::GTimeNTrig_type ) + //
                                  ( ( frame_stream_version >= 5 ) //
                                        ? sizeof( v5::amplitudeEvent_type ) //
                                        : 0 //
                                    ) + //
                                  sizeof( v4::positionTrig_type ) //
                                  ) //
                                ) //
                            ;
                    }
                }
                else
                {
                    logger_log_debug(logger(), 20, "FrTOCEvent::Bytes: No event data for stream version < 4");
                }
                
                logger_log_debug(logger(), 15, "FrTOCEvent::Bytes: Total ", retval, " bytes");
                return retval;
            }

            void
            FrTOCEventClassicIO::GetDescription(
                unsigned int         frame_stream_version,
                Common::Description& Desc ) const
            {
                using Common::Description;

                if ( frame_stream_version < 4 )
                {
                    return;
                }
                Desc( FrSE( ( (frame_stream_version >= 5 )
                              ? "nEventType"
                              : "nTrig"),
                            ( ( frame_stream_version >= 5 )
                              ? Description::Ascii< v5::nEventType_type >( ) //
                              : Description::Ascii< v4::nTrig_type >( ) //
                              ),
                            ( ( frame_stream_version >= 5 )
                              ? "Number of FrEvent in the file" //
                              : "Number of FrTrigData in the file" //
                              ) //
                            ) //
                      );
                Desc( FrSE( ( ( frame_stream_version >= 5 ) //
                              ? "nameEvent"
                              : "nameTrig" ),
                            ( ( frame_stream_version >= 8 ) //
                              ? Description::AsciiArray< v8::nameEvent_type >( "nEventType" )
                              : Description::Ascii< v5::nameEvent_type *>( ) ),
                            ( ( frame_stream_version >= 5 ) //
                              ? "Array of FrEvent names" //
                              : "Array of FrTrig names" //
                              ) //
                            ) //
                      );
                if ( frame_stream_version >= 5 )
                {
                    Desc( FrSE( "nEvent",
                                ((frame_stream_version >= 8)
                                 ? Description::AsciiArray< v8::nEvent_type >( "nEventType" ) //
                                 : Description::Ascii< v5::nEvent_type* >( ) //
                                 ),
                                "Number of FrEvent for each type of FrEvent"
                                " (size of nEventType)" ) );
                }
                if ( frame_stream_version >= 8 )
                {
                    Desc( FrSE( "nTotalEvent",
                                Description::Ascii< v8::nTotalEvent_type >( ),
                                "Total number of FrEvent" ) );
                }
                Desc( FrSE( ( (frame_stream_version >= 5 ) //
                              ? "GTimeSEvent" //
                              : "GTimeSTrig" //
                              ),
                            ( (frame_stream_version >= 8 )
                              ? Description::AsciiArray< v8::GTimeSEvent_type >("nTotalEvent" ) //
                              : ( (frame_stream_version >= 5 ) //
                                  ? Description::Ascii< v5::GTimeSEvent_type *>( ) //
                                  : Description::Ascii< v4::GTimeSTrig_type *>( ) //
                                  ) //
                              ),
                            "GPS time in integer seconds" ) );
                Desc( FrSE( ( ( frame_stream_version >= 5 ) //
                              ? "GTimeNEvent" //
                              : "GTimeNTrig" //
                              ),
                            ( ( frame_stream_version >= 8 )
                              ? Description::AsciiArray< v8::GTimeNEvent_type >( "nTotalEvent" ) //
                              : ( ( frame_stream_version >= 5 )
                                  ? Description::Ascii< v5::GTimeNEvent_type *>( ) //
                                  : Description::Ascii< v4::GTimeNTrig_type *>( ) //
                                  ) //
                              ), //
                            "Residual GPS time in integer nanoseconds" ) );
                if ( frame_stream_version >= 5 )
                {
                  Desc( FrSE(
                             "amplitudeEvent",
                             ( (frame_stream_version >= 8 ) //
                               ? Description::AsciiArray< v8::amplitudeEvent_type >( "nTotalEvent" ) //
                               : Description::Ascii< v8::amplitudeEvent_type *>(  ) //
                               ) //
                             ,
                        "Event amplitude" ) );
                }
                Desc(
                     FrSE( (( frame_stream_version >= 5 ) //
                            ? "positionEvent" //
                            : "positionTrig"//
                            ),
                           ( ( frame_stream_version >= 8 ) //
                             ? Description::AsciiArray< v8::positionEvent_type >( "nTotalEvent" ) //
                             : ( (frame_stream_version >= 5 )
                                 ? Description::Ascii< v5::positionEvent_type *>( ) //
                                 : Description::Ascii< v4::positionTrig_type *>( ) //
                                 )//
                             ),
                              "Array of FrEvent positions, in bytes,"
                              " from beginning of file" ) );
            }

            void
            FrTOCEventClassicIO::Load( istream_type& Stream, INT_4U& nframe )
            {
                auto            frame_stream_version = StreamVersion( Stream );
                logger_log_debug(logger(), 10, "FrTOCEvent::Load: Starting load for stream version ", frame_stream_version, ", nframe=", nframe);

                nEventType_type number_of_events;

                if ( frame_stream_version < 4 )
                {
                    logger_log_debug(logger(), 15, "FrTOCEvent::Load: No event data for stream version < 4");
                    return;
                }
                Stream >> number_of_events;
                logger_log_debug(logger(), 15, "FrTOCEvent::Load: Read ", number_of_events, " event types");

                if ( number_of_events )
                {
                    //---------------------------------------------------------------
                    // Read in the information
                    //---------------------------------------------------------------
                    std::vector< nameEvent_type > names( number_of_events );
                    std::vector< nEvent_type >    events( number_of_events );

                    logger_log_debug(logger(), 20, "FrTOCEvent::Load: Reading ", number_of_events, " event names");
                    for ( auto& name : names )
                    {
                        Common::FrTOC::string_stream_type::Read( Stream, name );
                    }
                    if ( frame_stream_version >= 5 )
                    {
                        logger_log_debug(logger(), 20, "FrTOCEvent::Load: Reading event counts for v5+ format");
                        Stream >> events;
                    }
                    else if ( frame_stream_version >= 4 )
                    {
                        logger_log_debug(logger(), 20, "FrTOCEvent::Load: Filling default event counts for v4 format");
                        std::fill( events.begin( ), events.end( ), 1 );
                    }

                    nEvent_type offset = 0;

                    auto adder = []( nTotalEvent_type sum,
                                     nEvent_type      increment ) {
                        return ( ( increment == NO_DATA_AVAILABLE )
                                     ? sum
                                     : sum + increment );
                    };
                    nTotalEvent_type nevent_sum = std::accumulate(
                        events.begin( ), events.end( ), 0, adder );

                    nTotalEvent_type nTotalEvent;

                    if ( frame_stream_version >= 8 )
                    {
                        Stream >> nTotalEvent;
                        logger_log_debug(logger(), 20, "FrTOCEvent::Load: Read nTotalEvent=", nTotalEvent, " from stream");
                    }
                    else if ( frame_stream_version >= 4 )
                    {
                        nTotalEvent = nevent_sum;
                        logger_log_debug(logger(), 20, "FrTOCEvent::Load: Calculated nTotalEvent=", nTotalEvent);
                    }

                    if ( nTotalEvent != nTotalEvent_type( nevent_sum ) )
                    {
                        logger_log_debug(logger(), 10, "FrTOCEvent::Load: ERROR - nTotalEvent mismatch: ", nTotalEvent, " vs ", nevent_sum);
                        throw std::runtime_error(
                            "nTotalEvent is not the sumation of nEvent" );
                    }

                    std::vector< GTimeSEvent_type >    gtimes( nTotalEvent );
                    std::vector< GTimeNEvent_type >    gtimen( nTotalEvent );
                    std::vector< amplitudeEvent_type > amplitude( nTotalEvent );
                    std::vector< positionEvent_type >  position( nTotalEvent );

                    logger_log_debug(logger(), 30, "FrTOCEvent::Load: Reading ", nTotalEvent, " event records");
                    Stream //
                        >> gtimes //
                        >> gtimen //
                        ;
                    if ( frame_stream_version >= 5 )
                    {
                        Stream //
                            >> amplitude //
                            ;
                    }
                    else
                    {
                        std::fill( amplitude.begin( ), amplitude.end( ), 0 );
                    }
                    Stream //
                        >> position //
                        ;
                    std::vector< nEvent_type >::const_iterator
                        cur_event_counter = events.begin( );

                    logger_log_debug(logger(), 30, "FrTOCEvent::Load: Organizing events into containers");
                    for ( const auto& name : names )
                    {
                        IncrementOnExit guard( cur_event_counter );

                        auto& cur_event = this->events[ name ];

                        if ( ( *cur_event_counter == 0 ) ||
                             ( *cur_event_counter == NO_DATA_AVAILABLE ) )
                        {
                            continue;
                        }
                        cur_event.resize( *cur_event_counter );

                        const auto x_last = *cur_event_counter;

                        for ( nEvent_type x{ 0 }; x != x_last; ++x )
                        {
                            cur_event[ x ].GTime =
                                GPSTime( gtimes[ offset ], gtimen[ offset ] );
                            cur_event[ x ].amplitudeEvent = amplitude[ offset ];
                            cur_event[ x ].positionEvent = position[ offset ];
                            ++offset;
                        }
                    }
                }
                else
                {
                    logger_log_debug(logger(), 15, "FrTOCEvent::Load: No events for this frame");
                    // For v8+, need to read nTotalEvent even when number_of_events=0
                    if ( frame_stream_version >= 8 )
                    {
                        nTotalEvent_type nTotalEvent;
                        Stream >> nTotalEvent;
                        logger_log_debug(logger(), 20, "FrTOCEvent::Load: Read nTotalEvent=", nTotalEvent, " (should be 0)");
                    }
                    // For v4-v7, write() only wrote number_of_events=0, nothing more to read
                }
                
                logger_log_debug(logger(), 10, "FrTOCEvent::Load: Load complete");
            }

            void
            FrTOCEventClassicIO::QueryEvent( const Common::TOCInfo& Info,
                                             INT_4U                 FrameOffset,
                                             INT_8U                 Position )
            {
                using FrameCPP::Common::TOCInfo;

                STRING name;
                INT_4U sec;
                INT_4U nsec;
                REAL_4 ampl;

                Info.TOCQuery( TOCInfo::IC_NAME,
                               TOCInfo::DataType( name ),
                               &name,
                               TOCInfo::IC_GTIME_S,
                               TOCInfo::DataType( sec ),
                               &sec,
                               TOCInfo::IC_GTIME_N,
                               TOCInfo::DataType( nsec ),
                               &nsec,
                               TOCInfo::IC_AMPLITUDE,
                               TOCInfo::DataType( ampl ),
                               &ampl,
                               TOCInfo::IC_EOQ );

                events_container_type& i( events[ name ] );

                event_type e;
                e.GTime = GPSTime( sec, nsec );
                e.amplitudeEvent = ampl;
                e.positionEvent = Position;

                i.push_back( e );
            }

            void
            FrTOCEventClassicIO::write( Common::OStream& Stream ) const
            {
                logger_log_debug(logger(), 10, "FrTOCEvent::write: Starting write operation");
                auto starting_location{ Stream.tellp( ) };
                auto frame_stream_version = StreamVersion( Stream );

                logger_log_debug(logger(), 15, "FrTOCEvent::write: Writing ", events.size(), " event types for stream version ", frame_stream_version);

                //-----------------------------------------------------------------
                // Flatten data so it is streamable
                //-----------------------------------------------------------------
                if ( events.size( ) > 0 )
                {
                    std::vector< nameEvent_type >      names( events.size( ) );
                    std::vector< nEvent_type >         nevent( events.size( ) );
                    nEvent_type                        offset( 0 );
                    nEvent_type                        eoffset( 0 );
                    std::vector< GTimeSEvent_type >    gtimes;
                    std::vector< GTimeNEvent_type >    gtimen;
                    std::vector< amplitudeEvent_type > amplitude;
                    std::vector< positionEvent_type >  position;

                    logger_log_debug(logger(), 20, "FrTOCEvent::write: Flattening data for streaming");
                    for ( nameEvent_container_type::const_iterator
                              cur = events.begin( ),
                              last = events.end( );
                          cur != last;
                          ++cur, ++offset )
                    {
                        names[ offset ] = cur->first;
                        const nEvent_type c = ( cur->second.size( ) == 0 )
                            ? ( NO_DATA_AVAILABLE )
                            : ( cur->second.size( ) );
                        nevent[ offset ] = c;
                        if ( c != NO_DATA_AVAILABLE )
                        {
                            const int ns( c + eoffset );

                            gtimes.resize( ns );
                            gtimen.resize( ns );
                            amplitude.resize( ns );
                            position.resize( ns );

                            for ( nEvent_type x = 0; x != c; ++x, ++eoffset )
                            {
                                gtimes[ eoffset ] =
                                    cur->second[ x ].GTime.GetSeconds( );
                                gtimen[ eoffset ] =
                                    cur->second[ x ].GTime.GetNanoseconds( );
                                amplitude[ eoffset ] =
                                    cur->second[ x ].amplitudeEvent;
                                position[ eoffset ] =
                                    cur->second[ x ].positionEvent;
                            }
                        }
                    }

                    const nTotalEvent_type nTotalEvent( gtimes.size( ) );
                    logger_log_debug(logger(), 20, "FrTOCEvent::write: Total events to write: ", nTotalEvent);

                    logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing event type count and names");
                    Stream << nEvent_type( events.size( ) );
                    for ( auto& name : names )
                    {
                        STRING::Write( Stream, name );
                    }
                    if ( frame_stream_version >= 5 )
                    {
                        logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing event counts for v5+");
                        Stream //
                            << nevent //
                            ;
                    }
                    if ( frame_stream_version >= 8 )
                    {
                        logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing nTotalEvent for v8+");
                        Stream //
                            << nTotalEvent //
                            ;
                    }
                    if ( frame_stream_version >= 4 )
                    {
                        logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing GPS times for v4+");
                        Stream //
                            << gtimes //
                            << gtimen //
                            ;
                    }
                    if ( frame_stream_version >= 5 )
                    {
                        logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing amplitudes for v5+");
                        Stream //
                            << amplitude //
                            ;
                    }
                    logger_log_debug(logger(), 30, "FrTOCEvent::write: Writing positions");
                    Stream //
                        << position //
                        ;
                }
                else
                {
                    logger_log_debug(logger(), 20, "FrTOCEvent::write: No events, writing empty markers");
                    Stream << nEvent_type( 0 ); // nEventType
                    if ( frame_stream_version >= 8 )
                    {
                        Stream << nTotalEvent_type( 0 ); // nTotalEvent
                    }
                }
                
                auto bytes_written = Stream.tellp( ) - starting_location;
                logger_log_debug(logger(), 15, "FrTOCEvent::write: Wrote ", bytes_written, " bytes");
                logger_log_debug(logger(), 10, "FrTOCEvent::write: Write complete");
            }

            // Implementation of FrTOCEventData static logger method
            SimpleLogger* FrTOCEventData::logger() {
                static SimpleLogger* instance = get_channel_logger("framecpp.frtoc.event");
                return instance;
            }

            // Implementation of FrTOCEventData dependency injection logger method
            SimpleLogger* FrTOCEventData::logger(const FrameCPP::Common::FrameCPPServices& services) {
                return services.logger_mgr.getLogger("framecpp.frtoc.event");
            }

        } // namespace FrTOCImpl
    } // namespace Universal
} // namespace FrameCPP
