--[[

	This file is part of 'Masque', an add-on for World of Warcraft. For bug reports,
	suggestions and license information, please visit https://github.com/SFX-WoW/Masque.

	* File...: Core\Group.lua
	* Author.: StormFX, JJSheets

	Group API

]]

local MASQUE, Core = ...

----------------------------------------
-- Lua API
---

local error, pairs, select, type = error, pairs, select, type

----------------------------------------
-- Internal
---

-- @ Skins\Skins
local Skins = Core.Skins

-- @ Skins\Blizzard_*
local DEFAULT_SKIN = Core.DEFAULT_SKIN

-- @ Skins\Regions
local ActionTypes, RegTypes = Core.ActionTypes, Core.RegTypes

-- @ Core\Utility
local GetColor, GetScale = Core.GetColor, Core.GetScale

-- @ Core\Core
local GetRegion, GetType = Core.GetRegion, Core.GetType

-- @ Core\Button
local SkinButton = Core.SkinButton

-- @ Core\Regions\*
local SetPulse, SetTextureColor = Core.SetPulse, Core.SetTextureColor

----------------------------------------
-- Locals
---

-- Group Tables
local Group, GMT = {}, {}

-- layers with Color Options
local C_Layers = {
	Backdrop = true,
	Checked = true,
	Cooldown = true,
	Flash = true,
	Gloss = true,
	Highlight = true,
	Normal = true,
	Pushed = true,
	Shadow = true,
}

----------------------------------------
-- Group Metatable
---

-- Adds or reassigns a button to the group.
function GMT:AddButton(Button, Regions, Type, Strict)
	local oType, bType = GetType(Button, Type)

	if not oType then
		if Core.Debug then
			error("Bad argument to group method 'AddButton'. 'Button' must be a Button, CheckButton or Frame object.", 2)
		end
		return
	elseif oType == "Frame" then
		Strict = true
	end

	Regions = Regions or Button.__Regions

	local Parent = Group[Button]

	if Parent then
		if Parent == self then
			return
		else
			Regions = Regions or Parent.Buttons[Button]
			Parent.Buttons[Button] = nil
		end
	end

	Group[Button] = self

	if ActionTypes[bType] then
		self.ActionButtons = true
	end

	if type(Regions) ~= "table" then
		Regions = {}
	end

	if not Strict then
		local Layers = RegTypes[bType]

		for Layer, Info in pairs(Layers) do
			local Region = Regions[Layer]

			if Region == nil and not Info.Ignore then
				if Layer == "AutoCastShine" then
					Region = Regions.Shine or Regions.AutoCast or GetRegion(Button, Info)
				elseif Layer == "Backdrop" then
					Region = Regions.FloatingBG or GetRegion(Button, Info)
				else
					Region = GetRegion(Button, Info)
				end

				Regions[Layer] = Region
			end
		end
	end

	self.Buttons[Button] = Regions
	Button.__Regions = Regions
	Button.__MSQ_Addon = self.Addon

	local db = self.db

	if not self.Queued then
		if Parent and not Parent.db.Disabled and db.Disabled then
			SkinButton(Button, Regions, false)
		elseif not db.Disabled then
			SkinButton(Button, Regions, db.SkinID, db.Backdrop, db.Shadow, db.Gloss, db.Colors, db.Scale, db.Pulse)
		end
	end
end

-- Deletes the group and applies the default skin to its buttons.
function GMT:Delete()
	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:Delete()
		end
	end

	for Button in pairs(self.Buttons) do
		self:RemoveButton(Button)
	end

	local Parent = self.Parent

	if Parent then
		Parent.SubList[self.ID] = nil
	end

	Core:UpdateSkinOptions(self, true)
	Core.Groups[self.ID] = nil
end

-- Returns a layer's current color.
function GMT:GetColor(Layer)
	if Layer then
		local Skin = Skins[self.db.SkinID] or DEFAULT_SKIN
		return GetColor(self.db.Colors[Layer] or Skin[Layer].Color)
	end
end

-- Returns a button region.
function GMT:GetLayer(Button, Layer)
	if Button and Layer then
		local Regions = self.Buttons[Button]

		if Regions then
			return Regions[Layer]
		end
	end
end

-- Creates and returns an AceConfig-3.0 options table for the group.
function GMT:GetOptions(Order)
	return Core.GetOptions(self, Order)
end

-- Registers a group-specific callback.
function GMT:RegisterCallback(func, ...)
	if self.ID == MASQUE then return end

	if type(func) ~= "function" then
		if Core.Debug then
			error("Bad argument to Group method 'RegisterCallback'. 'func' must be a function.", 2)
		end
		return
	end

	local Count = select("#", ...)

	if Count > 0 then
		local cbs = self.Callbacks

		for i = 1, Count do
			local arg = select(i, ...)

			if i == 1 and type(arg) == "table" then
				self.__arg = arg

				if Count == 1 then
					self.__func = func
				end
			elseif type(arg) == "string" then
				cbs[arg] = func
			end
		end
	else
		self.__func = func
	end
end

-- Removes a button from the group and applies the default skin.
function GMT:RemoveButton(Button)
	if Button then
		local Regions = self.Buttons[Button]

		if Regions and not self.db.Disabled then
			SkinButton(Button, Regions, false)
		end

		Group[Button] = nil
		self.Buttons[Button] = nil
	end
end

-- Reskins the group with its current settings.
function GMT:ReSkin(Button)
	local db = self.db

	if not db.Disabled then
		if type(Button) == "table" then
			local Regions = self.Buttons[Button]

			if Regions then
				SkinButton(Button, Regions, db.SkinID, db.Backdrop, db.Shadow, db.Gloss, db.Colors, db.Scale, db.Pulse)
			end
		else
			local SkinID, Backdrop, Shadow = db.SkinID, db.Backdrop, db.Shadow
			local Gloss, Colors, Pulse = db.Gloss, db.Colors, db.Pulse

			for Button, Regions in pairs(self.Buttons) do
				SkinButton(Button, Regions, SkinID, Backdrop, Shadow, Gloss, Colors, db.Scale, Pulse)
			end
		end
	end
end

-- Renames the group.
function GMT:SetName(Name)
	if not self.StaticID then
		return
	elseif type(Name) ~= "string" or self.ID == MASQUE then
		if Core.Debug then
			error("Bad argument to group method 'SetName'. 'Name' must be a string.", 2)
		end
		return
	end

	self.Group = Name
	Core:UpdateSkinOptions(self)
end

----------------------------------------
-- Internal Methods
---

-- Disables the group.
-- * This methods is intended for internal use only.
function GMT:__Disable(Silent)
	self.db.Disabled = true
	self.db.Scale = 1
	self.db.UseScale = false

	for Button, Regions in pairs(self.Buttons) do
		SkinButton(Button, Regions, false)
	end

	if not Silent then
		self:__FireCB("Disabled", true)
	end

	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:__Disable(Silent)
		end
	end
end

-- Enables the group.
-- * This methods is intended for internal use only.
function GMT:__Enable()
	self.db.Disabled = false
	self:ReSkin()
	self:__FireCB("Disabled", false)

	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:__Enable()
		end
	end
end

-- Fires the callback.
-- * This methods is intended for internal use only.
function GMT:__FireCB(Option, Value)
	local arg = self.__arg
	local func = self.Callbacks[Option] or self.__func

	if func then
		if arg then
			func(arg, self, Option, Value)
		else
			func(self, Option, Value)
		end
	end
end

-- Resets the group's skin settings.
-- * This methods is intended for internal use only.
function GMT:__Reset()
	self.db.Backdrop = false
	self.db.Shadow = false
	self.db.Gloss = false
	self.db.Pulse = true
	self.db.Scale = 1
	self.db.UseScale = false

	for Layer in pairs(self.db.Colors) do
		self.db.Colors[Layer] = nil
	end

	self:ReSkin()
	self:__FireCB("Reset", true)

	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:__Reset()
		end
	end
end

-- Validates and sets a skin option.
-- * This methods is intended for internal use only.
function GMT:__Set(Option, Value)
	if not Option then return end

	local db = self.db

	if Option == "SkinID" then
		if Value and Skins[Value] then
			db.SkinID = Value
		end

		self:ReSkin()
	elseif Option == "Scale" then
		db.Scale = Value or 1
		self:ReSkin()
	elseif db[Option] ~= nil then
		Value = (Value and true) or false
		db[Option] = Value

		if Option == "Pulse" then
			for Button in pairs(self.Buttons) do
				SetPulse(Button, Value)
			end
		elseif Option == "UseScale" and not Value then
			db.Scale = 1
			self:ReSkin()
		else
			local func = Core["Skin"..Option]

			if func then
				local Skin = Skins[db.SkinID] or DEFAULT_SKIN

				if Option == "Backdrop" then
					for Button, Regions in pairs(self.Buttons) do
						func(Value, Regions.Backdrop, Button, Skin.Backdrop, db.Colors.Backdrop, GetScale(Button))
					end
				else
					for Button in pairs(self.Buttons) do
						func(Value, Button, Skin[Option], db.Colors[Option], GetScale(Button))
					end
				end
			end
		end
	else
		return
	end

	self:__FireCB(Option, Value)

	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:__Set(Option, Value)
		end
	end
end

-- Sets the specified layer color.
-- * This methods is intended for internal use only.
function GMT:__SetColor(Layer, r, g, b, a)
	if not Layer then return end

	local db = self.db
	local Skin = Skins[db.SkinID] or DEFAULT_SKIN
	local sr, sg, sb, sa = GetColor(Skin[Layer].Color)

	-- Prevent saving the skin's default color.
	if r and ((r ~= sr) or (g ~= sg) or (b ~= sb) or (a ~= sa)) then
		db.Colors[Layer] = {r, g, b, a}
	else
		db.Colors[Layer] = nil
	end

	local func = Core["Set"..Layer.."Color"]

	if func then
		for Button, Regions in pairs(self.Buttons) do
			func(Regions[Layer], Button, Skin[Layer], db.Colors[Layer])
		end
	else
		for Button, Regions in pairs(self.Buttons) do
			SetTextureColor(Layer, Regions[Layer], Button, Skin[Layer], db.Colors[Layer])
		end
	end

	local Subs = self.SubList

	if Subs then
		for _, Sub in pairs(Subs) do
			Sub:__SetColor(Layer, r, g, b, a)
		end
	end
end

-- Updates the group on creation or profile activity.
-- * This methods is intended for internal use only.
function GMT:__Update(IsNew)
	local db_Groups = Core.db.profile.Groups
	local db = db_Groups[self.ID]

	if db == self.db then return end

	-- Update the DB the first time a StaticID is used.
	if self.StaticID and not db.Upgraded then
		local o_id = Core.GetID(self.Addon, self.Group)
		local o_db = db_Groups[o_id]

		if not o_db.Inherit then
			db_Groups[self.ID] = o_db
			db = db_Groups[self.ID]
		end

		db_Groups[o_id] = nil
		db.Upgraded = true
	end

	self.db = db

	-- Inheritance
	if self.Parent then
		local p_db = self.Parent.db

		if db.Inherit then
			local Options = {"SkinID", "Backdrop", "Shadow", "Gloss", "Pulse", "Scale", "UseScale"}

			for _, v in ipairs(Options) do
				db[v] = p_db[v]
			end

			local Colors = db.Colors
			local p_Colors = p_db.Colors

			for Layer in pairs(C_Layers) do
				local Color = Colors[Layer]
				local p_Color = p_Colors[Layer]

				if type(p_Color) == "table" then
					Color = Color or {}

					Color[1] = p_Color[1]
					Color[2] = p_Color[2]
					Color[3] = p_Color[3]
					Color[4] = p_Color[4]

					Colors[Layer] = Color
				else
					Colors[Layer] = nil
				end
			end

			db.Inherit = false
		end

		if p_db.Disabled then
			db.Disabled = true
		end
	end

	if IsNew then
		-- Queue the group if PLAYER_LOGIN hasn't fired and the skin hasn't loaded.
		if Core.Queue and not self.Queued and not Skins[db.SkinID] then
			Core.Queue(self)
		end

		Core:UpdateSkinOptions(self)
	else
		if db.Disabled then
			for Button, Regions in pairs(self.Buttons) do
				SkinButton(Button, Regions, false)
			end
		else
			self:ReSkin()
		end

		local Subs = self.SubList

		if Subs then
			for _, Sub in pairs(Subs) do
				Sub:__Update()
			end
		end
	end
end

----------------------------------------
-- Deprecated
---

-- Temporary function to catch add-ons using deprecated API.
function GMT:SetCallback()
	if self.ID == MASQUE then return end

	local Warn = Core.db.profile.CB_Warn
	local Addon = self.Addon

	if Addon and Warn[Addon] then
		print("|cffff8800Masque Warning:|r", Addon, "called the deprecated API method, |cff000099'SetCallback'|r.  Please notify the author or post in the relevant issue on the Masque project page.")
		Warn[Addon] = false
	end
end

----------------------------------------
-- Core
---

Core.Group_MT = {__index = GMT}
