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

local _, TSM = ...
local Mirror = TSM.Init("Service.SyncClasses.Mirror")
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Settings = TSM.Include("Service.Settings")
local Constants = TSM.Include("Service.SyncClasses.Constants")
local Comm = TSM.Include("Service.SyncClasses.Comm")
local Connection = TSM.Include("Service.SyncClasses.Connection")
local private = {
	numConnected = 0,
	accountStatus = {},
	callbacks = {},
}
local BROADCAST_INTERVAL = 3



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

Mirror:OnModuleLoad(function()
	Connection.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, private.CharacterHashesBroadcastHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, private.CharacterSettingHashesRequestHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, private.CharacterSettingHashesResponseHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, private.CharacterSettingDataRequestHandler)
	Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, private.CharacterSettingDataResponseHandler)
end)



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

function Mirror.GetStatus(account)
	local status = private.accountStatus[account]
	if not status then
		return false, false
	elseif status == "UPDATING" then
		return true, false
	elseif status == "SYNCED" then
		return true, true
	else
		error("Invalid status: "..tostring(status))
	end
end

function Mirror.RegisterCallback(callback)
	tinsert(private.callbacks, callback)
end



-- ============================================================================
-- Connection Callback Handlers
-- ============================================================================

function private.ConnectionChangedHandler(account, player, connected)
	if connected == nil then
		-- new account, but not yet connected
		return
	end
	if connected then
		Log.Info("Connected to %s (%s)", account, player)
	else
		Log.Info("Disconnected from %s (%s)", account, player)
	end
	private.numConnected = private.numConnected + (connected and 1 or -1)
	assert(private.numConnected >= 0)
	if connected then
		private.accountStatus[account] = "UPDATING"
		Delay.AfterTime("mirrorCharacterHashes", 0, private.SendCharacterHashes, BROADCAST_INTERVAL)
	else
		private.accountStatus[account] = nil
		if private.numConnected == 0 then
			Delay.Cancel("mirrorCharacterHashes")
		end
	end
end



-- ============================================================================
-- Delay-Based Last Update Send Function
-- ============================================================================

function private.SendCharacterHashes()
	assert(private.numConnected > 0)

	-- calculate the hashes of the sync settings for all characters on this account
	local hashes = TempTable.Acquire()
	for _, character in Settings.CharacterByAccountFactionrealmIterator() do
		hashes[character] = private.CalculateCharacterHash(character)
	end

	-- send the hashes to all connected accounts
	for _, character in Connection.ConnectedAccountIterator() do
		Comm.SendData(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, character, hashes)
	end
	TempTable.Release(hashes)
end



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

function private.CharacterHashesBroadcastHandler(dataType, sourceAccount, sourcePlayer, data)
	assert(dataType == Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST)
	if not Connection.IsCharacterConnected(sourcePlayer) then
		-- we're not connected to this player
		Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
		return
	end
	local didChange = false
	for _, character in Settings.CharacterByAccountFactionrealmIterator(sourceAccount) do
		if not data[character] then
			-- this character doesn't exist anymore, so remove it
			Log.Info("Removed character: '%s'", character)
			Settings.RemoveSyncCharacter(character)
			didChange = true
		end
	end
	for character, hash in pairs(data) do
		if not Settings.GetCharacterSyncAccountKey(character) then
			-- this is a new character, so add it to our DB
			Log.Info("New character: '%s' '%s'", character, sourceAccount)
			Settings.NewSyncCharacter(sourceAccount, character)
			didChange = true
		end
		if hash ~= private.CalculateCharacterHash(character) then
			-- this character's data has changed so request a hash of each of the keys
			Log.Info("Character data has changed: '%s'", character)
			Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, sourcePlayer, character)
			didChange = true
		end
	end
	if didChange then
		private.accountStatus[sourceAccount] = "UPDATING"
	else
		private.accountStatus[sourceAccount] = "SYNCED"
	end
end

function private.CharacterSettingHashesRequestHandler(dataType, sourceAccount, sourcePlayer, data)
	assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST)
	if not Connection.IsCharacterConnected(sourcePlayer) then
		-- we're not connected to this player
		Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
		return
	elseif Settings.GetCharacterSyncAccountKey(data) ~= Settings.GetCurrentSyncAccountKey() then
		-- we don't own this character
		Log.Err("Request for character we don't own ('%s', '%s')", tostring(data), tostring(Settings.GetCharacterSyncAccountKey(data)))
		return
	end
	Log.Info("CHARACTER_SETTING_HASHES_REQUEST (%s)", data)
	local responseData = TempTable.Acquire()
	responseData._character = data
	for _, namespace, settingKey in Settings.SyncSettingIterator() do
		responseData[namespace.."."..settingKey] = private.CalculateCharacterSettingHash(data, namespace, settingKey)
	end
	Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, sourcePlayer, responseData)
	TempTable.Release(responseData)
end

function private.CharacterSettingHashesResponseHandler(dataType, sourceAccount, sourcePlayer, data)
	assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE)
	if not Connection.IsCharacterConnected(sourcePlayer) then
		-- we're not connected to this player
		Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
		return
	end
	local character = data._character
	data._character = nil
	Log.Info("CHARACTER_SETTING_HASHES_RESPONSE (%s)", character)
	for key, hash in pairs(data) do
		local namespace, settingKey = strsplit(".", key)
		if private.CalculateCharacterSettingHash(character, namespace, settingKey) ~= hash then
			-- the settings data for key changed, so request the latest data for it
			Log.Info("Setting data has changed: '%s', '%s'", character, key)
			Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, sourcePlayer, character.."."..key)
		end
	end
end

function private.CharacterSettingDataRequestHandler(dataType, sourceAccount, sourcePlayer, data)
	assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST)
	local character, namespace, settingKey = strsplit(".", data)
	if not Connection.IsCharacterConnected(sourcePlayer) then
		-- we're not connected to this player
		Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
		return
	elseif Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
		-- we don't own this character
		Log.Err("Request for character we don't own ('%s', '%s')", tostring(character), tostring(Settings.GetCharacterSyncAccountKey(character)))
		return
	end
	Log.Info("CHARACTER_SETTING_DATA_REQUEST (%s,%s,%s)", character, namespace, settingKey)
	local responseData = TempTable.Acquire()
	responseData.character = character
	responseData.namespace = namespace
	responseData.settingKey = settingKey
	responseData.data = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
	Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, sourcePlayer, responseData)
	TempTable.Release(responseData)
end

function private.CharacterSettingDataResponseHandler(dataType, sourceAccount, sourcePlayer, data)
	assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE)
	if not Connection.IsCharacterConnected(sourcePlayer) then
		-- we're not connected to this player
		Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
		return
	end
	local dataValueType = type(data.data)
	Log.Info("CHARACTER_SETTING_DATA_RESPONSE (%s,%s,%s,%s,%s)", data.character, data.namespace, data.settingKey, dataValueType, (dataValueType == "string" or dataValueType == "table") and #dataValueType or "-")
	if dataValueType == "table" then
		local tbl = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey)
		wipe(tbl)
		for i, v in pairs(data.data) do
			tbl[i] = v
		end
	else
		Settings.Set("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey, data.data)
	end
	for _, callback in ipairs(private.callbacks) do
		callback()
	end
end



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

function private.CalculateCharacterHash(character)
	local hash = nil
	local settingKeys = TempTable.Acquire()
	for _, namespace, settingKey in Settings.SyncSettingIterator() do
		tinsert(settingKeys, strjoin(".", namespace, settingKey))
	end
	sort(settingKeys)
	for _, key in ipairs(settingKeys) do
		hash = Math.CalculateHash(key, hash)
		local namespace, settingKey = strsplit(".", key)
		local settingValue = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
		hash = Math.CalculateHash(settingValue, hash)
	end
	assert(hash)
	TempTable.Release(settingKeys)
	return hash
end

function private.CalculateCharacterSettingHash(character, namespace, settingKey)
	return Math.CalculateHash(Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey))
end
