-- LUALOCALS < ---------------------------------------------------------
local core, setmetatable, tostring
    = core, setmetatable, tostring
-- LUALOCALS > ---------------------------------------------------------

------------------------------------------------------------------------
-- Lower-level websocket abstraction: recreates a websocket connection
-- with async/event-driven message passing through Luanti HTTP API and
-- the websockproxy.
------------------------------------------------------------------------

local env = ...
local include = env.include
local fetch = env.http.fetch

------------------------------------------------------------------------

local util = include("util")
local deepcopy = util.deepcopy
local create_emitter = util.create_emitter
local urlencode = core.urlencode
local parse_json = core.parse_json

local timeout_poll = 30
local timeout_immediate = 5
local timeout_ignore = 30

-- nil properties document configuration we expect consumer to set
local prototype = {
	url_proxy = "http://localhost:9839",
	url_target = nil,
}

-- Constructor
local function websocket(options)
	local self = deepcopy(options)
	setmetatable(self, {__index = prototype})
	self.on, self.emit = create_emitter()
	return self
end

-- Events emitted:
-- ("message", message) for each message received
-- ("disconnect") when connection lost because of peer or network
-- ("pollerror", code) for transient errors during polling
-- A disconnect event is NOT emitted for disconnects initiated locally

------------------------------------------------------------------------
-- MESSAGE RECEIVE PUMP
-- Trigger a single action immediately with a callback

-- Poll for messages from proxy
local function pollcycle(self)
	if not self.session then return end

	if self.polling then return end
	self.polling = true

	local url = self.url_proxy .. "/poll?session=" .. urlencode(self.session)
	fetch({
			url = url,
			timeout = timeout_poll,
		}, function(res)
			self.polling = nil
			if res.succeeded and res.code >= 200 and res.code < 300 then
				local raw = res.data
				local data = raw and raw ~= "" and parse_json(raw)
				local msgs = data and data.messages or {}
				util.log("action", "websocket poll", #msgs)
				for i = 1, #msgs do
					self.emit("message", msgs[i])
				end
				return pollcycle(self)
			elseif res.code >= 400 and res.code < 500 then
				util.log("action", "websocket poll", res)
				self.session = nil
				self.emit("disconnect", res.code)
			else
				util.log("action", "websocket poll", res)
				self.emit("pollerror", res.code)
				core.after(1, pollcycle, self)
			end
		end)
end

------------------------------------------------------------------------
-- ACTIVE METHODS
-- Trigger a single action immediately with a callback

-- Connect to websocket through proxy
function prototype:connect(cb)
	local url = self.url_proxy .. "/connect?target=" .. urlencode(self.url_target)
	util.log("action", "websocket", url)
	fetch({
			url = url,
			timeout = timeout_immediate
		}, function(res)
			util.log("action", "websocket", url, res)
			if (not res.succeeded) or res.code < 200 or res.code >= 300 then
				return cb and cb(false, res.code or "timeout")
			end
			local data = parse_json(res.data)
			self.session = data and data.session
			if not self.session then
				return cb and cb(false, "no session id")
			end
			pollcycle(self)
			return cb and cb(true)
		end)
end

-- Disconnect from proxy
-- The connection is immediately invalidated locally. We fire and forget a
-- request to disconnect to the proxy; if that request is lost, the proxy
-- will eventually disconnect on timeout.
function prototype:disconnect()
	if not self.session then return end
	local url = self.url_proxy .. "/disconnect?session=" .. core.urlencode(self.session)
	self.session = nil
	util.log("action", "websocket", url)
	fetch({
			url = url,
			method = "POST",
			timeout = timeout_ignore,
		}, function() end)
end

-- Send data through proxy
function prototype:send(data, cb)
	if not self.session then return cb(false, "not connected") end
	local url = self.url_proxy .. "/send?session=" .. urlencode(self.session)
	util.log("action", "websocket", url, #tostring(data))
	fetch({
			url = url,
			method = "POST",
			data = data,
			timeout = timeout_immediate,
		}, function(res)
			if not cb then return end
			if (not res.succeeded) or res.code < 200 or res.code >= 300 then
				return cb(false, res.code or "timeout")
			end
			return cb(true)
		end)
end

------------------------------------------------------------------------

return websocket
