-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local Connection = TSM.Init("Service.SyncClasses.Connection")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local Event = TSM.Include("Util.Event")
local Settings = TSM.Include("Service.Settings")
local Threading = TSM.Include("Service.Threading")
local Constants = TSM.Include("Service.SyncClasses.Constants")
local Comm = TSM.Include("Service.SyncClasses.Comm")
local private = {
	isActive = false,
	hasFriendsInfo = false,
	newCharacter = nil,
	newAccount = nil,
	newSyncAcked = nil,
	connectionChangedCallbacks = {},
	threadId = {},
	threadRunning = {},
	connectedCharacter = {},
	lastHeartbeat = {},
	suppressThreadTime = {},
	connectionRequestReceived = {},
	addedFriends = {},
	invalidCharacters = {},
}
local RECEIVE_TIMEOUT = 5
local HEARTBEAT_TIMEOUT = 10



-- ============================================================================
-- Module Loading
-- ============================================================================

Connection:OnSettingsLoad(function()
	Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
	Event.Register("FRIENDLIST_UPDATE", private.PrepareFriendsInfo)
	for _ in Settings.SyncAccountIterator() do
		private.isActive = true
	end
	Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.WhoAmIAccountHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACK, private.WhoAmIAckHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST, private.ConnectionHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, private.ConnectionHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.DISCONNECT, private.DisconnectHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.HEARTBEAT, private.HeartbeatHandler)

	private.PrepareFriendsInfo()
end)

Connection:OnModuleUnload(function()
	for _, player in pairs(private.connectedCharacter) do
		Comm.SendData(Constants.DATA_TYPES.DISCONNECT, player)
	end
end)



-- ============================================================================
-- Module Functions
-- ============================================================================

function Connection.RegisterConnectionChangedCallback(handler)
	tinsert(private.connectionChangedCallbacks, handler)
end

function Connection.IsCharacterConnected(targetCharacter)
	for _, player in pairs(private.connectedCharacter) do
		if player == targetCharacter then
			return true
		end
	end
	return false
end

function Connection.ConnectedAccountIterator()
	return pairs(private.connectedCharacter)
end

function Connection.Establish(targetCharacter)
	if not private.hasFriendsInfo then
		Log.PrintUser(L["TSM is not yet ready to establish a new sync connection. Please try again later."])
		return false
	end
	local wasFriend = C_FriendList.GetFriendInfo(targetCharacter) and true or false
	if strlower(targetCharacter) == strlower(UnitName("player")) then
		Log.PrintUser(L["Sync Setup Error: You entered the name of the current character and not the character on the other account."])
		return false
	elseif not private.IsOnline(targetCharacter) and wasFriend then
		Log.PrintUser(L["Sync Setup Error: The specified player on the other account is not currently online."])
		return false
	end
	local invalidCharacter = false
	for _, player in Settings.CharacterByFactionrealmIterator() do
		if strlower(player) == strlower(targetCharacter) then
			invalidCharacter = true
		end
	end
	if invalidCharacter then
		Log.PrintUser(L["Sync Setup Error: This character is already part of a known account."])
		return false
	end
	if not private.isActive then
		private.isActive = true
		Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
	end
	private.newCharacter = targetCharacter
	private.newAccount = nil
	private.newSyncAcked = nil
	Delay.Cancel("syncNewAccount")
	Delay.AfterTime("syncNewAccount", 0, private.SendNewAccountWhoAmI, 1)
	return true
end

function Connection.GetNewAccountStatus()
	if not private.newCharacter then
		return nil
	end
	return format(L["Connecting to %s"], private.newCharacter)
end

function Connection.GetStatus(account)
	if private.connectedCharacter[account] then
		return true, private.connectedCharacter[account]
	else
		return false
	end
end

function Connection.Remove(account)
	if private.threadRunning[account] then
		Threading.Kill(private.threadId[account])
		private.ConnectionThreadDone(account)
	end
	Settings.RemoveSyncAccount(account)
end

function Connection.GetConnectedCharacterByAccount(account)
	return private.connectedCharacter[account]
end



-- ============================================================================
-- Message Handlers
-- ============================================================================

function private.WhoAmIAckHandler(dataType, sourceAccount, sourceCharacter, data)
	assert(dataType == Constants.DATA_TYPES.WHOAMI_ACK)
	if not private.newCharacter or strlower(private.newCharacter) ~= strlower(sourceCharacter) then
		-- we aren't trying to connect with a new account
		return
	end
	Log.Info("WHOAMI_ACK '%s'", tostring(private.newCharacter))
	private.newSyncAcked = true
	private.CheckNewAccountStatus()
end

function private.WhoAmIAccountHandler(dataType, sourceAccount, sourceCharacter, data)
	assert(dataType == Constants.DATA_TYPES.WHOAMI_ACCOUNT)
	if not private.newCharacter then
		-- we aren't trying to connect with a new account
		return
	elseif strlower(private.newCharacter) ~= strlower(sourceCharacter) then
		Log.Info("WHOAMI_ACCOUNT from unknown player \"%s\", expected \"%s\"", private.newCharacter, sourceCharacter)
		return
	end
	private.newCharacter = sourceCharacter -- get correct capatilization
	private.newAccount = sourceAccount
	Log.Info("WHOAMI_ACCOUNT '%s' '%s'", private.newCharacter, private.newAccount)
	Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACK, private.newCharacter)
	private.CheckNewAccountStatus()
end

function private.ConnectionHandler(dataType, sourceAccount, sourceCharacter, data)
	if not private.threadRunning[sourceAccount] then
		return
	end
	private.connectionRequestReceived[sourceAccount] = true
end

function private.DisconnectHandler(dataType, sourceAccount, sourceCharacter, data)
	assert(dataType == Constants.DATA_TYPES.DISCONNECT)
	if not private.threadRunning[sourceAccount] then
		return
	end

	-- kill the thread and prevent it from running again for 2 seconds
	Threading.Kill(private.threadId[sourceAccount])
	private.ConnectionThreadDone(sourceAccount)
	private.suppressThreadTime[sourceAccount] = time() + 2
end

function private.HeartbeatHandler(dataType, sourceAccount, sourceCharacter)
	assert(dataType == Constants.DATA_TYPES.HEARTBEAT)
	if not Connection.IsCharacterConnected(sourceCharacter) then
		-- we're not connected to this player
		return
	end
	private.lastHeartbeat[sourceAccount] = time()
end



-- ============================================================================
-- Management Loop / Sync Thread
-- ============================================================================

function private.RequestFriendsInfo()
	C_FriendList.ShowFriends()
end

function private.PrepareFriendsInfo()
	-- wait for friend info to populate
	local isValid
	local num = C_FriendList.GetNumFriends()
	if not num then
		isValid = false
	else
		isValid = true
	end
	for i = 1, num or 0 do
		if not C_FriendList.GetFriendInfoByIndex(i) then
			isValid = false
			break
		end
	end
	if isValid then
		if not private.hasFriendsInfo and private.isActive then
			-- start the management loop
			Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
		end
		private.hasFriendsInfo = true
	else
		-- try again
		Log.Err("Missing friends info - will try again")
		Delay.AfterTime("SYNC_PREPARE_FRIENDS_INFO", 0.5, private.RequestFriendsInfo)
	end
end

function private.ManagementLoop()
	-- continuously spawn connection threads with online players as necessary
	private.RequestFriendsInfo()
	local hasAccount = false
	for _, account in Settings.SyncAccountIterator() do
		hasAccount = true
		local targetCharacter = private.GetTargetCharacter(account)
		if targetCharacter then
			if not private.threadId[account] then
				private.threadId[account] = Threading.New("SYNC_"..strmatch(account, "(%d+)$"), private.ConnectionThread)
			end
			if not private.threadRunning[account] and (private.suppressThreadTime[account] or 0) < time() then
				private.threadRunning[account] = true
				Threading.Start(private.threadId[account], account, targetCharacter)
			end
		end
	end
	if not hasAccount then
		Log.Info("No more sync accounts.")
		private.isActive = false
		if not private.newCharacter then
			Delay.Cancel("SYNC_CONNECTION_MANAGEMENT")
		end
	end
end


function private.ConnectionThreadInner(account, targetCharacter)
	-- for the initial handshake, the lower account key is the server, other is the client - after this it doesn't matter
	-- add some randomness to the timeout so we don't get stuck in a race condition
	local timeout = GetTime() + RECEIVE_TIMEOUT + random(0, 1000) / 1000
	if account < Settings.GetCurrentSyncAccountKey() then
		-- wait for the connection request from the client
		while not private.connectionRequestReceived[account] do
			if GetTime() > timeout then
				-- timed out on the connection - don't try again for a bit
				Log.Warn("Timed out")
				return
			end
			Threading.Yield(true)
		end
		-- send an connection request ACK back to the client
		Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, targetCharacter)
	else
		-- send a connection request to the server
		Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST, targetCharacter)
		-- wait for the connection request ACK
		while not private.connectionRequestReceived[account] do
			if GetTime() > timeout then
				-- timed out on the connection - don't try again for a bit
				Log.Warn("Timed out")
				private.suppressThreadTime[account] = time() + RECEIVE_TIMEOUT
				return
			end
			Threading.Yield(true)
		end
	end

	-- we are now connected
	Log.Info("Connected to: %s %s", account, targetCharacter)
	private.connectedCharacter[account] = targetCharacter
	private.lastHeartbeat[account] = time()
	for _, callback in ipairs(private.connectionChangedCallbacks) do
		callback(account, targetCharacter, true)
	end

	-- now that we are connected, data can flow in both directions freely
	local lastHeartbeatSend = time()
	while true do
		-- check if they either logged off or the heartbeats have timed-out
		if not private.IsOnline(targetCharacter, true) or time() - private.lastHeartbeat[account] > HEARTBEAT_TIMEOUT then
			return
		end

		-- check if we should send a heartbeat
		if time() - lastHeartbeatSend > floor(HEARTBEAT_TIMEOUT / 2) then
			Comm.SendData(Constants.DATA_TYPES.HEARTBEAT, targetCharacter)
			lastHeartbeatSend = time()
		end

		Threading.Yield(true)
	end
end

function private.ConnectionThread(account, targetCharacter)
	private.ConnectionThreadInner(account, targetCharacter)
	private.ConnectionThreadDone(account)
end

function private.ConnectionThreadDone(account)
	Log.Info("Connection ended to %s", account)
	local player = private.connectedCharacter[account]
	private.connectedCharacter[account] = nil
	if player then
		for _, callback in ipairs(private.connectionChangedCallbacks) do
			callback(account, player, false)
		end
	end
	private.threadRunning[account] = nil
	private.connectionRequestReceived[account] = nil
end



-- ============================================================================
-- Helper Functions
-- ============================================================================

function private.SendNewAccountWhoAmI()
	if not private.newCharacter then
		Delay.Cancel("syncNewAccount")
	elseif not C_FriendList.GetFriendInfo(private.newCharacter) then
		Log.Info("Waiting for friends list to update")
	elseif not private.IsOnline(private.newCharacter) then
		Delay.Cancel("syncNewAccount")
		private.newCharacter = nil
		private.newAccount = nil
		private.newSyncAcked = nil
		Log.Err("New player went offline")
	else
		Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.newCharacter)
		Log.Info("Sent WHOAMI_ACCOUNT")
	end
end

function private.CheckNewAccountStatus()
	if not private.newCharacter or not private.newAccount or not private.newSyncAcked then
		return
	end
	Log.Info("New sync character: '%s' '%s'", private.newCharacter, private.newAccount)
	-- the other account ACK'd so setup a connection
	Settings.NewSyncCharacter(private.newAccount, private.newCharacter)

	-- call the callbacks for this new account
	for _, callback in ipairs(private.connectionChangedCallbacks) do
		callback(private.newAccount, private.newCharacter, nil)
	end

	private.newCharacter = nil
	private.newAccount = nil
	private.newSyncAcked = nil
end

function private.GetTargetCharacter(account)
	local tempTbl = TempTable.Acquire()
	for _, character in Settings.CharacterByAccountFactionrealmIterator(account) do
		tinsert(tempTbl, character)
	end

	-- find the player to connect to without adding to the friends list
	for _, player in ipairs(tempTbl) do
		if private.IsOnline(player, true) then
			TempTable.Release(tempTbl)
			return player
		end
	end
	-- if we failed, try again with adding to friends list
	for _, player in ipairs(tempTbl) do
		if private.IsOnline(player) then
			TempTable.Release(tempTbl)
			return player
		end
	end
	TempTable.Release(tempTbl)
end

function private.IsOnline(target, noAdd)
	local info = C_FriendList.GetFriendInfo(target)
	if not info and not noAdd and not private.invalidCharacters[strlower(target)] and C_FriendList.GetNumFriends() ~= 50 then
		-- add them as a friend
		C_FriendList.AddFriend(target)
		private.RequestFriendsInfo()
		tinsert(private.addedFriends, target)
		info = C_FriendList.GetFriendInfo(target)
	end
	return info and info.connected or false
end

function private.ChatMsgSystemEventHandler(_, msg)
	if #private.addedFriends == 0 then
		return
	end
	if msg == ERR_FRIEND_NOT_FOUND then
		if #private.addedFriends > 0 then
			private.invalidCharacters[strlower(tremove(private.addedFriends, 1))] = true
		end
	else
		for i, v in ipairs(private.addedFriends) do
			if format(ERR_FRIEND_ADDED_S, v) == msg then
				tremove(private.addedFriends, i)
				private.invalidCharacters[strlower(v)] = true
			end
		end
	end
end
