

--------------------
-- What's Next? ----
----- by Strucker --
--------------------
local _
local WhatsNext = LibStub('AceAddon-3.0'):NewAddon('WhatsNext', 'AceTimer-3.0', 'AceEvent-3.0', 'AceConsole-3.0');
WhatsNext.AceGUI = LibStub('AceGUI-3.0');	
WhatsNext.ScrollingTable = LibStub('ScrollingTable');
WhatsNext.SharedMedia = LibStub("LibSharedMedia-3.0");
_G.WhatsNext = WhatsNext;

local TYPE_SPELL = "spell"
local TYPE_ITEM = "item"
local TYPE_MACRO = "macro"
local TYPE_MOVEMENT = "movement"
local TYPE_NOTE = "note"
local ICON_QUESTION_MARK = "Interface\\Icons\\INV_Misc_QuestionMark"
local ICON_NOTE = "Interface\\Icons\\INV_MISC_NOTE_05"
_G.BINDING_HEADER_WHATSNEXT = "What's Next?"
local BINDING_HEADER_MYDOSTUFF = "The header";

local lastUpdate = 0
local inCombat = false
local optionsFrames = {}
local suggestionPreUpdateListeners, suggestionUpdateListeners, logicSetUpdateListeners, displaySettingsListeners = {},{},{},{}
			
local defaultSuggestion = {
	type = "text", 
	text = WhatsNextLocal:Localize("TITLE"),
	icon = ICON_QUESTION_MARK,  
	timeTillCast = 100,
}

WhatsNext.suggestionFrames = {}
WhatsNext.suggestions = {}
WhatsNext.castSequence = {}

WhatsNext.CLASSES = {
	"WARRIOR",
	"MONK",
	"DEATHKNIGHT",
	"SHAMAN",
	"PRIEST",
	"ROGUE",
	"HUNTER",
	"PALADIN",
	"WARLOCK",
	"MAGE",
	"DRUID",
	"DEMONHUNTER"
}
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",
	["DEMONHUNTER"] = "|cFFA330C9"
}
WhatsNext.CLASS_SPECIALIZATION_COUNTS = {
	["WARRIOR"] = 3,
	["MONK"] = 3,
	["DEATHKNIGHT"] = 3,
	["SHAMAN"] = 3,
	["PRIEST"] = 3,
	["ROGUE"] = 3,
	["HUNTER"] = 3,
	["PALADIN"] = 3,
	["WARLOCK"] = 3,
	["MAGE"] = 3,
	["DRUID"] = 4,
	["DEMONHUNTER"] = 2
}

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

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

local 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, WhatsNext.CLASS_SPECIALIZATION_COUNTS[class] or 3 do
			castSequences[class][i] = castSequences[class][i] or {}
		end
	end
end

function WhatsNext:OnInitialize()
	self:RegisterOptions()
	self:SetupLogicSets()
	self.spellNameList = {}
	local name
	for i = 1, 1000000 do
		name = GetSpellInfo(i);
		if name then
			self.spellNameList[name] = i
		end
	end
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"
	else
		return "Open World"
	end
end

function WhatsNext:AddLogicSetToActive(set,sets,setstack)
	tinsert(setstack,set.name)
	for i, block in ipairs(set) do
		if block.type == "logicset" then
			if not tContains(setstack,block.logicset) then
				for i, subset in ipairs(sets) do
					if subset.name == block.logicset then
						self:AddLogicSetToActive(subset,sets,setstack)
						break
					end
				end
			else
				print("ERROR, loop detected between logic sets.")
			end
		else
			tinsert(self.castSequence, block)
			local newBlock, check, key, var
			if block.unit and block.copyUnits and #block.copyUnits > 0 then
				for _, copyUnit in ipairs(block.copyUnits) do
					newBlock = DeepCopy(block)
					newBlock.unit = copyUnit
					
					for _,check in ipairs(newBlock.checks) do
						for key,var in pairs(check) do
							if var == block.unit then
								check[key] = copyUnit
							end
						end
					end
					
					tinsert(self.castSequence, newBlock)
				end
			end
		end
	end
	tremove(setstack)
end


function WhatsNext:UpdateActiveLogicSet()
	if not self.db.profile.castSequences then
		self:SetupLogicSets()
	end
	
	wipe(self.castSequence)

	local class, spec = select(2,UnitClass("player")), GetSpecialization() or 1;
	local sets = self.db.profile.castSequences[class] and self.db.profile.castSequences[class][spec]
	if sets then
		local battleType = GetBattleType()
		for i = 2, #sets do
			if sets[i].use == battleType then
				self:AddLogicSetToActive(sets[i],sets,{})
				break
			end
		end
		if #(self.castSequence) == 0 then
			for i = 2, #sets do
				if sets[i].use == "General" then
					self:AddLogicSetToActive(sets[i],sets,{})
					break
				end
			end
			if #(self.castSequence) == 0 then
				self:AddLogicSetToActive(sets[1],sets,{})
			end
		end
	end
	self:SetupTestChecks()
	WhatsNext_ExamineLogicSet(self.castSequence)
	self:ApplyExtensionSettings()
	for _,listener in ipairs(logicSetUpdateListeners) do
		listener(self.castSequence)
	end
	lastUpdate = GetTime()
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:AddDisplaySettingsListener(listener)
	if type(listener) == "function" then
		tinsert(displaySettingsListeners, listener)
	end
end

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

function WhatsNext:ApplyDisplaySettings()
	local frame = self: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:ApplyBaseAppearance(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)
	
	for _,listener in ipairs(displaySettingsListeners) do
		listener()
	end
end

local function UpdateSuggestionFrames()

	if not WhatsNext.lastUpdate  or WhatsNext.lastUpdate + WhatsNext.db.profile.timeBetweenUpdates <= GetTime() then
		WhatsNext:UpdateSuggestions()
		local i = 1
		for i, suggestion in ipairs(WhatsNext.suggestions) do
			--print(suggestion,defaultSuggestion,suggestion == defaultSuggestion)
			if suggestion == defaultSuggestion or (suggestion.timeTillCast <= 1.5 and i <= WhatsNext.db.profile.maxSuggestions) then
				WhatsNext:GetSuggestionFrame(i):SetSuggestion(suggestion)
				WhatsNext:ApplyAppearance(WhatsNext:GetSuggestionFrame(i),suggestion)
			end
		end
		for i = #WhatsNext.suggestions+1, WhatsNext.db.profile.maxSuggestions do
			WhatsNext: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
		--print(logicBlock[type],i)
		if not logicBlock.checks[i] or  not WhatsNext:ContainsConditionalStatement(logicBlock.checks[i].type) then
			tremove(logicBlock.checks,i)
			err = "Unknown conditional statement removed. " .. (logicBlock.checks[i] and  logicBlock.checks[i].type or "") .. ". " .. 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 ICON_QUESTION_MARK
end

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

local function IsValidTarget(unit, spell)
	return not unit or ( UnitExists(unit) and IsSpellInRange(spell,unit or "target"))
end


local function AddSpellSuggestion(block,suggestion)
	block.spell = WhatsNext:GetSpellID(block.spell) or block.spell
	--print(block.spell)
	suggestion.spell = block.spell
	suggestion.unit = block.unit
	suggestion.text, _, suggestion.icon, _, _, powerType = GetSpellInfo(suggestion.spell)

	if GetSpellInfo(suggestion.text) then
		suggestion.text,_,suggestion.icon = GetSpellInfo(suggestion.text)
	end
	
	if suggestion.text and IsUsableSpell(suggestion.spell) then
		start, duration = GetSpellCooldown(suggestion.text)	
		if suggestion.text and IsValidTarget(suggestion.unit, suggestion.text) 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
end

local function AddItemSuggestion(block,suggestion)
	local id = GetItemID(block.item)
	suggestion.item = block.item
	if id then
		start, duration, enabled = GetItemCooldown(id)
		suggestion.text, _, _, _, _, _, _, _, _, suggestion.icon = GetItemInfo(block.item) 

		local equippedIfNeeded = IsEquippedItem(block.item) or not IsEquippableItem(block.item)
		local usable = (IsItemInRange(block.item,"target") ~= 0) and IsUsableItem(block.item)

		if suggestion.text and enabled ~= 0 and equippedIfNeeded and usable 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
end

local function AddMacroSuggestion(block,suggestion)
	suggestion.text, suggestion.icon = GetMacroInfo(block.macro);
	suggestion.macro = block.macro
	if suggestion.text and suggestion.icon and WhatsNext:TestChecks(block.checks) then
		suggestion.timeTillCast = 0
		tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
	end
end

local function AddMovementSuggestion(block,suggestion)
	suggestion.text, suggestion.icon = GetMacroInfo(block.macro);
	suggestion.macro = block.macro
	if suggestion.text and suggestion.icon and WhatsNext:TestChecks(block.checks) then
		suggestion.timeTillCast = 0
		tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
	end
end

local function AddNoteSuggestion(block,suggestion)
	suggestion.text = block.note
	suggestion.note = block.note
	suggestion.icon = ICON_NOTE
	suggestion.timeTillCast = 0
	if WhatsNext:TestChecks(block.checks) then
		tinsert(WhatsNext.suggestions,DeepCopy(suggestion))
	end
end

function WhatsNext:UpdateSuggestions()
	self:ResetTestChecks()

	for _, listener in ipairs(suggestionPreUpdateListeners) do
		listener()
	end

	wipe(WhatsNext.suggestions)
	
	if self.castSequence then
	
		--print(#self.castSequence)
		for i,block in ipairs(self.castSequence) do
			wipe(suggestion)
			suggestion.type = block.type or TYPE_SPELL
			suggestion.index = i
			suggestion.appearance = block.appearence
			
			local func;
			if suggestion.type == TYPE_SPELL then
				func = AddSpellSuggestion
			elseif suggestion.type == TYPE_ITEM then
				func = AddItemSuggestion
			elseif suggestion.type == TYPE_MACRO then
				func = AddMacroSuggestion
			elseif suggestion.type == TYPE_MOVEMENT then
				func = AddMovementSuggestion
			elseif suggestion.type == TYPE_NOTE then
				func = AddNoteSuggestion
			end
			
			if func then
				local retOk,bound = pcall(func,block,suggestion)
				if not retOk then
					print(bound)
				end
			end
		end	
	
		for i = 1, #WhatsNext.suggestions do
			if not WhatsNext.suggestions[i] then
				tremove(WhatsNext.suggestions,i)
			end
		end
		--print(#WhatsNext.suggestions)
		if #WhatsNext.suggestions > 0 then
			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
		else
			tinsert(WhatsNext.suggestions,1,defaultSuggestion)
		end	

		for _, listener in ipairs(suggestionUpdateListeners) do
			listener()
		end
	end
	
end

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

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

function events:PLAYER_REGEN_DISABLED() 
	inCombat = true
	if WhatsNext.db.profile.enabled and #WhatsNext.suggestionFrames > 0 then
		WhatsNext.suggestionFrames[1]:Show()
	end
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)