--
-- utility functions
--

function string.hasprefix(s, prefix)
	return s:sub(1, #prefix) == prefix
end

function string.hassuffix(s, suffix)
	return s:sub(-#suffix) == suffix
end

-- collects the results of an iterator into a table
local function collect(f, s, i)
	local t = {}
	for v in f, s, i do
		t[#t + 1] = v
	end
	return t
end

-- collects the keys of a table into a sorted table
function table.keys(t, f)
	local keys = collect(next, t)
	table.sort(keys, f)
	return keys
end

-- iterates over the sorted keys and values of a table
function sortedpairs(t, f)
	return function(s, i)
		local k = s[i]
		return k and i + 1, k, t[k]
	end, table.keys(t, f), 1
end

-- yields string values of table or nested tables
local function stringsgen(t)
	for _, val in ipairs(t) do
		if type(val) == 'string' then
			coroutine.yield(val)
		else
			stringsgen(val)
		end
	end
end

function iterstrings(x)
	return coroutine.wrap(stringsgen), x
end

function strings(s)
	return collect(iterstrings(s))
end

-- yields strings generated by concateting all strings in a table, for every
-- combination of strings in subtables
local function expandgen(t, i)
	while true do
		local val
		i, val = next(t, i)
		if not i then
			coroutine.yield(table.concat(t))
			break
		elseif type(val) == 'table' then
			for opt in iterstrings(val) do
				t[i] = opt
				expandgen(t, i)
			end
			t[i] = val
			break
		end
	end
end

function expand(t)
	return collect(coroutine.wrap(expandgen), t)
end

-- yields expanded paths from the given path specification string
local function pathsgen(s, i)
	local results = {}
	local first = not i
	while true do
		i = s:find('[^%s]', i)
		local _, j, arch = s:find('^@([^%s()]*)%s*[^%s]?', i)
		if arch then i = j end
		if not i or s:sub(i, i) == ')' then
			break
		end
		local parts, nparts = {}, 0
		local c
		while true do
			local j = s:find('[%s()]', i)
			if not j or j > i then
				nparts = nparts + 1
				parts[nparts] = s:sub(i, j and j - 1)
			end
			i = j
			c = i and s:sub(i, i)
			if c == '(' then
				local opts, nopts = {}, 0
				local fn = coroutine.wrap(pathsgen)
				local opt
				opt, i = fn(s, i + 1)
				while opt do
					nopts = nopts + 1
					opts[nopts] = opt
					opt, i = fn(s)
				end
				nparts = nparts + 1
				parts[nparts] = opts
				if not i or s:sub(i, i) ~= ')' then
					error('unmatched (')
				end
				i = i + 1
				c = s:sub(i, i)
			else
				break
			end
		end
		if not arch or arch == config.target.platform:match('[^-]*') then
			expandgen(parts)
		end
		if not c or c == ')' then
			break
		end
	end
	if first and i then
		error('unmatched )')
	end
	return nil, i
end

function iterpaths(s)
	return coroutine.wrap(pathsgen), s
end

function paths(s)
	return collect(iterpaths(s))
end

-- yields non-empty non-comment lines in a file
local function linesgen(file)
	for line in io.lines(file) do
		if #line > 0 and not line:hasprefix('#') then
			coroutine.yield(line)
		end
	end
end

function iterlines(file, raw)
	table.insert(pkg.inputs.gen, '$dir/'..file)
	file = string.format('%s/%s/%s', basedir, pkg.gendir, file)
	if raw then
		return io.lines(file)
	end
	return coroutine.wrap(linesgen), file
end

function lines(file, raw)
	return collect(iterlines(file, raw))
end

function load(file)
	table.insert(pkg.inputs.gen, '$dir/'..file)
	return dofile(string.format('%s/%s/%s', basedir, pkg.gendir, file))
end

--
-- base constructs
--

function set(var, val, indent)
	if type(val) == 'table' then
		val = table.concat(val, ' ')
	end
	io.write(string.format('%s%s = %s\n', indent or '', var, val))
end

function subninja(file)
	if not file:match('^[$/]') then
		file = '$gendir/'..file
	end
	io.write(string.format('subninja %s\n', file))
end

function include(file)
	io.write(string.format('include %s\n', file))
end

local function let(bindings)
	for var, val in pairs(bindings) do
		set(var, val, '  ')
	end
end

function rule(name, cmd, bindings)
	io.write(string.format('rule %s\n  command = %s\n', name, cmd))
	if bindings then
		let(bindings)
	end
end

function build(rule, outputs, inputs, bindings)
	if type(outputs) == 'table' then
		outputs = table.concat(strings(outputs), ' ')
	end
	if not inputs then
		inputs = ''
	elseif type(inputs) == 'table' then
		local srcs, nsrcs = {}, 0
		for src in iterstrings(inputs) do
			nsrcs = nsrcs + 1
			srcs[nsrcs] = src
			if src:hasprefix('$srcdir/') then
				pkg.inputs.fetch[src] = true
			end
		end
		inputs = table.concat(srcs, ' ')
	elseif inputs:hasprefix('$srcdir/') then
		pkg.inputs.fetch[inputs] = true
	end
	io.write(string.format('build %s: %s %s\n', outputs, rule, inputs))
	if bindings then
		let(bindings)
	end
end

--
-- higher-level rules
--

function sub(name, fn)
	local old = io.output()
	io.output(pkg.gendir..'/'..name)
	fn()
	io.output(old)
	subninja(name)
end

function toolchain(tc)
	set('ar', tc.ar or (tc.platform and tc.platform..'-ar') or 'ar')
	set('as', tc.as or (tc.platform and tc.platform..'-as') or 'as')
	set('cc', tc.cc or (tc.platform and tc.platform..'-cc') or 'cc')
	set('ld', tc.ld or (tc.platform and tc.platform..'-ld') or 'ld')
	set('objcopy', tc.objcopy or (tc.platform and tc.platform..'-objcopy') or 'objcopy')
	set('mc', tc.mc or 'false')

	set('cflags', tc.cflags)
	set('ldflags', tc.ldflags)
end

function phony(name, inputs)
	build('phony', '$gendir/'..name, inputs)
end

function cflags(flags)
	set('cflags', '$cflags '..table.concat(flags, ' '))
end

function nasmflags(flags)
	set('nasmflags', '$nasmflags '..table.concat(flags, ' '))
end

function compile(rule, src, deps, args)
	local obj = src..'.o'
	if not src:match('^[$/]') then
		src = '$srcdir/'..src
		obj = '$outdir/'..obj
	end
	if not deps and pkg.deps then
		deps = '$gendir/deps'
	end
	if deps then
		src = {src, '||', deps}
	end
	build(rule, obj, src, args)
	return obj
end

function cc(src, deps, args)
	return compile('cc', src, deps, args)
end

function objects(srcs, deps, args)
	local objs, nobjs = {}, 0
	local rules = {
		c='cc',
		s='cc',
		S='cc',
		cc='cc',
		cpp='cc',
		asm='nasm',
	}
	local fn
	if type(srcs) == 'string' then
		fn = coroutine.wrap(pathsgen)
	else
		fn = coroutine.wrap(stringsgen)
	end
	for src in fn, srcs do
		local rule = rules[src:match('[^.]*$')]
		if rule then
			src = compile(rule, src, deps, args)
		end
		nobjs = nobjs + 1
		objs[nobjs] = src
	end
	return objs
end

function link(out, files, args)
	local objs = {}
	local deps = {}
	for _, file in ipairs(files) do
		if not file:match('^[$/]') then
			file = '$outdir/'..file
		end
		if file:hassuffix('.d') then
			deps[#deps + 1] = file
		else
			objs[#objs + 1] = file
		end
	end
	out = '$outdir/'..out
	if not args then
		args = {}
	end
	if next(deps) then
		local rsp = out..'.rsp'
		build('awk', rsp, {deps, '|', '$basedir/scripts/rsp.awk'}, {expr='-f $basedir/scripts/rsp.awk'})
		objs[#objs + 1] = '|'
		objs[#objs + 1] = rsp
		args.ldlibs = '@'..rsp
	end
	build('link', out, objs, args)
	return out
end

function ar(out, files)
	out = '$outdir/'..out
	local objs, nobjs = {}, 0
	local deps, ndeps = {out}, 1
	for _, file in ipairs(files) do
		if not file:match('^[$/]') then
			file = '$outdir/'..file
		end
		if file:find('%.[ad]$') then
			ndeps = ndeps + 1
			deps[ndeps] = file
		else
			nobjs = nobjs + 1
			objs[nobjs] = file
		end
	end
	build('ar', out, objs)
	build('rsp', out..'.d', deps)
end

function lib(out, srcs, deps)
	return ar(out, objects(srcs, deps))
end

function exe(out, srcs, deps, args)
	return link(out, objects(srcs, deps), args)
end

function yacc(name, gram)
	if not gram:match('^[$/]') then
		gram = '$srcdir/'..gram
	end
	build('yacc', expand{'$outdir/', name, {'.tab.c', '.tab.h'}}, gram, {
		yaccflags='-d -b $outdir/'..name,
	})
end

function waylandproto(proto, outs, args)
	proto = '$srcdir/'..proto
	if outs.client then
		build('wayland-proto', '$outdir/'..outs.client, proto, {type='client-header'})
	end
	if outs.server then
		build('wayland-proto', '$outdir/'..outs.server, proto, {type='server-header'})
	end
	if outs.code then
		local code = '$outdir/'..outs.code
		build('wayland-proto', code, proto, {type='public-code'})
		cc(code, {'pkg/wayland/headers'}, args)
	end
end

function fetch(method)
	local script
	local deps = {'|', '$dir/ver', script}
	if method == 'local' then
		script = '$dir/fetch.sh'
	else
		script = '$basedir/scripts/fetch-'..method..'.sh'
	end
	if method ~= 'git' then
		table.insert(deps, '$builddir/pkg/pax/host/pax')
	end
	build('fetch', '$dir/fetch', deps, {script=script})
	if basedir ~= '.' then
		build('phony', '$gendir/fetch', '$dir/fetch')
	end
	if next(pkg.inputs.fetch) then
		build('phony', table.keys(pkg.inputs.fetch), '$dir/fetch')
	end
end

local function findany(path, pats)
	for _, pat in pairs(pats) do
		if path:find(pat) then
			return true
		end
	end
	return false
end

local function specmatch(spec, path)
	if spec.include and not findany(path, spec.include) then
		return false
	end
	if spec.exclude and findany(path, spec.exclude) then
		return false
	end
	return true
end

local function fs(name, path)
	for _, spec in ipairs(config.fs) do
		for specname in iterstrings(spec) do
			if name == specname then
				return specmatch(spec, path)
			end
		end
	end
	return (config.fs.include or config.fs.exclude) and specmatch(config.fs, path)
end

function gitfile(path, mode, src)
	local out = '$builddir/root.hash/'..path
	local perm = ('10%04o %s'):format(tonumber(mode, 8), path)
	build('git-hash', out, {src, '|', '$basedir/scripts/hash.sh', '||', '$builddir/root.stamp'}, {
		args=perm,
	})
	table.insert(pkg.inputs.index, out)
end

function file(path, mode, src)
	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	mode = ('%04o'):format(tonumber(mode, 8))
	pkg.fspec[path] = {
		type='reg',
		mode=mode,
		source=src:gsub('^%$(%w+)', pkg, 1),
	}
	gitfile(path, mode, src)
end

function dir(path, mode)
	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	pkg.fspec[path] = {
		type='dir',
		mode=('%04o'):format(tonumber(mode, 8)),
	}
end

function sym(path, target)
	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	pkg.fspec[path] = {
		type='sym',
		mode='0777',
		target=target,
	}
	local out = '$builddir/root.hash/'..path
	build('git-hash', out, {'|', '$basedir/scripts/hash.sh', '||', '$builddir/root.stamp'}, {
		args=string.format('120000 %s %s', path, target),
	})
	table.insert(pkg.inputs.index, out)
end

function man(srcs, section)
	for _, src in ipairs(srcs) do
		local out = src..'.gz'
		if not src:match('^[$/]') then
			src = '$srcdir/'..src
			out = '$outdir/'..out
		end

		local base = src:match('[^/]*$')
		local ext = base:match('%.([^.]*)$')
		if ext then base = base:sub(1, -(#ext + 2)) end
		if section then ext = section end
		local path = 'share/man/man'..ext..'/'..base..'.'..ext
		if config.gzman ~= false then
			build('gzip', out, src)
			src = out
			path = path..'.gz'
		end
		file(path, '644', src)
	end
end

function copy(outdir, srcdir, files)
	local outs = {}
	for file in iterstrings(files) do
		local out = outdir..'/'..file
		table.insert(outs, out)
		build('copy', out, srcdir..'/'..file)
	end
	return outs
end
