-- Rings application for WSAPI

local rings = require "rings"

local _M = {}

local init = [==[
	local app_name, bootstrap_code, is_file = ...
	if bootstrap_code then
		local bootstrap, err
		if string.match(bootstrap_code, "%w%.lua$") then
			bootstrap, err = loadfile(bootstrap_code)
		else
			bootstrap, err = loadstring(bootstrap_code)
		end
		if bootstrap then
			bootstrap()
		else
			error("could not load " .. bootstrap_code .. ": " .. err)
		end
	else
		_, package.path = remotedostring("return package.path")
		_, package.cpath = remotedostring("return package.cpath")
	end
	local common = require"wsapi.common"
	require"coxpcall"
	pcall = copcall
	xpcall = coxpcall
	local wsapi_error = {
		write = function (self, err)
			remotedostring("env.error:write(...)", err)
		end
	}
	local wsapi_input =  {
		read = function (self, n)
			return coroutine.yield("RECEIVE", n)
		end
	}
	local wsapi_meta = { 
		__index = function (tab, k)
			if k == "headers" then
				local headers
				local _, all_headers = remotedostring([[
								if env.headers then
									local out = {}
									for k, v in pairs(env.headers) do
										table.insert(out, 
											"[" .. string.format("%q", k) .. "]=" .. 
											string.format("%q", v))
									end
									return "return {" .. table.concat(out, ",") .. "}"
								end
							]])
				if all_headers then
					local v = loadstring(all_headers)()
					rawset(tab, k, v)
					return v
				end
			else
				local  _, v = remotedostring("return env[(...)]", k)
				rawset(tab, k, v)
				return v
			end
		end,
		__newindex = function (tab, k, v)
			rawset(tab, k, v)
			remotedostring("local k, v = ...; env[k] = v", k, v)
		end
	}
	local app = common.normalize_app(app_name, is_file)
	main_func = function ()
		local wsapi_env = { error = wsapi_error, input = wsapi_input }
		setmetatable(wsapi_env, wsapi_meta)
		local ok, status, headers, res = common.run_app(app, wsapi_env)
		if not ok then
			local msg = status
			headers = { ["Content-Type"] = "text/html" }
			if wsapi_env.STATUS ~= "" then
				status = wsapi_env.STATUS
				res = coroutine.wrap(function() coroutine.yield(msg) end)
			else
				status = 500
				res = coroutine.wrap(function() coroutine.yield(common.error_html(msg)) end)
			end
		end
		remotedostring("status = ...", status)
		for k, v in pairs(headers) do
			if type(v) == "table" then
				remotedostring("headers[(...)] = {}", k)
				for _, val in ipairs(v) do
					remotedostring("local k, v = ...; table.insert(headers[k], v)", k, val)
				end
			else
				remotedostring("local k, v = ...; headers[k] = v", k, v)
			end
		end
		local s, v = res()
		while s do
			if s == "RECEIVE" and v then
				s, v = res(coroutine.yield(s, v))
			else
				coroutine.yield("SEND", s)
			end
			s, v = res()
		end
		return "SEND", nil
	end
]==]

-- Returns a WSAPI application that runs the provided WSAPI application
-- in an isolated Lua environment
function _M.new(app_name, bootstrap, is_file)
	local data = { created_at = os.time() }
	setmetatable(data, { __index = _G })
	local state = rings.new(data)
	assert(state:dostring(init, app_name, bootstrap, is_file))
	local error = function (msg)
		data.status, data.headers, data.env = nil
		error(msg)
	end
	return function (wsapi_env)
		if state and wsapi_env == "close" then
			state:close()
			state = nil
		end
		if not state then
			return nil
		end
		if rawget(data, "status") then 
			error("this state is already in use")
		end
		data.status = 500
		data.headers = {}
		data.env = wsapi_env
		local ok, flag, s = 
			state:dostring([[
				main_coro = coroutine.wrap(main_func)
				return main_coro(...)
			]])
		repeat
			if not ok then error(flag) end
			if flag == "RECEIVE" then
				ok, flag, s = state:dostring("return main_coro(...)",
												wsapi_env.input:read(s))
			elseif flag == "SEND" then
				break
			else
				error("Invalid command: " .. tostring(flag))
			end
		until flag == "SEND"
		local res
		if not s then
			res = function() return nil end
		else
			res = function ()
				if s then 
					local res = s
					s = nil
					return res
				end
				local ok, flag, s = state:dostring("return main_coro()")
				while ok and flag and s do
					if flag == "RECEIVE" then
						ok, flag, s = state:dostring("return main_coro(...)",
								wsapi_env.input:read(s))
					elseif flag == "SEND" then
						return s
					else
						error("Invalid command: " .. tostring(flag))
					end
				end
				data.status, data.headers, data.env = nil
				if data.cleanup then
					state:close()
					state = nil
				end
				if not ok then error(flag) end
			end
		end
		return data.status, data.headers, res 
	end, data
end

return _M
