// Copyright (C) 2003  Davis E. King (davisking@users.sourceforge.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_CONFIG_READER_KERNEl_1_
#define DLIB_CONFIG_READER_KERNEl_1_

#include "config_reader_kernel_abstract.h"
#include <string>
#include <iostream>
#include <sstream>
#include "../algs.h"
#include "../interfaces/enumerable.h"

namespace dlib
{

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking = false
        >
    class config_reader_kernel_1 : public enumerable<config_reader_kernel_1<map_string_string,
                                                                            map_string_void,
                                                                            tokenizer,
                                                                            checking> >
    {

        /*!                
            REQUIREMENTS ON map_string_string
                is an implementation of map/map_kernel_abstract.h that maps std::string to std::string

            REQUIREMENTS ON map_string_void 
                is an implementation of map/map_kernel_abstract.h that maps std::string to void*

            REQUIREMENTS ON tokenizer
                is an implementation of tokenizer/tokenizer_kernel_abstract.h 

            REQUIREMENTS ON checking
                - if (checking == true) then
                    - The preconditions for this object will be checked.
                - else
                    - The preconditions for this object will NOT be checked.

            CONVENTION
                key_table.is_in_domain(x) == is_key_defined(x)
                block_table.is_in_domain(x) == is_block_defined(x)

                key_table[x] == operator[](x)
                block_table[x] == (void*)&block(x)
        !*/
        
    public:

        config_reader_kernel_1();

        class config_reader_error : public dlib::error 
        {
            friend class config_reader_kernel_1;
            config_reader_error(
                unsigned long ln, 
                bool r = false
            ) : 
                dlib::error(ECONFIG_READER),
                line_number(ln), 
                redefinition(r)
            {
                std::ostringstream sout;
                sout << "Error in config_reader while parsing at line number " << line_number << ".";
                if (redefinition)
                    sout << "\nThe identifier on this line has already been defined in this scope.";
                const_cast<std::string&>(info) = sout.str();
            }
        public:
            const unsigned long line_number;
            const bool redefinition;
        };


        config_reader_kernel_1(
            std::istream& in
        );

        virtual ~config_reader_kernel_1(
        ); 

        void clear (
        );

        void load_from (
            std::istream& in
        );

        bool is_key_defined (
            const std::string& key
        ) const;

        bool is_block_defined (
            const std::string& name
        ) const;

        typedef config_reader_kernel_1 this_type;
        const this_type& block (
            const std::string& name
        ) const;

        const std::string& operator[] (
            const std::string& key
        ) const;

        template <
            typename queue_of_strings
            >
        void get_keys (
            queue_of_strings& keys
        ) const;

        inline bool at_start (
        ) const ;

        inline void reset (
        ) const ;

        inline bool current_element_valid (
        ) const ;

        inline const this_type& element (
        ) const ;

        inline this_type& element (
        ) ;

        inline bool move_next (
        ) const ;

        inline unsigned long size (
        ) const ;

        inline const std::string& current_block_name (
        ) const;

    private:

        static void parse_config_file (
            config_reader_kernel_1& cr,
            tokenizer& tok,
            unsigned long& line_number,
            const bool top_of_recursion = true
        );
        /*!
            requires
                - line_number == 1
                - cr == *this
                - top_of_recursion == true
            ensures
                - parses the data coming from tok and puts it into cr.
            throws
                - config_reader_error
        !*/

        map_string_string key_table;
        map_string_void block_table;

        // restricted functions
        config_reader_kernel_1(config_reader_kernel_1&);     
        config_reader_kernel_1& operator=(config_reader_kernel_1&);

    };

// ----------------------------------------------------------------------------------------

    /* 
        This is a bunch of crap so we can enable and disable the DLIB_CASSERT statements
        without getting warnings about conditions always being true or false.
    */
    namespace config_reader_kernel_1_helpers
    {
        template <typename cr_type, bool do_check>
        struct helper;

        template <typename cr_type>
        struct helper<cr_type,false>
        {
            static void check_operator_bracket_precondition (const cr_type&, const std::string& ) {}
            static void check_block_precondition (const cr_type&,  const std::string& ) {}
            static void check_current_block_name_precondition (const cr_type& cr) {} 
            static void check_element_precondition (const cr_type& cr) {}
        };

        template <typename cr_type>
        struct helper<cr_type,true>
        {
            static void check_operator_bracket_precondition (const cr_type& cr, const std::string& key) 
            {
                DLIB_CASSERT ( cr.is_key_defined(key) == true ,
                          "\tconst std::string& config_reader::operator[](key)"
                          << "\n\tTo access a key's value in the config_reader the key must actually exist."
                          << "\n\tkey == " << key 
                          << "\n\t&cr:  " << &cr 
                );
            }

            static void check_block_precondition (const cr_type& cr, const std::string& name) 
            {
                DLIB_CASSERT ( cr.is_block_defined(name) == true ,
                          "\tconst this_type& config_reader::block(name)"
                          << "\n\tTo access a sub block in the config_reader the block must actually exist."
                          << "\n\tname == " << name 
                          << "\n\t&cr:   " << &cr 
                );
            }

            static void check_current_block_name_precondition (const cr_type& cr) 
            {
                DLIB_CASSERT ( cr.current_element_valid() == true ,
                          "\tconst std::string& config_reader::current_block_name()"
                          << "\n\tYou can't call current_block_name() if the current element isn't valid."
                          << "\n\t&cr: " << &cr 
                );
            }

            static void check_element_precondition (const cr_type& cr) 
            {
                DLIB_CASSERT ( cr.current_element_valid() == true ,
                          "\tthis_type& config_reader::element()"
                          << "\n\tYou can't call element() if the current element isn't valid."
                          << "\n\t&cr: " << &cr 
                );
            }
        };
    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
    // member function definitions
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    config_reader_kernel_1(
    )
    {
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    void config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    clear(
    )
    {
        // free all our blocks
        block_table.reset();
        while (block_table.move_next())
        {
            delete reinterpret_cast<config_reader_kernel_1*>(block_table.element().value());
        }
        block_table.clear();
        key_table.clear();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    void config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    load_from(
        std::istream& in
    )
    {
        clear();

        tokenizer tok;
        tok.set_stream(in);
        tok.set_identifier_token(
            tok.lowercase_letters() + tok.uppercase_letters(),
            tok.lowercase_letters() + tok.uppercase_letters() + tok.numbers() + "_-."
        );

        unsigned long line_number = 1;
        try
        {
            parse_config_file(*this,tok,line_number);
        }
        catch (...)
        {
            clear();
            throw;
        }
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    config_reader_kernel_1(
        std::istream& in
    )
    {
        load_from(in);
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    void config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    parse_config_file(
        config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>& cr,
        tokenizer& tok,
        unsigned long& line_number,
        const bool top_of_recursion
    )
    {
        int type;
        std::string token;
        bool in_comment = false;
        bool seen_identifier = false;
        std::string identifier;
        while (true)
        {
            tok.get_token(type,token);
            // ignore white space
            if (type == tokenizer::WHITE_SPACE)
                continue;

            // basically ignore end of lines
            if (type == tokenizer::END_OF_LINE)
            {
                ++line_number;
                in_comment = false;
                continue;
            }

            // we are in a comment still so ignore this
            if (in_comment)
                continue;

            // if this is the start of a comment
            if (type == tokenizer::CHAR && token[0] == '#')
            {
                in_comment = true;
                continue;
            }

            // if this is the case then we have just finished parsing a block so we should
            // quit this function
            if ( (type == tokenizer::CHAR && token[0] == '}' && !top_of_recursion) ||
                 (type == tokenizer::END_OF_FILE && top_of_recursion) )
            {
                break;
            }

            if (seen_identifier)
            {
                seen_identifier = false;
                // the next character should be either a '=' or a '{'
                if (type != tokenizer::CHAR || (token[0] != '=' && token[0] != '{'))
                    throw config_reader_error(line_number);
                
                if (token[0] == '=')
                {
                    // we should parse the value out now
                    // first discard any white space
                    if (tok.peek_type() == tokenizer::WHITE_SPACE)
                        tok.get_token(type,token);

                    std::string value;
                    type = tok.peek_type();
                    token = tok.peek_token();
                    while (true)
                    {
                        if (type == tokenizer::END_OF_FILE || type == tokenizer::END_OF_LINE)
                            break;

                        if (type == tokenizer::CHAR && token[0] == '\\')
                        {
                            tok.get_token(type,token);
                            if (tok.peek_type() == tokenizer::CHAR && 
                                tok.peek_token()[0] == '#')
                            {
                                tok.get_token(type,token);
                                value += '#';
                            }
                            else if (tok.peek_type() == tokenizer::CHAR && 
                                tok.peek_token()[0] == '}')
                            {
                                tok.get_token(type,token);
                                value += '}';
                            }
                            else
                            {
                                value += '\\';
                            }
                        }
                        else if (type == tokenizer::CHAR && 
                                 (token[0] == '#' || token[0] == '}'))
                        {
                            break;
                        }
                        else
                        {
                            value += token;
                            tok.get_token(type,token);
                        }
                        type = tok.peek_type();
                        token = tok.peek_token();
                    } // while(true)

                    // strip of any tailing white space from value
                    std::string::size_type pos = value.find_last_not_of(" \t\r\n");
                    if (pos == std::string::npos)
                        value.clear();
                    else
                        value.erase(pos+1);

                    // make sure this key isn't already in the key_table
                    if (cr.key_table.is_in_domain(identifier))
                        throw config_reader_error(line_number,true);

                    // add this key/value pair to the key_table
                    cr.key_table.add(identifier,value);

                }
                else  // when token[0] == '{'
                {
                    // make sure this identifier isn't already in the block_table
                    if (cr.block_table.is_in_domain(identifier))
                        throw config_reader_error(line_number,true);

                    config_reader_kernel_1* new_cr = new config_reader_kernel_1;
                    void* vtemp = new_cr;
                    try { cr.block_table.add(identifier,vtemp); }
                    catch (...) { delete new_cr; throw; }

                    // now parse this block 
                    parse_config_file(*new_cr,tok,line_number,false);
                }
            }
            else
            {
                // the next thing should be an identifier but if it isn't this is an error
                if (type != tokenizer::IDENTIFIER)
                    throw config_reader_error(line_number);

                seen_identifier = true;
                identifier = token;
            }
        } // while (true) 
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    ~config_reader_kernel_1(
    ) 
    {
        clear();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    bool config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    is_key_defined (
        const std::string& key
    ) const
    {
        return key_table.is_in_domain(key);
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    bool config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    is_block_defined (
        const std::string& name
    ) const
    {
        return block_table.is_in_domain(name);
    }

// ----------------------------------------------------------------------------------------

    template <
        typename mss,
        typename msv,
        typename tokenizer,
        bool checking
        >
    const config_reader_kernel_1<mss,msv,tokenizer,checking>& config_reader_kernel_1<mss,msv,tokenizer,checking>::
    block (
        const std::string& name
    ) const
    {
        config_reader_kernel_1_helpers::helper<config_reader_kernel_1,checking>::
            check_block_precondition(*this,name);
        return *reinterpret_cast<config_reader_kernel_1*>(block_table[name]);
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    const std::string& config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    operator[] (
        const std::string& key
    ) const
    {
        config_reader_kernel_1_helpers::helper<config_reader_kernel_1,checking>::
            check_operator_bracket_precondition(*this,key);
        return key_table[key];
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    template <
        typename queue_of_strings
        >
    void config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    get_keys (
        queue_of_strings& keys
    ) const
    {
        keys.clear();
        key_table.reset();
        std::string temp;
        while (key_table.move_next())
        {
            temp = key_table.element().key();
            keys.enqueue(temp);
        }
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    bool config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    at_start (
    ) const 
    {
        return block_table.at_start();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    void config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    reset (
    ) const 
    {
        block_table.reset();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    bool config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    current_element_valid (
    ) const 
    {
        return block_table.current_element_valid();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename mss,
        typename msv,
        typename tokenizer,
        bool checking
        >
    const config_reader_kernel_1<mss,msv,tokenizer,checking>& config_reader_kernel_1<mss,msv,tokenizer,checking>::
    element (
    ) const 
    {
        config_reader_kernel_1_helpers::helper<config_reader_kernel_1,checking>::
            check_element_precondition(*this);
        return *reinterpret_cast<config_reader_kernel_1*>(block_table.element().value());
    }

// ----------------------------------------------------------------------------------------

    template <
        typename mss,
        typename msv,
        typename tokenizer,
        bool checking
        >
    config_reader_kernel_1<mss,msv,tokenizer,checking>& config_reader_kernel_1<mss,msv,tokenizer,checking>::
    element (
    ) 
    {
        config_reader_kernel_1_helpers::helper<config_reader_kernel_1,checking>::
            check_element_precondition(*this);
        return *reinterpret_cast<config_reader_kernel_1*>(block_table.element().value());
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    bool config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    move_next (
    ) const 
    {
        return block_table.move_next();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    unsigned long config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    size (
    ) const 
    {
        return block_table.size();
    }

// ----------------------------------------------------------------------------------------

    template <
        typename map_string_string,
        typename map_string_void,
        typename tokenizer,
        bool checking
        >
    const std::string& config_reader_kernel_1<map_string_string,map_string_void,tokenizer,checking>::
    current_block_name (
    ) const
    {
        config_reader_kernel_1_helpers::helper<config_reader_kernel_1,checking>::
            check_current_block_name_precondition(*this);
        return block_table.element().key();
    }

// ----------------------------------------------------------------------------------------

}

#endif // DLIB_CONFIG_READER_KERNEl_1_

