-- $Id

-- Kernel for DFUI backend in Lua.

-- Inspiration drawn from Tiago Dionizio's requireso.lua
-- and Asko Kauppi's LuaX kernel.lua.

-- Bindings written with much help from:
-- http://lua-users.org/wiki/BindingWithMetatableAndClosures

--[[--------]]--
--[[ Kernel ]]--
--[[--------]]--

Kernel = {}

--
-- Pure Lua version of dirname, used for locating shared libraries.
--
Kernel.dirname = function(path)
	while true do
		if path == "" or
		   string.sub(path, -1) == "/" or
		   string.sub(path, -2) == "/." or
		   string.sub(path, -3) == "/.." or
		   (string.sub(path, -1) == "." and
		    string.len(path) == 1) or
		   (string.sub(path, -2) == ".." and
		    string.len(path) == 2) then
			break
		end
		path = string.sub(path, 1, -2)
	end
	if path == "" then
		path = "."
	end
	if string.sub(path, -1) ~= "/" then
		path = path .. "/"
	end
	return path
end

--
-- Add a directory to LUA_PATH (used by require.)
--
Kernel.add_lua_path = function(dir)
	if LUA_PATH ~= "" then
		LUA_PATH = LUA_PATH .. ";"
	end
	LUA_PATH = LUA_PATH .. tostring(dir) .. "/?.lua"
end

--
-- Add a directory to CONF_PATH (used by App.load_conf().)
--
Kernel.add_conf_path = function(dir)
	if CONF_PATH ~= "" then
		CONF_PATH = CONF_PATH .. ";"
	end
	CONF_PATH = CONF_PATH .. tostring(dir) .. "/?.lua"
end

--
-- Return an external function (one defined in a .so file.)
--
Kernel.external = function(so_path, fun_name)
	local fun, msg, stage =
	    loadlib(so_path, fun_name)
	if (not fun) then
	    error(tostring(msg) .. " [" .. tostring(stage) .. "]")
	end
	return fun
end

Kernel.errors_are_fatal = false

--
-- Run a Lua script.
-- Note that the script name must be either relative to the
-- current working directory, or fully-qualified.
-- If relative to the current script, use Kernel.find_script first.
-- This function returns two values:
--    the first is the success code, either true or false
--    if true, the second is the result of the script
--    if false, the second is an error message string.
--
Kernel.run = function(script_name, ...)
	local save_script = CURRENT_SCRIPT
	local save_args = ARG
	local ok, result, fun, errmsg

	if Kernel.errors_are_fatal then
		assert(script_name and type(script_name) == "string",
		       "bad filename " .. tostring(script_name))
	end
	if not script_name or type(script_name) ~= "string" then
		return false, "bad filename " .. tostring(script_name)
	end

	Kernel.add_lua_path(Kernel.dirname(script_name) .. "lib")
	Kernel.add_conf_path(Kernel.dirname(script_name) .. "conf")

	fun, errmsg = loadfile(script_name)

	if Kernel.errors_are_fatal then
		assert(fun, errmsg)
	end
	if not fun then
		return false, errmsg
	end

	CURRENT_SCRIPT = script_name
	ARG = arg
	if Kernel.errors_are_fatal then
		ok = true
		result = fun()
	else
		ok, result = pcall(fun)
	end
	ARG = save_args
	CURRENT_SCRIPT = save_script

	return ok, result
end

--
-- Find a Lua script.
--
Kernel.find_script = function(script_name)
	script_name = Kernel.dirname(CURRENT_SCRIPT) .. script_name

	if FileSystem.is_dir(script_name) then
		if string.sub(script_name, -1, -1) ~= "/" then
			script_name = script_name .. "/"
		end
		return script_name .. "main.lua"
	elseif FileSystem.is_file(script_name) then
		--
		-- Just execute that script.
		--
		return script_name
	else
		--
		-- Couldn't find it relative to the current script.
		--
		io.stderr:write("WARNING: could not find `" .. script_name .. "'\n")
		return nil
	end
end

--
-- Dump the contents of the given table to stdout,
-- primarily intended for debugging.
--
Kernel.dump_table = function(tab, indent)
	local k, v

	if not indent then
		indent = ""
	end

	for k, v in tab do
		if type(v) == "table" then
			print(indent .. tostring(k) .. "=")
			Kernel.dump_table(v, indent .. "\t")
		else
			print(indent .. tostring(k) .. "=" .. tostring(v))
		end
	end
end

--
-- Merge two tables by looking at each item from the second (src)
-- table and putting a value into the first (dest) table based on
-- the result of a provided callback function which receives the
-- key and bother values, and returns the resulting value.
--
-- An 'overriding' merge can be accomplished with:
--	function(key, dest_val, src_val)
--		return src_val
--	end
--
-- A 'non-overriding' merge can be accomplished with:
--	function(key, dest_val, src_val)
--		if dest_val == nil then
--			return src_val
--		else
--			return dest_val
--		end
--	end
--
Kernel.merge_tables = function(dest, src, fun)
	local k, v

	for k, v in src do
		if type(v) == "table" then
			if not dest[k] then
				dest[k] = {}
			end
			if type(dest[k]) == "table" then
				Kernel.merge_tables(dest[k], v, fun)
			end
		else
			dest[k] = fun(k, dest[k], v)
		end
	end
end

--
-- Display usage information.
--
Kernel.usage = function()
	io.stderr:write("Usage: lua %s [-F] {-C dir} {-L dir} {var=value} {script | dir}\n", arg[0])
	os.exit(64)
end

--[[---------]]--
--[[ Globals ]]--
--[[---------]]--

LUA_PATH = ""
CONF_PATH = ""
CURRENT_SCRIPT = arg[0]
CMDLINE_ENV = {}
ARG = nil

--[[------]]--
--[[ Main ]]--
--[[------]]--

--
-- Add our custom bindings to the interpreter.
--
local so = Kernel.dirname(CURRENT_SCRIPT) .. "so/dfuibe_lua.so"

Kernel.external(so, "lua_bitwise_register")()
Kernel.external(so, "lua_gettext_register")()
Kernel.external(so, "lua_dfui_register")()
Kernel.external(so, "lua_dfui_progress_register")()
Kernel.external(so, "lua_pipe_register")()
Kernel.external(so, "lua_pty_register")()
Kernel.external(so, "lua_fs_register")()

Kernel.add_lua_path(Kernel.dirname(CURRENT_SCRIPT) .. "lib")
Kernel.add_conf_path(Kernel.dirname(CURRENT_SCRIPT) .. "conf")

--
-- Parse and execute command-line arguments.
--
local argn = 1
while arg[argn] do
	if arg[argn] == "-C" then
		argn = argn + 1
		add_conf_path(arg[argn])
	elseif arg[argn] == "-F" then
		Kernel.errors_are_fatal = true
	elseif arg[argn] == "-L" then
		argn = argn + 1
		add_lua_path(arg[argn])
	elseif arg[argn] == "-v" then
		Kernel.dump_table(CMDLINE_ENV)
	elseif string.sub(arg[argn], 1, 1) == "-" then
		Kernel.usage()
	elseif string.find(arg[argn], "=") then
		local found, len, k, v, c, t, r, i

		t = CMDLINE_ENV
		r = {}
		found, len, k, v = string.find(arg[argn], "^(.*)=(.*)$")
		for c in string.gfind(k, "[^%.]+") do
			table.insert(r, c)
		end
		for i, c in r do
			if i == table.getn(r) then
				t[c] = v
			else
				if not t[c] then
					t[c] = {}
				end
				if type(t[c]) == "table" then
					t = t[c]
				else
					io.stderr:write("warning: " .. c .. ": not a table\n")
				end
			end
		end
	else
		local script_name = Kernel.find_script(arg[argn]) or arg[argn]
		local ok, result = Kernel.run(script_name)
		if not ok then
			io.stderr:write("warning: could not run `" ..
			    tostring(script_name) .. "':\n")
			io.stderr:write(result .. "\n")
		end
	end

	argn = argn + 1
end
