/**
 * strings.cpp: Useful functions for manipulating strings
 **/
#include "core/strings.h"
#include "core/session.h" // locale

#include <QByteArray>
#include <QCoreApplication>
#include <QDebug>
#include <QLocale>
#include <QString>
#include <QTextDocument>

#include <algorithm>
#include <cctype>
#include <codecvt>
#include <iomanip>
#include <iostream>
#include <locale>
#include <string>
#include <unordered_map>
#include <vector>

#include "utf8proc.h"

namespace Strings {

/* ew */
std::string Implode(const std::vector<std::string> &vector, const std::string &delimiter)
{
	if (vector.size() < 1)
		return "";

	std::string out;

	for (unsigned long long i = 0; i < vector.size(); i++) {
		out.append(vector.at(i));
		if (i < vector.size() - 1)
			out.append(delimiter);
	}

	return out;
}

std::vector<std::string> Split(const std::string &text, const std::string &delimiter)
{
	if (text.length() < 1)
		return {};

	std::vector<std::string> tokens;

	std::size_t start = 0, end = 0;
	while ((end = text.find(delimiter, start)) != std::string::npos) {
		tokens.push_back(text.substr(start, end - start));
		start = end + delimiter.length();
	}
	tokens.push_back(text.substr(start));

	return tokens;
}

/* This function is really only used for cleaning up the synopsis of
 * horrible HTML debris from AniList :)
 */
void ReplaceAll(std::string &string, std::string_view find, std::string_view replace)
{
	size_t pos = 0;
	while ((pos = string.find(find, pos)) != std::string::npos) {
		string.replace(pos, find.length(), replace);
		pos += replace.length();
	}
}

void ConvertRomanNumerals(std::string &string)
{
	static const std::vector<std::pair<std::string_view, std::string_view>> vec = {
	    {"2",  "II"  },
        {"3",  "III" },
        {"4",  "IV"  },
        {"5",  "V"   },
        {"6",  "VI"  },
        {"7",  "VII" },
	    {"8",  "VIII"},
        {"9",  "IX"  },
        {"11", "XI"  },
        {"12", "XII" },
        {"13", "XIII"}
    };

	for (const auto &item : vec)
		ReplaceAll(string, item.second, item.first);
}

/* this also performs case folding, so our string is lowercase after this */
void NormalizeUnicode(std::string &string)
{
	static constexpr utf8proc_option_t options = static_cast<utf8proc_option_t>(
	    UTF8PROC_COMPAT | UTF8PROC_COMPOSE | UTF8PROC_STABLE | UTF8PROC_IGNORE | UTF8PROC_STRIPCC | UTF8PROC_STRIPMARK |
	    UTF8PROC_LUMP | UTF8PROC_CASEFOLD | UTF8PROC_NLF2LS);

	/* ack */
	utf8proc_uint8_t *buf = nullptr;

	const utf8proc_ssize_t size =
	    utf8proc_map(reinterpret_cast<const utf8proc_uint8_t *>(string.data()), string.size(), &buf, options);

	if (buf) {
		if (size)
			string.assign(reinterpret_cast<const char *>(buf), size);

		std::free(buf);
	}
}

void NormalizeAnimeTitle(std::string &string)
{
	ConvertRomanNumerals(string);
	NormalizeUnicode(string);
	RemoveLeadingChars(string, ' ');
	RemoveTrailingChars(string, ' ');
}

void TextifySynopsis(std::string &string)
{
	/* Just let Qt deal with it. */
	QTextDocument text;
	text.setHtml(Strings::ToQString(string));
	string = Strings::ToUtf8String(text.toPlainText());
}

/* let Qt handle the heavy lifting of locale shit
 * I don't want to deal with
 */
std::string ToUpper(const std::string &string)
{
	return ToUtf8String(session.config.locale.GetLocale().toUpper(ToQString(string)));
}

std::string ToLower(const std::string &string)
{
	return ToUtf8String(session.config.locale.GetLocale().toLower(ToQString(string)));
}

#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
std::wstring ToWstring(const std::string &string)
{
	static std::wstring_convert<std::codecvt_utf8<wchar_t>> converter("", L"");

	std::wstring wstr;
	try {
		wstr = converter.from_bytes(string);
	} catch (std::range_error const &ex) {
		/* XXX how? */
		std::cerr << "Failed to convert UTF-8 to wide string!" << std::endl;
	}
	return wstr;
}

std::string ToUtf8String(const std::wstring &wstring)
{
	static std::wstring_convert<std::codecvt_utf8<wchar_t>> converter("", L"");
	return converter.to_bytes(wstring);
}

std::string ToUtf8String(const std::u32string &u32string)
{
	static std::wstring_convert<std::codecvt_utf8_utf16<char32_t>, char32_t> converter;
	return converter.to_bytes(u32string);
}

std::u32string ToUcs4String(const std::string &string)
{
	static std::wstring_convert<std::codecvt_utf8_utf16<char32_t>, char32_t> converter;
	return converter.from_bytes(string);
}
#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif

std::wstring ToWstring(const QString &string)
{
	std::wstring arr(string.size(), L'\0');
	string.toWCharArray(&arr.front());
	return arr;
}

std::string ToUtf8String(const QString &string)
{
	const QByteArray ba = string.toUtf8();
	return std::string(ba.constData(), ba.size());
}

std::string ToUtf8String(const QByteArray &ba)
{
	return std::string(ba.constData(), ba.size());
}

QString ToQString(const std::string &string)
{
	return QString::fromUtf8(string.c_str(), string.length());
}

QString ToQString(const std::wstring &wstring)
{
	return QString::fromWCharArray(wstring.c_str(), wstring.length());
}

std::string ToUtf8String(const bool b)
{
	return b ? "true" : "false"; // lol
}

#if defined(__APPLE__) && defined(__MACH__)
CFStringRef ToCFString(const std::string &string)
{
	return CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(string.data()), string.size(), kCFStringEncodingUTF8, false);
}
std::string ToUtf8String(CFStringRef str)
{
	if (const char *ptr = CFStringGetCStringPtr(str, kCFStringEncodingUTF8))
		return std::string(ptr); // easy!

	// ...
	const CFIndex len = CFStringGetLength(str);
	std::string buf(CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8), 0);
	CFRange range;
	range.length = len;
	range.location = 0;
	CFIndex used;
	CFStringGetBytes(str, range, kCFStringEncodingUTF8, 0, false, reinterpret_cast<UInt8 *>(buf.data()), buf.size(), &used);
	buf.resize(used);
	return buf;
}
#endif

bool ToBool(const std::string &str, bool def)
{
	std::string l = Strings::ToLower(str);

	return (l == "true") ? true : (l == "false") ? false : def;
}

template<typename T>
constexpr T ipow(T num, unsigned int pow)
{
	return (pow >= sizeof(unsigned int) * 8) ? 0 : pow == 0 ? 1 : num * ipow(num, pow - 1);
}

/* util funcs */
uint64_t HumanReadableSizeToBytes(const std::string &str)
{
	static const std::unordered_map<std::string, uint64_t> bytes_map = {
	    {"KB",  1e3       },
        {"MB",  1e6       },
        {"GB",  1e9       },
        {"TB",  1e12      },
	    {"PB",  1e15      },
        {"KiB", 1ull << 10},
        {"MiB", 1ull << 20},
        {"GiB", 1ull << 30},
	    {"TiB", 1ull << 40},
        {"PiB", 1ull << 50}  /* surely we won't need more than this */
	};

	for (const auto &suffix : bytes_map) {
		if (str.find(suffix.first) != std::string::npos) {
			try {
				uint64_t size = std::stod(str) * suffix.second;
				return size;
			} catch (std::invalid_argument const &ex) {
				continue;
			}
		}
	}

	return ToInt(str, 0);
}

std::string BytesToHumanReadableSize(uint64_t bytes, int precision)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
	/* QLocale in Qt >= 5.10.0 has a function for this */
	return Strings::ToUtf8String(session.config.locale.GetLocale().formattedDataSize(bytes, precision));
#else
	static const std::unordered_map<uint64_t, std::string> map = {
	    {1ull << 10, "KiB"},
        {1ull << 20, "MiB"},
        {1ull << 30, "GiB"},
        {1ull << 40, "TiB"},
        {1ull << 50, "PiB"}
    };

	for (const auto &suffix : map) {
		if (bytes / suffix.first < 1)
			continue;

		std::stringstream ss;
		ss << std::setprecision(precision) << (static_cast<double>(bytes) / suffix.first) << " " << suffix.second;
		return ss.str();
	}

	/* better luck next time */
	return "0 bytes";
#endif
}

void RemoveLeadingChars(std::string &s, const char c)
{
	s.erase(0, std::min(s.find_first_not_of(c), s.size() - 1));
}

void RemoveTrailingChars(std::string &s, const char c)
{
	s.erase(s.find_last_not_of(c) + 1, std::string::npos);
}

bool BeginningMatchesSubstring(const std::string &str, const std::string &sub)
{
	for (unsigned long long i = 0; i < str.length() && i < sub.length(); i++)
		if (str[i] != sub[i])
			return false;

	return true;
}

std::string Translate(const char *str)
{
	return Strings::ToUtf8String(QCoreApplication::tr(str));
}

/* Moved here from glib code because xsettings parser needs it */
bool IsGTKThemeDark(const std::string_view str)
{
	/* if that doesn't exist, use the GTK theme and check for some known
	 * suffixes. if one is found, return
	 *
	 * XXX probably better to use case folding here */
	static constexpr std::array<std::string_view, 3> suffixes = {
	    "-dark",   /* Adwaita-dark */
	    "-Dark",   /* Arc-Dark */
	    "-Darker", /* Arc-Darker */
	};

	for (const auto &suffix : suffixes) {
		if (str.size() < suffix.size())
			continue;

		if (std::equal(str.data() + str.size() - suffix.length(), str.data() + str.size(), suffix.begin(),
		               suffix.end()))
			return true;
	}

	return false;
}

} // namespace Strings
