#pragma once

#include "nil.qh"
#include "sort.qh"
#include "oo.qh"

// this is not exactly 16KiB (16384 bytes) because one byte is reserved for the \0 terminator
#define VM_TEMPSTRING_MAXSIZE 16383

// string logic
//
// true: is truthy
// == "": is equal to ""
// is "": has the same string index as the string constant ""
// strunzone: can be strunzoned
//
// |              | true | == "" | is "" | strunzone |
// | :----------: | :--: | :---: | :---: | :-------: |
// | nil          |      | yes   |       |           |
// | strcat(nil)  | yes  | yes   |       |           |
// | strzone(nil) | yes  | yes   |       | yes       |
// | ""           | yes  | yes   | yes   |           |
// | strcat("")   | yes  | yes   |       |           |
// | strzone("")  | yes  | yes   |       | yes       |
// | "s"          | yes  |       |       |           |
// | strcat("s")  | yes  |       |       |           |
// | strzone("s") | yes  |       |       | yes       |

#ifdef CSQC
	float stringwidth_colors(string s, vector theSize)
	{
		return stringwidth_builtin(s, true, theSize);
	}

	float stringwidth_nocolors(string s, vector theSize)
	{
		return stringwidth_builtin(s, false, theSize);
	}
#elifdef MENUQC
	float stringwidth_colors(string s, vector theSize)
	{
		return stringwidth(s, true, theSize);
	}

	float stringwidth_nocolors(string s, vector theSize)
	{
		return stringwidth(s, false, theSize);
	}
#endif

#define strcpy(this, s) MACRO_BEGIN \
	if (this) \
		strunzone(this); \
	this = strzone(s); \
MACRO_END

#define strfree(this) MACRO_BEGIN \
	if (this) \
		strunzone(this); \
	this = string_null; \
MACRO_END

/// Returns the number of days since 0000-03-01 (March 1, year 0).
/// Starting counting from March, as the 1st month of the year, February becomes the 12th and last month,
/// so its variable duration does not affect, given that the 29th is the last day of the period
ERASEABLE
int days_up_to_date(int Y, int M, int D)
{
	int years = (M <= 2) ? Y - 1 : Y;

	int leap_days = floor(years / 4) - floor(years / 100) + floor(years / 400);

	// using these 2 formulas to save 2 arrays or switches (performance isn't important here)
	int months = (M <= 2) ? (M + 9) : (M - 3); // 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
	int leftover_days = (M <= 2) ? (M + 5) : floor(0.58 * M - 1.1); // 6, 7, 0, 1, 1, 2, 2, 3, 4, 4, 5, 5

	int month_days = 30 * months + leftover_days;

	return 365 * years + month_days + D + leap_days;
}

#define DAYS_UP_TO_EPOCH 719469 // days_up_to_date(1970, 1, 1);

/// Returns the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
/// This function exists only as a replacement for strftime(false, "%s") which doesn't work
/// on Windows (%s is not supported) and at least in some linux systems doesn't return the
/// correct result.
/// NOTE: at the current date, the number (string) returned by both strftime(false, "%s") and
/// strftime_s() is so high that can't be converted to int (with ftos) without precision loss
ERASEABLE
string strftime_s()
{
	string date = strftime(false, "%Y-%m-%d %H:%M:%S");
	int i, seconds = 0;
	i =0; int Y = stof(substring(date, i, 4)); // years
	i+=5; int M = stof(substring(date, i, 2)); // months
	i+=3; int D = stof(substring(date, i, 2)); // days

	i+=3; seconds += stof(substring(date, i, 2)) * 60 * 60; // hours
	i+=3; seconds += stof(substring(date, i, 2)) * 60; // minutes
	i+=3; seconds += stof(substring(date, i, 2)); // seconds

	// doing so we loose precision
	//seconds += (days_up_to_date(Y, M, D) - DAYS_UP_TO_EPOCH) * 24 * 60 * 60;
	//return ftos(seconds);

	int days_since_epoch = days_up_to_date(Y, M, D) - DAYS_UP_TO_EPOCH;
	// use hundreds of seconds as unit to avoid precision loss
	int hundreds_of_seconds = days_since_epoch * 24 * 6 * 6;
	hundreds_of_seconds += floor(seconds / 100);

	// tens of seconds and seconds
	string seconds_str = ftos(seconds % 100);
	if ((seconds % 100) < 10)
		seconds_str = strcat("0", seconds_str);

	return strcat(ftos(hundreds_of_seconds), seconds_str);
}

/// \param[in] seconds number of seconds, can be negative too
/// \return time as "m:ss" string (floored)
ERASEABLE
string seconds_tostring(float seconds)
{
	bool negative = false;
	if (seconds < 0)
	{
		negative = true;
		seconds = -seconds;
		if (floor(seconds) != seconds)
			++seconds; // make floor work in the other direction
	}
	int minutes = floor(seconds / 60);
	seconds -= minutes * 60;
	if (negative)
		return sprintf("-%d:%02d", minutes, seconds);
	return sprintf("%d:%02d", minutes, seconds);
}

/// \param[in] tm integer clocked time in tenths or hundredths, CANNOT be negative
/// \param[in] hundredths if true append hundredths too, otherwise only tenths
/// \param[in] compact if true leading 0s are omitted (except the seconds unit digit)
/// \return clocked time as "m:ss.t" or "m:ss.th" string (rounded)
ERASEABLE
string clockedtime_tostring(int tm, bool hundredths, bool compact)
{
	if (tm < 0)
	{
		if (compact)
			return strcat("0.0", hundredths ? "0" : "");
		else
			return strcat("0:00.0", hundredths ? "0" : "");
	}
	int acc = hundredths ? 6000 : 600;
	tm = floor(tm + 0.5);
	int minutes = floor(tm / acc);
	int tm_without_minutes = tm - minutes * acc;
	// NOTE: the start digit of s is a placeholder and won't be displayed
	string s = ftos(acc * 10 + tm_without_minutes);
	if (!compact || minutes > 0)
		return strcat(ftos(minutes), ":", substring(s, 1, 2), ".", substring(s, 3, hundredths ? 2 : 1));

	int ofs = 2, digits = 1;
	if (tm_without_minutes >= 10 * (hundredths ? 100 : 10))
	{
		ofs = 1;
		digits = 2;
	}
	return strcat(substring(s, ofs, digits), ".", substring(s, 3, hundredths ? 2 : 1));

}

#define mmsst(tm, compact)  clockedtime_tostring(tm, false, compact)
#define mmssth(tm, compact) clockedtime_tostring(tm, true,  compact)

ERASEABLE
string format_time(float seconds)
{
	seconds = floor(seconds + 0.5);
	float days = floor(seconds / 864000);
	seconds -= days * 864000;
	float hours = floor(seconds / 36000);
	seconds -= hours * 36000;
	float minutes = floor(seconds / 600);
	seconds -= minutes * 600;
	if (days > 0)
		return sprintf(_("%d days, %02d:%02d:%02d"), days, hours, minutes, seconds);
	return sprintf(_("%02d:%02d:%02d"), hours, minutes, seconds);
}

int ColorTranslateMode;

ERASEABLE
string ColorTranslateRGB(string s)
{
	return (ColorTranslateMode & 1) ? strdecolorize(s) : s;
}

#ifdef GAMEQC
// color code replace, place inside of sprintf and parse the string... defaults described as constants
// foreground/normal colors
string autocvar_hud_colorset_foreground_1 = "2"; ///< F1 - Green  :  primary priority (important names, etc)
string autocvar_hud_colorset_foreground_2 = "3"; ///< F2 - Yellow :  secondary priority (items, locations, numbers, etc)
string autocvar_hud_colorset_foreground_3 = "4"; ///< F3 - Blue   :  tertiary priority or relatively inconsequential text
string autocvar_hud_colorset_foreground_4 = "1"; ///< F4 - Red    :  notice/attention grabbing texting
// "kill" colors
string autocvar_hud_colorset_kill_1 = "1";       ///< K1 - Red    :  "bad" or "dangerous" text (death messages against you, kill notifications, etc)
string autocvar_hud_colorset_kill_2 = "3";       ///< K2 - Yellow :  similar to above, but less important... OR, a highlight out of above message type
string autocvar_hud_colorset_kill_3 = "4";       ///< K3 - Blue   :  "good" or "beneficial" text (you fragging someone, etc)
// background color
string autocvar_hud_colorset_background = "7";   ///< BG - White  :  neutral/unimportant text

/// color code replace, place inside of sprintf and parse the string
string CCR(string input)
{
	// foreground/normal colors
	input = strreplace("^F1", strcat("^", autocvar_hud_colorset_foreground_1), input);
	input = strreplace("^F2", strcat("^", autocvar_hud_colorset_foreground_2), input);
	input = strreplace("^F3", strcat("^", autocvar_hud_colorset_foreground_3), input);
	input = strreplace("^F4", strcat("^", autocvar_hud_colorset_foreground_4), input);

	// "kill" colors
	input = strreplace("^K1", strcat("^", autocvar_hud_colorset_kill_1), input);
	input = strreplace("^K2", strcat("^", autocvar_hud_colorset_kill_2), input);
	input = strreplace("^K3", strcat("^", autocvar_hud_colorset_kill_3), input);

	// background colors
	input = strreplace("^BG", strcat("^", autocvar_hud_colorset_background), input);
	input = strreplace("^N", "^7", input);  // "none"-- reset to white...
	return input;
}
#endif

#define startsWith(haystack, needle) (strstrofs(haystack, needle, 0) == 0)

ERASEABLE
bool startsWithNocase(string haystack, string needle)
{
	return strcasecmp(substring(haystack, 0, strlen(needle)), needle) == 0;
}

noref string _endsWith_suffix;
#define endsWith(this, suffix) (_endsWith_suffix = suffix, substring(this, -strlen(_endsWith_suffix), -1) == _endsWith_suffix)

/// unzone the string, and return it as tempstring. Safe to be called on string_null
ERASEABLE
string fstrunzone(string s)
{
	if (!s)
		return s;
	string sc = strcat(s, "");
	strunzone(s);
	return sc;
}

/// Returns first word
ERASEABLE
string car(string s)
{
	int o = strstrofs(s, " ", 0);
	if (o < 0)
		return s;
	return substring(s, 0, o);
}

/// Returns all but first word
ERASEABLE
string cdr(string s)
{
	int o = strstrofs(s, " ", 0);
	if (o < 0)
		return string_null;
	return substring(s, o + 1, strlen(s) - (o + 1));
}

ERASEABLE
string cons(string a, string b)
{
	if (a == "")  return b;
	if (b == "")  return a;
	return strcat(a, " ", b);
}

ERASEABLE
string cons_mid(string a, string mid, string b)
{
	if (a == "")  return b;
	if (b == "")  return a;
	return strcat(a, mid, b);
}

ERASEABLE
string substring_range(string s, float b, float e)
{
	return substring(s, b, e - b);
}

ERASEABLE
string swapwords(string str, float i, float j)
{
	string s1, s2, s3, s4, s5;
	float si, ei, sj, ej, s0, en;
	int n = tokenizebyseparator(str, " ");
	si = argv_start_index(i);
	sj = argv_start_index(j);
	ei = argv_end_index(i);
	ej = argv_end_index(j);
	s0 = argv_start_index(0);
	en = argv_end_index(n - 1);
	s1 = substring_range(str, s0, si);
	s2 = argv(i);
	s3 = substring_range(str, ei, sj);
	s4 = argv(j);
	s5 = substring_range(str, ej, en);
	return strcat(s1, s4, s3, s2, s5);
}

string _shufflewords_str;
ERASEABLE
void _shufflewords_swapfunc(float i, float j, entity pass)
{
	_shufflewords_str = swapwords(_shufflewords_str, i, j);
}

ERASEABLE
string shufflewords(string str)
{
	_shufflewords_str = str;
	int n = tokenizebyseparator(str, " ");
	shuffle(n, _shufflewords_swapfunc, NULL);
	str = _shufflewords_str;
	_shufflewords_str = string_null;
	return str;
}

ERASEABLE
string unescape(string in)
{
	in = strzone(in);  // but it doesn't seem to be necessary in my tests at least

	int len = strlen(in);
	string str = "";
	for (int i = 0; i < len; ++i)
	{
		string s = substring(in, i, 1);
		if (s == "\\")
		{
			s = substring(in, i + 1, 1);
			if (s == "n")
				str = strcat(str, "\n");
			else if (s == "\\")
				str = strcat(str, "\\");
			else
				str = strcat(str, substring(in, i, 2));
			++i;
			continue;
		}
		str = strcat(str, s);
	}
	strunzone(in);
	return str;
}

ERASEABLE
string strwords(string s, int w)
{
	int endpos = 0;
	for (; w && endpos >= 0; --w)
		endpos = strstrofs(s, " ", endpos + 1);
	if (endpos < 0)
		return s;
	return substring(s, 0, endpos);
}

#define strhasword(s, w) (strstrofs(strcat(" ", s, " "), strcat(" ", w, " "), 0) >= 0)

ERASEABLE
int u8_strsize(string s)
{
	int l = 0;
	for (int i = 0, c; (c = str2chr(s, i)) > 0; ++i, ++l)
	{
		l += (c >= 0x80);
		l += (c >= 0x800);
		l += (c >= 0x10000);
	}
	return l;
}

// List of Unicode spaces: http://jkorpela.fi/chars/spaces.html
ERASEABLE
bool isInvisibleString(string s)
{
	s = strdecolorize(s);
	bool utf8 = cvar("utf8_enable");
	for (int i = 0, n = strlen(s); i < n; ++i)
	{
		int c = str2chr(s, i);
		switch (c)
		{
			case 0:
			case 32:           // space
				break;
			case 192:          // charmap space
				if (!utf8)
					break;
				return false;
			case 0xE000: // invisible char of the utf8 quake charmap
			case 0xE00A: // invisible char of the utf8 quake charmap
			case 0xE0A0: // invisible char of the utf8 quake charmap
			case 0xE020: // invisible char of the utf8 quake charmap
			case 0x00A0: // NO-BREAK SPACE
			case 0x180E: // MONGOLIAN VOWEL SEPARATOR
			case 0x2000: // EN QUAD
			case 0x2001: // EM QUAD
			case 0x2002: // EN SPACE
			case 0x2003: // EM SPACE
			case 0x2004: // THREE-PER-EM SPACE
			case 0x2005: // FOUR-PER-EM SPACE
			case 0x2006: // SIX-PER-EM SPACE
			case 0x2007: // FIGURE SPACE
			case 0x2008: // PUNCTUATION SPACE
			case 0x2009: // THIN SPACE
			case 0x200A: // HAIR SPACE
			case 0x200B: // ZERO WIDTH SPACE
			case 0x202F: // NARROW NO-BREAK SPACE
			case 0x205F: // MEDIUM MATHEMATICAL SPACE
			case 0x3000: // IDEOGRAPHIC SPACE
			case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE
			case 0xFFA0: // Halfwidth Hangul Filler
			case 0x3164: // Hangul Filler
				if (utf8)
					break;
			default:
				return false;
		}
	}
	return true;
}

// Multiline text file buffers

ERASEABLE
int buf_load(string pFilename)
{
	int buf = buf_create();
	if (buf < 0)
		return -1;
	int fh = fopen(pFilename, FILE_READ);
	if (fh < 0)
	{
		buf_del(buf);
		return -1;
	}
	string l;
	for (int i = 0; (l = fgets(fh)); ++i)
		bufstr_set(buf, i, l);
	fclose(fh);
	return buf;
}

ERASEABLE
void buf_save(float buf, string pFilename)
{
	int fh = fopen(pFilename, FILE_WRITE);
	if (fh < 0)
		error(strcat("Can't write buf to ", pFilename));
	int n = buf_getsize(buf);
	for (int i = 0; i < n; ++i)
		fputs(fh, strcat(bufstr_get(buf, i), "\n"));
	fclose(fh);
}

/// Converts a number to a string with the indicated number of decimals
ERASEABLE
string ftos_decimals(float number, int decimals)
{
	// inhibit stupid negative zero
	if (number == 0) number = 0;
	return sprintf("%.*f", decimals, number);
}

/// Converts a percentage to a string with the indicated number of decimals
ERASEABLE
string ftos_decimals_percentage(float number, int decimals)
{
	if (number == 0)
		number = 0; // inhibit stupid negative zero
	return sprintf(_("%.*f%%"), decimals, number * 100); // translatable percentage string
}

/**
 * Converts a number to a string with the minimum number of decimals
 * It assumes that an extreme accuracy of the input number is not needed and
 * reduces the max number of decimals to 4 in order to hide minor representation
 * errors that can easily happen even with small numbers, e.g.:
 *   x = 32.1;  sprintf("%f", x): "32.099998"
 *   x = 4; x += 0.01; x += 0.01; sprintf("%f", x): "4.020001"
 * Reducing the max number of decimals to 5 is not enough to hide most errors.
 */
ERASEABLE
string ftos_mindecimals(float number)
{
	// inhibit stupid negative zero
	if (fabs(number) < 0.0001)
		return "0";

	int rounded = rint(number);
	if (fabs(number - rounded) < 0.0001)
		return ftos(rounded);

	string s = sprintf("%.4f", number);

	// if the decimal part got lost (large numbers), return what we got
	int dot_ofs = strstrofs(s, ".", 0);
	if (dot_ofs < 0)
		return s;

	// count trailing zeros that will need to be removed
	string ch = "";
	int i = 0;
	for (; i < 4; ++i)
	{
		ch = substring(s, -1 - i, -1 - i);
		if (ch != "0")
			break;
	}

	// no trailing zeros, return what we got
	// FLOAT_NAN falls under this case and returned as is ("-1.#IND")
	if (i == 0)
		return s;

	// if we've found 4 trailing zeros (i == 4) assume the next char is a dot and remove it too
	// ch == "." handles cases where the decimal part has less than 4 decimal digits (large numbers)
	if (i == 4 || ch == ".")
		++i; // remove dot too

	return substring(s, 0, -1 - i);
}

ERASEABLE
int vercmp_recursive(string v1, string v2)
{
	int dot1 = strstrofs(v1, ".", 0);
	int dot2 = strstrofs(v2, ".", 0);
	string s1 = (dot1 == -1) ? v1 : substring(v1, 0, dot1);
	string s2 = (dot2 == -1) ? v2 : substring(v2, 0, dot2);

	float r = stof(s1) - stof(s2);
	if (r != 0)
		return r;
	r = strcasecmp(s1, s2);
	if (r != 0)
		return r;

	if (dot1 == -1)
		return (dot2 == -1) ? 0 : -1;
	return (dot2 == -1) ? 1 : vercmp_recursive(substring(v1, dot1 + 1, 999), substring(v2, dot2 + 1, 999));
}

ERASEABLE
int vercmp(string v1, string v2)
{
	if (strcasecmp(v1, v2) == 0)
		return 0; // early out check

	// "git" beats all
	if (v1 == "git")  return 1;
	if (v2 == "git")  return -1;

	return vercmp_recursive(v1, v2);
}

const string HEXDIGITS_MINSET = "0123456789ABCDEFabcdef";
const string HEXDIGITS = "0123456789ABCDEF0123456789abcdef";
#define HEXDIGIT_TO_DEC_RAW(d) (strstrofs(HEXDIGITS, (d), 0))
#define HEXDIGIT_TO_DEC(d) ((HEXDIGIT_TO_DEC_RAW(d) | 0x10) - 0x10)
#define DEC_TO_HEXDIGIT(d) (substring(HEXDIGITS_MINSET, (d), 1))
#define IS_HEXDIGIT(d) (strstrofs(HEXDIGITS_MINSET, (d), 0) >= 0)

const string DIGITS = "0123456789";
#define IS_DIGIT(d) (strstrofs(DIGITS, (d), 0) >= 0)

/// Returns true if the caret at position pos is escaped
ERASEABLE
bool isCaretEscaped(string theText, float pos)
{
	// count all the previous carets
	int carets = 0;
	while (pos - carets >= 1 && substring(theText, pos - carets - 1, 1) == "^")
		++carets;
	// if number of previous carets is odd then this carets is escaped
	return (carets & 1);
}

ERASEABLE
bool isValidColorCodeValue(string theText, int cc_len, int tag_start)
{
	if (cc_len == 2)
		return IS_DIGIT(substring(theText, tag_start + 1, 1));
	if (cc_len == 5)
		return (IS_HEXDIGIT(substring(theText, tag_start + 2, 1))
		     && IS_HEXDIGIT(substring(theText, tag_start + 3, 1))
		     && IS_HEXDIGIT(substring(theText, tag_start + 4, 1)));
	return false;
}

/// Returns 0 if pos is NOT in the middle or at the end of a color code
/// otherwise it returns a vector with color code length as the first component
/// and the offset from '^' position to pos as the second component
/// e.g.:
/// "j^2kl" | returns 0 if pos == 0 or 1 or 4
///    ^^   | returns '2 1' or '2 2' if pos == 2 or 3
ERASEABLE
vector checkColorCode(string theText, int text_len, int pos, bool check_at_the_end)
{
	if (text_len == 0)
		text_len = strlen(theText);
	string tag_type = "^";
	int cc_len = 2;
	int tag_len = 1;

	LABEL(check_color_tag)

	int ofs = cc_len;
	if (!check_at_the_end)
		--ofs;
	for (; ofs >= 1; --ofs)
	{
		if (!(pos >= ofs && text_len >= pos + (cc_len - ofs)))
			continue;
		if (substring(theText, pos - ofs, tag_len) == tag_type)
		{
			if (!isCaretEscaped(theText, pos - ofs) && isValidColorCodeValue(theText, cc_len, pos - ofs))
				return eX * cc_len + eY * ofs;
		}
	}
	if (cc_len == 2)
	{
		tag_type = "^x";
		cc_len = 5;
		tag_len = 2;
		goto check_color_tag;
	}
	return '0 0 0';
}


#define PAGE_TEXT_INIT() string _page_text = ""
#define PAGE_TEXT _page_text
/// Adds an individually translatable paragraph to PAGE_TEXT without having to deal with strcat and sprintf.
/// Secondary arguments are optional format arguments for internal sprintf calls.
/// This is to make translators' work easier: paragraphs are shorter than the whole page, sometimes
/// they can be shared across pages (reducing translatable text) and don't require translators
/// to check the whole text in the case of a paragraph update.
#define PAR(...) EVAL_PAR(OVERLOAD(PAR, __VA_ARGS__))
#define EVAL_PAR(...) __VA_ARGS__
#define PAR_1(msg)                                 _page_text = (_page_text == "" ? msg : strcat(_page_text, "\n\n", msg))
#define PAR_2(msg, a1)                             MACRO_BEGIN string _msg = sprintf(msg, a1); PAR_1(_msg); MACRO_END
#define PAR_3(msg, a1, a2)                         MACRO_BEGIN string _msg = sprintf(msg, a1, a2); PAR_1(_msg); MACRO_END
#define PAR_4(msg, a1, a2, a3)                     MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3); PAR_1(_msg); MACRO_END
#define PAR_5(msg, a1, a2, a3, a4)                 MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3, a4); PAR_1(_msg); MACRO_END
#define PAR_6(msg, a1, a2, a3, a4, a5)             MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3, a4, a5); PAR_1(_msg); MACRO_END
#define PAR_7(msg, a1, a2, a3, a4, a5, a6)         MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3, a4, a5, a6); PAR_1(_msg); MACRO_END
#define PAR_8(msg, a1, a2, a3, a4, a5, a6, a7)     MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3, a4, a5, a6, a7); PAR_1(_msg); MACRO_END
#define PAR_9(msg, a1, a2, a3, a4, a5, a6, a7, a8) MACRO_BEGIN string _msg = sprintf(msg, a1, a2, a3, a4, a5, a6, a7, a8); PAR_1(_msg); MACRO_END
