/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */

#if !defined(_WIN32) && !defined(__sun) && !defined(__OpenBSD__)
// POSIX APIs are needed
// NOLINTNEXTLINE(bugprone-reserved-identifier)
#  define _POSIX_C_SOURCE 200809L
#endif
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__QNX__)
// For isascii
// NOLINTNEXTLINE(bugprone-reserved-identifier)
#  define _XOPEN_SOURCE 700
#endif

#include "cmTimestamp.h"

#include <cstdlib>
#include <cstring>
#include <ctime>
#include <sstream>
#include <utility>

#ifdef __MINGW32__
#  include <libloaderapi.h>
#endif

#include <cm3p/uv.h>

#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

std::string cmTimestamp::CurrentTime(std::string const& formatString,
                                     bool utcFlag) const
{
  // get current time with microsecond resolution
  uv_timeval64_t timeval;
  uv_gettimeofday(&timeval);
  auto currentTimeT = static_cast<time_t>(timeval.tv_sec);
  auto microseconds = static_cast<uint32_t>(timeval.tv_usec);

  // check for override via SOURCE_DATE_EPOCH for reproducible builds
  std::string source_date_epoch;
  cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
  if (!source_date_epoch.empty()) {
    std::istringstream iss(source_date_epoch);
    iss >> currentTimeT;
    if (iss.fail() || !iss.eof()) {
      cmSystemTools::Error("Cannot parse SOURCE_DATE_EPOCH as integer");
      exit(27);
    }
    // SOURCE_DATE_EPOCH has only a resolution in the seconds range
    microseconds = 0;
  }
  if (currentTimeT == static_cast<time_t>(-1)) {
    return std::string();
  }

  return this->CreateTimestampFromTimeT(currentTimeT, microseconds,
                                        formatString, utcFlag);
}

std::string cmTimestamp::FileModificationTime(char const* path,
                                              std::string const& formatString,
                                              bool utcFlag) const
{
  std::string real_path =
    cmSystemTools::GetRealPathResolvingWindowsSubst(path);

  if (!cmsys::SystemTools::FileExists(real_path)) {
    return std::string();
  }

  // use libuv's implementation of stat(2) to get the file information
  time_t mtime = 0;
  uint32_t microseconds = 0;
  uv_fs_t req;
  if (uv_fs_stat(nullptr, &req, real_path.c_str(), nullptr) == 0) {
    mtime = static_cast<time_t>(req.statbuf.st_mtim.tv_sec);
    // tv_nsec has nanosecond resolution, but we truncate it to microsecond
    // resolution in order to be consistent with cmTimestamp::CurrentTime()
    microseconds = static_cast<uint32_t>(req.statbuf.st_mtim.tv_nsec / 1000);
  }
  uv_fs_req_cleanup(&req);

  return this->CreateTimestampFromTimeT(mtime, microseconds, formatString,
                                        utcFlag);
}

std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT,
                                                  std::string formatString,
                                                  bool utcFlag) const
{
  return this->CreateTimestampFromTimeT(timeT, 0, std::move(formatString),
                                        utcFlag);
}

std::string cmTimestamp::CreateTimestampFromTimeT(time_t timeT,
                                                  uint32_t const microseconds,
                                                  std::string formatString,
                                                  bool utcFlag) const
{
  if (formatString.empty()) {
    formatString = "%Y-%m-%dT%H:%M:%S";
    if (utcFlag) {
      formatString += "Z";
    }
  }

  struct tm timeStruct;
  memset(&timeStruct, 0, sizeof(timeStruct));

  struct tm* ptr = nullptr;
  if (utcFlag) {
    ptr = gmtime(&timeT);
  } else {
    ptr = localtime(&timeT);
  }

  if (!ptr) {
    return std::string();
  }

  timeStruct = *ptr;

  std::string result;
  for (std::string::size_type i = 0; i < formatString.size(); ++i) {
    char c1 = formatString[i];
    char c2 = (i + 1 < formatString.size()) ? formatString[i + 1]
                                            : static_cast<char>(0);

    if (c1 == '%' && c2 != 0) {
      result += this->AddTimestampComponent(c2, timeStruct, timeT, utcFlag,
                                            microseconds);
      ++i;
    } else {
      result += c1;
    }
  }

  return result;
}

time_t cmTimestamp::CreateUtcTimeTFromTm(struct tm& tm) const
{
#if defined(_MSC_VER) && _MSC_VER >= 1400
  return _mkgmtime(&tm);
#else
  // From Linux timegm() manpage.

  std::string tz_old;
  bool const tz_was_set = cmSystemTools::GetEnv("TZ", tz_old);
  tz_old = "TZ=" + tz_old;

  // The standard says that "TZ=" or "TZ=[UNRECOGNIZED_TZ]" means UTC.
  // It seems that "TZ=" does NOT work, at least under Windows
  // with neither MSVC nor MinGW, so let's use explicit "TZ=UTC"

  cmSystemTools::PutEnv("TZ=UTC");

  tzset();

  time_t result = mktime(&tm);

#  ifndef CMAKE_BOOTSTRAP
  if (tz_was_set) {
    cmSystemTools::PutEnv(tz_old);
  } else {
    cmSystemTools::UnsetEnv("TZ");
  }
#  else
  // No UnsetEnv during bootstrap.  This is good enough for CMake itself.
  cmSystemTools::PutEnv(tz_old);
  static_cast<void>(tz_was_set);
#  endif

  tzset();

  return result;
#endif
}

std::string cmTimestamp::AddTimestampComponent(
  char flag, struct tm& timeStruct, time_t const timeT, bool const utcFlag,
  uint32_t const microseconds) const
{
  std::string formatString = cmStrCat('%', flag);

  switch (flag) {
    case 'a':
    case 'A':
    case 'b':
    case 'B':
    case 'd':
    case 'H':
    case 'I':
    case 'j':
    case 'm':
    case 'M':
    case 'S':
    case 'U':
    case 'V':
    case 'w':
    case 'y':
    case 'Y':
    case '%':
      break;
    case 'Z':
#if defined(__GLIBC__)
      // 'struct tm' has the time zone, so strftime can honor UTC.
      static_cast<void>(utcFlag);
#else
      // 'struct tm' may not have the time zone, so strftime may
      // use local time.  Hard-code the UTC result.
      if (utcFlag) {
        return std::string("GMT");
      }
#endif
      break;
    case 'z': {
#if defined(__GLIBC__)
      // 'struct tm' has the time zone, so strftime can honor UTC.
      static_cast<void>(utcFlag);
#else
      // 'struct tm' may not have the time zone, so strftime may
      // use local time.  Hard-code the UTC result.
      if (utcFlag) {
        return std::string("+0000");
      }
#endif
#ifndef _AIX
      break;
#else
      std::string xpg_sus_old;
      bool const xpg_sus_was_set =
        cmSystemTools::GetEnv("XPG_SUS_ENV", xpg_sus_old);
      if (xpg_sus_was_set && xpg_sus_old == "ON") {
        break;
      }
      xpg_sus_old = "XPG_SUS_ENV=" + xpg_sus_old;

      // On AIX systems, %z requires XPG_SUS_ENV=ON to work as desired.
      cmSystemTools::PutEnv("XPG_SUS_ENV=ON");
      tzset();

      char buffer[16];
      size_t size = strftime(buffer, sizeof(buffer), "%z", &timeStruct);

#  ifndef CMAKE_BOOTSTRAP
      if (xpg_sus_was_set) {
        cmSystemTools::PutEnv(xpg_sus_old);
      } else {
        cmSystemTools::UnsetEnv("XPG_SUS_ENV");
      }
#  else
      // No UnsetEnv during bootstrap.  This is good enough for CMake itself.
      cmSystemTools::PutEnv(xpg_sus_old);
      static_cast<void>(xpg_sus_was_set);
#  endif
      tzset();

      return std::string(buffer, size);
#endif
    }
    case 's': // Seconds since UNIX epoch (midnight 1-jan-1970)
    {
      // Build a time_t for UNIX epoch and subtract from the input "timeT":
      struct tm tmUnixEpoch;
      memset(&tmUnixEpoch, 0, sizeof(tmUnixEpoch));
      tmUnixEpoch.tm_mday = 1;
      tmUnixEpoch.tm_year = 1970 - 1900;

      time_t const unixEpoch = this->CreateUtcTimeTFromTm(tmUnixEpoch);
      if (unixEpoch == -1) {
        cmSystemTools::Error(
          "Error generating UNIX epoch in string(TIMESTAMP ...) or "
          "file(TIMESTAMP ...). Please, file a bug report against CMake");
        return std::string();
      }

      return std::to_string(
        static_cast<int64_t>(std::difftime(timeT, unixEpoch)));
    }
    case 'f': // microseconds
    {
      // clip number to 6 digits and pad with leading zeros
      std::string microsecs = std::to_string(microseconds % 1000000);
      return std::string(6 - microsecs.length(), '0') + microsecs;
    }
    default: {
      return formatString;
    }
  }

  char buffer[16];

#ifdef __MINGW32__
  /* See a bug in MinGW: https://sourceforge.net/p/mingw-w64/bugs/793/. A work
   * around is to try to use strftime() from ucrtbase.dll. */
  using T = size_t(__cdecl*)(char*, size_t, char const*, const struct tm*);
  auto loadUcrtStrftime = []() -> T {
    auto handle =
      LoadLibraryExA("ucrtbase.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
    if (handle) {
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wcast-function-type"
      return reinterpret_cast<T>(GetProcAddress(handle, "strftime"));
#  pragma GCC diagnostic pop
    }
    return nullptr;
  };
  static T ucrtStrftime = loadUcrtStrftime();

  if (ucrtStrftime) {
    size_t size =
      ucrtStrftime(buffer, sizeof(buffer), formatString.c_str(), &timeStruct);
    return std::string(buffer, size);
  }
#endif

  size_t size =
    strftime(buffer, sizeof(buffer), formatString.c_str(), &timeStruct);

  return std::string(buffer, size);
}
