/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/containers/ts_hash_map.h>
#include <mrpt/core/exceptions.h>
#include <mrpt/system/COutputLogger.h>
#include <mrpt/system/CTicTac.h>

#include <deque>
#include <map>
#include <optional>
#include <stack>
#include <vector>

namespace mrpt::system
{
/** A versatile "profiler" that logs the time spent within each pair of calls to
 * enter(X)-leave(X), among other stats.
 *  The results can be dumped to cout or to Visual Studio's output panel.
 * This class can be also used to monitorize min/mean/max/total stats of any
 * user-provided parameters via the method CTimeLogger::registerUserMeasure().
 *
 * Optional recording of **all** data can be enabled via
 * enableKeepWholeHistory() (use with caution!).
 *
 * Cost of the profiler itself (measured on MSVC2015, Windows 10, Intel i5-2310
 * 2.9GHz):
 * - `enter()`: average 445 ns
 * - `leave()`: average 316 ns
 *
 *  Recursive methods are supported with no problems, that is, calling "enter(X)
 * enter(X) ... leave(X) leave(X)".
 *  `enter()`/`leave()` are thread-safe, in the sense of they being safe to be
 * called from different threads. However, calling `enter()`/`leave()` for the
 * same user-supplied "section name", from different threads, is not allowed. In
 * the latter case (and, actually, in general since it's safer against
 * exceptions), use the RAII helper class CTimeLoggerEntry.
 *
 * \sa CTimeLoggerEntry
 *
 * \note The default behavior is dumping all the information at destruction.
 * \ingroup mrpt_system_grp
 */
class CTimeLogger : public mrpt::system::COutputLogger
{
 private:
  CTicTac m_tictac;
  bool m_enabled;
  std::string m_name;
  bool m_keep_whole_history{false};

  //! Data of all the calls:
  struct TCallData
  {
    size_t n_calls{0};
    double min_t{0}, max_t{0}, mean_t{0}, last_t{0};
    std::stack<double, std::vector<double>> open_calls;
    bool has_time_units{true};
    std::optional<std::deque<double>> whole_history{};

    // Each instance holds its own mutex, even after = operations.
    std::mutex mtx;

    TCallData() = default;

    TCallData(const TCallData& d) { *this = d; }
    TCallData(TCallData&& d) { *this = std::move(d); }

    TCallData& operator=(const TCallData& d)
    {
      n_calls = d.n_calls;
      min_t = d.min_t;
      max_t = d.max_t;
      mean_t = d.mean_t;
      last_t = d.last_t;
      open_calls = d.open_calls;
      has_time_units = d.has_time_units;
      whole_history = d.whole_history;
      return *this;
    }
    TCallData& operator=(TCallData&& d)
    {
      n_calls = d.n_calls;
      min_t = d.min_t;
      max_t = d.max_t;
      mean_t = d.mean_t;
      last_t = d.last_t;
      open_calls = std::move(d.open_calls);
      has_time_units = d.has_time_units;
      whole_history = std::move(d.whole_history);
      return *this;
    }
  };

 protected:
  constexpr static unsigned int HASH_SIZE_IN_BYTES = 1;
  constexpr static unsigned int HASH_ALLOWED_COLLISIONS = 10;
  // Note: we CANNOT store a std::string_view here due to literals life scope.
  using TDataMap = mrpt::containers::
      ts_hash_map<std::string, TCallData, HASH_SIZE_IN_BYTES, HASH_ALLOWED_COLLISIONS>;

  TDataMap m_data;

  void do_enter(const std::string_view& func_name) noexcept;
  double do_leave(const std::string_view& func_name) noexcept;

 public:
  /** Data of each call section: # of calls, minimum, maximum, average and
   * overall execution time (in seconds) \sa getStats */
  struct TCallStats
  {
    size_t n_calls{0};
    double min_t{0}, max_t{0}, mean_t{0}, total_t{0}, last_t{0};
  };

  CTimeLogger(
      bool enabled = true, const std::string& name = "", const bool keep_whole_history = false);
  /** Destructor */
  ~CTimeLogger() override;

  // We must define these 4 because of the definition of a virtual dtor
  // (compiler will not generate the defaults)
  CTimeLogger(const CTimeLogger& o) = default;
  CTimeLogger& operator=(const CTimeLogger& o) = default;
  CTimeLogger(CTimeLogger&& o) = default;
  CTimeLogger& operator=(CTimeLogger&& o) = default;

  /** Dump all stats to a multi-line text string. \sa dumpAllStats,
   * saveToCVSFile */
  std::string getStatsAsText(size_t column_width = 80) const;
  /** Returns all the current stats as a map: section_name => stats. \sa
   * getStatsAsText, dumpAllStats, saveToCVSFile */
  void getStats(std::map<std::string, TCallStats>& out_stats) const;
  /** Dump all stats through the COutputLogger interface. \sa getStatsAsText,
   * saveToCVSFile */
  void dumpAllStats(size_t column_width = 80) const;

  /** Resets all stats. By default (deep_clear=false), all section names are
   * remembered (not freed) so the cost of creating upon the first next call
   * is avoided.
   *
   * \note By design, calling this method is the only one which is not thread
   * safe. It's not made thread-safe to save the performance cost. Please,
   * ensure that you call `clear()` only while there are no other threads
   * registering annotations in the object.
   */
  void clear(bool deep_clear = false);

  void enable(bool enabled = true) { m_enabled = enabled; }
  void disable() { m_enabled = false; }
  bool isEnabled() const { return m_enabled; }

  void enableKeepWholeHistory(bool enable = true) { m_keep_whole_history = enable; }
  bool isEnabledKeepWholeHistory() const { return m_keep_whole_history; }

  /** Dump all stats to a Comma Separated Values (CSV) file. \sa dumpAllStats
   */
  void saveToCSVFile(const std::string& csv_file) const;
  /** Dump all stats to a Matlab/Octave (.m) file. \sa dumpAllStats */
  void saveToMFile(const std::string& m_file) const;
  void registerUserMeasure(
      const std::string_view& event_name, const double value, const bool is_time = false) noexcept;

  const std::string& getName() const noexcept { return m_name; }
  void setName(const std::string& name) noexcept;

  /** Start of a named section \sa enter */
  inline void enter(const std::string_view& func_name) noexcept
  {
    if (m_enabled) do_enter(func_name);
  }
  /** End of a named section \return The ellapsed time, in seconds or 0 if
   * disabled. \sa enter */
  inline double leave(const std::string_view& func_name) noexcept
  {
    return m_enabled ? do_leave(func_name) : 0;
  }
  /** Return the mean execution time of the given "section", or 0 if it hasn't
   * ever been called "enter" with that section name */
  double getMeanTime(const std::string& name) const;
  /** Return the last execution time of the given "section", or 0 if it hasn't
   * ever been called "enter" with that section name */
  double getLastTime(const std::string& name) const;

  friend struct CTimeLoggerEntry;
};  // End of class def.

/** A safe way to call enter() and leave() of a mrpt::system::CTimeLogger upon
 * construction and destruction of
 * this auxiliary object, making sure that leave() will be called upon
 * exceptions, etc.
 * Usage mode #1 (scoped):
 * \code
 *    CTimeLogger logger;
 *    // ...
 *    { // Start of scope to be monitorized
 *       CTimeLoggerEntry tle(logger,"operation-name");
 *
 *       // do whatever
 *
 *    } // End of scope
 *    // **DO NOT** call tle.stop() explicitly here, it's called by its dtor
 * \endcode
 *
 * Usage mode #2 (unscoped):
 * \code
 *    CTimeLogger logger;
 *    // ...
 *
 *    CTimeLoggerEntry tle(logger,"operation-name");
 *    // do whatever
 *    tle.stop();
 *
 *    // tle dtor does nothing else, since you already called stop()
 * \endcode
 *
 * \ingroup mrpt_system_grp
 */
struct CTimeLoggerEntry
{
  CTimeLoggerEntry(const CTimeLogger& logger, const std::string_view& section_name);
  ~CTimeLoggerEntry();
  CTimeLogger& m_logger;
  void stop();  //!< for correct use, see docs for CTimeLoggerEntry

 private:
  // Note we cannot store the string_view since we have no guarantees of the
  // life-time of the provided string buffer.
  const std::string m_section_name;
  double m_entry = 0;
  bool stopped_{false};
};

/** A helper class to save CSV stats upon self destruction, for example, at the
 * end of a program run. The target file will be named after timelogger's name.
 * \ingroup mrpt_system_grp
 */
struct CTimeLoggerSaveAtDtor
{
  mrpt::system::CTimeLogger& m_tm;

  CTimeLoggerSaveAtDtor(mrpt::system::CTimeLogger& tm) : m_tm(tm) {}
  ~CTimeLoggerSaveAtDtor();
};

/** @name Auxiliary stuff for the global profiler used in MRPT_START / MRPT_END
  macros.
  @{ */
void global_profiler_enter(const char* func_name) noexcept;
void global_profiler_leave(const char* func_name) noexcept;
mrpt::system::CTimeLogger& global_profiler_getref() noexcept;
/** @} */

}  // namespace mrpt::system
