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

#include <cmath>
#include <chrono>
#include <limits>
#include <iostream>

namespace dlib
{

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

    class console_progress_indicator
    {
        /*!
            WHAT THIS OBJECT REPRESENTS
                This object is a tool for reporting how long a task will take
                to complete.  

                For example, consider the following bit of code:

                    console_progress_indicator pbar(100)
                    for (int i = 1; i <= 100; ++i)
                    {
                        pbar.print_status(i);
                        long_running_operation();
                    }

                The above code will print a message to the console each iteration
                which shows the current progress and how much time is remaining until
                the loop terminates.
        !*/

    public:

        inline explicit console_progress_indicator (
            double target_value 
        ); 
        /*!
            ensures
                - #target() == target_value
        !*/

        inline void reset (
            double target_value
        );
        /*!
            ensures
                - #target() == target_value
                - performs the equivalent of:
                    *this = console_progress_indicator(target_value)
                    (i.e. resets this object with a new target value)

        !*/

        inline double target (
        ) const;
        /*!
            ensures
                - This object attempts to measure how much time is
                  left until we reach a certain targeted value.  This
                  function returns that targeted value.
        !*/

        inline bool print_status (
            double cur,
            bool always_print = false,
            std::ostream& out = std::clog
        );
        /*!
            ensures
                - print_status() assumes it is called with values which are linearly
                  approaching target().  It will display the current progress and attempt
                  to predict how much time is remaining until cur becomes equal to target().
                - prints a status message to out which indicates how much more time is
                  left until cur is equal to target()
                - if (always_print) then
                    - This function prints to the screen each time it is called.
                - else
                    - This function throttles the printing so that at most 1 message is
                      printed each second.  Note that it won't print anything to the screen
                      until about one second has elapsed.  This means that the first call
                      to print_status() never prints to the screen.
                - This function returns true if it prints to the screen and false
                  otherwise.
        !*/

        inline void finish (
            std::ostream& out = std::cout
        ) const;
        /*!
            ensures
                - This object prints the completed progress and the elapsed time to out.
                  It is meant to be called after the loop we are tracking the progress of.
        !*/

    private:

        double target_val;
        std::chrono::time_point<std::chrono::steady_clock> start_time;
        double first_val;
        double seen_first_val;
        std::chrono::time_point<std::chrono::steady_clock> last_time;

    };

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
//                               IMPLEMENTATION DETAILS
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    console_progress_indicator::
    console_progress_indicator (
        double target_value 
    ) :
        target_val(target_value),
        start_time(std::chrono::steady_clock::now()),
        first_val(0),
        seen_first_val(false),
        last_time(std::chrono::steady_clock::now())
    {
    }

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

    bool console_progress_indicator::
    print_status (
        double cur,
        bool always_print,
        std::ostream& out
    )
    {
        const auto cur_time = std::chrono::steady_clock::now();

        // if this is the first time print_status has been called
        // then collect some information and exit.  We will print status
        // on the next call.
        if (!seen_first_val)
        {
            start_time = cur_time;
            last_time = cur_time;
            first_val = cur;
            seen_first_val = true;
            return false;
        }

        if ((cur_time - last_time) >= std::chrono::seconds(1) || always_print)
        {
            last_time = cur_time;
            const auto delta_t = cur_time - start_time;
            double delta_val = std::abs(cur - first_val);

            // don't do anything if cur is equal to first_val
            if (delta_val < std::numeric_limits<double>::epsilon())
                return false;

            const auto rem_time = delta_t / delta_val * std::abs(target_val - cur);

            const auto oldflags = out.flags();
            out.setf(std::ios::fixed,std::ios::floatfield);
            std::streamsize ss;

            // adapt the precision based on whether the target val is an integer
            if (std::trunc(target_val) == target_val)
                ss = out.precision(0);
            else
                ss = out.precision(2);

            out << "Progress: " << cur << "/" << target_val;
            ss = out.precision(2);
            out << " (" << cur / target_val * 100. << "%). ";

            const auto hours = std::chrono::duration_cast<std::chrono::hours>(rem_time);
            const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(rem_time) - hours;
            const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(rem_time) - hours - minutes;
            out << "Time remaining: ";
            if (rem_time >= std::chrono::hours(1))
                out << hours.count() << "h ";
            if (rem_time >= std::chrono::minutes(1))
                out << minutes.count() << "min ";
            out << seconds.count() << "s.                \r" << std::flush;

            // restore previous output flags and precision settings
            out.flags(oldflags);
            out.precision(ss);

            return true;
        }

        return false;
    }

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

    double console_progress_indicator::
    target (
    ) const
    {
        return target_val;
    }

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

    void console_progress_indicator::
    reset (
        double target_value
    ) 
    {
        *this = console_progress_indicator(target_value);
    }

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

    void console_progress_indicator::
    finish (
        std::ostream& out
    ) const
    {
        const auto oldflags = out.flags();
        out.setf(std::ios::fixed,std::ios::floatfield);
        std::streamsize ss;

        // adapt the precision based on whether the target val is an integer
        if (std::trunc(target_val) == target_val)
            ss = out.precision(0);
        else
            ss = out.precision(2);

        out << "Progress: " << target_val << "/" << target_val;
        out << " (100.00%). ";
        const auto delta_t = std::chrono::steady_clock::now() - start_time;
        const auto hours = std::chrono::duration_cast<std::chrono::hours>(delta_t);
        const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(delta_t) - hours;
        const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(delta_t) - hours - minutes;
        out << "Time elapsed: ";
        if (delta_t >= std::chrono::hours(1))
            out << hours.count() << "h ";
        if (delta_t >= std::chrono::minutes(1))
            out << minutes.count() << "min ";
        out << seconds.count() << "s.                " << std::endl;

        // restore previous output flags and precision settings
        out.flags(oldflags);
        out.precision(ss);
    }

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

}

#endif // DLIB_CONSOLE_PROGRESS_INDiCATOR_Hh_