-- LUALOCALS < ---------------------------------------------------------
local core, ipairs, pairs, setmetatable, type
    = core, ipairs, 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 lut = util.getpath(self.state, "data_package", "games",
		self.login_gamename, "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

------------------------------------------------------------------------
-- Location Scouting/Hinting

do
	local function has_flag(flags, mask)
		if not flags then return end
		local div = (flags - flags % mask) / mask
		return div % 2 == 1 or nil
	end
	local hint_status_names = {
		[10] = "no_priority",
		[20] = "avoid",
		[30] = "priority",
		[40] = "found",
	}

	-- Retrieve information about a location in our world, and the item located
	-- there. Combines scouts and hints (scout data "wins"). Consumer is
	-- responsible for deciding what information to withhold from player for
	-- gameplay reasons (especially hiding everything except receiver and
	-- progression flag if hint_status is nil)
	function prototype:location_info(id)
		local state = self.state
		local myslot = util.getpath(state, "slot")

		local scouted = util.getpath(state, "location_info", id)

		-- Look for matching hints. If multiple match, keep the largest
		-- value for each key: arbitrary for most fields, but significant
		-- for the status field.
		local hinted
		for _, h in ipairs(util.getpath(state, "hints") or {}) do
			if h.location == id and h.finding_player == myslot then
				hinted = hinted or {}
				for k, v in pairs(h) do
					if (not hinted[k]) or v > hinted[k] then hinted[k] = v end
				end
			end
		end

		local result = {
			location_id = id,
			location_name = util.get_location_name(state, self.login_gamename, id),
			item_id = scouted and scouted.item or hinted and hinted.item,
			-- item_name (below)
			receiving_player_id = scouted and scouted.player
			or hinted and hinted.receiving_player,
			-- receiving_player_name (below)
			-- receiving_game (below)
			hint_status = hinted and (hint_status_names[hinted.status] or "unspecified"),
			item_flags = scouted and {
				progression = has_flag(scouted.flags, 1),
				useful = has_flag(scouted.flags, 2),
				trap = has_flag(scouted.flags, 4),
			}
		}

		result.receiving_player_name = util.get_player_name(
			state, result.receiving_player_id)
		result.receiving_game = result.receiving_player_id
		and util.get_slot_game(state, result.receiving_player_id)
		result.item_name = result.item_id and result.receiving_game
		and util.get_item_name(state, result.receiving_game, result.item_id)

		return result
	end
end

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

return manager
