/*!
    @file  SReadline.h
    @brief C++ wrapper around libreadline.
           Supported: editing, history, custom completers, keymaps.
           Attention: implementation is not thread safe!
                      It is mainly because the readline library provides
                      pure C interface and has many calls for an "atomic" 
                      completion operation
 */
//
// Date:      17 December 2005
//            03 April    2006
//            20 April    2006
//            07 May      2006
//
// Copyright (c) Sergey Satskiy 2005 - 2006
//               <sergesatsky@yahoo.com>
//
// Permission to copy, use, modify, sell and distribute this software 
// is granted provided this copyright notice appears in all copies. 
// This software is provided "as is" without express or implied
// warranty, and with no claim as to its suitability for any purpose.
//

#ifndef SREADLINE_H
#define SREADLINE_H

#include <stdio.h>

#include <readline/readline.h>
#include <readline/history.h>
#include <readline/keymaps.h>

#include <string>
#include <fstream>
#include <vector>
#include <stdexcept>
#include <map>

#include <boost/algorithm/string/trim.hpp>
#include <boost/tokenizer.hpp>
#include <boost/function.hpp>


    /*! @brief Used to avoid linking errors in case
               of including into many compilation units
     */
namespace
{
        //! @brief Tokens in a single variation of a user command
    typedef std::vector< std::string >              TokensStorage;
        //! @brief Set of variations of user commands
    typedef std::vector< TokensStorage >            CompletionsStorage;
        //! @brief Pressed key callback. Must return 0 if OK, != 0 otherwise
    typedef boost::function< int ( int, int ) >     KeyCallback;
        //! @brief A set of keys binding
    typedef std::map< int, KeyCallback >            KeysBind;

    
    const size_t                    DefaultHistoryLimit( 64 );  //!< Default value for the history length
    CompletionsStorage              Completions;                //!< Global storage of custom completions
    TokensStorage                   Tokens;                     //!< Tokens storage for a single completion session
    std::map< Keymap, KeysBind >    Keymaps;                    //!< Global storage for keymaps

    bool                            KeymapWasSetup( false );    //!< Has sense if a keymap was
                                                                //!< setup before the first readline call
    Keymap                          Earlykeymap( 0 );           //!< The keymap which was setup before the first readline call


        /*! @brief Custom completion generator
            @param text Pointer to a token to be completed
            @param State 0 for a first call, non 0 for all consequent calls
         */
    char * Generator( const char *  text, int  State );

    
        /*! @brief The function is called before trying to complete a token
            @param text A token to be completed
            @param start Index of the beginning of the token in the readline buffer
            @param end   Index of the end of the token in the readline buffer
         */
    char ** UserCompletion( const char * text,
                            int          start,
                            int          end );


        /*! @brief The function selects the set of bindings and 
                   makes the corresponding call.
            @param Count The parameter is passed by readline
            @param Key   The pressed key
         */
    int  KeyDispatcher( int  Count, int  Key );


        /*! @brief The readline startup hook. It is required to setup the proper keymap.
         */
    int  StartupHook( void );

    
        /*! @brief Compares all the Input tokens with starts tokens in the Pattern
            @param Pattern pattern tokens
            @param Input user input tokens
            @return true if first Input.size() tokens are equal to the pattern tokens
         */
    template < typename  Container >
    bool AreTokensEqual( const Container &  Pattern, 
                         const Container &  Input )
    {
        if ( Input.size() > Pattern.size() ) return false;

        typename Container::const_iterator  k( Pattern.begin() );
        typename Container::const_iterator  j( Input.begin() );
        for ( ; j != Input.end(); ++k, ++j )
        {
            if ( *k == "%file" ) continue;
            if ( *k != *j )      return false;
        }
        return true;
    }


        // See description near the prototype
    template < typename  ContainerType >
    void SplitTokens( const std::string &  Source, ContainerType &  Container )
    {
        typedef boost::tokenizer< boost::char_separator< char > >       TokenizerType;

        boost::char_separator<char>     Separators( " \t\n" );              // Set of token separators
        TokenizerType                   Tokenizer( Source, Separators );    // Tokens provider
        std::string                     SingleToken;                        // Temporary storage for a token
        
        Container.clear();
        for ( TokenizerType::const_iterator  k( Tokenizer.begin() ); k != Tokenizer.end(); ++k )
        {
            SingleToken = *k;
            boost::algorithm::trim( SingleToken );
            Container.push_back( SingleToken );
        }
    }

    
        // See description near the prototype
    char ** UserCompletion( const char *  text, int  start, int  end )
    {
            // No default completion at all
        rl_attempted_completion_over = 1;

        if ( Completions.empty() )  return NULL;

            // Memorize all the previous tokens
        std::string     PreInput( rl_line_buffer, start );
        SplitTokens( PreInput, Tokens );


            // Detect whether we should call the standard file name completer or a custom one
        bool    FoundPretender( false );

        for ( CompletionsStorage::const_iterator  k( Completions.begin() ); k != Completions.end(); ++k )
        {
            if ( ! AreTokensEqual( *k, Tokens ) ) continue;

            if ( (*k).size() > Tokens.size() )
            {
                FoundPretender = true;
                if ( (*k)[ Tokens.size() ] == "%file" )
                {
                        // Standard file name completer - called for the "%file" keyword
                    return rl_completion_matches( text, rl_filename_completion_function );
                }
            }
        }
        
        if ( FoundPretender )
        {
            return rl_completion_matches( text, Generator );
        }
        return NULL;
    }
    
   

        // See description near the prototype
    char * Generator( const char *  text, int  State )
    {
        static int                                  Length;
        static CompletionsStorage::const_iterator   Iterator;

        
        if ( State == 0 )
        {
            Iterator = Completions.begin();
            Length = strlen( text );
        }

        for ( ; Iterator != Completions.end(); ++Iterator )
        {
            if ( ! AreTokensEqual( *Iterator, Tokens ) ) continue;

            if ( (*Iterator).size() > Tokens.size() )
            {
                if ( (*Iterator)[ Tokens.size() ] == "%file" ) continue;

            
                if ( strncmp( text, (*Iterator)[ Tokens.size() ].c_str(), Length ) == 0 )
                {
                        // readline will free the allocated memory
                    char *  NewString( (char*)malloc( strlen( (*Iterator)[ Tokens.size() ].c_str() ) + 1 ) );
                    strcpy( NewString, (*Iterator)[ Tokens.size() ].c_str() );
                    ++Iterator;
                    return NewString;
                }
            }
        }
       
        return NULL;
    }

    
        // See the description near the prototype
    int  KeyDispatcher( int  Count, int  Key )
    {
        std::map< Keymap, KeysBind >::iterator  Set( Keymaps.find( rl_get_keymap() ) );
        if ( Set == Keymaps.end() )
        {
                // Most probably it happens bacause the header was 
                // included into many compilation units and the
                // keymap setting calls were made in different files.
                // This is the problem of "global" data.
                // The storage of all the registered keymaps is in anonymous
                // namespace.
            throw std::runtime_error( "Error selecting a keymap." );
        }

        (Set->second)[ Key ]( Count, Key );
        return 0;
    }


        // See the description near the prototype
    int  StartupHook( void )
    {
        if ( KeymapWasSetup )
        {
            rl_set_keymap( Earlykeymap );
        }
        return 0;
    }

}   // Anonymous namespace




    /*! @brief The wrapper namespace. 
               The namespace is also used for other library elements.
     */
namespace swift 
{

        /*! @brief The readline keymap wrapper.
                   Attention: It is not thread safe!
                   Supports: key binding, key unbinding
         */
    class SKeymap
    {
        private:
            Keymap      keymap;     // Readline keymap
            
        public:
                /*! @brief Creates a new keymap
                    @param PrintableBound if true - the printable characters are bound
                                          if false - the keymap is empty
                 */                  
            explicit SKeymap( bool  PrintableBound = false ) : 
                keymap( NULL )
            {
                if ( PrintableBound )
                {
                    keymap = rl_make_keymap();      // Printable characters are bound
                }
                else
                {
                    keymap = rl_make_bare_keymap(); // Empty keymap
                }

                if ( keymap == NULL )
                {
                    throw std::runtime_error( "Cannot allocate keymap." );
                }

                    // Register a new keymap in the global list
                Keymaps[ keymap ] = KeysBind();
            }
            
                /*! @brief Creates a new keymap which is a copy of Pattern
                    @param Pattern A keymap to be copied
                 */
            explicit SKeymap( Keymap   Pattern ) :
                keymap( rl_copy_keymap( Pattern ) )
            {
                if ( keymap == NULL )
                {
                    throw std::runtime_error( "Cannot allocate keymap." );
                }
                
                    // Register a new keymap in the global list
                Keymaps[ keymap ] = KeysBind();
            }

                /*! @brief Frees the allocated keymap
                 */
            ~SKeymap()
            {
                    // Deregister the keymap
                Keymaps.erase( keymap );
                rl_discard_keymap( keymap );
            }

                /*! @brief Binds the given key to a function
                    @param Key A key to be bound
                    @param Callback A function to be called when the Key is pressed
                 */
            void Bind( int  Key, KeyCallback  Callback )
            {
                Keymaps[ keymap ][ Key ] = Callback;
                if ( rl_bind_key_in_map( Key, KeyDispatcher, keymap ) != 0 )
                {
                        // Remove from the map just bound key
                    Keymaps[ keymap ].erase( Key );
                    throw std::runtime_error( "Invalid key." );
                }
            }


                /*! @brief Unbinds the given key
                    @param Key A key to be unbound
                 */
            void Unbind( int  Key )
            {
                rl_unbind_key_in_map( Key, keymap );
                Keymaps[ keymap ].erase( Key );
            }

            // void Bind( const std::string &  Sequence, boost::function< int ( int, int ) > );
            // void Unbind( std::string &  Sequence );

        public:
                /*! @brief Copy constructor
                    @param rhs Right hand side object of SKeymap
                 */
            SKeymap( const SKeymap &  rhs )
            {
                if ( this == & rhs )
                {
                    return;
                }
                keymap = rl_copy_keymap( rhs.keymap );
            }

                /*! @brief operator=
                    @param rhs Right hand side object of SKeymap
                 */
            SKeymap &  operator=( const SKeymap &  rhs )
            {
                if ( this == & rhs )
                {
                    return *this;
                }
                keymap = rl_copy_keymap( rhs.keymap );
                return *this;
            }
            
            friend class SReadline;
    };
    

        /*! @brief The readline library wrapper.
                   Attention: It is not thread safe!
                   Supports: editing, history, custom completers
         */
    class SReadline
    {
        public:
                /*! @brief Constructs the object, sets the completion function
                    @param Limit History size
                 */
            SReadline( const size_t  Limit = DefaultHistoryLimit ) : 
                HistoryLimit( Limit ),
                HistoryFileName( "" ), 
                OriginalCompletion( rl_attempted_completion_function )
            {
                rl_startup_hook = StartupHook;
                rl_attempted_completion_function = UserCompletion;
                using_history();
            }
            
                /*! @brief Constructs the object, sets the completion function, loads history
                    @param historyFileName File name to load history from
                    @param Limit History size
                 */
            SReadline( const std::string &  historyFileName,
                       const size_t  Limit = DefaultHistoryLimit ) :
                HistoryLimit( Limit ),
                HistoryFileName( historyFileName ),
                OriginalCompletion( rl_attempted_completion_function )
            {
                rl_startup_hook = StartupHook;
                rl_attempted_completion_function = UserCompletion;
                using_history();
                LoadHistory( HistoryFileName );
            }

                /*! @brief Saves the session history (if the file name was provided) 
                           and destroys the object
                 */
            ~SReadline()
            {
                rl_attempted_completion_function = OriginalCompletion;
                SaveHistory( HistoryFileName );
            }

                /*! @brief Gets a single line from a user
                    @param Prompt A printed prompt
                    @return A string which was actually inputed
                 */
            std::string  GetLine( const std::string &  Prompt )
            {
                bool    Unused;
                return GetLine( Prompt, Unused );
            }
            

                /*! @brief Gets a single line from a user
                    @param Prompt A printed prompt
                    @param ReadTokens A user inputed string splitted into tokens. 
                           The container is cleared first
                    @return A string which was actually inputed
                 */
            template < typename  Container >
            std::string  GetLine( const std::string &  Prompt,
                                  Container &          ReadTokens )
            {
                bool    Unused;
                return GetLine( Prompt, ReadTokens, Unused );
            }


                /*! @brief Gets a single line from a user
                    @param Prompt A printed prompt
                    @param BreakOut it is set to true if the EOF found
                    @param ReadTokens A user inputed string splitted into tokens.
                           The container is cleared first
                    @return A string which was actually inputed
                 */
            template < typename  Container >
            std::string  GetLine( const std::string &  Prompt,
                                  Container &          ReadTokens,
                                  bool &               BreakOut )
            {
                std::string     Input( GetLine( Prompt, BreakOut ) );
                SplitTokens( Input, ReadTokens );
                return Input;
            }


                /*! @brief Gets a single line from a user
                    @param Prompt A printed prompt
                    @param BreakOut it is set to true if the EOF found
                    @return A string which was actually inputed
                 */
            std::string  GetLine( const std::string &  Prompt, 
                                  bool &               BreakOut )
            {
                BreakOut = true;

                char *  ReadLine( readline( Prompt.c_str() ) );
                if ( ReadLine == NULL ) return std::string();
                
                    // It's OK
                BreakOut = false;
                std::string     Input( ReadLine );
                free( ReadLine );

                boost::algorithm::trim( Input );
                if ( !Input.empty() )
                {
                    if ( (history_length == 0) ||
                         (Input != history_list()[ history_length - 1 ]->line)
                       )
                    {
                        add_history( Input.c_str() );
                        if ( history_length >= static_cast< int >( HistoryLimit ) )
                        {
                            stifle_history( HistoryLimit );
                        }
                    }
                }

                return Input;
            }


                /*! @brief Fills the given container with the current history list
                           Does not clear the given container
                 */
            template < typename ContainerType >
            void GetHistory( ContainerType &  Container )
            {
                for ( int  k( 0 ); k < history_length; ++k )
                {
                    Container.push_back( history_list()[ k ]->line );
                }
            }

                /*! @brief Saves the history to the given file stream
                    @param OS output file stream
                    @return true if success
                 */
            bool SaveHistory( std::ostream & OS )
            {
                if ( ! OS ) return false;
                for ( int  k( 0 ); k < history_length; ++k )
                {
                    OS << history_list()[ k ]->line << std::endl;
                }
                return true;
            }

                /*! @brief Saves the history to the given file
                    @param FileName File name to save the history to
                    @return true if success
                 */
            bool SaveHistory( const std::string &  FileName )
            {
                if ( FileName.empty() ) return false;

                std::ofstream  OS( FileName.c_str() );
                return SaveHistory( OS );
            }

                /*! @brief Clears the history. Does not affect the file where
                           the previous session history is saved.
                 */
            void ClearHistory()
            {
                clear_history();
            }
            

                /*! @brief Loads a history from a file stream
                    @param IS Input file stream
                    @return true if success
                 */
            bool LoadHistory( std::istream &  IS )
            {
                if ( ! IS ) return false;

                ClearHistory();
                std::string     OneLine;

                while ( ! getline( IS, OneLine ).eof() )
                {
                    boost::algorithm::trim( OneLine );
                    if ( (history_length == 0) ||
                         (OneLine != history_list()[ history_length - 1 ]->line)
                       )
                    {
                        add_history( OneLine.c_str() );
                    }
                }
                stifle_history( HistoryLimit );
                return true;
            }
            
                /*! @brief Loads a history from the given file
                    @param FileName File name to be load from
                    @return true if success
                 */
            bool LoadHistory( const std::string &  FileName )
            {
                if ( FileName.empty() ) return false;

                std::ifstream   IS( FileName.c_str() );
                return LoadHistory( IS );
            }

                /*! @brief Allows to register custom completers.
                           Supports a special keyword: %file. It means to use the standard file name completer
                           For example the given container elements could be as follows:
                               command1 opt1
                               command1 opt2 %file
                               command2
                               command2 opt1
                           Each container element must describe a single possible command line
                           The container element must have a conversion to std::string operator
                    @param Container A container which has all the user possible commands
                 */       
            template < typename  ContainerType >
            void RegisterCompletions( const ContainerType &  Container )
            {
                Completions.clear();
                for ( typename ContainerType::const_iterator  k( Container.begin() );
                      k != Container.end(); ++k )
                {
                    std::vector< std::string >  OneLine;
                    SplitTokens( static_cast<std::string>(*k), OneLine );
                    Completions.push_back( OneLine );
                }
            }

                
                /*! @brief Sets the given keymap
                    @param NewKeymap The keymap that should be used from now
                 */
            void  SetKeymap( SKeymap &  NewKeymap )
            {
                rl_set_keymap( NewKeymap.keymap );
                KeymapWasSetup = true;
                Earlykeymap = NewKeymap.keymap;
            }

        private:
            const size_t            HistoryLimit;       //!< Constructor accepted or default
            const std::string       HistoryFileName;    //!< Original constructor accepted
            rl_completion_func_t *  OriginalCompletion; //!< Original state will be restored
    };


}; // namespace swift

#endif