--[[

	SaferLua [safer_lua]
	====================

	Copyright (C) 2018 Joachim Stolberg

	LGPLv2.1+
	See LICENSE.txt for more information

	environ.lua:

]]--

safer_lua.MaxCodeSize = 5000    -- size if source code in bytes
safer_lua.MaxTableSize = 1000   -- sum over all table sizes
safer_lua.MaxExeTime = 20000    -- max. execution time in us

local function memsize()
	return safer_lua.MaxTableSize
end

local function range(from, to)
    return function(expired_at,last)
		assert(expired_at > minetest.get_us_time(), "Runtime limit exceeded")
		if last >= to then 
			return nil
		else 
			return last+1
		end
	end, minetest.get_us_time() + safer_lua.MaxExeTime, from-1
end

local BASE_ENV = {
	Array = safer_lua.Array,
	Store = safer_lua.Store,
	Set = safer_lua.Set,
	memsize = memsize,
	range = range,
	math = {
		floor = math.floor,
		abs = math.abs,
		max = math.max,
		min = math.min,
		random = math.random,
	},
	string = {
		byte = string.byte,
		char = string.char,
		find = string.find,
		format = string.format,
		gmatch = string.gmatch,
		gsub = string.gsub,
		len = string.len,
		lower = string.lower,
		match = string.match,
		rep = string.rep,
		sub = string.sub,
		upper = string.upper,
		split = function(str, separator, include_empty, max_splits, sep_is_pattern) 
			if separator == "" then separator = " " end
			return safer_lua.Array(unpack(string.split(str, separator, include_empty, max_splits, sep_is_pattern))) 
		end,
		split2 = function(str, separator, include_empty, max_splits, sep_is_pattern) 
			if separator == "" then separator = " " end
			return unpack(string.split(str, separator, include_empty, max_splits, sep_is_pattern))
		end,
		trim = string.trim,
	},
	tonumber = tonumber,
	tostring = tostring,
	type = type,
	ticks = 0,
}

local function map(dest, source)
  for k,v in pairs(source) do
    dest[k] = v
  end
  return dest
end

local function calc_used_mem_size(env)
	local size = 0
	for key,val in pairs(env) do
		if type(val) == "table" and val.size ~= nil then
			size = size + val.size() or 0
		end
	end
	return size
end

function safer_lua.config(max_code_size, max_table_size)
	safer_lua.MaxCodeSize = max_code_size
	safer_lua.MaxTableSize = max_table_size
end	

local function format_error_str(str, label)
	local tbl = {}
	for s in str:gmatch("[^\r\n]+") do
		s = s:match("^%s*(.-)%s*$")
		if s:find("function 'xpcall'") then
			break
		elseif s:find(".-%.lua:%d+:(.+)") then
			local err = s:gsub(".-%.lua:%d+:%s*(.+)", "%1")
			table.insert(tbl, err)
		elseif s:find('%[string ".-"%]') then
			local line, err = s:match('^%[string ".-"%]:(%d+): (.+)$')
			table.insert(tbl, label..":"..line..": "..err)
		elseif s:find('%(load%):(%d+):') then
			local line, err = s:match('%(load%):(%d+): (.+)$')
			table.insert(tbl, label..":"..line..": "..err)
		end
	end    
	return "Error: "..table.concat(tbl, "\n >> ")
end

local function format_error(err, label)
	if err:find("stack overflow") then
		return "Error: Stack overflow due to recursive function calls!"
	end
	return format_error_str(err, label)
end

local function compile(pos, text, label, err_clbk)
	if safer_lua:check(pos, text, label, err_clbk) == 0 then
		text = text:gsub("%$", "S:")
		local code, err = loadstring(text)
		if not code then
			err_clbk(pos, format_error(err, label)) 
		else
			return code
		end
	end
end

local function runtime_delimiter(code)
	local time = minetest.get_us_time()
	local timeout = function ()
		if minetest.get_us_time() - time > safer_lua.MaxExeTime then
			debug.sethook()
			error("Runtime limit exceeded")
		end
	end
	debug.sethook(timeout, "c")
	code()
	debug.sethook()
end

local function error_handler(...)
	debug.sethook()
	return debug.traceback(...)
end

-------------------------------------------------------------------------------
-- Standard init/loop controller
-------------------------------------------------------------------------------
function safer_lua.init(pos, init, loop, environ, err_clbk)
	if (#init + #loop) > safer_lua.MaxCodeSize then
		err_clbk(pos, "Error: Code size limit exceeded")
		return
	end
	local code = compile(pos, init, "init", err_clbk, 0)
	if code then
		local env = table.copy(BASE_ENV)
		env.S = {}
		env.S = map(env.S, environ)
		setfenv(code, env)
		local res, err = xpcall(runtime_delimiter, error_handler, code)
		if not res then
			err_clbk(pos, format_error(err, "init"))
		else
			env = getfenv(code)
			code = compile(pos, loop, "loop", err_clbk)
			if code then
				setfenv(code, env)
				return code
			end
		end
	end
end

function safer_lua.run_loop(pos, elapsed, code, err_clbk)
	local env = getfenv(code)
	env.elapsed = elapsed
	if elapsed < 0 then  -- event?
		env.event = true
	else
		env.event = false
		env.ticks = env.ticks + 1
	end
	local res, err = xpcall(runtime_delimiter, error_handler, code)
	if calc_used_mem_size(env) > safer_lua.MaxTableSize then 
		err_clbk(pos, "Error: Data memory limit exceeded")
		return false
	end
	if not res then
		err_clbk(pos, format_error(err, "loop"))
		return false
	end
	return true
end

-------------------------------------------------------------------------------
-- Endless/Coroutine controller
-------------------------------------------------------------------------------
local function thread(pos, code, err_clbk)
	while true do
		local res, err = xpcall(runtime_delimiter, error_handler, code)
		if not res then
			err_clbk(pos, format_error(err, "loop"))
			return false
		end
		local env = getfenv(code)
		if calc_used_mem_size(env) > safer_lua.MaxTableSize then 
			err_clbk(pos, "Error: Memory limit exceeded")
			return false
		end
		coroutine.yield()
	end
end	

function safer_lua.co_create(pos, init, loop, environ, err_clbk)
	local code = safer_lua.init(pos, init, loop, environ, err_clbk)
	return coroutine.create(thread), code
end

function safer_lua.co_resume(pos, co, code, err_clbk)
	local res, err = coroutine.resume(co, pos, code, err_clbk)
	if not res then
		err_clbk(pos, format_error(err, "loop"))
		return false
	end
	return true
end
