---------------
--  Globals  --
---------------
DBM.InfoFrame = {}

-------------------
-- Local Globals --
-------------------
local DBM = DBM
local L = DBM_CORE_L
local UnitClass, GetTime, GetPartyAssignment, UnitGroupRolesAssigned, GetRaidTargetIndex, UnitExists, UnitName, UnitHealth, UnitPower, UnitPowerMax, UnitIsDeadOrGhost, UnitThreatSituation, UnitIsUnit, UIDropDownMenu_CreateInfo, UIDropDownMenu_AddButton = UnitClass, GetTime, GetPartyAssignment, UnitGroupRolesAssigned, GetRaidTargetIndex, UnitExists, UnitName, UnitHealth, UnitPower, UnitPowerMax, UnitIsDeadOrGhost, UnitThreatSituation, UnitIsUnit, UIDropDownMenu_CreateInfo, UIDropDownMenu_AddButton
local error, tostring, type, pairs, ipairs, select, tonumber, tsort, twipe, mfloor, mmax, mmin, mrandom, schar, ssplit = error, tostring, type, pairs, ipairs, select, tonumber, table.sort, table.wipe, math.floor, math.max, math.min, math.random, string.char, string.split
local ADDITIONAL_POWER_BAR_INDEX = ADDITIONAL_POWER_BAR_INDEX or 0
local NORMAL_FONT_COLOR, SPELL_FAILED_OUT_OF_RANGE = NORMAL_FONT_COLOR, SPELL_FAILED_OUT_OF_RANGE
local RAID_CLASS_COLORS = _G["CUSTOM_CLASS_COLORS"] or RAID_CLASS_COLORS-- for Phanx' Class Colors

--Hard code STANDARD_TEXT_FONT since skinning mods like to taint it (or worse, set it to nil, wtf?)
local standardFont
if LOCALE_koKR then
	standardFont = "Fonts\\2002.TTF"
elseif LOCALE_zhCN then
	standardFont = "Fonts\\ARKai_T.ttf"
elseif LOCALE_zhTW then
	standardFont = "Fonts\\blei00d.TTF"
elseif LOCALE_ruRU then
	standardFont = "Fonts\\FRIZQT___CYR.TTF"
else
	standardFont = "Fonts\\FRIZQT__.TTF"
end

--------------
--  Locals  --
--------------
local infoFrame = DBM.InfoFrame
local frame, initializeDropdown, currentEvent, createFrame
local maxLines, modLines, maxCols, modCols, prevLines = 5, 5, 1, 1, 0
local sortMethod = 1--1 Default, 2 SortAsc, 3 GroupId
local lines, sortedLines, icons, value = {}, {}, {}, {}
local playerName = UnitName("player")
local AceTimer = LibStub("AceTimer-3.0")
local SpecializedAbsorbs = LibStub("SpecializedAbsorbs-1.0")
---------------------
--  Dropdown Menu  --
---------------------
do
	local function toggleLocked()
		DBM.Options.InfoFrameLocked = not DBM.Options.InfoFrameLocked
	end

	local function toggleShowSelf()
		DBM.Options.InfoFrameShowSelf = not DBM.Options.InfoFrameShowSelf
	end

	local function setLines(_, line)
		prevLines = 0
		DBM.Options.InfoFrameLines = line
		if line ~= 0 then
			maxLines = line
		else
			maxLines = modLines or 5
		end
	end

	local function setCols(_, col)
		prevLines = 0
		DBM.Options.InfoFrameCols = col
		if col ~= 0 then
			maxCols = col
		else
			maxCols = modCols or 5
		end
	end

	function initializeDropdown(_, level, menu)
		local info

		if level == 1 then
			info = UIDropDownMenu_CreateInfo()
			info.text = L.LOCK_FRAME
			if DBM.Options.InfoFrameLocked then
				info.checked = true
			end
			info.func = toggleLocked
			UIDropDownMenu_AddButton(info, 1)

			info = UIDropDownMenu_CreateInfo()
			info.keepShownOnClick = true
			info.text = L.INFOFRAME_SHOW_SELF
			if DBM.Options.InfoFrameShowSelf then
				info.checked = true
			end
			info.func = toggleShowSelf
			UIDropDownMenu_AddButton(info, 1)

			info = UIDropDownMenu_CreateInfo()
			info.text = L.INFOFRAME_SETLINES
			info.notCheckable = true
			info.hasArrow = true
			info.keepShownOnClick = true
			info.menuList = "lines"
			UIDropDownMenu_AddButton(info, 1)

			info = UIDropDownMenu_CreateInfo()
			info.text = L.INFOFRAME_SETCOLS
			info.notCheckable = true
			info.hasArrow = true
			info.keepShownOnClick = true
			info.menuList = "cols"
			UIDropDownMenu_AddButton(info, 1)

			info = UIDropDownMenu_CreateInfo()
			info.text = HIDE
			info.notCheckable = true
			info.func = infoFrame.Hide
			info.arg1 = infoFrame
			UIDropDownMenu_AddButton(info, 1)
		elseif level == 2 then
			if menu == "lines" then
				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINESDEFAULT
				info.func = setLines
				info.arg1 = 0
				info.checked = (DBM.Options.InfoFrameLines == 0)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(3)
				info.func = setLines
				info.arg1 = 3
				info.checked = (DBM.Options.InfoFrameLines == 3)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(5)
				info.func = setLines
				info.arg1 = 5
				info.checked = (DBM.Options.InfoFrameLines == 5)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(8)
				info.func = setLines
				info.arg1 = 8
				info.checked = (DBM.Options.InfoFrameLines == 8)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(10)
				info.func = setLines
				info.arg1 = 10
				info.checked = (DBM.Options.InfoFrameLines == 10)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(15)
				info.func = setLines
				info.arg1 = 15
				info.checked = (DBM.Options.InfoFrameLines == 15)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(20)
				info.func = setLines
				info.arg1 = 20
				info.checked = (DBM.Options.InfoFrameLines == 20)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINES_TO:format(40)
				info.func = setLines
				info.arg1 = 40 -- Use 40 in classic for full man raids
				info.checked = (DBM.Options.InfoFrameLines == 40)
				UIDropDownMenu_AddButton(info, 2)
			elseif menu == "cols" then
				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_LINESDEFAULT
				info.func = setCols
				info.arg1 = 0
				info.checked = (DBM.Options.InfoFrameCols == 0)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(1)
				info.func = setCols
				info.arg1 = 1
				info.checked = (DBM.Options.InfoFrameCols == 1)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(2)
				info.func = setCols
				info.arg1 = 2
				info.checked = (DBM.Options.InfoFrameCols == 2)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(3)
				info.func = setCols
				info.arg1 = 3
				info.checked = (DBM.Options.InfoFrameCols == 3)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(4)
				info.func = setCols
				info.arg1 = 4
				info.checked = (DBM.Options.InfoFrameCols == 4)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(5)
				info.func = setCols
				info.arg1 = 5
				info.checked = (DBM.Options.InfoFrameCols == 5)
				UIDropDownMenu_AddButton(info, 2)

				info = UIDropDownMenu_CreateInfo()
				info.text = L.INFOFRAME_COLS_TO:format(6)
				info.func = setCols
				info.arg1 = 6
				info.checked = (DBM.Options.InfoFrameCols == 6)
				UIDropDownMenu_AddButton(info, 2)
			end
		end
	end
end

------------------------
--  Create the frame  --
------------------------
function createFrame()
	frame = CreateFrame("Frame", "DBMInfoFrame", UIParent)
	frame:Hide()
	frame:SetFrameStrata("DIALOG")
	frame.backdropInfo = {
		bgFile		= "Interface\\DialogFrame\\UI-DialogBox-Background", -- 131071
		tile		= true,
		tileSize	= 16
	}
	frame:SetBackdrop(frame.backdropInfo)
	frame:SetPoint(DBM.Options.InfoFramePoint, UIParent, DBM.Options.InfoFramePoint, DBM.Options.InfoFrameX, DBM.Options.InfoFrameY)
	frame:SetSize(10, 10)
	frame:SetClampedToScreen(true)
	frame:EnableMouse(true)
	frame:SetToplevel(true)
	frame:SetMovable(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", function(self)
		if not DBM.Options.InfoFrameLocked then
			self:StartMoving()
		end
	end)
	frame:SetScript("OnDragStop", function(self)
		self:StopMovingOrSizing()
		local point, _, _, x, y = self:GetPoint(1)
		DBM.Options.InfoFrameX = x
		DBM.Options.InfoFrameY = y
		DBM.Options.InfoFramePoint = point
	end)
	frame:SetScript("OnEvent", function(self, event, ...)
		if infoFrame[event] then
			infoFrame[event](self, ...)
		end
	end)
	frame:SetScript("OnMouseDown", function(_, button)
		if button == "RightButton" then
			local dropdownFrame = CreateFrame("Frame", "DBMInfoFrameDropdown", frame, "UIDropDownMenuTemplate")
			UIDropDownMenu_Initialize(dropdownFrame, initializeDropdown)
			ToggleDropDownMenu(1, nil, dropdownFrame, "cursor", 5, -10)
		end
	end)

	local header = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	header:SetTextColor(1, 1, 1)
	header:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 2, 2)
	frame.header = header
	infoFrame:SetHeader()

	frame.lines = {}
end

------------------------
--  Update functions  --
------------------------
local updateCallbacks = {}

local function sortFuncDesc(a, b)
	return lines[a] > lines[b]
end

local function sortFuncAsc(a, b)
	return lines[a] < lines[b]
end

local function sortGroupId(a, b)
	return DBM.GetGroupId(DBM, a) < DBM.GetGroupId(DBM, b)
end

local function updateLines(preSorted)
	twipe(sortedLines)
	if preSorted then
		-- Copy table as code from mod keeps around references this this and the "normal" table is wiped regularly
		for i, v in ipairs(preSorted) do
			sortedLines[i] = v
		end
	else
		for i in pairs(lines) do
			sortedLines[#sortedLines + 1] = i
		end
		if sortMethod == 3 then
			tsort(sortedLines, sortGroupId)
		elseif sortMethod == 2 then
			tsort(sortedLines, sortFuncAsc)
		else
			tsort(sortedLines, sortFuncDesc)
		end
	end
	for _, v in ipairs(updateCallbacks) do
		v(sortedLines)
	end
end

--[[
local function namesortFuncAsc(a, b)
	return a < b
end

local function updateNamesortLines()
	twipe(sortedLines)
	for i in pairs(lines) do
		sortedLines[#sortedLines + 1] = i
	end
	tsort(sortedLines, namesortFuncAsc)
	for _, v in ipairs(updateCallbacks) do
		v(sortedLines)
	end
end
]]--

local function updateLinesCustomSort(sortFunc)
	twipe(sortedLines)
	for i in pairs(lines) do
		sortedLines[#sortedLines + 1] = i
	end
	tsort(sortedLines, sortFunc)
	for _, v in ipairs(updateCallbacks) do
		v(sortedLines)
	end
end

local function updateIcons()
	twipe(icons)
	for uId in DBM:GetGroupMembers() do
		local icon = GetRaidTargetIndex(uId)
		local icon2 = GetRaidTargetIndex(uId .. "target")
		if icon and icon <= 8 then
			icons[DBM:GetUnitFullName(uId)] = ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(icon)
		end
		if icon2 and icon2 <= 8 then
			icons[DBM:GetUnitFullName(uId .. "target")] = ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(icon2)
		end
	end
	for i = 1, 5 do
		local icon = GetRaidTargetIndex("boss" .. i)
		if icon then
			icons[UnitName("boss" .. i)] = ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(icon)
		end
	end
end

local function updateHealth()
	twipe(lines)
	local threshold = value[1]
	for uId in DBM:GetGroupMembers() do
		if UnitHealth(uId) < threshold and not UnitIsDeadOrGhost(uId) then
			lines[DBM:GetUnitFullName(uId)] = UnitHealth(uId) - threshold
		end
	end
	updateLines()
	updateIcons()
end

local function updatePlayerPower()
	twipe(lines)
	local threshold = value[1]
	local powerType = value[2]
	local spellFilter = value[3]
	-- Value 4 is the noUpdate handler
	-- Value 5 is sorting method, handled in show handler
	for uId in DBM:GetGroupMembers() do
		if not spellFilter or not DBM:UnitDebuff(uId, spellFilter) then
			local maxPower = UnitPowerMax(uId, powerType)
			if maxPower ~= 0 and not UnitIsDeadOrGhost(uId) and UnitPower(uId, powerType) / UnitPowerMax(uId, powerType) * 100 >= threshold then
				lines[DBM:GetUnitFullName(uId)] = UnitPower(uId, powerType)
			end
		end
	end
	if DBM.Options.InfoFrameShowSelf and not lines[playerName] and UnitPower("player", powerType) > 0 then
		lines[playerName] = UnitPower("player", powerType)
	end
	updateLines()
	updateIcons()
end

local function updateEnemyPower()
	twipe(lines)
	local threshold = value[1]
	local powerType = value[2]
	local specificUnit = value[3]
	if powerType then -- Only do power type defined
		if specificUnit then
			specificUnit = UnitExists(specificUnit) or DBM:GetUnitIdFromGUID(specificUnit)--unitID already passed or GUID we convert into unitID
			if UnitExists(specificUnit) then
				local currentPower, maxPower = UnitPower(specificUnit, powerType), UnitPowerMax(specificUnit, powerType)
				if maxPower and maxPower > 0 then
					local percent = currentPower / maxPower * 100
					if percent >= threshold then
						lines[UnitName(specificUnit)] = mfloor(percent) .. "%"
					end
				end
			end
		else
			for i = 1, 5 do
				local uId = "boss" .. i
				local currentPower, maxPower = UnitPower(uId), UnitPowerMax(uId)
				if maxPower and maxPower > 0 then
					local percent = currentPower / maxPower * 100
					if percent >= threshold then
						lines[UnitName(uId)] = mfloor(percent) .. "%"
					end
				end
			end
		end
	else -- Check primary power type and alternate power types together. This should only be used if BOTH power types exist on same boss
		if specificUnit then
			specificUnit = UnitExists(specificUnit) or DBM:GetUnitIdFromGUID(specificUnit)--unitID already passed or GUID we convert into unitID
			if UnitExists(specificUnit) then
				-- Primary Power
				local currentPower, maxPower = UnitPower(specificUnit), UnitPowerMax(specificUnit)
				if maxPower and maxPower > 0 then
					local percent = currentPower / maxPower * 100
					if percent >= threshold then
						lines[UnitName(specificUnit)] = mfloor(percent) .. "%"
					end
				end
				-- Alternate Power
				local currentAltPower, maxAltPower = UnitPower(specificUnit, ADDITIONAL_POWER_BAR_INDEX), UnitPowerMax(specificUnit, ADDITIONAL_POWER_BAR_INDEX)
				if maxAltPower and maxAltPower > 0 then
					if currentAltPower / maxAltPower * 100 >= threshold then
						lines[UnitName(specificUnit)] = L.INFOFRAME_ALT .. currentAltPower
					end
				end
			end
		else
			for i = 1, 5 do
				local uId = "boss" .. i
				-- Primary Power
				local currentPower, maxPower = UnitPower(uId), UnitPowerMax(uId)
				if maxPower and maxPower > 0 then
					if currentPower / maxPower * 100 >= threshold then
						lines[UnitName(uId)] = currentPower
					end
				end
				-- Alternate Power
				local currentAltPower, maxAltPower = UnitPower(uId, ADDITIONAL_POWER_BAR_INDEX), UnitPowerMax(uId, ADDITIONAL_POWER_BAR_INDEX)
				if maxAltPower and maxAltPower > 0 then
					if currentAltPower / maxAltPower * 100 >= threshold then
						lines[UnitName(uId)] = L.INFOFRAME_ALT .. currentAltPower
					end
				end
			end
		end
	end
	updateLines()
	updateIcons()
end

-- WotLK does not have absorb API, so absorb numbers will be estimated using SpecializedAbsorbs library
local function updateEnemyAbsorb()
	twipe(lines)
	local spellInput = value[1]
	local totalAbsorb = value[2]
	local specificUnit = value[3]
	if specificUnit then
		specificUnit = UnitExists(specificUnit) or DBM:GetUnitIdFromGUID(specificUnit)--unitID already passed or GUID we convert into unitID
		if UnitExists(specificUnit) then
			local absorbAmount
			if spellInput then -- Get specific spell absorb
				absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(specificUnit), spellInput)
			else -- Get all of them
				absorbAmount = SpecializedAbsorbs:UnitTotal(UnitGUID(specificUnit))
			end
			if absorbAmount and absorbAmount > 0 then
				if totalAbsorb then
					lines[UnitName(specificUnit)] = mfloor(absorbAmount / totalAbsorb * 100) .. "%"
				else
					lines[UnitName(specificUnit)] = mfloor(absorbAmount)
				end
			end
		end
	else--Generic absorbs for bosses. Not to be mistaken for updateMultiEnemyAbsorb, which supports checking multiple units that might or might not be bosses
		for i = 1, 5 do
			local uId = "boss" .. i
			if UnitExists(uId) then
				local absorbAmount
				if spellInput then -- Get specific spell absorb
					absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(uId), spellInput)
				else -- Get all of them
					absorbAmount = SpecializedAbsorbs:UnitTotal(UnitGUID(uId))
				end
				if absorbAmount and absorbAmount > 0 then
					if totalAbsorb then
						lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
					else
						lines[UnitName(uId)] = mfloor(absorbAmount)
					end
				end
			end
		end
	end
	updateLines()
	updateIcons()
end

--Really hate splitting this off from updateEnemyAbsorb but having them merged was even uglier
--Note, this method also less efficient than boss only or single unit absorb tracking.
--Don't use this function to be lazy (ie just passing 1 guid instead of finding valid unit at mod level)
local function updateMultiEnemyAbsorb()
	twipe(lines)
	local spellInput = value[1]
	local totalAbsorb = value[2]
	local guidTable = value[3]--Multi target by table
	local guidTracked = {}
	for i = 1, 10 do
		if #guidTable == #guidTracked then--Stop searching, found everything we're looking for.
			break
		end
		local uId = "boss" .. i
		if UnitExists(uId) then
			local targetGUID = UnitGUID(uId)
			if guidTable[targetGUID] and not guidTracked[targetGUID] then
				guidTracked[targetGUID] = true
				local absorbAmount
				if spellInput then -- Get specific spell absorb
					absorbAmount = SpecializedAbsorbs:UnitEffect(targetGUID, spellInput)
				else -- Get all of them
					absorbAmount = SpecializedAbsorbs:UnitTotal(targetGUID)
				end
				if absorbAmount and absorbAmount > 0 then
					if totalAbsorb then
						lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
					else
						lines[UnitName(uId)] = mfloor(absorbAmount)
					end
				end
			end
		end
	end
	for unitId in DBM:GetGroupMembers() do--Do not use self on this function, because self might be bossModPrototype
		if #guidTable == #guidTracked then--Stop searching, found everything we're looking for.
			break
		end
		local uId = unitId .. "target"
		local targetGUID = UnitGUID(unitId)
		if guidTable[targetGUID] and not guidTracked[targetGUID] then
			guidTracked[targetGUID] = true
			local absorbAmount
			if spellInput then -- Get specific spell absorb
				absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(uId), spellInput)
			else -- Get all of them
				absorbAmount = SpecializedAbsorbs:UnitTotal(UnitGUID(uId))
			end
			if absorbAmount and absorbAmount > 0 then
				if totalAbsorb then
					lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
				else
					lines[UnitName(uId)] = mfloor(absorbAmount)
				end
			end
		end
	end
	updateLines()
	updateIcons()
end

local function updateAllAbsorb()
	twipe(lines)
	local spellInput = value[1]
	local totalAbsorb = value[2]
	for i = 1, 5 do
		local uId = "boss" .. i
		if UnitExists(uId) then
			local absorbAmount
			if spellInput then -- Get specific spell absorb
				absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(uId), spellInput)
			else -- Get all of them
				absorbAmount = SpecializedAbsorbs:UnitTotal(UnitGUID(uId))
			end
			if absorbAmount and absorbAmount > 0 then
				if totalAbsorb then
					lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
				else
					lines[UnitName(uId)] = mfloor(absorbAmount)
				end
			end
		end
	end
	if spellInput then
		for uId in DBM:GetGroupMembers() do
			local absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(uId), spellInput)
			if absorbAmount and absorbAmount > 0 then
				if totalAbsorb then
					lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
				else
					lines[UnitName(uId)] = mfloor(absorbAmount)
				end
			end
		end
	end
	updateLines()
	updateIcons()
end

local function updatePlayerAbsorb()
	twipe(lines)
	local spellInput = value[1]
	local totalAbsorb = value[2]
	for uId in DBM:GetGroupMembers() do
		local absorbAmount
		if spellInput then -- Get specific spell absorb
			absorbAmount = SpecializedAbsorbs:UnitEffect(UnitGUID(uId), spellInput)
		else--Not even spell input given, this is a very generic infoframe
			absorbAmount = SpecializedAbsorbs:UnitTotal(UnitGUID(uId))
		end
		if absorbAmount and absorbAmount > 0 then
			if totalAbsorb then
				lines[UnitName(uId)] = mfloor(totalAbsorb and absorbAmount / totalAbsorb * 100) .. "%"
			else
				lines[UnitName(uId)] = mfloor(absorbAmount)
			end
		end
	end
	updateLines()
	updateIcons()
end

-- Buffs that are good to have, therefor bad not to have them.
local function updatePlayerBuffs()
	twipe(lines)
	local spellName = value[1]
	local tankIgnored = value[2]
	for uId in DBM:GetGroupMembers() do
		if tankIgnored and (UnitGroupRolesAssigned(uId) == "TANK" or GetPartyAssignment("MAINTANK", uId, 1)) then
		else
			if not DBM:UnitBuff(uId, spellName) and not UnitIsDeadOrGhost(uId) then
				lines[DBM:GetUnitFullName(uId)] = ""
			end
		end
	end
	updateLines()
	updateIcons()
end

-- Debuffs that are good to have, therefor it's bad NOT to have them.
local function updateGoodPlayerDebuffs()
	twipe(lines)
	local spellInput = value[1]
	local tankIgnored = value[2]
	for uId in DBM:GetGroupMembers() do
		if tankIgnored and (UnitGroupRolesAssigned(uId) == "TANK" or GetPartyAssignment("MAINTANK", uId, 1)) then
		else
			if not DBM:UnitDebuff(uId, spellInput) and not UnitIsDeadOrGhost(uId) then
				lines[DBM:GetUnitFullName(uId)] = ""
			end
		end
	end
	updateLines()
	updateIcons()
end

-- Debuffs that are bad to have, therefor it is bad to have them.
-- Function will auto handle spellName or spellId via DBMs unit debuff handler and spellInput type
local function updateBadPlayerDebuffs()
	twipe(lines)
	local spellInput = value[1]
	local tankIgnored = value[2]
	for uId in DBM:GetGroupMembers() do
		if tankIgnored and (UnitGroupRolesAssigned(uId) == "TANK" or GetPartyAssignment("MAINTANK", uId, 1)) then
		else
			if DBM:UnitDebuff(uId, spellInput) and not UnitIsDeadOrGhost(uId) then
				lines[DBM:GetUnitFullName(uId)] = ""
			end
		end
	end
	updateLines()
	updateIcons()
end

--Debuffs with important durations that we track
local function updatePlayerDebuffRemaining()
	twipe(lines)
	local spellInput = value[1]
	for uId in DBM:GetGroupMembers() do
		local expires = select(6, DBM:UnitDebuffNew(uId, spellInput))
		if expires then
			if expires == 0 then
				lines[DBM:GetUnitFullName(uId)] = 9000--Force sorting the unknowns under the ones we do know.
			else
				local debuffTime = expires - GetTime()
				lines[DBM:GetUnitFullName(uId)] = mfloor(debuffTime)
			end
		end
	end
	updateLines()
	updateIcons()
end

-- Buffs with important durations that we track
local function updatePlayerBuffRemaining()
	twipe(lines)
	local spellInput = value[1]
	for uId in DBM:GetGroupMembers() do
		local expires = select(6, DBM:UnitBuffNew(uId, spellInput))
		if expires then
			if expires == 0 then
				lines[DBM:GetUnitFullName(uId)] = 9000--Force sorting the unknowns under the ones we do know.
			else
				local debuffTime = expires - GetTime()
				lines[DBM:GetUnitFullName(uId)] = mfloor(debuffTime)
			end
		end
	end
	updateLines()
	updateIcons()
end

-- Debuffs that are bad to have, but we want to show players who do NOT have them
-- Function will auto handle spellName or spellId via DBMs unit debuff handler and spellInput type
local function updateReverseBadPlayerDebuffs()
	twipe(lines)
	local spellInput = value[1]
	local tankIgnored = value[2]
	for uId in DBM:GetGroupMembers() do
		if tankIgnored and (UnitGroupRolesAssigned(uId) == "TANK" or GetPartyAssignment("MAINTANK", uId, 1)) then
		else
			if not DBM:UnitDebuff(uId, spellInput) and not UnitIsDeadOrGhost(uId) and not DBM:UnitBuff(uId, 27827) then--27827 Spirit of Redemption. This particular info frame wants to ignore this
				lines[DBM:GetUnitFullName(uId)] = ""
			end
		end
	end
	updateLines()
	updateIcons()
end

local function updatePlayerBuffStacks()
	twipe(lines)
	local spellInput = value[1]
	for uId in DBM:GetGroupMembers() do
		local spellName, _, count = DBM:UnitBuffNew(uId, spellInput)
		if spellName and count then
			lines[DBM:GetUnitFullName(uId)] = count
		end
	end
	updateIcons()
	updateLines()
end

local function updatePlayerDebuffStacks()
	twipe(lines)
	local spellInput = value[1]
	for uId in DBM:GetGroupMembers() do
		local spellName, _, count = DBM:UnitDebuffNew(uId, spellInput)
		if spellName and count then
			lines[DBM:GetUnitFullName(uId)] = count
		end
	end
	updateIcons()
	updateLines()
end

local function updatePlayerAggro()
	twipe(lines)
	local aggroType = value[1]
	local tankIgnored = value[2]
	for uId in DBM:GetGroupMembers() do
		if tankIgnored and (UnitGroupRolesAssigned(uId) == "TANK" or GetPartyAssignment("MAINTANK", uId, 1)) then
		else
			local currentThreat = UnitThreatSituation(uId) or 0
			if currentThreat >= aggroType then
				lines[DBM:GetUnitFullName(uId)] = ""
			end
		end
	end
	updateLines()
	updateIcons()
end

local function updatePlayerTargets()
	twipe(lines)
	local cId = value[1]
	for uId, _ in DBM:GetGroupMembers() do
		if DBM:GetUnitCreatureId(uId .. "target") ~= cId and (UnitGroupRolesAssigned(uId) == "DAMAGER" or UnitGroupRolesAssigned(uId) == "NONE") then
			lines[DBM:GetUnitFullName(uId)] = ""
		end
	end
	updateLines()
	updateIcons()
end

local function updateByFunction()
	local func = value[1]
	local sortFunc = value[2]
	local useIcon = value[3]
	local presortedLines
	lines, presortedLines = func()
	if sortFunc then
		if type(sortFunc) == "function" then
			updateLinesCustomSort(sortFunc)
		else
			updateLines()
		end
	else
		updateLines(presortedLines)
	end
	if useIcon then
		updateIcons()
	end
end

-- Unsorted table maintained by mod and just sent here.
-- Never updated by onupdate method, requires manual updates when mod updates table
local function updateByTable(table)
	twipe(lines)
	if table then
		for i, v in pairs(table) do
			lines[i] = v
		end
	end
	updateLines()
	updateIcons()
end

local function updateTest()
	twipe(lines)
	lines["Alpha"] = 1
	lines["Beta"] = 10
	lines["Gamma"] = 25
	lines["Delta"] = 50
	lines["Epsilon"] = 100
	updateLines()
end

local test2Table
local function updateTest2()
	twipe(lines)
	if not test2Table then
		test2Table = {}
		for _ = 1, 40 do
			local ident = ""
			for _ = 1, mrandom(5, 12) do
				ident = ident .. schar(mrandom(65, 90))
			end
			test2Table[ident] = mrandom(1, 10) * 10
		end
	end
	for k, v in pairs(test2Table) do
		lines[k] = v
	end
	updateLines()
end

local test3Counter = 0
local function updateTest3()
	twipe(lines)
	if not test2Table then
		test2Table = {}
		for _ = 1, 40 do
			local ident = ""
			for _ = 1, mrandom(5, 12) do
				ident = ident .. schar(mrandom(65, 90))
			end
			test2Table[ident] = mrandom(1, 10) * 10
		end
	end
	if test3Counter < 40 then
		local count = 0
		for k, _ in pairs(test2Table) do
			if count > 40 - test3Counter then
				break
			end
			count = count + 1
			lines[k] = mrandom(1, 10) * 10
		end
		test3Counter = test3Counter + 1
	end
	updateLines()
end

local events = {
	["health"] = updateHealth,
	["playerpower"] = updatePlayerPower,
	["enemypower"] = updateEnemyPower,
	["enemyabsorb"] = updateEnemyAbsorb,
	["multienemyabsorb"] = updateMultiEnemyAbsorb,
	["allabsorb"] = updateAllAbsorb,
	["playerabsorb"] = updatePlayerAbsorb,
	["playerbuff"] = updatePlayerBuffs,
	["playergooddebuff"] = updateGoodPlayerDebuffs,
	["playerbaddebuff"] = updateBadPlayerDebuffs,
	["playerdebuffremaining"] = updatePlayerDebuffRemaining,
	["playerbuffremaining"] = updatePlayerBuffRemaining,
	["reverseplayerbaddebuff"] = updateReverseBadPlayerDebuffs,
	["playeraggro"] = updatePlayerAggro,
	["playerbuffstacks"] = updatePlayerBuffStacks,
	["playerdebuffstacks"] = updatePlayerDebuffStacks,
	["playertargets"] = updatePlayerTargets,
	["function"] = updateByFunction,
	["table"] = updateByTable,
	["test"] = updateTest,
	["test2"] = updateTest2,
	["test3"] = updateTest3
}

----------------
--  OnUpdate  --
----------------
local friendlyEvents = {
	["health"] = true,
	["playerpower"] = true,
	["playerabsorb"] = true,
	["playerbuff"] = true,
	["playergooddebuff"] = true,
	["playerbaddebuff"] = true,
	["playerdebuffremaining"] = true,
	["playerbuffremaining"] = true,
	["reverseplayerbaddebuff"] = true,
	["playeraggro"] = true,
	["playerbuffstacks"] = true,
	["playerdebuffstacks"] = true,
	["playertargets"] = true
}

local function onUpdate(frame, table)
	if events[currentEvent] then
		events[currentEvent](table)
	else
		if frame then
			frame:Hide()
		end
	end
	local color = NORMAL_FONT_COLOR
	infoFrame:ClearLines()
	local linesShown = 0
	for i = 1, #sortedLines do
		if linesShown >= maxLines * maxCols then
			break
		end
		local leftText = sortedLines[i]
		if not leftText then
			error("DBM InfoFrame: leftText cannot be nil, Notify DBM author. Infoframe force shutting down ", 2)
			frame:Hide()
			return
		elseif leftText and type(leftText) ~= "string" then
			leftText = tostring(leftText)
		end
		local rightText = lines[leftText]
		local extra, extraName = ssplit("*", leftText) -- Find just unit name, if extra info had to be added to make unique
		local icon = icons[extraName or leftText] and icons[extraName or leftText] .. leftText
		if friendlyEvents[currentEvent] then
			local unitId = DBM:GetRaidUnitId(DBM:GetUnitFullName(extraName or leftText)) or "player"--Prevent nil logical error
			if unitId and UnitExists(unitId) then
				local _, class = UnitClass(unitId)
				if class then
					color = RAID_CLASS_COLORS[class]
					if DBM.Options.StripServerName then -- StripServerName option is checked here, even though it's checked in GetShortServerName function, because we have to apply custom 3rd column hack reconstruction
						local shortName = DBM:GetShortServerName(extraName or leftText)
						if extraName then -- 3 column hack is present, we need to reconstruct leftText with shortened name
							leftText = extra.."*"..shortName
						else -- LeftText is name, just replace it with shortname
							leftText = shortName
						end
					end
				end
				linesShown = linesShown + 1
				if unitId and UnitIsUnit(unitId, "player") then -- It's player.
					if currentEvent == "health" or currentEvent == "playerpower" or currentEvent == "playerabsorb" or currentEvent == "playerbuff" or currentEvent == "playergooddebuff" or currentEvent == "playerbaddebuff" or currentEvent == "playerdebuffremaining" or currentEvent == "playerdebuffstacks" or currentEvent == "playerbuffremaining" or currentEvent == "playertargets" or currentEvent == "playeraggro" then--Red
						infoFrame:SetLine(linesShown, icon or leftText, rightText, 255, 0, 0, 255, 255, 255)
					else -- Green
						infoFrame:SetLine(linesShown, icon or leftText, rightText, 0, 255, 0, 255, 255, 255)
					end
				else -- It's not player, do nothing special with it. Ordinary class colored text.
					if currentEvent == "playerdebuffremaining" or currentEvent == "playerbuffremaining" then
						local numberValue = tonumber(rightText)
						if numberValue < 6 then
							infoFrame:SetLine(linesShown, icon or leftText, rightText, color.r, color.g, color.b, 255, 0, 0)--Red
						elseif numberValue < 11 then
							infoFrame:SetLine(linesShown, icon or leftText, rightText, color.r, color.g, color.b, 255, 127.5, 0)--Orange
						else
							if numberValue == 9000 then -- Out of range players
								infoFrame:SetLine(linesShown, icon or leftText, SPELL_FAILED_OUT_OF_RANGE, color.r, color.g, color.b, 255, 0, 0)--Red
							else
								infoFrame:SetLine(linesShown, icon or leftText, rightText, color.r, color.g, color.b, 255, 255, 255)--White
							end
						end
					else
						infoFrame:SetLine(linesShown, icon or leftText, rightText, color.r, color.g, color.b, 255, 255, 255)
					end
				end
			end
		else
			local color2 = NORMAL_FONT_COLOR -- Only custom into frames will have chance of putting player names on right side
			local unitId = DBM:GetRaidUnitId(DBM:GetUnitFullName(extraName or leftText))
			local unitId2 = DBM:GetRaidUnitId(DBM:GetUnitFullName(rightText))
			-- Class color names in custom functions too, IF unitID exists
			if unitId then -- Check left text
				local _, class = UnitClass(unitId)
				if class then
					color = RAID_CLASS_COLORS[class]
					if DBM.Options.StripServerName then--StripServerName option is checked here, even though it's checked in GetShortServerName function, because we have to apply custom 3rd column hack reconstruction
						local shortName = DBM:GetShortServerName(extraName or leftText)
						if extraName then -- 3 column hack is present, we need to reconstruct leftText with shortened name
							leftText = extra.."*"..shortName
						else -- LeftText is name, just replace it with shortname
							leftText = shortName
						end
					end
				end
			else
				color = NORMAL_FONT_COLOR
				leftText = extraName or leftText
			end
			if unitId2 then -- Check right text
				local _, class = UnitClass(unitId2)
				if class then
					color2 = RAID_CLASS_COLORS[class]
					rightText = DBM:GetShortServerName(rightText)
				end
			end
			linesShown = linesShown + 1
			infoFrame:SetLine(linesShown, icon or leftText, rightText, color.r, color.g, color.b, color2.r, color2.g, color2.b)
		end
	end
	local maxWidth1, maxWidth2, linesPerRow = {}, {}, 5
	if linesShown > 5 then
		linesPerRow = mmin(maxLines, mfloor(linesShown / maxCols + 0.99))
	end
	local shouldUpdate = prevLines ~= linesShown
	for i = 1, linesShown do
		if shouldUpdate then
			infoFrame:AlignLine(i * 2 - 1, linesPerRow * 2)
			infoFrame:AlignLine(i * 2, linesPerRow * 2)
		end
		local m = mfloor(i / linesPerRow + 0.99)
		frame.lines[i * 2 - 1]:SetWidth(0) -- Hack here, because the string width doesn't calculate properly.
		frame.lines[i * 2]:SetWidth(0)
		maxWidth1[m] = mmax(maxWidth1[m] or 0, frame.lines[i * 2 - 1]:GetStringWidth())
		maxWidth2[m] = mmax(maxWidth2[m] or 0, frame.lines[i * 2]:GetStringWidth())
	end
	local size = DBM.Options.InfoFrameFontSize
	local width = 0
	for i, _ in pairs(maxWidth1) do
		local maxWid, maxWid2 = maxWidth1[i], maxWidth2[i]
		width = width + maxWid + maxWid2 + size + (size / 2)
		for ii = 1, linesPerRow do
			local m = ((i - 1) * linesPerRow * 2) + (ii * 2)
			if not frame.lines[m] then
				break
			end
			frame.lines[m - 1]:SetSize(maxWid, size)
			frame.lines[m]:SetSize(maxWid2, size)
		end
	end
	if width == 0 then
		width = 105
	end
	local height = size
	if linesShown > linesPerRow then
		height = height + (linesPerRow * size)
	else
		height = height + (mmin(linesShown, maxLines) * size)
	end
	frame:SetSize(width, height)
	frame:Show()
	prevLines = linesShown
end

---------------
--  Methods  --
---------------
--Arg 1: spellName, health/powervalue, customfunction, table type. Arg 2: TankIgnore, Powertype, SortFunction, totalAbsorb, sortmethod (table/stacks). Arg 3: SpellFilter, UseIcon. Arg 4: disable onUpdate. Arg 5: sortmethod (playerpower)
function infoFrame:Show(modMaxLines, event, ...)
	if DBM.Options.DontShowInfoFrame and not (event or ""):find("test") then
		return
	end
	prevLines = 0
	-- local currentMapId = GetCurrentMapAreaID()
	modLines = modMaxLines
	if DBM.Options.InfoFrameLines and DBM.Options.InfoFrameLines ~= 0 then
		maxLines = DBM.Options.InfoFrameLines
	else
		maxLines = modMaxLines or 5
	end
	if DBM.Options.InfoFrameCols and DBM.Options.InfoFrameCols ~= 0 then
		maxCols = DBM.Options.InfoFrameCols
	else
		-- TODO, still needs more finite control via gui later on
		-- I think dynamic options modMaxLines/10 modMaxLines/5 are both valid options, as well as just capping at 2 >= 10
		maxCols = modMaxLines and modMaxLines >= 10 and 2 or 1
	end
	twipe(value)
	for i = 1, select("#", ...) do
		value[i] = select(i, ...)
	end
	if not frame then
		createFrame()
	end
	-- Orders event to use spellID no matter what and not spell name
	if event:find("byspellid") then
		event = event:gsub("byspellid", "") -- Strip off the byspellid, it served it's purpose, it simply told infoframe to not convert to spellName
		if type(value[1]) ~= "number" then
			error("DBM-InfoFrame: byspellid method must use spellId", 2)
			return
		end
	-- If spellId is given as value one and it's not a byspellid event, convert to spellname
	-- This also allows spell name to be given by mod, since value 1 verifies it's a number
	elseif type(value[1]) == "number" and event ~= "health" and event ~= "function" and event ~= "table" and event ~= "playertargets" and event ~= "playeraggro" and event ~= "playerpower" and event ~= "enemypower" and event ~= "test" and event ~= "test2" then
		-- Outside of "byspellid" functions, typical frames will still use spell NAME matching not spellID.
		-- This just determines if we convert the spell input to a spell Name, if a spellId was provided for a non byspellid infoframe
		value[1] = DBM:GetSpellInfo(value[1])
	end
	currentEvent = event
	if event == "playerbuff" or event == "playerbaddebuff" or event == "playergooddebuff" then
		sortMethod = 3 -- Sort by group ID
	elseif (event == "playerdebuffstacks" or event == "table" or event == "playerdebuffremaining") and value[2] and type(value[2]) == "number" then
		sortMethod = value[2]
	elseif event == "health" or event == "playerdebuffremaining" then
		sortMethod = 2 -- Sort lowest first
	elseif event == "playerpower" and value[5] and type(value[5]) == "number" then
		sortMethod = value[5]
	else
		sortMethod = 1 -- Sort highest first
	end
	if events[currentEvent] then
		events[currentEvent](value[1])
	else
		error("DBM-InfoFrame: Unsupported event", 2)
		return
	end
	if not friendlyEvents[currentEvent] then
		twipe(icons)
	end
	frame:Show()
	onUpdate(frame, value[1])
	if not frame.ticker and not value[4] and event ~= "table" then
		frame.ticker = AceTimer:ScheduleRepeatingTimer(function()
			onUpdate(frame)
		end, 0.5)
	elseif frame.ticker and value[4] then -- Redundancy, in event calling a non onupdate infoframe show without a hide event to unschedule ticker based infoframe
		AceTimer:CancelTimer(frame.ticker)
		frame.ticker = nil
	end
end

function infoFrame:RegisterCallback(cb)
	updateCallbacks[#updateCallbacks + 1] = cb
end

function infoFrame:Update(time)
	if not frame then
		createFrame()
	end
	if frame:IsShown() then
		if time then
			DBM:Unschedule(onUpdate)
			DBM:Schedule(time, onUpdate, frame)
		else
			onUpdate(frame)
		end
	end
end

function infoFrame:UpdateTable(table, time)
	if not frame then
		createFrame()
	end
	if frame:IsShown() and table then
		if time then
			DBM:Unschedule(onUpdate)
			DBM:Schedule(time, onUpdate, frame, table)
		else
			onUpdate(frame, table)
		end
	end
end

function infoFrame:SetHeader(text)
	if not frame then
		createFrame()
	end
	frame.header:SetText(text or "DBM Info Frame")
end

function infoFrame:ClearLines()
	if not frame then
		createFrame()
	end
	for i = 1, #frame.lines do
		frame.lines[i]:SetText("")
		frame.lines[i]:Hide()
	end
end

function infoFrame:AlignLine(lineNum, linesPerRow)
	local size = DBM.Options.InfoFrameFontSize
	local line = frame.lines[lineNum]
	line:SetJustifyH(lineNum % 2 == 0 and "RIGHT" or "LEFT")
	if lineNum == 1 then -- 1st entry left
		line:SetPoint("TOPLEFT", frame, "TOPLEFT", size / 2, -(size / 2))
	elseif lineNum == 2 then -- 1st entry right
		line:SetPoint("TOPLEFT", frame.lines[1], "TOPRIGHT", size / 2, 0)
	else
		if lineNum % linesPerRow == 1 then -- Column 2-x, 1st entry left
			line:SetPoint("TOPLEFT", frame.lines[lineNum - linesPerRow + 1], "TOPRIGHT", size, 0)
		elseif lineNum % 2 == 0 then -- Column 2-x, Right entry
			line:SetPoint("TOPLEFT", frame.lines[lineNum - 1], "TOPRIGHT", size / 2, 0)
		else -- Column 2-x, Left entry
			line:SetPoint("TOPLEFT", frame.lines[lineNum - 2], "LEFT", 0, -(size / 2))
		end
	end
end

function infoFrame:UpdateStyle()
	if not frame then
		createFrame()
	end
	prevLines = 0
	local font = DBM.Options.InfoFrameFont == "standardFont" and standardFont or DBM.Options.InfoFrameFont
	local size = DBM.Options.InfoFrameFontSize
	local style = DBM.Options.InfoFrameFontStyle == "none" and nil or DBM.Options.InfoFrameFontStyle
	for i = 1, #frame.lines do
		frame.lines[i]:SetFont(font, size, style)
	end
end

function infoFrame:SetLine(lineNum, leftText, rightText, colorR, colorG, colorB, color2R, color2G, color2B)
	if not frame then
		createFrame()
	end
	lineNum = lineNum * 2 - 1
	if not frame.lines[lineNum] then
		local font = DBM.Options.InfoFrameFont == "standardFont" and standardFont or DBM.Options.InfoFrameFont
		local size = DBM.Options.InfoFrameFontSize
		local style = DBM.Options.InfoFrameFontStyle == "none" and nil or DBM.Options.InfoFrameFontStyle
		frame.lines[lineNum] = frame:CreateFontString("Line" .. lineNum, "OVERLAY", "GameFontNormal")
		frame.lines[lineNum]:SetFont(font, size, style)
		frame.lines[lineNum + 1] = frame:CreateFontString("Line" .. lineNum + 1, "OVERLAY", "GameFontNormal")
		frame.lines[lineNum + 1]:SetFont(font, size, style)
		frame.lines[lineNum + 1]:SetJustifyH("RIGHT")
	end
	frame.lines[lineNum]:SetText(leftText)
	frame.lines[lineNum]:SetTextColor(colorR or 255, colorG or 255, colorB or 255)
	frame.lines[lineNum]:Show()
	frame.lines[lineNum + 1]:SetText(rightText)
	frame.lines[lineNum + 1]:SetTextColor(color2R or 255, color2G or 255, color2B or 255)
	frame.lines[lineNum + 1]:Show()
end

function infoFrame:Hide()
	twipe(lines)
	twipe(icons)
	twipe(sortedLines)
	twipe(updateCallbacks)
	infoFrame:SetHeader()
	twipe(value)
	if frame then
		if frame.ticker then
			AceTimer:CancelTimer(frame.ticker)
			frame.ticker = nil
		end
		frame:Hide()
	end
	currentEvent = nil
	sortMethod = 1
end

function infoFrame:SetLines(lines)
	modLines = lines
	if DBM.Options.InfoFrameLines == 0 then
		maxLines = lines
	end
end

function infoFrame:SetColumns(columns)
	modCols = columns
	if DBM.Options.InfoFrameCols == 0 then
		maxCols = columns
	end
end

function infoFrame:IsShown()
	return frame and frame:IsShown()
end

function infoFrame:SetSortingAsc()
	sortMethod = 2
end

function infoFrame:SetSortingGroupId()
	sortMethod = 3
end
