local strfind, strsub, strrep = string.find, string.sub, string.rep
local strmatch, strgsub = string.match, string.gsub
local floor = math.floor

local function split(str, sep)
	local pos, endp = 1, #str+1
	return function()
		if (not pos) or pos > endp then return end
		local s, e = strfind(str, sep, pos, true)
		local part = strsub(str, pos, s and s-1)
		pos = e and e + 1
		return part
	end
end

local function trim(str)
	return strmatch(str, "^%s*(.-)%s*$")
end

local escapes = { n="\n", r="\r", t="\t" }

local function unescape(str)
	return (strgsub(str, "(\\+)([nrt]?)", function(bs, c)
		local bsl = #bs
		local realbs = strrep("\\", bsl/2)
		if bsl%2 == 1 then
			c = escapes[c] or c
		end
		return realbs..c
	end))
end

local function parse_po(str)
	local state, msgid, msgid_plural, msgstrind
	local texts = { }
	local lineno = 0
	local function perror(msg)
		return error(msg.." at line "..lineno)
	end
	for line in split(str, "\n") do repeat
		lineno = lineno + 1
		line = trim(line)

		if line == "" or strmatch(line, "^#") then
			state, msgid, msgid_plural = nil, nil, nil
			break -- continue
		end

		local mid = strmatch(line, "^%s*msgid%s*\"(.*)\"%s*$")
		if mid then
			if state == "id" then
				return perror("unexpected msgid")
			end
			state, msgid = "id", unescape(mid)
			break -- continue
		end

		mid = strmatch(line, "^%s*msgid_plural%s*\"(.*)\"%s*$")
		if mid then
			if state ~= "id" then
				return perror("unexpected msgid_plural")
			end
			state, msgid_plural = "idp", unescape(mid)
			break -- continue
		end

		local ind, mstr = strmatch(line,
				"^%s*msgstr([0-9%[%]]*)%s*\"(.*)\"%s*$")
		if ind then
			if not msgid then
				return perror("missing msgid")
			elseif ind == "" then
				msgstrind = 0
			elseif strmatch(ind, "%[[0-9]+%]") then
				msgstrind = tonumber(strsub(ind, 2, -2))
			else
				return perror("malformed msgstr")
			end
			texts[msgid] = texts[msgid] or { }
			if msgid_plural then
				texts[msgid_plural] = texts[msgid]
			end
			texts[msgid][msgstrind] = unescape(mstr)
			state = "str"
			break -- continue
		end

		mstr = strmatch(line, "^%s*\"(.*)\"%s*$")
		if mstr then
			if state == "id" then
				msgid = msgid..unescape(mstr)
				break -- continue
			elseif state == "idp" then
				msgid_plural = msgid_plural..unescape(mstr)
				break -- continue
			elseif state == "str" then
				local text = texts[msgid][msgstrind]
				texts[msgid][msgstrind] = text..unescape(mstr)
				break -- continue
			end
		end

		return perror("malformed line")

	until true end -- end for

	return texts
end

local M = { }

local domains = { }
local dgettext_cache = { }
local dngettext_cache = { }
local langs

local function detect_languages()
	if langs then return langs end

	langs = { }

	local function addlang(l)
		local sep
		langs[#langs+1] = l
		sep = strfind(l, ".", 1, true)
		if sep then
			l = strsub(l, 1, sep-1)
			langs[#langs+1] = l
		end
		sep = strfind(l, "_", 1, true)
		if sep then
			langs[#langs+1] = strsub(l, 1, sep-1)
		end
	end

	local v

	v = rawget(_G, "minetest") and minetest.setting_get("language")
	if v and v~="" then
		addlang(v)
	end

	v = os.getenv("LANGUAGE")
	if v then
		for item in split(v, ":") do
			addlang(item)
		end
	end

	v = os.getenv("LANG")
	if v then
		addlang(v)
	end

	return langs
end

local function warn(msg)
	if rawget(_G, "minetest") then
		minetest.log("warning", msg)
	else
		io.stderr:write("WARNING: ", msg, "\n")
	end
end

-- hax!
-- This function converts a C expression to an equivalent Lua expression.
-- It handles enough stuff to parse the `Plural-Forms` header correctly.
-- Note that it assumes the C expression is valid to begin with.
local function compile_plural_forms(str)
	local plural = strmatch(str, "plural=([^;]+);?$")
	local function replace_ternary(str)
		local c, t, f = strmatch(str, "^(.-)%?(.-):(.*)")
		if c then
			return ("__if("
					..replace_ternary(c)
					..","..replace_ternary(t)
					..","..replace_ternary(f)
					..")")
		end
		return str
	end
	plural = replace_ternary(plural)
	plural = strgsub(plural, "&&", " and ")
	plural = strgsub(plural, "||", " or ")
	plural = strgsub(plural, "!=", "~=")
	plural = strgsub(plural, "!", " not ")
	local f, err = loadstring([[
		local function __if(c, t, f)
			if c and c~=0 then return t else return f end
		end
		local function __f(n)
			return (]]..plural..[[)
		end
		return (__f(...))
	]])
	if not f then return nil, err end
	local env = { }
	env._ENV, env._G = env, env
	setfenv(f, env)
	return function(n)
		local v = f(n)
		if type(v) == "boolean" then
			-- Handle things like a plain `n != 1`
			v = v and 1 or 0
		end
		return v
	end
end

local function parse_headers(str)
	local headers = { }
	for line in split(str, "\n") do
		local k, v = strmatch(line, "^([^:]+):%s*(.*)")
		if k then
			headers[k] = v
		end
	end
	return headers
end

local function load_catalog(filename)
	local f, data, err

	local function bail(msg)
		warn(msg..(err and ": ")..(err or ""))
		return nil
	end

	f, err = io.open(filename, "rb")
	if not f then
		return --bail("failed to open catalog")
	end

	data, err = f:read("*a")

	f:close()

	if not data then
		return bail("failed to read catalog")
	end

	data, err = parse_po(data)
	if not data then
		return bail("failed to parse catalog")
	end

	err = nil
	local hdrs = data[""]
	if not (hdrs and hdrs[0]) then
		return bail("catalog has no headers")
	end

	hdrs = parse_headers(hdrs[0])

	local pf = hdrs["Plural-Forms"]
	if not pf then
		return bail("failed to load catalog:"
				.." catalog has no Plural-Forms header")
	end

	data.plural_index, err = compile_plural_forms(pf)
	if not data.plural_index then
		return bail("failed to compile plural forms")
	end

	--warn("loaded: "..filename)

	return data
end

function M.load_catalogs(path)
	detect_languages()

	local cats = { }
	for _, lang in ipairs(langs) do
		local cat = load_catalog(path.."/"..lang..".po")
		if cat then
			cats[#cats+1] = cat
		end
	end

	return cats
end

return M