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

#ifndef FrameCPP_UNIVERSAL__IMPL__FrVectClassicIO_HH
#define FrameCPP_UNIVERSAL__IMPL__FrVectClassicIO_HH

#if !defined( SWIGIMPORTED )
#include <algorithm>
#include <type_traits>

#include "framecpp/Common/Description.hh"
#include "framecpp/Common/FrameStream.hh"
#include "framecpp/Common/Verify.hh"

#endif /* !defined( SWIGIMPORTED ) */

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

#if !defined( SWIGIMPORTED )
#include "framecpp/Universal/impl/FrameSpec.hh"
#endif /* ! defined(SWIGIMPORTED) */

#if !defined( SWIGIMPORTED )
namespace FrameCPP
{
    namespace Universal
    {
        class FrVect;

        namespace FrVectImpl
        {
            template < typename T >
            class ClassicIO : public virtual Data,
                              public Universal::ObjectWithChecksum<
                                  Common::FrameSpec::Object >
            {
            public:
                using FrameCPP::Common::FrameSpec::ObjectInterface::
                    demote_arg_type;
                using FrameCPP::Common::FrameSpec::ObjectInterface::
                    demote_stream_type;
                using FrameCPP::Common::FrameSpec::ObjectInterface::
                    promote_arg_type;
                
                // Bring base class GetDescription methods into scope
                using FrameCPP::Common::FrameSpec::Object::GetDescription;
                using FrameCPP::Common::FrameSpec::ObjectInterface::
                    promote_stream_type;

                //-----------------------------------------------------------------
                /// \brief Retrieve the compression algorithm number.
                ///
                /// \return
                ///     The compression algorithm number.
                //-----------------------------------------------------------------
                INT_2U
                compressDemote( Common::FrameSpec::version_type FrameVersion );

                //-----------------------------------------------------------------
                /// \brief Get converted data for target version (with caching)
                ///
                /// This method performs conversion ONCE and caches the result.
                /// Both pBytes() and pWrite() call this method, ensuring they
                /// use identical conversion results.
                ///
                /// \param[in] target_version
                ///     Frame specification version to convert to
                ///
                /// \return
                ///     Const reference to cached ConversionCache
                //-----------------------------------------------------------------
                const typename Data::ConversionCache&
                getConvertedData( INT_2U target_version ) const
                {
                    // Cache hit: return existing conversion
                    if ( m_data.m_conversion_cache.valid &&
                         m_data.m_conversion_cache.target_version == target_version )
                    {
                        return m_data.m_conversion_cache;
                    }

                    // Cache miss: perform conversion ONCE
                    using NonConstType = std::remove_const_t<
                        std::remove_reference_t< decltype( *this ) > >;
                    auto* nonConstThis = const_cast< NonConstType* >( this );

                    if ( target_version >= 9 )
                    {
                        // No conversion needed for v9+
                        m_data.m_conversion_cache.target_version = target_version;
                        m_data.m_conversion_cache.compression_mode =
                            static_cast< INT_2U >( m_data.compress );
                        m_data.m_conversion_cache.converted_data = m_data.data;
                        m_data.m_conversion_cache.converted_nbytes = m_data.nBytes;
                        m_data.m_conversion_cache.valid = true;
                    }
                    else
                    {
                        // Save original state
                        auto orig_compress = m_data.compress;
                        auto orig_data = m_data.data;
                        auto orig_nBytes = m_data.nBytes;
                        auto orig_version = m_data.actual_compression_version;

                        // Perform conversion (modifies object temporarily)
                        m_data.m_conversion_cache.compression_mode =
                            nonConstThis->compressDemote( target_version );

                        // DEEP COPY converted data to cache (not just shared_ptr copy)
                        auto src_data = GetDataRaw();
                        auto src_nbytes = GetNBytes();

                        m_data.m_conversion_cache.converted_data.reset( new CHAR_U[ src_nbytes ] );
                        std::copy( src_data.get(), src_data.get() + src_nbytes,
                                   m_data.m_conversion_cache.converted_data.get() );
                        m_data.m_conversion_cache.converted_nbytes = src_nbytes;
                        m_data.m_conversion_cache.target_version = target_version;
                        m_data.m_conversion_cache.valid = true;

                        // Restore original state
                        nonConstThis->m_data.compress = orig_compress;
                        nonConstThis->m_data.data = orig_data;
                        nonConstThis->m_data.nBytes = orig_nBytes;
                        nonConstThis->m_data.actual_compression_version =
                            orig_version;
                    }

                    return m_data.m_conversion_cache;
                }

                //-------------------------------------------------------
                //
                //-------------------------------------------------------
                ClassicIO( )
                    : Universal::ObjectWithChecksum<
                          Common::FrameSpec::Object >( FrameSpecId( ), nullptr )
                {
                }

                virtual Common::FrameSpec::Object*
                CloneCompressed( cmn_compression_scheme_type Scheme,
                                 cmn_compression_level_type  Level ) const
                {
                    Common::FrameSpec::ObjectInterface::unique_object_type
                                                  retval;
                    boost::shared_array< CHAR_U > compressed_data;
                    INT_8U                        nbytes( 0 );

                    if ( Scheme == GetCompress( ) )
                    {
                        // Nothing to do. Just return
                        return retval.release( );
                    }

                    compressToBuffer( compress_type_map( ),
                                      compress_type_reverse_map( ),
                                      data_type_map( ),
                                      GetType( ),
                                      GetNData( ),
                                      GetDataRaw( ).get( ),
                                      GetNBytes( ),
                                      GetCompress( ),
                                      compressed_data,
                                      nbytes,
                                      Scheme,
                                      Level );

                    if ( nbytes && compressed_data.get( ) )
                    {
                        //-------------------------------------------------------------------
                        // Save the compressed data
                        //-------------------------------------------------------------------
                        retval.reset(
                            new T( GetName( ),
                                   static_cast< compression_scheme_type >(
                                       MODE_HOSTENDIAN | ( Scheme & 0xFF ) ),
                                   GetType( ),
                                   GetNDim( ),
                                   &( m_data.dimension[ 0 ] ),
                                   GetNData( ),
                                   nbytes,
                                   compressed_data,
                                   GetUnitY( ) ) );

                        //-------------------------------------------------------------------
                        // Preserve dataValid information if present
                        //-------------------------------------------------------------------
                        if ( GetNDataValid() > 0 && m_data.dataValid.get() != nullptr )
                        {
                            static_cast< T* >( retval.get() )->SetDataValid(
                                m_data.dataValid.get(),
                                GetNDataValid(),
                                static_cast< dataValidCompScheme_type >( GetDataValidCompress() ) );
                        }
                    }
                    //---------------------------------------------------------------------
                    // Return the results
                    //---------------------------------------------------------------------
                    return retval.release( );
                }

                //-----------------------------------------------------------------
                /// \brief equality operator for abstract data type
                ///
                /// Required override of the pure-virtual operator== on the
                /// base Common::FrameSpec::Object. Typed equality is
                /// provided by a hidden friend on FrVect itself — see
                /// FrVect.hh.
                ///
                /// \param[in] RHS
                ///     The object to be compared.
                ///
                /// \return
                ///     The value true is returned if this object is
                ///     equivelent to the RHS instance; false otherwise.
                //-----------------------------------------------------------------
                virtual bool
                operator==( const Common::FrameSpec::Object& RHS ) const;

            protected:
                typedef STRING io_name_type;
                typedef STRING io_unit_y_type;
                using io_compress_type = compress_type;

                //-----------------------------------------------------------------
                /// \brief Retrieve the compression algorithm number.
                ///
                /// \return
                ///     The compression algorithm number.promoted to the latest
                ///     frame specification
                //-----------------------------------------------------------------
                void
                compressPromote( Common::FrameSpec::version_type FrameVersion );

                //-----------------------------------------------------------------
                /// \brief Verify the integrity of the object
                ///
                /// \param Verifier
                ///     The handle to the Verify object used to store data
                ///     that needs to be accumulated before being processed.
                /// \param Stream
                ///     The input stream from which the object was created.
                ///
                /// \remarks
                ///     Upon failure, this method thows an exception.
                //-----------------------------------------------------------------
                virtual void
                VerifyObject( Common::Verify&       Verifier,
                              Common::IFrameStream& Stream ) const
                {
                    if ( Verifier.Expandability( ) )
                    {
                        //-------------------------------------------------------------------
                        // Verify that the buffer can be decompressed
                        //-------------------------------------------------------------------
                        nBytes_type nBytes( 0 );
                        data_type   expanded_buffer;

                        expandToBuffer( expanded_buffer, nBytes );
                    }
                }

                //-----------------------------------------------------------------
                /// \brief Demotes object to previous version of the frame
                /// spec
                ///
                /// \param[in] Target
                ///     The version of the frame specification to demote
                ///     too.
                /// \param[in] Obj
                ///     The version of the object to demote.
                /// \param[in] Stream
                ///     The input stream from which the original object was
                ///     read.
                ///
                /// \return
                ///     An object of the previous generation.
                //-----------------------------------------------------------------
                virtual demote_ret_type
                demote( INT_2U              Target,
                        demote_arg_type     Obj,
                        demote_stream_type* Stream ) const
                {
                    // Universal objects are version-agnostic, no conversion needed
                    return Obj;
#if NEEDED
                    if ( Target >= DATA_FORMAT_VERSION )
                    {
                        return Obj;
                    }
                    try
                    {
                        //-------------------------------------------------------------------
                        // Copy non-reference information
                        //-------------------------------------------------------------------
                        std::vector< Previous::Dimension > dims;
                        for ( std::vector< Dimension >::const_iterator
                                  cur = m_data.dimension.begin( ),
                                  last = m_data.dimension.end( );
                              cur != last;
                              ++cur )
                        {
                            dims.push_back(
                                Previous::Dimension( cur->GetNx( ),
                                                     cur->GetDx( ),
                                                     cur->GetUnitX( ),
                                                     cur->GetStartX( ) ) );
                        }
                        // Do actual down conversion
                        auto compression_mode = GetCompress( );
                        auto data = GetDataRaw( );
                        auto nbytes = GetNBytes( );
                        switch ( compression_mode )
                        {
                        case BIGENDIAN_RAW:
                            compression_mode = Previous::FrVect::BIGENDIAN_RAW;
                            break;
                        case LITTLEENDIAN_RAW:
                            compression_mode =
                                Previous::FrVect::LITTLEENDIAN_RAW;
                            break;
                        case BIGENDIAN_GZIP:
                            compression_mode = Previous::FrVect::BIGENDIAN_GZIP;
                            break;
                        case LITTLEENDIAN_GZIP:
                            compression_mode =
                                Previous::FrVect::LITTLEENDIAN_GZIP;
                            break;
                        case BIGENDIAN_DIFF_GZIP:
                            compression_mode =
                                Previous::FrVect::BIGENDIAN_DIFF_GZIP;
                            break;
                        case LITTLEENDIAN_DIFF_GZIP:
                            compression_mode =
                                Previous::FrVect::LITTLEENDIAN_DIFF_GZIP;
                            break;
                        case BIGENDIAN_ZERO_SUPPRESS:
                            switch ( GetType( ) )
                            {
                            case FR_VECT_2S:
                            case FR_VECT_2U:
                                compression_mode = Previous::FrVect::
                                    BIGENDIAN_ZERO_SUPPRESS_WORD_2;
                                break;
                            case FR_VECT_4S:
                            case FR_VECT_4U:
                            case FR_VECT_4R:
                            case FR_VECT_8C:
                                compression_mode = Previous::FrVect::
                                    BIGENDIAN_ZERO_SUPPRESS_WORD_4;
                                break;
                            case FR_VECT_8S:
                            case FR_VECT_8U:
                            case FR_VECT_8R:
                            case FR_VECT_16C:
                                compression_mode = Previous::FrVect::
                                    BIGENDIAN_ZERO_SUPPRESS_WORD_8;
                                break;
                            default:
                                //-----------------------------------------
                                // Since it cannot be handled here,
                                //   need to use a decompressed buffer.
                                //-----------------------------------------
                                goto do_default;
                            }
                            throw Unimplemented( "Object* FrEndOfFile::demote( "
                                                 "Object* Obj ) const",
                                                 DATA_FORMAT_VERSION,
                                                 __FILE__,
                                                 __LINE__ );
                            return ( nullptr );
                            throw Unimplemented( "Object* FrEndOfFile::demote( "
                                                 "Object* Obj ) const",
                                                 DATA_FORMAT_VERSION,
                                                 __FILE__,
                                                 __LINE__ );
                            return ( nullptr );
                            break;
                        case LITTLEENDIAN_ZERO_SUPPRESS:
                            switch ( GetType( ) )
                            {
                            case FR_VECT_2S:
                            case FR_VECT_2U:
                                compression_mode = Previous::FrVect::
                                    LITTLEENDIAN_ZERO_SUPPRESS_WORD_2;
                                break;
                            case FR_VECT_4S:
                            case FR_VECT_4U:
                            case FR_VECT_4R:
                            case FR_VECT_8C:
                                compression_mode = Previous::FrVect::
                                    LITTLEENDIAN_ZERO_SUPPRESS_WORD_4;
                                break;
                            case FR_VECT_8S:
                            case FR_VECT_8U:
                            case FR_VECT_8R:
                            case FR_VECT_16C:
                                compression_mode = Previous::FrVect::
                                    LITTLEENDIAN_ZERO_SUPPRESS_WORD_8;
                                break;
                            default:
                                //-----------------------------------------
                                // Since it cannot be handled here,
                                //   need to use a decompressed buffer.
                                //-----------------------------------------
                                goto do_default;
                            }
                            break;
                        default:
                        do_default:
                            //-------------------------------------------
                            // Unable to directly convert the compression
                            //   mode. Return the data uncompressed.
                            //-------------------------------------------
                            compression_mode = Previous::FrVect::RAW;
                            nbytes = GetNData( ) * GetTypeSize( GetType( ) );
                            data = Previous::FrVect::DataAlloc(
                                GetType( ), &( dims[ 0 ] ), dims.size( ) );
                            GetDataUncompressed( data );
                        }
                        boost::shared_ptr< Previous::FrVect > retval(
                            new Previous::FrVect( GetName( ),
                                                  compression_mode,
                                                  GetType( ),
                                                  dims.size( ),
                                                  &( dims[ 0 ] ),
                                                  GetNData( ),
                                                  nbytes,
                                                  data,
                                                  GetUnitY( ) ) );
                        //-------------------------------------------------------------------
                        // Return demoted object
                        //-------------------------------------------------------------------
                        return retval;
                    }
                    catch ( ... )
                    {
                    }
                    throw Unimplemented(
                        "Object* FrVect::demote( Object* Obj ) const",
                        DATA_FORMAT_VERSION,
                        __FILE__,
                        __LINE__ );
#endif /* NEEDED */
                }

                //-----------------------------------------------------------------
                /// \brief The name structure name of this object.
                ///
                /// \return
                ///     The name of the structure as specified by the frame
                ///     specification.
                //-----------------------------------------------------------------
                virtual const char*
                ObjectStructName( ) const
                {
                    return ( FrameSpecName( ) );
                }

                //-----------------------------------------------------------------
                /// @brief The data structure description
                ///
                /// @return
                ///     The description of the data structure used to
                ///     represent this object when written to a GWF stream
                //-----------------------------------------------------------------
                virtual const Common::Description*
                GetDescription( unsigned int version ) const;

                //-----------------------------------------------------------------
                /// \brief Promotes object to another version of the frame
                /// spec
                ///
                /// \param[in] Target
                ///     The version of the promoted frame specification.
                /// \param[in] Obj
                ///     The object to be promoted.
                /// \param[in] Stream
                ///     The input stream from which the original object was
                ///     read.
                ///
                /// \return
                ///     An object promoted to the next generation.
                //-----------------------------------------------------------------
                virtual promote_ret_type
                promote( INT_2U               Target,
                         promote_arg_type     Obj,
                         promote_stream_type* Stream ) const
                {
                    // Universal objects are version-agnostic, no conversion needed
                    return Obj;
                }

                //-----------------------------------------------------------------
                /// \brief Number of bytes needed to write this structure
                ///
                /// \param[in] Stream
                ///     The stream from which to the object is being read or
                ///     written.
                ///
                /// \return
                ///     The number of bytes need to read or write this
                ///     object.
                //-----------------------------------------------------------------
                virtual FrameCPP::cmn_streamsize_type
                pBytes( const Common::StreamBase& Stream ) const
                {
                    auto frame_stream_version = StreamVersion( Stream );

                    // Get converted data (may use cache or perform conversion once)
                    const auto& converted = getConvertedData( frame_stream_version );

                    return (
                        io_name_type::Bytes( m_data.name ) // name
                        + sizeof( io_compress_type(
                              converted.compression_mode ) ) // compress
                        + sizeof( m_data.type ) // type
                        + ( ( frame_stream_version >= 5 ) // nData
                                ? sizeof( m_data.nData )
                                : sizeof( v3::nData_type ) ) +
                        ( ( frame_stream_version >= 5 ) // nBytes
                              ? sizeof( m_data.nBytes )
                              : sizeof( v3::nBytes_type ) ) +
                        converted.converted_nbytes // data (FIXED: use converted size)
                        +
                        get_bytes_for_object( m_data.dimension,
                                              Stream ) // dimension information
                        + io_unit_y_type::Bytes( m_data.unitY ) // unitY
                        //-----------------------------------------------
                        + ( ( frame_stream_version >= 9 ) // nDataValid
                                ? sizeof( m_data.nDataValid )
                                : 0 ) +
                        ( ( frame_stream_version >= 9 ) // dataValidCompScheme
                              ? sizeof( io_compress_type(
                                    m_data.dataValidCompScheme ) )
                              : 0 ) +
                        ( ( frame_stream_version >= 9 ) // nDataValidCompBytes
                              ? sizeof( m_data.nDataValidCompBytes )
                              : 0 ) +
                        ( ( frame_stream_version >= 9 ) // dataValid
                              ? m_data.nDataValidCompBytes
                              : 0 )
                        //-----------------------------------------------
                        + Stream.PtrStructBytes( ) // next
                    );
                }

                //-----------------------------------------------------------------
                /// \brief Stream constructor
                ///
                /// \param[in] Stream
                ///     The input stream from where the object is being
                ///     read.
                //-----------------------------------------------------------------
                virtual self_type* pCreate( istream_type& Stream ) const;

                //-----------------------------------------------------------------
                /// \brief Write the structure to the stream
                ///
                /// \param[in] Stream
                ///     The output stream where the object is to be written.
                //-----------------------------------------------------------------
                virtual void pWrite( ostream_type& Stream ) const;

            private:
                //=====================================================================
                // Local classes
                //=====================================================================
                class DimBuffer
                {
                public:
                    DimBuffer( )
                    {
                    }

                    DimBuffer( const dimension_container_type& Dims )
                    {
                        operator( )( Dims );
                    }

                    void
                    operator( )( const dimension_container_type& Dims )
                    {
                        nDim_type nDim( Dims.size( ) );

                        m_nx.resize( nDim );
                        m_dx.resize( nDim );
                        m_startX.resize( nDim );
                        m_unitX.resize( nDim );

                        for ( nDim_type x = 0; x < nDim; x++ )
                        {
                            m_nx[ x ] = Dims[ x ].GetNx( );
                            m_dx[ x ] = Dims[ x ].GetDx( );
                            m_startX[ x ] = Dims[ x ].GetStartX( );
                            m_unitX[ x ] = Dims[ x ].GetUnitX( );
                        }
                    }

#if NEEDED
                    void
                    operator( )(
                        const std::vector< Previous::Dimension >& Dims )
                    {
                        nDim_type nDim( Dims.size( ) );

                        m_nx.resize( nDim );
                        m_dx.resize( nDim );
                        m_startX.resize( nDim );
                        m_unitX.resize( nDim );

                        for ( nDim_type x = 0; x < nDim; x++ )
                        {
                            m_nx[ x ] = Dims[ x ].GetNx( );
                            m_dx[ x ] = Dims[ x ].GetDx( );
                            m_startX[ x ] = Dims[ x ].GetStartX( );
                            m_unitX[ x ] = Dims[ x ].GetUnitX( );
                        }
                    }
#endif /* NEEDED  */

                    void
                    copy( dimension_container_type& Dims )
                    {
                        nDim_type nDim( m_nx.size( ) );

                        Dims.resize( nDim );
                        for ( nDim_type x = 0; x < nDim; x++ )
                        {
                            Dims[ x ] = dimension_container_type::value_type(
                                m_nx[ x ],
                                m_dx[ x ],
                                m_unitX[ x ],
                                m_startX[ x ] );
                        }
                    }

                    static FrameCPP::cmn_streamsize_type
                    GetMinimumBytes( )
                    {
                        return sizeof( INT_8U ) + sizeof( REAL_8 ) +
                            sizeof( REAL_8 ) + STRING::Bytes( "" );
                    }

                    friend ostream_type&
                    operator<<( ostream_type& Stream, const DimBuffer& Dims )
                    {
                        auto frame_stream_version = StreamVersion( Stream );
                        nDim_type nDim( Dims.m_nx.size( ) );

                        Stream << nDim;
                        if ( frame_stream_version >= 5 )
                        {
                            Stream << Dims.m_nx;
                        }
                        else
                        {
                            auto nx_array =
                                std::vector< nx_v3_type >( Dims.m_nx.size( ) );
                            std::transform(
                                Dims.m_nx.begin( ),
                                Dims.m_nx.end( ),
                                nx_array.begin( ),
                                []( INT_8U value ) {
                                    return static_cast< decltype(
                                        nx_array )::value_type >(
                                        value ); // Direct conversion
                                                 // without checks
                                } );
                            Stream << nx_array;
                        }
                        Stream << Dims.m_dx;
                        if ( frame_stream_version >= 4 )
                        {
                            Stream << Dims.m_startX;
                        }
                        Stream << Dims.m_unitX;
                        return Stream;
                    }

                    friend istream_type&
                    operator>>( istream_type& Stream, DimBuffer& Dims )
                    {
                        nDim_type nDim( 0 );
                        Stream >> nDim;

                        Dims.m_nx.resize( nDim );
                        Dims.m_dx.resize( nDim );
                        Dims.m_startX.resize( nDim );
                        Dims.m_unitX.resize( nDim );

                        Stream >> Dims.m_nx >> Dims.m_dx >> Dims.m_startX >>
                            Dims.m_unitX;

                        return Stream;
                    }

                private:
                    friend class FrameCPP::Common::IStream;
                    friend class FrameCPP::Common::OStream;

                    typedef INT_4U nx_v3_type;

                    std::vector< INT_8U > m_nx;
                    std::vector< REAL_8 > m_dx;
                    std::vector< REAL_8 > m_startX;
                    std::vector< STRING > m_unitX;
                };

                //-----------------------------------------------------------------
                /// \brief Object with checksum data.
                //-----------------------------------------------------------------
                typedef Universal::ObjectWithChecksum<
                    Common::FrameSpec::Object >
                    object_type;

                using Common::FrameSpec::Object::Create;

                template < typename OBJECT_TYPE >
                static FrameCPP::cmn_streamsize_type
                get_bytes_for_object( const OBJECT_TYPE&        Object,
                                      const Common::StreamBase& Stream );
            }; // namespace FrVectImpl

        } // namespace FrVectImpl
    } // namespace Universal
} // namespace FrameCPP

#endif /* ! defined(SWIGIMPORTED) */

#endif /* FrameCPP_UNIVERSAL__IMPL__FrVectClassicIO_HH */
