/*
 * 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
 */

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

#ifdef _WIN32
#include <io.h>
#define fileno _fileno
#define write _write
#else
#include <unistd.h>
#endif

#include <cassert>
#include <chrono>
#include <utility>

#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/textui/term_vt100.h"
#include "mysqlshdk/libs/utils/strformat.h"

namespace mysqlshdk {
namespace textui {

namespace {

constexpr auto k_throughput_progress_config = "throughputProgressConfig";

}  // namespace

Throughput::Throughput() {
  for (auto &s : m_history) {
    s.bytes = 0;
    s.time = {};
  }
}

uint64_t Throughput::rate() const {
  const auto &current_point =
      m_history[(m_index + k_moving_average_points - 1) %
                k_moving_average_points];
  const auto &past_point = m_history[(m_index + 1) % k_moving_average_points];

  const auto bytes_diff = current_point.bytes - past_point.bytes;
  const auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(
                             current_point.time - past_point.time)
                             .count();
  if (time_diff == 0) {
    return bytes_diff * 1000;
  }
  return bytes_diff * 1000 / time_diff;  // bytes per second
}

void Throughput::reset() {
  m_index = 0;
  m_index_prev = 0;

  for (unsigned int i = 0; i < k_moving_average_points; ++i) {
    m_history[i].bytes = 0;
    m_history[i].time = {};
  }
}

void Base_progress::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) {
  m_initial = 0;
  m_current = 0;
  m_total = 0;
  m_changed = true;
  m_throughput.reset();
  m_eta.reset();
  m_status = std::string(80, ' ');
  m_refresh_clock = {};
  m_items_full = items_full;
  m_items_abbrev = items_abbrev;
  m_item_singular = item_singular;
  m_item_plural = item_plural;
  m_left_label.clear();
  m_right_label.clear();
  m_space_before_item = space_before_item;
  m_total_is_approx = total_is_approx;
}

std::string Base_progress::format_progress() const {
  return (0 == m_total ? "?" : std::to_string(percent())) + "%";
}

std::string Base_progress::format_current() const {
  return format_value(m_current);
}

std::string Base_progress::format_total() const {
  return 0 == m_total ? "?"
                      : (m_total_is_approx ? "~" : "") + format_value(m_total);
}

std::string Base_progress::format_throughput(uint64_t items) const {
  return mysqlshdk::utils::format_throughput_items(
      m_item_singular, m_item_plural, items, 1.0, m_space_before_item);
}

std::string Base_progress::format_value(uint64_t value) const {
  return mysqlshdk::utils::format_items(m_items_full, m_items_abbrev, value,
                                        m_space_before_item);
}

void Text_progress::show_status(bool force) {
  if (m_hide != 0) return;

  const auto current_time = std::chrono::steady_clock::now();
  const auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(
                             current_time - m_refresh_clock)
                             .count();
  const bool refresh_timeout = time_diff >= 200;  // update status every 200ms
  if ((refresh_timeout && m_changed) || force) {
    m_prev_status = std::move(m_status);

    render_status();

    m_changed = false;
    const auto status_printable_size =
        m_status.size() - 1;  // -1 because of leading '\r'

    if (status_printable_size < m_last_status_size) {
      clear_status();
    }

    if (force || m_prev_status != m_status) {
      [[maybe_unused]] const auto _ =
          write(fileno(stderr), &m_status[0], m_status.size());
    }

    m_status_shown = true;
    m_refresh_clock = current_time;
    m_last_status_size = status_printable_size;
  }
}

void Text_progress::hide(bool flag) {
  if (flag) {
    if (m_hide == 0 && m_status_shown) clear_status();
    m_hide++;
  } else {
    m_hide--;
    if (m_hide == 0 && m_status_shown) show_status(false);
  }
  assert(m_hide >= 0);
}

void Text_progress::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) {
  Base_progress::reset(items_full, items_abbrev, item_singular, item_plural,
                       space_before_item, total_is_approx);

  m_last_status_size = 0;
  m_prev_status.clear();
  m_status_shown = false;
  // do not reset m_hide, if we interleave reset() with hide(true) and
  // hide(false), we're going to hit the assert
}

void Text_progress::clear_status() {
  std::string space(m_last_status_size + 2, ' ');
  space[0] = '\r';
  space[m_last_status_size + 1] = '\r';
  {
    const auto ignore = write(fileno(stderr), &space[0], space.size());
    (void)ignore;
  }
}

void Text_progress::shutdown() {
  [[maybe_unused]] const auto _ = write(fileno(stderr), "\n", 1);
  m_status_shown = false;
}

void Text_progress::render_status() {
  // 100% (1024.00 MB / 1024.00 MB), 1024.00 MB/s
  m_status.clear();
  m_status += "\r";
  m_status += m_left_label;
  m_status += format_progress() + " (" + format_current() + " / " +
              format_total() + "), " + format_throughput();
  m_status += m_right_label;
}

void Json_progress::show_status(bool force) {
  const auto current_time = std::chrono::steady_clock::now();
  const auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(
                             current_time - m_refresh_clock)
                             .count();
  const bool refresh_timeout = time_diff >= 1000;  // update status every 1s
  if ((refresh_timeout && m_changed) || force) {
    render_status();
    m_changed = false;
    mysqlsh::current_console()->raw_print(
        m_status, mysqlsh::Output_stream::STDOUT, true, extra_attributes());
    m_refresh_clock = current_time;

    if (!m_status_shown) {
      // display config only once
      extra_attributes()->erase(k_throughput_progress_config);

      m_status_shown = true;
    }
  }
}

void Json_progress::render_status() {
  const auto rate = throughput_rate();

  auto progress_update = shcore::make_dict();
  progress_update->emplace("current", current());
  progress_update->emplace("total", total());
  progress_update->emplace("throughput", format_json_throughput(rate, 1.0));
  progress_update->emplace("etaSeconds", eta());

  extra_attributes()->set("throughputProgressUpdate",
                          shcore::Value{std::move(progress_update)});

  // 100% (1024.00 MB / 1024.00 MB), 1024.00 MB/s
  m_status.clear();
  m_status += m_left_label;
  m_status += format_progress() + " (" + format_current() + " / " +
              format_total() + "), " + format_throughput(rate);
  m_status += m_right_label;
}

void Json_progress::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) {
  Base_progress::reset(items_full, items_abbrev, item_singular, item_plural,
                       space_before_item, total_is_approx);

  m_status_shown = false;

  auto progress_config = shcore::make_dict();
  progress_config->emplace("itemsFull", items_full);
  progress_config->emplace("itemsAbbreviated", items_abbrev);
  progress_config->emplace("itemSingular", item_singular);
  progress_config->emplace("itemPlural", item_plural);
  progress_config->emplace("spaceBeforeItem", space_before_item);
  progress_config->emplace("totalIsApproximate", total_is_approx);

  set_extra_attributes(shcore::make_dict(k_throughput_progress_config,
                                         std::move(progress_config)));
}

}  // namespace textui
}  // namespace mysqlshdk
