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

local _, TSM = ...
local GoldTracker = TSM.Accounting:NewPackage("GoldTracker")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local CSV = TSM.Include("Util.CSV")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Table = TSM.Include("Util.Table")
local TempTable = TSM.Include("Util.TempTable")
local Settings = TSM.Include("Service.Settings")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
	truncateGoldLog = {},
	characterGoldLog = {},
	guildGoldLog = {},
	currentCharacterKey = nil,
	playerLogCount = 0,
	searchValueTemp = {},
}
local CSV_KEYS = { "minute", "copper" }
local CHARACTER_KEY_SEP = " - "
local SECONDS_PER_MIN = 60
local SECONDS_PER_DAY = SECONDS_PER_MIN * 60 * 24
local MAX_COPPER_VALUE = 10 * 1000 * 1000 * COPPER_PER_GOLD - 1
local ERRONEOUS_ZERO_THRESHOLD = 5 * 1000 * COPPER_PER_GOLD



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

function GoldTracker.OnInitialize()
	if not TSM.IsWowClassic() then
		Event.Register("GUILDBANKFRAME_OPENED", private.GuildLogGold)
		Event.Register("GUILDBANK_UPDATE_MONEY", private.GuildLogGold)
	end
	Event.Register("PLAYER_MONEY", private.PlayerLogGold)

	-- get a list of known characters / guilds
	local validCharacterGuilds = TempTable.Acquire()
	for _, character in Settings.CharacterByFactionrealmIterator() do
		validCharacterGuilds[character..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()] = true
		local guild = TSM.db.factionrealm.internalData.characterGuilds[character]
		if guild then
			validCharacterGuilds[guild] = true
		end
	end

	-- load the gold log data
	for realm in TSM.db:GetConnectedRealmIterator("realm") do
		for factionrealm in TSM.db:FactionrealmByRealmIterator(realm) do
			for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
				local data = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLog")
				if data then
					local lastUpdate = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLogLastUpdate") or 0
					local characterKey = character..CHARACTER_KEY_SEP..factionrealm
					private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
				end
			end
			local guildData = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLog")
			if guildData then
				for guild, data in pairs(guildData) do
					local entries = {}
					local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
					if decodeContext then
						for minute, copper in CSV.DecodeIterator(decodeContext) do
							tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
						end
						CSV.DecodeEnd(decodeContext)
					end
					private.guildGoldLog[guild] = entries
					local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
					local lastUpdate = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLogLastUpdate")
					if not validCharacterGuilds[guild] and max(lastEntryTime, lastUpdate and lastUpdate[guild] or 0) < time() - 30 * SECONDS_PER_DAY then
						-- this guild may not be valid and the last entry is over 30 days old, so truncate the data
						private.truncateGoldLog[guild] = lastEntryTime
					end
				end
			end
		end
	end
	TempTable.Release(validCharacterGuilds)
	private.currentCharacterKey = UnitName("player")..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()
	assert(private.characterGoldLog[private.currentCharacterKey])
end

function GoldTracker.OnEnable()
	-- Log the current player gold (need to wait for OnEnable, otherwise GetMoney() returns 0 when first logging in)
	private.PlayerLogGold()
end

function GoldTracker.OnDisable()
	private.PlayerLogGold()
	TSM.db.sync.internalData.goldLog = CSV.Encode(CSV_KEYS, private.characterGoldLog[private.currentCharacterKey])
	TSM.db.sync.internalData.goldLogLastUpdate = private.characterGoldLog[private.currentCharacterKey].lastUpdate
	local guild = PlayerInfo.GetPlayerGuild(UnitName("player"))
	if guild and private.guildGoldLog[guild] then
		TSM.db.factionrealm.internalData.guildGoldLog[guild] = CSV.Encode(CSV_KEYS, private.guildGoldLog[guild])
		TSM.db.factionrealm.internalData.guildGoldLogLastUpdate[guild] = private.guildGoldLog[guild].lastUpdate
	end
end

function GoldTracker.CharacterGuildIterator()
	return private.CharacterGuildIteratorHelper
end

function GoldTracker.GetGoldAtTime(timestamp, ignoredCharactersGuilds)
	local value = 0
	for character, logEntries in pairs(private.characterGoldLog) do
		if #logEntries > 0 and not ignoredCharactersGuilds[character] and (private.truncateGoldLog[character] or math.huge) > timestamp then
			value = value + private.GetValueAtTime(logEntries, timestamp)
		end
	end
	for guild, logEntries in pairs(private.guildGoldLog) do
		if #logEntries > 0 and not ignoredCharactersGuilds[guild] and (private.truncateGoldLog[guild] or math.huge) > timestamp then
			value = value + private.GetValueAtTime(logEntries, timestamp)
		end
	end
	return value
end

function GoldTracker.GetGraphTimeRange(ignoredCharactersGuilds)
	local minTime = Math.Floor(time(), SECONDS_PER_MIN)
	for character, logEntries in pairs(private.characterGoldLog) do
		if #logEntries > 0 and not ignoredCharactersGuilds[character] then
			minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
		end
	end
	for guild, logEntries in pairs(private.guildGoldLog) do
		if #logEntries > 0 and not ignoredCharactersGuilds[guild] then
			minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
		end
	end
	return minTime, Math.Floor(time(), SECONDS_PER_MIN), SECONDS_PER_MIN
end



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

function private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
	assert(not private.characterGoldLog[characterKey])
	local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
	if not decodeContext then
		Log.Err("Failed to decode (%s, %d)", characterKey, #data)
		private.characterGoldLog[characterKey] = {}
		return
	end

	local entries = {}
	for minute, copper in CSV.DecodeIterator(decodeContext) do
		tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
	end
	CSV.DecodeEnd(decodeContext)

	-- clean up any erroneous 0 entries, entries which are too high, and duplicate entries
	local didChange = true
	while didChange do
		didChange = false
		for i = #entries - 1, 2, -1 do
			local prevValue = entries[i-1].copper
			local value = entries[i].copper
			local nextValue = entries[i+1].copper
			if prevValue > ERRONEOUS_ZERO_THRESHOLD and value == 0 and nextValue > ERRONEOUS_ZERO_THRESHOLD then
				-- this is likely an erroneous 0 value
				didChange = true
				tremove(entries, i)
			end
		end
		for i = #entries, 2, -1 do
			local prevValue = entries[i-1].copper
			local value = entries[i].copper
			if prevValue == value or value > MAX_COPPER_VALUE then
				-- this is either a duplicate or invalid value
				didChange = true
				tremove(entries, i)
			end
		end
	end

	private.characterGoldLog[characterKey] = entries
	local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
	if not validCharacterGuilds[characterKey] and max(lastEntryTime, lastUpdate) < time() - 30 * SECONDS_PER_DAY then
		-- this character may not be valid and the last entry is over 30 days old, so truncate the data
		private.truncateGoldLog[characterKey] = lastEntryTime
	end
end

function private.UpdateGoldLog(goldLog, copper)
	copper = Math.Round(copper, COPPER_PER_GOLD * (TSM.IsWowClassic() and 1 or 1000))
	local currentMinute = floor(time() / SECONDS_PER_MIN)
	local prevRecord = goldLog[#goldLog]

	-- store the last update time
	goldLog.lastUpdate = time()

	if prevRecord and copper == prevRecord.copper then
		-- amount of gold hasn't changed, so nothing to do
		return
	elseif prevRecord and prevRecord.minute == currentMinute then
		-- gold has changed and the previous record is for the current minute so just modify it
		prevRecord.copper = copper
	else
		-- amount of gold changed and we're in a new minute, so insert a new record
		while prevRecord and prevRecord.minute > currentMinute - 1 do
			-- their clock may have changed - just delete everything that's too recent
			tremove(goldLog)
			prevRecord = goldLog[#goldLog]
		end
		tinsert(goldLog, {
			minute = currentMinute,
			copper = copper
		})
	end
end

function private.GuildLogGold()
	local guildName = GetGuildInfo("player")
	local isGuildLeader = IsGuildLeader()
	if guildName and not isGuildLeader then
		-- check if our alt is the guild leader
		for i = 1, GetNumGuildMembers() do
			local name, _, rankIndex = GetGuildRosterInfo(i)
			if name and rankIndex == 0 and PlayerInfo.IsPlayer(gsub(name, "%-", " - "), true) then
				isGuildLeader = true
			end
		end
	end
	if guildName and isGuildLeader then
		if not private.guildGoldLog[guildName] then
			private.guildGoldLog[guildName] = {}
		end
		private.UpdateGoldLog(private.guildGoldLog[guildName], GetGuildBankMoney())
	end
end

function private.PlayerLogGold()
	-- GetMoney sometimes returns 0 for a while after login, so keep trying for 30 seconds before recording a 0
	local money = GetMoney()
	if money == 0 and private.playerLogCount < 30 then
		private.playerLogCount = private.playerLogCount + 1
		Delay.AfterTime(1, private.PlayerLogGold)
		return
	end
	private.playerLogCount = 0
	private.UpdateGoldLog(private.characterGoldLog[private.currentCharacterKey], money)
	TSM.db.sync.internalData.money = money
end

function private.GetValueAtTime(logEntries, timestamp)
	local minute = floor(timestamp / SECONDS_PER_MIN)
	if logEntries[1].minute > minute then
		-- timestamp is before we had any data
		return 0
	end
	private.searchValueTemp.minute = minute
	local index, insertIndex = Table.BinarySearch(logEntries, private.searchValueTemp, private.GetEntryMinute)
	-- if we didn't find an exact match, the index is the previous one (compared to the insert index)
	-- as that point's gold value is true up until the next point
	index = index or (insertIndex - 1)
	return logEntries[index].copper
end

function private.GetEntryMinute(entry)
	return entry.minute
end

function private.CharacterGuildIteratorHelper(_, lastKey)
	local result, isGuild = nil, nil
	if not lastKey or private.characterGoldLog[lastKey] then
		result = next(private.characterGoldLog, lastKey)
		isGuild = false
		if not result then
			lastKey = nil
		end
	end
	if not result then
		result = next(private.guildGoldLog, lastKey)
		isGuild = result and true or false
	end
	return result, isGuild
end
