--[[
This library is a custom version of LibActionButton-1.0, originally by
Hendrik 'nevcairiel' Leppkes (h.leppkes@gmail.com), used in Bartender4. 
https://www.curseforge.com/wow/addons/libactionbutton-1-0

This version is heavily modified for ConsolePort, using a separate button
registry in case other action bar addons are simultaneously loaded.
Do not copy or use this library for anything else.
]]

local MAJOR_VERSION = 'CPActionButton';
local MINOR_VERSION = 1;

if not LibStub then error(MAJOR_VERSION .. ' requires LibStub.') end
local lib = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
if not lib then return end

local _, db = ...;

-- Lua functions
local type, error, tostring, tonumber, assert, select = type, error, tostring, tonumber, assert, select
local setmetatable, wipe, unpack, pairs, next = setmetatable, wipe, unpack, pairs, next
local str_match, format, tinsert, tremove = string.match, format, tinsert, tremove

local WoW10 = select(4, GetBuildInfo()) >= 100000

-- Libs
local LBG = LibStub('CPButtonGlow');

lib.eventFrame = lib.eventFrame or CreateFrame('Frame')
lib.eventFrame:UnregisterAllEvents()

lib.buttonRegistry = lib.buttonRegistry or {}
lib.activeButtons = lib.activeButtons or {}
lib.actionButtons = lib.actionButtons or {}
lib.nonActionButtons = lib.nonActionButtons or {}

lib.ChargeCooldowns = lib.ChargeCooldowns or {}
lib.NumChargeCooldowns = lib.NumChargeCooldowns or 0

lib.ACTION_HIGHLIGHT_MARKS = lib.ACTION_HIGHLIGHT_MARKS or setmetatable({}, { __index = ACTION_HIGHLIGHT_MARKS })

-- Meta function map
local Generic 	= CreateFrame('CheckButton')
local Action 	= setmetatable({}, {__index = Generic})
local Spell 	= setmetatable({}, {__index = Generic})
local Item 		= setmetatable({}, {__index = Generic})
local Macro 	= setmetatable({}, {__index = Generic})
local Toy       = setmetatable({}, {__index = Generic})
local Pet       = setmetatable({}, {__index = Generic})
local Custom 	= setmetatable({}, {__index = Generic})

local Generic_MT = {__index = Generic}
local Action_MT  = {__index = Action}
local Spell_MT 	 = {__index = Spell}
local Item_MT 	 = {__index = Item}
local Macro_MT 	 = {__index = Macro}
local Toy_MT     = {__index = Toy}
local Pet_MT     = {__index = Pet}
local Custom_MT  = {__index = Custom}

local type_meta_map = {
	empty  = Generic_MT,
	action = Action_MT,
	spell  = Spell_MT,
	item   = Item_MT,
	macro  = Macro_MT,
	toy    = Toy_MT,
	pet    = Pet_MT,
	custom = Custom_MT
}

lib.TypeMetaMap = type_meta_map;

local ButtonRegistry, ActiveButtons, ActionButtons, NonActionButtons = lib.buttonRegistry, lib.activeButtons, lib.actionButtons, lib.nonActionButtons

-- Local functions
local Update, UpdateButtonState, UpdateUsable, UpdateCount, UpdateCooldown, UpdateTooltip, UpdateNewAction, UpdatePage
local StartFlash, StopFlash, UpdateFlash, UpdateRangeTimer, UpdateOverlayGlow
local ShowGrid, HideGrid, SetupSecureSnippets, WrapOnClick
local ShowOverlayGlow, HideOverlayGlow
local EndChargeCooldown

local InitializeEventHandler, OnEvent, ForAllButtons, OnUpdate

local DefaultConfig = {
	tooltip = 'enabled',
	showGrid = true,
	colors = {
		range = { 0.8, 0.1, 0.1 },
		coold = { 0.5, 0.5, 0.5 },
		mana = { 0.5, 0.5, 1.0 }
	},
	hideElements = {
		macro = false,
		equipped = false,
	},
	keyBoundTarget = false,
	clickOnDown = false,
	flyoutDirection = 'UP',
}

--- Create a new action button.
-- @param id Internal id of the button
-- @param name Name of the button frame to be created
-- @param header Header that drives these action buttons (if any)
function lib:CreateButton(id, name, header, config, xml)
	local templates = xml or 'SecureActionButtonTemplate, SecureHandlerEnterLeaveTemplate, CPUIActionButtonTemplate'
	return self:InitButton(CreateFrame('CheckButton', name, header, templates), id, header)
end

function lib:InitButton(button, id, header)
	button = setmetatable(button, Generic_MT)
	button:RegisterForDrag('LeftButton', 'RightButton')

	-- Frame Scripts
	button:HookScript('OnEnter', Generic.OnEnter)
	button:HookScript('OnLeave', Generic.OnLeave)
	button:HookScript('PreClick', Generic.PreClick)
	button:HookScript('PostClick', Generic.PostClick)

	if WoW10 then
		button:RegisterForClicks('AnyDown', 'AnyUp')
	else
		button:RegisterForClicks('AnyDown')
	end

	button.id = id
	button.header = header or button:GetParent()
	-- Mapping of state -> action
	button.state_types = {}
	button.state_actions = {}

	SetupSecureSnippets(button)
	WrapOnClick(button)

	-- adjust count/stack size
	button.Count:SetFont(button.Count:GetFont(), 16, 'OUTLINE')

	-- Store the button in the registry, needed for event and OnUpdate handling
	if not next(ButtonRegistry) then
		InitializeEventHandler()
	end
	ButtonRegistry[button] = true

	button:UpdateConfig(config)

	-- run an initial update
	button:UpdateAction()

	-- somewhat of a hack for the Flyout buttons to not error.
	button.action = 0

	return button
end

function SetupSecureSnippets(button)
	button:SetAttribute('_custom', Custom.RunCustom)
	-- secure UpdateState(self, state)
	-- update the type and action of the button based on the state
	button:SetAttribute('UpdateState', [[
		local state = ...
		local _type = type

		self:SetAttribute('state', state)
		local type, action = (self:GetAttribute(format('labtype-%s', state)) or 'empty'), self:GetAttribute(format('labaction-%s', state))

		self:SetAttribute('type', type)
		if type ~= 'empty' and type ~= 'custom' then
			local action_field = (type == 'pet') and 'action' or type
			self:SetAttribute(action_field, action)
			self:SetAttribute('action_field', action_field)
		end
		self:SetID((type == 'action' and _type(action) == 'number' and action <= 12 and action) or 0)
		if self:GetID() > 0 then
			self:CallMethod('ButtonContentsChanged', state, type, action + ((self:GetAttribute('actionpage') - 1) * 12))
		end
		if IsPressHoldReleaseSpell then
			local pressAndHold = false
			if type == "action" then
				self:SetAttribute("typerelease", "actionrelease")
				local actionType, id = GetActionInfo(action)
				if actionType == "spell" then
					pressAndHold = IsPressHoldReleaseSpell(id)
				elseif actionType == "macro" then
					-- GetMacroSpell is not in the restricted environment
					--[=[
						local spellID = GetMacroSpell(id)
						if spellID then
							pressAndHold = IsPressHoldReleaseSpell(spellID)
						end
					]=]
				end
			elseif type == "spell" then
				self:SetAttribute("typerelease", nil)
				-- XXX: while we can query this attribute, there is no corresponding action to release a spell button, only "actionrelease" exists
				pressAndHold = IsPressHoldReleaseSpell(action)
			else
				self:SetAttribute("typerelease", nil)
			end

			self:SetAttribute("pressAndHoldAction", pressAndHold)
		end
		local onStateChanged = self:GetAttribute('OnStateChanged')
		if onStateChanged then
			self:Run(onStateChanged, state, type, action)
		end
	]])

	button:SetAttribute('actionpage', 1)
	button:SetAttribute('useparent-unit', true)

	-- this function is invoked by the header when the state changes
	button:SetAttribute('_childupdate-state', [[
		self:RunAttribute('UpdateState', message)
		self:CallMethod('UpdateAction')
	]])

	button:SetAttribute('_childupdate-actionpage', [[
		self:SetAttribute('actionpage', message)
		if self:GetID() > 0 then
			local state = self:GetAttribute('state')
			local kind, value = (self:GetAttribute(format('labtype-%s', state)) or 'empty'), self:GetAttribute(format('labaction-%s', state))
			self:CallMethod('ButtonContentsChanged', state, kind, value + ((message - 1) * 12))
		end
	]])

	button:SetAttribute('_childupdate-hover', [[
		self:CallMethod('Hover', message)
	]])

	button:SetAttribute('_onenter', [[
		self:CallMethod('SetClicks', true)
	]])

	button:SetAttribute('_onleave', [[
		self:CallMethod('SetClicks', false)
	]])

	-- secure PickupButton(self, kind, value, ...)
	-- utility function to place a object on the cursor
	button:SetAttribute('PickupButton', [[
		local kind, value = ...
		if kind == 'empty' then
			return 'clear'
		elseif kind == 'action' or kind == 'pet' then
			local actionType = (kind == 'pet') and 'petaction' or kind
			return actionType, value
		elseif kind == 'spell' or kind == 'item' or kind == 'macro' then
			return 'clear', kind, value
		else
			print('ActionButton: Unknown type: ' .. tostring(kind))
			return false
		end
	]])

	button:SetAttribute('OnDragStart', [[
		if self:GetAttribute('disableDragNDrop') then return false end
		local state = self:GetAttribute('state')
		local type = self:GetAttribute('type')
		-- if the button is empty, we can't drag anything off it
		if type == 'empty' or type == 'custom' then
			return false
		end
		-- Get the value for the action attribute
		local action_field = self:GetAttribute('action_field')
		local action = self:GetAttribute(action_field)

		-- non-action fields need to change their type to empty
		if type ~= 'action' and type ~= 'pet' then
			self:SetAttribute(format('labtype-%s', state), 'empty')
			self:SetAttribute(format('labaction-%s', state), nil)
			-- update internal state
			self:RunAttribute('UpdateState', state)
			-- send a notification to the insecure code
			self:CallMethod('ButtonContentsChanged', state, 'empty', nil)
		end			
		if self:GetID() > 0 then
			action = action + ((self:GetAttribute('actionpage') - 1) * 12)
		end
		-- return the button contents for pickup
		return self:RunAttribute('PickupButton', type, action)
	]])

	button:SetAttribute('OnReceiveDrag', [[
		if self:GetAttribute('disableDragNDrop') then return false end
		local kind, value, subtype, extra = ...
		if not kind or not value then return false end
		local state = self:GetAttribute('state')
		local buttonType, buttonAction = self:GetAttribute('type'), nil
		if buttonType == 'custom' then
			self:CallMethod('OnReceiveDragCustom', ...)
			return false
		end
		-- action buttons can do their magic themself
		-- for all other buttons, we'll need to update the content now
		if buttonType ~= 'action' and buttonType ~= 'pet' then
			-- with 'spell' types, the 4th value contains the actual spell id
			if kind == 'spell' then
				if extra then
					value = extra
				else
					print('no spell id?', ...)
				end
			elseif kind == 'item' and value then
				value = format('item:%d', value)
			end

			-- Get the action that was on the button before
			if buttonType ~= 'empty' then
				buttonAction = self:GetAttribute(self:GetAttribute('action_field'))
			end

			-- TODO: validate what kind of action is being fed in here
			-- We can only use a handful of the possible things on the cursor
			-- return false for all those we can't put on buttons

			self:SetAttribute(format('labtype-%s', state), kind)
			self:SetAttribute(format('labaction-%s', state), value)
			-- update internal state
			self:RunAttribute('UpdateState', state)
			-- send a notification to the insecure code
			self:CallMethod('ButtonContentsChanged', state, kind, value)
		else
			-- get the action for (pet-)action buttons
			buttonAction = self:GetAttribute('action')
			if self:GetID() > 0 then
				buttonAction = buttonAction + ((self:GetAttribute('actionpage') - 1) * 12)
			end
		end
		return self:RunAttribute('PickupButton', buttonType, buttonAction)
	]])

	button:SetScript('OnDragStart', nil)
	-- Wrapped OnDragStart(self, button, kind, value, ...)
	button.header:WrapScript(button, 'OnDragStart', [[
		return self:RunAttribute('OnDragStart')
	]])
	-- Wrap twice, because the post-script is not run when the pre-script causes a pickup (doh)
	-- we also need some phony message, or it won't work =/
	button.header:WrapScript(button, 'OnDragStart', [[
		return 'message', 'update'
	]], [[
		self:RunAttribute('UpdateState', self:GetAttribute('state'))
	]])

	button:SetScript('OnReceiveDrag', nil)
	-- Wrapped OnReceiveDrag(self, button, kind, value, ...)
	button.header:WrapScript(button, 'OnReceiveDrag', [[
		return self:RunAttribute('OnReceiveDrag', kind, value, ...)
	]])
	-- Wrap twice, because the post-script is not run when the pre-script causes a pickup (doh)
	-- we also need some phony message, or it won't work =/
	button.header:WrapScript(button, 'OnReceiveDrag', [[
		return 'message', 'update'
	]], [[
		self:RunAttribute('UpdateState', self:GetAttribute('state'))
	]])
end

function WrapOnClick(button)
	-- Wrap OnClick, to catch changes to actions that are applied with a click on the button.
	button.header:WrapScript(button, 'OnClick', [[
		if down then
			if self:GetAttribute('type') == 'action' then
				local type, action = GetActionInfo(self:GetAttribute('action'))
				return nil, format('%s|%s', tostring(type), tostring(action))
			end
		end
	]], [[
		if down then
			local type, action = GetActionInfo(self:GetAttribute('action'))
			if message ~= format('%s|%s', tostring(type), tostring(action)) then
				self:RunAttribute('UpdateState', self:GetAttribute('state'))
			end
		end
	]])
end

-----------------------------------------------------------
--- utility

function lib:GetAllButtons()
	local buttons = {}
	for button in next, ButtonRegistry do
		buttons[button] = true
	end
	return buttons
end

function Generic:ClearSetPoint(...)
	self:ClearAllPoints()
	self:SetPoint(...)
end

function Generic:NewHeader(header)
	self.header = header
	self:SetParent(header)
	SetupSecureSnippets(self)
	WrapOnClick(self)
end


-----------------------------------------------------------
--- state management

function Generic:ClearStates()
	for state in pairs(self.state_types) do
		self:SetAttribute(format('labtype-%s', state), nil)
		self:SetAttribute(format('labaction-%s', state), nil)
	end
	wipe(self.state_types)
	wipe(self.state_actions)
end

function Generic:SetState(state, kind, action)
	if not state then state = self:GetAttribute('state') end
	state = tostring(state)
	-- we allow a nil kind for setting a empty state
	if not kind then kind = 'empty' end
	if not type_meta_map[kind] then
		error('SetStateAction: unknown action type: ' .. tostring(kind), 2)
	end
	if kind ~= 'empty' and action == nil then
		error('SetStateAction: an action is required for non-empty states', 2)
	end
	if kind ~= 'custom' and action ~= nil and type(action) ~= 'number' and type(action) ~= 'string' or (kind == 'custom' and type(action) ~= 'table') then
		error('SetStateAction: invalid action data type, only strings and numbers allowed', 2)
	end

	if kind == 'item' then
		if tonumber(action) then
			action = format('item:%s', action)
		else
			local itemString = str_match(action, '^|c%x+|H(item[%d:]+)|h%[')
			if itemString then
				action = itemString
			end
		end
	end

	self.state_types[state] = kind
	self.state_actions[state] = action
	self:UpdateState(state)
end

function Generic:UpdateState(state)
	if not state then state = self:GetAttribute('state') end
	state = tostring(state)
	self:SetAttribute(format('labtype-%s', state), self.state_types[state])
	self:SetAttribute(format('labaction-%s', state), self.state_actions[state])
	if state ~= tostring(self:GetAttribute('state')) then return end
	if self.header then
		self.header:SetFrameRef('updateButton', self)
		self.header:Execute([[
			local frame = self:GetFrameRef('updateButton')
			control:RunFor(frame, frame:GetAttribute('UpdateState'), frame:GetAttribute('state'))
		]])
	else
	-- TODO
	end
	self:UpdateAction()
end

function Generic:GetAction(state)
	if not state then state = self:GetAttribute('state') end
	state = tostring(state)
	return self.state_types[state] or 'empty', self.state_actions[state]
end

function Generic:UpdateAllStates()
	for state in pairs(self.state_types) do
		self:UpdateState(state)
	end
end

function Generic:ButtonContentsChanged(state, kind, value)
	state = tostring(state)
	self.state_types[state] = kind or 'empty'
	self.state_actions[state] = value
	self:UpdateAction(self)
end

function Generic:DisableDragNDrop(flag)
	if InCombatLockdown() then
		error('LibActionButton-CP: You can only toggle DragNDrop out of combat!', 2)
	end
	if flag then
		self:SetAttribute('disableDragNDrop', true)
	else
		self:SetAttribute('disableDragNDrop', nil)
	end
end

function Generic:UpdateAlpha()
	UpdateCooldown(self)
end

function Generic:OnReceiveDragCustom(...)
	--print('test', ...)
end

-----------------------------------------------------------
--- frame scripts

-- copied (and adjusted) from SecureHandlers.lua
local function PickupAny(kind, target, detail, ...)
	if kind == 'clear' then
		ClearCursor()
		kind, target, detail = target, detail, ...
	end

	if kind == 'action' then
		PickupAction(target)
	elseif kind == 'item' then
		PickupItem(target)
	elseif kind == 'macro' then
		PickupMacro(target)
	elseif kind == 'petaction' then
		PickupPetAction(target)
	elseif kind == 'spell' then
		PickupSpell(target)
	elseif kind == 'companion' then
		PickupCompanion(target, detail)
	elseif kind == 'equipmentset' then
		PickupEquipmentSet(target)
	end
end

function Generic:OnEnter()
	if self.header.FadeIn then
		self.header:FadeIn(self.header:GetAlpha())
	end
	self:FadeIn()
	if self.config.tooltip ~= 'disabled' and (self.config.tooltip ~= 'nocombat' or not InCombatLockdown()) then
		UpdateTooltip(self)
	end

	if self._state_type == 'action' and self.NewActionTexture then
		lib.ACTION_HIGHLIGHT_MARKS[self._state_action] = false
		UpdateNewAction(self)
	end
end

function Generic:OnLeave()
	if self.header:GetAttribute('hidesafe') and not InCombatLockdown() then
		self.header:FadeOut(self.header:GetAlpha())
	end
	self:FadeOut()
	GameTooltip:Hide()
end

-- Insecure drag handler to allow clicking on the button with an action on the cursor
-- to place it on the button. Like action buttons work.
function Generic:PreClick()
	if self._state_type == 'action' or self._state_type == 'pet'
	   or InCombatLockdown() or self:GetAttribute('disableDragNDrop')
	then
		return
	end
	-- check if there is actually something on the cursor
	local kind, value = GetCursorInfo()
	if not (kind and value) then return end
	self._old_type = self._state_type
	if self._state_type and self._state_type ~= 'empty' then
		self._old_type = self._state_type
		self:SetAttribute('type', 'empty')
		--self:SetState(nil, 'empty', nil)
	end
	self._receiving_drag = true
end

local function formatHelper(input)
	if type(input) == 'string' then
		return format('%q', input)
	else
		return tostring(input)
	end
end

function Generic:PostClick(button)
	UpdateButtonState(self)
	if self._receiving_drag and not InCombatLockdown() then
		if self._old_type then
			self:SetAttribute('type', self._old_type)
			self._old_type = nil
		end
		local oldType, oldAction = self._state_type, self._state_action
		local kind, data, subtype, extra = GetCursorInfo()
		self.header:SetFrameRef('updateButton', self)
		self.header:Execute(format([[
			local frame = self:GetFrameRef('updateButton')
			control:RunFor(frame, frame:GetAttribute('OnReceiveDrag'), %s, %s, %s, %s)
			control:RunFor(frame, frame:GetAttribute('UpdateState'), %s)
		]], formatHelper(kind), formatHelper(data), formatHelper(subtype), formatHelper(extra), formatHelper(self:GetAttribute('state'))))
		PickupAny('clear', oldType, oldAction)
	end
	self._receiving_drag = nil
end

-----------------------------------------------------------
--- configuration

local function merge(target, source, default)
	for k,v in pairs(default) do
		if type(v) ~= 'table' then
			if source and source[k] ~= nil then
				target[k] = source[k]
			else
				target[k] = v
			end
		else
			if type(target[k]) ~= 'table' then target[k] = {} else wipe(target[k]) end
			merge(target[k], type(source) == 'table' and source[k], v)
		end
	end
	return target
end

function Generic:UpdateConfig(config)
	if config and type(config) ~= 'table' then
		error('LibActionButton-CP: UpdateConfig requires a valid configuration!', 2)
	end
	if not self.config then self.config = {} end
	-- merge the two configs
	merge(self.config, config, DefaultConfig)

	UpdateUsable(self)

	if self.config.hideElements.macro then
		self.Name:Hide()
	else
		self.Name:Show()
	end

	self:SetAttribute('flyoutDirection', self.config.flyoutDirection)

	Update(self)
end

-----------------------------------------------------------
--- event handler

function ForAllButtons(method, onlyWithAction)
	assert(type(method) == 'function')
	for button in next, (onlyWithAction and ActiveButtons or ButtonRegistry) do
		method(button)
	end
end

function InitializeEventHandler()
	local eventFrame = lib.eventFrame
	eventFrame:SetScript('OnEvent', OnEvent)
	--------------------------------
	for _, event in ipairs({
		----------------------------
	--	'ACTIONBAR_PAGE_CHANGED',
	--	'UPDATE_BONUS_ACTIONBAR',
	--	'UPDATE_SHAPESHIFT_FORM',
		'ACTIONBAR_HIDEGRID',
		'ACTIONBAR_SHOWGRID',
		'ACTIONBAR_SLOT_CHANGED',
		'ACTIONBAR_UPDATE_COOLDOWN',
		'ACTIONBAR_UPDATE_STATE',
		'ACTIONBAR_UPDATE_USABLE',

		'ARCHAEOLOGY_CLOSED',
		'BAG_UPDATE_DELAYED',
		'COMPANION_UPDATE',
		'LEARNED_SPELL_IN_TAB',

		'LOSS_OF_CONTROL_ADDED',
		'LOSS_OF_CONTROL_UPDATE',

		'PET_BAR_HIDEGRID',
		'PET_BAR_UPDATE',
		'PET_BAR_UPDATE_COOLDOWN',

		'PET_STABLE_SHOW',
		'PET_STABLE_UPDATE',

		'PLAYER_ENTER_COMBAT',
		'PLAYER_ENTERING_WORLD',
		'PLAYER_EQUIPMENT_CHANGED',
		'PLAYER_LEAVE_COMBAT',
		'PLAYER_TARGET_CHANGED',

		'SPELL_ACTIVATION_OVERLAY_GLOW_HIDE',
		'SPELL_ACTIVATION_OVERLAY_GLOW_SHOW',

		'SPELL_UPDATE_CHARGES',
		'SPELL_UPDATE_COOLDOWN',
		'SPELL_UPDATE_USABLE',

		'SPELLS_CHANGED',

		'START_AUTOREPEAT_SPELL',
		'STOP_AUTOREPEAT_SPELL',

		'TRADE_SKILL_CLOSE',
		'TRADE_SKILL_SHOW',

		'UNIT_ENTERED_VEHICLE',
		'UNIT_EXITED_VEHICLE',
		'UNIT_INVENTORY_CHANGED',

		'UPDATE_BINDINGS',
		'UPDATE_SUMMONPETS_ACTION',
		'UPDATE_VEHICLE_ACTIONBAR',
		----------------------------
	}) do pcall(eventFrame.RegisterEvent, eventFrame, event) end
	--------------------------------

	eventFrame:Show()
	eventFrame:SetScript('OnUpdate', OnUpdate)
end

function OnEvent(_, event, arg1, ...)
	if (event == 'UNIT_INVENTORY_CHANGED' and arg1 == 'player') or event == 'LEARNED_SPELL_IN_TAB' then
		local tooltipOwner = GameTooltip:GetOwner()
		if ButtonRegistry[tooltipOwner] then
			tooltipOwner:SetTooltip()
		end
	elseif event == 'ACTIONBAR_SLOT_CHANGED' then
		for button in next, ButtonRegistry do
			if button._state_type == 'action' and (arg1 == 0 or arg1 == tonumber(button._state_action)) then
				Update(button)
			end
		end
	elseif event == 'PLAYER_ENTERING_WORLD' or event == 'UPDATE_SHAPESHIFT_FORM' or event == 'UPDATE_VEHICLE_ACTIONBAR' then
		ForAllButtons(Update)
	elseif event == 'ACTIONBAR_SHOWGRID' then
		ShowGrid()
	elseif event == 'ACTIONBAR_HIDEGRID' or event == 'PET_BAR_HIDEGRID' then
		HideGrid()
	elseif event == 'PLAYER_TARGET_CHANGED' then
		UpdateRangeTimer()
	elseif (event == 'ACTIONBAR_UPDATE_STATE') or
		((event == 'UNIT_ENTERED_VEHICLE' or event == 'UNIT_EXITED_VEHICLE') and (arg1 == 'player')) or
		((event == 'COMPANION_UPDATE') and (arg1 == 'MOUNT')) then
		ForAllButtons(UpdateButtonState, true)
	elseif event == 'ACTIONBAR_UPDATE_USABLE' then
		for button in next, ActionButtons do
			UpdateUsable(button)
		end
	elseif event == 'SPELL_UPDATE_USABLE' then
		for button in next, NonActionButtons do
			UpdateUsable(button)
		end
	elseif event == 'ACTIONBAR_UPDATE_COOLDOWN' then
		for button in next, ActionButtons do
			UpdateCooldown(button)
			if GameTooltip:GetOwner() == button then
				UpdateTooltip(button)
			end
		end
	elseif event == 'SPELL_UPDATE_COOLDOWN' then
		for button in next, NonActionButtons do
			UpdateCooldown(button)
			if GameTooltip:GetOwner() == button then
				UpdateTooltip(button)
			end
		end
	elseif event == 'LOSS_OF_CONTROL_ADDED' then
		for button in next, ActiveButtons do
			UpdateCooldown(button)
			if GameTooltip:GetOwner() == button then
				UpdateTooltip(button)
			end
		end
	elseif event == 'LOSS_OF_CONTROL_UPDATE' then
		for button in next, ActiveButtons do
			UpdateCooldown(button)
		end
	elseif event == 'TRADE_SKILL_SHOW' or event == 'TRADE_SKILL_CLOSE'  or event == 'ARCHAEOLOGY_CLOSED' then
		ForAllButtons(UpdateButtonState, true)
	elseif event == 'PLAYER_ENTER_COMBAT' then
		for button in next, ActiveButtons do
			if button:IsAttack() then
				StartFlash(button)
			end
		end
	elseif event == 'PLAYER_LEAVE_COMBAT' then
		for button in next, ActiveButtons do
			if button:IsAttack() then
				StopFlash(button)
			end
		end
	elseif event == 'START_AUTOREPEAT_SPELL' then
		for button in next, ActiveButtons do
			if button:IsAutoRepeat() then
				StartFlash(button)
			end
		end
	elseif event == 'STOP_AUTOREPEAT_SPELL' then
		for button in next, ActiveButtons do
			if button.flashing == 1 and not button:IsAttack() then
				StopFlash(button)
			end
		end
	elseif event == 'PET_STABLE_UPDATE' or event == 'PET_STABLE_SHOW' or event == 'SPELLS_CHANGED' then
		ForAllButtons(Update)
	elseif event == 'PET_BAR_UPDATE' then
		for button in next, NonActionButtons do
			Update(button)
		end
	elseif event == 'PET_BAR_UPDATE_COOLDOWN' then
		for button in next, NonActionButtons do
			UpdateCooldown(button)
		end
	elseif event == 'SPELL_ACTIVATION_OVERLAY_GLOW_SHOW' then
		for button in next, ActiveButtons do
			local spellId = button:GetSpellId()
			if spellId and spellId == arg1 then
				if not button.isMainButton then
					button:SetGlowing(true, true)
				end
				ShowOverlayGlow(button)
			else
				if button._state_type == 'action' then
					local actionType, id = GetActionInfo(button._state_action)
					if actionType == 'flyout' and FlyoutHasSpell(id, arg1) then
						button:SetGlowing(true, true)
						ShowOverlayGlow(button)
					end
				end
			end
		end
	elseif event == 'SPELL_ACTIVATION_OVERLAY_GLOW_HIDE' then
		for button in next, ActiveButtons do
			local spellId = button:GetSpellId()
			if spellId and spellId == arg1 then
				HideOverlayGlow(button)
				if not button.isMainButton then
					button:SetGlowing(false)
					UpdateCooldown(button)
				end
			else
				if button._state_type == 'action' then
					local actionType, id = GetActionInfo(button._state_action)
					if actionType == 'flyout' and FlyoutHasSpell(id, arg1) then
						button:SetGlowing(false)
						HideOverlayGlow(button)
					end
				end
			end
		end
	elseif event == 'PLAYER_EQUIPMENT_CHANGED' then
		for button in next, ActiveButtons do
			if button._state_type == 'item' then
				Update(button)
			end
		end
	elseif event == 'SPELL_UPDATE_CHARGES' or event == 'BAG_UPDATE_DELAYED' then
		ForAllButtons(UpdateCount, true)
	elseif event == 'UPDATE_SUMMONPETS_ACTION' then
		for button in next, ActiveButtons do
			if button._state_type == 'action' then
				local actionType, id = GetActionInfo(button._state_action)
				if actionType == 'summonpet' then
					local texture = GetActionTexture(button._state_action)
					if texture then
						button.icon:SetTexture(texture)
						if button.mainIcon then
							button.mainIcon:SetTexture(texture)
						end
					end
				end
			end
		end
	end
end

local flashTime = 0
local rangeTimer = -1
function OnUpdate(_, elapsed)
	flashTime = flashTime - elapsed
	rangeTimer = rangeTimer - elapsed
	-- Run the loop only when there is something to update
	if rangeTimer <= 0 or flashTime <= 0 then
		for button in next, ActiveButtons do
			-- Flashing
			if button.flashing == 1 and flashTime <= 0 then
				if button.Flash:IsShown() then
					button.Flash:Hide()
				else
					button.Flash:Show()
				end
			end

			-- Range
			if rangeTimer <= 0 then
				local inRange = button:IsInRange()
				local oldRange = button.outOfRange
				button.outOfRange = (inRange == false)
				if oldRange ~= button.outOfRange then
					UpdateUsable(button)
				end
			end
		end

		-- Update values
		if flashTime <= 0 then
			flashTime = flashTime + ATTACK_BUTTON_FLASH_TIME
		end
		if rangeTimer <= 0 then
			rangeTimer = TOOLTIP_UPDATE_TIME
		end
	end
end

function ShowGrid()
	for button in next, ButtonRegistry do
		if button:IsShown() then
			button:SetShowGrid(true)
		end
	end
end

function HideGrid()
	for button in next, ButtonRegistry do
		button:SetShowGrid(false)
	end
end

-----------------------------------------------------------
--- button management

function Generic:SetClicks(mouseover)
	if WoW10 then return end
	if mouseover then
		self:RegisterForClicks('AnyUp')
	else
		self:RegisterForClicks('AnyDown')
	end
end

function Generic:Hover(isEnabled)
	self.forceShow = isEnabled
	if self.isMainButton then return end
	if isEnabled then
		self:FadeIn(1)
	else
		self:FadeOut(0)
	end
end

function Generic:SetShowGrid(isEnabled)
	if self.isMainButton then return end
	self.showGrid = isEnabled
	if isEnabled then
		self:FadeIn(1, 0)
	else
		self:FadeOut(0, 0)
	end
end

function Generic:SetGlowing(isEnabled, instantAlpha)
	self.isGlowing = isEnabled
	if self.isMainButton then return end
	if isEnabled then
		self:FadeIn(1, instantAlpha and 0 or 0.2)
	else
		self:FadeOut(0, instantAlpha and 0 or 0.2) 
	end
end

function Generic:SetOnCooldown(isEnabled, instantAlpha)
	self.isOnCooldown = isEnabled
	if self.isMainButton then return end
	if isEnabled then
		self:FadeIn(1, instantAlpha and 0 or 0.2)
	else
		self:FadeOut(0, instantAlpha and 0 or 0.2) 
	end
end

function Generic:FadeIn(newAlpha, speed)
	db.Alpha.FadeIn(self, speed or 0.2, self:GetAlpha(), newAlpha or 1)
end

function Generic:FadeOut(newAlpha, speed)
	if not self.isMainButton and
		not self.isGlowing and
		not self.isOnCooldown and
		not self.forceShow and
		not self.showGrid then
		db.Alpha.FadeOut(self, speed or 0.2, self:GetAlpha(), newAlpha or 0)
	end
end

function Generic:UpdateAction(force)
	local type, action = self:GetAction()
	if force or type ~= self._state_type or action ~= self._state_action then
		-- type changed, update the metatable
		if force or self._state_type ~= type then
			local meta = type_meta_map[type] or type_meta_map.empty
			setmetatable(self, meta)
			self._state_type = type
		end
		self._state_action = action
		Update(self)
	end
end



function Generic:UpdateFlyout()
	self.FlyoutBorder:Hide()
	if self._state_type == 'action' then
		-- based on ActionButton_UpdateFlyout in ActionButton.lua
		local actionType = GetActionInfo(self._state_action)
		if actionType == 'flyout' then
			
			--self.FlyoutArrow:Show()
			--self.FlyoutArrow:ClearAllPoints()
			
			--self.FlyoutArrow:SetPoint('CENTER', 0, self.isMainButton and -20 or -10)
			--SetClampedTextureRotation(self.FlyoutArrow, 180)
			return
		end
	end
	--self.FlyoutArrow:Hide()
end

function Update(self)
	if self:HasAction() then
		ActiveButtons[self] = true
		if self._state_type == 'action' then
			ActionButtons[self] = true
			NonActionButtons[self] = nil
		else
			ActionButtons[self] = nil
			NonActionButtons[self] = true
		end
		UpdateButtonState(self)
		UpdateUsable(self)
		UpdateCooldown(self)
		UpdateFlash(self)
		UpdatePage(self)
	else
		ActiveButtons[self] = nil
		ActionButtons[self] = nil
		NonActionButtons[self] = nil
		self.cooldown:Hide()
		self:SetChecked(false)

		self.isGlowing = false
		self.isOnCooldown = false
		self:FadeOut()
		UpdateUsable(self)
		UpdatePage(self)

		if self.chargeCooldown then
			EndChargeCooldown(self.chargeCooldown)
		end
	end

	-- Add a green border if button is an equipped item
	if self:IsEquipped() and not self.config.hideElements.equipped then
		self.Border:SetVertexColor(0, 1.0, 0, 0.35)
		self.Border:Show()
	else
		self.Border:Hide()
	end

	-- Update Action Text
	if self.isMainButton and not self:IsConsumableOrStackable() then
		self.Name:SetText(self:GetActionText())
	else
		self.Name:SetText('')
	end

	-- Update icon and hotkey
	local texture, preventTextureUpdate = self:GetTexture()

	-- Draenor zone button handling
	self.draenorZoneDisabled = false
	if self._state_type == 'action' then
		local action_type, id = GetActionInfo(self._state_action)
		if ((action_type == 'spell' or action_type == 'companion') and DraenorZoneAbilityFrame and DraenorZoneAbilityFrame.baseName and not HasDraenorZoneAbility()) then
			local name = GetSpellInfo(DraenorZoneAbilityFrame.baseName)
			local abilityName = GetSpellInfo(id)
			if name == abilityName then
				texture = GetLastDraenorSpellTexture()
				self.draenorZoneDisabled = true
			end
		end
	end

	if texture then
		self.rangeTimer = - 1
	else
		self.cooldown:Hide()
		self.rangeTimer = nil
	end

	if not preventTextureUpdate then
		self:SetIcon(texture)
	end

	self:UpdateFlyout()
	UpdateCount(self)
	UpdateOverlayGlow(self)
	UpdateNewAction(self)

	if GameTooltip:GetOwner() == self then
		UpdateTooltip(self)
	end

	-- this could've been a spec change, need to call OnStateChanged for action buttons, if present
	if not InCombatLockdown() and self._state_type == 'action' then
		local onStateChanged = self:GetAttribute('OnStateChanged')
		if onStateChanged then
			self.header:SetFrameRef('updateButton', self)
			self.header:Execute(([[
				local frame = self:GetFrameRef('updateButton')
				control:RunFor(frame, frame:GetAttribute('OnStateChanged'), %s, %s, %s)
			]]):format(formatHelper(self:GetAttribute('state')), formatHelper(self._state_type), formatHelper(self._state_action)))
		end
	end
end

function UpdateButtonState(self)
	if self:IsCurrentlyActive() or self:IsAutoRepeat() then
		self:SetChecked(true)
	else
		self:SetChecked(false)
	end
end

function UpdateUsable(self)
	-- TODO: make the colors configurable
	-- TODO: allow disabling of the whole recoloring
	if self.outOfRange then
		self:SetVertexColor(unpack(self.config.colors.range))
	else
		local isUsable, notEnoughMana = self:IsUsable()
		if isUsable then
			self:ClearVertexColor()
			--self.NormalTexture:SetVertexColor(1.0, 1.0, 1.0)
		elseif notEnoughMana then
			self:SetVertexColor(unpack(self.config.colors.mana))
			--self.NormalTexture:SetVertexColor(0.5, 0.5, 1.0)
		else
			self:SetVertexColor(0.4, 0.4, 0.4)
			--self.NormalTexture:SetVertexColor(1.0, 1.0, 1.0)
		end
	end
end

function UpdateCount(self)
	if not self:HasAction() then
		self.Count:SetText('')
		return
	end
	if self:IsConsumableOrStackable() then
		local count = self:GetCount()
		if count > (self.maxDisplayCount or 9999) then
			self.Count:SetText('*')
		else
			self.Count:SetText(count)
		end
	else
		local charges, maxCharges, chargeStart, chargeDuration = self:GetCharges()
		if charges and maxCharges and maxCharges > 0 then
			self.Count:SetText(charges)
		else
			self.Count:SetText('')
		end
	end
end

function UpdatePage(self)
	if self:GetAction() == 'action' then
		local page = self:GetAttribute('actionpage')
		local action = self:GetAttribute('action')
		if (page and action) and (page > 1) and (page < 7) then
			if action <= NUM_ACTIONBAR_BUTTONS then
				self.Page:Show()
				self.Page:SetText(page)
				return
			end
		end
	end
	self.Page:Hide()
end

function EndChargeCooldown(self)
	self:Hide()
	self:SetParent(UIParent)
	self.parent.chargeCooldown = nil
	self.parent = nil
	tinsert(lib.ChargeCooldowns, self)
end

local function StartChargeCooldown(parent, chargeStart, chargeDuration)
	if not parent.chargeCooldown then
		local cooldown = tremove(lib.ChargeCooldowns)
		if not cooldown then
			lib.NumChargeCooldowns = lib.NumChargeCooldowns + 1
			cooldown = CreateFrame('Cooldown', '$parentChargeCooldown', parent, 'CooldownFrameTemplate')
			cooldown:SetScript('OnCooldownDone', EndChargeCooldown)
			cooldown:SetHideCountdownNumbers(true)
			cooldown:SetEdgeTexture(CPAPI.GetAsset('Textures\\Cooldown\\Edge'))
			cooldown:SetBlingTexture(CPAPI.GetAsset('Textures\\Cooldown\\Bling'))
			cooldown:SetSwipeTexture(CPAPI.GetAsset('Textures\\Cooldown\\Charge'))
			cooldown:SetDrawEdge(true)
			cooldown:SetDrawSwipe(true)
		end
		cooldown:SetParent(parent)
		cooldown:SetAllPoints(parent)
		cooldown:Show()
		parent.chargeCooldown = cooldown
		cooldown.parent = parent
	end
	parent.chargeCooldown:SetDrawBling(parent.chargeCooldown:GetEffectiveAlpha() > 0.5)
	parent.chargeCooldown:SetCooldown(chargeStart, chargeDuration)
	if not chargeStart or chargeStart == 0 then
		EndChargeCooldown(parent.chargeCooldown)
	end
end

local function OnCooldownDone(self)
	local button = self:GetParent()
	button:SetOnCooldown(false)
	self:SetScript('OnCooldownDone', nil)
	UpdateCooldown(button)
end

local function OnModifierCooldownDone(self)
	local button = self:GetParent()
	button:SetOnCooldown(false)
	self:SetScript('OnCooldownDone', nil)
end

function UpdateCooldown(self)
	local locStart, locDuration = self:GetLossOfControlCooldown()
	local start, duration, enable = self:GetCooldown()
	local charges, maxCharges, chargeStart, chargeDuration = self:GetCharges()

	self.cooldown:SetDrawBling(self.cooldown:GetEffectiveAlpha() > 0.5)

	if not self.isMainButton and not self.isGlowing then
		if (duration > 2) then
			self:SetOnCooldown(true)
			self.cooldown:SetSwipeColor(0.17, 0, 0)
			self.cooldown:SetScript('OnCooldownDone', OnModifierCooldownDone)
		end
	end

	if (locStart + locDuration) > (start + duration) then
		if self.cooldown.currentCooldownType ~= COOLDOWN_TYPE_LOSS_OF_CONTROL then
			self.cooldown:SetHideCountdownNumbers(true)
			self.cooldown.currentCooldownType = COOLDOWN_TYPE_LOSS_OF_CONTROL
		end
		CooldownFrame_Set(self.cooldown, locStart, locDuration, true, true)
	else
		if self.cooldown.currentCooldownType ~= COOLDOWN_TYPE_NORMAL then
			self.cooldown:SetHideCountdownNumbers(false)
			self.cooldown.currentCooldownType = COOLDOWN_TYPE_NORMAL
		end
		if locStart > 0 then
			self.cooldown:SetScript('OnCooldownDone', OnCooldownDone)
		end

		if charges and maxCharges and maxCharges > 0 and charges > 0 and charges < maxCharges then
			StartChargeCooldown(self, chargeStart, chargeDuration)
		elseif self.chargeCooldown then
			EndChargeCooldown(self.chargeCooldown)
		end
		CooldownFrame_Set(self.cooldown, start, duration, enable)
	end
end

function StartFlash(self)
	self.flashing = 1
	flashTime = 0
	UpdateButtonState(self)
end

function StopFlash(self)
	self.flashing = 0
	self.Flash:Hide()
	UpdateButtonState(self)
end

function UpdateFlash(self)
	if (self:IsAttack() and self:IsCurrentlyActive()) or self:IsAutoRepeat() then
		StartFlash(self)
	else
		StopFlash(self)
	end
end

function UpdateTooltip(self)
	if (GetCVar('UberTooltips') == '1') then
		GameTooltip_SetDefaultAnchor(GameTooltip, self);
	else
		GameTooltip:SetOwner(self, 'ANCHOR_RIGHT');
	end
	if self:SetTooltip() then
		self.UpdateTooltip = UpdateTooltip
	else
		self.UpdateTooltip = nil
	end
--	local currentModifier = self.isMainButton and ConsolePort:GetCurrentModifier() or self.mod
--	local bindingString = ConsolePort:GetFormattedButtonCombination(self.plainID, currentModifier, 20, true)
--	if bindingString and GameTooltip:IsVisible() then
--		local title = GameTooltipTextRight1
--		title:SetText(bindingString)
--		title:Show()
--		GameTooltip:Show()
--	end
end

function ShowOverlayGlow(self)
	LBG.ShowOverlayGlow(self)
end

function HideOverlayGlow(self)
	LBG.HideOverlayGlow(self)
end

function UpdateOverlayGlow(self)
	local spellId = self:GetSpellId()
	if spellId and CPAPI.IsSpellOverlayed(spellId) then
		ShowOverlayGlow(self)
	else
		HideOverlayGlow(self)
	end
end

hooksecurefunc('MarkNewActionHighlight', function(action, flag)
	lib.ACTION_HIGHLIGHT_MARKS[action] = flag
	for button in next, ButtonRegistry do
		if button._state_type == 'action' and action == tonumber(button._state_action) then
			UpdateNewAction(button)
		end
	end
end)

function UpdateNewAction(self)
	-- special handling for 'New Action' markers
	if self.NewActionTexture then
		if self._state_type == 'action' and lib.ACTION_HIGHLIGHT_MARKS[self._state_action] then
			self.NewActionTexture:Show()
			db.Alpha.FadeOut(self.NewActionTexture, 10, 1, 0)
		else
			self.NewActionTexture:Hide()
		end
	end
end

-- Hook UpdateFlyout so we can use the blizzy templates
do local function UpdateFlyoutHook(self, ...)
		if ButtonRegistry[self] then
			self:UpdateFlyout()
		end
	end

	if ActionButton_UpdateFlyOut then
		hooksecurefunc('ActionButton_UpdateFlyout', UpdateFlyoutHook)
	elseif ActionBarActionButtonMixin and ActionBarActionButtonMixin.UpdateFlyout then
		hooksecurefunc(ActionBarActionButtonMixin, 'UpdateFlyout', UpdateFlyoutHook)
	end
end

function UpdateRangeTimer()
	rangeTimer = -1
end

-----------------------------------------------------------
--- WoW API mapping
--- Generic Button
Generic.HasAction               = nop;
Generic.GetActionText           = function(self) return '' end
Generic.GetTexture              = nop;
Generic.GetCharges              = nop;
Generic.GetCount                = function(self) return 0 end
Generic.GetCooldown             = function(self) return 0, 0, 0 end
Generic.IsAttack                = nop;
Generic.IsEquipped              = nop;
Generic.IsCurrentlyActive       = nop;
Generic.IsAutoRepeat            = nop;
Generic.IsUsable                = nop;
Generic.IsConsumableOrStackable = nop;
Generic.IsUnitInRange           = nop;
Generic.IsInRange               = function(self)
	local unit = self:GetAttribute('unit')
	if unit == 'player' then
		unit = nil
	end
	local val = self:IsUnitInRange(unit)
	-- map 1/0 to true false, since the return values are inconsistent between actions and spells
	if val == 1 then val = true elseif val == 0 then val = false end
	return val
end
Generic.SetTooltip              = nop;
Generic.GetSpellId              = nop;
Generic.GetLossOfControlCooldown = function(self) return 0, 0 end

-----------------------------------------------------------
--- Action Button
Action.HasAction               = function(self) return HasAction(self._state_action) end
Action.GetActionText           = function(self) return GetActionText(self._state_action) end
Action.GetTexture              = function(self) return GetActionTexture(self._state_action) end
Action.GetCharges              = function(self) return GetActionCharges(self._state_action) end
Action.GetCount                = function(self) return GetActionCount(self._state_action) end
Action.GetCooldown             = function(self) return GetActionCooldown(self._state_action) end
Action.IsAttack                = function(self) return IsAttackAction(self._state_action) end
Action.IsEquipped              = function(self) return IsEquippedAction(self._state_action) end
Action.IsCurrentlyActive       = function(self) return IsCurrentAction(self._state_action) end
Action.IsAutoRepeat            = function(self) return IsAutoRepeatAction(self._state_action) end
Action.IsUsable                = function(self) return IsUsableAction(self._state_action) end
Action.IsConsumableOrStackable = function(self) return IsConsumableAction(self._state_action) or IsStackableAction(self._state_action) or (not IsItemAction(self._state_action) and GetActionCount(self._state_action) > 0) end
Action.IsUnitInRange           = function(self, unit) return IsActionInRange(self._state_action, unit) end
Action.SetTooltip              = function(self) return GameTooltip:SetAction(self._state_action) end
Action.GetSpellId              = function(self)
	local actionType, id, subType = GetActionInfo(self._state_action)
	if actionType == 'spell' then
		return id
	elseif actionType == 'macro' then
		local _, _, spellId = GetMacroSpell(id)
		return spellId
	end
end
Action.GetLossOfControlCooldown = function(self) return GetActionLossOfControlCooldown(self._state_action) end

-----------------------------------------------------------
--- Spell Button
local function getSpellId(input)
	return tonumber(input) or (select(7, GetSpellInfo(input)))
end

local function getSpellInfo(func, spellID, ...)
	spellID = getSpellId(spellID)
	if tonumber(spellID) then
		local slot = FindSpellBookSlotBySpellID(spellID, 'spell')
		return slot and func(slot, ...)
	end
end

Spell.HasAction               = function(self) return true end
Spell.GetActionText           = function(self) return (GetSpellInfo(self._state_action)) end
Spell.GetTexture              = function(self) return (GetSpellTexture(self._state_action)) end
Spell.GetCharges              = function(self) return GetSpellCharges(self._state_action) end
Spell.GetCount                = function(self) return GetSpellCount(self._state_action) end
Spell.GetCooldown             = function(self) return GetSpellCooldown(self._state_action) end
Spell.IsAttack                = function(self) return getSpellInfo(IsAttackSpell, self._state_action) end
Spell.IsCurrentlyActive       = function(self) return IsCurrentSpell(self._state_action) end
Spell.IsAutoRepeat            = function(self) return getSpellInfo(IsAutoRepeatSpell, self._state_action) end
Spell.IsUsable                = function(self) return IsUsableSpell(self._state_action) end
Spell.IsConsumableOrStackable = function(self) return IsConsumableSpell(self._state_action) end
Spell.IsUnitInRange           = function(self, unit) return getSpellInfo(IsSpellInRange, self._state_action, unit) end
Spell.SetTooltip              = function(self) return GameTooltip:SetSpellByID(getSpellId(self._state_action)) end
Spell.GetSpellId              = function(self) return getSpellId(self._state_action) end

-----------------------------------------------------------
--- Item Button
local function getItemId(input)
	return input:match('^item:(%d+)')
end

Item.HasAction               = function(self) return true end
Item.GetActionText           = function(self) return (GetItemInfo(self._state_action)) end
Item.GetTexture              = function(self) return GetItemIcon(self._state_action) end
Item.GetCount                = function(self) return GetItemCount(self._state_action, nil, true) end
Item.GetCooldown             = function(self) return GetItemCooldown(getItemId(self._state_action)) end
Item.IsEquipped              = function(self) return IsEquippedItem(self._state_action) end
Item.IsCurrentlyActive       = function(self) return IsCurrentItem(self._state_action) end
Item.IsUsable                = function(self) return IsUsableItem(self._state_action) end
Item.IsConsumableOrStackable = function(self) return IsConsumableItem(self._state_action) end
Item.IsUnitInRange           = function(self, unit) return IsItemInRange(self._state_action, unit) end
Item.SetTooltip              = function(self) return GameTooltip:SetHyperlink(self._state_action) end

-----------------------------------------------------------
--- Macro Button
-- TODO: map results of GetMacroSpell/GetMacroItem to proper results
Macro.HasAction              = function(self) return true end
Macro.GetActionText          = function(self) return (GetMacroInfo(self._state_action)) end
Macro.GetTexture             = function(self) return (select(2, GetMacroInfo(self._state_action))) end
Macro.IsUsable               = function(self) return true end

-----------------------------------------------------------
--- Toy Button
Toy.HasAction                = function(self) return true end
Toy.GetTexture               = function(self) return select(3, C_ToyBox.GetToyInfo(self._state_action)) end
Toy.GetCooldown              = function(self) return GetItemCooldown(self._state_action) end
Toy.IsUnitInRange            = function(self, unit) return nil end
Toy.SetTooltip               = function(self) return GameTooltip:SetToyByItemID(self._state_action) end

-----------------------------------------------------------
--- Pet Button
Pet.HasAction                = function(self) return true end
Pet.GetTexture               = function(self)
	local texture, isToken = select(2, GetPetActionInfo(self._state_action))
	return isToken and _G[texture] or texture;
end
Pet.GetCooldown              = function(self) return GetPetActionCooldown(self._state_action) end
Pet.IsAttack                 = function(self) return IsPetAttackAction(self._state_action) end
Pet.IsCurrentlyActive        = function(self) return select(4, GetPetActionInfo(self._state_action)) end
Pet.IsAutoRepeat             = function(self) return select(6, GetPetActionInfo(self._state_action)) end
Pet.IsUsable                 = function(self) return GetPetActionSlotUsable(self._state_action) end
Pet.SetTooltip               = function(self) return GameTooltip:SetPetAction(self._state_action) end

-----------------------------------------------------------
--- Custom Button
Custom.HasAction             = function(self) return true end
Custom.GetActionText         = function(self) return self._state_action.text end
Custom.GetTexture            = function(self)
	local texture = self._state_action.texture
	if type(texture) == 'function' then
		return texture(self.icon, self._state_action), true
	end
	return texture
end
Custom.IsUsable              = function(self) return true end
Custom.SetTooltip            = function(self) return GameTooltip:SetText(self._state_action.tooltip) end
Custom.RunCustom             = function(self, unit, button) return self._state_action.func(self, unit, button) end

-----------------------------------------------------------