--[[ Aoe Heals module

	* Created statuses
	aoe-ChainHeal			Best targets to use Shaman "Chain Heal"
	aoe-PrayerOfHealing		Best target in every raid group to use Priest "Prayer of Healing"
	aoe-CircleOfHealing		Best targets to use Priest "Circle of Healing"
	aoe-WildGrowth			Best targets to use Druid "Wild Growth"
	aoe-neighbors			Tracks how many units are near every unit (radius is configurable from options)
	aoe-highlighter			Highlight nearby units when the mouse enters on a cell unit.

	* Roster units structure, data calculated by UpdateRoster() and GetFilteredRoster()
		unit, group      Unit name and group index (example: "raid5", 2 )
		deficit			 Health deficit + incoming heals
		percent			 Health percent (range 0-1)
		x,y				 Position of unit
		curMask          Bitmask of the current unit, each roster[index] element has a bitmask of 2^(index-1)
--]]
local Grid2 = Grid2
local AOEM = Grid2:NewModule("Grid2AoeHeals")
local _

AOEM.defaultDB = {profile = {updateRate = 0.25, showInCombat = true, showInRaid = false}}
AOEM.playerClass = Grid2.playerClass
if not AOEM.playerClass then
	_, AOEM.playerClass = UnitClass("player")
	Grid2.playerClass = AOEM.playerClass
end

local IsInRaid = Grid2.IsInRaid
local Grid2Layout = Grid2Layout
local UnitExists = UnitExists
local UnitIsEnemy = UnitIsEnemy
local UnitIsVisible = UnitIsVisible
local InCombatLockdown = InCombatLockdown
local GetPlayerMapPosition = GetPlayerMapPosition
local GetNumGroupMembers = Grid2.GetNumGroupMembers
local next = next
local min = math.min
local max = math.max
local floor = math.floor
local tsort = table.sort
local tinsert = table.insert
local tostring = tostring
local bit_band = bit.band

local UnitGetIncomingHeals = _G.UnitGetIncomingHeals
if not UnitGetIncomingHeals then
	local HealComm
	UnitGetIncomingHeals = function(unit, healer)
		HealComm = HealComm or LibStub("LibHealComm-4.0", true)
		if HealComm and healer then
			return HealComm:GetCasterHealAmount(UnitGUID(healer), HealComm.CASTED_HEALS, GetTime() + 5)
		elseif HealComm then
			return HealComm:GetHealAmount(UnitGUID(unit), HealComm.ALL_HEALS, GetTime() + 5)
		else
			return 0
		end
	end
end

local raidSizes = {raid40 = 25, raid25 = 25, raid20 = 20, raid15 = 15, raid10 = 10, party = 5, solo = 5}

local frame, timer
local statuses = {}
local hlStatuses = {}

local roster = {} -- current roster, array part indexed by position, hash part indexed by unit
local rosterv = {} -- valid roster,  excluded: out of range, dead, charmed units (or units at full health for chainheal status)
local rosterRaid  -- precalculated roster tables (25 units) to avoid garbage

local rosterValid  -- True if roster units are up to date
local rosterPosValid  -- True if roster units position and health data was updated

local mapValid, mapWidth, mapHeight

-- Misc functions

local function ClearAllIndicators()
	for _, status in next, statuses do
		status:ClearIndicators()
	end
end

local function UpdateRoster()
	ClearAllIndicators()
	wipe(roster)
	local m = min(IsInRaid() and GetNumGroupMembers() or 0, 25)
	if m > 0 then
		local g = raidSizes[Grid2Layout.partyType or "solo"] / 5
		local i = 1
		for j = 1, m do
			local _, _, h = GetRaidRosterInfo(j)
			if h <= g then
				local p = rosterRaid[i]
				roster[i] = p
				p.group = h
				p.unit = "raid" .. j
				roster[p.unit] = p
				i = i + 1
			end
		end
		tsort(roster, function(a, b) return a.group < b.group end)
		for r = 1, #roster do
			roster[r].curMask = 2 ^ (r - 1)
		end
	else
		for i = 1, 5 do
			local p = rosterRaid[i]
			roster[i] = p
			p.group = 1
			p.curMask = 2 ^ (i - 1)
			p.unit = (i == 1) and "player" or "party" .. (i - 1)
			roster[p.unit] = p
		end
	end
	rosterValid = true
end

-- Initialize data tables

local function Init()
	if not rosterRaid then
		rosterRaid = {}
		for i = 1, 25 do
			rosterRaid[i] = {neighbors = {}}
		end
	end
end

-- Timer

local function TimerEvent()
	mapWidth, mapHeight = AOEM:MapGetSize()
	if mapWidth then
		mapValid = true
		rosterPosValid = false
		for _, status in next, statuses do
			status:Update()
		end
	elseif mapValid then
		mapValid = false
		ClearAllIndicators()
	end
end

local function SetTimer(enable)
	if enable == not timer then
		rosterValid = false
		if enable then
			timer = Grid2:ScheduleRepeatingTimer(TimerEvent, AOEM.db.profile.updateRate)
			AOEM:Debug("Aoe Heals Timer Enabled.")
		else
			Grid2:CancelTimer(timer, true)
			timer = nil
			if mapValid then
				ClearAllIndicators()
			end
			AOEM:Debug("Aoe Heals Timer Disabled.")
		end
	end
end

-- statuses private methods

local function status_GetRoster()
	return roster
end

local function status_GetFilteredRoster()
	if not rosterPosValid then
		local chEnabled = AOEM.chEnabled
		if not rosterValid then
			UpdateRoster()
		end
		wipe(rosterv)
		for i = 1, #roster do
			local p = roster[i]
			local u = p.unit
			if UnitExists(u) and (not UnitIsDeadOrGhost(u)) and UnitIsVisible(u) and (not UnitIsEnemy("player", u)) then
				local hc = UnitHealth(u)
				local hm = UnitHealthMax(u)
				local hd = hm - hc
				if hd > 0 or (not chEnabled) then
					local x, y = GetPlayerMapPosition(u)
					p.deficit = max(hd - (UnitGetIncomingHeals(u) or 0), 0)
					p.percent = hc / hm
					p.x = x * mapWidth
					p.y = y * mapHeight
					rosterv[#rosterv + 1] = p
				end
			end
		end
		AOEM:Debug("Calculating units positions. Valid units count: ", #rosterv)
		rosterPosValid = true
	end
	return rosterv
end

local function status_SwapUnits(self)
	self.states, self.statesu = self.statesu, self.states
end

local function status_UpdateUnits(self)
	for unit in next, self.statesu do
		self:UpdateIndicators(unit)
	end
	wipe(self.statesu)
end

local function status_AddUnit(self, p)
	local unit = p.unit
	local count = p.count
	self.states[unit] = count
	if self.statesu[unit] ~= count then
		self.statesu[unit] = count
	else
		self.statesu[unit] = nil
	end
	return true
end

local function status_ClearIndicators(self)
	status_SwapUnits(self)
	status_UpdateUnits(self)
end

local function status_GetHighlightMask(self, unit)
	if unit and self.states[unit] then
		return roster[unit][self.HighlightField] or 0
	end
end

-- statuses public interface methods

local function UpdateTimerState(InCombat)
	if InCombat == nil then
		InCombat = InCombatLockdown()
	end
	local disabled = (AOEM.db.profile.showInCombat and (not InCombat)) or (AOEM.db.profile.showInRaid and (not IsInRaid())) or (GetNumGroupMembers() == 0)
	SetTimer(not disabled)
end

local function FrameEvents(self, event)
	local dbx = AOEM.db.profile
	if event == "RAID_ROSTER_UPDATE" then
		rosterValid = false
		if dbx.showInRaid or (not timer) then
			UpdateTimerState()
		end
	elseif event == "PARTY_MEMBERS_CHANGED" then
		if not IsInRaid() then
			rosterValid = false
			UpdateTimerState()
		end
	elseif dbx.showInCombat then -- REGEN_DISABLED and REGEN_ENABLED events
		UpdateTimerState(event == "PLAYER_REGEN_DISABLED")
	end
end

local function status_OnEnable(self)
	if self.isChainHeal then
		AOEM.chEnabled = true
	end
	Init()
	if #statuses == 0 then
		if not frame then
			frame = CreateFrame("Frame")
			frame:SetScript("OnEvent", FrameEvents)
		end
		frame:RegisterEvent("RAID_ROSTER_UPDATE")
		frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
		frame:RegisterEvent("PLAYER_REGEN_DISABLED")
		frame:RegisterEvent("PLAYER_REGEN_ENABLED")
		AOEM:MapEnable()
		UpdateTimerState()
	end
	tinsert(statuses, self)
	tsort(statuses, function(a, b) return a.order < b.order end)
	if self.StatusEnabled then
		self:StatusEnabled()
	end
end

local function status_OnDisable(self)
	for i = 1, #statuses do
		if statuses[i] == self then
			tremove(statuses, i)
			break
		end
	end
	if #statuses == 0 and frame then
		frame:UnregisterEvent("RAID_ROSTER_UPDATE")
		frame:UnregisterEvent("PARTY_MEMBERS_CHANGED")
		frame:UnregisterEvent("PLAYER_REGEN_DISABLED")
		frame:UnregisterEvent("PLAYER_REGEN_ENABLED")
		AOEM:MapDisable()
		SetTimer(false)
	end
	if self.StatusDisabled then
		self:StatusDisabled()
	end
	if self.isChainHeal then
		AOEM.chEnabled = nil
	end
end

local function status_IsActive(self, unit)
	if self.states[unit] then
		return true
	end
end

local function status_GetIcon(self, unit)
	return self.texture
end

local function status_GetBorder()
	return 1
end

local function status_GetCount(self, unit)
	return bit_band(self.states[unit], 0x00FF)
end

local function status_GetText(self, unit)
	return tostring(bit_band(self.states[unit], 0x00FF))
end

local function status_GetColor(self, unit)
	local color = self.dbx.color1
	return color.r, color.g, color.b, color.a
end

-- Statuses Creation

local function Create(baseKey, dbx)
	local setupFunc = AOEM.setupFunc[baseKey]
	if setupFunc then
		local status = Grid2.statusPrototype:new(baseKey)
		status.states = {}
		status.statesu = {}
		status.order = 0
		-- private methods
		status.GetRoster = status_GetRoster
		status.GetFilteredRoster = status_GetFilteredRoster
		status.AddUnit = status_AddUnit
		status.SwapUnits = status_SwapUnits
		status.UpdateUnits = status_UpdateUnits
		status.ClearIndicators = status_ClearIndicators
		status.GetHighlightMask = status_GetHighlightMask
		-- public interface methods
		status.OnEnable = status_OnEnable
		status.OnDisable = status_OnDisable
		status.IsActive = status_IsActive
		status.GetIcon = status_GetIcon
		status.GetBorder = status_GetBorder
		status.GetColor = status_GetColor
		status.GetCount = status_GetCount
		status.GetText = status_GetText
		Grid2:RegisterStatus(status, setupFunc(status, dbx) or {"color", "icon", "text"}, baseKey, dbx)
		if status.HighlightField then
			hlStatuses[baseKey] = status
		end
		return status
	end
end

AOEM.setupFunc = {}

AOEM.hlStatuses = hlStatuses
AOEM.statuses = statuses

-- Module methods
function AOEM:OnModuleInitialize()
	for key in next, self.setupFunc do
		Grid2.setupFunc[key] = Create
	end
end

function AOEM:PlayerHasGlyph(id)
	for i = 1, 9 do
		local _, _, _, si = GetGlyphSocketInfo(i)
		if id == si then
			return true
		end
	end
end

function AOEM.SortByDeficit(p1, p2)
	return p1.percent < p2.percent
end

function AOEM:RefreshUpdateRate()
	if timer then
		SetTimer(false)
		SetTimer(true)
	end
end

function AOEM:RefreshDisplayState()
	if #statuses > 0 then
		UpdateTimerState()
	end
end