//
// 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 <algorithm>
#include <iostream>
#include <memory>
#include <stdexcept>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/CommandLineOptions.hh"
#include "ldastoolsal/Directory.hh"
#include "ldastoolsal/regex.hh"
#include "ldastoolsal/regexmatch.hh"

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

#define DEFAULT_BUFFER_SIZE 256

using LDASTools::AL::MemChecker;

typedef LDASTools::AL::CommandLineOptions CommandLineOptions;
typedef CommandLineOptions::Option        Option;
typedef CommandLineOptions::OptionSet     OptionSet;

typedef struct
{
    size_t offset;
    size_t len;
    size_t num;
} param_type;

namespace
{
    std::string RegexReplace( const std::string& Source,
                              const Regex&       Pattern,
                              const std::string& Format );

    enum error_type
    {
        USAGE = 1,
        UNHANDLED_EXCEPTION,
        MAX_ERROR = UNHANDLED_EXCEPTION
    };

    class TrAdcData : public FrameCPP::FrAdcData
    {
    public:
        TrAdcData( const FrameCPP::FrAdcData& Source )
            : FrameCPP::FrAdcData( Source )
        {
        }

        void
        SetName( const std::string& Name )
        {
            setName( Name );
        }

    private:
        FrameCPP::cmn_streamsize_type
        Bytes( const FrameCPP::Common::StreamBase& Stream ) const
        {
            return 0;
        }

        virtual self_type*
        Create( istream_type& Stream ) const
        {
            return ( (self_type*)NULL );
        }

        virtual void
        Write( ostream_type& Stream ) const
        {
        }
    };

    class TrProcData : public FrameCPP::FrProcData
    {
    public:
        TrProcData( const FrameCPP::FrProcData& Source )
            : FrameCPP::FrProcData( Source )
        {
        }

        void
        SetName( const std::string& Name )
        {
            name = Name;
        }

    private:
        FrameCPP::cmn_streamsize_type
        Bytes( const FrameCPP::Common::StreamBase& Stream ) const
        {
            return 0;
        }

        virtual self_type*
        Create( istream_type& Stream ) const
        {
            return ( (self_type*)NULL );
        }

        virtual void
        Write( ostream_type& Stream ) const
        {
        }
    };

    class TrSimData : public FrameCPP::FrSimData
    {
    public:
        TrSimData( const FrameCPP::FrSimData& Source )
            : FrameCPP::FrSimData( Source )
        {
        }

        void
        SetName( const std::string& Name )
        {
            name = Name;
        }

    private:
        FrameCPP::cmn_streamsize_type
        Bytes( const FrameCPP::Common::StreamBase& Stream ) const
        {
            return 0;
        }

        virtual self_type*
        Create( istream_type& Stream ) const
        {
            return ( (self_type*)NULL );
        }

        virtual void
        Write( ostream_type& Stream ) const
        {
        }
    };

    template < class TrData, class TContainer >
    void
    ReplaceChannelNames( TContainer&        List,
                         const Regex&       Pattern,
                         const std::string& Format )
    {
        //-------------------------------------------------------------
        // Loop over elements
        //-------------------------------------------------------------
        std::string new_channel_name;
        TContainer  dest;

        for ( typename TContainer::iterator cur = List.begin( ),
                                            last = List.end( );
              cur != last;
              ++cur )
        {
            new_channel_name =
                RegexReplace( ( *cur )->GetName( ), Pattern, Format );
            TrData c( *( *cur ) );
            c.SetName( new_channel_name );
            //-----------------------------------------------------------------
            // Loop over data vector elements changing their names also
            //-----------------------------------------------------------------
            for ( typename TrData::data_type::iterator
                      dcur = c.RefData( ).begin( ),
                      dlast = c.RefData( ).end( );
                  dcur != dlast;
                  ++dcur )
            {
                ( *dcur )->SetName(
                    RegexReplace( ( *dcur )->GetName( ), Pattern, Format ) );
            }
            //-----------------------------------------------------------------
            // Push onto the list the transformed channel
            //-----------------------------------------------------------------
            dest.push_back( typename TContainer::value_type(
                new typename TContainer::value_type::element_type( c ) ) );
        }
        //-------------------------------------------------------------------
        // Make the transformed channels the default values
        //-------------------------------------------------------------------
        List.swap( dest );
    }

    inline std::string
    RegexReplace( const std::string& Source,
                  const Regex&       Pattern,
                  const std::string& Format )
    {
        std::string result( Source );

        size_t pcount = std::count( Pattern.getRegex( ).begin( ),
                                    Pattern.getRegex( ).end( ),
                                    '(' ) +
            1;
        RegexMatch match( pcount );
        if ( match.match( Pattern, Source.c_str( ) ) )
        {
            //-----------------------------------------------------------------
            // Create temporary which can be modified
            //-----------------------------------------------------------------
            std::string replacement( Format );
            //-----------------------------------------------------------------
            // Locate the dollar sign symbols
            //-----------------------------------------------------------------
            std::list< param_type > d;
            size_t                  pos( 0 );

            for ( std::string::const_iterator cur = Format.begin( ),
                                              last = Format.end( );
                  cur != last; )
            {
                if ( *cur == '$' )
                {
                    param_type p;
                    size_t     l = 0;

                    p.offset = pos;
                    ++cur;
                    ++pos;
                    while ( ::isdigit( *cur ) )
                    {
                        ++l;
                        ++cur;
                        ++pos;
                    }
                    p.len = l + 1;
                    d.push_back( p );
                    std::istringstream( Format.substr( p.offset + 1, l ) ) >>
                        p.num;
                }
                else
                {
                    ++cur;
                    ++pos;
                }
            }
            //-----------------------------------------------------------------
            // Substitute the parameters in the format string with the
            //    patterns found in the source string
            //-----------------------------------------------------------------
            for ( std::list< param_type >::reverse_iterator cur = d.rbegin( ),
                                                            last = d.rend( );
                  cur != last;
                  ++cur )
            {
                replacement.replace(
                    cur->offset, cur->len, match.getSubString( cur->num ) );
            }
            //-----------------------------------------------------------------
            // Replace the characters matched in the Source with the expanded
            //    Format string
            //-----------------------------------------------------------------
            result.replace( match.getSubStartOffset( 0 ),
                            match.getSubLength( 0 ),
                            replacement );
        }

        return result;
    }

} // 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( ) << 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 void
    DirectoryInput( const std::string& Value )
    {
        directory_input = Value;
    }

    inline const std::string&
    DirectoryInput( ) const
    {
        return directory_input;
    }

    inline void
    DirectoryOutput( const std::string& Value )
    {
        directory_output = Value;
    }

    inline const std::string&
    DirectoryOutput( ) const
    {
        return directory_output;
    }

    inline const Regex&
    FilenamePattern( ) const
    {
        return pattern_filename.pattern;
    }

    inline const std::string&
    FilenameFormat( ) const
    {
        return pattern_filename.format;
    }

    inline void
    FilenamePattern( const std::string& Value )
    {
        if ( Value.size( ) >= 3 )
        {
            const char sep = Value[ 0 ];
            if ( Value[ Value.size( ) - 1 ] != sep )
            {
                throw std::runtime_error( "Invalid pattern" );
            }
            size_t offset = Value.find( sep, 1 );
            if ( offset == ( Value.size( ) - 1 ) )
            {
                throw std::runtime_error( "Invalid pattern" );
            }
            std::string pat = Value.substr( 1, offset - 1 );
            std::string fmt =
                Value.substr( offset + 1, Value.size( ) - offset - 2 );
            pattern_filename.Pattern( Regex( pat ) );
            pattern_filename.format = fmt;
        }
    }

    inline const std::string&
    Format( ) const
    {
        return pattern.format;
    }

    inline const Regex&
    Pattern( ) const
    {
        return pattern.pattern;
    }

    inline void
    Pattern( const std::string& Value )
    {
        if ( Value.size( ) >= 3 )
        {
            const char sep = Value[ 0 ];
            if ( Value[ Value.size( ) - 1 ] != sep )
            {
                throw std::runtime_error( "Invalid pattern" );
            }
            size_t offset = Value.find( sep, 1 );
            if ( offset == ( Value.size( ) - 1 ) )
            {
                throw std::runtime_error( "Invalid pattern" );
            }
            std::string pat = Value.substr( 1, offset - 1 );
            std::string fmt =
                Value.substr( offset + 1, Value.size( ) - offset - 2 );
            pattern.Pattern( Regex( pat ) );
            pattern.format = fmt;
        }
    }

#if 0
  inline size_t
  BlockSize( )
  {
    return m_block_size;
  }

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

  inline bool
  UseMemoryMappedIO( )
  {
    return m_use_memory_mapped_io;
  }

  inline void
  UseMemoryMappedIO( bool Value )
  {
    m_use_memory_mapped_io = Value;
  }
#endif /* 0 */

    inline size_t
    Verbose( )
    {
        return verbose;
    }

    inline void
    VerboseInc( )
    {
        ++verbose;
    }

private:
    enum option_types
    {
        OPT_HELP,
        OPT_DIRECTORY_IN,
        OPT_DIRECTORY_OUT,
        OPT_PATTERN,
        OPT_PATTERN_FILENAME,
        OPT_VERBOSE
    };

    struct regex_sub
    {
        Regex       pattern;
        std::string format;
        size_t      pcount;

        inline void
        Pattern( const Regex& Value )
        {
            pattern = Value;
            //-----------------------------------------------------------------
            // This is sloppy but should provide a good upper bound
            //-----------------------------------------------------------------
            pcount = std::count( Value.getRegex( ).begin( ),
                                 Value.getRegex( ).end( ),
                                 '(' ) +
                1;
        }
    };

    OptionSet   options;
    std::string directory_input;
    std::string directory_output;
    regex_sub   pattern;
    regex_sub   pattern_filename;
    int         verbose;
};

CommandLine::CommandLine( int ArgC, char** ArgV )
    : CommandLineOptions( ArgC, ArgV ), verbose( 0 )
{
    //---------------------------------------------------------------------
    // Setup the options that will be recognized.
    //---------------------------------------------------------------------
    std::ostringstream desc;

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

    options.Summary( "Transform channel names of FrAdcData, FrProcData, and "
                     "FrSimData structures"
                     " and corrisponding data vectors" );

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

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specify the directory for the input frames.";
    options.Add( Option( OPT_DIRECTORY_IN,
                         "directory-input",
                         Option::ARG_REQUIRED,
                         desc.str( ),
                         "<directory>" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specify the directory for the output frames.";
    options.Add( Option( OPT_DIRECTORY_OUT,
                         "directory-output",
                         Option::ARG_REQUIRED,
                         desc.str( ),
                         "<directory>" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Specify the transformation rule which may include positional"
            " pattern substitution. "
            "The first character in the string is used as the separator"
            " and cannot occur in either the <pattern? or <format> specifiers."
            "The <pattern> string may contain paired parentheses to specify"
            " groupings. "
            "The <format> string may include $<num> pattern to reference"
            " a matched grouping from the <pattern> string. "
            "Groupings start with 1 (one) as group 0 (zero) refernces the"
            " fully matched pattern.";
    options.Add( Option( OPT_PATTERN,
                         "pattern",
                         Option::ARG_REQUIRED,
                         desc.str( ),
                         "<sep><pattern><sep><format><sep>" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc
        << "Specify the transformation rule for filenames. "
           "The transformation rules are the same as for the --pattern option.";
    options.Add( Option( OPT_PATTERN_FILENAME,
                         "pattern-filename",
                         Option::ARG_REQUIRED,
                         desc.str( ),
                         "<sep><pattern><sep><format><sep>" ) );

    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    desc.str( "" );
    desc << "Display file name of each file processed."
            " If it appears multiple times on the command line,"
            " the level of verbosity will increase."
            " (Default: "
         << ( ( Verbose( ) ) ? "verbose" : "silent" ) << " )";
    options.Add(
        Option( OPT_VERBOSE, "verbose", Option::ARG_NONE, desc.str( ) ) );
    //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

    //---------------------------------------------------------------------
    // 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( options, arg_name, arg_value ) );

            switch ( cmd_id )
            {
            case OPT_HELP:
            {
                Usage( 0 );
            }
            break;
            case OPT_DIRECTORY_IN:
                DirectoryInput( arg_value );
                break;
            case OPT_DIRECTORY_OUT:
                DirectoryOutput( arg_value );
                break;
            case OPT_PATTERN:
                Pattern( arg_value );
                break;
            case OPT_PATTERN_FILENAME:
                FilenamePattern( arg_value );
                break;
            case OPT_VERBOSE:
                VerboseInc( );
                break;
            case CommandLineOptions::OPT_END_OF_OPTIONS:
                parsing = false;
                break;
            }
        }
    }
    catch ( ... )
    {
    }
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
int
main( int ArgC, char** ArgV ) try
{
    typedef std::list< std::string > filename_container;

    INT_4U              exit_status = 0;
    MemChecker::Trigger gc_trigger( true );
    CommandLine         cl( ArgC, ArgV );
    filename_container  files;

    FrameCPP::Initialize( );

    //---------------------------------------------------------------------
    //
    //---------------------------------------------------------------------
    if ( cl.empty( ) && cl.DirectoryInput( ).size( ) > 0 )
    {
        //-------------------------------------------------------------------
        // Generate a list of inputfiles by reading the directory
        //-------------------------------------------------------------------
        LDASTools::AL::Directory d( cl.DirectoryInput( ) );
        std::string              fullpath( cl.DirectoryInput( ) );

        fullpath += "/";
        size_t fullpath_offset = fullpath.size( );

        while ( d.Next( ) )
        {
            struct stat file_info;

            if ( cl.Verbose( ) >= 3 )
            {
                std::cout << "DEBUG:"
                          << " Getting file information concearning: "
                          << d.EntryName( ) << std::endl;
            }
            fullpath.replace(
                fullpath_offset, std::string::npos, d.EntryName( ) );
            int err = ::stat( fullpath.c_str( ), &file_info );
            if ( cl.Verbose( ) >= 3 )
            {
                std::cout << "DEBUG:"
                          << " err: " << err
                          << " S_ISREG: " << S_ISREG( file_info.st_mode )
                          << " file: " << d.EntryName( ) << std::endl;
            }
            if ( ( err == 0 ) && ( S_ISREG( file_info.st_mode ) ) )
            {
                files.push_back( d.EntryName( ) );
            }
        }
    }
    else if ( !cl.empty( ) )
    {
        //-------------------------------------------------------------------
        // Generate a list of inputfiles by reading the rest of the options
        //   passed
        //-------------------------------------------------------------------
        while ( cl.empty( ) == false )
        {
            files.push_back( cl.Pop( ) );
        }
    }

    //---------------------------------------------------------------------
    //---------------------------------------------------------------------
    if ( cl.BadOption( ) )
    {
        cl.Usage( USAGE );
    }

    std::string fullpath( "." );
    std::string fulloutputpath( "." );

    if ( cl.DirectoryInput( ).size( ) > 0 )
    {
        fullpath = cl.DirectoryInput( );
    }
    fullpath += "/";
    size_t fullpath_offset = fullpath.size( );

    if ( cl.DirectoryOutput( ).size( ) > 0 )
    {
        fulloutputpath = cl.DirectoryOutput( );
    }
    fulloutputpath += "/";
    size_t fulloutputpath_offset = fulloutputpath.size( );

    if ( cl.Verbose( ) >= 2 )
    {
        std::cout << "DEBUG:"
                  << " fullpath: " << fullpath
                  << " fulloutputpath: " << fulloutputpath
                  << " file count: " << files.size( ) << std::endl;
    }
    for ( filename_container::const_iterator filename = files.begin( ),
                                             last = files.end( );
          filename != last;
          ++filename )
    {
        INT_4U new_exit_status = 0;
        try
        {
            //-----------------------------------------------------------------
            // Generate the qualified path to the input && output file
            //-----------------------------------------------------------------
            fullpath.replace( fullpath_offset, std::string::npos, *filename );
            fulloutputpath.replace( fulloutputpath_offset,
                                    std::string::npos,
                                    RegexReplace( *filename,
                                                  cl.FilenamePattern( ),
                                                  cl.FilenameFormat( ) ) );
            if ( cl.Verbose( ) >= 3 )
            {
                std::cout << "DEBUG:"
                          << " fullpath: " << fullpath
                          << " fulloutputpath: " << fulloutputpath << std::endl;
            }
            //-----------------------------------------------------------------
            // Open the stream
            //-----------------------------------------------------------------
            FrameCPP::IFrameFStream               stream( fullpath.c_str( ) );
            FrameCPP::IFrameFStream::frame_h_type frameh;
            FrameCPP::OFrameFStream ostream( fulloutputpath.c_str( ) );

            while ( true )
            {
                try
                {
                    frameh = stream.ReadNextFrame( );
                }
                catch ( const std::out_of_range& Exception )
                {
                    //-------------------------------------------------------------
                    // Reached the end frames within the file
                    //-------------------------------------------------------------
                    break;
                }
                FrameCPP::FrameH::rawData_type raw( frameh->GetRawData( ) );
                std::string                    new_channel_name;

                if ( raw )
                {
                    //-------------------------------------------------------------
                    // Loop over FrAdcData elements
                    //-------------------------------------------------------------
                    ReplaceChannelNames< TrAdcData >(
                        raw->RefFirstAdc( ), cl.Pattern( ), cl.Format( ) );
                }
                //---------------------------------------------------------------
                // Loop over FrProcData elements
                //---------------------------------------------------------------
                ReplaceChannelNames< TrProcData >(
                    frameh->RefProcData( ), cl.Pattern( ), cl.Format( ) );
                //---------------------------------------------------------------
                // Loop over FrSimData elements
                //---------------------------------------------------------------
                ReplaceChannelNames< TrSimData >(
                    frameh->RefSimData( ), cl.Pattern( ), cl.Format( ) );
                //---------------------------------------------------------------
                // Write the frame to the output stream
                //---------------------------------------------------------------
                ostream.WriteFrame( frameh );
            }
            //-----------------------------------------------------------------
            // Report the files since things are winding down
            //-----------------------------------------------------------------
            if ( cl.Verbose( ) )
            {
                std::cout << fullpath << " ==> " << fulloutputpath << std::endl;
            }
        }
        catch ( const std::exception& Exception )
        {
            if ( cl.Verbose( ) )
            {
                std::cout << "ABORT: Caught exception: " << Exception.what( )
                          << std::endl;
            }
            new_exit_status = UNHANDLED_EXCEPTION;
            continue;
        }
        catch ( ... )
        {
            if ( cl.Verbose( ) )
            {
                std::cout << "ABORT: Caught unknown exception: " << std::endl;
            }
            new_exit_status = UNHANDLED_EXCEPTION;
            continue;
        }

        //------------------------------------------------------------------
        // Check to see if processing should continue
        //------------------------------------------------------------------
        if ( new_exit_status != 0 )
        {
            //----------------------------------------------------------------
            // 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::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 );
}
