-- lib/app.lua
-- $Id: app.lua,v 1.28 2005/01/25 07:41:55 cpressey Exp $
-- Lua-based Application Environment static object.

-- BEGIN lib/app.lua --

require "dfui"
require "expander"

--[[-----]]--
--[[ App ]]--
--[[-----]]--

-- Application Environment - roughly equivalent to
-- InstallerContext (or i_fn_args in the C version,) but:
--
-- *  this version is written purely in Lua, and
-- *  this version is not specific to the Installer - it could just as well
--    be used for any application that needs:
--
--   o  DFUI
--   o  configuration, possibly loaded from config files
--      - locations of directories (root dir, temp dir, etc)
--      - names of system commands
--      - etc
--   o  application-wide options
--   o  application-wide state
--   o  logging
--   o  temporary files
--
-- For simplicity, we consider this to be a singleton or
-- "static object" (with a single global "instance" called App.)

App = {}

--
-- Some globals...
--

App.defaults = {
    name	= "Unnamed Application",
    logfile	= "unnamed.log",
    dir = {
	root	= "/",
	tmp	= "/tmp/"
    },
    transport	= "tcp",
    rendezvous	= "9999"
}

App.last_log_time = -1

--
-- Startup and shutdown.
--

App.start = function(opt)
	local k, v
	
	-- Set up defaults.
	if not opt then
		opt = {}
	end

	Kernel.merge_tables(opt, App.defaults, function(key, dest_val, src_val)
		if not dest_val then
			return src_val
		else
			return dest_val
		end
	end)

	-- Override anything that was given on the command-line.
	Kernel.merge_tables(opt, CMDLINE_ENV, function(key, dest_val, src_val)
		return src_val
	end)

	-- Set name of application.
	App.name = opt.name
	App.log_filename = opt.logfile

	-- Set up directories, and make sure each ends with a slash.
	App.dir = opt.dir
	for k, v in App.dir do
		if string.sub(v, -1) ~= "/" then
			App.dir[k] = v .. "/"
		end
	end

	-- Determine the operating system.
	App.os = {}
	App.os.name = App.determine_os_name()
	-- App.os.version = App.determine_os_version()

	-- Open our logfile.
	App.open_log(App.dir.tmp .. App.log_filename)
	App.log(App.name .. " started")

	-- Load command names, if available.
	App.cmd_names = App.load_conf("cmdnames")

	-- Set up our expander.
	App.expander = Expander.new(App.cmd_names, App.dir)
	App.expand = function(str, ...)
		return App.expander:expand(str, unpack(arg))
	end

	-- Set up temporary files.
	App.tmpfile = {}
	
	-- Set up application-specific containers:
	--	config:	application configuration
	--	option:	application-wide options
	--	state:	application-wide state
	App.config = opt.config or {}
	App.option = opt.option or {}
	App.state = opt.state or {}

	-- Seed the random-number generator.
	math.randomseed(os.time())

	-- Create a DFUI connection for the application.
	if not transport then transport = "tcp" end
	if not rendezvous then rendezvous = "9999" end
	App.c = DFUIConnection.new(transport, rendezvous)
	if App.c:start() == 0 then
		App.c:stop()
		App.log_fatal("Could not establish DFUI connection on " ..
		    transport .. ":" .. rendezvous)
	end
	App.log("DFUI connection on " .. transport .. ":" .. rendezvous ..
	    " successfully established")
end

App.stop = function()
	App.clean_tmpfiles()
	App.c:stop()
	App.log("Shutting down")
	App.close_log()
end

--
-- Run a script.  Expects the full filename (will not search.)
-- Displays a nice dialog box if the script contained errors.
--
App.run_script = function(script_name, ...)
	local ok, result = Kernel.run(script_name, unpack(arg))
	if ok then
		return result
	end
	App.log_warn("Error occurred while loading script `" ..
		      tostring(script_name) .. "': " .. tostring(result))
	if App.c then
		App.c:present{
		    id = "script_error",
		    name = "Error Loading Script",
		    short_desc = 
			"An internal Lua error occurred while " ..
			"trying to run the script " ..
			tostring(script_name) .. ":\n\n" ..
			tostring(result),
		    role = "alert",
		    actions = {
		        {
			    id = "ok",
			    name = "OK"
			}
		    }
		}
	end
	return nil
end

--
-- Run a sub-application (a script relative to the current script.)
--
App.descend = function(script_name, ...)
	return App.run_script(Kernel.find_script(script_name), unpack(arg))
end

--
-- Configuration file loading.
--

App.locate_conf = function(name)
	local path = CONF_PATH
	local comp

	for comp in string.gfind(path, "[^;]+") do
		comp = string.gsub(comp, "?", name)
		if FileSystem.is_file(comp) then
			return comp
		end
	end
	
	return nil
end

App.load_conf = function(name)
	return App.run_script(App.locate_conf(name))
end

--
-- Logging.
--

App.open_log = function(filename, mode)
	if App.log_file then
		return
	end
	if not mode then
		mode = "w"
	end
	local fh, err = io.open(filename, mode)
	App.log_file = nil
	if fh then
		App.log_file = fh
	else
		error(err)
	end
end

App.close_log = function()
	if App.log_file then
		App.log_file:close()
		App.log_file = nil
	end
end

App.log = function(str, ...)
	local stamp = math.floor(os.time())
	local line = ""

	local write_log = function(s)
		s = s .. "\n"
		io.stderr:write(s)
		if App.log_file then
			App.log_file:write(s)
			App.log_file:flush()
		end
	end

	if stamp > App.last_log_time then
		App.last_log_time = stamp
		write_log("[" .. os.date() .. "]")
	end

	write_log(string.format(str, unpack(arg)))
end

App.log_warn = function(str)
	App.log("WARNING: " .. str)
end

App.log_fatal = function(str)
	App.log(str)
	error(str)
end

App.view_log = function()
	local contents = ""
	local fh

	App.close_log()

	fh = io.open(App.dir.tmp .. App.log_filename, "r")
	for line in fh:lines() do
		contents = contents .. line .. "\n"
	end
	fh:close()

	App.c:present({
		id = "app_log",
		name = App.name .. ": Log",
		short_desc = contents,
		role = "informative",
		minimum_width = "72",
		monospaced = "true",
		actions = {
			{ id = "ok", name = "OK" }
		}
	})
	
	App.open_log(App.dir.tmp .. App.log_filename, "a")
end

--
-- Temporary file handling.
--

App.clean_tmpfiles = function()
	local filename, unused

	for filename, unused in App.tmpfile do
		App.log("Deleting tmpfile: " .. filename)
		os.remove(App.dir.tmp .. filename)
	end
end

-- Registers that the given file (which resides in App.dir.tmp)
-- is a temporary file, and may be deleted when upon exit.
App.register_tmpfile = function(filename)
	App.tmpfile[filename] = 1
end

-- Creates and opens a new temporary file (in App.dir.tmp).
-- If the filename is omitted, one is chosen using the mkstemp
-- system call.  If the mode is omitted, updating ("w+") is
-- assumed.  The file object and the file name are returned.
App.open_tmpfile = function(filename, mode)
	local fh, err

	if not filename then
		fh, filename = FileSystem.mkstemp(App.dir.tmp .. "Lua.XXXXXXXX")
		filename = FileSystem.basename(filename)
	else
		fh, err = io.open(App.dir.tmp .. filename, mode or "w+")
		if err then
			return nil, err
		end
	end
	App.register_tmpfile(filename)
	return fh, filename
end

--
-- Operating system determination.
-- NOTE: this is pretty weak - this is before we have
-- loaded the command locations, and sysctl could be anywhere on path.
-- Besides, this should be overridable somehow on principle.
-- Perhaps even hard-coded.
--

App.determine_os_name = function()
	local pipe = Pipe.open("sysctl -n kern.ostype")
	local osname = pipe:readline()
	pipe:close()
	return osname
end

-- END of lib/app.lua --
