

--------------------
-- What's Next? ----
----- by Strucker --
--------------------
local _
local WhatsNext = LibStub('AceAddon-3.0'):NewAddon('WhatsNext', 'AceEvent-3.0', 'AceConsole-3.0');
_G.WhatsNext = WhatsNext;
local SML = LibStub("LibSharedMedia-3.0");
local inCombat = false
local optionsFrames = {}
local suggestionPreUpdateListeners, suggestionUpdateListeners, logicSetUpdateListeners = {},{},{}
WhatsNext.suggestionFrames = {}
WhatsNext.suggestions = {}

WhatsNext.CLASSES = {
	"WARRIOR",
	"MONK",
	"DEATHKNIGHT",
	"SHAMAN",
	"PRIEST",
	"ROGUE",
	"HUNTER",
	"PALADIN",
	"WARLOCK",
	"MAGE",
	"DRUID"
}
WhatsNext.CLASS_COLOR_CODES = {
	["WARRIOR"] = "|cFFC79C6E",
	["MONK"] = "|cFF558A84",
	["DEATHKNIGHT"] = "|cFFC41F3B",
	["SHAMAN"] = "|cFF0070DE",
	["PRIEST"] = "|cFFFFFFFF",
	["ROGUE"] = "|cFFFFF569",
	["HUNTER"] = "|cFFABD473",
	["PALADIN"] = "|cFFF58CBA",
	["WARLOCK"] = "|cFF9482C9",
	["MAGE"] = "|cFF69CCF0",
	["DRUID"] = "|cFFFF7D0A"
}
function WhatsNext_AddOptionsScreen(frame)
	tinsert(optionsFrames,frame)
end

function WhatsNext_ShowOptionsScreen(frame)
	for i, optionFrame in ipairs(optionsFrames) do
		local hideScript = optionFrame:GetScript("OnHide")
		optionFrame:SetScript("OnHide", function() end)
		optionFrame:Hide()
		optionFrame:SetScript("OnHide", hideScript)
	end
	
	WhatsNext.showFrame = frame
end

local function GetSuggestionFrame(i)
	if not WhatsNext.suggestionFrames[i] then
		local frame
		if i > 1 then
			frame = CreateFrame("Frame","SuggestionFrame"..i,GetSuggestionFrame(i-1),"WhatsNext_SuggestionFrame")
		else
			frame = CreateFrame("Frame","SuggestionFrame"..i,nil,"WhatsNext_SuggestionFrame")
		end
		frame.index = i
		WhatsNext.suggestionFrames[i] = frame
		WhatsNext:ApplyFrameDisplaySettings(frame)
	end
	return WhatsNext.suggestionFrames[i]
end

function WhatsNext_ClearFocus()
	if GetCurrentKeyBoardFocus() and GetCurrentKeyBoardFocus().ClearFocus then
		GetCurrentKeyBoardFocus():ClearFocus()
	end
end
WhatsNext.defaultCastSequences = {}
for _, class in pairs(WhatsNext.CLASSES) do
	WhatsNext.defaultCastSequences[class] = {}
	for i = 1,(class=="DRUID" and 4 or 3) do
		WhatsNext.defaultCastSequences[class][i] = {}
	end
end

function GetItemID(itemName)
	return select(5, string.find(select(2, GetItemInfo(itemName) ) or "", "|?c?f?f?(%x*)|?H?([^:]*):?(%d+):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%-?%d*):?(%-?%d*):?(%d*)|?h?%[?([^%[%]]*)%]?|?h?|?r?"))
end

function WhatsNext:SetupLogicSets()
	
	self.db.profile.castSequences = self.db.profile.castSequences or {}
	local castSequences = self.db.profile.castSequences
	
	for _, class in pairs(self.CLASSES) do
		castSequences[class] = castSequences[class] or {}
		for i = 1,(class=="DRUID" and 4 or 3) do
			castSequences[class][i] = castSequences[class][i] or {}
			
			-- This is to save any user modifications made before the logic set manager was implemented.
			if castSequences[class][i][1] and castSequences[class][i][1].spell then
				castSequences[class][i] = {[1] = nil, [2] = castSequences[class][i]}
				castSequences[class][i][2].name = "Unnamed"
			end
			
			castSequences[class][i][1] = self.defaultCastSequences[class][i]
			castSequences[class][i][1].name = "Default"
		end
	end
end

function WhatsNext:OnInitialize()
	self:RegisterOptions()
	self:SetupLogicSets()
end

local function GetBattleType()
	local inInstance, instanceType = IsInInstance()
	if not inInstance then
		return "Open World"
	elseif instanceType == "pvp" then
		return "Battleground"
	elseif instanceType == "arena" then
		return "Arena"
	elseif instanceType == "party" then
		return "Dungeon"
	elseif instanceType == "raid" then
		return "Raid"
	end
end

function WhatsNext:UpdateActiveLogicSet()
	if not self.db.profile.castSequences then
		self:SetupLogicSets()
	end
	
	local class, spec = select(2,UnitClass("player")), GetSpecialization() or 0;
	local sets = self.db.profile.castSequences[class][spec]
	if sets then
		local battleType = GetBattleType()
		self.castSequence = sets[1] 
		for i = 2, #sets do
			if sets[i].use == battleType then
				self.castSequence = sets[i]
				break
			elseif sets[i].use == "General" then
				self.castSequence = sets[i]
			end
		end
	else
		self.castSequence = {}
	end
	self:SetupTestChecks()
	WhatsNext_ExamineLogicSet(self.castSequence)
	self:ApplyExtensionSettings()
	for _,listener in ipairs(logicSetUpdateListeners) do
		listener(self.castSequence)
	end
end

function WhatsNext:AddLogicSetUpdateListener(listener)
	if type(listener) == "function" then
		tinsert(logicSetUpdateListeners, listener)
	end
end

function WhatsNext:AddSuggestionUpdateListener(listener)
	if type(listener) == "function" then
		tinsert(suggestionUpdateListeners, listener)
	end
end

function WhatsNext:AddSuggestionPreUpdateListener(listener)
	if type(listener) == "function" then
		tinsert(suggestionPreUpdateListeners, listener)
	end
end

function WhatsNext:ApplyMainSettings()
	self:UpdateActiveLogicSet()
	self:ApplyDisplaySettings()
end

function WhatsNext:ApplyDisplaySettings()
	local frame = GetSuggestionFrame(1)
	if not self.db.profile.enabled or (not inCombat and self.db.profile.hideOutOfCombat) then
		frame:Hide()
	else
		frame:Show()
	end
	
	for i, sFrame in pairs(WhatsNext.suggestionFrames) do
		WhatsNext:ApplyFrameDisplaySettings(sFrame)
	end
	local scale = frame:GetEffectiveScale()
	local pos = self.db.profile.pos
	frame:SetPoint(pos.p1,nil,pos.p2,pos.x/scale,pos.y/scale)
	
	scale = WhatsNext_CastLogicDisplay:GetEffectiveScale()
	pos = self.db.profile.configPos
	WhatsNext_CastLogicDisplay:ClearAllPoints()
	WhatsNext_CastLogicDisplay:SetPoint(pos.p1,nil,pos.p2,pos.x/scale,pos.y/scale)
	WhatsNext_CastBlockEditDisplay:ClearAllPoints()
	WhatsNext_CastBlockEditDisplay:SetPoint(pos.p1,nil,pos.p2,pos.x/scale,pos.y/scale)	
	WhatsNext_LogicSetManagementDisplay:ClearAllPoints()
	WhatsNext_LogicSetManagementDisplay:SetPoint(pos.p1,nil,pos.p2,pos.x/scale,pos.y/scale)	
end

function WhatsNext:ApplyFrameDisplaySettings(frame)

	frame:EnableMouse(not self.db.profile.locked)
	local color = self.db.profile.backgroundColor
	frame.bg:SetTexture(color.r,color.g,color.b,color.a)
	color = self.db.profile.textColor
	frame.spellName:SetFont(SML:Fetch('font', self.db.profile.font), 16, self.db.profile.outline)
	frame.spellName:SetTextColor(color.r, color.g, color.b, color.a)
	frame.cd:SetFont(SML:Fetch('font', self.db.profile.font), 10, self.db.profile.outline)
	frame.cd:SetTextColor(color.r, color.g, color.b, color.a)
	frame.blockIndex:SetFont(SML:Fetch('font', self.db.profile.font), 9, self.db.profile.outline)
	frame.blockIndex:SetTextColor(color.r, color.g, color.b, color.a)
	if self.db.profile.showBlockIndex then
		frame.blockIndex:Show()
	else
		frame.blockIndex:Hide()
	end
	
	frame:ClearAllPoints()
	if frame.index == 1 then
		frame:SetScale(self.db.profile.scale)
		local scale = frame:GetEffectiveScale()
		local pos = self.db.profile.pos
		frame:SetPoint(pos.p1,nil,pos.p2,pos.x/scale,pos.y/scale)
	else
		frame:SetScale(self.db.profile.subsequentScale)
		if self.db.profile.stackDirection == "bottom" then
			frame:SetPoint("TOPLEFT",GetSuggestionFrame(frame.index-1),"BOTTOMLEFT",2.2,-4)
		elseif self.db.profile.stackDirection == "top" then
			frame:SetPoint("BOTTOMLEFT",GetSuggestionFrame(frame.index-1),"TOPLEFT",2.2,4)
		elseif self.db.profile.stackDirection == "left" then
			frame:SetPoint("RIGHT",GetSuggestionFrame(frame.index-1),"LEFT",-4,0)
		else
			frame:SetPoint("LEFT",GetSuggestionFrame(frame.index-1),"RIGHT",4,0)
		end
	end
	
	frame:UpdateLayout()
end

local i
local function UpdateSuggestionFrames()
	if not WhatsNext.lastUpdate  or WhatsNext.lastUpdate + WhatsNext.db.profile.timeBetweenUpdates <= GetTime() then
		WhatsNext:UpdateSuggestions()
		i = 1
		for i, suggestion in ipairs(WhatsNext.suggestions) do
			if suggestion.timeTillCast <= 1.5 and i <= WhatsNext.db.profile.maxSuggestions then
				GetSuggestionFrame(i):SetSuggestion(suggestion)
			end
		end
		for i = #WhatsNext.suggestions+1, WhatsNext.db.profile.maxSuggestions do
			GetSuggestionFrame(i):Hide()
		end
		WhatsNext.lastUpdate = GetTime()
	end
end
		
local function WhatsNext_ExamineLogicBlock(logicBlock)
	local err = ""
	if not logicBlock.type then
		logicBlock.type = "spell"
	end
	
	local type = logicBlock.type
	local entry = logicBlock[type]
	if not entry or strtrim(entry) == "" then
		return false, "No " .. type .. " entry found."
	end
	
	for i = #logicBlock.checks,1,-1 do
		if not WhatsNext:ContainsConditionalStatement(logicBlock.checks[i].type) then
			tremove(logicBlock.checks,i)
			err = "Unknown conditional statement removed. " ..  logicBlock.checks[i].type .. ". " .. err
		end
	end
	
	
	return true, err
end
		
function WhatsNext_ExamineLogicSet(logicSet)
	local rtn,err
	for i = #logicSet,1,-1 do
		rtn,err = WhatsNext_ExamineLogicBlock(logicSet[i])
		if not rtn then
			tremove(logicSet,i)
			print("Logic Block " .. i .. " has been removed.",err)
		elseif err and err ~= "" then
			print("Logic Block " .. i .. " has been salvaged.",err)
		end
	end
end
			
local function GetMovementIcon(movement)
	return "Interface\\Icons\\INV_Misc_QuestionMark"
end
			
local defaultSuggestion = {
	type = "text", 
	text = WhatsNextLocal:Localize("TITLE"),
	icon = "Interface\\Icons\\INV_Misc_QuestionMark",  
	timeTillCast =1.5,
}

local function SuggestionComparer(s1,s2)
	if not s1 and not s2 then
		return true
	elseif not s1 then
		return false
	elseif not s2 then 
		return true
	elseif abs(s1.timeTillCast - s2.timeTillCast) < .5 then
		return s1.index < s2.index
	else
		return s1.timeTillCast < s2.timeTillCast
	end
end

local suggestion, suggestedTexts, suggestionsToRemove = {}, {}, {}
local start, duration, enabled, id, lastSuggestionText

function WhatsNext:UpdateSuggestions()
	self:ResetTestChecks()
	if self.castSequence then
		for _, listener in ipairs(suggestionPreUpdateListeners) do
			listener()
		end
	
		lastSuggestionText = WhatsNext.suggestions[1] and WhatsNext.suggestions[1].text or nil
		wipe(WhatsNext.suggestions)
		for i,block in ipairs(self.castSequence) do
			wipe(suggestion)
			suggestion.type = block.type or "spell"
			suggestion.index = i
			if suggestion.type == "spell" then
				block.spell = self:GetSpellID(block.spell) or block.spell
				start, duration = GetSpellCooldown(block.spell)	
				suggestion.text,_,suggestion.icon = GetSpellInfo(block.spell)
				if suggestion.text and IsUsableSpell(block.spell) and ( not GetSpellInfo(suggestion.text) or IsUsableSpell(suggestion.text)) and IsSpellInRange(suggestion.text,"target") ~= 0 and WhatsNext:TestChecks(block.checks) then
					suggestion.timeTillCast = not start and 99 or start == 0 and 0 or start + duration - GetTime()
					if suggestion.timeTillCast <= 1.5 then
						tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
					end
				end
			elseif suggestion.type == "item" then
				id = GetItemID(block.item)
				if id then
					start, duration, enabled = GetItemCooldown(id)
					suggestion.text, _, _, _, _, _, _, _, _, suggestion.icon = GetItemInfo(block.item) 
					if suggestion.text and enabled ~= 0 and (not IsEquippedItem(suggestion.text) and IsEquippableItem(suggestion.text) or GetItemSpell(suggestion.text)) and IsUsableItem(suggestion.text) and IsItemInRange(suggestion.text,"target") ~= 0 and WhatsNext:TestChecks(block.checks) then
						suggestion.timeTillCast = not start and 99 or start == 0 and 0 or start + duration - GetTime()
						if suggestion.timeTillCast <= 1.5 then
							tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
						end
					end
				end
			elseif suggestion.type == "macro" then
				suggestion.text, suggestion.icon = GetMacroInfo(block.macro);
				if suggestion.text and suggestion.icon and WhatsNext:TestChecks(block.checks) then
					suggestion.timeTillCast = 0
					tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
				end
			elseif suggestion.type == "movement" then
				suggestion.text = block.movement
				suggestion.icon = GetMovementIcon(block.movement)
				suggestion.timeTillCast = 0
				if WhatsNext:TestChecks(block.checks) then
					tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
				end
			elseif suggestion.type == "note" then
				suggestion.text = block.note
				suggestion.icon = "Interface\\Icons\\INV_MISC_NOTE_05"
				suggestion.timeTillCast = 0
				if WhatsNext:TestChecks(block.checks) then
					tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
				end
			end
		end	
	
		for i = 1, #WhatsNext.suggestions do
			if not WhatsNext.suggestions[i] then
				tremove(WhatsNext.suggestions,i)
			end
		end
		
		sort(WhatsNext.suggestions, SuggestionComparer)
		
		wipe(suggestedTexts)
		wipe(suggestionsToRemove)
		
		for i, suggestion in ipairs(WhatsNext.suggestions) do
			if tContains(suggestedTexts,suggestion.text) then
				tinsert(suggestionsToRemove,1,i)
			else
				tinsert(suggestedTexts,suggestion.text)
			end
		end
		
		for i = 1, #suggestionsToRemove do
			tremove(WhatsNext.suggestions,suggestionsToRemove[i])
		end
		
		if #WhatsNext.suggestions == 0 then
			tinsert(WhatsNext.suggestions,1,defaultSuggestion)
		end	

		for _, listener in ipairs(suggestionUpdateListeners) do
			listener()
		end
		
	else
		wipe(WhatsNext.suggestions)
	end
end

-------------------
-- Set up events
-------------------

local events = {}
function events:PLAYER_REGEN_ENABLED() 
	inCombat = false
	if WhatsNext.db.profile.hideOutOfCombat then
		WhatsNext.suggestionFrames[1]:Hide()
	end
end

function events:PLAYER_REGEN_DISABLED() 
	inCombat = true
	if WhatsNext.db.profile.enabled then
		WhatsNext.suggestionFrames[1]:Show()
	end
end

function events:PLAYER_LOGIN()
	WhatsNext:ApplyDisplaySettings()
end

function events:PLAYER_SPECIALIZATION_CHANGED()
	WhatsNext:UpdateActiveLogicSet()
end

function events:PLAYER_ENTERING_WORLD()
	WhatsNext:UpdateActiveLogicSet()
end

local function EventFunc(self, event, ...)
	events[event](event, ...)
end
local eventHandler = CreateFrame("Frame", nil, WorldFrame)
eventHandler:SetScript("OnEvent", EventFunc)
for eventname in pairs(events) do eventHandler:RegisterEvent(eventname) end	

eventHandler:SetScript("OnUpdate", function()
	if WhatsNext.showFrame then
		WhatsNext.showFrame:Show()
		WhatsNext.showFrame = nil
	end
	if WhatsNext.db.profile.enabled and (inCombat or not WhatsNext.db.profile.hideOutOfCombat) then
		UpdateSuggestionFrames()
	end
end)