//
// 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 <cctype>
#include <cstdlib>
#include <cstring>

#include <string>

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

namespace
{
    typedef LDASTools::AL::CommandLineOptions::OptionSet OptionSet;

    class doxygen_option_set : public OptionSet
    {
    public:
        doxygen_option_set( const std::string& ProgramName,
                            const OptionSet&   Options );

        std::ostream& Arguments( std::ostream& Stream );
        std::ostream& Header( std::ostream& Stream );
        std::ostream& Footer( std::ostream& Stream );
        std::ostream& Usage( std::ostream& Stream );

        void Write( std::ostream& Stream );

    private:
        int         column;
        std::string program_name;

        void               column_dec( );
        void               column_inc( );
        std::string        indent( ) const;
        static std::string cononical_program_name( const std::string& Source );
        std::string        pagename( ) const;
        std::string        synopsis( ) const;
    };

    doxygen_option_set::doxygen_option_set( const std::string& ProgramName,
                                            const OptionSet&   Options )
        : OptionSet( Options ), column( 0 ),
          program_name( cononical_program_name( ProgramName ) )

    {
    }

    std::ostream&
    doxygen_option_set::Arguments( std::ostream& Stream )
    {
        if ( true )
        {
            Stream << indent( ) << "<TABLE>" << std::endl;
            column_inc( );
            Stream << indent( ) << "<TR>" << std::endl;
            column_inc( );
            Stream << indent( ) << "<TH><CENTER>Option</CENTER></TH>"
                   << std::endl;
            Stream << indent( ) << "<TH><CENTER>Description</CENTER></TH>"
                   << std::endl;
            column_dec( );
            Stream << indent( ) << "</TR>" << std::endl;
            for ( option_container_type::const_iterator
                      cur = m_options.begin( ),
                      last = m_options.end( );
                  cur != last;
                  ++cur )
            {
                Stream << indent( ) << "<TR>" << std::endl;
                column_inc( );
                Stream << indent( ) << "<TD>" << cur->Prefix( )
                       << cur->ArgumentName( ) << "</TD>" << std::endl;
                Stream << indent( ) << "<TD>" << std::endl;
                column_inc( );
                Stream << indent( ) << cur->Description( ) << std::endl;
                column_dec( );
                Stream << indent( ) << "</TD>" << std::endl;
                column_dec( );
                Stream << indent( ) << "</TR>" << std::endl;
            }
            column_dec( );
            Stream << indent( ) << "</TABLE>" << std::endl;
        }
        return Stream;
    }

    std::ostream&
    doxygen_option_set::Header( std::ostream& Stream )
    {
        Stream << indent( ) << "/**" << std::endl;
        column_inc( );
        Stream << indent( ) << "\\page " << pagename( ) << " " << program_name
               << std::endl;

        return Stream;
    }

    std::ostream&
    doxygen_option_set::Footer( std::ostream& Stream )
    {
        column_dec( );
        Stream << indent( ) << "*/" << std::endl;
        return Stream;
    }

    std::ostream&
    doxygen_option_set::Usage( std::ostream& Stream )
    {
        Stream << indent( ) << "<p>" << std::endl
               << indent( ) << "<b>Usage:</b> " << program_name << " "
               << synopsis( ) << std::endl;
        Stream << indent( ) << "<p>" << std::endl;
        column_inc( );
        Stream << indent( ) << m_summary << std::endl;
        Stream << std::endl;
        column_dec( );
        return Stream;
    }

    void
    doxygen_option_set::Write( std::ostream& Stream )
    {
        Header( Stream );
        Usage( Stream );
        Arguments( Stream );
        Footer( Stream );
    }

    std::string
    doxygen_option_set::pagename( ) const
    {
        bool        cap = true;
        std::string retval = "Command";
        for ( std::string::const_iterator cur = program_name.begin( ),
                                          last = program_name.end( );
              cur != last;
              ++cur )
        {
            if ( std::isalnum( *cur ) == false )
            {
                cap = true;
                continue;
            }
            if ( cap )
            {
                retval += toupper( *cur );
                cap = false;
            }
            else
            {
                retval += *cur;
            }
        }
        return retval;
    }

    inline void
    doxygen_option_set::column_dec( )
    {
        column -= 2;
    }

    inline void
    doxygen_option_set::column_inc( )
    {
        column += 2;
    }

    std::string
    doxygen_option_set::cononical_program_name( const std::string& Source )
    {
        std::string            retval( Source );
        std::string::size_type x;
        if ( ( x = retval.find_last_of( "/" ) ) != std::string::npos )
        {
            retval.erase( 0, x + 1 );
        }
        if ( retval.compare( 0, 3, "lt-" ) == 0 )
        {
            retval.erase( 0, 3 );
        }
        return retval;
    }

    std::string
    doxygen_option_set::indent( ) const
    {
        static const std::string spaces( 256, ' ' );

        return spaces.substr( 0, column );
    }

    std::string
    doxygen_option_set::synopsis( ) const
    {
        std::string                         retval( m_synopsis );
        std::string::size_type              pos = 0;
        std::string::value_type             c( '<' );
        std::list< std::string::size_type > positions;
        bool                                v = true;

        for ( std::string::const_iterator cur = retval.begin( ),
                                          last = retval.end( );
              cur != last;
              ++cur, ++pos )
        {
            if ( *cur == c )
            {
                positions.push_back( pos );
                if ( c == '<' )
                {
                    c = '>';
                }
                else
                {
                    c = '<';
                }
            }
        }
        for ( std::list< std::string::size_type >::const_reverse_iterator
                  cur = positions.rbegin( ),
                  last = positions.rend( );
              cur != last;
              ++cur, v = !v )
        {
            if ( v )
            {
                retval.replace( *cur, 1, "</em>" );
            }
            else
            {
                retval.replace( *cur, 1, "<em>" );
            }
        }

        return retval;
    }
} // namespace

namespace LDASTools
{
    namespace AL
    {
        typedef CommandLineOptions::Option    Option;
        typedef CommandLineOptions::OptionSet OptionSet;
        typedef CommandLineOptions::ostream   ostream;

        //=====================================================================
        // CommandLineOptions::Validate
        //=====================================================================

        CommandLineOptions::Validator::~Validator( )
        {
        }

        bool
        CommandLineOptions::Validator::operator( )( const std::string& Arg )
        {
            return true;
        }

        //=====================================================================
        // CommandLineOptions::ValidateBool
        //=====================================================================

        bool
        CommandLineOptions::ValidateBool::operator( )( const std::string& Arg )
        {
            bool retval = true;

            try
            {
                InterpretBoolean( Arg );
            }
            catch ( ... )
            {
                retval = false;
            }

            return retval;
        }

        //=====================================================================
        // CommandLineOptions::Option
        //=====================================================================
        const char* CommandLineOptions::Option::m_prefix( "--" );

        CommandLineOptions::Option::Option( int                ArgumentId,
                                            const std::string& ArgumentName,
                                            arg_type           ArgType,
                                            const std::string& Usage,
                                            const std::string& ArgumentArg,
                                            Validator*         V )
            : m_id( ArgumentId ), m_name( ArgumentName ), m_arg_type( ArgType ),
              m_usage( Usage ), m_argument( ArgumentArg ), validator( V )
        {
        }

        CommandLineOptions::Option::Option( const Option& Option )
            : m_id( Option.m_id ), m_name( Option.m_name ),
              m_arg_type( Option.m_arg_type ), m_usage( Option.m_usage ),
              m_argument( Option.m_argument ), validator( Option.validator )
        {
        }

        //=====================================================================
        // CommandLineOptions::OptionSet
        //=====================================================================

        void
        CommandLineOptions::OptionSet::Add( const Option& Source )
        {
            m_options.push_front( Source );
            m_name_mapping[ Source.ArgumentName( ) ] = m_options.begin( );
            m_key_mapping[ Source.ArgumentId( ) ] = m_options.begin( );
        }

        void
        CommandLineOptions::OptionSet::Add( const OptionSet& SubCommand )
        {
            m_subcommands.push_back( &SubCommand );
        }

        const CommandLineOptions::Option& OptionSet::
                                          operator[]( const std::string& Name ) const
        {
            name_container_type::const_iterator pos =
                m_name_mapping.find( Name );

            if ( pos != m_name_mapping.end( ) )
            {
                return *( pos->second );
            }
            std::ostringstream msg;

            msg << "Unknown option: " << Name;
            throw std::range_error( msg.str( ) );
        }

        const CommandLineOptions::Option& OptionSet::operator[]( int Key ) const
        {
            key_container_type::const_iterator pos = m_key_mapping.find( Key );

            if ( pos != m_key_mapping.end( ) )
            {
                return *( pos->second );
            }
            std::ostringstream msg;

            msg << "Unknown option: " << Key;
            throw std::range_error( msg.str( ) );
        }

        ostream&
        CommandLineOptions::OptionSet::Write( ostream& Stream,
                                              int      Indent ) const
        {
            std::string pad( Indent, ' ' );

            if ( m_synopsis.empty( ) )
            {
                for ( option_container_type::const_reverse_iterator
                          cur = m_options.rbegin( ),
                          last = m_options.rend( );
                      cur != last;
                      ++cur )
                {
                    Stream << " " << cur->Prefix( ) << cur->ArgumentName( );
                }
            }
            else
            {
                Stream << " " << m_synopsis;
            }
            if ( m_summary.empty( ) == false )
            {
                if ( SummaryOnly( ) == false )
                {
                    Stream << std::endl;
                }
                hanging_par( Stream, Indent, m_summary );
            }
            if ( m_options.empty( ) == false )
            {
                Stream << std::endl;
                for ( option_container_type::const_reverse_iterator
                          cur = m_options.rbegin( ),
                          last = m_options.rend( );
                      cur != last;
                      ++cur )
                {
                    Stream << std::endl
                           << pad << " " << cur->Prefix( )
                           << cur->ArgumentName( );
                    if ( cur->ArgumentArg( ).empty( ) == false )
                    {
                        if ( cur->ArgumentType( ) == Option::ARG_REQUIRED )
                        {
                            Stream << " " << cur->ArgumentArg( );
                        }
                        else if ( cur->ArgumentType( ) == Option::ARG_OPTIONAL )
                        {
                            Stream << " [" << cur->ArgumentArg( ) << "]";
                        }
                    }
                    hanging_par( Stream, ( Indent + 4 ), cur->Description( ) );
                }
            }
            if ( m_subcommands.empty( ) == false )
            {
                //-----------------------------------------------------------------
                // Print help information about subcommands
                //-----------------------------------------------------------------
                for ( subcommand_container_type::const_iterator
                          cur = m_subcommands.begin( ),
                          last = m_subcommands.end( );
                      cur != last;
                      ++cur )
                {
                    if ( ( *cur )->SummaryOnly( ) == false )
                    {
                        Stream << std::endl << std::endl;
                    }
                    ( *cur )->Write( Stream, Indent + 4 );
                }
            }
            return Stream;
        }

        //---------------------------------------------------------------------
        /// This method formats a string of text appropriately for a fixed
        ///  width display.
        //---------------------------------------------------------------------
        void
        CommandLineOptions::OptionSet::hanging_par(
            ostream& Stream, size_t Indent, const std::string& Paragraph )
        {
            //-------------------------------------------------------------------
            /// \note
            ///     The current assumption is a screen width of 72 characters.
            //-------------------------------------------------------------------
            static const size_t max_width = 72;

            const size_t      width( max_width - Indent );
            const std::string pad( Indent, ' ' );

            size_t pos = 0;
            size_t pos_search_start = width;
            size_t ws_pos;

            do
            {
                if ( ( Paragraph.length( ) - pos ) > width )
                {
                    ws_pos = Paragraph.find_last_of( " \t", pos_search_start );
                }
                else
                {
                    ws_pos = std::string::npos;
                }
                Stream << std::endl
                       << pad
                       << Paragraph.substr( pos,
                                            ( ws_pos == std::string::npos )
                                                ? ws_pos
                                                : ( ws_pos - pos ) );
                pos = ws_pos + 1;
                pos_search_start = pos + width;
            } while ( ws_pos != std::string::npos );
        }

        //====================================================================
        // CommandLineOptions
        //====================================================================
        CommandLineOptions::CommandLineOptions( argc_type ArgC, argv_type ArgV )
            : m_program_name( ArgV[ 0 ] )
        {
            if ( ArgC > 1 )
            {
                //----------------------------------------------------------------
                // Seed container of unparsed options
                //----------------------------------------------------------------
                argv_type pos = &ArgV[ 1 ];
                while ( --ArgC > 0 )
                {
                    push_back( *pos );
                    pos++;
                }
            }
        }

        CommandLineOptions::CommandLineOptions( argc_type ArgC, char** ArgV )
            : m_program_name( ArgV[ 0 ] )
        {
            if ( ArgC > 1 )
            {
                //----------------------------------------------------------------
                // Seed container of unparsed options
                //----------------------------------------------------------------
                char** pos = &ArgV[ 1 ];
                while ( --ArgC > 0 )
                {
                    push_back( *pos );
                    pos++;
                }
            }
        }

        bool
        CommandLineOptions::InterpretBoolean( const std::string& Value )
        {
            static Regex exp_true( "^true|TRUE|yes|YES|1$" );
            static Regex exp_false( "^false|FALSE|no|NO|0$" );

            RegexMatch is( 1 );

            if ( is.match( exp_true, Value.c_str( ) ) )
            {
                return true;
            }
            else if ( is.match( exp_false, Value.c_str( ) ) )
            {
                return false;
            }
            throw std::range_error( "Value is neither 'true' nor 'false'" );
        }

        int
        CommandLineOptions::Parse( const OptionSet& Options,
                                   std::string&     ArgumentName,
                                   std::string&     ArgumentValue )
        {
            if ( empty( ) )
            {
                return OPT_END_OF_OPTIONS;
            }
            //-------------------------------------------------------------------
            // Look at the front of the list
            //-------------------------------------------------------------------
            ArgumentName = "";
            ArgumentValue = "";

            if ( front( ).length( ) >= 3 )
            {
                size_t prefix_len( strlen( Option::Prefix( ) ) );

                if ( front( ).compare( 0, prefix_len, Option::Prefix( ) ) == 0 )
                {
                    size_t e = front( ).find_first_of( "=" );
                    size_t vpos = e;

                    if ( e != std::string::npos )
                    {
                        e -= 2;
                    }

                    ArgumentName = front( ).substr( prefix_len, e );

                    if ( ArgumentName.compare( "doxygen-help" ) == 0 )
                    {
                        //-----------------------------------------------------------
                        // Handle internally
                        //-----------------------------------------------------------
                        doxygen_option_set dos( m_program_name, Options );

                        dos.Write( std::cout );
                        exit( 0 );
                    }
                    //---------------------------------------------------------------
                    // Lookup the argument
                    //---------------------------------------------------------------
                    const Option& opt_info( Options[ ArgumentName ] );

                    switch ( opt_info.ArgumentType( ) )
                    {
                    case Option::ARG_NONE:
                        break;
                    case Option::ARG_REQUIRED:
                        if ( vpos == std::string::npos )
                        {
                            erase( begin( ) );
                            if ( ( size( ) <= 0 ) ||
                                 ( front( ).compare(
                                       0, prefix_len, Option::Prefix( ) ) ==
                                   0 ) ||
                                 ( opt_info.ValidateArg( front( ) ) == false ) )
                            {
                                std::ostringstream msg;

                                msg << "Missing a required option for "
                                    << ArgumentName;
                                throw std::runtime_error( msg.str( ) );
                            }
                            ArgumentValue = front( );
                        }
                        else
                        {
                            ArgumentValue = front( ).substr( ++vpos );
                        }
                        break;
                    case Option::ARG_OPTIONAL:
                        if ( vpos == std::string::npos )
                        {
                            std::string save = front( );
                            erase( begin( ) );
                            if ( empty( ) )
                            {
                                push_front( save );
                            }
                            else
                            {
                                if ( ( front( ).compare(
                                           0, prefix_len, Option::Prefix( ) ) !=
                                       0 ) &&
                                     ( opt_info.ValidateArg( front( ) ) ) )
                                {
                                    ArgumentValue = front( );
                                }
                                else
                                {
                                    push_front( save );
                                }
                            }
                        }
                        else
                        {
                            ArgumentValue = front( ).substr( ++vpos );
                        }
                        break;
                    default:
                    {
                        std::ostringstream msg;

                        msg << "CommandLineOptions::Parse:"
                            << " The value of " << opt_info.ArgumentType( )
                            << " is currently not understood";
                        throw std::invalid_argument( msg.str( ) );
                    }
                    break;
                    }
                    //---------------------------------------------------------------
                    // Removed the option just processed
                    //---------------------------------------------------------------
                    erase( begin( ) );
                    //---------------------------------------------------------------
                    // Return the arguemnt id
                    //---------------------------------------------------------------
                    return opt_info.ArgumentId( );
                }
            }

            std::ostringstream msg;

            msg << "Unknown option: " << front( );

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

    } // namespace AL
} // namespace LDASTools
