//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2019 James D. Mitchell
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

// This file contains a class for reporting things during a computation.

#ifndef LIBSEMIGROUPS_REPORT_HPP_
#define LIBSEMIGROUPS_REPORT_HPP_

#include <cxxabi.h>  // for abi::

#include <atomic>         // for atomic
#include <cstddef>        // for size_t
#include <cstdint>        // for uint64_t
#include <cstdlib>        // for free
#include <iosfwd>         // for string, operator<<, cout, ost...
#include <mutex>          // for mutex, lock_guard
#include <string>         // for string
#include <thread>         // for get_id, thread, thread::id
#include <unordered_map>  // for unordered_map
#include <unordered_set>  // for unordered_set
#include <utility>        // for pair
#include <vector>         // for vector

#include "config.hpp"  // for LIBSEMIGROUPS_FMT_ENABLED
#include "string.hpp"  // for string_format, to_string

#include "textflowcpp/TextFlow.hpp"

#ifdef LIBSEMIGROUPS_FMT_ENABLED
#include "fmt/color.h"
#include "fmt/printf.h"
#include <array>  // for array
#endif

// To avoid computing the parameters __VA_ARGS__ when we aren't even
// reporting, we check if we are reporting before calling REPORTER.
#define REPORT(...) \
  (REPORTER.report() ? REPORTER(__VA_ARGS__).prefix(this) : REPORTER)

#ifdef LIBSEMIGROUPS_FMT_ENABLED
#define REPORT_DEFAULT_V3(...) \
  REPORTER(__VA_ARGS__).prefix(nullptr).thread_color().flush()
#else
#define REPORT_DEFAULT_V3(...) REPORTER(__VA_ARGS__).prefix(nullptr).flush();
#endif

#ifdef LIBSEMIGROUPS_FMT_ENABLED
#define REPORT_DEFAULT(...) REPORT(__VA_ARGS__).thread_color().flush();
#else
#define REPORT_DEFAULT(...) REPORT(__VA_ARGS__).flush();
#endif

#ifdef LIBSEMIGROUPS_DEBUG
#ifdef LIBSEMIGROUPS_FMT_ENABLED
#define REPORT_DEBUG(...) REPORT(__VA_ARGS__).color(fmt::color::dim_gray)
#else
#define REPORT_DEBUG(...) REPORT(__VA_ARGS__)
#endif
#define REPORT_DEBUG_DEFAULT(...) REPORT_DEBUG(__VA_ARGS__).flush();
#else
#define REPORT_DEBUG(...)
#define REPORT_DEBUG_DEFAULT(...)
#endif

#ifdef LIBSEMIGROUPS_VERBOSE
#ifdef LIBSEMIGROUPS_FMT_ENABLED
#define REPORT_VERBOSE(...) REPORT(__VA_ARGS__).color(fmt::color::cyan)
#else
#define REPORT_VERBOSE(...) REPORT(__VA_ARGS__)
#endif
#define REPORT_VERBOSE_DEFAULT(...) REPORT_VERBOSE(__VA_ARGS__).flush();
#else
#define REPORT_VERBOSE(...)
#define REPORT_VERBOSE_DEFAULT(...)
#endif

#define REPORT_TIME(var) \
  REPORT_DEFAULT("elapsed time (%s): %s\n", __func__, var.string().c_str());

namespace libsemigroups {
#ifndef LIBSEMIGROUPS_FMT_ENABLED
#define FORMAT(...) __VA_ARGS__
#else
#define FORMAT(...) fmt::format(__VA_ARGS__)
#endif

  namespace detail {

    class ThreadIdManager final {
     public:
      ThreadIdManager();
      ThreadIdManager(ThreadIdManager const&)            = delete;
      ThreadIdManager(ThreadIdManager&&)                 = delete;
      ThreadIdManager& operator=(ThreadIdManager const&) = delete;
      ThreadIdManager& operator=(ThreadIdManager&&)      = delete;

      void   reset();
      size_t tid(std::thread::id t);

     private:
      std::mutex                                  _mtx;
      size_t                                      _next_tid;
      std::unordered_map<std::thread::id, size_t> _thread_map;
    };
  }  // namespace detail

  extern detail::ThreadIdManager THREAD_ID_MANAGER;

  namespace detail {
    template <typename T>
    static std::string string_class_name(T const* o) {
      static std::unordered_map<size_t, std::string> _class_name_map;
      auto it = _class_name_map.find(typeid(*o).hash_code());
      if (it != _class_name_map.end()) {
        return (*it).second;
      }
      int         status;
      char*       ptr = abi::__cxa_demangle(typeid(*o).name(), 0, 0, &status);
      std::string out = "";
      if (status == 0) {  // successfully demangled
        std::string full = std::string(ptr);
        size_t      last = full.size();
        if (full.back() == '>') {
          size_t bracket_count = 0;
          do {
            last = full.find_last_of("<>", last - 1);
            if (last != std::string::npos) {
              if (full.at(last) == '>') {
                bracket_count++;
              } else if (full.at(last) == '<') {
                bracket_count--;
              }
            }
          } while (bracket_count != 0);
        }
        size_t first = full.rfind("::", last - 1);
        first        = (first == std::string::npos ? 0 : first + 2);
        out          = full.substr(first, last - first);
      }
      free(ptr);
      // If we couldn't demangle the name, then just add the empty string to
      // the map
      _class_name_map.emplace(typeid(*o).hash_code(), out);
      return out;
    }

#ifdef LIBSEMIGROUPS_FMT_ENABLED
    static std::array<fmt::color, 146> const thread_colors
        = {fmt::color::white,
           fmt::color::red,
           fmt::color::green,
           fmt::color::blue,
           fmt::color::yellow,
           fmt::color::aquamarine,
           fmt::color::antique_white,
           fmt::color::aqua,
           fmt::color::azure,
           fmt::color::beige,
           fmt::color::bisque,
           fmt::color::blanched_almond,
           fmt::color::blue_violet,
           fmt::color::brown,
           fmt::color::burly_wood,
           fmt::color::cadet_blue,
           fmt::color::chartreuse,
           fmt::color::chocolate,
           fmt::color::coral,
           fmt::color::cornflower_blue,
           fmt::color::cornsilk,
           fmt::color::crimson,
           fmt::color::cyan,
           fmt::color::dark_blue,
           fmt::color::dark_cyan,
           fmt::color::dark_golden_rod,
           fmt::color::dark_gray,
           fmt::color::dark_green,
           fmt::color::dark_khaki,
           fmt::color::dark_magenta,
           fmt::color::dark_olive_green,
           fmt::color::dark_orange,
           fmt::color::dark_orchid,
           fmt::color::dark_red,
           fmt::color::dark_salmon,
           fmt::color::dark_sea_green,
           fmt::color::dark_slate_blue,
           fmt::color::dark_slate_gray,
           fmt::color::dark_turquoise,
           fmt::color::dark_violet,
           fmt::color::deep_pink,
           fmt::color::deep_sky_blue,
           fmt::color::dim_gray,
           fmt::color::dodger_blue,
           fmt::color::fire_brick,
           fmt::color::floral_white,
           fmt::color::forest_green,
           fmt::color::fuchsia,
           fmt::color::gainsboro,
           fmt::color::ghost_white,
           fmt::color::gold,
           fmt::color::golden_rod,
           fmt::color::gray,
           fmt::color::green_yellow,
           fmt::color::honey_dew,
           fmt::color::hot_pink,
           fmt::color::indian_red,
           fmt::color::indigo,
           fmt::color::ivory,
           fmt::color::khaki,
           fmt::color::lavender,
           fmt::color::lavender_blush,
           fmt::color::lawn_green,
           fmt::color::lemon_chiffon,
           fmt::color::light_blue,
           fmt::color::light_coral,
           fmt::color::light_cyan,
           fmt::color::light_golden_rod_yellow,
           fmt::color::light_gray,
           fmt::color::light_green,
           fmt::color::light_pink,
           fmt::color::light_salmon,
           fmt::color::light_sea_green,
           fmt::color::light_sky_blue,
           fmt::color::light_slate_gray,
           fmt::color::light_steel_blue,
           fmt::color::light_yellow,
           fmt::color::lime,
           fmt::color::lime_green,
           fmt::color::linen,
           fmt::color::magenta,
           fmt::color::maroon,
           fmt::color::medium_aquamarine,
           fmt::color::medium_blue,
           fmt::color::medium_orchid,
           fmt::color::medium_purple,
           fmt::color::medium_sea_green,
           fmt::color::medium_slate_blue,
           fmt::color::medium_spring_green,
           fmt::color::medium_turquoise,
           fmt::color::medium_violet_red,
           fmt::color::midnight_blue,
           fmt::color::mint_cream,
           fmt::color::misty_rose,
           fmt::color::moccasin,
           fmt::color::navajo_white,
           fmt::color::navy,
           fmt::color::old_lace,
           fmt::color::olive,
           fmt::color::olive_drab,
           fmt::color::orange,
           fmt::color::orange_red,
           fmt::color::orchid,
           fmt::color::pale_golden_rod,
           fmt::color::pale_green,
           fmt::color::pale_turquoise,
           fmt::color::pale_violet_red,
           fmt::color::papaya_whip,
           fmt::color::peach_puff,
           fmt::color::peru,
           fmt::color::pink,
           fmt::color::plum,
           fmt::color::powder_blue,
           fmt::color::purple,
           fmt::color::rebecca_purple,
           fmt::color::rosy_brown,
           fmt::color::royal_blue,
           fmt::color::saddle_brown,
           fmt::color::salmon,
           fmt::color::sandy_brown,
           fmt::color::sea_green,
           fmt::color::sea_shell,
           fmt::color::sienna,
           fmt::color::silver,
           fmt::color::sky_blue,
           fmt::color::slate_blue,
           fmt::color::slate_gray,
           fmt::color::snow,
           fmt::color::spring_green,
           fmt::color::steel_blue,
           fmt::color::tan,
           fmt::color::teal,
           fmt::color::thistle,
           fmt::color::tomato,
           fmt::color::turquoise,
           fmt::color::violet,
           fmt::color::wheat,
           fmt::color::white_smoke,
           fmt::color::yellow_green};
#endif

    class Reporter final {
     public:
      explicit Reporter(bool report = true);

      Reporter(Reporter const&)            = delete;
      Reporter(Reporter&&)                 = delete;
      Reporter& operator=(Reporter const&) = delete;
      Reporter& operator=(Reporter&&)      = delete;

      template <typename TClass>
      Reporter& prefix(TClass const* const ptr, bool override = false) {
        if (_report || override) {
          std::lock_guard<std::mutex> lg(_mtx);
          uint64_t tid = THREAD_ID_MANAGER.tid(std::this_thread::get_id());
          resize(tid + 1);
          _options[tid].prefix
#ifdef LIBSEMIGROUPS_FMT_ENABLED
              = fmt::sprintf("#%llu: ", tid);
#else
              = string_format("#%llu: ", tid);
#endif
          if (ptr != nullptr) {
            _options[tid].prefix +=
#ifdef LIBSEMIGROUPS_FMT_ENABLED
                fmt::sprintf("%s: ", string_class_name(ptr));
#else
                string_format("%s: ", string_class_name(ptr).c_str());
#endif
          }
        }
        return *this;
      }

#ifdef LIBSEMIGROUPS_FMT_ENABLED
      Reporter& color(fmt::color);
      Reporter& thread_color();
#endif
      Reporter& prefix();

      Reporter& prefix(std::nullptr_t) {
        if (_report) {
          std::lock_guard<std::mutex> lg(_mtx);
          uint64_t tid = THREAD_ID_MANAGER.tid(std::this_thread::get_id());
          resize(tid + 1);
          _options[tid].prefix
#ifdef LIBSEMIGROUPS_FMT_ENABLED
              = fmt::sprintf("#%llu: ", tid);
#else
              = string_format("#%llu: ", tid);
#endif
        }
        return *this;
      }

      std::string get_prefix() const noexcept {
        uint64_t tid = THREAD_ID_MANAGER.tid(std::this_thread::get_id());
        return _options[tid].prefix;
      }

      Reporter& flush_right();

      void flush();

      void report(bool val) {
        _report = val;
      }

      bool report() const noexcept {
        return _report;
      }

      template <typename... TArgs>
      Reporter& operator()(TArgs... args) {
        if (_report) {
          std::lock_guard<std::mutex> lg(_mtx);
          size_t tid = THREAD_ID_MANAGER.tid(std::this_thread::get_id());
          if (tid >= _last_msg.size()) {
            resize(tid + 1);
          }
          _last_msg[tid] = _msg[tid];

#ifdef LIBSEMIGROUPS_FMT_ENABLED
          _msg[tid] = fmt::sprintf(args...);
          color(thread_colors[tid % thread_colors.size()]);
#else
          _msg[tid] = string_format(args...);
#endif
        }
        return *this;
      }

      Reporter& suppress(std::string const& class_name) {
        _suppressions.insert(class_name);
        return *this;
      }

      Reporter& clear_suppressions() {
        _suppressions.clear();
        return *this;
      }

     private:
      void resize(size_t);

      struct Options {
        Options()
            :
#ifdef LIBSEMIGROUPS_FMT_ENABLED
              color(fmt::color::alice_blue),
#endif
              flush_right(false),
              prefix() {
        }
#ifdef LIBSEMIGROUPS_FMT_ENABLED
        fmt::color color;
#endif
        bool        flush_right;
        std::string prefix;
      };
      std::vector<std::string>        _last_msg;
      std::mutex                      _mtx;
      std::vector<std::string>        _msg;
      std::vector<Options>            _options;
      std::atomic<bool>               _report;
      std::unordered_set<std::string> _suppressions;
    };
  }  // namespace detail

  extern detail::Reporter REPORTER;

  namespace detail {
    class PrintTable {
     public:
      explicit PrintTable(size_t width = 72)
          : _rows(), _header(), _footer(), _width(width) {}

      ~PrintTable();

      std::string emit() {
        std::string result = lineohash() + _header + lineohash();
        for (auto const& row : _rows) {
          size_t n = 0;
          // Check if we contain a \infty
          if (!row.second.empty()) {
            if (row.second.find("\u221E") == std::string::npos) {
              n = _width - row.first.size() - row.second.size();
            } else {
              n = _width - row.first.size() - row.second.size() + 2;
            }
          }
          result += row.first + std::string(n, ' ') + row.second + "\n";
        }
        result += lineohash() + _footer + lineohash();
        return result;
      }

      void header(char const* text) {
        _header = wrap(text);
        _header += "\n";
      }

      void footer(char const* text) {
        _footer = wrap(text);
        _footer += "\n";
      }

      void divider() {
        _rows.emplace_back(lineohash(false), "");
      }

      void operator()(char const* col0) {
        _rows.emplace_back(wrap(col0), std::string());
      }

      template <typename T>
      void operator()(char const* col0, T const& col1) {
        _rows.emplace_back(col0, detail::to_string(col1));
      }

      template <typename T>
      void operator()(char const* col0,
                      char const* col1_format,
                      T const&    col1) {
        _rows.emplace_back(col0, string_format(col1_format, col1));
      }

     private:
      std::string wrap(char const* text) {
        return TextFlow::Column(text).width(_width).toString();
      }

      std::string lineohash(bool newline = true) {
        return std::string(_width, '#') + (newline ? "\n" : "");
      }

      std::vector<std::pair<std::string, std::string>> _rows;
      std::string                                      _header;
      std::string                                      _footer;
      size_t                                           _width;
    };
  }  // namespace detail

  namespace report {
    bool should_report() noexcept;
    void suppress(std::string const&);
    void clear_suppressions();
  }  // namespace report

  //! This struct can be used to enable printing of some information during
  //! various of the computation in ``libsemigroups``. Reporting is enable (or
  //! not) at construction time, and disable when the ReportGuard goes out of
  //! scope.
  struct ReportGuard {
    //! Constructs a ReportGuard with reporting enabled by default.
    //!
    //! \param val whether to report or not (default: \c true).
    explicit ReportGuard(bool val = true) {
      REPORTER.report(val);
    }

    ~ReportGuard() {
      REPORTER.report(false);
    }
  };

}  // namespace libsemigroups
#endif  // LIBSEMIGROUPS_REPORT_HPP_
