#include "duckdb/common/string_util.hpp"

#include "duckdb/common/exception.hpp"
#include "duckdb/common/pair.hpp"
#include "duckdb/common/to_string.hpp"
#include "duckdb/common/helper.hpp"
#include "duckdb/function/scalar/string_functions.hpp"

#include <algorithm>
#include <cctype>
#include <iomanip>
#include <memory>
#include <sstream>
#include <stdarg.h>
#include <string.h>
#include <random>

namespace duckdb {

string StringUtil::GenerateRandomName(idx_t length) {
	std::random_device rd;
	std::mt19937 gen(rd());
	std::uniform_int_distribution<> dis(0, 15);

	std::stringstream ss;
	ss << std::hex;
	for (idx_t i = 0; i < length; i++) {
		ss << dis(gen);
	}
	return ss.str();
}

bool StringUtil::Contains(const string &haystack, const string &needle) {
	return (haystack.find(needle) != string::npos);
}

void StringUtil::LTrim(string &str) {
	auto it = str.begin();
	while (it != str.end() && CharacterIsSpace(*it)) {
		it++;
	}
	str.erase(str.begin(), it);
}

// Remove trailing ' ', '\f', '\n', '\r', '\t', '\v'
void StringUtil::RTrim(string &str) {
	str.erase(find_if(str.rbegin(), str.rend(), [](char ch) { return ch > 0 && !CharacterIsSpace(ch); }).base(),
	          str.end());
}

void StringUtil::RTrim(string &str, const string &chars_to_trim) {
	str.erase(find_if(str.rbegin(), str.rend(),
	                  [&chars_to_trim](char ch) { return ch > 0 && chars_to_trim.find(ch) == string::npos; })
	              .base(),
	          str.end());
}

void StringUtil::Trim(string &str) {
	StringUtil::LTrim(str);
	StringUtil::RTrim(str);
}

bool StringUtil::StartsWith(string str, string prefix) {
	if (prefix.size() > str.size()) {
		return false;
	}
	return equal(prefix.begin(), prefix.end(), str.begin());
}

bool StringUtil::EndsWith(const string &str, const string &suffix) {
	if (suffix.size() > str.size()) {
		return false;
	}
	return equal(suffix.rbegin(), suffix.rend(), str.rbegin());
}

string StringUtil::Repeat(const string &str, idx_t n) {
	std::ostringstream os;
	for (idx_t i = 0; i < n; i++) {
		os << str;
	}
	return (os.str());
}

vector<string> StringUtil::Split(const string &str, char delimiter) {
	std::stringstream ss(str);
	vector<string> lines;
	string temp;
	while (getline(ss, temp, delimiter)) {
		lines.push_back(temp);
	}
	return (lines);
}

namespace string_util_internal {

inline void SkipSpaces(const string &str, idx_t &index) {
	while (index < str.size() && std::isspace(str[index])) {
		index++;
	}
}

inline void ConsumeLetter(const string &str, idx_t &index, char expected) {
	if (index >= str.size() || str[index] != expected) {
		throw ParserException("Invalid quoted list: %s", str);
	}

	index++;
}

template <typename F>
inline void TakeWhile(const string &str, idx_t &index, const F &cond, string &taker) {
	while (index < str.size() && cond(str[index])) {
		taker.push_back(str[index]);
		index++;
	}
}

inline string TakePossiblyQuotedItem(const string &str, idx_t &index, char delimiter, char quote) {
	string entry;

	if (str[index] == quote) {
		index++;
		TakeWhile(
		    str, index, [quote](char c) { return c != quote; }, entry);
		ConsumeLetter(str, index, quote);
	} else {
		TakeWhile(
		    str, index, [delimiter, quote](char c) { return c != delimiter && c != quote && !std::isspace(c); }, entry);
	}

	return entry;
}

} // namespace string_util_internal

vector<string> StringUtil::SplitWithQuote(const string &str, char delimiter, char quote) {
	vector<string> entries;
	idx_t i = 0;

	string_util_internal::SkipSpaces(str, i);
	while (i < str.size()) {
		if (!entries.empty()) {
			string_util_internal::ConsumeLetter(str, i, delimiter);
		}

		entries.emplace_back(string_util_internal::TakePossiblyQuotedItem(str, i, delimiter, quote));
		string_util_internal::SkipSpaces(str, i);
	}

	return entries;
}

string StringUtil::Join(const vector<string> &input, const string &separator) {
	return StringUtil::Join(input, input.size(), separator, [](const string &s) { return s; });
}

string StringUtil::Join(const set<string> &input, const string &separator) {
	// The result
	std::string result;

	auto it = input.begin();
	while (it != input.end()) {
		result += *it;
		it++;
		if (it == input.end()) {
			break;
		}
		result += separator;
	}
	return result;
}

string StringUtil::BytesToHumanReadableString(idx_t bytes, idx_t multiplier) {
	D_ASSERT(multiplier == 1000 || multiplier == 1024);
	string db_size;
	idx_t array[6] = {};
	const char *unit[2][6] = {{"bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}, {"bytes", "kB", "MB", "GB", "TB", "PB"}};

	const int sel = (multiplier == 1000);

	array[0] = bytes;
	for (idx_t i = 1; i < 6; i++) {
		array[i] = array[i - 1] / multiplier;
		array[i - 1] %= multiplier;
	}

	for (idx_t i = 5; i >= 1; i--) {
		if (array[i]) {
			// Map 0 -> 0 and (multiplier-1) -> 9
			idx_t fractional_part = (array[i - 1] * 10) / multiplier;
			return to_string(array[i]) + "." + to_string(fractional_part) + " " + unit[sel][i];
		}
	}

	return to_string(array[0]) + (bytes == 1 ? " byte" : " bytes");
}

string StringUtil::Upper(const string &str) {
	string copy(str);
	transform(copy.begin(), copy.end(), copy.begin(), [](unsigned char c) { return std::toupper(c); });
	return (copy);
}

string StringUtil::Lower(const string &str) {
	string copy(str);
	transform(copy.begin(), copy.end(), copy.begin(), [](unsigned char c) { return StringUtil::CharacterToLower(c); });
	return (copy);
}

bool StringUtil::IsLower(const string &str) {
	return str == Lower(str);
}

// Jenkins hash function: https://en.wikipedia.org/wiki/Jenkins_hash_function
uint64_t StringUtil::CIHash(const string &str) {
	uint32_t hash = 0;
	for (auto c : str) {
		hash += StringUtil::CharacterToLower(c);
		hash += hash << 10;
		hash ^= hash >> 6;
	}
	hash += hash << 3;
	hash ^= hash >> 11;
	hash += hash << 15;
	return hash;
}

bool StringUtil::CIEquals(const string &l1, const string &l2) {
	if (l1.size() != l2.size()) {
		return false;
	}
	const auto charmap = LowerFun::ascii_to_lower_map;
	for (idx_t c = 0; c < l1.size(); c++) {
		if (charmap[(uint8_t)l1[c]] != charmap[(uint8_t)l2[c]]) {
			return false;
		}
	}
	return true;
}

bool StringUtil::CILessThan(const string &s1, const string &s2) {
	const auto charmap = UpperFun::ascii_to_upper_map;

	unsigned char u1, u2;

	idx_t length = MinValue<idx_t>(s1.length(), s2.length());
	length += s1.length() != s2.length();
	for (idx_t i = 0; i < length; i++) {
		u1 = (unsigned char)s1[i];
		u2 = (unsigned char)s2[i];
		if (charmap[u1] != charmap[u2]) {
			break;
		}
	}
	return (charmap[u1] - charmap[u2]) < 0;
}

vector<string> StringUtil::Split(const string &input, const string &split) {
	vector<string> splits;

	idx_t last = 0;
	idx_t input_len = input.size();
	idx_t split_len = split.size();
	while (last <= input_len) {
		idx_t next = input.find(split, last);
		if (next == string::npos) {
			next = input_len;
		}

		// Push the substring [last, next) on to splits
		string substr = input.substr(last, next - last);
		if (!substr.empty()) {
			splits.push_back(substr);
		}
		last = next + split_len;
	}
	if (splits.empty()) {
		splits.push_back(input);
	}
	return splits;
}

string StringUtil::Replace(string source, const string &from, const string &to) {
	if (from.empty()) {
		throw InternalException("Invalid argument to StringUtil::Replace - empty FROM");
	}
	idx_t start_pos = 0;
	while ((start_pos = source.find(from, start_pos)) != string::npos) {
		source.replace(start_pos, from.length(), to);
		start_pos += to.length(); // In case 'to' contains 'from', like
		                          // replacing 'x' with 'yx'
	}
	return source;
}

vector<string> StringUtil::TopNStrings(vector<pair<string, idx_t>> scores, idx_t n, idx_t threshold) {
	if (scores.empty()) {
		return vector<string>();
	}
	sort(scores.begin(), scores.end(), [](const pair<string, idx_t> &a, const pair<string, idx_t> &b) -> bool {
		return a.second < b.second || (a.second == b.second && a.first.size() < b.first.size());
	});
	vector<string> result;
	result.push_back(scores[0].first);
	for (idx_t i = 1; i < MinValue<idx_t>(scores.size(), n); i++) {
		if (scores[i].second > threshold) {
			break;
		}
		result.push_back(scores[i].first);
	}
	return result;
}

struct LevenshteinArray {
	LevenshteinArray(idx_t len1, idx_t len2) : len1(len1) {
		dist = make_unsafe_uniq_array<idx_t>(len1 * len2);
	}

	idx_t &Score(idx_t i, idx_t j) {
		return dist[GetIndex(i, j)];
	}

private:
	idx_t len1;
	unsafe_unique_array<idx_t> dist;

	idx_t GetIndex(idx_t i, idx_t j) {
		return j * len1 + i;
	}
};

// adapted from https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#C++
idx_t StringUtil::LevenshteinDistance(const string &s1_p, const string &s2_p, idx_t not_equal_penalty) {
	auto s1 = StringUtil::Lower(s1_p);
	auto s2 = StringUtil::Lower(s2_p);
	idx_t len1 = s1.size();
	idx_t len2 = s2.size();
	if (len1 == 0) {
		return len2;
	}
	if (len2 == 0) {
		return len1;
	}
	LevenshteinArray array(len1 + 1, len2 + 1);
	array.Score(0, 0) = 0;
	for (idx_t i = 0; i <= len1; i++) {
		array.Score(i, 0) = i;
	}
	for (idx_t j = 0; j <= len2; j++) {
		array.Score(0, j) = j;
	}
	for (idx_t i = 1; i <= len1; i++) {
		for (idx_t j = 1; j <= len2; j++) {
			// d[i][j] = std::min({ d[i - 1][j] + 1,
			//                      d[i][j - 1] + 1,
			//                      d[i - 1][j - 1] + (s1[i - 1] == s2[j - 1] ? 0 : 1) });
			auto equal = s1[i - 1] == s2[j - 1] ? 0 : not_equal_penalty;
			idx_t adjacent_score1 = array.Score(i - 1, j) + 1;
			idx_t adjacent_score2 = array.Score(i, j - 1) + 1;
			idx_t adjacent_score3 = array.Score(i - 1, j - 1) + equal;

			idx_t t = MinValue<idx_t>(adjacent_score1, adjacent_score2);
			array.Score(i, j) = MinValue<idx_t>(t, adjacent_score3);
		}
	}
	return array.Score(len1, len2);
}

idx_t StringUtil::SimilarityScore(const string &s1, const string &s2) {
	return LevenshteinDistance(s1, s2, 3);
}

vector<string> StringUtil::TopNLevenshtein(const vector<string> &strings, const string &target, idx_t n,
                                           idx_t threshold) {
	vector<pair<string, idx_t>> scores;
	scores.reserve(strings.size());
	for (auto &str : strings) {
		if (target.size() < str.size()) {
			scores.emplace_back(str, SimilarityScore(str.substr(0, target.size()), target));
		} else {
			scores.emplace_back(str, SimilarityScore(str, target));
		}
	}
	return TopNStrings(scores, n, threshold);
}

string StringUtil::CandidatesMessage(const vector<string> &candidates, const string &candidate) {
	string result_str;
	if (!candidates.empty()) {
		result_str = "\n" + candidate + ": ";
		for (idx_t i = 0; i < candidates.size(); i++) {
			if (i > 0) {
				result_str += ", ";
			}
			result_str += "\"" + candidates[i] + "\"";
		}
	}
	return result_str;
}

string StringUtil::CandidatesErrorMessage(const vector<string> &strings, const string &target,
                                          const string &message_prefix, idx_t n) {
	auto closest_strings = StringUtil::TopNLevenshtein(strings, target, n);
	return StringUtil::CandidatesMessage(closest_strings, message_prefix);
}

static void SkipSpaces(const string &message, idx_t &pos) {
	for (; pos < message.size() && StringUtil::CharacterIsSpace(message[pos]); pos++) {
	}
}

static bool MatchCharacter(const string &message, idx_t &pos, char c) {
	if (pos >= message.size()) {
		return false;
	}
	return message[pos] == c;
}

static string ParseJSONValue(const string &message, idx_t &pos) {
	string result;
	if (!MatchCharacter(message, pos, '"')) {
		// values need to start with a quote
		D_ASSERT(0);
		return result;
	}
	pos++;
	for (; pos < message.size(); pos++) {
		if (message[pos] == '\\') {
			// escape
			pos++;
			if (pos >= message.size()) {
				// escape at end of string!?
				D_ASSERT(0);
				return result;
			}
			switch (message[pos]) {
			case 'r':
				result += '\r';
				break;
			case 'n':
				result += '\n';
				break;
			case 't':
				result += '\t';
				break;
			case 'b':
				result += '\b';
				break;
			case 'f':
				result += '\f';
				break;
			case '0':
				result += '\0';
				break;
			case '\\':
			case '"':
			case '/':
				result += message[pos];
				break;
			default:
				// unsupported escape character
				// NOTE: we do not support unicode escape sequences here
				D_ASSERT(0);
				result += message[pos];
				break;
			}
		} else if (message[pos] == '"') {
			// end of message
			pos++;
			return result;
		} else {
			result += message[pos];
		}
	}
	// no end-of-value found
	D_ASSERT(0);
	return result;
}

unordered_map<string, string> StringUtil::ParseJSONMap(const string &json) {
	unordered_map<string, string> result;
	if (json.empty()) {
		return result;
	}
	idx_t pos = 0;
	SkipSpaces(json, pos);
	if (!MatchCharacter(json, pos, '{')) {
		D_ASSERT(0);
		return result;
	}
	pos++;
	while (true) {
		SkipSpaces(json, pos);
		if (MatchCharacter(json, pos, '}')) {
			// end of object
			break;
		}
		if (!result.empty()) {
			// objects are comma separated
			if (!MatchCharacter(json, pos, ',')) {
				D_ASSERT(0);
				return result;
			}
			pos++;
		}
		string key = ParseJSONValue(json, pos);
		SkipSpaces(json, pos);
		if (!MatchCharacter(json, pos, ':')) {
			D_ASSERT(0);
			return result;
		}
		pos++;
		string value = ParseJSONValue(json, pos);
		auto entry = result.find(key);
		if (entry != result.end()) {
			// entry already exists
			D_ASSERT(0);
			continue;
		}
		result.insert(make_pair(std::move(key), std::move(value)));
	}
	return result;
}

static void WriteJSONValue(const string &value, string &result) {
	result += '"';
	for (auto c : value) {
		// check for characters we need to escape
		switch (c) {
		case '\0':
			result += "\\0";
			break;
		case '\\':
			result += "\\\\";
			break;
		case '\b':
			result += "\\b";
			break;
		case '\f':
			result += "\\f";
			break;
		case '\t':
			result += "\\t";
			break;
		case '\r':
			result += "\\r";
			break;
		case '\n':
			result += "\\n";
			break;
		case '"':
			result += "\\\"";
			break;
		default:
			result += c;
			break;
		}
	}
	result += '"';
}

static void WriteJSONPair(const string &key, const string &value, string &result) {
	WriteJSONValue(key, result);
	result += ":";
	WriteJSONValue(value, result);
}

string StringUtil::ToJSONMap(ExceptionType type, const string &message, const unordered_map<string, string> &map) {
	D_ASSERT(map.find("exception_type") == map.end());
	D_ASSERT(map.find("exception_message") == map.end());
	string result;
	result += "{";
	// we always write exception type/message
	WriteJSONPair("exception_type", Exception::ExceptionTypeToString(type), result);
	result += ",";
	WriteJSONPair("exception_message", message, result);
	for (auto &entry : map) {
		result += ",";
		WriteJSONPair(entry.first, entry.second, result);
	}
	result += "}";
	return result;
}

string StringUtil::GetFileName(const string &file_path) {

	idx_t pos = file_path.find_last_of("/\\");
	if (pos == string::npos) {
		return file_path;
	}
	auto end = file_path.size() - 1;

	// If the rest of the string is just slashes or dots, trim them
	if (file_path.find_first_not_of("/\\.", pos) == string::npos) {
		// Trim the trailing slashes and dots
		while (end > 0 && (file_path[end] == '/' || file_path[end] == '.' || file_path[end] == '\\')) {
			end--;
		}

		// Now find the next slash
		pos = file_path.find_last_of("/\\", end);
		if (pos == string::npos) {
			return file_path.substr(0, end + 1);
		}
	}

	return file_path.substr(pos + 1, end - pos);
}

string StringUtil::GetFileExtension(const string &file_name) {
	auto name = GetFileName(file_name);
	idx_t pos = name.find_last_of('.');
	// We dont consider e.g. `.gitignore` to have an extension
	if (pos == string::npos || pos == 0) {
		return "";
	}
	return name.substr(pos + 1);
}

string StringUtil::GetFileStem(const string &file_name) {
	auto name = GetFileName(file_name);
	if (name.size() > 1 && name[0] == '.') {
		return name;
	}
	idx_t pos = name.find_last_of('.');
	if (pos == string::npos) {
		return name;
	}
	return name.substr(0, pos);
}

string StringUtil::GetFilePath(const string &file_path) {

	// Trim the trailing slashes
	auto end = file_path.size() - 1;
	while (end > 0 && (file_path[end] == '/' || file_path[end] == '\\')) {
		end--;
	}

	auto pos = file_path.find_last_of("/\\", end);
	if (pos == string::npos) {
		return "";
	}

	while (pos > 0 && (file_path[pos] == '/' || file_path[pos] == '\\')) {
		pos--;
	}

	return file_path.substr(0, pos + 1);
}

} // namespace duckdb
