/*
 * Copyright (c) 2018, 2025, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * 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, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#ifndef MYSQLSHDK_LIBS_TEXTUI_TEXT_PROGRESS_H_
#define MYSQLSHDK_LIBS_TEXTUI_TEXT_PROGRESS_H_

#include <array>
#include <chrono>
#include <string>

#include "mysqlshdk/libs/textui/progress_attribs.h"

namespace mysqlshdk {
namespace textui {

/**
 * Class calculating estimated time to finish an operation.
 */
class Eta final {
 public:
  Eta() { reset(); }

  Eta(const Eta &) = default;
  Eta(Eta &&) = default;

  Eta &operator=(const Eta &) = default;
  Eta &operator=(Eta &&) = default;

  ~Eta() = default;

  /**
   * Resets the counters.
   */
  inline void reset() noexcept {
    m_started_at = k_time_point_not_set;
    m_initialized = false;
    m_total = 0;
    m_sum = 0.0;
    m_index = 0;
  }

  /**
   * Sets the total value.
   */
  inline void set_total(std::uint64_t total) noexcept {
    // if total is not known or didn't change, there's nothing to do
    if (!total || total == m_total) {
      return;
    }

    // total has changed, reset all the stats, keep the starting time
    const auto started_at = m_started_at;
    reset();
    m_started_at = started_at;

    // finally, set the total value
    m_total = total;
  }

  /**
   * Pushes the current value.
   */
  inline void push(std::uint64_t current) noexcept {
    // if total is not known, there's nothing to do
    if (!m_total) [[unlikely]] {
      return;
    }

    if (k_time_point_not_set == m_started_at) [[unlikely]] {
      m_started_at = std::chrono::steady_clock::now();
    }

    // if progress didn't start, or already has passed the total, there's
    // nothing to do
    if (!current || current > m_total) [[unlikely]] {
      return;
    }

    const auto elapsed_time =
        std::chrono::duration_cast<std::chrono::duration<double>>(
            std::chrono::steady_clock::now() - m_started_at)
            .count();

    // start computing ETA after a while
    if (elapsed_time < 5.0) [[unlikely]] {
      return;
    }

    const auto estimated_time = (m_total * elapsed_time) / current;
    const auto remaining_time = estimated_time - elapsed_time;

    if (!m_initialized) [[unlikely]] {
      // first value, initialize the structure
      m_eta.fill(remaining_time);
      m_sum = remaining_time * k_sliding_window_size;
      m_initialized = true;
    } else {
      // update the sum
      m_sum -= m_eta[m_index];
      m_eta[m_index] = remaining_time;
      m_sum += remaining_time;

      m_index = (m_index + 1) % k_sliding_window_size;
    }
  }

  /**
   * Provides the ETA in seconds.
   */
  inline std::uint64_t eta() const noexcept {
    return m_sum / k_sliding_window_size;
  }

 private:
  using time_point = std::chrono::steady_clock::time_point;

  static constexpr std::uint8_t k_sliding_window_size = 10;
  static constexpr time_point k_time_point_not_set{};

  time_point m_started_at;
  bool m_initialized;

  std::uint64_t m_total;

  // uses sliding window to compute the average of ETAs to smooth out the result
  std::array<double, k_sliding_window_size> m_eta;
  double m_sum;
  std::uint8_t m_index;
};

/**
 * Class calculating throughput based on total bytes transferred.
 */
class Throughput final {
 public:
  Throughput();
  Throughput(const Throughput &other) = default;
  Throughput(Throughput &&other) = default;

  Throughput &operator=(const Throughput &other) = default;
  Throughput &operator=(Throughput &&other) = default;

  ~Throughput() = default;

  /**
   * Store time point with given total bytes
   *
   * @param total_bytes Current value of total bytes transferred.
   */
  void push(uint64_t total_bytes);

  /**
   * Calculate transfer rate using `k_moving_average_points`-point moving
   * average.
   *
   * @return Current transfer rate in bytes per second.
   */
  uint64_t rate() const;

  /**
   * Resets the status.
   */
  void reset();

 private:
  struct Snapshot {
    int64_t bytes = 0;
    std::chrono::steady_clock::time_point time;
  };

  static constexpr const unsigned int k_moving_average_points = 16;
  Snapshot m_history[k_moving_average_points];
  uint16_t m_index = 0;
  uint16_t m_index_prev = 0;
};

inline void Throughput::push(uint64_t total_bytes) {
  const auto current_time = std::chrono::steady_clock::now();
  const auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(
                             current_time - m_history[m_index_prev].time)
                             .count();
  if (time_diff >= 500 /* ms */) {
    if (m_index == m_index_prev) {
      // first insert, fill the whole array
      for (unsigned int i = 0; i < k_moving_average_points; ++i) {
        m_history[i].bytes = total_bytes;
        m_history[i].time = current_time;
      }
    } else {
      m_history[m_index].bytes = total_bytes;
      m_history[m_index].time = current_time;
    }

    m_index_prev = m_index;
    m_index = (m_index + 1) % k_moving_average_points;
  }
}

/**
 * Class that represents No-operation progress.
 */
class IProgress : public Progress_attributes {
 public:
  using Attributes = shcore::Dictionary_t;

  IProgress() = default;

  IProgress(const IProgress &other) = default;
  IProgress(IProgress &&other) = default;

  IProgress &operator=(const IProgress &other) = default;
  IProgress &operator=(IProgress &&other) = default;

  virtual ~IProgress() = default;

  virtual void show_status(bool = false) {}
  virtual void set_current(uint64_t /* value */) {}
  virtual void set_total(uint64_t /* value */) {}
  virtual void set_total(uint64_t /* value */, uint64_t /* initial */) {}
  virtual void set_left_label(const std::string &) {}
  virtual void set_right_label(const std::string &) {}
  virtual void hide(bool) {}
  virtual void shutdown() {}
};

class Base_progress : public IProgress {
 public:
  Base_progress() : Base_progress("bytes", "B", "B", "B", false) {}

  Base_progress(const char *items_full, const char *items_abbrev,
                const char *item_singular, const char *item_plural,
                bool space_before_item = true, bool total_is_approx = false) {
    // this is a virtual call, but that's OK, if something is not initialized
    // by this call, was initialized by fields initializers
    reset(items_full, items_abbrev, item_singular, item_plural,
          space_before_item, total_is_approx);
  }

  Base_progress(const Base_progress &other) = default;
  Base_progress(Base_progress &&other) = default;

  Base_progress &operator=(const Base_progress &other) = default;
  Base_progress &operator=(Base_progress &&other) = default;

  ~Base_progress() override = default;

  /**
   * Update done work value.
   *
   * @param value Absolute value of done work.
   */
  void set_current(uint64_t value) override {
    m_current = value;
    const auto delta = value - m_initial;
    m_throughput.push(delta);
    m_eta.push(delta);
    m_changed = true;
  }

  /**
   * Set total value of work to-do.
   *
   * @param value Absolute value of work to-do.
   */
  void set_total(uint64_t value) override { set_total(value, 0); }

  /**
   * Set total value of work to-do.
   *
   * @param value Absolute value of work to-do.
   * @param initial Initial value for "current"
   */
  void set_total(uint64_t value, uint64_t initial) override {
    m_total = value;
    m_initial = initial;
    m_eta.set_total(value ? value - initial : value);
  }

  void set_left_label(const std::string &label) override {
    m_left_label = label;
  }

  void set_right_label(const std::string &label) override {
    m_right_label = label;
  }

  virtual void reset(const char *items_full, const char *items_abbrev,
                     const char *item_singular, const char *item_plural,
                     bool space_before_item = true,
                     bool total_is_approx = false);

 protected:
  /**
   * Renders progress information status string.
   */
  virtual void render_status() = 0;

  /**
   * Formats current progress.
   */
  std::string format_progress() const;

  /**
   * Formats current value.
   */
  std::string format_current() const;

  /**
   * Formats total value.
   */
  std::string format_total() const;

  /**
   * Formats current throughput.
   */
  inline std::string format_throughput() const {
    return format_throughput(throughput_rate());
  }

  /**
   * Formats throughput as number of items per one second.
   *
   * @param items Number of items in one second.
   */
  std::string format_throughput(uint64_t items) const;

  inline uint64_t current() const noexcept { return m_current; }

  inline uint64_t total() const noexcept { return m_total; }

  inline uint64_t throughput_rate() const noexcept {
    return m_throughput.rate();
  }

  inline uint64_t eta() const noexcept { return m_eta.eta(); }

  std::string m_status;
  std::string m_left_label;
  std::string m_right_label;
  std::chrono::steady_clock::time_point
      m_refresh_clock;  //< Last status refresh
  bool m_changed = true;

 private:
  std::string format_value(uint64_t value) const;

  /**
   * Calculates work done to total work to-do ratio.
   *
   * @return Ratio of work done.
   */
  double ratio() const noexcept {
    return (m_total > 0) ? (static_cast<double>(m_current) / m_total) : 0;
  }

  /**
   * Calculates work done to total work to-do ratio in percent.
   *
   * @return Ratio of work done in percent.
   */
  int percent() const noexcept { return ratio() * 100; }

  uint64_t m_initial = 0;
  uint64_t m_current = 0;
  uint64_t m_total = 0;
  Throughput m_throughput;
  Eta m_eta;
  std::string m_items_full;
  std::string m_items_abbrev;
  std::string m_item_singular;
  std::string m_item_plural;
  bool m_space_before_item = true;
  bool m_total_is_approx = false;
};

/**
 * Class that generates one-line text progress information.
 */
class Text_progress final : public Base_progress {
 public:
  using Base_progress::Base_progress;

  Text_progress(const Text_progress &other) = default;
  Text_progress(Text_progress &&other) = default;

  Text_progress &operator=(const Text_progress &other) = default;
  Text_progress &operator=(Text_progress &&other) = default;

  ~Text_progress() override = default;

  /**
   * Update and print progress information status.
   *
   * @param force If true, force status refresh and print.
   */
  void show_status(bool force = false) override;

  /**
   * Hide progress text.
   *
   * @param flag If true, hides/clears the text, false shows it again.
   */
  void hide(bool flag) override;

  /**
   * Finish progress status display.
   */
  void shutdown() override;

 private:
  void reset(const char *items_full, const char *items_abbrev,
             const char *item_singular, const char *item_plural,
             bool space_before_item, bool total_is_approx) override;

  /**
   * Erase currently visible progress information status.
   */
  void clear_status();

  /**
   * Renders progress information status string.
   */
  void render_status() override;

  unsigned int m_last_status_size = 0;  //< Last displayed status length
  std::string m_prev_status;
  int m_hide = 0;
  bool m_status_shown = false;
};

class Json_progress final : public Base_progress {
 public:
  using Base_progress::Base_progress;

  Json_progress(const Json_progress &other) = default;
  Json_progress(Json_progress &&other) = default;

  Json_progress &operator=(const Json_progress &other) = default;
  Json_progress &operator=(Json_progress &&other) = default;

  ~Json_progress() = default;

  /**
   * Update and print progress information status.
   *
   * @param force If true, force status refresh and print.
   */
  void show_status(bool force = false) override;

 private:
  /**
   * Renders progress information status string.
   */
  void render_status() override;

  void reset(const char *items_full, const char *items_abbrev,
             const char *item_singular, const char *item_plural,
             bool space_before_item, bool total_is_approx) override;

  bool m_status_shown = false;
};

}  // namespace textui
}  // namespace mysqlshdk

#endif  // MYSQLSHDK_LIBS_TEXTUI_TEXT_PROGRESS_H_
