local ns = oUF

local oUF = ns.oUF
local Private = oUF.Private

local argcheck = Private.argcheck

local print = Private.print
local error = Private.error

local gsub, lower, match, split, upper = string.gsub, string.lower, string.match, string.split, string.upper
local insert, remove = table.insert, table.remove

local styles, style = {}
local callback, units, objects, headers = {}, {}, {}, {}

local elements = {}
local activeElements = {}

-- updating of "invalid" units.
local function enableTargetUpdate(object)
	object.onUpdateFrequency = object.onUpdateFrequency or .5
	object.__eventless = true

	local total = 0
	object:SetScript("OnUpdate", function()
		if not this.unit then
			return
		elseif total > this.onUpdateFrequency then
			this:UpdateAllElements("OnUpdate")
			total = 0
		end

		total = total + GetTime()
	end)
end
Private.enableTargetUpdate = enableTargetUpdate

local function updateActiveUnit(self, event, unit)
	return true
end

local function iterateChildren(...)
	for i = 1, getn(arg) do
		local obj = arg[i]

		if(type(obj) == "table" and obj.isChild) then
			updateActiveUnit(obj, "iterateChildren")
		end
	end
end

local function onAttributeChanged(self, name, value)
	if(name == "unit" and value) then
		if(self.hasChildren) then
			iterateChildren(self:GetChildren())
		end

		if(not self.onlyProcessChildren) then
			updateActiveUnit(self, "OnAttributeChanged")
		end

		if(self.unit and self.unit == value) then
			return
		else
			if(self.hasChildren) then
				iterateChildren(self:GetChildren())
			end
		end

	end
end

local frame_metatable = {
	__index = CreateFrame("Button")
}
Private.frame_metatable = frame_metatable

for k, v in next, {
	UpdateElement = function(self, name)
		local unit = self.unit
		if(not unit or not UnitExists(unit)) then return end

		local element = elements[name]
		if(not element or not self:IsElementEnabled(name) or not activeElements[self]) then return end
		if(element.update) then
			element.update(self, "OnShow", unit)
		end
	end,

	UpdateElement = function(self, name)
		local unit = self.unit
		if(not unit or not UnitExists(unit)) then return end

		local element = elements[name]
		if(not element or not self:IsElementEnabled(name) or not activeElements[self]) then return end
		if(element.update) then
			element.update(self, "OnShow", unit)
		end
	end,

	--[[ frame:EnableElement(name, unit)
	Used to activate an element for the given unit frame.

	* self - unit frame for which the element should be enabled
	* name - name of the element to be enabled (string)
	* unit - unit to be passed to the element"s Enable function. Defaults to the frame"s unit (string?)
	--]]
	EnableElement = function(self, name, unit)
		local unit = unit or self.unit
		if not unit then return end

		argcheck(name, 2, "string")
		argcheck(unit or self.unit, 3, "string", "nil")

		local element = elements[name]
		if(not element or self:IsElementEnabled(name) or not activeElements[self]) then return end

		if(element.enable(self, unit or self.unit)) then
			activeElements[self][name] = true

			if(element.update) then
				insert(self.__elements, element.update)
			end
		end
	end,

	--[[ frame:DisableElement(name)
	Used to deactivate an element for the given unit frame.

	* self - unit frame for which the element should be disabled
	* name - name of the element to be disabled (string)
	--]]
	DisableElement = function(self, name)
		argcheck(name, 2, "string")

		local enabled = self:IsElementEnabled(name)
		if(not enabled) then return end

		local update = elements[name].update
		for k, func in next, self.__elements do
			if(func == update) then
				remove(self.__elements, k)
				break
			end
		end

		activeElements[self][name] = nil

		-- We need to run a new update cycle in-case we knocked ourself out of sync.
		-- The main reason we do this is to make sure the full update is completed
		-- if an element for some reason removes itself _during_ the update
		-- progress.
		self:UpdateAllElements("DisableElement")

		return elements[name].disable(self)
	end,

	--[[ frame:IsElementEnabled(name)
	Used to check if an element is enabled on the given frame.

	* self - unit frame
	* name - name of the element (string)
	--]]
	IsElementEnabled = function(self, name)
		argcheck(name, 2, "string")

		local element = elements[name]
		if(not element) then return end

		local active = activeElements[self]
		return active and active[name]
	end,

	--[[ frame:Enable(asState)
	Used to toggle the visibility of a unit frame based on the existence of its unit. This is a reference to
	`RegisterUnitWatch`.

	* self    - unit frame
	* asState - if true, the frame"s "state-unitexists" attribute will be set to a boolean value denoting whether the
	            unit exists; if false, the frame will be shown if its unit exists, and hidden if it does not (boolean)
	--]]
	Enable = RegisterUnitWatch,
	--[[ frame:Disable()
	Used to UnregisterUnitWatch for the given frame and hide it.

	* self - unit frame
	--]]
	Disable = function(self)
		UnregisterUnitWatch(self)
		self:Hide()
	end,

	--[[ frame:UpdateAllElements(event)
	Used to update all enabled elements on the given frame.

	* self  - unit frame
	* event - event name to pass to the elements" update functions (string)
	--]]
	UpdateAllElements = function(self, event)
		local unit = self.unit
		if(not UnitExists(unit)) then return end

		assert(type(event) == "string", "Invalid argument 'event' in UpdateAllElements.")

		if(self.PreUpdate) then
			self:PreUpdate(event)
		end

		for _, func in next, self.__elements do
			func(self, event, unit)
		end

		if(self.PostUpdate) then
			self:PostUpdate(event)
		end
	end,

	SetUnit = function(self, unit)
		if unit ~= unit then
			self.unit = unit
		end
	end,
} do
	frame_metatable.__index[k] = v
end

local secureDropdown
local function InitializeSecureMenu()
	local unit = SecureTemplatesDropdown.unit
	if(not unit) then return end

	local unitType = match(unit, "^([a-z]+)[0-9]+$") or unit

	local menu
	if(unitType == "party") then
		menu = "PARTY"
	elseif(UnitIsUnit(unit, "player")) then
		menu = "SELF"
	elseif(UnitIsUnit(unit, "pet")) then
		menu = "PET"
	elseif(UnitIsPlayer(unit)) then
		if(UnitInRaid(unit) or UnitInParty(unit)) then
			menu = "PARTY"
		else
			menu = "PLAYER"
		end
	elseif(UnitIsUnit(unit, "target")) then
		menu = "RAID_TARGET_ICON"
	end

	if(menu) then
		UnitPopup_ShowMenu(SecureTemplatesDropdown, menu, unit)
	end
end

local function togglemenu(self, unit)
	if(not secureDropdown) then
		secureDropdown = CreateFrame("Frame", "SecureTemplatesDropdown", nil, "UIDropDownMenuTemplate")
		secureDropdown:SetID(1)

		insert(UnitPopupFrames, secureDropdown:GetName())
		UIDropDownMenu_Initialize(secureDropdown, InitializeSecureMenu, "MENU")
	end

	if(secureDropdown.openedFor and secureDropdown.openedFor ~= self) then
		CloseDropDownMenus()
	end

	secureDropdown.unit = lower(unit)
	secureDropdown.openedFor = self

	ToggleDropDownMenu(1, nil, secureDropdown, "cursor")
end

local function onShow(self)
	return self:UpdateAllElements("OnShow")
end

local function initObject(unit, style, styleFunc, header, ...)
	local num = getn(arg)
	for i = 1, num do
		local object = arg[i]
		local objectUnit = object.guessUnit or unit
		local suffix = match(objectUnit or unit, "%w+target")

		object.__elements = {}
		object.__registeredEvents = {}
		object.style = style
		object = setmetatable(object, frame_metatable)

		-- Expose the frame through oUF.objects.
		insert(objects, object)

		-- We have to force update the frames when PEW fires.
		object:RegisterEvent("PLAYER_ENTERING_WORLD", object.UpdateAllElements)

		object:SetScript("OnClick", function()
			if arg1 == "RightButton" then
				togglemenu(this, object.unit)
			else
				TargetUnit(this.unit)
			end
		end)

		if(not header) then
			-- Other target units are handled by :HandleUnit().
			if(suffix == "target") then
				enableTargetUpdate(object)
			else
				oUF:HandleUnit(object, unit)
			end
		else
			-- Used to update frames when they change position in a group.
			object:RegisterEvent("RAID_ROSTER_UPDATE", object.UpdateAllElements)

			if(num > 1) then
				if(object:GetParent() == header) then
					object.hasChildren = true
				else
					object.isChild = true
				end
			end

			if(suffix == "target") then
				enableTargetUpdate(object)
			end
		end

		Private.UpdateUnits(object, objectUnit)

		styleFunc(object, objectUnit, not header)

		object:SetScript("OnAttributeChanged", onAttributeChanged)
		object:SetScript("OnShow", function() onShow(this) end)

		activeElements[object] = {}
		for element in next, elements do
			object:EnableElement(element, objectUnit)
		end

		for _, func in next, callback do
			func(object)
		end

		-- Make Clique kinda happy
		_G.ClickCastFrames = ClickCastFrames or {}
		ClickCastFrames[object] = true
	end
end

local function walkObject(object, unit)
	local parent = object:GetParent()
	local style = parent.style or style
	local styleFunc = styles[style]

	local header = parent.headerType and parent

	-- Check if we should leave the main frame blank.
	if(object.onlyProcessChildren) then
		object.hasChildren = true
		object:SetScript("OnAttributeChanged", onAttributeChanged)
		return initObject(unit, style, styleFunc, header, object:GetChildren())
	end

	return initObject(unit, style, styleFunc, header, object, object:GetChildren())
end

--[[ oUF:RegisterInitCallback(func)
Used to add a function to a table to be executed upon unit frame/header initialization.

* self - the global oUF object
* func - function to be added
--]]
function oUF:RegisterInitCallback(func)
	insert(callback, func)
end

--[[ oUF:RegisterMetaFunction(name, func)
Used to make a (table of) function(s) available to all unit frames.

* self - the global oUF object
* name - unique name of the function (string)
* func - function or a table of functions (function or table)
--]]
function oUF:RegisterMetaFunction(name, func)
	argcheck(name, 2, "string")
	argcheck(func, 3, "function", "table")

	if(frame_metatable.__index[name]) then
		return
	end

	frame_metatable.__index[name] = func
end

--[[ oUF:RegisterStyle(name, func)
Used to register a style with oUF. This will also set the active style if it hasn"t been set yet.

* self - the global oUF object
* name - name of the style
* func - function(s) defining the style (function or table)
--]]
function oUF:RegisterStyle(name, func)
	argcheck(name, 2, "string")
	argcheck(func, 3, "function", "table")

	if(styles[name]) then return error("Style [%s] already registered.", name) end
	if(not style) then style = name end

	styles[name] = func
end

--[[ oUF:SetActiveStyle(name)
Used to set the active style.

* self - the global oUF object
* name - name of the style (string)
--]]
function oUF:SetActiveStyle(name)
	argcheck(name, 2, "string")
	if(not styles[name]) then return error("Style [%s] does not exist.", name) end

	style = name
end

do
	local function iter(_, n)
		-- don"t expose the style functions.
		return (next(styles, n))
	end

	--[[ oUF:IterateStyles()
	Returns an iterator over all registered styles.

	* self - the global oUF object
	--]]
	function oUF.IterateStyles()
		return iter, nil, nil
	end
end

local getCondition
do
	local conditions = {
		raid40 = "[target=raid26,exists] show;",
		raid25 = "[target=raid11,exists] show;",
		raid10 = "[target=raid6,exists] show;",
		raid = "[group:raid] show;",
		party = "[group:party,nogroup:raid] show;",
		solo = "[target=player,exists,nogroup:party] show;",
	}

	function getCondition(...)
		local cond = ""

		for i = 1, getn(arg) do
			local short = select(i, unpack(arg))

			local condition = conditions[short]
			if(condition) then
				cond = cond .. condition
			end
		end

		return cond .. "hide"
	end
end

local function generateName(unit, ...)
	local name = "oUF_" .. gsub(style, gsub("^oUF_?", ""), gsub("[^%a%d_]+", ""))

	local raid, party, groupFilter
	for i = 1, getn(arg), 2 do
		local att, val = select(i, unpack(arg))
		if(att == "showRaid") then
			raid = true
		elseif(att == "showParty") then
			party = true
		elseif(att == "groupFilter") then
			groupFilter = val
		end
	end

	local append
	if(raid) then
		if(groupFilter) then
			if(type(groupFilter) == "number" and groupFilter > 0) then
				append = groupFilter
			elseif(match(groupFilter, "TANK")) then
				append = "MainTank"
			elseif(match(groupFilter, "ASSIST")) then
				append = "MainAssist"
			else
				local _, count = gsub(groupFilter, ",", "")
				if(count == 0) then
					append = "Raid" .. groupFilter
				else
					append = "Raid"
				end
			end
		else
			append = "Raid"
		end
	elseif(party) then
		append = "Party"
	elseif(unit) then
		append = gsub(unit, "^%l", upper)
	end

	if(append) then
		name = name .. append
	end

	local base = name
	local i = 2
	while(_G[name]) do
		name = base .. i
		i = i + 1
	end

	return name
end

do
	local function styleProxy(self, frame, ...)
		return walkObject(_G[frame])
	end

	-- There has to be an easier way to do this.
	local initialConfigFunction = function(self)
		local header = self:GetParent()

		local unit
		if(not self.onlyProcessChildren) then
			local groupFilter = header:GetAttribute("groupFilter")

			if(header:GetAttribute("showRaid")) then
				unit = "raid"
			elseif(header:GetAttribute("showParty")) then
				unit = "party"
			end

			self.menu = togglemenu
			self:SetAttribute("type1", "target")
			self:SetAttribute("type2", "menu")
			self.guessUnit = unit
			self.unit = "player"
		--	self.onlyProcessChildren =true
		end

		styleProxy(nil, self:GetName())
	end

	-- local setAttribute = function(self, name, value)
	-- 	if self.attributes[name] ~= value then
	-- 		self.attributes[name] = value

	-- 		if self:IsVisible() then
	-- 			SecureGroupHeader_Update(self)
	-- 		end
	-- 	end
	-- end
	-- local getAttribute = function(self, name)
	-- 	return self.attributes[name]
	-- end
	--[[ oUF:SpawnHeader(overrideName, template, visibility, ...)
	Used to create a group header and apply the currently active style to it.

	* self         - the global oUF object
	* overrideName - unique global name to be used for the header. Defaults to an auto-generated name based on the name
	                 of the active style and other arguments passed to `:SpawnHeader` (string?)
	* template     - name of a template to be used for creating the header. Defaults to `"oUF_GroupHeaderTemplate"`
	                 (string?)
	* visibility   - macro conditional(s) which define when to display the header (string).
	* ...          - further argument pairs. Consult [Group Headers](http://wowprogramming.com/docs/secure_template/Group_Headers)
	                 for possible values.

	In addition to the standard group headers, oUF implements some of its own attributes. These can be supplied by the
	layout, but are optional.

	* oUF-initialConfigFunction - can contain code that will be securely run at the end of the initial secure
	                              configuration (string?)
	* oUF-onlyProcessChildren   - can be used to force headers to only process children (boolean?)
	--]]
	function oUF:SpawnHeader(overrideName, template, visibility, ...)
		if(not style) then return error("Unable to create frame. No styles have been registered.") end

		template = (template or "oUF_GroupHeaderTemplate")

		local isPetHeader = match(template, "PetHeader")
		local name = overrideName or generateName(nil, unpack(arg))
		local header = CreateFrame("Frame", name, UIParent, template)
		header:Hide()

		header.attributes = {}
		-- header.SetAttribute = SetAttribute
		-- header.GetAttribute = GetAttribute

		header:SetAttribute("template", "SecureUnitButtonTemplate")
		for i = 1, getn(arg), 2 do
			local att, val = select(i, unpack(arg))
			if(not att) then break end

			header:SetAttribute(att, val)
		end

		header.style = style
		header.styleFunction = styleProxy
		header.visibility = visibility

		-- Expose the header through oUF.headers.
		insert(headers, header)

		header.initialConfigFunction = initialConfigFunction
		header.headerType = isPetHeader and "pet" or "group"

		if(header:GetAttribute("showParty")) then
			self:DisableBlizzard("party")
		end


		if(visibility) then
			local type, list = split(" ", visibility, 2)
			if(list and type == "custom") then
				RegisterStateDriver(header, "visibility", list)
				header.visibility = list
			else
				local condition = getCondition(split(",", visibility))
				RegisterStateDriver(header, "visibility", condition)
				header.visibility = condition
			end
		end

		return header
	end
end

--[[ oUF:Spawn(unit, overrideName)
Used to create a single unit frame and apply the currently active style to it.

* self         - the global oUF object
* unit         - the frame"s unit (string)
* overrideName - unique global name to use for the unit frame. Defaults to an auto-generated name based on the unit
                 (string?)
--]]
function oUF:Spawn(unit, overrideName)
	argcheck(unit, 2, "string")
	if(not style) then return error("Unable to create frame. No styles have been registered.") end

	unit = lower(unit)

	local name = overrideName or generateName(unit)
	local object = CreateFrame("Button", name, UIParent)
	object:Hide()
	Private.UpdateUnits(object, unit)

	self:DisableBlizzard(unit)
	units[unit] = object
	walkObject(object, unit)

	object.unit = unit

	return object
end

--[[ oUF:AddElement(name, update, enable, disable)
Used to register an element with oUF.

* self    - the global oUF object
* name    - unique name of the element (string)
* update  - used to update the element (function?)
* enable  - used to enable the element for a given unit frame and unit (function?)
* disable - used to disable the element for a given unit frame (function?)
--]]
function oUF:AddElement(name, update, enable, disable)
	argcheck(name, 2, "string")
	argcheck(update, 3, "function", "nil")
	argcheck(enable, 4, "function", "nil")
	argcheck(disable, 5, "function", "nil")

	if(elements[name]) then return error("Element [%s] is already registered.", name) end
	elements[name] = {
		update = update;
		enable = enable;
		disable = disable;
	}
end

--[[ oUF.units
Array containing all unit frames with existing units created by oUF:Spawn.
--]]
oUF.units = units
--[[ oUF.objects
Array containing all unit frames created by `oUF:Spawn`.
--]]
oUF.objects = objects
--[[ oUF.headers
Array containing all group headers created by `oUF:SpawnHeader`.
--]]
oUF.headers = headers