Proximo = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", "AceDB-2.0", "AceComm-2.0", "AceHook-2.1")
local L = AceLibrary("AceLocale-2.2"):new("Proximo")
local BZ = AceLibrary("Babble-Zone-2.2")
local BC = AceLibrary("Babble-Class-2.2")

Proximo.title = "Proximo"
Proximo.OnCommReceive = {}

local prox_list = {}
local unit = 1
local arena_start = false
local mainassist = "target"
local mainassistset = false
local zone = ""

local UnitName = UnitName
local UnitClass = UnitClass
local UnitHealth = UnitHealth
local UnitPowerType = UnitPowerType
local UnitMana = UnitMana
local UnitManaMax = UnitManaMax
local UnitExists = UnitExists
local UnitCanAttack = UnitCanAttack
local UnitIsPlayer = UnitIsPlayer
local InCombatLockdown = InCombatLockdown
local GetNumPartyMembers = GetNumPartyMembers
local pairs = pairs
local GetPlayerBuff = GetPlayerBuff
local GetPlayerBuffTexture = GetPlayerBuffTexture
local GetRealZoneText = GetRealZoneText
local UNKNOWNOBJECT = UNKNOWNOBJECT

----------------------
--table caching
local new, del
do
	local list = setmetatable({}, {__mode='k'})
	function new(...)
		local t = next(list)
		if t then
			list[t] = nil
			for i = 1, select('#', ...) do
				t[i] = select(i, ...)
			end
			return t
		else
			return { ... }
		end
	end
	function del(t)
		for k in pairs(t) do
			t[k] = nil
		end
		list[t] = true
		return nil
	end
end

local function in_arena_zone()
	if zone == BZ["Ruins of Lordaeron"] or zone == BZ["Nagrand Arena"] or zone == BZ["Blade's Edge Arena"] then
		return true
	else
		return false
	end
end

local function has_arena_started()
	if not arena_start and in_arena_zone() then
		local pindex, icon
		for i = 1, 16 do
			pindex = GetPlayerBuff(i, "HELPFUL")
			icon = GetPlayerBuffTexture(pindex)
			if icon and icon:match("Spell_Nature_WispSplode") then
				arena_start = false
				return
			end
		end
		arena_start = true
	end
end

local function valid_unit(unitid)
	if arena_start and UnitExists(unitid) and UnitName(unitid) and UnitIsPlayer(unitid) and UnitCanAttack("player",unitid) and not UnitIsCharmed(unitid) and not UnitIsCharmed("player") then
		return true
	else
		return false
	end 
end

----------------------
--On start
function Proximo:OnInitialize()
	self:SetupOptions()
	self:SetDefaultCommPriority("ALERT")
	self:SetCommPrefix(self.title)
	self:RegisterMemoizations {"Druid","Hunter","Mage","Paladin","Priest","Rogue","Shaman","Warlock","Warrior"}
  self:RegisterDB("ProximoDB")
	self:RegisterDefaults("profile", self:GetDefaultConfig())   
	self:RegisterChatCommand({"/proximo", "/prox"}, self.options)
end

----------------------
--Disabled
function Proximo:OnDisable()
	self:ResetList()
	if self.frame then
		self.frame:Hide()
	end
end

----------------------
--Loaded
function Proximo:OnEnable()
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self:RegisterComm(self.title, "GROUP")
end

----------------------
--Player Zoned
function Proximo:PLAYER_ENTERING_WORLD()
	arena_start = false
	mainassistset = false
	zone = ""
	self:ResetList()
	self:ScheduleEvent(self.ZoneCheck, 3, self)
end

----------------------
--Hostile Death
function Proximo:CHAT_MSG_COMBAT_HOSTILE_DEATH(msg)
  local k, v
	for k, v in pairs(prox_list) do
		if msg == string.format(UNITDIESOTHER, k) then
			v.health = 0
			v.mana = 0
			break
		end
  end
end

----------------------
--Zone Check
function Proximo:ZoneCheck()
	zone = GetRealZoneText()
	if InCombatLockdown() or zone == "" or zone == nil then
		self:ScheduleEvent(self.ZoneCheck, 1, self)
		return
	end
	if in_arena_zone() then
		if self.frame==nil then
			self:MakeFrame()
		end
		self.frame:Show()
		self:RegisterEvent("UNIT_HEALTH")
 		self:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH")
 		self:RegisterEvent("UPDATE_MOUSEOVER_UNIT") 
		self:RegisterEvent("PLAYER_TARGET_CHANGED")
		if self.db.profile.mana then self:RegisterEvent("UNIT_MANA") end
		self:ScheduleRepeatingEvent("proximorefresh", self.FrameRefresh, .1, self)
		self:ScheduleRepeatingEvent("proximoparty", self.PartyTargets, 1, self)
		self:ScheduleRepeatingEvent("proximosync", self.Sync, 1, self)
		--enable nameplate tracking if they are on
		if NAMEPLATES_ON then
			self:CreateUnitUpdate()
		end
	else 
		if self:IsEventRegistered("UNIT_HEALTH") then self:UnregisterEvent("UNIT_HEALTH") end
	  if self:IsEventRegistered("CHAT_MSG_COMBAT_HOSTILE_DEATH") then self:UnregisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH") end
	  if self:IsEventRegistered("UPDATE_MOUSEOVER_UNIT") then self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT") end
	 	if self:IsEventRegistered("PLAYER_TARGET_CHANGED") then self:UnregisterEvent("PLAYER_TARGET_CHANGED") end
	 	if self:IsEventRegistered("UNIT_MANA") then self:UnregisterEvent("UNIT_MANA") end
	  if self:IsEventScheduled("proximorefresh") then self:CancelScheduledEvent("proximorefresh") end
	  if self:IsEventScheduled("proximosync") then self:CancelScheduledEvent("proximosync") end
	  if self:IsEventScheduled("proximoparty") then self:CancelScheduledEvent("proximoparty") end
	  if NAMEPLATES_ON then
			self:StopUnitUpdate()
		end
		if self.frame then self.frame:Hide() end
	end
end


----------------------
--Add a unit to local list
function Proximo:AddToList(name, class, health, mana)
	if not name or name == UNKNOWNOBJECT then return end
	local t = new()
	t.class = class
	t.health = health
	t.lhealth = 100
	t.mana = mana
	t.click = false
	t.unit = unit
  prox_list[name] = t
  --setup frame class
  self:FrameClass(self.frame.buttons[unit],class)
  --update unit count
  unit = unit + 1
  if unit > 5 then unit = 5 end
  --sent annoucement
  self:SendAnnouncement(name, class)
end

----------------------
--Reset local list
function Proximo:ResetList()
	local k, v
	for k, v in pairs(prox_list) do
		prox_list[k] = del(v)
	end
	unit = 1
	self:FrameClean()
end

----------------------
--get local list
function Proximo:GetList()
	return prox_list
end

----------------------
--get local arena start flag
function Proximo:ArenaStarted()
	return arena_start
end

----------------------
--Check unit for updates
function Proximo:CheckUnitIDChanges(unitid)
	if valid_unit(unitid) then
		local name, health, mana = UnitName(unitid), UnitHealth(unitid), 0
		if self.db.profile.mana and UnitPowerType(unitid) == 0 then 
			mana = math.floor((UnitMana(unitid)/UnitManaMax(unitid)) * 100)
		end
		if not prox_list[name] then 
	 		local _,class = UnitClass(unitid)
	 		class = string.gsub(string.lower(class), "^%l", string.upper)
			self:AddToList(name, class, health, mana)
		else
			prox_list[name].health = health
			prox_list[name].mana = mana
		end
	end
end

----------------------
--Mouse over event
function Proximo:UPDATE_MOUSEOVER_UNIT()
	self:CheckUnitIDChanges("mouseover")
end

----------------------
--Target changed event
function Proximo:PLAYER_TARGET_CHANGED()
	self:CheckUnitIDChanges("target")
end

----------------------
--Unit Health event
function Proximo:UNIT_HEALTH(arg1)
 if arg1=="target" or arg1=="focus" then
	 self:CheckUnitIDChanges(arg1)
 end
end

----------------------
--Unit Mana event
function Proximo:UNIT_MANA(arg1)
 if arg1=="target" or arg1=="focus" then
	 self:CheckUnitIDChanges(arg1)
 end
end

----------------------
--Scan party for target updates
function Proximo:PartyTargets()
	local i, party
	for i=1, GetNumPartyMembers() do
		party = "party"..i.."target"
		self:CheckUnitIDChanges(party)
		--update party targets
		self:UpdatePartyTarget(i, party)
	end
	--update arena_start flags
	if not arena_start then
		has_arena_started()
	end
	--update MA while arena has not started
	if not arena_start and not mainassistset then
		self:SetMainAssist(self.db.profile.ma)
	end
end

----------------------
--Set the MA based on a name
function Proximo:SetMainAssist(ma)
	if mainassistset then return end
	if UnitName("player") == ma then 
		mainassist = "target"
		mainassistset = true
	else
		local i, party
		for i=1, GetNumPartyMembers() do
			party = "party"..i
			if UnitName(party) == ma then
				mainassist = party.."target"
				mainassistset = true
				return
			end
		end
	end
end

----------------------
--Sync all data with clients
function Proximo:Sync()
	if arena_start and self.db.profile.sync then
		local k, v
		for k, v in pairs(prox_list) do
			if not v.synchealth or not v.syncmana or v.health ~= v.synchealth or v.syncmana ~= v.mana then
				self:SendCommMessage("GROUP", "ReceiveSync", k, v.class, v.health, v.mana)
				v.synchealth, v.syncmana = v.health, v.mana
			end
		end
	end
end

----------------------
--Receive a sync
function Proximo.OnCommReceive:ReceiveSync(prefix, sender, distribution, name, class, health, mana)
	--if arena has started
	if arena_start and self.db.profile.sync then
		--add if new name
		if not prox_list[name] then
	 		self:AddToList(name, class, health, mana)
	 	--don't sync health for your target or focus (you will always have best data)
	 	elseif UnitName("target") ~= name and UnitName("focus") ~= name then
	 		--only update if you don't see them as dead, wait for client to see them as alive
	 		if prox_list[name].health ~= 0 then
	  		prox_list[name].health = health
	  	end
	  	if prox_list[name].mana ~= 0 then
	  		prox_list[name].mana = mana
	  	end
		end
	end
end

----------------------
--Receive a MA
function Proximo.OnCommReceive:ReceiveMainAssist(prefix, sender, distribution, ma)
	self.db.profile.ma = ma
	mainassistset = false
	self:SetMainAssist(ma)
	self:Print(L["MainAssist"]..": "..ma)
end

----------------------
--Receive a version check
function Proximo.OnCommReceive:ReceiveVersion(prefix, sender, distribution)
	self:Print(L["VersionCheckDesc"])
	self:SendCommMessage("GROUP", "ReceiveVersionResponse", self.version)
end

----------------------
--Receive a version check response
function Proximo.OnCommReceive:ReceiveVersionResponse(prefix, sender, distribution,version)
	self:Print(L["VersionCheck"]..": "..sender.." - "..version)
end

----------------------
--Refresh the frame display
function Proximo:FrameRefresh() 
	local k, v, name, lname, class, health, mana, unit, colortext, alpha, button
	--update each frame
	for k, v in pairs(prox_list) do
    name, class, health, mana, unit = k, v.class, v.health, v.mana, v.unit
    button = self.frame.buttons[unit]
		alpha, colortext = 0, "|cFFFFFFFF"
		--set colors
		if health <= 0 then
			colortext = "|cFF444444"
			button.icon:SetAlpha(.33)
		else
			if health <= 33 then
				colortext = "|cFFFF0000"
			elseif health <= 66 then
				colortext = "|cFFFFFF00"
			end
			if button.icon:GetAlpha(1) ~= 1 then button.icon:SetAlpha(1) end
		end
    --update status bar
		button.healthBar:SetValue(health)
		button.healthtext:SetText(colortext..health.."%|r")
		--update mana bar
		if self.db.profile.mana and button.manaBar then
			button.manaBar:SetValue(mana)
			if mana > 0 and not button.manaBar:IsVisible() then button.manaBar:Show() end
		end
		--update highlight
		if UnitName(mainassist) == name then alpha = 1 end
		button.healthBar.highlight:SetBackdropBorderColor(self.db.profile.highlightcolor.r,self.db.profile.highlightcolor.g,self.db.profile.highlightcolor.b,alpha)
		--update selected target
		if self.db.profile.highlighttar then
			if UnitName("target") == name then 
				button.healthBar.selected:Show()
			else
				button.healthBar.selected:Hide()
			end
		end
		--update macros if not updated already and not in combat
		if not v.click and not InCombatLockdown() then 
			self:UpdateClicks(button, self.db.profile.leftclick, "macrotext1", name, self.db.profile.leftspellname)
			self:UpdateClicks(button, self.db.profile.rightclick, "macrotext2", name, self.db.profile.spellname)
			v.click = 1
		end
		--Update unit text
		if self.db.profile.classtext and BC:HasTranslation(class) then
			lname = name.." - "..BC[class]
		else 
			lname = name
		end
    if v.click then
    	button:SetAlpha(1)
			button.text:SetText(colortext..lname.."|r")
		else
			button:SetAlpha(.66)
			button.text:SetText(colortext.."** "..lname.." **|r")
    end
    --update low health annoucement
		if self.db.profile.lowhealth then
			if (health ~= 0) and (health <= 20) and (v.lhealth > 20) then
	    	self:SendAnnouncement(name, L["LowHealth"])
	    	if self.db.profile.lowhealthsound then
	    	  PlaySound("RaidWarning")
	    	end
	    end
	    v.lhealth = health;
	  end
  end
end

----------------------
--update a buttons clicks
function Proximo:UpdateClicks(button, option, type, name, spell)
	if option == "target" then
		button:SetAttribute(type, "/script PlaySound(\"igMainMenuOptionCheckBoxOn\")\n/target [target="..name.."]")
	elseif option == "focus" then
		button:SetAttribute(type, "/target [target="..name.."]\n/focus\n/targetlasttarget")
	elseif option == "spell" and spell and spell ~= "" then
		button:SetAttribute(type, "/target [target="..name.."]\n/cast "..spell)
	elseif option == "macro" then
		local macro = string.gsub(spell, "*t", name)
		button:SetAttribute(type, macro)
	end
end

----------------------
--Scan party for target updates
function Proximo:UpdatePartyTarget(num, target)
	if self.db.profile.partytargets then
		local k, v, name, unit, button
		--update each frame
		for k, v in pairs(prox_list) do
	    name, unit = k, v.unit
	    button = self.frame.buttons[unit]
			if UnitExists(target) and UnitName(target) == name then
				local r,g,b = BC:GetColor(UnitClass("party"..num))
				button.partytargets[num]:SetStatusBarColor(r,g,b,1)
				button.partytargets[num]:SetValue(1)
			else
				button.partytargets[num]:SetValue(0)
			end
			if not button.partytargets[num]:IsVisible() then button.partytargets[num]:Show() end
		end
	end
end