// Copyright (C) 2006  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_LOGGER_KERNEl_1_
#define DLIB_LOGGER_KERNEl_1_

#include <limits>
#include <memory>
#include <cstring>
#include <streambuf>
#include <vector>

#include "../threads.h"
#include "../misc_api.h"
#include "../set.h"
#include "logger_kernel_abstract.h"
#include "../algs.h"
#include "../assert.h"
#include "../uintn.h"
#include "../map.h"
#include "../member_function_pointer.h"

namespace dlib
{

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

    class log_level
    {
    public:
        log_level(
            int priority_, 
            const char* name_
        ) : 
            priority(priority_)
        {
            strncpy(name,name_,19);
            name[19] = '\0';
        }

        bool operator< (const log_level& rhs) const { return priority <  rhs.priority; }
        bool operator<=(const log_level& rhs) const { return priority <= rhs.priority; }
        bool operator> (const log_level& rhs) const { return priority >  rhs.priority; }
        bool operator>=(const log_level& rhs) const { return priority >= rhs.priority; }

        int priority;
        char name[20];
    };

    inline std::ostream& operator<< (std::ostream& out, const log_level& item)
    {
        out << item.name;
        return out;
    }

    const log_level LALL  (std::numeric_limits<int>::min(),"ALL");
    const log_level LNONE (std::numeric_limits<int>::max(),"NONE");
    const log_level LTRACE(-100,"TRACE");
    const log_level LDEBUG(0  ,"DEBUG");
    const log_level LINFO (100,"INFO ");
    const log_level LWARN (200,"WARN ");
    const log_level LERROR(300,"ERROR");
    const log_level LFATAL(400,"FATAL");

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

    void set_all_logging_output_streams (
        std::ostream& out
    );

    void set_all_logging_levels (
        const log_level& new_level
    );

    typedef void (*print_header_type)(
        std::ostream& out, 
        const std::string& logger_name, 
        const log_level& l,
        const uint64 thread_id
    );

    void set_all_logging_headers (
        const print_header_type& new_header
    );

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

    void print_default_logger_header (
        std::ostream& out,
        const std::string& logger_name,
        const log_level& l,
        const uint64 thread_id
    );

    template <
        typename T
        >
    void set_all_logging_output_hooks (
        T& object,
        void (T::*hook_)(const std::string& logger_name, 
                         const log_level& l,
                         const uint64 thread_id,
                         const char* message_to_log)
    );

    template <
        typename T
        >
    void set_all_logging_output_hooks (
        T& object
    )
    {
        set_all_logging_output_hooks(object, &T::log);
    }

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

    class logger 
    {
        /*!
            INITIAL VALUE
                - print_header == print_default_logger_header
                - out.rdbuf() == std::cout.rdbuf()
                - cur_level == LERROR
                - auto_flush_enabled == true 
                - hook.is_set() == false

            CONVENTION
                - print_header == logger_header()
                - if (hook.is_set() == false) then
                    - out.rdbuf() == output_streambuf()
                - else
                    - out.rdbuf() == &gd.hookbuf
                    - output_streambuf() == 0

                - cur_level == level()
                - logger_name == name()
                - auto_flush_enabled == auto_flush()

                - logger::gd::loggers == a set containing all currently existing loggers.
                - logger::gd::m == the mutex used to lock everything in the logger
                - logger::gd::thread_names == a map of thread ids to thread names.  
                - logger::gd::next_thread_name == the next thread name that will be given out
                  to a thread when we find that it isn't already in thread_names.
        !*/

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

        class logger_stream
        {
            /*!
                INITIAL VALUE
                    - been_used == false

                CONVENTION
                    - enabled == is_enabled()
                    - if (been_used) then
                        - logger::gd::m is locked
                        - someone has used the << operator to write something to the
                          output stream.
            !*/
        public:
            logger_stream (
                const log_level& l_,
                logger& log_
            ) :
                l(l_),
                log(log_),
                been_used(false),
                enabled (l.priority >= log.cur_level.priority)
            {}

            inline ~logger_stream(
            )
            {
                if (!been_used)
                {
                    return;
                }
                else
                {
                    print_end_of_line();
                }
            }

            bool is_enabled (
            ) const { return enabled; }

            template <typename T>
            inline logger_stream& operator << (
                const T& item
            )
            {
                if (!enabled)
                {
                    return *this;
                }
                else
                {
                    print_header_and_stuff();
                    log.out << item;
                    return *this;
                }
            }

        private:

            void print_header_and_stuff (
            );
            /*!
                ensures
                    - if (!been_used) then
                        - prints the logger header 
                        - locks log.gd.m
                        - #been_used == true
            !*/

            void print_end_of_line (
            );
            /*!
                ensures
                    - prints a newline to log.out
                    - unlocks log.gd.m
            !*/

            const log_level& l;
            logger& log;
            bool been_used;
            const bool enabled;
        }; // end of class logger_stream

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

        friend class logger_stream;
    public:

        typedef member_function_pointer<const std::string&, const log_level&, 
                                        const uint64, const char*> hook_mfp;

        logger (  
            const std::string& name_
        );

        virtual ~logger (
        ); 

        const std::string& name (
        ) const { return logger_name; }

        logger_stream operator << (
            const log_level& l
        ) const { return logger_stream(l,const_cast<logger&>(*this)); }

        bool is_child_of (
            const logger& log
        ) const
        {
            return (name().find(log.name() + ".") == 0) || (log.name() == name());
        }

        const log_level level (
        ) const 
        { 
            auto_mutex M(gd.m);
            return log_level(cur_level); 
        };

        void set_level (
            const log_level& new_level
        )
        {
            auto_mutex M(gd.m);
            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                if (gd.loggers.element()->is_child_of(*this))
                    gd.loggers.element()->cur_level = new_level;
            }

            gd.set_level(logger_name, new_level);
        }

        bool auto_flush (
        ) const 
        { 
            auto_mutex M(gd.m);
            return auto_flush_enabled;
        };

        void set_auto_flush (
            bool enabled
        )
        {
            auto_mutex M(gd.m);
            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                if (gd.loggers.element()->is_child_of(*this))
                    gd.loggers.element()->auto_flush_enabled = enabled;
            }

            gd.set_auto_flush(logger_name, enabled);
        }

        std::streambuf* output_streambuf (
        )
        {
            auto_mutex M(gd.m);

            // if there is an output hook set then we are supposed to return 0.
            if (hook)
                return 0;
            else
                return out.rdbuf();
        }

        template <
            typename T
            >
        void set_output_hook (
            T& object,
            void (T::*hook_)(const std::string& logger_name, 
                            const log_level& l,
                            const uint64 thread_id,
                            const char* message_to_log)
        )
        {
            auto_mutex M(gd.m);
            hook.set(object, hook_);

            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                if (gd.loggers.element()->is_child_of(*this))
                {
                    gd.loggers.element()->out.rdbuf(&gd.hookbuf);
                    gd.loggers.element()->hook = hook;
                }
            }

            gd.set_output_hook(logger_name, hook);
            gd.set_output_stream(logger_name, gd.hookbuf);
        }

        void set_output_stream (
            std::ostream& out_
        ) 
        {
            auto_mutex M(gd.m);
            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                if (gd.loggers.element()->is_child_of(*this))
                {
                    gd.loggers.element()->out.rdbuf(out_.rdbuf());
                    gd.loggers.element()->hook.clear();
                }
            }

            gd.set_output_stream(logger_name, out_);

            hook.clear();
            gd.set_output_hook(logger_name, hook);
        }

        print_header_type logger_header (
        ) const { return print_header; }

        void set_logger_header (
            print_header_type ph
        )
        {
            auto_mutex M(gd.m);
            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                if (gd.loggers.element()->is_child_of(*this))
                    gd.loggers.element()->print_header = ph;
            }

            gd.set_logger_header(logger_name, ph);
        }

    private:

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

        struct global_data
        {
            rmutex m;
            set<logger*>::kernel_1b loggers;
            map<thread_id_type,uint64>::kernel_1b thread_names;
            uint64 next_thread_name;

            // Make a very simple streambuf that writes characters into a std::vector<char>.  We can
            // use this as the output target for hooks.  The reason we don't just use a std::ostringstream
            // instead is that this way we can be guaranteed that logging doesn't perform memory allocations.
            // This is because a std::vector never frees memory.  I.e. its capacity() doesn't go down when
            // you resize it back to 0.  It just stays the same.
            class hook_streambuf : public std::streambuf
            {
            public:
                std::vector<char> buffer;
                int_type overflow ( int_type c)
                {
                    if (c != EOF) buffer.push_back(static_cast<char>(c));
                    return c;
                }

                std::streamsize xsputn ( const char* s, std::streamsize num)
                {
                    buffer.insert(buffer.end(), s, s+num);
                    return num;
                }
            };

            hook_streambuf hookbuf;

            global_data (
            );

            ~global_data(
            );

            uint64 get_thread_name (
            );
            /*!
                requires
                    - m is locked
                ensures
                    - returns a unique id for the calling thread.  also makes the number
                      small and nice unlike what you get from get_thread_id()
            !*/

            void thread_end_handler (
            );
            /*!
                ensures
                    - removes the terminated thread from thread_names
            !*/

            struct level_container
            {
                level_container ();

                log_level val;
                map<std::string,std::unique_ptr<level_container> >::kernel_1b_c table;
            } level_table;

            const log_level level (
                const std::string& name
            ) const; 
            /*!
                ensures
                    - returns the level loggers with the given name are supposed 
                      to have
            !*/

            void set_level (
                const std::string& name,
                const log_level& new_level
            );
            /*!
                ensures
                    - for all children C of name:
                        - #level(C) == new_level
                    - if name == "" then
                        - for all loggers L:
                            - #level(L) == new_level
            !*/

            struct auto_flush_container
            {
                bool val;
                map<std::string,std::unique_ptr<auto_flush_container> >::kernel_1b_c table;
            } auto_flush_table;

            bool auto_flush (
                const std::string& name
            ) const;
            /*!
                ensures
                    - returns the auto_flush value loggers with the given name are supposed 
                      to have
            !*/

            void set_auto_flush (
                const std::string& name,
                bool enabled
            );
            /*!
                ensures
                    - for all children C of name:
                        - #auto_flush_enabled(C) == enabled 
                    - if name == "" then
                        - for all loggers L:
                            - #auto_flush_enabled(L) == enabled 
            !*/

            struct output_streambuf_container
            {
                std::streambuf* val;
                map<std::string,std::unique_ptr<output_streambuf_container> >::kernel_1b_c table;
            } streambuf_table;

            std::streambuf* output_streambuf (
                const std::string& name
            );
            /*!
                ensures
                    - returns the streambuf loggers with the given name are supposed 
                      to have
            !*/

            void set_output_stream (
                const std::string& name,
                std::ostream& out_
            );
            /*!
                ensures
                    - for all children C of name:
                        - #output_streambuf(C) == out_.rdbuf() 
                    - if name == "" then
                        - for all loggers L:
                            - #output_streambuf(L) == out_.rdbuf() 
            !*/

            void set_output_stream (
                const std::string& name,
                std::streambuf& buf 
            );
            /*!
                ensures
                    - for all children C of name:
                        - #output_streambuf(C) == &buf 
                    - if name == "" then
                        - for all loggers L:
                            - #output_streambuf(L) == &buf 
            !*/

            struct output_hook_container
            {
                hook_mfp val;
                map<std::string,std::unique_ptr<output_hook_container> >::kernel_1b_c table;
            } hook_table;

            hook_mfp output_hook (
                const std::string& name
            );
            /*!
                ensures
                    - returns the hook loggers with the given name are supposed 
                      to have
            !*/

            void set_output_hook (
                const std::string& name,
                const hook_mfp& hook
            );
            /*!
                ensures
                    - for all children C of name:
                        - #output_hook(C) == hook 
                    - if name == "" then
                        - for all loggers L:
                            - #output_hook(L) == hook 
            !*/

            struct logger_header_container
            {
                print_header_type val;
                map<std::string,std::unique_ptr<logger_header_container> >::kernel_1b_c table;
            } header_table;

            print_header_type logger_header (
                const std::string& name
            );
            /*!
                ensures
                    - returns the header function loggers with the given name are supposed 
                      to have
            !*/

            void set_logger_header (
                const std::string& name,
                print_header_type ph
            );
            /*!
                ensures
                    - for all children C of name:
                        - #logger_header(C) == ph 
                    - if name == "" then
                        - for all loggers L:
                            - #logger_header(L) == ph 
            !*/

        }; // end of struct global_data

        static global_data& get_global_data();

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

        friend void set_all_logging_levels (
            const log_level& new_level
        );

        friend void set_all_logging_headers (
            const print_header_type& new_header 
        );

        friend void set_all_logging_output_streams (
            std::ostream& out
        );

        template <
            typename T
            >
        friend void set_all_logging_output_hooks (
            T& object,
            void (T::*hook_)(const std::string& logger_name, 
                            const log_level& l,
                            const uint64 thread_id,
                            const char* message_to_log)
        )
        {
            logger::hook_mfp hook;

            // There is a bug in one of the versions (but not all apparently) of 
            // Visual studio 2005 that causes it to error out if <T> isn't in the
            // following line of code.  However, there is also a bug in gcc-3.3 
            // that causes it to error out if <T> is present.  So this works around
            // this problem.
#if defined(_MSC_VER) && _MSC_VER == 1400
            hook.set<T>(object, hook_);
#else
            hook.set(object, hook_);
#endif

            logger::global_data& gd = logger::get_global_data();
            auto_mutex M(gd.m);
            gd.loggers.reset();
            while (gd.loggers.move_next())
            {
                gd.loggers.element()->out.rdbuf(&gd.hookbuf);
                gd.loggers.element()->hook = hook;
            }

            gd.set_output_stream("",gd.hookbuf);
            gd.set_output_hook("",hook);
        }

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

        global_data& gd;

        const std::string logger_name;

        print_header_type print_header;
        bool auto_flush_enabled;
        std::ostream out;
        log_level cur_level;

        hook_mfp hook;


        // restricted functions
        logger(const logger&);        // copy constructor
        logger& operator=(const logger&);    // assignment operator

    };    

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




}

#ifdef NO_MAKEFILE
#include "logger_kernel_1.cpp"
#endif

#endif // DLIB_LOGGER_KERNEl_1_

