// 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_CPp_
#define DLIB_LOGGER_KERNEL_1_CPp_

#include "logger_kernel_1.h"
#include <iostream>
#include <sstream>

namespace dlib
{
    
// ----------------------------------------------------------------------------------------

    void set_all_logging_output_streams (
        std::ostream& out_
    )
    {
        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(out_.rdbuf());
            gd.loggers.element()->hook.clear();
        }

        gd.set_output_stream("",out_);

        // set the default hook to be an empty member function pointer
        logger::hook_mfp hook;
        gd.set_output_hook("",hook);
    }

    void set_all_logging_levels (
        const log_level& new_level
    )
    {
        logger::global_data& gd = logger::get_global_data();
        auto_mutex M(gd.m);
        gd.loggers.reset();
        while (gd.loggers.move_next())
        {
            gd.loggers.element()->cur_level = new_level;
        }

        gd.set_level("",new_level);
    }

    void set_all_logging_headers (
        const print_header_type& new_header
    )
    {
        logger::global_data& gd = logger::get_global_data();
        auto_mutex M(gd.m);
        gd.loggers.reset();
        while (gd.loggers.move_next())
        {
            gd.loggers.element()->print_header = new_header;
        }

        gd.set_logger_header("",new_header);
    }

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

    namespace logger_helper_stuff
    {
        class helper
        {
        public:
            helper()
            {
                std::ostringstream sout;
                print_default_logger_header(sout,"some_name",LDEBUG,0);
            }
        };
        // do this to make sure all the static members of print_default_logger_header get 
        // initialized when the program turns on.
        static helper a;
        // make a logger to make extra sure the static global_data object gets
        // initialized before any threads start up.  Also do this so that there is always
        // at least one logger so that the global data won't be deleted until the 
        // program is terminating.
        static logger log("dlib");
    }

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

    void print_default_logger_header (
        std::ostream& out,
        const std::string& logger_name,
        const log_level& l,
        const uint64 thread_id
    )
    {
        using namespace std;
        static timestamper ts;
        static const uint64 first_time = ts.get_timestamp();

        const uint64 cur_time = (ts.get_timestamp() - first_time)/1000;
        streamsize old_width = out.width(); out.width(5);
        out << cur_time << " " << l.name; 
        out.width(old_width);

        out << " [" << thread_id << "] " << logger_name << ": ";
    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
//                 global_data stuff
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    logger::global_data::
    ~global_data (
    )
    {
        unregister_thread_end_handler(*this,&global_data::thread_end_handler);
    }

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

    logger::global_data::
    global_data(
    ) : 
        next_thread_name(1) 
    { 
        // make sure the main program thread always has id 0.  Since there is
        // a global logger object declared in this file we should expect that 
        // the global_data object will be initialized in the main program thread
        // so if we call get_thread_id() now we should get the main thread id.
        thread_id_type main_id = get_thread_id();
        uint64 id_zero = 0;
        thread_names.add(main_id,id_zero);

        // set up the defaults
        auto_flush_table.val = true;
        streambuf_table.val = std::cout.rdbuf(); 
        header_table.val = print_default_logger_header;

        // also allocate an initial buffer for hook based logging
        hookbuf.buffer.reserve(1000);
    }

    logger::global_data::level_container::
    level_container (
    ) : val(300,"ERROR") {}

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

    template <typename T>
    const T& search_tables (
        const T& c,
        const std::string& name
    )
    {
        if (c.table.size() == 0 || name.size() == 0)
            return c;

        const std::string::size_type pos = name.find_first_of(".");
        const std::string first = name.substr(0,pos);
        std::string last;
        if (pos != std::string::npos)
            last = name.substr(pos+1);

        if (c.table.is_in_domain(first))
        {
            return search_tables(*c.table[first], last); 
        }
        else
        {
            return c;
        }
    }

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

    template <typename T, typename U>
    void assign_tables (
        T& c,
        const std::string& name,
        const U& val
    )
    {
        if (name.size() == 0)
        {
            c.val = val;
            c.table.clear();
            return;
        }

        const std::string::size_type pos = name.find_first_of(".");
        std::string first = name.substr(0,pos);
        std::string last;
        if (pos != std::string::npos)
            last = name.substr(pos+1);

        if (c.table.is_in_domain(first))
        {
            assign_tables(*c.table[first], last, val); 
        }
        else
        {
            std::unique_ptr<T> temp (new T);
            temp->val = c.val;
            assign_tables(*temp, last, val);
            c.table.add(first,temp);
        }
    }

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

    const log_level logger::global_data::
    level (
        const std::string& name
    ) const 
    {  
        auto_mutex M(m);
        return search_tables(level_table, name).val;
    }

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

    void logger::global_data::
    set_level (
        const std::string& name,
        const log_level& new_level
    )
    {
        auto_mutex M(m);
        assign_tables(level_table, name, new_level);
    }

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

    bool logger::global_data::
    auto_flush (
        const std::string& name
    ) const
    {
        auto_mutex M(m);
        return search_tables(auto_flush_table, name).val;
    }

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

    void logger::global_data::
    set_auto_flush (
        const std::string& name,
        bool enabled
    )
    {
        auto_mutex M(m);
        assign_tables(auto_flush_table, name, enabled);
    }

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

    std::streambuf* logger::global_data::
    output_streambuf (
        const std::string& name
    )
    {
        auto_mutex M(m);
        return search_tables(streambuf_table, name).val;
    }

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

    void logger::global_data::
    set_output_stream (
        const std::string& name,
        std::ostream& out_
    )
    {
        auto_mutex M(m);
        assign_tables( streambuf_table, name, out_.rdbuf());
    }

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

    void logger::global_data::
    set_output_stream (
        const std::string& name,
        std::streambuf& buf 
    )
    {
        auto_mutex M(m);
        assign_tables( streambuf_table, name, &buf);
    }

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

    logger::hook_mfp logger::global_data::
    output_hook (
        const std::string& name
    )
    {
        auto_mutex M(m);
        return search_tables(hook_table, name).val;
    }

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

    void logger::global_data::
    set_output_hook (
        const std::string& name,
        const hook_mfp& hook
    )
    {
        auto_mutex M(m);
        assign_tables( hook_table, name, hook);
    }

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

    print_header_type logger::global_data::
    logger_header (
        const std::string& name
    )
    {
        auto_mutex M(m);
        return search_tables(header_table, name).val;
    }

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

    void logger::global_data::
    set_logger_header (
        const std::string& name,
        print_header_type ph
    )
    {
        auto_mutex M(m);
        assign_tables(header_table, name, ph);
    }

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

    logger::global_data& logger::get_global_data()
    {
        // Allocate the global_data on the heap rather than on the stack because
        // we want to guard against the case where this static object would be destroyed
        // during program termination BEFORE all logger objects are destroyed.
        static global_data* gd = new global_data;
        return *gd;
    }

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

    void logger::global_data::
    thread_end_handler (
    )
    {
        auto_mutex M(m);
        thread_id_type id = get_thread_id();
        thread_id_type junkd;
        uint64 junkr;
        thread_names.remove(id,junkd,junkr);
    }

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

    uint64 logger::global_data::
    get_thread_name (
    )
    {
        thread_id_type id = get_thread_id();
        uint64 thread_name;
        if (thread_names.is_in_domain(id))
        {
            thread_name = thread_names[id];
        }
        else
        {
            if (is_dlib_thread(id))
                register_thread_end_handler(*this,&global_data::thread_end_handler);
            thread_name = next_thread_name;
            thread_names.add(id,thread_name);
            thread_name = next_thread_name;
            ++next_thread_name;
        }
        return thread_name;
    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
//               logger_stream stuff
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    void logger::logger_stream::
    print_header_and_stuff (
    )
    {
        if (!been_used)
        {
            log.gd.m.lock();

            // Check if the output hook is setup.  If it isn't then we print the logger
            // header like normal.  Otherwise we need to remember to clear out the output
            // stringstream we always write to.
            if (log.hook.is_set() == false)
            {
                log.logger_header()(log.out,log.name(),l,log.gd.get_thread_name());
            }
            else
            {
                // Make sure the hook buffer doesn't have any old data in it before we start
                // logging a new message into it.
                log.gd.hookbuf.buffer.resize(0);
            }
            been_used = true;
        }
    }

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

    void logger::logger_stream::
    print_end_of_line (
    )
    {
        auto_unlock M(log.gd.m);

        if (log.hook.is_set() == false)
        {
            if (log.auto_flush_enabled)
                log.out << std::endl;
            else
                log.out << "\n";
        }
        else
        {
            // Make sure the buffer is a proper C-string
            log.gd.hookbuf.buffer.push_back('\0');
            // call the output hook with all the info regarding this log message.
            log.hook(log.name(), l, log.gd.get_thread_name(), &log.gd.hookbuf.buffer[0]);
        }
    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
//         logger stuff
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    logger::
    logger (  
        const std::string& name_
    ) : 
        gd(get_global_data()),
        logger_name(name_),
        out(gd.output_streambuf(logger_name)),
        cur_level(gd.level(logger_name))
    {
        DLIB_ASSERT(name_[0] != '\0',
                    "\tlogger::logger()"
                    << "\n\tYou can't make a logger with an empty name"
                    << "\n\tthis: " << this
        );

        auto_mutex M(gd.m);
        logger* temp = this;
        gd.loggers.add(temp);

        // load the appropriate settings
        print_header        = gd.logger_header(logger_name);
        auto_flush_enabled  = gd.auto_flush(logger_name);
        hook                = gd.output_hook(logger_name);
    }

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

    logger::
    ~logger (
    ) 
    { 
        gd.m.lock();
        gd.loggers.destroy(this);            
        // if this was the last logger then delete the global data
        if (gd.loggers.size() == 0)
        {
            gd.m.unlock();
            delete &gd;
        }
        else
        {
            gd.m.unlock();
        }
    }

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

}

#endif // DLIB_LOGGER_KERNEL_1_CPp_