

--------------------
-- What's Next? ----
----- by Strucker --
--------------------
local _
local WhatsNext = LibStub('AceAddon-3.0'):NewAddon('WhatsNext', 'AceEvent-3.0', 'AceConsole-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 inCombat = false
local optionsFrames = {}
local suggestionPreUpdateListeners, suggestionUpdateListeners, logicSetUpdateListeners, displaySettingsListeners = {},{},{},{}

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

function WhatsNext_ClearFocus()
	if GetCurrentKeyBoardFocus() and GetCurrentKeyBoardFocus().ClearFocus then
		GetCurrentKeyBoardFocus():ClearFocus()
	end
end
print(WhatsNext.defaultCastSequences)
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, WhatsNext.CLASS_SPECIALIZATION_COUNTS[class] 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"
	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

local lastUpdate = 0

function WhatsNext:UpdateActiveLogicSet()





	if not self.db.profile.castSequences then
		self:SetupLogicSets()
	end
	
	if GetTime() - 6 < lastUpdate then
		return
	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)
	
	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)	
	
	for _,listener in ipairs(displaySettingsListeners) do
		listener()
	end
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
				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
		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 ICON_QUESTION_MARK
end
			
local defaultSuggestion = {
	type = "text", 
	text = WhatsNextLocal:Localize("TITLE"),
	icon = ICON_QUESTION_MARK,  
	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
local powerCost, powerType, timeTillPower, missingPower, regen

local function GetTimeTillPower(targetPower, powerType)
	missingPower = (targetPower or 0) - (UnitPower("player") or 0)
	if missingPower <= 0 then
		return 0
	end
	if UnitPowerType("player") == powerType then
		_, regen = GetPowerRegen()
		if regen and regen > 0 then
			return missingPower / regen
		end
	end
	return nil
end

local spellCostBugCorrector = {
	[101546] = 40
}

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

local CostTip = CreateFrame('GameTooltip')
local CostText = CostTip:CreateFontString()
CostTip:AddFontStrings(CostTip:CreateFontString(), CostTip:CreateFontString())
CostTip:AddFontStrings(CostText, CostTip:CreateFontString())
-- the next function is what will be called to find the cost of a spell
local function GetPowerCostLine(spellId)
	-- returns the value of the second line of the tooltip
	-- bail if we don't have a spellId
	if spellId == nil then
		return nil
	else
		CostTip:SetOwner(WorldFrame, 'ANCHOR_NONE')
	--	print(spellId .. ' = ' )
		CostTip:SetSpellByID(spellId)
		--print('     ' .. CostText:GetText())
		return CostText:GetText()
	end
end

function WhatsNext_GetSpellCost(spell)
	local spellId = WhatsNext:GetSpellID(spell) or spell
	local costString = GetPowerCostLine(spellId) or "";
	if string.find(costString,"Range") or string.find(costString,"range") or string.find(costString,"Blood") or string.find(costString,"Frost") or string.find(costString,"Unholy") then
		return 0
	end
	local costEnd = string.find(costString,' ') or 0;
	local cost = costEnd > 0 and tonumber(string.sub(costString,0,costEnd-1)) or 0;
	--print(string.find(costString,"Range"));
	return cost > 3 and cost or 0;
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)
	powerCost = WhatsNext_GetSpellCost(block.spell);
	if GetSpellInfo(suggestion.text) then
		suggestion.text,_,suggestion.icon = GetSpellInfo(suggestion.text)
	end
	--print(suggestion.text .. " - " .. (IsValidTarget(suggestion.unit, suggestion.text) and "t" or "f" ).. " ? " .. ( WhatsNext:TestChecks(block.checks) and "t" or "f"))
	if suggestion.text and powerCost <= UnitPower("player") then
		start, duration = GetSpellCooldown(suggestion.text)	
		--print(suggestion.text ..": " .. IsValidTarget(suggestion.unit, 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)
	id = GetItemID(block.item)
	suggestion.item = 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
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()
	if self.castSequence then
		for _, listener in ipairs(suggestionPreUpdateListeners) do
			listener()
		end
	
		wipe(WhatsNext.suggestions)
		--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.appearance
			if suggestion.type == TYPE_SPELL then
				AddSpellSuggestion(block,suggestion)
			elseif suggestion.type == TYPE_ITEM then
				AddItemSuggestion(block,suggestion)
			elseif suggestion.type == TYPE_MACRO then
				AddMacroSuggestion(block,suggestion)
			elseif suggestion.type == TYPE_MOVEMENT then
				AddMovementSuggestion(block,suggestion)
			elseif suggestion.type == TYPE_NOTE then
				AddNoteSuggestion(block,suggestion)
			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
	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:MODIFIER_STATE_CHANGED(key,state)
	--print((IsLeftShiftKeyDown() and "LSHIFT" or "") .. (IsRightShiftKeyDown() and "RSHIFT" or "") .. (IsLeftControlKeyDown() and "LCTRL" or "") .. (IsRightControlKeyDown() and "RCTRL" or "") );
--	print(key,state);
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)