/* vim:set ts=2 sw=2 sts=2 et: */
/**
 * \author     Marcus Holland-Moritz (github@mhxnet.de)
 * \copyright  Copyright (c) Marcus Holland-Moritz
 *
 * This file is part of dwarfs.
 *
 * dwarfs 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.
 *
 * dwarfs 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 dwarfs.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <algorithm>
#include <array>
#include <charconv>
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <mutex>
#include <optional>
#include <type_traits>

#if __has_include(<utf8cpp/utf8.h>)
#include <utf8cpp/utf8.h>
#else
#include <utf8.h>
#endif

#include <date/date.h>

#include <folly/ExceptionString.h>
#include <folly/String.h>
#include <folly/portability/Fcntl.h>
#include <folly/portability/SysStat.h>
#include <folly/portability/Windows.h>
#include <folly/system/HardwareConcurrency.h>

#include <dwarfs/config.h>

#ifdef DWARFS_STACKTRACE_ENABLED
#include <cpptrace/cpptrace.hpp>
#include <signal.h>

#ifdef _WIN32
#include <tlhelp32.h>
#endif
#endif

#include <dwarfs/conv.h>
#include <dwarfs/error.h>
#include <dwarfs/util.h>

extern "C" int dwarfs_wcwidth(int ucs);

namespace dwarfs {

namespace {

inline std::string trimmed(std::string in) {
  while (!in.empty() && in.back() == ' ') {
    in.pop_back();
  }
  return in;
}

} // namespace

std::string size_with_unit(size_t size) {
  return trimmed(folly::prettyPrint(size, folly::PRETTY_BYTES_IEC, true));
}

std::string time_with_unit(double sec) {
  return trimmed(folly::prettyPrint(sec, folly::PRETTY_TIME_HMS, false));
}

std::string time_with_unit(std::chrono::nanoseconds ns) {
  return time_with_unit(1e-9 * ns.count());
}

size_t parse_size_with_unit(std::string const& str) {
  size_t value;
  auto [ptr, ec]{std::from_chars(str.data(), str.data() + str.size(), value)};

  if (ec != std::errc()) {
    DWARFS_THROW(runtime_error, "cannot parse size value");
  }

  if (ptr[0] == '\0') {
    return value;
  }

  if (ptr[1] == '\0') {
    switch (ptr[0]) {
    case 't':
    case 'T':
      value <<= 10;
      [[fallthrough]];
    case 'g':
    case 'G':
      value <<= 10;
      [[fallthrough]];
    case 'm':
    case 'M':
      value <<= 10;
      [[fallthrough]];
    case 'k':
    case 'K':
      value <<= 10;
      return value;
    default:
      break;
    }
  }

  DWARFS_THROW(runtime_error, "unsupported size suffix");
}

std::chrono::milliseconds parse_time_with_unit(std::string const& str) {
  uint64_t value;
  auto [ptr, ec]{std::from_chars(str.data(), str.data() + str.size(), value)};

  if (ec != std::errc()) {
    DWARFS_THROW(runtime_error, "cannot parse time value");
  }

  switch (ptr[0]) {
  case 'h':
    if (ptr[1] == '\0') {
      return std::chrono::hours(value);
    }
    break;

  case 'm':
    if (ptr[1] == '\0') {
      return std::chrono::minutes(value);
    } else if (ptr[1] == 's' && ptr[2] == '\0') {
      return std::chrono::milliseconds(value);
    }
    break;

  case 's':
    if (ptr[1] != '\0') {
      break;
    }
    [[fallthrough]];
  case '\0':
    return std::chrono::seconds(value);

  default:
    break;
  }

  DWARFS_THROW(runtime_error, "unsupported time suffix");
}

std::chrono::system_clock::time_point parse_time_point(std::string const& str) {
  static constexpr std::array<char const*, 9> formats{
      "%Y%m%dT%H%M%S", "%Y%m%dT%H%M", "%Y%m%dT", "%F %T", "%FT%T",
      "%F %R",         "%FT%R",       "%FT",     "%F"};

  for (auto const& fmt : formats) {
    std::istringstream iss(str);
    std::chrono::system_clock::time_point tp;
    date::from_stream(iss, fmt, tp);
    if (!iss.fail()) {
      iss.peek();
      if (iss.eof()) {
        return tp;
      }
    }
  }

  DWARFS_THROW(runtime_error, "cannot parse time point");
}

size_t utf8_display_width(char const* p, size_t len) {
  char const* const e = p + len;
  size_t rv = 0;

  while (p < e) {
    auto cp = utf8::next(p, e);
    rv += dwarfs_wcwidth(cp);
  }

  return rv;
}

size_t utf8_display_width(std::string const& str) {
  return utf8_display_width(str.data(), str.size());
}

void utf8_truncate(std::string& str, size_t len) {
  char const* p = str.data();
  char const* const e = p + str.size();
  size_t l = 0;

  while (p < e && l <= len) {
    auto np = p;
    auto cp = utf8::next(np, e);
    l += dwarfs_wcwidth(cp);
    if (l > len) {
      break;
    }
    p = np;
  }

  str.resize(p - str.data());
}

void utf8_sanitize(std::string& str) {
  if (!utf8::is_valid(str)) [[unlikely]] {
    str = utf8::replace_invalid(str);
  }
}

void shorten_path_string(std::string& path, char separator, size_t max_len) {
  if (utf8_display_width(path) > max_len) {
    if (max_len < 3) {
      path.clear();
      return;
    }

    size_t start = 0;

    while (utf8_display_width(path.data() + start, path.size() - start) >
           max_len - 3) {
      auto next = path.find(separator, start + 1);
      if (next == std::string::npos) {
        break;
      }
      start = next;
    }

    path.replace(0, start, "...");

    if (utf8_display_width(path) > max_len) {
      if (max_len >= 7) {
        utf8_truncate(path, max_len - 3);
        path += "...";
      } else {
        path = "...";
      }
    }
  }
}

std::filesystem::path canonical_path(std::filesystem::path p) {
  if (!p.empty()) {
    try {
      p = std::filesystem::canonical(p);
    } catch (std::filesystem::filesystem_error const&) {
      p = std::filesystem::absolute(p);
    }

#ifdef _WIN32
    if (auto wstr = p.wstring(); !wstr.starts_with(L"\\\\")) {
      p = std::filesystem::path(L"\\\\?\\" + wstr);
    }
#endif
  }

  return p;
}

std::string path_to_utf8_string_sanitized(std::filesystem::path const& p) {
#ifdef _WIN32
  if constexpr (std::is_same_v<std::filesystem::path::value_type, wchar_t>) {
    auto const& in = p.native();
    if (in.empty()) {
      return {};
    }
    int size_needed = ::WideCharToMultiByte(
        CP_UTF8, 0, in.data(), (int)in.size(), NULL, 0, NULL, NULL);
    std::string out(size_needed, 0);
    ::WideCharToMultiByte(CP_UTF8, 0, in.data(), (int)in.size(), &out[0],
                          size_needed, NULL, NULL);
    return out;
  }
#endif

  return u8string_to_string(p.u8string());
}

bool getenv_is_enabled(char const* var) {
  if (auto val = std::getenv(var)) {
    if (auto maybeBool = try_to<bool>(val); maybeBool && *maybeBool) {
      return true;
    }
  }
  return false;
}

void setup_default_locale() {
  try {
#ifdef _WIN32
    char const* locale = "en_US.utf8";
#else
    char const* locale = "";
#endif
    std::locale::global(std::locale(locale));
    if (!std::setlocale(LC_ALL, locale)) {
      std::cerr << "warning: setlocale(LC_ALL, \"\") failed\n";
    }
  } catch (std::exception const& e) {
    std::cerr << "warning: failed to set user default locale: " << e.what()
              << "\n";
    try {
      std::locale::global(std::locale::classic());
      if (!std::setlocale(LC_ALL, "C")) {
        std::cerr << "warning: setlocale(LC_ALL, \"C\") failed\n";
      }
    } catch (std::exception const& e) {
      std::cerr << "warning: also failed to set classic locale: " << e.what()
                << "\n";
    }
  }
#ifdef _WIN32
  SetConsoleOutputCP(CP_UTF8);
#endif
}

std::string error_cp_to_utf8(std::string_view error) {
  std::string input(error);
#ifdef _WIN32
  UINT cp = GetACP();
  // convert from codepage to UTF-16
  int wclen = MultiByteToWideChar(cp, 0, input.c_str(), -1, NULL, 0);
  if (wclen == 0) {
    utf8_sanitize(input);
    return input;
  }
  std::wstring wstr(wclen, L'\0');
  MultiByteToWideChar(cp, 0, input.c_str(), -1, &wstr[0], wclen);

  // convert from UTF-16 to UTF-8
  int utf8len =
      WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
  if (utf8len == 0) {
    utf8_sanitize(input);
    return input;
  }
  std::string utf8err(utf8len, '\0');
  WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8err[0], utf8len, NULL,
                      NULL);

  // remove the null terminator added by WideCharToMultiByte
  if (!utf8err.empty() && utf8err.back() == '\0') {
    utf8err.pop_back();
  }

  return utf8err;
#else
  return input;
#endif
}

std::string_view basename(std::string_view path) {
  auto pos = path.find_last_of("/\\");
  if (pos == std::string_view::npos) {
    return path;
  }
  return path.substr(pos + 1);
}

void ensure_binary_mode(std::ostream& os [[maybe_unused]]) {
#ifdef _WIN32
  if (&os == &std::cout) {
    _setmode(_fileno(stdout), _O_BINARY);
  } else if (&os == &std::cerr) {
    _setmode(_fileno(stderr), _O_BINARY);
  }
#endif
}

std::string exception_str(std::exception const& e) {
  return folly::exceptionStr(e).toStdString();
}

std::string exception_str(std::exception_ptr const& e) {
  return folly::exceptionStr(e).toStdString();
}

unsigned int hardware_concurrency() noexcept {
  return folly::hardware_concurrency();
}

int get_current_umask() {
  // I'm pretty certain these warnings by Flawfinder are false positives.
  // After all, we're just doing a no-op by re-setting the original value
  // in order to read it.
  auto mask = ::umask(0077); /* Flawfinder: ignore */
  ::umask(mask);             /* Flawfinder: ignore */
  return mask;
}

#ifdef DWARFS_STACKTRACE_ENABLED

namespace {

struct fatal_signal {
  int signum;
  std::string_view name;
};

constexpr std::array kFatalSignals{
    fatal_signal{SIGSEGV, "SIGSEGV"}, fatal_signal{SIGILL, "SIGILL"},
    fatal_signal{SIGFPE, "SIGFPE"},   fatal_signal{SIGABRT, "SIGABRT"},
    fatal_signal{SIGTERM, "SIGTERM"},
#ifndef _WIN32
    fatal_signal{SIGBUS, "SIGBUS"},
#endif
};

std::once_flag g_signal_handlers_installed;

#ifdef _WIN32

std::vector<HANDLE> suspend_other_threads() {
  std::vector<HANDLE> handles;
  DWORD currend_tid = ::GetCurrentThreadId();
  DWORD current_pid = ::GetCurrentProcessId();

  HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  if (snapshot == INVALID_HANDLE_VALUE) {
    return handles;
  }

  THREADENTRY32 te;
  te.dwSize = sizeof(THREADENTRY32);

  if (::Thread32First(snapshot, &te)) {
    do {
      if (te.th32OwnerProcessID == current_pid &&
          te.th32ThreadID != currend_tid) {
        HANDLE th =
            ::OpenThread(THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION,
                         FALSE, te.th32ThreadID);
        if (th) {
          if (::SuspendThread(th) != static_cast<DWORD>(-1)) {
            handles.push_back(th);
          } else {
            ::CloseHandle(th);
          }
        }
      }
    } while (::Thread32Next(snapshot, &te));
  }

  ::CloseHandle(snapshot);

  return handles;
}

void resume_suspended_threads(const std::vector<HANDLE>& handles) {
  for (auto th : handles) {
    ::ResumeThread(th);
    ::CloseHandle(th);
  }
}

void fatal_signal_handler_win(int signal) {
  auto suspended = suspend_other_threads();

  std::optional<std::string> signame;

  for (size_t i = 0; i < kFatalSignals.size(); ++i) {
    if (signal == kFatalSignals[i].signum) {
      signame = kFatalSignals[i].name;
      break;
    }
  }

  if (!signame) {
    signame = std::to_string(signal);
  }

  std::cerr << "Caught signal " << *signame << "\n";
  cpptrace::generate_trace().print();

  resume_suspended_threads(suspended);

  ::exit(1);
}

#else

std::array<struct ::sigaction, kFatalSignals.size()> old_handlers;

void fatal_signal_handler_posix(int signal) {
  std::optional<std::string> signame;

  for (size_t i = 0; i < kFatalSignals.size(); ++i) {
    if (signal == kFatalSignals[i].signum) {
      ::sigaction(signal, &old_handlers[i], nullptr);
      signame = kFatalSignals[i].name;
      break;
    }
  }

  if (!signame) {
    struct ::sigaction sa_dfl;
    ::memset(&sa_dfl, 0, sizeof(sa_dfl));
    sa_dfl.sa_handler = SIG_DFL;
    ::sigaction(signal, &sa_dfl, nullptr);
    signame = std::to_string(signal);
  }

  std::cerr << "Caught signal " << *signame << "\n";
  cpptrace::generate_trace().print();

  ::raise(signal);
}

#endif

void install_signal_handlers_impl() {
  for (size_t i = 0; i < kFatalSignals.size(); ++i) {
#ifdef _WIN32
    ::signal(kFatalSignals[i].signum, fatal_signal_handler_win);
#else
    struct ::sigaction new_sa;
    ::memset(&new_sa, 0, sizeof(new_sa));
    // this is potentially implemented as a macro
    sigfillset(&new_sa.sa_mask);
    new_sa.sa_handler = fatal_signal_handler_posix;
    ::sigaction(kFatalSignals[i].signum, &new_sa, &old_handlers[i]);
#endif
  }
}

} // namespace

#endif

void install_signal_handlers() {
#ifdef DWARFS_STACKTRACE_ENABLED
  std::call_once(g_signal_handlers_installed, install_signal_handlers_impl);
#endif
}

} // namespace dwarfs
