local sanitable = {
	from = {
		['\xfe'] = '\xf0';
		['\1'] = '\xf1';
		['\2'] = '\xf2';
		['\3'] = '\xf3';
		['\0'] = '\xf4'; -- NULs apparently can't be saved in sqlite,
						 -- or possibly just player metadata
	};

	to = {
		['\xf0'] = '\xfe';
		['\xf1'] = '\1';
		['\xf2'] = '\2';
		['\xf3'] = '\3';
		['\xf4'] = '\0';
	};
}

local utf8
if _G.minetest then
	if minetest.global_exists 'utf8' then
		utf8 = _G.utf8
	end
else
	utf8 = _G.utf8
end
if not utf8 then -- sigh
	utf8 = {}
	local bptns = {
		{0x80, 0x00};
		{0xE0, 0xC0};
		{0xF0, 0xE0};
		{0xF8, 0xF0};
	}
	local function bl(n)
		for i = 1,4 do
			local mask, ptn = bptns[i][1], bptns[i][2]
			if bit.band(mask,n) == ptn then
				return i, bit.bnot(mask)
			end
		end
		-- invalid codepoint
	end
	local function ub(bytes, ofs) ofs = ofs or 0
		local function B(n) return string.byte(bytes, n+ofs) end
		local eb, m1 = bl(B(1))
		if not eb then return -1 end
		local val = bit.band(B(1), m1)
		for i = 2,eb do
			val = bit.bor(bit.lshift(val, 6), bit.band(0x3F, B(i)))
		end
		return val
	end
	function utf8.codepoint(str, n) return ub(str, (n and n-1 or nil)) end
	local uMatchPtn = "()([^\x80-\xC1\xF5-\xFF][\x80-\xBF]*)"
	function utf8.codes(s)
		local nx = string.gmatch(s,uMatchPtn)
		return function()
			local pos,bytes = nx()
			if not pos then return nil end
			local by = ub(bytes)
			return pos, by
		end
	end
	function utf8.len(s)
		local i = 0
		for _ in utf8.codes(s) do i=i+1 end
		return i
	end
	function utf8.char(s, ...)
		local v
		if s <= 0x7F then
			v = string.char(s)
		elseif s <= 0x7FF then
			v = string.char(
				bit.bor(0xC0, bit.rshift(s,6)),
				bit.bor(0x80, bit.band(s, 0x3F))
			)
		elseif s <= 0xFFFF then
			v = string.char(
				bit.bor(0xE0,          bit.rshift(s,6*2)     ),
				bit.bor(0x80, bit.band(bit.rshift(s,6), 0x3F)),
				bit.bor(0x80, bit.band(           s,    0x3F))
			)
		elseif s <= 0x10FFFF then
			v = string.char(
				bit.bor(0xF0,          bit.rshift(s,6*3)       ),
				bit.bor(0x80, bit.band(bit.rshift(s,6*2), 0x3F)),
				bit.bor(0x80, bit.band(bit.rshift(s,6*1), 0x3F)),
				bit.bor(0x80, bit.band(           s,      0x3F))
			)
		else -- invalid byte
			v=""
		end
		if select('#', ...) > 0 then
			return v, utf8.char(...)
		else return v end
	end
end

local function nToStr(n, b, tbl)
	local str = {}
	local i = 0
	if n < 0 then
		n = -n
		str[1] = tbl['-']
		i = 1
	end
	if n == 0 then return tbl[0] else repeat i = i + 1
		local v = n%b
		n = math.floor(n / b)
		str[i] = assert(tbl[v])
	until n == 0 end
	return table.concat(str)
end

return {
	utf8 = utf8;

	capitalize = function(str)
		return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2)
	end;

	beginswith = function(str,pfx)
		if #str < #pfx then return false end
		if string.sub(str,1,#pfx) == pfx then
			return true, string.sub(str,1 + #pfx)
		end
	end;

	endswith = function(str,sfx)
		if #str < #sfx then return false end
		if string.sub(str,#sfx) == sfx then
			return true, string.sub(str,1,#sfx)
		end
	end;

	explode = function(str,delim,pat) -- this is messy as fuck but it works so im keeping it
		local i = 1
		local tbl = {}
		if pat == nil then pat = false end
		repeat
			local ss = string.sub(str, i)
			local d
			if pat then
				local matches = {string.match(ss, '()' .. delim .. '()')}
				if #matches > 0 then
					local start,stop = matches[1], matches[#matches]
					d = start
					i = i + stop - 1
				end
			else
				local dl = string.len(delim)
				d = string.find(ss, delim, 1, not pat)
				if d then i = i + d + dl - 1 end
			end
			if not d then
				tbl[#tbl+1] = string.sub(ss,1,string.len(ss))
				break
			else
				tbl[#tbl+1] = string.sub(ss,1,d-1)
			end
		until i > string.len(str)
		return tbl
	end;

	rand = function(rng, min,max)
		if not min then min = 16  end
		if not max then max = min end
		local str = ''
		local r_int   =            0x39 - 0x30
		local r_upper = r_int   + (0x5a - 0x41)
		local r_lower = r_upper + (0x7a - 0x61)
		for i = 1,rng:int(min,max) do
			-- 0x30 -- 0x39
			-- 0x41 -- 0x5A
			-- 0x61 -- 0x71
			local codepoint = rng:int(r_lower)
			if codepoint > r_upper then
				codepoint = (codepoint - r_upper) + 0x61
			elseif codepoint > r_int then
				codepoint = (codepoint - r_int) + 0x41
			else
				codepoint = codepoint + 0x30
			end
			str = str .. string.char(codepoint)
		end
		return str
	end;

	htsan = function(str)
		return str:gsub('([<\\])', '\\%1')
	end;

	chop = function(str)
		if string.sub(str, 1,1) == ' ' then
			str = string.sub(str, 2)
		end
		if string.sub(str, #str,#str) == ' ' then
			str = string.sub(str, 1, #str - 1)
		end
		return str
	end;

	meta_armor = function(str,mark_struct)
		-- binary values stored in metadata need to be sanitized so
		-- they don't contain values that will disrupt parsing of the
		-- KV store, as minetest (stupidly) uses in-band signalling
		local sanitized = string.gsub(str, '.', function(char)
			if sanitable.from[char] then
				return '\xfe' .. sanitable.from[char]
			else return char end
		end)
		if sanitized ~= str and mark_struct then
			-- use different type code to mark struct headers for
			-- back-compat
			return string.gsub(sanitized,'^\xfe\xf0\x99','\xfe\x98')
		else return sanitized end
	end;
	meta_dearmor = function(str,cond)
		local dearmor = function(s)
			return string.gsub(s, '\xfe([\xf0\xf1\xf2\xf3\xf4])', function(char)
				return sanitable.to[char]
			end)
		end
		if cond then
			if string.sub(str,1,2) == '\xfe\x98' then
				return dearmor(string.gsub(str,'^\xfe\x98','\xfe\xf0\x99'))
			else return str end
		else return dearmor(str) end
	end;

	nExp = function(n)
		return nToStr(n, 10, {
			[0]='⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹';
			['-'] = '⁻',  ['('] = '⁽', [')'] = '⁾';
		})
	end;
	nIdx = function(n)
		return nToStr(n, 10, {
			[0]='₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉';
			['-'] = '₋', ['('] = '₍', [')'] = '₎';
		})
	end;
}