Quantcast
local Ellipsis	= _G['Ellipsis']
local L			= LibStub('AceLocale-3.0'):GetLocale('Ellipsis')
local LSM		= LibStub('LibSharedMedia-3.0')
local Unit		= {}

local unitPool			= Ellipsis.unitPool
local activeUnits		= Ellipsis.activeUnits
local unitID			= 1 -- unique ID for each unit object created

local FORMAT_NAME		= '%s'
local FORMAT_LEVEL_NAME	= '[%s] %s'

local RAID_CLASS_COLORS = RAID_CLASS_COLORS

local ceil, floor, min = math.ceil, math.floor, math.min
local format, strsplit = string.format, string.split
local tinsert, tremove, tsort, wipe = table.insert, table.remove, table.sort, table.wipe
local unpack, ipairs, pairs = unpack, ipairs, pairs

local controlDB, unitDB
local anchorLookup, priorityLookup

-- variables configured by user options
local unitWidth, headerAnchor
local opacityFaded, opacityNoTarget
local auraSize, auraPaddingY, auraSetPoint, auraSetPointInv, auraOffsetX, auraOffsetY
local wrapAuras, wrapNumber
local SortAuras, UpdateDisplay -- function refs set by user options

Ellipsis.Unit = Unit


-- ------------------------
-- AURA SORTING FUNCTIONS
-- ------------------------
function Unit.SortAuras_NAME_ASC(a, b)
	return a.spellName < b.spellName
end

function Unit.SortAuras_NAME_DESC(a, b)
	return a.spellName > b.spellName
end

function Unit.SortAuras_EXPIRY_ASC(a, b)
	if (a.expireTime == b.expireTime) then -- both auras expire at the same time, either passive, unverified (or double cast), sort by timetstamp
		return a.created < b.created
	elseif (a.expireTime <= 0) then -- a is either passive or unverified, sort to the 'head'
		return false
	elseif (b.expireTime <= 0) then -- b is either passive or unverified, sort to the 'head'
		return true
	else
		return a.expireTime < b.expireTime
	end
end

function Unit.SortAuras_EXPIRY_DESC(a, b)
	if (a.expireTime == b.expireTime) then -- both auras expire at the same time, either passive, unverified (or double cast), sort by timetstamp
		return a.created > b.created
	elseif (a.expireTime <= 0) then -- a is either passive or unverified, sort to the 'head'
		return true
	elseif (b.expireTime <= 0) then -- b is either passive or unverified, sort to the 'head'
		return false
	else
		return a.expireTime > b.expireTime
	end
end

function Unit.SortAuras_CREATE_ASC(a, b)
	return a.created < b.created
end

function Unit.SortAuras_CREATE_DESC(a, b)
	return a.created > b.created
end


-- ------------------------
-- AURA DISPLAY FUNCTIONS
-- ------------------------
local function UpdateDisplay_BAR(self, sortFirst)
	if (sortFirst) then -- Aura ordering has changed, resort before display
		tsort(self.aurasSorted, SortAuras)
	end

	for i, aura in ipairs(self.aurasSorted) do
		aura:ClearAllPoints()
		aura:SetPoint(auraSetPoint, self.header, auraSetPointInv, 0, auraOffsetY * (i - 1))
	end

	self:SetHeight(self.headerHeight + (#self.aurasSorted * (auraSize + auraPaddingY)) - auraPaddingY)
end

local function UpdateDisplay_ICON_LEFTRIGHT(self, sortFirst)
	if (sortFirst) then -- Aura ordering has changed, resort before display
		tsort(self.aurasSorted, SortAuras)
	end

	if (wrapAuras) then -- wrapping auras once they reach unitWidth
		for i, aura in ipairs(self.aurasSorted) do
			aura:ClearAllPoints()
			aura:SetPoint(auraSetPoint, self.header, auraSetPointInv, auraOffsetX * ((i - 1) % wrapNumber), auraOffsetY * floor((i - 1) / wrapNumber))
		end

		self:SetHeight(self.headerHeight + (ceil(#self.aurasSorted / wrapNumber) * (auraSize + auraPaddingY)))
	else
		for i, aura in ipairs(self.aurasSorted) do
			aura:ClearAllPoints()
			aura:SetPoint(auraSetPoint, self.header, auraSetPointInv, auraOffsetX * (i - 1), 0)
		end

		self:SetHeight(self.headerHeight + auraSize + auraPaddingY) -- paddingY needed to account for the height of timers (user choice)
	end
end

local function UpdateDisplay_ICON_CENTER(self, sortFirst)
	if (sortFirst) then -- Aura ordering has changed, resort before display
		tsort(self.aurasSorted, SortAuras)
	end

	if (wrapAuras) then -- wrapping auras once they reach unitWidth
		local numAuras	= #self.aurasSorted
		local offsetX	= auraOffsetX * ((min(numAuras, wrapNumber) - 1) / 2)
		local offsetY	= 0

		for i, aura in ipairs(self.aurasSorted) do
			aura:ClearAllPoints()
			aura:SetPoint(auraSetPoint, self.header, auraSetPointInv, offsetX, offsetY)--auraOffsetY * floor((i - 1) / wrapNumber))

			if ((i % wrapNumber) == 0) then -- new row, reset offsetX and increment offsetY
				numAuras	= numAuras - wrapNumber
				offsetX		= auraOffsetX * ((min(numAuras, wrapNumber) - 1) / 2)
				offsetY		= offsetY + auraOffsetY
			else
				offsetX		= offsetX - auraOffsetX
			end
		end

		self:SetHeight(self.headerHeight + (ceil(#self.aurasSorted / wrapNumber) * (auraSize + auraPaddingY)))
	else
		local offsetX = auraOffsetX * ((#self.aurasSorted - 1) / 2)

		for i, aura in ipairs(self.aurasSorted) do
			aura:ClearAllPoints()
			aura:SetPoint(auraSetPoint, self.header, auraSetPointInv, offsetX, 0)
			offsetX = offsetX - auraOffsetX
		end

		self:SetHeight(self.headerHeight + auraSize + auraPaddingY) -- paddingY needed to account for the height of timers (user choice)
	end
end


-- ------------------------
-- UNIT CREATION
-- ------------------------
local function CreateUnit()
	local new = CreateFrame('Frame', nil, UIParent)
	local widget

	-- main gui widgets
	widget = CreateFrame('Frame', nil, new)
	new.header = widget

	widget = new:CreateFontString(nil, 'OVERLAY', 'GameFontNormal')
	widget:SetAllPoints(new.header)
	widget:SetJustifyH('CENTER')
	new.headerText = widget

	new.unitID			= unitID
	unitID				= unitID + 1

	new.auras			= {}	-- [spellID] = auraObject
	new.aurasSorted		= {}	-- sorted list, indexed

	new['Release']				= Unit.Release
	new['Configure']			= Unit.Configure

	new['UpdateHeader']			= Unit.UpdateHeader
	new['UpdateHeaderColour']	= Unit.UpdateHeaderColour
	new['UpdateHeaderText']		= Unit.UpdateHeaderText
--	new['UpdateDisplay']		= [[SET BY CONFIGURE]]

	new['AddAura']				= Unit.AddAura
	new['RemoveAura']			= Unit.RemoveAura

	return new
end

function Unit:New(currentTime, groupBase, override, guid, unitName, unitClass, unitLevel)
	local new	= tremove(unitPool, 1) -- grab a unit from the inactive pool (if any)
	local group	= (override) and override or groupBase

	if (not new) then -- no inactive units, create new
		new = CreateUnit()
		new:Configure()
	else -- existing object, wipe aura data tables
		new.auras			= wipe(new.auras)
		new.aurasSorted		= wipe(new.aurasSorted)
	end

	new.created			= currentTime
	new.updated			= currentTime

	new.group			= group
	new.groupBase		= groupBase
	new.priority		= priorityLookup[group]

	new.guid			= guid
	new.unitName		= (unitDB.stripServer) and strsplit('-', unitName) or unitName
	new.unitClass		= unitClass
	new.unitLevel		= (unitLevel == -1) and L.UnitLevel_Boss or unitLevel
	new.unitHostile		= (groupBase == 'harmful') -- all other (non-override) group types are friendly

	if (groupBase == 'notarget') then -- special case Unit, configured differently from others
		new.headerText:SetTextColor(unpack(unitDB.colourHeader))
		new.headerText:SetFormattedText(L.UnitName_NoTarget)
		new:UpdateHeader(unitDB.collapseNoTarget or unitDB.collapseAllUnits)

		new:SetAlpha(opacityNoTarget)
	else
		new:UpdateHeaderColour()
		new:UpdateHeaderText()
		new:UpdateHeader((groupBase == 'player' and unitDB.collapsePlayer) or unitDB.collapseAllUnits)

		new:SetAlpha((group == 'target') and 1 or opacityFaded)
	end

	activeUnits[guid] = new -- add new unit to primary unit lookup

	new:Show()

	anchorLookup[group]:AddUnit(new)

	return new
end

-- ------------------------
-- UNIT FUNCTIONS
-- ------------------------
function Unit:Release()
	for _, aura in pairs(self.auras) do
		aura:Release(true) -- release any remaining auras attached to this Unit (and flag them not to callback here for removal)
	end

	self:Hide()

	self.parentAnchor:RemoveUnit(self.guid)	-- tell parent Anchor to remove ourselves

	activeUnits[self.guid] = nil -- remove self from unit lookup

	tinsert(unitPool, self) -- add self back into the unitPool
end

function Unit:Configure()
	self:SetWidth(unitWidth) -- height set by UpdateDisplay

	self.header:SetWidth(unitWidth) -- height set by UpdateHeader
	self.header:ClearAllPoints()
	self.header:SetPoint(headerAnchor, self, headerAnchor, 0, 0) -- attach the header to the top or bottom of the unit as required

	self.headerText:SetFont(LSM:Fetch('font', unitDB.headerFont), unitDB.headerFontSize, unitDB.headerFontStyle)
	self.headerText:SetJustifyV(headerAnchor) -- repurpose var to set vertical justify to TOP|BOTTOM

	self.UpdateDisplay = UpdateDisplay -- set appropriate display function based on user options
end

function Unit:UpdateHeader(collapse)
	if (collapse) then
		self.headerText:Hide()
		self.headerHeight = 1		-- height has to be at least 1 or display breaks
		self.header:SetHeight(1)	-- cannot SetPoint to a 0 height widget
	else
		self.headerHeight = unitDB.headerHeight
		self.header:SetHeight(self.headerHeight)
		self.headerText:Show()
	end
end

function Unit:UpdateHeaderColour()
	local headerColourBy = unitDB.headerColourBy

	if (self.unitClass and headerColourBy == 'CLASS') then -- only if a class is set and colouring by class
		local colours = RAID_CLASS_COLORS[self.unitClass]
		self.headerText:SetTextColor(colours.r, colours.g, colours.b, 1) -- no alpha given, we assume an alpha return, so provide one (fully opaque)
	elseif (headerColourBy == 'REACTION') then
		if (self.unitHostile) then
			self.headerText:SetTextColor(unpack(unitDB.colourHostile))
		else
			self.headerText:SetTextColor(unpack(unitDB.colourFriendly))
		end
	else -- headerColourBy == 'NONE' (or by class but unit is either unverified or has no class)
		self.headerText:SetTextColor(unpack(unitDB.colourHeader))
	end
end

function Unit:UpdateHeaderText()
	if (unitDB.headerShowLevel) then
		self.headerText:SetFormattedText(FORMAT_LEVEL_NAME, self.unitLevel, self.unitName)
	else
		self.headerText:SetFormattedText(FORMAT_NAME, self.unitName)
	end
end


-- ------------------------
-- UNIT FUNCS - AURA CONTROL
-- ------------------------
function Unit:AddAura(aura)
	if (self.auras[aura.spellID]) then -- an aura with this spellID already exists, cancel the new aura and return existing
		aura:Release(true) -- flagBurst set so we don't update the Unit (going to be corrected right after anyhow)

		return self.auras[aura.spellID]
	else
		self.auras[aura.spellID] = aura

		tinsert(self.aurasSorted, aura)

		-- UpdateDisplay not called here to allow for bulk addition/display (all calling functions must call UpdateDisplay themselves)

		return aura
	end
end

function Unit:RemoveAura(spellID)
	self.auras[spellID] = nil

	for i, aura in ipairs(self.aurasSorted) do
		if (aura.spellID == spellID) then
			tremove(self.aurasSorted, i)
			break
		end
	end

	if (#self.aurasSorted == 0) then -- this was the last aura on this unit, time to die
		self:Release()
	else
		self:UpdateDisplay(false) -- still auras remaining, update display (no need to sort, the order won't have changed)
	end
end


-- ------------------------
-- UNIT OBJECT FUNCTIONS
-- ------------------------
function Ellipsis:InitializeUnits()
	controlDB		= self.db.profile.control
	unitDB			= self.db.profile.units

	anchorLookup	= self.anchorLookup
	priorityLookup	= self.priorityLookup

	self:ConfigureUnits()
end

function Ellipsis:ConfigureUnits()
	unitWidth		= unitDB.width
	opacityFaded	= unitDB.opacityFaded
	opacityNoTarget	= unitDB.opacityNoTarget

	-- configure aura sorting function to use (fallback to NAME_ASC if any problems)
	SortAuras = Unit['SortAuras_' .. controlDB.auraSorting] or Unit.SortAuras_NAME_ASC

	local auraDB	= self.db.profile.auras -- unlike the other DB shortcuts, this is only needed for configuration

	-- configure aura display functions to use
	if (auraDB.style == 'BAR') then
		auraSize		= auraDB.barSize
		auraPaddingY	= controlDB.auraBarPaddingY

		auraSetPoint 	= (controlDB.auraBarGrowth == 'DOWN') and 'TOP' or 'BOTTOM'
		auraSetPointInv	= (auraSetPoint == 'TOP') and 'BOTTOM' or 'TOP'
		auraOffsetY		= (auraPaddingY + auraSize) * ((controlDB.auraBarGrowth == 'DOWN') and - 1 or 1)

		headerAnchor	= (controlDB.auraBarGrowth == 'DOWN') and 'TOP' or 'BOTTOM'

		UpdateDisplay = UpdateDisplay_BAR
	else -- auraDB.style == 'ICON'
		auraSize		= auraDB.iconSize
		auraPaddingY	= controlDB.auraIconPaddingY

		local auraPaddingX = controlDB.auraIconPaddingX

		if (controlDB.auraIconGrowth == 'CENTER') then
			auraSetPoint	= 'TOP'
			auraSetPointInv = 'BOTTOM'
		else
			auraSetPoint	= (controlDB.auraIconGrowth == 'LEFT') and 'TOPRIGHT' or 'TOPLEFT'
			auraSetPointInv	= (auraSetPoint == 'TOPRIGHT') and 'BOTTOMRIGHT' or 'BOTTOMLEFT'
		end

		auraOffsetX		= (auraPaddingX + auraSize) * ((controlDB.auraIconGrowth == 'RIGHT') and 1 or -1) -- CENTER display uses same offset as LEFT
		auraOffsetY		= (auraPaddingY + auraSize) * -1 -- only growing down from header

		wrapAuras		= controlDB.auraIconWrapAuras -- only used if wrapping auras (same as below)
		wrapNumber		= floor((unitWidth + auraPaddingX) / (auraSize + auraPaddingX))

		headerAnchor	= 'TOP' -- icon style is always below the header

		UpdateDisplay = (controlDB.auraIconGrowth == 'CENTER') and UpdateDisplay_ICON_CENTER or UpdateDisplay_ICON_LEFTRIGHT
	end
end

function Ellipsis:UpdateExistingUnits()
	for _, unit in pairs(unitPool) do
		unit:Configure()
	end

	for _, unit in pairs(activeUnits) do -- doesn't include priority (thats only set on major control changes)
		unit:Configure()

		unit.unitName	= (unitDB.stripServer) and strsplit('-', unit.unitName) or unit.unitName

		if (unit.groupBase == 'notarget') then
			unit.headerText:SetTextColor(unpack(unitDB.colourHeader))
			unit:UpdateHeader(unitDB.collapseNoTarget or unitDB.collapseAllUnits)

			unit:SetAlpha(opacityNoTarget)
		else
			unit:UpdateHeaderColour()
			unit:UpdateHeaderText()
			unit:UpdateHeader((unit.groupBase == 'player' and unitDB.collapsePlayer) or unitDB.collapseAllUnits)

			unit:SetAlpha((unit.group == 'target') and 1 or opacityFaded)
		end

		unit:UpdateDisplay(true)	-- update display of auras
	end
end