#include "torlinkc/util/format.hpp"

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <map>
#include <regex>
#include <vector>

namespace torlinkc {

std::string formatBytes(double bytes) {
  if (!std::isfinite(bytes) || bytes <= 0) return "0 B";
  static const char* units[] = {"B", "KB", "MB", "GB", "TB"};
  int i = 0;
  while (bytes >= 1024.0 && i < 4) {
    bytes /= 1024.0;
    i++;
  }
  char buf[64];
  std::snprintf(buf, sizeof(buf), i == 0 ? "%.0f %s" : "%.2f %s", bytes, units[i]);
  return buf;
}

std::string formatCount(double n) {
  if (!std::isfinite(n) || n <= 0) return "0";
  if (n < 10000) return std::to_string(static_cast<long long>(std::lround(n)));
  const double k = std::round(n / 1000.0);
  if (k < 1000) return std::to_string(static_cast<long long>(k)) + "k";
  const double m = n / 1'000'000.0;
  char buf[32];
  if (m < 10) {
    std::snprintf(buf, sizeof(buf), "%.1f", m);
    std::string s = buf;
    if (s.size() >= 2 && s[s.size() - 2] == '.' && s.back() == '0') s.resize(s.size() - 2);
    return s + "m";
  }
  std::snprintf(buf, sizeof(buf), "%.0fm", std::round(m));
  return buf;
}

std::int64_t parseSize(const std::string& s) {
  static const std::map<std::string, double> kUnits = {
      {"B", 1}, {"KIB", 1024}, {"MIB", 1024.0 * 1024}, {"GIB", 1024.0 * 1024 * 1024},
      {"TIB", 1024.0 * 1024 * 1024 * 1024}, {"KB", 1000}, {"MB", 1e6}, {"GB", 1e9}, {"TB", 1e12},
  };
  static const std::regex kSizeRe(R"(([\d.]+)\s*([KMGT]?I?B))", std::regex::icase);
  std::smatch m;
  if (!std::regex_search(s, m, kSizeRe)) return 0;

  std::string unit = m[2].str();
  std::transform(unit.begin(), unit.end(), unit.begin(), [](unsigned char c) { return std::toupper(c); });
  double multiplier = 1;
  if (auto it = kUnits.find(unit); it != kUnits.end()) multiplier = it->second;

  try {
    return static_cast<std::int64_t>(std::llround(std::stod(m[1].str()) * multiplier));
  } catch (...) {
    return 0;
  }
}

std::string stripControl(const std::string& s) {
  std::string out;
  out.reserve(s.size());
  std::size_t i = 0;
  while (i < s.size()) {
    const unsigned char c = static_cast<unsigned char>(s[i]);
    std::size_t len = 1;
    if ((c & 0xE0) == 0xC0) len = 2;
    else if ((c & 0xF0) == 0xE0) len = 3;
    else if ((c & 0xF8) == 0xF0) len = 4;
    len = std::min(len, s.size() - i);
    if (len > 1) {
      // A valid multi-byte lead byte is always >= 0xC2, outside every
      // control range below -- only a lone byte can be one, so a whole
      // sequence is always kept together, never inspected byte-by-byte
      // (which would treat a continuation byte in 0x80-0x9F as a C1
      // control and corrupt the encoding -- the bug this used to have).
      out.append(s, i, len);
    } else {
      const bool isControl = c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f);
      if (!isControl) out += static_cast<char>(c);
    }
    i += len;
  }
  return out;
}

std::string truncate(const std::string& s, int maxWidth) {
  std::vector<std::size_t> starts;
  std::size_t i = 0;
  while (i < s.size()) {
    starts.push_back(i);
    const unsigned char c = static_cast<unsigned char>(s[i]);
    std::size_t len = 1;
    if ((c & 0xE0) == 0xC0) len = 2;
    else if ((c & 0xF0) == 0xE0) len = 3;
    else if ((c & 0xF8) == 0xF0) len = 4;
    i += std::min(len, s.size() - i);
  }
  starts.push_back(s.size());
  const int total = static_cast<int>(starts.size()) - 1;

  if (maxWidth <= 1) {
    const int keep = std::max(0, maxWidth);
    return keep >= total ? s : s.substr(0, starts[static_cast<std::size_t>(keep)]);
  }
  if (total <= maxWidth) return s;
  return s.substr(0, starts[static_cast<std::size_t>(maxWidth - 1)]) + "\xE2\x80\xA6";
}

}  // namespace torlinkc
