//
// 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 <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

#include <iostream>
#include <memory>
#include <stdexcept>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/CommandLineOptions.hh"
#include "ldastoolsal/reverse.hh"

#include "framecpp/Common/FrameSpec.hh"
#include "framecpp/Common/Verify.hh"
#include "framecpp/IFrameStream.hh"

#define DEFAULT_BUFFER_SIZE 256

using std::filebuf;

using LDASTools::AL::MemChecker;

typedef LDASTools::AL::CommandLineOptions CommandLineOptions;
typedef CommandLineOptions::Option        Option;
typedef CommandLineOptions::OptionSet     OptionSet;
using FrameCPP::Common::FrameSpec;
using FrameCPP::Common::Verify;
using FrameCPP::Common::VerifyException;

typedef FrameCPP::Common::FrameBuffer< filebuf > FrameBuffer;

class IFrameStream : public FrameCPP::Common::IFrameStream
{
public:
    inline IFrameStream( buffer_type* Stream,
                         INT_2U       Version = FRAME_SPEC_CURRENT )
        : FrameCPP::Common::IFrameStream( Stream, 0 )
    {
    }
    using FrameCPP::Common::IFrameStream::tellg;
};

namespace
{
    enum error_type
    {
        MIN_ERROR = VerifyException::MAX_ERROR + 1,
        USAGE = MIN_ERROR,
        UNHANDLED_EXCEPTION,
        MAX_ERROR = UNHANDLED_EXCEPTION
    };

    static const char* error_strings[] = {
        "usage error",
        "an exception was thown and not handled within the control loop "
    };

} // namespace

//-----------------------------------------------------------------------
// Common tasks to be performed when exiting.
//-----------------------------------------------------------------------
inline void
depart( int ExitCode )
{
    exit( ExitCode );
}

//-----------------------------------------------------------------------
/// \brief Class to handle command line options for this application
//-----------------------------------------------------------------------
class CommandLine : protected CommandLineOptions
{
public:
    using CommandLineOptions::empty;
    using CommandLineOptions::Pop;
    typedef CommandLineOptions::option_type option_type;

    CommandLine( int ArgC, char** ArgV );

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

    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 size_t
    BlockSize( )
    {
        return m_block_size;
    }

    inline void
    BlockSize( size_t Value )
    {
        m_block_size = Value;
    }

    inline bool
    ChecksumFile( )
    {
        return m_checksum_file;
    }

    inline void
    ChecksumFile( bool Value )
    {
        m_checksum_file = Value;
    }

    inline bool
    ChecksumFileOnly( )
    {
        return m_checksum_file_only;
    }

    inline void
    ChecksumFileOnly( bool Value )
    {
        m_checksum_file_only = Value;
    }

    inline bool
    ChecksumFrames( )
    {
        return m_checksum_frames;
    }

    inline void
    ChecksumFrames( bool Value )
    {
        m_checksum_frames = Value;
    }

    inline bool
    ChecksumStructure( )
    {
        return m_checksum_structure;
    }

    inline void
    ChecksumStructure( bool Value )
    {
        m_checksum_structure = Value;
    }

    inline bool
    Force( )
    {
        return m_force;
    }

    inline void
    Force( bool Value )
    {
        m_force = Value;
    }

    void operator( )( );

    inline bool
    RelaxEOFChecksum( )
    {
        return m_relax_eof_checksum;
    }

    inline void
    RelaxEOFChecksum( bool Value )
    {
        m_relax_eof_checksum = Value;
    }

    inline bool
    UseMemoryMappedIO( )
    {
        return m_use_memory_mapped_io;
    }

    inline void
    UseMemoryMappedIO( bool Value )
    {
        m_use_memory_mapped_io = Value;
    }

    inline size_t
    Verbose( )
    {
        return m_verbose;
    }

    inline void
    VerboseInc( )
    {
        ++m_verbose;
    }

private:
    enum option_types
    {
        OPT_BLOCK_SIZE,
        OPT_CHECKSUM_FILE,
        OPT_CHECKSUM_FRAMES,
        OPT_CHECKSUM_STRUCTURE,
        OPT_FORCE,
        OPT_HELP,
        OPT_RELAX_EOF_CHECKSUM,
        OPT_USE_MEMORY_MAPPED_IO,
        OPT_VERBOSE
    };

    OptionSet m_options;
    size_t    m_block_size;
    bool      m_checksum_file;
    bool      m_checksum_file_only;
    bool      m_checksum_frames;
    bool      m_checksum_structure;
    bool      m_force;
    bool      m_relax_eof_checksum;
    bool      m_use_memory_mapped_io;
    size_t    m_verbose;
};

CommandLine::CommandLine( int ArgC, char** ArgV )
    : CommandLineOptions( ArgC, ArgV ), m_block_size( DEFAULT_BUFFER_SIZE ),
      m_checksum_file( false ), m_checksum_file_only( true ),
      m_checksum_frames( false ), m_checksum_structure( false ),
      m_force( false ), m_relax_eof_checksum( false ),
      m_use_memory_mapped_io( false ), m_verbose( 0 )
{
    std::set< VerifyException::error_type > acceptable_errors;

    acceptable_errors.insert( VerifyException::NO_ERROR );
    acceptable_errors.insert( VerifyException::CHECKSUM_ERROR );
    acceptable_errors.insert( VerifyException::FILE_OPEN_ERROR );
    acceptable_errors.insert( VerifyException::UNSUPPORTED_CHECKSUM_TYPE );
    acceptable_errors.insert( VerifyException::NO_CHECKSUM );
    acceptable_errors.insert( VerifyException::INVALID_FRAME_STRUCTURE );
    acceptable_errors.insert( VerifyException::FILE_TRUNCATION );
    //---------------------------------------------------------------------
    // Setup the options that will be recognized.
    //---------------------------------------------------------------------
    std::ostringstream desc;

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

    m_options.Summary(
        "This verify the contents of each file on the command line." );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    m_options.Add(
        Option( OPT_HELP, "help", Option::ARG_NONE, "Display this message" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Verify each of the files instead of exiting on first exit."
            " (Default: "
         << ( ( Force( ) ) ? "true" : "false" ) << " )";
    m_options.Add(
        Option( OPT_FORCE, "force", Option::ARG_NONE, desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Accepts files that lack an end of file checksum."
            " (Default: "
         << ( ( RelaxEOFChecksum( ) ) ? "enabled" : "disabled" ) << " )";
    m_options.Add( Option( OPT_RELAX_EOF_CHECKSUM,
                           "relax-eof-checksum",
                           Option::ARG_NONE,
                           desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Display file name and exit status of each file processed."
            " If it appears multiple times on the command line,"
            " the level of verbosity will increase."
            " At level 2 or higher, surfaces the calculated and stored"
            " checksum values for each comparison performed."
            " (Default: "
         << ( ( Verbose( ) ) ? "verbose" : "silent" ) << " )";
    m_options.Add(
        Option( OPT_VERBOSE, "verbose", Option::ARG_NONE, desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Enable validation of the file checksum.";
    m_options.Add( Option(
        OPT_CHECKSUM_FILE, "checksum-file", Option::ARG_NONE, desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Enable validation of the frame checksums.";
    m_options.Add( Option( OPT_CHECKSUM_FRAMES,
                           "checksum-frame",
                           Option::ARG_NONE,
                           desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Enable validation of the structure checksums.";
    m_options.Add( Option( OPT_CHECKSUM_STRUCTURE,
                           "checksum-structure",
                           Option::ARG_NONE,
                           desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Use memory mapped io."
            " (Default: "
         << ( ( UseMemoryMappedIO( ) ) ? "enabled" : "disabled" ) << " )";
    m_options.Add( Option(
        OPT_USE_MEMORY_MAPPED_IO, "use-mmap", Option::ARG_NONE, desc.str( ) ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Number of 1k blocks to use for io."
            " (Default: "
         << BlockSize( ) << " )";
    m_options.Add( Option( OPT_BLOCK_SIZE,
                           "bs",
                           Option::ARG_REQUIRED,
                           desc.str( ),
                           "<block_size>" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    static OptionSet exit_group[ 3 ];
    static OptionSet exit_help[ MAX_ERROR + 1 ];

    static OptionSet* exit_group_ptr =
        &exit_group[ ( sizeof( exit_group ) / sizeof( *exit_group ) ) - 1 ];

    exit_group[ 0 ].Summary( " " );
    exit_group[ 1 ].Summary( "Exit Code Summary:" );
    exit_group[ 2 ].Summary( " " );
    for ( unsigned int e = 0;
          e < ( sizeof( exit_group ) / sizeof( *exit_group ) );
          ++e )
    {
        m_options.Add( exit_group[ e ] );
    }

    for ( unsigned int e = VerifyException::NO_ERROR;
          e <= VerifyException::MAX_ERROR;
          ++e )
    {
        if ( acceptable_errors.find( VerifyException::error_type( e ) ) ==
             acceptable_errors.end( ) )
        {
            continue;
        }
        std::ostringstream msg;

        msg << e << " - "
            << VerifyException::StrError( (VerifyException::error_type)e );
        exit_help[ e ].Summary( msg.str( ) );
        exit_group_ptr->Add( exit_help[ e ] );
    }
    for ( unsigned int e = MIN_ERROR; e <= MAX_ERROR; ++e )
    {
        std::ostringstream msg;

        msg << e << " - " << error_strings[ e - MIN_ERROR ];
        exit_help[ e ].Summary( msg.str( ) );
        exit_group_ptr->Add( exit_help[ e ] );
    }
}

void
CommandLine::operator( )( )
{
    //---------------------------------------------------------------------
    // 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_HELP:
            {
                Usage( 0 );
            }
            break;
            case CommandLineOptions::OPT_END_OF_OPTIONS:
                parsing = false;
                break;
            case OPT_BLOCK_SIZE:
            {
                std::istringstream bs_buffer( arg_value );
                size_t             bs;

                bs_buffer >> bs;
                BlockSize( bs );
            }
            break;
            case OPT_CHECKSUM_FILE:
                ChecksumFile( true );
                ChecksumFileOnly( false );
                break;
            case OPT_CHECKSUM_FRAMES:
                ChecksumFrames( true );
                ChecksumFileOnly( false );
                break;
            case OPT_CHECKSUM_STRUCTURE:
                ChecksumStructure( true );
                ChecksumFileOnly( false );
                break;
            case OPT_FORCE:
                Force( true );
                break;
            case OPT_RELAX_EOF_CHECKSUM:
                RelaxEOFChecksum( true );
                break;
            case OPT_USE_MEMORY_MAPPED_IO:
                UseMemoryMappedIO( true );
                break;
            case OPT_VERBOSE:
                VerboseInc( );
                break;
            }
        }
    }
    catch ( const std::invalid_argument& Error )
    {
        std::cerr << "DEBUG:"
                  << " invalid argument" << std::endl;
        throw;
    }
    catch ( ... )
    {
    }
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
int
main( int ArgC, char** ArgV )
{
    MemChecker::Trigger gc_trigger( true );
    CommandLine         cl( ArgC, ArgV );

    FrameCPP::Initialize( );

    try
    {
        cl( );

        Verify verifier;
        INT_4U exit_status = VerifyException::NO_ERROR;

        //-------------------------------------------------------------------
        // Disable memory mapped I/O so failed reads can return without
        //   causing a signal.
        //-------------------------------------------------------------------
        verifier.BufferSize( cl.BlockSize( ) * 1024 );
        verifier.UseMemoryMappedIO( cl.UseMemoryMappedIO( ) );
        verifier.CheckDataValid( false );
        verifier.Expandability( false );
        verifier.MustHaveEOFChecksum( !cl.RelaxEOFChecksum( ) );
        verifier.Strict( false );
        verifier.ValidateMetadata( false );

        verifier.CheckFrameChecksum( cl.ChecksumFrames( ) );
        verifier.CheckFileChecksum( cl.ChecksumFile( ) ||
                                    cl.ChecksumStructure( ) );
        verifier.CheckFileChecksumOnly( cl.ChecksumFileOnly( ) );

        if ( cl.empty( ) || cl.BadOption( ) )
        {
            cl.Usage( USAGE );
        }

        while ( cl.empty( ) == false )
        {
            INT_4U new_exit_status = VerifyException::NO_ERROR;

            CommandLine::option_type filename = cl.Pop( );

            if ( cl.Verbose( ) )
            {
                std::cout << filename << ": " << std::flush;
            }
            try
            {
                verifier( filename );
            }
            catch ( const VerifyException& Exception )
            {
                new_exit_status = Exception.ErrorCode( );
            }
            catch ( const std::exception& Exception )
            {
                if ( cl.Verbose( ) )
                {
                    std::cout << "\tABORT: Caught exception: "
                              << Exception.what( );
                }
                std::cout << std::endl;
                new_exit_status = UNHANDLED_EXCEPTION;
                continue;
            }
            catch ( ... )
            {
                new_exit_status = UNHANDLED_EXCEPTION;
                if ( cl.Verbose( ) )
                {
                    std::cout << std::endl;
                    std::cout << "\tABORT: Caught unknown exception: ";
                }
                continue;
            }

            if ( cl.Verbose( ) )
            {
                std::string errorinfo( /* verifier.ErrorInfo( ) */ "" );

                std::cout << " " << new_exit_status
#if VERBOSE_TEXT_STATUS
                          << ( ( new_exit_status == 0 ) ? " PASSED"
                                                        : " FAILED" )
#endif /* VERBOSE_TEXT_STATUS */
                    ;
                if ( cl.Verbose( ) >= 2 )
                {
                    if ( errorinfo.length( ) > 0 )
                    {
                        std::cout << std::endl << "\t" << errorinfo;
                    }
                    // Issue #151: surface every calculated/stored checksum
                    // comparison performed during this file's verify pass.
                    Verify::checksum_info_container_type checksums(
                        verifier.ChecksumInfoQueue( ) );
                    for ( const auto& info : checksums )
                    {
                        std::cout << std::endl
                                  << "\tchecksum " << info.structure
                                  << ": stored=0x" << std::hex << info.stored
                                  << " calculated=0x" << info.calculated
                                  << std::dec << " ("
                                  << ( info.matched ? "match" : "MISMATCH" )
                                  << ")";
                    }
                }
                std::cout << std::endl << std::flush;
            }
            if ( ( cl.Force( ) == false ) &&
                 ( new_exit_status != VerifyException::NO_ERROR ) )
            {
                //--------------------------------------------------------------
                // No, so just exit with the exit status
                //--------------------------------------------------------------
                depart( new_exit_status );
            }
            if ( exit_status < new_exit_status )
            {
                exit_status = new_exit_status;
            }
        }
        depart( exit_status );
    }
    catch ( std::invalid_argument& e )
    {
        std::cerr << "ABORT: " << e.what( ) << std::endl;
        cl.Usage( USAGE );
    }
    catch ( std::exception& e )
    {
        std::cerr << std::endl
                  << "ABORT: Caught exception: " << e.what( ) << std::endl;
        depart( UNHANDLED_EXCEPTION );
    }
    catch ( ... )
    {
        std::cerr << std::endl
                  << "ABORT: Caught unknown exception: " << std::endl;
        depart( UNHANDLED_EXCEPTION );
    }
}
