-- LUALOCALS < ---------------------------------------------------------
local core, pairs, setmetatable, type
    = core, pairs, setmetatable, type
-- LUALOCALS > ---------------------------------------------------------

------------------------------------------------------------------------
-- High-level connection manager, which wraps archipelago connections,
-- manages automatic reconnect, message send buffering and retry,
-- and high-level message abstractions
------------------------------------------------------------------------

local env = ...
local include = env.include

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

local util = include("util")
local deepcopy = util.deepcopy
local create_emitter = util.create_emitter
local connection = include("connection")

-- nil properties document configuration we expect
-- to be set elsewhere.
local prototype = {
	-- For persisting send queue across restarts
	modstore = nil,
	storeprefix = nil,
	-- Plus any (e.g. login_slotname) from connection
	-- Plus any (e.g. url_target) from websocket

	-- Last known snapshot of state, which may include
	-- cached state from previous connection.
	state = nil,
}

-- Emits the same events as underlying connection

-- Maintains a cached version of state that's accessible even
-- when underlying connection is down.

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

	self.storage = self.storage or self.modstore
	and util.structstore(self.modstore, self.storeprefix)

	return self
end

-- Keep connections open when we have one enabled
function prototype:reconnect_check()
	if self.connection and not self.connection.sock then
		self.connection:connect()
	end
	if self.check_job then self.check_job:cancel() end
	self.check_job = core.after(5, function()
			return self:reconnect_check()
		end)
end

-- Enable/open connection
function prototype:open()
	if self.connection then return end

	-- Reuse self as connection config so our caller can override
	-- all lower level options (such as proxy) also.
	self.connection = connection(self)

	self:reconnect_check()

	-- Emit the same events as connection
	self.connection.on(false, self.emit)

	-- Update our cross-connection saved snapshot on update
	self.connection.on("update", function()
			local s = self.connection and self.connection.state
			if not s then return end
			self.state = self.state or {}
			for k, v in pairs(s) do self.state[k] = v end
		end)

	-- Translate PrintJSON messages
	self.connection.on("cmd:PrintJSON", function(cmd)
			local text = cmd and cmd.data
			and util.print_json_convert(self.state, cmd.data)
			self.emit("printtext", text)
		end)

	-- Flush any pending messages as soon as connection is
	-- reestablished.
	self.connection.on("cmd:Connected", function()
			return self:flush_send_queue()
		end)
end

-- Disable/close connection
function prototype:close(...)
	if self.connection then self.connection:disconnect(...) end
	self.connection = nil
end

------------------------------------------------------------------------
-- Message Send Pump

-- Manage message sending queue(s)
local function enqueue(self, cmds, reliable)
	-- Append the cmds to the appropriate queue.
	do
		local q, save
		if reliable and self.storage then
			q, save = self.storage("sendqueue")
		else
			q = self.sendq or {}
			self.sendq = q
		end
		for i = 1, #cmds do q[#q + 1] = cmds[i] end
		if save then save() end
	end

	-- Make sure there's a resend job pending.
	self.send_pending = self.send_pending
	or core.after(1, function()
			self.send_pending = nil
			return self:flush_send_queue()
		end)
end

-- Explicitly flush any pending messages in the send queue;
-- automatically run by queue processing timer, and also on
-- connection established. Note that if there's a pending
-- resend job, calling this out-of-cycle will not affect the
-- timing of that job.
function prototype:flush_send_queue()
	-- Reprocess unreliable messages.
	if self.sendq then
		local batch = self.sendq
		if #batch > 0 then
			self.sendq = nil
			self:send_queue(batch)
		end
	end

	-- Reprocess reliable messages.
	if self.storage then
		local data, save = self.storage("sendqueue")
		if #data > 0 then
			local batch = {}
			for i = 1, #data do batch[i] = data[i] end
			for i = #data, 1, -1 do data[i] = nil end
			save()
			self:send_queue(batch, true)
		end
	end
end

-- cmds = command(s) (single or array)
-- reliable = truthy to guarantee across restarts if modstore available
function prototype:send_queue(cmds, reliable)
	if cmds.cmd then cmds = {cmds} end
	if self.connection then
		return self.connection:send(cmds, function(ok)
				if not ok then enqueue(self, cmds, reliable) end
			end)
	end
	return enqueue(self, cmds, reliable)
end

-- Check a location or array of locations, either by ID
-- or by name. (persisted across restarts)
function prototype:check_location(locations)
	locations = util.deepcopy(type(locations) == "table"
		and locations or {locations})
	local pkg = self.state and self.state.data_package
	local game = pkg and pkg.games and pkg.games[self.login_gamename]
	local lut = game and game.location_name_to_id
	for i = 1, #locations do
		local id = locations[i]
		if lut and type(id) == "string" then
			locations[i] = lut[id] or id
		end
	end
	return self:send_queue({
			cmd = "LocationChecks",
			locations = locations
		}, true)
end

-- Send goal completion (persisted across restarts)
function prototype:goal_complete()
	return self:send_queue({
			cmd = "StatusUpdate",
			status = 30 -- ClientGoal.GOAL (30 = goal completed)
		}, true)
end

-- Send a chat message
function prototype:say(text)
	return self:send_queue({
			cmd = "Say",
			text = text
		})
end

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

return manager
