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

//=======================================================================
/// This program is useful generating a compressed frame
///
/// \todo
///     Support argument to specify the version of the frame
///     specification of the output frame file.
//=======================================================================
#if HAVE_CONFIG_H
#include <framecpp_config.h>
#endif /* HAVE_CONFIG_H */

extern "C" {
#include <unistd.h>
} // extern "C"

#include <memory>
#include <sstream>
#include <string>

#include <boost/shared_array.hpp>
#include <boost/shared_ptr.hpp>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/CommandLineOptions.hh"
#include "ldastoolsal/unordered_map.hh"
#include "ldastoolsal/fstream.hh"

#include "framecpp/Common/Compression.hh"
#include "framecpp/Common/FrameBuffer.hh"
#include "framecpp/Common/FrameStream.hh"
#include "framecpp/FrVect.hh"

using std::string;

using LDASTools::AL::filebuf;
using LDASTools::AL::MemChecker;
using LDASTools::AL::unordered_map;
typedef LDASTools::AL::CommandLineOptions CommandLineOptions;

typedef CommandLineOptions::Option    Option;
typedef CommandLineOptions::OptionSet OptionSet;

using FrameCPP::FrVect;
using FrameCPP::Common::FrameBuffer;
using FrameCPP::Common::FrameSpec;
using FrameCPP::Common::IFrameStream;
using FrameCPP::Common::OFrameStream;

inline void
depart( int Status )
{
    exit( Status );
}

//-----------------------------------------------------------------------
//
//-----------------------------------------------------------------------
typedef unordered_map< string, FrVect::compress_type >
    compression_mode_map_type;

static const compression_mode_map_type&
init_cmm( )
{
    static compression_mode_map_type cmm;

    using namespace FrameCPP::Compression;

    //---------------------------------------------------------------------
    /// \todo This needs to be version specific
    //---------------------------------------------------------------------
    cmm[ "raw" ] = FrVect::RAW;
    cmm[ "none" ] = FrVect::RAW;
    cmm[ "gzip" ] = FrVect::GZIP;
    cmm[ "diff_gzip" ] = FrVect::DIFF_GZIP;
    cmm[ "zero_suppress" ] = FrVect::ZERO_SUPPRESS;
    cmm[ "zero_suppress_otherwize_gzip" ] =
        FrVect::ZERO_SUPPRESS_OTHERWISE_GZIP;

    return cmm;
}

const compression_mode_map_type& CompressionModeMap = init_cmm( );

//-----------------------------------------------------------------------
/// \brief Class to handle command line options for this application
//-----------------------------------------------------------------------
class CommandLine : protected CommandLineOptions
{
public:
    using CommandLineOptions::argc_type;
    using CommandLineOptions::argv_type;

    CommandLine( argc_type ArgC, char** ArgV );

    inline bool
    BadOption( ) const
    {
        bool retval = false;

        for ( const_iterator cur = begin( ), last = end( ); cur != last; ++cur )
        {
            if ( ( *cur )[ 0 ] == '-' )
            {
                std::cerr << "ABORT: Bad option: " << *cur << std::endl;
                retval = true;
            }
        }
        return retval;
    }

    inline INT_4U
    BlockSize( ) const
    {
        return m_block_size;
    }

    inline const std::string&
    Compression( ) const
    {
        return m_compression;
    }

    inline INT_4U
    Level( ) const
    {
        return m_level;
    }

    inline bool
    MemoryMappedIO( ) const
    {
        return m_memory_mapped_io;
    }

    inline const std::string&
    OutputName( ) const
    {
        return m_output_name;
    }

    inline void
    Usage( int ExitValue ) const
    {
        std::cout << "Usage: " << ProgramName( ) << m_options << std::endl;
        depart( ExitValue );
    }

    using CommandLineOptions::empty;
    using CommandLineOptions::Pop;
    using CommandLineOptions::size;

private:
    enum option_types
    {
        OPT_BLOCK_SIZE,
        OPT_HELP,
        OPT_COMPRESSION_LEVEL,
        OPT_COMPRESSION_MODE,
        OPT_MEMORY_MAPPED_IO,
        OPT_OUTPUT
    };

    OptionSet   m_options;
    INT_4U      m_block_size;
    std::string m_compression;
    INT_4U      m_level;
    bool        m_memory_mapped_io;
    std::string m_output_name;
};

CommandLine::CommandLine( argc_type ArgC, char** ArgV )
    : CommandLineOptions( ArgC, ArgV ), m_block_size( 256 ),
      m_compression( "zero_suppress_otherwize_gzip" ), m_level( 1 ),
      m_memory_mapped_io( false ), m_output_name( "CompressedOut.gwf" )
{
    INT_4U multiplier = 1024;

    //---------------------------------------------------------------------
    // Setup the options that will be recognized.
    //---------------------------------------------------------------------
    std::ostringstream desc;

    m_options.Synopsis( "[options] <input file>" );

    m_options.Summary( "This command will take the data from an input frame"
                       " and create a compressed output frame." );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specify the size in bytes of the input and output buffer."
            " If the number is followed by the letter 'k' or the"
            " letter 'M', then the buffer size is multiplied by 1024 or"
            " 1048576 respectively. (Default: "
         << BlockSize( ) << ( ( multiplier == 1024 ) ? "k" : "M" ) << " )";

    m_options.Add( Option( OPT_BLOCK_SIZE,
                           "block-size",
                           Option::ARG_REQUIRED,
                           desc.str( ),
                           "size" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specifies the level of compression."
            " Note that this option has no effect for some"
            " compression modes."
            " Also note that valid values are dependant on the"
            " compression mode used."
            " (Default: "
         << Level( ) << " )";
    m_options.Add( Option( OPT_COMPRESSION_LEVEL,
                           "compression-level",
                           Option::ARG_REQUIRED,
                           desc.str( ),
                           "level" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    {
        std::list< std::string > c;

        for ( compression_mode_map_type::const_iterator
                  cur = CompressionModeMap.begin( ),
                  last = CompressionModeMap.end( );
              cur != last;
              ++cur )
        {
            c.push_back( cur->first );
        }
        c.sort( );

        desc.str( "" );
        desc << "Specify the algorithm for compressing the data."
                " Currently supported modes are:";
        for ( std::list< std::string >::const_iterator cur = c.begin( ),
                                                       last = c.end( );
              cur != last;
              ++cur )
        {
            desc << " " << *cur;
        }
        desc << " (Default: " << Compression( ) << " )";
    }

    m_options.Add( Option( OPT_COMPRESSION_MODE,
                           "compression-mode",
                           Option::ARG_REQUIRED,
                           desc.str( ),
                           "mode" ) );
    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    m_options.Add(
        Option( OPT_HELP, "help", Option::ARG_NONE, "Display this message" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Enables memory mapped i/o for the input data."
            " (Default: "
         << ( ( MemoryMappedIO( ) ) ? "enabled" : "disabled" ) << " )";
    m_options.Add( Option( OPT_MEMORY_MAPPED_IO,
                           "memory-mapped-io",
                           Option::ARG_NONE,
                           desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specifies the output filename for the compressed data."
            " (Default: "
         << OutputName( ) << " )";
    m_options.Add( Option(
        OPT_OUTPUT, "output", Option::ARG_REQUIRED, desc.str( ), "filename" ) );

    //---------------------------------------------------------------------
    // Parse the options specified on the command line
    //---------------------------------------------------------------------

    try
    {
        std::string arg_name;
        std::string arg_value;
        bool        parsing = true;

        while ( parsing )
        {
            const int cmd_id( Parse( m_options, arg_name, arg_value ) );

            switch ( cmd_id )
            {
            case OPT_BLOCK_SIZE:
            {
                if ( arg_value.length( ) >= 1 )
                {
                    switch ( arg_value[ arg_value.length( ) - 1 ] )
                    {
                    case 'k':
                        multiplier = 1024;
                        break;
                    case 'M':
                        multiplier = 1024 * 1024;
                        break;
                    default:
                        break;
                    }
                }

                std::istringstream num( arg_value.substr(
                    0,
                    ( multiplier == 1 ) ? std::string::npos
                                        : arg_value.length( ) - 1 ) );
                num >> m_block_size;
            }
            break;
            case OPT_COMPRESSION_LEVEL:
            {
                std::istringstream input( arg_value );
                input >> m_level;
            }
            break;
            case OPT_COMPRESSION_MODE:
                m_compression = arg_value;
                break;
            case OPT_HELP:
            {
                Usage( 0 );
            }
            break;
            case OPT_MEMORY_MAPPED_IO:
                m_memory_mapped_io = true;
                break;
            case OPT_OUTPUT:
                m_output_name = arg_value;
                break;
            case CommandLineOptions::OPT_END_OF_OPTIONS:
                parsing = false;
                break;
            }
        }
    }
    catch ( ... )
    {
    }

    m_block_size *= multiplier;
}

//=======================================================================
// The main entry point to the application
//=======================================================================
int
main( int ArgC, char** ArgV )
try
{
    MemChecker::Trigger gc_trigger( true );
    CommandLine         cl( ArgC, ArgV );

    if ( ( cl.size( ) != 1 ) || ( cl.BadOption( ) ) )
    {
        cl.Usage( 1 );
    }

    FrameCPP::Initialize( );

    std::string InputName = cl.Pop( );

    //---------------------------------------------------------------------
    // Lookup the compression
    //---------------------------------------------------------------------
    INT_2U c = FrameCPP::Compression::MODE_RAW;

    compression_mode_map_type::const_iterator cm =
        CompressionModeMap.find( cl.Compression( ) );
    if ( cm != CompressionModeMap.end( ) )
    {
        c = cm->second;
    }
    else
    {
        std::ostringstream msg;
        msg << "Unknown compression mode: " << cl.Compression( );

        throw std::runtime_error( msg.str( ) );
    }

    //---------------------------------------------------------------------
    // Open the input stream
    //---------------------------------------------------------------------

    boost::shared_ptr< CHAR > ibuffer( new CHAR[ cl.BlockSize( ) ] );
    boost::shared_ptr< CHAR > obuffer( new CHAR[ cl.BlockSize( ) ] );

    FrameBuffer< filebuf >* ibuf( new FrameBuffer< filebuf >( std::ios::in ) );
    FrameBuffer< filebuf >* obuf( new FrameBuffer< filebuf >( std::ios::out ) );

    ibuf->pubsetbuf( ibuffer.get( ), cl.BlockSize( ) );
    ibuf->UseMemoryMappedIO( cl.MemoryMappedIO( ) );

    obuf->pubsetbuf( obuffer.get( ), cl.BlockSize( ) );
    obuf->UseMemoryMappedIO( cl.MemoryMappedIO( ) );

    ibuf->open( InputName.c_str( ), std::ios::in | std::ios::binary );
    obuf->open( cl.OutputName( ).c_str( ), std::ios::out | std::ios::binary );

    INT_4U       frame_index( 0 );
    IFrameStream iframe_stream( ibuf );
    OFrameStream oframe_stream( obuf );

    try
    {
        boost::shared_ptr< FrameSpec::Object > frame(
            iframe_stream.ReadFrameN( frame_index, true /* decompress */ ) );

        while ( frame )
        {
            //-----------------------------------------------------------------
            // Write the source frame to the output frame file
            //-----------------------------------------------------------------
            oframe_stream.WriteFrame( frame, c, cl.Level( ) );
            //-------------------------------------------------------------------
            // Read the next input frame
            //-------------------------------------------------------------------
            frame = iframe_stream.ReadFrameN( ++frame_index,
                                              true /* decompress */ );
        }
    }
    catch ( ... )
    {
    }
    //---------------------------------------------------------------------
    // Prepare to leave
    //---------------------------------------------------------------------
    oframe_stream.Close( );
    depart( 0 );
}
catch ( std::exception& e )
{
    std::cerr << "ABORT: Caught exception: " << e.what( ) << std::endl;
    depart( 1 );
}
catch ( ... )
{
    std::cerr << "ABORT: Caught unknown exception: " << std::endl;
    depart( 1 );
}
