#define _USE_MATH_DEFINES

/* ######################### includes ############################################################################### */

#include "utilcpp_int.hxx"
#include <cmath>
#include <iomanip>
#include <algorithm>
#include <string_view>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <io.h>
#include <process.h>
#include <direct.h>
#define F_OK 0
#define access _access
#else
#include <unistd.h>
#endif


/* ######################### internal implementation ################################################################ */

/* ========================= functions ============================================================================== */

/* ------------------------- util ----------------------------------------------------------------------------------- */

std::string_view lTrim(std::string_view s)
{
  size_t start = s.find_first_not_of(WHITESPACE);
  return (start == std::string::npos) ? "" : s.substr(start);
}

std::string_view rTrim(std::string_view s)
{
  size_t end = s.find_last_not_of(WHITESPACE);
  return (end == std::string::npos) ? "" : s.substr(0, end + 1);
}

std::string_view trim(std::string_view s)
{
  return rTrim(lTrim(s));
}

bool startsWith(std::string_view str, std::string_view prefix)
{
  return str.size() >= prefix.size() && 0 == str.compare(0, prefix.size(), prefix);
}

bool endsWith(std::string_view str, std::string_view suffix)
{
  return str.size() >= suffix.size() && 0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix);
}

/*!
 * \brief Check if a given string ends with any subprefix of a given prefix (excluding the prefix itself). For example,
 * the string "abcdefg" ends with any subprefix of "fghij" (subprefix "fg")."
 *
 * \param[in] str The input string which is checked at the end.
 * \param[in] prefix The prefix which will be shortened to any subprefix (removing one character at the end in each
 *            step).
 * \return The position of a found subprefix. If no subprefix is found, `std::string_view::npos` is returned.
 */
size_t endsWithAnySubPrefix(std::string_view str, std::string_view prefix)
{
  for (auto i = prefix.size() - 1; i > 0; --i)
    {
      if (endsWith(str, prefix.substr(0, i))) return str.size() - i;
    }

  return std::string_view::npos;
}

/*!
 * \brief Check if a substring of a given input string consists of a certain character, ending with an another end
 *        character.
 *
 * \param[in] input The string to check.
 * \param[in] c The character to check for.
 * \param[in] ends_with The end character to check for.
 * \param[in] pos The starting position of the substring. All characters starting from `&input[pos]` will be checked.
 * \return The index of the ending character. If the substring does not consist of `c` and `ends_with`,
 *         `std::string_view::npos` is returned instead.
 */
size_t stringConsistsOf(std::string_view input, char c, char ends_with, size_t pos)
{
  auto it_not_c =
      std::find_if_not(std::begin(input) + pos, std::end(input), [&](char current_char) { return current_char == c; });
  if (it_not_c == std::end(input) || *it_not_c != ends_with) return std::string_view::npos;

  return it_not_c - std::begin(input);
}

namespace internal
{
/*!
 * \brief Escape or unescape two consecutive characters (like `--`) in a string.
 *
 * This function can be used to escape or unescape two consecutive characters in a string by putting a given escape
 * character in between them. To simplfy the explanation, assume the character which should be escaped is `-` and the
 * escape character is `\`. Occurences of `--` in the string will be replaced with `-\-` and occurrences of `-\-` will
 * be replaced with `-\\-` and so forth. Strings with even more consecutive characters (`---` or more) will also be
 * handled correctly (`---` will be replaced with `-\-\-`, `-\--` will be replaced by `-\\-\-` and so forth).
 *
 * \param[in] input The string to escape or unescape.
 * \param[in] escape_char The character which will be used as an escape character (`\` in the above example)
 * \param[in] to_escape_char The character which will be escaped if two consecutive characters are found (`-` in the
 *                           above example).
 * \param[in] unescape Can be set to `true` to unescape instead.
 */
std::string escapeOrUnescape(std::string_view input, char escape_char, char to_escape_char, bool unescape)
{
  std::vector<std::string_view> output_parts;

  auto start_pos = input.find(to_escape_char);
  while (start_pos != std::string_view::npos)
    {
      auto end_pos = stringConsistsOf(input, escape_char, to_escape_char, start_pos + 1);
      if (end_pos != std::string_view::npos)
        {
          /* Explanation of `(unescape && end_pos - start_pos > 1 ? 1 : 0)`:
           *   In unescape mode, one escape character needs to be removed, so substract one from the found end position.
           *   However, if the routine found an already unescaped occurence (`--` instead of `-\-`), skip the
           *   substraction to not lose a non-escape character. */
          output_parts.push_back(input.substr(0, end_pos - (unescape && end_pos - start_pos > 1 ? 1 : 0)));
          // Leave the second minus of `--` in the input, so strings like `---` can be handled in the next correctly.
          input = input.substr(end_pos);
          start_pos = 0;
        }
      else
        {
          start_pos = input.find(to_escape_char, start_pos + 1);
        }
    }
  output_parts.push_back(input);

  return stringJoin(std::begin(output_parts), std::end(output_parts), unescape ? "" : std::string{escape_char});
}
} // namespace internal

/*!
 * \brief Escape double minus signs (`--`) in a string, by replacing them with `-\-`. See `escape_or_unescape` for more
 *        details.
 *
 * \param[in] input String to escape.
 * \return The escaped string.
 */
std::string escapeDoubleMinus(std::string_view input)
{
  return internal::escapeOrUnescape(input, '\\', '-', false);
}

/*!
 * \brief Unescape escaped double minus signs (`-\-`) in a string, by replacing them with `--`. See `escape_or_unescape`
 *        for more details.
 *
 * \param[in] input String to unescape.
 * \return The unescaped string.
 */
std::string unescapeDoubleMinus(std::string_view input)
{
  return internal::escapeOrUnescape(input, '\\', '-', true);
}

/*!
 * \brief Check if an attribute is a backup copy of another attribute of a graphics tree element.
 *
 * This function makes a decision based only on the attribute name itself, there is no graphcs tree check involved!
 *
 * \param[in] name The attribute name to check.
 * \return The attribute name of the original attribute if the input is a backup attribute, `std::nullopt` otherwise.
 */
std::optional<std::string_view> isBackupAttributeFor(std::string_view name)
{
  if (name.empty() || !(name[0] == '_' && endsWith(name, "_org") && name.size() > 5)) return std::nullopt;

  return name.substr(1, name.size() - 5);
}

bool fileExists(const std::string &name)
{
  return (access(name.c_str(), F_OK) != -1);
}

void linSpace(double start, double end, int n, std::vector<double> &x)
{
  if (x.size() < n) x.resize(n);
  for (int i = 0; i < n; i++)
    {
      x[i] = (start + i * (end - start) / (n - 1));
    }
}

/* like python list comprehension [factor * func(element) for element in list] saves values in result starting at start
 * index */
void listComprehension(double factor, double (*function_ptr)(double), std::vector<double> &list, int num, int start,
                       std::vector<double> &result)
{
  if (result.size() < num) result.resize(num);

  for (int i = 0; i < num; ++i)
    {
      // just in case if start + num + 1 exceeds the size of the vector
      if (i + start >= result.size()) break;
      result[i + start] = factor * (*function_ptr)(list[i]);
    }
}

std::complex<double> moivre(double r, int x, int n)
{
  if (n != 0) return {pow(r, (1.0 / n)) * (cos(2.0 * x * M_PI / n)), pow(r, (1.0 / n)) * (sin(2.0 * x * M_PI / n))};
  return {1.0, 0.0};
}

bool isNumber(std::string_view str)
{
  const char minus[] = {(char)0xe2, (char)0x88, (char)0x92, '\0'}; // gr minus sign
  auto em_dash = std::string(minus);
  size_t start_pos = 0;
  if (startsWith(str, em_dash)) start_pos = em_dash.size();
  auto pos = str.find_first_not_of(".-0123456789e", start_pos);
  return pos == std::string::npos;
}

bool isIntNumber(std::string_view str)
{
  return str.find_first_not_of("0123456789") == std::string::npos;
}

double round(double val, int digits)
{
  if (digits < 0) return (round((val * pow(0.1, digits)) + ((val < 0) ? -0.5 : 0.5))) / pow(0.1, digits);
  return (round((val * pow(0.1, digits)) + ((val < 0) ? -0.5 : 0.5) * pow(0.1, digits))) / pow(0.1, digits);
}

double ceil(double val, int digits)
{
  return ceil(val * pow(0.1, digits)) / pow(0.1, digits);
}

double floor(double val, int digits)
{
  return floor(round(val, -15) * pow(0.1, digits)) / pow(0.1, digits);
}


/* ========================= methods ================================================================================ */

/* ------------------------- IdPool --------------------------------------------------------------------------------- */

template <typename T> IdPool<T>::IdPool(T start_id) : start_id_(start_id) {}

template <typename T> T IdPool<T>::current()
{
  if (current_id_) return current_id_.value();
  throw NoCurrentIdError();
}

template <typename T> T IdPool<T>::next()
{
  T next_id;
  if (used_id_ranges_.empty())
    {
      // There are no IDs in use, so the next ID is the start ID
      next_id = start_id_;
      used_id_ranges_.emplace_front(next_id, next_id);
    }
  else if (used_id_ranges_.front().first != start_id_)
    {
      // The `start_id_` was released in a previous operation, so use it as the next ID
      next_id = start_id_;
      if (used_id_ranges_.front().first == start_id_ + 1)
        {
          /* In this case, only the `start_id_` is missing from the first interval, so we can just add it, for
           * example:
           * ```
           * [1, 10] -> [0, 10]  (`start_id_` is `0`)
           * ``` */
          used_id_ranges_.front().first = next_id;
        }
      else
        {
          /* Otherwise, multiple IDs are missing from the first interval, for example:
           * ```
           * [5, 10]
           * ```
           * so create a new interval with the `start_id_` and add it to the front of the list:
           * ```
           * [0, 0], [5, 10]
           * ``` */
          used_id_ranges_.emplace_front(next_id, next_id);
        }
    }
  else
    {
      next_id = used_id_ranges_.front().second + 1;
      if (used_id_ranges_.size() > 1 && (++std::cbegin(used_id_ranges_))->first == next_id + 1)
        {
          /* In this case we have a gap of a single value between the first two intervals, for example:
           * ```
           * [0, 9], [11, 20]
           * ```
           * so `next_id` is `10` and both intervals can be merged to a single interval `[0, 20]`. */
          (++std::begin(used_id_ranges_))->first = start_id_;
          used_id_ranges_.pop_front();
        }
      else
        {
          used_id_ranges_.front().second = next_id;
        }
    }
  current_id_ = next_id;
  return next_id;
}

template <typename T> void IdPool<T>::print(std::ostream &os, bool compact) const
{
  os << "Used id ranges:";
  if (used_id_ranges_.empty())
    {
      os << std::endl;
      return;
    }
  if (compact)
    {
      os << " ";
      for (auto it = std::begin(used_id_ranges_); it != std::end(used_id_ranges_); ++it)
        {
          if (it != std::begin(used_id_ranges_)) os << ", ";
          os << it->first;
          if (it->first != it->second) os << "-" << it->second;
        }
      os << std::endl;
    }
  else
    {
      os << std::endl;
      auto field_width = std::to_string(used_id_ranges_.back().second).length();
      for (auto it = std::begin(used_id_ranges_); it != std::end(used_id_ranges_); ++it)
        {
          os << "  " << std::setw(field_width) << it->first;
          if (it->first != it->second) os << " - " << std::setw(field_width) << it->second;
          os << std::endl;
        }
    }
}

template <typename T> void IdPool<T>::release(T id)
{
  for (auto it = std::begin(used_id_ranges_); it != std::end(used_id_ranges_); ++it)
    {
      if (it->first <= id && id <= it->second)
        {
          if (it->first == it->second)
            {
              // In this case, the interval only consists of one value (`id`), so we can just remove it.
              used_id_ranges_.erase(it);
            }
          else if (it->first == id)
            {
              /* `id` is at the start of the interval and the interval consists of multiple values, for example
               * ```
               * [10, 12] with id = 10
               * ```
               * so increment the start value of the interval, so we get:
               * ```
               * [11, 12]
               * ``` */
              ++(it->first);
            }
          else if (it->second == id)
            {
              /* `id` is at the end of the interval and the interval consists of multiple values, for example
               * ```
               * [10, 12] with id = 12
               * ```
               * so decrement the end value of the interval, so we get:
               * ```
               * [10, 11]
               * ``` */
              --(it->second);
            }
          else
            {
              /* In this case, `id` is in the middle of the interval, so split it into two intervals, for example:
               * ```
               * [10, 12] with id = 11
               * ```
               * so we get:
               * ```
               * [10, 10], [12, 12]
               * ``` */
              auto second = it->second;
              it->second = id - 1;
              used_id_ranges_.emplace(++it, id + 1, second);
            }
          if (id == current_id_) current_id_.reset();
          return;
        }
    }
  throw IdNotFoundError(id);
}

template <typename T> void IdPool<T>::reset()
{
  used_id_ranges_.clear();
  current_id_.reset();
}

/* Generate code for int IDs since this is needed in the code */
template class IdPool<int>;

/*!
 * \brief Parse an ISO 8601 string without timezone (format `YYYY-MM-DDTHH:MM:SS`).
 *
 * \param[in] s The string to parse
 * \param[out] tm The timestamp struct to fill
 * \return If the string could be parsed as an IOS 8601 string
 */
bool parseIso8601WithoutTimezone(const std::string &s, struct tm &tm)
{
  /* First set ALL members to default values to generate consistent results */
  tm = {};
  /* Then set relevant members to invalid values to check parsing state later on. */
  tm.tm_sec = -1;
  tm.tm_min = -1;
  tm.tm_hour = -1;
  tm.tm_mday = -1;
  tm.tm_mon = -1;
  tm.tm_year = -1;
  tm.tm_wday = -1;
  tm.tm_yday = -1;
  tm.tm_isdst = -1;
  std::istringstream input(s);
  input >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
  return !input.fail() &&
         (tm.tm_sec >= 0 && tm.tm_min >= 0 && tm.tm_hour >= 0 && tm.tm_mday >= 0 && tm.tm_mon >= 0 && tm.tm_year >= 0);
}

#ifdef _WIN32
std::wstring getEnvVar(const std::wstring &name, const std::wstring &default_value)
#else
std::string getEnvVar(const std::string &name, const std::string &default_value)
#endif
{
#ifdef _WIN32
  DWORD needed_wide_chars = GetEnvironmentVariableW(name.c_str(), nullptr, 0);
  if (GetLastError() != ERROR_ENVVAR_NOT_FOUND)
    {
      std::vector<wchar_t> value_wide(needed_wide_chars);
      GetEnvironmentVariableW(name.c_str(), value_wide.data(), needed_wide_chars);
      return std::wstring(value_wide.data());
    }
  else
    {
      return default_value;
    }
#else
  const char *value_c_ptr = getenv(name.c_str());
  if (value_c_ptr != nullptr) return std::string(value_c_ptr);
  return default_value;
#endif
}

#ifdef _WIN32
std::wstring w(const std::string &s)
{
  if (s.empty()) return {};
  int size = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
  std::wstring result(size, 0);
  MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), result.data(), size);
  return result;
}
#endif
