-- vim: ft=terra

local util = dofile('common.lua')
local buildopts, buildargs = util.parseargs{...}
config = dofile('config.lua')

lib = {
	init = {}, util = util;
	load = function(lst)
		for _, l in pairs(lst) do
			io.stdout:write(' · processing module \27[1m' .. l ..'\27[m… ')
			local path = {}
			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
			local tgt = lib
			for i=1,#path-1 do
				if tgt[path[i]] == nil then tgt[path[i]] = {} end
				tgt = tgt[path[i]]
			end
			local chunk = terralib.loadfile(l:gsub(':','/') .. '.t')
			if chunk ~= nil then
				tgt[path[#path]:gsub('-','_')] = chunk()
				print(' \27[1m[ \27[32mok\27[;1m ]\27[m')
			else
				print(' \27[1m[\27[31mfail\27[;1m]\27[m')
				os.exit(2)
			end
		end
	end;
	loadlib = function(name,hdr)
		local p = config.pkg[name]
		-- for _,v in pairs(p.dylibs) do
		-- 	terralib.linklibrary(p.libdir .. '/' .. v)
		-- end
		return terralib.includec(p.incdir .. '/' .. hdr)
	end;
	dispatch = function(tbl)
		return macro(function(v,...)
			for ty,fn in pairs(tbl) do
				if v.tree.type == ty then return fn(v,...) end
			end
			return (tbl[false])(v,...)
		end)
	end;
	emit_unitary = function(nl,fd,...)
		local code = {}
		for i,v in ipairs{...} do
			if type(v) == 'string' or type(v) == 'number' then
				local str = tostring(v)
				code[#code+1] = `lib.io.send(2, str, [#str])
			elseif type(v) == 'table' and #v == 2 then
				code[#code+1] = `lib.io.send(2, [v[1]], [v[2]])
			elseif v.tree:is 'constant' then
				local str = tostring(v:asvalue())
				code[#code+1] = `lib.io.send(2, str, [#str])
			else
				code[#code+1] = quote var n = v in
					lib.io.send(2, n, lib.str.sz(n)) end
			end
		end
		if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
		elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
		return code
	end;
	emitv = function(nl,fd,...)
		local vec = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
				if v.tree.type.convertible == 'tuple' then
					str = `v._0
					ct = `v._1
				else
					local n = symbol(v.tree.type)
					defs[#defs + 1] = quote var [n] = v end
					str = n
					ct = `lib.str.sz(n)
				end
			else
				if type(v) == 'string' or type(v) == 'number' then
					str = tostring(v) 
				else--if v.tree:is 'constant' then
					str = tostring(v:asvalue())
				end
				ct = ct or #str
			end
			vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct}
		end
		if nl == true then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1}
		elseif nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](nl), iov_len = [#nl]} end
		return quote
			[defs]
			var strs = array( [vec] )
		in lib.uio.writev(fd, strs, [#vec]) end
	end;
	trn = macro(function(cond, i, e)
		return quote
			var c: bool = [cond]
			var r: i.tree.type
			if c == true then r = i else r = e end
		in r end
	end);
	coalesce = macro(function(...)
		local args = {...}
		local ty = args[1].tree.type
		local val = symbol(ty)
		local empty
		if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0}
		elseif ty.type == 'integer' then empty = `0
		else empty = `nil end
		local exp = quote val = [empty] end

		for i=#args, 1, -1 do
			local v = args[i]
			if ty.ptr_basetype then
				exp = quote
					if [v].ptr ~= nil
						then val = v
						else [exp]
					end
				end
			else
				exp = quote
					if [v] ~= [empty]
						then val = v
						else [exp]
					end
				end
			end
		end

		local q = quote
			var [val]
			[exp]
		in val end
		return q
	end);
	proc = {
		fork = terralib.externfunction('fork', {} -> int);
		exit = terralib.externfunction('exit', int -> {});
		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
		exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int);
		execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int);
	};
	io = {
		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
		recv = terralib.externfunction('read',  {int, rawstring, intptr} -> ptrdiff);
		close = terralib.externfunction('close', {int} -> int);
		say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
		fmt = terralib.externfunction('printf',
			terralib.types.funcpointer({rawstring},{int},true));
		ttyp = terralib.externfunction('isatty', int -> int);
	};
	str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
	copy = function(tbl)
		local new = {}
		for k,v in pairs(tbl) do new[k] = v end
		setmetatable(new, getmetatable(tbl))
		return new
	end;
	osclock = terralib.includec 'time.h';
}
if config.posix then
	lib.uio = terralib.includec 'sys/uio.h';
	lib.emit = lib.emitv -- use more efficient call where available
else lib.emit = lib.emit_unitary end


lib.noise = {
	level = global(uint8,1);
	starttime = global(lib.osclock.time_t);
	lasttime = global(lib.osclock.time_t);
	header = function(code,txt,mod)
		if mod then
			return string.format('\27[%s;1m(%s %s)\27[m ', code,mod,txt)
		else
			return string.format('\27[%s;1m(%s)\27[m ', code,txt)
		end
	end;
}

local terra timehdr()
	var now = lib.osclock.time(nil)
	var diff = now - lib.noise.lasttime
	if diff > 30 then -- print cur time
		lib.noise.lasttime = now
		var curtime: int8[26]
		lib.osclock.ctime_r(&now, &curtime[0])
		for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/
		[ lib.emit(false, 2, '\27[1m', `&curtime[0], '\27[;36m\n +00 ') ]
	else -- print time since last msg
		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
	end
end

local defrep = function(level,n,code)
	if level >= 3 and config.debug == false then
		return macro(function(...) return {} end)
	end
	return macro(function(...)
		local fn = (...).filename
		local ln = tostring((...).linenumber)
		local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln)
		local q = lib.emit(level < 3 and true or dbgtag, 2, lib.noise.header(code,n), ...)
		return quote
		--lib.io.fmt(['attempting to emit at ' .. fn..':'..ln.. '\n'])
		if lib.noise.level >= level then timehdr(); [q] end end
	end);
end
local terra final_cleanup :: {} -> {}
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
	local q = lib.emit(true, 2, lib.noise.header('31','fatal'), ...)
	return quote
		if lib.noise.level ~= 0 then
			timehdr(); [q]
		end
		final_cleanup()
		lib.proc.exit(1)
	end
end);
lib.stat = terralib.memoize(function(ty)
	local n = struct {
		ok: bool
		union {
			error: uint8
			val: ty
		}
	}
	n.name = string.format("stat<%s>", ty.name)
	n.stat_basetype = ty
	return n
end)
lib.enum = function(tbl)
	local ty = uint8
	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
	elseif #tbl >= 2^16 then ty = uint32
	elseif #tbl >= 2^8 then ty = uint16 end
	local o = { t = ty, members = tbl }
	local strings = {}
	for i, name in ipairs(tbl) do
		o[name] = `[ty]([i - 1])
		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
	end
	o._str = terra(val: ty)
		var l = array([strings])
		return l[val]
	end
	return o
end
lib.set = function(tbl)
	local bytes = math.ceil(#tbl / 8)
	local o = {}
	for i, name in ipairs(tbl) do o[name] = i end
	local struct set { _store: uint8[bytes] }
	local struct bit { _v: intptr _set: &set}
	terra set:clear() for i=0,bytes do self._store[i] = 0 end end
	terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
	set.members = tbl
	set.name = string.format('set<%s>', table.concat(tbl, '|'))
	set.metamethods.__entrymissing = macro(function(val, obj)
		if o[val] == nil then error('value ' .. val .. ' not in set') end
		return `bit { _v=[o[val] - 1], _set = &(obj) }
	end)
	terra set:sz()
		var ct: intptr = 0
		for i = 0, [#tbl] do
			if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end
		end
		return ct
	end
	set.methods.dump = macro(function(self)
		local q = quote lib.io.say('dumping set:\n') end
		for i,v in ipairs(tbl) do
			q = quote [q]
				if [bool](self.[v])
					then lib.io.say([' - ' .. v .. ': true\n'])
					else lib.io.say([' - ' .. v .. ': false\n'])
				end
			end
		end
		return q
	end)
	terra set:setbit(i: intptr, val: bool)
		if val then
			self._store[i/8] = self._store[i/8] or (1 << (i % 8))
		else
			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
		end
	end
	set.bits = {}
	set.idvmap = {}
	for i,v in ipairs(tbl) do
		set.idvmap[v] = i
		set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end
	end
	set.metamethods.__add = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__and = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and other._store[i]
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__eq = macro(function(self,other)
		local rt = symbol(bool)
		local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end
		local q = quote rt = true end
		for i = 0, fb do
			q = quote
				if self._store[i] ~= other._store[i] then rt = false else [q] end
			end
		end
		-- we need to mask out any extraneous bits the values might have, as we
		-- don't want the kind of noise introduced by :fill() to affect comparison
		if #tbl % 8 ~= 0 then
			local last = #tbl-1
			local msk = (2 ^ (#tbl % 8)) - 1
			q = quote
				if (self._store [last] and [uint8](msk)) ~=
				   (other._store[last] and [uint8](msk)) then rt = false else [q] end
			end
		end
		return quote var [rt]; [q] in rt end
	end)
	set.metamethods.__not = macro(function(self)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = not self._store[i] 
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__sub = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and (not other._store[i])
			end
		end
		return quote [q] in new end
	end)
	bit.metamethods.__cast = function(from,to,e)
		local q = quote var s = e
			in (s._set._store[s._v/8] and (1 << s._v % 8)) end
		if to == bit then error('casting to bit is not meaningful')
		elseif to == bool then return `([q] ~= 0)
		elseif to:isintegral() then return q
		elseif from == bit then error('cannot cast bit to ' .. tostring(to))
		else return nil end
	end
	bit.metamethods.__apply = terra(self: &bit): bool return @self end
	bit.metamethods.__lshift = terra(self: &bit, hl: bool)
		var byte = self._v / 8
		var bit = self._v % 8
		if hl then
			self._set._store[byte] = self._set._store[byte] or (1 << bit)
		else
			self._set._store[byte] = self._set._store[byte] and not (1 << bit)
		end
	end
	return set
end

lib.err = lib.loadlib('mbedtls','mbedtls/error.h')
lib.rsa = lib.loadlib('mbedtls','mbedtls/rsa.h')
lib.pk = lib.loadlib('mbedtls','mbedtls/pk.h')
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')

lib.load {
	'mem', 'math', 'str', 'file', 'crypt', 'ipc';
	'http', 'html', 'session', 'tpl', 'store', 'acl';

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
end
lib.store.backends = global(`array([be]))

lib.cmdparse = terralib.loadfile('cmdparse.t')()

do local collate = function(path,f, ...)
	return loadfile(path..'/'..f..'.lua')(path, ...)
end
data = {
	doc = collate('doc','load');
	view = collate('view','load');
	static = {};
	stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content
} end
for i,e in ipairs(config.embeds) do local v = e[1]
	local fh = io.open('static/' .. v,'r')
	if fh == nil then error('static file ' .. v .. ' missing') end
	data.static[v] = fh:read('*a') fh:close()
end

-- slightly silly -- because we're using plain lua to gather up
-- the template sources, they have to be actually turned into
-- templates in the terra code, so we "mutate" them here
for k,v in pairs(data.view) do
	local t = lib.tpl.mk { body = v, id = 'view/'..k }
	data.view[k] = t
end

lib.load {
	'srv';
	'render:nav';
	'render:nym';
	'render:login';
	'render:profile';
	'render:compose';
	'render:tweet';
	'render:tweet-page';
	'render:user-page';
	'render:timeline';
	'render:notices';

	'render:media-gallery';

	'render:docpage';

	'render:conf:profile';
	'render:conf:sec';
	'render:conf:users';
	'render:conf';
	'route';
}

do
	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
	terra version() lib.io.send(1, p, [#p]) end
end

terra lib.noise.init(default_level: uint)
	lib.noise.starttime = lib.osclock.time(nil)
	lib.noise.lasttime = 0
	var n = lib.proc.getenv('parsav_noise')
	if n ~= nil then
		if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then
			lib.noise.level = n[0] - 0x30
			return
		end
	end
	lib.noise.level = default_level
end
lib.load{'mgtool'}

local options = lib.cmdparse {
	version = {'V', 'display information about the binary build and exit'};
	verbose = {'v', 'increase logging verbosity', inc=1};
	quiet = {'q', 'do not print to standard out'};
	help = {'h', 'display this list'};
	backend_file = {'B', 'init from specified backend file', consume=1};
	static_dir = {'S', 'directory with overrides for static content', consume=1};
	builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'};
	instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1};
	no_ipc = {'I', 'disable IPC'};
	chroot = {'C', 'chroot to the specified directory after starting and connecting to backends', consume=1};
	no_lockdown = {'L', 'don\'t drop privileges or apply other security measures that could cause compatibility or communication problems'}
}


local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
for i,e in ipairs(config.embeds) do local v = e[1]
	local d = data.static[v]
	static_setup = quote [static_setup]
		([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] { ptr = [d], ct = [#d] })
	end
	mapin = quote [mapin]
		var osz = pathbuf.sz
		pathbuf:push([v],[#v])
		var f = lib.file.open(pathbuf.buf, [lib.file.mode.read])
		if f.ok then defer f.val:close()
			var map = f.val:mapin(0,0) -- currently maps are leaked, maybe do something more elegant in future
			lib.report('loading static override content from ', pathbuf.buf)
			([data.stmap])[([i-1])] = ([lib.mem.ref(int8)] {
				ptr = [rawstring](map.addr);
				ct = map.sz;
			})
		end
		pathbuf.sz = osz
	end
end
local terra static_init(mode: &options)
	[static_setup]
	if mode.builtin_data then return end

	var [odir] = lib.proc.getenv('parsav_override_dir')
	if mode.static_dir ~= nil then
		odir=@mode.static_dir
	end
	if odir == nil then [
		config.prefix_static and quote
			odir = [config.prefix_static]
		end or quote return end
	] end

	var [pathbuf] defer pathbuf:free()
	pathbuf:compose(odir,'/')
	[mapin]
end

terra final_cleanup()
	lib.ipc.global_emperor:release()
end

local terra entry_daemon(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise.init(1)
	[lib.init]

	-- shut mongoose the fuck up
	lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
	var srv: lib.srv.overlord

	do var mode: options
		mode:parse(argc,argv) defer mode:free()
		static_init(&mode)
		if mode.version then version() return 0 end
		if mode.help then
			[ lib.emit(true, 1, 'usage: ',`argv[0],' ', options.helptxt.flags, ' [<args>…]', options.helptxt.opts) ]
			return 0
		end
		if mode.quiet then lib.noise.level = 0 end
		var cnf: rawstring
		if mode.backend_file ~= nil
			then cnf = @mode.backend_file
			else cnf = lib.proc.getenv('parsav_backend_file')
		end
		if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end

		srv:setup(cnf)
		if argv[0][0] == @'-' and argv[0][1] == 0 then
			lib.ipc.notify_parent(lib.ipc.signals.state_success)
			argv[0] = 'parsav service daemon'
		end
		srv:start(lib.trn(mode.instance ~= nil, @mode.instance, nil))
	end
	lib.ipc.global_emperor = lib.ipc.emperor.mk(true) defer lib.ipc.global_emperor:release()

	lib.report('listening for requests')
	while lib.ipc.proc_active do
		srv:poll()
		while true do
			var d: lib.ipc.demand
			lib.ipc.global_emperor:poll(&d)
			var a = lib.ipc.ack {success = true}
			if d.cmd == lib.ipc.cmd.none then break
			elseif d.cmd == lib.ipc.cmd.stop then
				lib.ipc.proc_active = false
			elseif d.cmd == lib.ipc.cmd.enumerate then
				if srv.id ~= nil then
					lib.str.ncpy(&a.iname[0], srv.id, [(`a.iname).tree.type.N])
				else a.iname[0] = 0 end
			elseif d.cmd == lib.ipc.cmd.chnoise then
				lib.noise.level = d.operand
			elseif d.cmd == lib.ipc.cmd.cfgrefresh then
				srv.cfg:free()
				srv.cfg:load()
			end
			d:ack(&lib.ipc.global_emperor, &a)
		end
	end
	srv:shutdown()

	return 0
end

local bflag = function(long,short)
	if short and util.has(buildopts, short) then return true end
	if long and util.has(buildopts, long) then return true end
	return false
end

if bflag('dump-config','C') then
	print(util.dump(config))
	os.exit(0)
end

local holler = print
local suffix = config.exe and '' or ('.'..config.outform)
local out = 'parsavd' .. suffix
local linkargs = {}
local target = config.tgttrip and terralib.newtarget {
	Triple = config.tgttrip;
	CPU = config.tgtcpu;
	FloatABIHard = config.tgthf;
} or nil

if bflag('quiet','q') then holler = function() end end
if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end

for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end
local linkargs_d = linkargs -- controller is not multithreaded
if config.posix then
	linkargs_d[#linkargs_d+1] = '-pthread'
end

holler(' → linking \27[1mparsav\27[m with "' .. table.concat(linkargs,' ') .. '"')
terralib.saveobj('parsav' ..suffix, { main = lib.mgtool }, linkargs, target)

holler(' → linking \27[1mparsavd\27[m with "' .. table.concat(linkargs_d,' ') .. '"')
terralib.saveobj('parsavd'..suffix, { main = entry_daemon }, linkargs_d, target)
