//
// LDASTools AL - A library collection to provide an abstraction layer
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools AL 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 AL 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 "ldastoolsal/Sed.hh"

#include <cstring>
#include <sstream>
#include <stdexcept>

#include <boost/shared_ptr.hpp>

#include "ldastoolsal/regex.hh"
#include "ldastoolsal/regexmatch.hh"

namespace
{
    class command
    {
    public:
        virtual ~command( )
        {
        }

        inline command( const std::string& Expression )
            : expression( Expression )
        {
        }

        virtual void operator( )( std::istream& Input,
                                  std::ostream& Output ) const = 0;

    protected:
        std::string expression;
    };

    class substitution : public command
    {
    public:
        virtual ~substitution( )
        {
        }

        inline substitution( const std::string& Expression )
            : command( Expression ), global( false ), ignore_case( false )
        {
            size_t start; // Start of substring
            size_t stop; // Stop of substring

            //-----------------------------------------------------------------
            // Extract regexp
            //-----------------------------------------------------------------
            const char sep = expression[ 0 ];
            start = 1;
            stop = expression.find( sep, start );
            if ( stop == std::string::npos )
            {
                throw std::invalid_argument( "Substitution parse error" );
            }
            regexp_str = expression.substr( start, stop - start );
            //-----------------------------------------------------------------
            // Extract replacement
            //-----------------------------------------------------------------
            start = stop + 1;
            stop = expression.find( sep, start );
            if ( stop == std::string::npos )
            {
                throw std::invalid_argument( "Substitution parse error" );
            }
            replacement_str = expression.substr( start, stop - start );
            //-----------------------------------------------------------------
            // Looking if modifiers have been supplied.
            //-----------------------------------------------------------------
            for ( std::string::const_iterator
                      cur = expression.begin( ) + stop + 1,
                      last = expression.end( );
                  cur != last;
                  ++cur )
            {
                switch ( *cur )
                {
                case 'i':
                    ignore_case = true;
                    break;
                case 'g':
                    global = true;
                    break;
                }
            }
            //-----------------------------------------------------------------
            // Build the regular expression parser
            //-----------------------------------------------------------------
            int                flags = REG_EXTENDED;
            std::ostringstream pattern;

            flags |= ( ( ignore_case ) ? REG_ICASE : 0 );
            pattern << "(" << regexp_str << ")";

            regexp = Regex( pattern.str( ).c_str( ), flags );
        }

        virtual void
        operator( )( std::istream& Input, std::ostream& Output ) const
        {
            char       buffer[ 1024 * 2 ];
            RegexMatch sub( 3 );
            bool       add_eol;

            while ( true )
            {
                Input.getline( buffer, sizeof( buffer ) );
                add_eol =
                    ( ( ( size_t )( Input.gcount( ) ) == ::strlen( buffer ) )
                          ? false
                          : true );

                if ( buffer[ 0 ] != '\0' )
                {
                    std::string d( buffer );

                    while ( true )
                    {
                        if ( sub.match( regexp, d.c_str( ) ) )
                        {
                            //---------------------------------------------------------
                            // Found matching text ..
                            // - append new directory with leading text
                            // - replace the matching pattern with new text
                            //---------------------------------------------------------
                            Output << d.substr( 0, sub.getSubStartOffset( 1 ) )
                                   << replacement_str;
                            //---------------------------------------------------------
                            // Prepare for the next search by placing trailing
                            // text into the buffer holding the string to be
                            // matched.
                            //---------------------------------------------------------
                            d = d.substr( sub.getSubStartOffset( 1 ) +
                                          sub.getSubLength( 1 ) );
                            //---------------------------------------------------------
                            // Check if this is to happen once or for all
                            // instances.
                            //---------------------------------------------------------
                            if ( !global )
                            {
                                //-------------------------------------------------------
                                // Only once so append the remaining text and
                                // leave
                                //-------------------------------------------------------
                                Output << d;
                                break;
                            }
                        }
                        else
                        {
                            //---------------------------------------------------------
                            // Not a match so put everything into the string and
                            // leave
                            //---------------------------------------------------------
                            Output << d;
                            break;
                        }
                    }
                }
                if ( !add_eol )
                {
                    break;
                }
                //---------------------------------------------------------------
                // Put in the end of line that was stripped by getline
                //---------------------------------------------------------------
                Output << std::endl;
            }
        }

    private:
        bool        global;
        bool        ignore_case;
        Regex       regexp;
        std::string regexp_str;
        std::string replacement_str;
    };
} // namespace

namespace LDASTools
{
    namespace Cmd
    {
        class Sed::pd
        {
        public:
            inline pd( )
            {
            }

            inline command*
            action( )
            {
                return actions.get( );
            }

            inline void
            action( command* C )
            {
                actions.reset( C );
            }

        private:
            boost::shared_ptr< command > actions;
        };

        //-------------------------------------------------------------------
        /// The constructor takes Expression and tries to parse it for
        /// correctness.
        //-------------------------------------------------------------------
        Sed::Sed( const std::string& Expression )
        {
            pdata.reset( new pd );

            //-----------------------------------------------------------------
            /// @todo
            /// Handle the address portion of the expression
            //-----------------------------------------------------------------
            //-----------------------------------------------------------------
            // Handle the command portion of the expression
            //-----------------------------------------------------------------
            switch ( Expression[ 0 ] )
            {
            case 's':
                pdata->action( new substitution( Expression.substr( 1 ) ) );
                break;
            }
        }

        //-------------------------------------------------------------------
        //
        //-------------------------------------------------------------------
        std::string
        Sed::operator( )( const std::string& Input ) const
        {
            std::istringstream input( Input );
            std::ostringstream retval;

            operator( )( input, retval );

            return retval.str( );
        }

        //-------------------------------------------------------------------
        //
        //-------------------------------------------------------------------
        void
        Sed::operator( )( std::istream& Input, std::ostream& Output ) const
        {
            ( *( pdata->action( ) ) )( Input, Output );
        }
    } // namespace Cmd
} // namespace LDASTools
