--[[
License for the LZW compression:
MIT License
Copyright (c) 2016 Rochet2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

local util_raw = [[
local function canonicalize(path)
	return fs.combine(path, "")
end

local function segments(path)
	if canonicalize(path) == "" then return {} end
	local segs, rest = {}, path
	repeat
		table.insert(segs, 1, fs.getName(rest))
		rest = fs.getDir(rest)
	until rest == ""
	return segs
end

local function slice(tab, start, end_)
	return {table.unpack(tab, start, end_)}
end

local function compact_serialize(x)
	local t = type(x)
	if t == "number" then
		return tostring(x)
	elseif t == "string" then
		return textutils.serialise(x)
	elseif t == "table" then
		local out = "{"
		for k, v in pairs(x) do
			out = out .. string.format("[%s]=%s,", compact_serialize(k), compact_serialize(v))
		end
		return out .. "}"
	elseif t == "boolean" then
		return tostring(x)
	else
		error("Unsupported type " .. t)
	end
end

local function drop_last(t)
	local clone = slice(t)
	local length = #clone
	local v = clone[length]
	clone[length] = nil
	return clone, v
end
]]

local util = loadstring(util_raw .. "\nreturn {segments = segments, slice = slice, drop_last = drop_last, compact_serialize = compact_serialize}")()

local runtime = util_raw .. [[
local savepath = ".crane-persistent/" .. fname

-- Simple string operations
local function starts_with(s, with)
	return string.sub(s, 1, #with) == with
end
local function ends_with(s, with)
	return string.sub(s, -#with, -1) == with
end
local function contains(s, subs)
	return string.find(s, subs) ~= nil
end

local function copy_some_keys(keys)
	return function(from)
		local new = {}
		for _, key_to_copy in pairs(keys) do
			local x = from[key_to_copy]
			if type(x) == "table" then
				x = copy(x)
			end
			new[key_to_copy] = x
		end
		return new
	end
end

local function find_path(image, path)
	local focus = image
	local path = path
	if type(path) == "string" then path = segments(path) end
	for _, seg in pairs(path) do
		if type(focus) ~= "table" then error("Path segment " .. seg .. " is nonexistent or not a directory; full path " .. compact_serialize(path)) end
		focus = focus[seg]
	end
	return focus
end

local function get_parent(image, path)
	local init, last = drop_last(segments(path))
	local parent = find_path(image, init) or image
	return parent, last
end

-- magic from http://lua-users.org/wiki/SplitJoin
-- split string into lines
local function lines(str)
	local t = {}
	local function helper(line)
		table.insert(t, line)
		return ""
	end
	helper((str:gsub("(.-)\r?\n", helper)))
	return t
end

local function make_read_handle(text, binary)
	local lines = lines(text)
	local h = {}
	local line = 0
	function h.close() end
	if not binary then
		function h.readLine() line = line + 1 return lines[line] end
		function h.readAll() return text end
	else
		local remaining = text
		function h.read()
			local by = string.byte(remaining:sub(1, 1))
			remaining = remaining:sub(2)
			return by
		end
	end
	return h
end

local function make_write_handle(writefn, binary)
	local h = {}
	function h.close() end
	function h.flush() end
	if not binary then
		function h.write(t) return writefn(t) end
		function h.writeLine(t) return writefn(t .. "\n") end
	else
		function h.write(b) return writefn(string.char(b)) end
	end
	return h
end

local function mount_image(i)
	local options = i.options
	local image = i.tree
	
	local filesystem = copy_some_keys {"getName", "combine", "getDir"} (_G.fs)
	
	function filesystem.getFreeSpace()
		return math.huge -- well, it's in-memory...
	end
	
	function filesystem.exists(path)
		return find_path(image, path) ~= nil
	end
	
	function filesystem.isDir(path)
		return type(find_path(image, path)) == "table"
	end
	
	function filesystem.makeDir(path)
		local p, n = get_parent(image, path)
		p[n] = {}
	end
	
	function filesystem.delete(path)
		local p, n = get_parent(image, path)
		p[n] = nil
	end
	
	function filesystem.copy(from, to)
		local pf, nf = get_parent(image, from)
		local contents = pf[nf]
		local pt, nt = get_parent(image, to)
		pt[nt] = contents
	end
	
	function filesystem.move(from, to)
		filesystem.copy(from, to)
		local pf, nf = get_parent(image, from)
		pf[nf] = nil
	end
	
	function filesystem.contents(path)
		return find_path(image, path)
	end
	
	function filesystem.list(path)
		local out = {}
		local dir = find_path(image, path)
		for k, v in pairs(dir) do table.insert(out, k) end
		return out
	end
	
	function filesystem.open(path, mode)
		local parent, childname = get_parent(image, path)
		local node = parent[childname]
		local is_binary = ends_with(mode, "b")
		if starts_with(mode, "r") then
			if type(node) ~= "string" then error(path .. ": not a file!") end
			return make_read_handle(node, is_binary)
		elseif starts_with(mode, "a") or starts_with(mode, "w") then
			local function writer(str)
				parent[childname] = parent[childname] .. str
				if options.save_on_change then filesystem.save() end
			end
			if not starts_with(mode, "a") or node == nil then parent[childname] = "" end
			return make_write_handle(writer, is_binary)
		end
	end
	
	function filesystem.find(wildcard)
		-- Taken from Harbor: https://github.com/hugeblank/harbor/blob/master/harbor.lua
		-- Body donated to harbor by gollark, from PotatOS, and apparently indirectly from cclite:
		-- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua
		local function recurse_spec(results, path, spec)
			local segment = spec:match('([^/]*)'):gsub('/', '')
			local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.') .. '$'
			
			if filesystem.isDir(path) then
				for _, file in ipairs(filesystem.list(path)) do
					if file:match(pattern) then
						local f = filesystem.combine(path, file)
						
						if filesystem.isDir(f) then
							recurse_spec(results, f, spec:sub(#segment + 2))
						end
						if spec == segment then
							table.insert(results, f)
						end
					end
				end
			end
		end
		local results = {}
		recurse_spec(results, '', wildcard)
		return results
	end
	
	function filesystem.getDrive()
		return "crane-vfs-" .. fname
	end

	function filesystem.isReadOnly(path)
		return false
	end

	local fo = fs.open
	function filesystem.save()
		local f = fo(savepath, "w")
		f.write(compact_serialize(i))
		f.close()
	end

	return filesystem
end

local function deepmerge(t1, t2)
	local out = {}
	for k, v in pairs(t1) do
		local onother = t2[k]
		if type(v) == "table" and type(onother) == "table" then
			out[k] = deepmerge(v, onother)
		else
			out[k] = v
		end
	end
	for k, v in pairs(t2) do
		if not out[k] then
			out[k] = v
		end
	end
	return out
end

local cli_args = {...}

local f = fs.open("/rom/apis/io.lua", "r") -- bodge reloading IO library
local IO_API_code = f.readAll()
f.close()

local function load_API(code, env, name)
	local e = setmetatable({}, { __index = env })
	load(code, "@" .. name, "t", env)()
	env[name] = e
end

local function replacement_require(path)
	return dofile(path)	
end

local function execute(image, filename)
	local image = image
	if fs.exists(savepath) then
		local f = fs.open(savepath, "r")
		image = deepmerge(image, textutils.unserialise(f.readAll()))
	end

	local f = mount_image(image)

	local env = setmetatable({ fs = f, rawfs = _G.fs, require = replacement_require, 
		os = setmetatable({}, { __index = _ENV.os }), { __index = _ENV })
	load_API(IO_API_code, env, "io")
	local func, err = load(f.contents(filename), "@" .. filename, "t", env)
	if err then error(err)
	else
		env.os.reboot = function() func() end
		return func(unpack(cli_args)) 
	end
end
]]

-- LZW Compressor
local a=string.char;local type=type;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;local function compress(n)if type(n)~="string"then return nil,"string expected, got "..type(n)end;local o=#n;if o<=1 then return"u"..n end;local k={}local l,m=0,1;local p={"c"}local q=1;local r=2;local s=""for f=1,o do local t=b(n,f,f)local u=s..t;if not(d[u]or k[u])then local v=d[s]or k[s]if not v then return nil,"algorithm error, could not fetch word"end;p[r]=v;q=q+#v;r=r+1;if o<=q then return"u"..n end;k,l,m=i(u,k,l,m)s=t else s=u end end;p[r]=d[s]or k[s]q=q+#p[r]r=r+1;if o<=q then return"u"..n end;return c(p)end

local wrapper = [[local function y(b)local c="-"local d="__#"..math.random(0,10000)local e="\0";return b:gsub(c,d):gsub(e,c):gsub(d,e)end;local z="decompression failure; please redownload or contact developer";local a=string.char;local type=type;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;local function n(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[a(l,m)]=j;l=l+1;return k,l,m end;local function dec(o)if type(o)~="string"then return nil,z end;if#o<1 then return nil,z end;local p=b(o,1,1)if p=="u"then return b(o,2)elseif p~="c"then return nil,z end;o=b(o,2)local q=#o;if q<2 then return nil,z end;local k={}local l,m=0,1;local r={}local s=1;local t=b(o,1,2)r[s]=e[t]or k[t]s=s+1;for f=3,q,2 do local u=b(o,f,f+1)local v=e[t]or k[t]if not v then return nil,z end;local w=e[u]or k[u]if w then r[s]=w;s=s+1;k,l,m=n(v..b(w,1,1),k,l,m)else local x=v..b(v,1,1)r[s]=x;s=s+1;k,l,m=n(x,k,l,m)end;t=u end;return c(r)end;local o,e=dec(y(%s));if e then error(e) end;load(o,"@loader","t",_ENV)(...)]]

local function encode_nuls(txt)
	local replace = "\0"
	local temp_replacement = ("__#%d#__"):format(math.random(-131072, 131072))
	local replace_with = "-"
	return txt:gsub(replace, temp_replacement):gsub(replace_with, replace):gsub(temp_replacement, replace_with)
end

local function compress_code(c)
	local comp = encode_nuls(compress(c))
	local txt = string.format("%q", comp):gsub("\\(%d%d%d)([^0-9])", function(x, y) return string.format("\\%d%s", tonumber(x), y) end)
	local out = wrapper:format(txt)
	--print(loadstring(out)())
	return out
end

local function find_imports(code)
	local imports = {}

	for i in code:gmatch "%-%-| CRANE ADD [\"'](.-)[\"']" do
		table.insert(imports, i)
		print("Detected explicit import", i)
	end
	return imports
end

local function add(path, content, tree)
	local segs, last = util.drop_last(util.segments(path))
	local deepest = tree
	for k, seg in pairs(segs) do
		if not deepest[seg] then
			deepest[seg] = {}
		end
		deepest = deepest[seg]
	end
	deepest[last] = content
end

local function load_from_root(file, tree)
	print("Adding", file)
	if not fs.exists(file) then error(file .. " does not exist.") end
	if fs.isDir(file) then
		for _, f in pairs(fs.list(file)) do
			load_from_root(fs.combine(file, f), tree)
		end
		return
	end

	local f = fs.open(file, "r")
	local c = f.readAll()
	f.close()
	add(file, c, tree)
	local imports = find_imports(c)
	for _, i in pairs(imports) do
		load_from_root(i, tree)
	end
end

local args = {...}
if #args < 2 then
	error([[Usage:
crane [output] [bundle startup] [other files to bundle] ]])
end

local root = args[2]
local ftree = {}

for _, wildcard in pairs(util.slice(args, 2)) do
	for _, possibility in pairs(fs.find(wildcard)) do
		load_from_root(possibility, ftree)
	end
end


local function minify(code)
	local url = "https://osmarks.tk/luamin/"
	http.request(url, code)
	while true do
		local event, result_url, handle = os.pullEvent()
		if event == "http_success" then
			local text = handle.readAll()
			handle.close()
			return text
		elseif event == "http_failure" then
			local text = handle.readAll()
			handle.close()
			error(text)
		end
	end
end

ftree[root] = minify(ftree[root])
local serialized_tree = util.compact_serialize({
	tree = ftree,
	options = {
		save_on_change = true
	}
})

local function shortest(s1, s2)
	if #s1 < #s2 then return s1 else return s2 end
end

local output = minify(([[
local fname = %s
%s
local image = %s
return execute(image, fname)
]]):format(util.compact_serialize(root), runtime, serialized_tree))

local f = fs.open(args[1], "w")
f.write("--| CRANE BUNDLE v2\n" .. shortest(compress_code(output), output))
f.close()

print "Done!"