#include "utils/string.hpp"

#include <algorithm>
#include <iomanip>
#include <sstream>
#include <utility>

POLYBAR_NS

namespace string_util {
  /**
   * Check if haystack contains needle
   */
  bool contains(const string& haystack, const string& needle) {
    return haystack.find(needle) != string::npos;
  }

  bool ends_with(const string& haystack, const string& suffix) {
    if (haystack.length() < suffix.length()) {
      return false;
    }

    return haystack.compare(haystack.length() - suffix.length(), suffix.length(), suffix) == 0;
  }

  /**
   * Check if haystack contains needle ignoring case
   */
  bool contains_ignore_case(const string& haystack, const string& needle) {
    return lower(haystack).find(lower(needle)) != string::npos;
  }

  /**
   * Convert string to uppercase
   */
  string upper(const string& s) {
    string str(s);
    for (auto& c : str) {
      c = toupper(c);
    }
    return str;
  }

  /**
   * Convert string to lowercase
   */
  string lower(const string& s) {
    string str(s);
    for (auto& c : str) {
      c = tolower(c);
    }
    return str;
  }

  /**
   * Test lower case equality
   */
  bool compare(const string& s1, const string& s2) {
    return lower(s1) == lower(s2);
  }

  /**
   * Replace first occurrence of needle in haystack
   */
  string replace(const string& haystack, const string& needle, const string& replacement, size_t start, size_t end) {
    string str(haystack);
    string::size_type pos;

    if (needle != replacement && (pos = str.find(needle, start)) != string::npos) {
      if (end == string::npos || pos < end) {
        str = str.replace(pos, needle.length(), replacement);
      }
    }

    return str;
  }

  /**
   * Replace all occurrences of needle in haystack
   */
  string replace_all(
      const string& haystack, const string& needle, const string& replacement, size_t start, size_t end) {
    string result{haystack};
    string::size_type pos;
    while ((pos = result.find(needle, start)) != string::npos && pos < result.length() &&
           (end == string::npos || pos + needle.length() <= end)) {
      result.replace(pos, needle.length(), replacement);
      start = pos + replacement.length();
    }
    return result;
  }

  /**
   * Replace all consecutive occurrences of needle in haystack
   */
  string squeeze(const string& haystack, char needle) {
    string result = haystack;
    while (result.find({needle, needle}) != string::npos) {
      result = replace_all(result, {needle, needle}, {needle});
    }
    return result;
  }

  /**
   * Remove all occurrences of needle in haystack
   */
  string strip(const string& haystack, char needle) {
    string str(haystack);
    string::size_type pos;
    while ((pos = str.find(needle)) != string::npos) {
      str.erase(pos, 1);
    }
    return str;
  }

  /**
   * Remove trailing newline
   */
  string strip_trailing_newline(const string& haystack) {
    string str(haystack);
    if (str[str.length() - 1] == '\n') {
      str.erase(str.length() - 1, 1);
    }
    return str;
  }

  /**
   * Trims all characters that match pred from the left
   */
  string ltrim(string value, function<bool(char)> pred) {
    value.erase(value.begin(), find_if(value.begin(), value.end(), not1(pred)));
    return value;
  }

  /**
   * Trims all characters that match pred from the right
   */
  string rtrim(string value, function<bool(char)> pred) {
    value.erase(find_if(value.rbegin(), value.rend(), not1(pred)).base(), value.end());
    return value;
  }

  /**
   * Trims all characters that match pred from both sides
   */
  string trim(string value, function<bool(char)> pred) {
    return ltrim(rtrim(move(value), pred), pred);
  }

  /**
   * Remove needle from the start of the string
   */
  string ltrim(string&& value, const char& needle) {
    if (value.empty()) {
      return "";
    }
    while (*value.begin() == needle) {
      value.erase(0, 1);
    }
    return forward<string>(value);
  }

  /**
   * Remove needle from the end of the string
   */
  string rtrim(string&& value, const char& needle) {
    if (value.empty()) {
      return "";
    }
    while (*(value.end() - 1) == needle) {
      value.erase(value.length() - 1, 1);
    }
    return forward<string>(value);
  }

  /**
   * Remove needle from the start and end of the string
   */
  string trim(string&& value, const char& needle) {
    if (value.empty()) {
      return "";
    }
    return rtrim(ltrim(forward<string>(value), needle), needle);
  }

  /**
   * Counts the number of codepoints in a utf8 encoded string.
   */
  size_t char_len(const string& value) {
    // utf-8 bytes of the form 10xxxxxx are continuation bytes, so we
    // simply count the number of bytes not of this form.
    //
    // 0xc0 = 11000000
    // 0x80 = 10000000
    return std::count_if(value.begin(), value.end(), [](char c) { return (c & 0xc0) != 0x80; });
  }

  /**
   * Truncates a utf8 string at len number of codepoints. This isn't 100%
   * matching the user-perceived character count, but it should be close
   * enough and avoids having to pull in something like ICU to count actual
   * grapheme clusters.
   */
  string utf8_truncate(string&& value, size_t len) {
    if (value.empty()) {
      return "";
    }

    // utf-8 bytes of the form 10xxxxxx are continuation bytes, so we
    // simply jump forward to bytes not of that form and truncate starting
    // at that byte if we've counted too many codepoints
    //
    // 0xc0 = 11000000
    // 0x80 = 10000000
    auto it = value.begin();
    auto end = value.end();
    for (size_t i = 0; i < len; ++i) {
      if (it == end)
        break;
      ++it;
      it = std::find_if(it, end, [](char c) { return (c & 0xc0) != 0x80; });
    }
    value.erase(it, end);

    return forward<string>(value);
  }

  /**
   * Join all strings in vector into a single string separated by delim
   */
  string join(const vector<string>& strs, const string& delim) {
    string str;
    for (auto& s : strs) {
      str += (str.empty() ? "" : delim) + s;
    }
    return str;
  }

  /**
   * Explode string by delim, ignore empty tokens
   */
  vector<string> split(const string& s, char delim) {
    std::string::size_type pos = 0;
    std::vector<std::string> result;

    while ((pos = s.find_first_not_of(delim, pos)) != std::string::npos) {
      auto nextpos = s.find_first_of(delim, pos);
      result.emplace_back(s.substr(pos, nextpos - pos));
      pos = nextpos;
    }

    return result;
  }

  /**
   * Explode string by delim, include empty tokens
   */
  std::vector<std::string> tokenize(const string& str, char delimiters) {
    std::vector<std::string> tokens;
    std::string::size_type lastPos = 0;
    auto pos = str.find_first_of(delimiters, lastPos);

    while (pos != std::string::npos && lastPos != std::string::npos) {
      tokens.emplace_back(str.substr(lastPos, pos - lastPos));

      lastPos = pos + 1;
      pos = str.find_first_of(delimiters, lastPos);
    }

    tokens.emplace_back(str.substr(lastPos, pos - lastPos));
    return tokens;
  }

  /**
   * Find the nth occurrence of needle in haystack starting from pos
   */
  size_t find_nth(const string& haystack, size_t pos, const string& needle, size_t nth) {
    size_t found_pos = haystack.find(needle, pos);
    if (1 == nth || string::npos == found_pos) {
      return found_pos;
    }
    return find_nth(haystack, found_pos + 1, needle, nth - 1);
  }

  /**
   * Create a floating point string
   */
  string floating_point(double value, size_t precision, bool fixed, const string& locale) {
    std::stringstream ss;
    ss.imbue(!locale.empty() ? std::locale(locale.c_str()) : std::locale::classic());
    ss << std::fixed << std::setprecision(precision) << value;
    return fixed ? ss.str() : replace(ss.str(), ".00", "");
  }

  /**
   * Create a MiB filesize string
   */
  string filesize_mib(unsigned long long kibibytes, size_t precision, const string& locale) {
    return floating_point(kibibytes / 1024.0, precision, true, locale) + " MiB";
  }

  /**
   * Create a GiB filesize string
   */
  string filesize_gib(unsigned long long kibibytes, size_t precision, const string& locale) {
    return floating_point(kibibytes / 1024.0 / 1024.0, precision, true, locale) + " GiB";
  }

  /**
   * Create a GiB string, if the value in GiB is >= 1.0. Otherwise, create a MiB string.
   */
  string filesize_gib_mib(
      unsigned long long kibibytes, size_t precision_mib, size_t precision_gib, const string& locale) {
    if (kibibytes < 1024 * 1024) {
      return filesize_mib(kibibytes, precision_mib, locale);
    } else {
      return filesize_gib(kibibytes, precision_gib, locale);
    }
  }

  /**
   * Create a filesize string by converting given bytes to highest unit possible
   */
  string filesize(unsigned long long bytes, size_t precision, bool fixed, const string& locale) {
    vector<string> suffixes{"TB", "GB", "MB", "KB"};
    string suffix{"B"};
    double value = bytes;
    while (!suffixes.empty() && value >= 1024.0) {
      suffix = suffixes.back();
      suffixes.pop_back();
      value /= 1024.0;
    }
    return floating_point(value, precision, fixed, locale) + " " + suffix;
  }

  /**
   * Compute string hash
   */
  hash_type hash(const string& src) {
    return std::hash<string>()(src);
  }
}  // namespace string_util

POLYBAR_NS_END
