local _G = _G
local CurseProfiler = _G.CurseProfiler
local Quests = CurseProfiler:NewModule("Quests")

local getShortID = CurseProfiler.getShortID
local GetNumQuestLogEntries = _G.GetNumQuestLogEntries
local GetQuestLogTitle = _G.GetQuestLogTitle
local GetQuestLink = _G.GetQuestLink
local GetQuestTimers = _G.GetQuestTimers
local GetQuestIndexForTimer = _G.GetQuestIndexForTimer
local GetTime = _G.GetTime
local GetRewardText = _G.GetRewardText
local GetTitleText = _G.GetTitleText
local GetProgressText = _G.GetProgressText
local UnitLevel = _G.UnitLevel
local UnitGUID = _G.UnitGUID
local GetItemInfo = _G.GetItemInfo
local UnitName = _G.UnitName
local pairs = _G.pairs
local next = _G.next
local error = _G.error
local select = _G.select
local math_floor = _G.math.floor

local questData = {}
local temporaryQuestData = {}

local collapsedHeaders = {}
local function GetQuestIDFromTitle(title)
	if type(title) ~= "string" then
		error(("Bad argument #1 to `GetQuestIDFromTitle'. Expected %q, got %q"):format("string", type(title)), 2)
	end
	for i = GetNumQuestLogEntries(), 1, -1 do
		local t, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i)
		if isHeader and isCollapsed then
			collapsedHeaders[t] = true
			ExpandQuestHeader(i)
		end
	end
	local foundID
	for i = GetNumQuestLogEntries(), 1, -1 do
		local t, _, _, _, isHeader = GetQuestLogTitle(i)
		if isHeader then
			if collapsedHeaders[t] then
				CollapseQuestHeader(i)
				collapsedHeaders[t] = nil
			end
		elseif not foundID and t == title then
			local link = GetQuestLink(i)
			if link then
				local id = link:match("^|c%x%x%x%x%x%x%x%x|Hquest:(%d+):")
				if id then
					foundID = id+0
				end
			end
		end
	end
	if foundID then
		return foundID
	end
	return nil
end

local recQuestID, recQuestTime = 0, 0

local playerName = _G.UnitName("player")
local playerRace, englishPlayerRace = _G.UnitRace("player")
local playerRace_lower = playerRace:lower()
local playerClass, englishPlayerClass = _G.UnitClass("player")
local playerClass_lower = playerClass:lower()
playerName = "%f[%a]" .. playerName .. "%f[%A]"
playerRace = "%f[%a]" .. playerRace .. "%f[%A]"
playerRace_lower = "%f[%a]" .. playerRace_lower .. "%f[%A]"
playerClass = "%f[%a]" .. playerClass .. "%f[%A]"
playerClass_lower = "%f[%a]" .. playerClass_lower .. "%f[%A]"
local function fixQuestText(text)
	return (text:gsub(playerName, "$N"):gsub(playerRace, "$R"):gsub(playerRace_lower, "$r"):gsub(playerClass, "$C"):gsub(playerClass_lower, "$c"))
end

_G.hooksecurefunc("GetQuestReward", function(...)
	recQuestID = GetQuestIDFromTitle(GetTitleText()) or 0
	if recQuestID ~= 0 then
		recQuestTime = GetTime()
	else
		recQuestTime = 0
	end
end)

local function GetCurrentNPCObjectId()
	local guid = UnitGUID("npc")
	if guid then
		if guid:match("^0x400") then
			-- item
			local name = UnitName("npc")
			if GetItemInfo(name) then
				local objectID = getShortID(select(2, GetItemInfo(name)))
				if objectID then
					return "item", objectID
				end
			end
		elseif guid:match("^0xF11") then
			-- gameobject
			local mobID = tonumber(guid:sub(-12, -7), 16)
			if mobID <= 1 then
				return nil
			end
			return 'gameobject', mobID
		elseif guid:match("^0xF13") then
			-- npc
			local mobID = tonumber(guid:sub(-12, -7), 16)
			if mobID <= 1 then
				return nil
			end
			return 'npc', mobID
		end
	end
	return nil
end

function Quests:QUEST_DETAIL()
	local questTitle = GetTitleText()
	local currentQuestData = temporaryQuestData[questTitle]
	if not currentQuestData then
		currentQuestData = {}
		temporaryQuestData[questTitle] = currentQuestData
	end
	
	if not currentQuestData.lowLevel or UnitLevel("player") < currentQuestData.lowLevel then
		currentQuestData.lowLevel = UnitLevel("player")
	end
	
	local objectType, objectID = GetCurrentNPCObjectId()
	currentQuestData.startObjectType = objectType
	currentQuestData.startObjectID = objectID
end

function Quests:QUEST_PROGRESS()
	local questTitle = GetTitleText()
	
	local questID = GetQuestIDFromTitle(questTitle)
	if not questID then
		return
	end
	
	local currentQuestData = questData[questID]
	if not currentQuestData then
		currentQuestData = {}
		questData[questID] = currentQuestData
	end
	currentQuestData.midText = fixQuestText(GetProgressText())
end

function Quests:QUEST_COMPLETE()
	local questTitle = GetTitleText()
	
	local questID = GetQuestIDFromTitle(questTitle)
	if not questID then
		return
	end
	
	local currentQuestData = questData[questID]
	if not currentQuestData then
		currentQuestData = {}
		questData[questID] = currentQuestData
	end
	
	local objectType, objectID = GetCurrentNPCObjectId()
	currentQuestData.finishText = fixQuestText(GetRewardText())
	currentQuestData.finishObjectType = objectType
	currentQuestData.finishObjectID = objectID
end

local MATCH_XP = _G.ERR_QUEST_REWARD_EXP_I:gsub("%%d", "(%%d+)")
function Quests:CHAT_MSG_SYSTEM(event, msg)
    local xpReward = msg:match(MATCH_XP)
    if xpReward and GetTime() - recQuestTime <= 1 then
		local currentQuestData = questData[recQuestID]
		if not currentQuestData then
			currentQuestData = {}
			questData[recQuestID] = currentQuestData
		end
		currentQuestData.xpReward = xpReward+0
    end
end

local reverseMatch, MATCH_FACTION_DECREASE, MATCH_FACTION_INCREASE
if _G.FACTION_STANDING_DECREASED:find("%%2%$s") then
	reverseMatch = true
	MATCH_FACTION_DECREASE = _G.FACTION_STANDING_DECREASED:gsub("%%2%$s", "(%.+)"):gsub("%%1%$d", "(%%d+)")
	MATCH_FACTION_INCREASE = _G.FACTION_STANDING_INCREASED:gsub("%%2%$s", "(%.+)"):gsub("%%1%$d", "(%%d+)")
else
	MATCH_FACTION_DECREASE = _G.FACTION_STANDING_DECREASED:gsub("%%s", "(%.+)"):gsub("%%d", "(%%d+)")
	MATCH_FACTION_INCREASE = _G.FACTION_STANDING_INCREASED:gsub("%%s", "(%.+)"):gsub("%%d", "(%%d+)")
end
function Quests:CHAT_MSG_COMBAT_FACTION_CHANGE(event, msg)
    local factionChange = 0
	local faction, amount = msg:match(MATCH_FACTION_DECREASE)
    if faction and amount then
		if reverseMatch then
			faction, amount = amount, faction
		end
		if GetTime() - recQuestTime <= 1 then
            factionChange = -amount
        end
    else
		faction, amount = msg:match(MATCH_FACTION_INCREASE)
        if faction and amount then
			if reverseMatch then
				faction, amount = amount, faction
			end
            if GetTime() - recQuestTime <= 1 then
	            factionChange = amount+0
				if englishPlayerRace == "Human" then
					--Deal with Diplomacy racial so it doesn't have to be done server side.
					factionChange = math_floor(factionChange/1.1 + 0.5)
                end
            end
        end
    end
    if factionChange == 0 then
		return
	end
	local currentQuestData = questData[recQuestID]
	if not currentQuestData then
		currentQuestData = {}
		questData[recQuestID] = currentQuestData
	end
	local factionRewards = currentQuestData.factionRewards
	if not factionRewards then
		factionRewards = {}
		currentQuestData.factionRewards = factionRewards
	end
	factionRewards[faction] = factionChange
end

local function FigureTimers(...)
	local n = select('#', ...)
	if n == 0 then
		return
	end
	
	for i = 1, n do
		local v = select(i, ...)
		local questIndex = GetQuestIndexForTimer(i)
		local questTitle = GetQuestLogTitle(questIndex)
		local questID = GetQuestIDFromTitle(questTitle)
		if questID then
			local currentQuestData = questData[questID]
			if not currentQuestData then
				currentQuestData = {}
				questData[questID] = currentQuestData
			end
			local timer = currentQuestData.timer
			if not timer or timer < v then
				currentQuestData.timer = v
			end
		end
	end
end

function Quests:QUEST_LOG_UPDATE()
	for title in pairs(temporaryQuestData) do
		local id = GetQuestIDFromTitle(title)
		if id then
			questData[id] = temporaryQuestData[title]
		end
		temporaryQuestData[title] = nil
	end
	
	FigureTimers(GetQuestTimers())
end

local deserialize__text, deserialize__position

local OnDeserialize
function Quests:OnSerialize()
	local INT = CurseProfiler.makeSerializedInteger
	local TUPLE = CurseProfiler.makeSerializedTuple
	local ENUM = CurseProfiler.makeSerializedEnum
	local FACTION = CurseProfiler.getFactionID
	
	local savedQuestData = not deserialize__text and {} or OnDeserialize(deserialize__text, deserialize__position, true)
	
	for id, oldQuestData in pairs(savedQuestData) do
		local currentQuestData = questData[id]
		if currentQuestData then
			local lowLevel = currentQuestData.lowLevel
			if lowLevel and lowLevel ~= 0 and (oldQuestData.lowLevel == 0 or oldQuestData.lowLevel > lowLevel) then
				oldQuestData.lowLevel = lowLevel
			end
			oldQuestData.classes[englishPlayerClass] = true
			oldQuestData.races[englishPlayerRace] = true
			local startObjectType = currentQuestData.startObjectType
			local startObjectID = currentQuestData.startObjectID
			if startObjectType and startObjectType ~= '' and startObjectID and startObjectID ~= 0 then
				oldQuestData.startObjectType = startObjectType
				oldQuestData.startObjectID = startObjectID
			end
			local finishObjectType = currentQuestData.finishObjectType
			local finishObjectID = currentQuestData.finishObjectID
			if finishObjectType and finishObjectType ~= '' and finishObjectID and finishObjectID ~= 0 then
				oldQuestData.finishObjectType = finishObjectType
				oldQuestData.finishObjectID = finishObjectID
			end
			local timer = currentQuestData.timer
			if timer and timer ~= 0 and timer > oldQuestData.timer then
				oldQuestData.timer = timer
			end
			local xpReward = currentQuestData.xpReward
			if xpReward and xpReward ~= 0 then
				oldQuestData.xpReward = xpReward
			end
			local factionRewards = currentQuestData.factionRewards
			if factionRewards and next(factionRewards) then
				oldQuestData.factionRewards = factionRewards
			end
			local midText = currentQuestData.midText
			if midText and midText ~= '' then
				oldQuestData.midText = midText
			end
			local finishText = currentQuestData.finishText
			if finishText and finishText ~= '' then
				oldQuestData.finishText = finishText
			end
		end
	end
	for id, currentQuestData in pairs(questData) do
		local oldQuestData = savedQuestData[id]
		if not oldQuestData then
			savedQuestData[id] = currentQuestData
			currentQuestData.classes = { [englishPlayerClass] = true }
			currentQuestData.races = { [englishPlayerRace] = true }
		end
	end
	
	local fullData = {}
	for id, currentQuestData in pairs(savedQuestData) do
		local data = {}
		data[#data+1] = INT("UInt16", id)
		data[#data+1] = INT("Byte", currentQuestData.lowLevel or 0)
		local classes = {}
		if currentQuestData.classes then
			for class in pairs(currentQuestData.classes) do
				classes[ENUM("class", class)] = true
			end
		end
		data[#data+1] = classes
		local races = {}
		if currentQuestData.races then
			for race in pairs(currentQuestData.races) do
				races[ENUM("race", race)] = true
			end
		end
		data[#data+1] = races
		data[#data+1] = ENUM('objectType', currentQuestData.startObjectType or '')
		data[#data+1] = INT("Int32", currentQuestData.startObjectID or 0)
		data[#data+1] = ENUM('objectType', currentQuestData.finishObjectType or '')
		data[#data+1] = INT("Int32", currentQuestData.finishObjectID or 0)
		data[#data+1] = INT("Int16", currentQuestData.timer or 0)
		data[#data+1] = INT("Int32", currentQuestData.xpReward or 0)
		local factionRewards = {}
		if currentQuestData.factionRewards then
			for faction, amount in pairs(currentQuestData.factionRewards) do
				factionRewards[TUPLE {
					FACTION(faction),
					INT("Int32", amount)
				}] = true
			end
		end
		data[#data+1] = factionRewards
		data[#data+1] = currentQuestData.midText or ''
		data[#data+1] = currentQuestData.finishText or ''
		fullData[#fullData+1] = TUPLE(data)
	end
	return fullData
end

function OnDeserialize(text, position, run)
	local deserialize = CurseProfiler.deserialize
	
	local oldQuestData
	if run then
		oldQuestData = {}
	end
	local numQuests
	position, numQuests = deserialize("list", text, position)
	for _ = 1, numQuests do
		local questID, lowLevel, classes, races, startObjectType, startObjectID, finishObjectType, finishObjectID, timer, xpReward, factionRewards, midText, finishText
		if run then
			classes = {}
			races = {}
			factionRewards = {}
		end
		position, questID = deserialize("UInt16", text, position, not run)
		position, lowLevel = deserialize("Byte", text, position, not run)
		local numClasses
		position, numClasses = deserialize("list", text, position)
		for _ = 1, numClasses do
			local class
			position, class = deserialize("enum-class", text, position, not run)
			if run then
				classes[class] = true
			end
		end
		local numRaces
		position, numRaces = deserialize("list", text, position)
		for _ = 1, numRaces do
			local race
			position, race = deserialize("enum-race", text, position, not run)
			if run then
				races[race] = true
			end
		end
		position, startObjectType = deserialize("enum-objectType", text, position, not run)
		position, startObjectID = deserialize("Int32", text, position, not run)
		position, finishObjectType = deserialize("enum-objectType", text, position, not run)
		position, finishObjectID = deserialize("Int32", text, position, not run)
		position, timer = deserialize("Int16", text, position, not run)
		position, xpReward = deserialize("Int32", text, position, not run)
		local numFactionRewards
		position, numFactionRewards = deserialize("list", text, position)
		for _ = 1, numFactionRewards do
			local faction, amount
			position, faction = deserialize("faction", text, position, not run)
			position, amount = deserialize("Int32", text, position, not run)
			if run then
				factionRewards[faction] = amount
			end
		end
		position, midText = deserialize("string", text, position, not run)
		position, finishText = deserialize("string", text, position, not run)
		if run then
			oldQuestData[questID] = {
				lowLevel = lowLevel,
				classes = classes,
				races = races,
				startObjectType = startObjectType,
				startObjectID = startObjectID,
				finishObjectType = finishObjectType,
				finishObjectID = finishObjectID,
				timer = timer,
				xpReward = xpReward,
				factionRewards = factionRewards,
				midText = midText,
				finishText = finishText,
			}
		end
	end
	if run then
		return oldQuestData
	else
		return position
	end
end
function Quests:OnDeserialize(text, position)
	deserialize__text, deserialize__position = text, position
	return OnDeserialize(text, position, false)
end
