local _, db = ...;
---------------------------------------------------------------
local Utility = Mixin(CPAPI.EventHandler(ConsolePortUtilityToggle, {
	'ACTIONBAR_SLOT_CHANGED';
	'BAG_UPDATE_DELAYED';
	'SPELLS_CHANGED';
	'QUEST_WATCH_UPDATE';
	'QUEST_WATCH_LIST_CHANGED';
	'UPDATE_BINDINGS';
	CPAPI.IsRetailVersion and 'UPDATE_EXTRA_ACTIONBAR';
}), CPAPI.AdvancedSecureMixin)
local Button = CreateFromMixins(CPActionButton);
---------------------------------------------------------------
local DEFAULT_SET, EXTRA_ACTION_ID = 1, CPAPI.ExtraActionButtonID;
---------------------------------------------------------------
Utility.Data = {[DEFAULT_SET] = {}};
Utility:Execute(([[
	DATA = newtable()
	TYPE = '%s';
]]):format(CPAPI.ActionTypeRelease))
---------------------------------------------------------------
db:Register('Utility', Utility)
db:Save('Utility/Data', 'ConsolePortUtility')

---------------------------------------------------------------
-- Secure environment
---------------------------------------------------------------
Utility:CreateEnvironment({
	-----------------------------------------------------------
	-- Draw current ring
	-----------------------------------------------------------
	DrawSelectedRing = [[
		local set = ...;
		RING = DATA[set];
		if not RING then
			return
		end

		local radius = math.sqrt(self:GetWidth() * self:GetHeight()) / 2;
		local numActive = #RING;
		local invertY = self:GetAttribute('axisInversion')

		self:SetAttribute('trigger', self::GetButtonsHeld())
		self:SetAttribute('state', set)
		control:ChildUpdate('state', set)

		for i, action in ipairs(RING) do
			local x, y = radial::GetPointForIndex(i, numActive, radius)
			local widget = self:GetFrameRef(set..':'..i)

			widget:Show()
			widget:ClearAllPoints()
			widget:SetPoint('CENTER', '$parent', 'CENTER', x, invertY * y)
		end
		for i=numActive+1, self:GetAttribute('numButtons') do
			self:GetFrameRef(tostring(i)):Hide()
		end
		self:SetAttribute('size', numActive)
	]];
	CopySelectedIndex = [[
		local index = ...;
		if not RING or not index then
			return self:CallMethod('ClearInstantly')
		end

		self:CallMethod('OnSelection', true)
		for attribute, value in pairs(RING[index]) do
			local convertedAttribute = (attribute == 'type') and TYPE or attribute;
			self:SetAttribute(convertedAttribute, value)
			self:CallMethod('OnSelectionAttributeAdded', convertedAttribute, value)
		end
		self:CallMethod('OnSelection', false)
	]];
	GetRingSetFromButton = ([[
		local button = ...;
		if DATA[button] then
			return button;
		end
		return tostring(%s);
	]]):format(DEFAULT_SET);
	-----------------------------------------------------------
	-- Set pre-defined remove binding
	-----------------------------------------------------------
	SetRemoveBinding = [[
		local enabled = ...;
		if enabled then
			local binding, trigger = self:GetAttribute('removeButton'), self:GetAttribute('trigger')
			if trigger and binding and trigger:match(binding) then
				self:SetAttribute('removeButtonBlocked', true)
				return self:ClearBindings()
			end

			self:SetAttribute('removeButtonBlocked', false)
			local mods = {self::GetModifiersHeld()}
			table.sort(mods)
			mods[#mods+1] = table.concat(mods)

			local removeWidget = self:GetFrameRef('Remove')
			for _, mod in ipairs(mods) do
				self:SetBindingClick(true, mod..binding, removeWidget)
			end
			self:SetBindingClick(true, binding, removeWidget)
		else
			self:ClearBindings()
		end
	]];
});

---------------------------------------------------------------
-- Trigger script
---------------------------------------------------------------
Utility:SetAttribute('pressAndHoldAction', true)
Utility:Wrap('PreClick', ([[
	local stickySelect = self:GetAttribute('stickySelect')

	if stickySelect then
		if down then
			self:SetAttribute('backup', self:GetAttribute('TYPE'))
			self:SetAttribute('TYPE', nil)
		else
			self:SetAttribute('TYPE', self:GetAttribute('backup'))
			self:SetAttribute('backup', nil)
		end
	else
		self:SetAttribute('TYPE', nil)
	end

	if down then
		local set = self::GetRingSetFromButton(button)
		self:CallMethod('CheckCursorInfo', set)
		self::DrawSelectedRing(set)
		self::SetRemoveBinding(true)
		self:Show()
	else
		self::CopySelectedIndex(self::GetIndex())
		self:ClearBindings()
		self:Hide()
	end
]]):gsub('TYPE', CPAPI.ActionTypeRelease))


---------------------------------------------------------------
-- Secure removal
---------------------------------------------------------------
Utility:SetFrameRef('Remove', Utility.Remove)
Utility:WrapScript(Utility.Remove, 'OnClick', [[
	local index = control:Run(GetIndex)
	local set = control:GetAttribute('state')
	if set and index then
		control:CallMethod('SafeRemoveAction', set, index)
		control:Run(DrawSelectedRing, set)
	end
]])

---------------------------------------------------------------
-- Data handling
---------------------------------------------------------------
function Utility:OnDataLoaded()
	self:SetAttribute('size', 0)
	self:CreateFramePool('SecureActionButtonTemplate, SecureHandlerEnterLeaveTemplate, CPUIActionButtonTemplate', Button)
	db:Load('Utility/Data', 'ConsolePortUtility')

	local sticks = db.Radial:GetStickStruct(db('radialPrimaryStick'))
	db.Radial:Register(self, 'UtilityRing', {
		sticks = sticks;
		target = {sticks[1]};
		sizer  = [[
			local size = self:GetAttribute('size');
		]];
	});
	self:RefreshAll()
	setmetatable(self.Data, {__index = function(data, key)
		self:Parse([[
			DATA[{ring}] = newtable();
		]], {ring = tostring(key)})
		return rawset(data, key, {})[key];
	end})
	self:OnAutoAssignedChanged()
	self:OnRemoveButtonChanged()
	self:OnAxisInversionChanged()
	self:OnStickySelectChanged()
	if CPAPI.IsRetailVersion then
		self.FocusOverlay.BgRunes:SetAtlas('heartofazeroth-orb-activated')
	else
		self.FocusOverlay.BgRunes:SetAtlas('ChallengeMode-RuneBG')
	end
end

function Utility:OnAutoAssignedChanged()
	self.autoAssignExtras = db('autoExtra')
end

function Utility:OnAxisInversionChanged()
	self.axisInversion = db('radialCosineDelta')
	self:SetAttribute('axisInversion', self.axisInversion)
end

function Utility:OnRemoveButtonChanged()
	self:SetAttribute('removeButton', db('radialRemoveButton'))
end

function Utility:OnPrimaryStickChanged()
	local sticks = db.Radial:GetStickStruct(db('radialPrimaryStick'))
	self:SetInterrupt(sticks)
	self:SetIntercept({sticks[1]})
end

function Utility:OnStickySelectChanged()
	self:SetAttribute('stickySelect', db('radialStickySelect'))
	self:SetAttribute(CPAPI.ActionTypeRelease, nil)
	self:SetAttribute('backup', nil)
end

function Utility:OnSizeChanged()
	local width, height = self:GetSize()
	self.FocusOverlay.BgRunes:SetSize(width * 0.8, height * 0.8)
end

db:RegisterSafeCallback('Settings/autoExtra', Utility.OnAutoAssignedChanged, Utility)
db:RegisterSafeCallback('Settings/radialCosineDelta', Utility.OnAxisInversionChanged, Utility)
db:RegisterSafeCallback('Settings/radialRemoveButton', Utility.OnRemoveButtonChanged, Utility)
db:RegisterSafeCallback('Settings/radialPrimaryStick', Utility.OnPrimaryStickChanged, Utility)
db:RegisterSafeCallback('Settings/radialStickySelect', Utility.OnStickySelectChanged, Utility)
Utility:SetScript('OnSizeChanged', Utility.OnSizeChanged)

---------------------------------------------------------------
-- Widget handling
---------------------------------------------------------------
function Utility:RefreshAll()
	self:ClearAllActions()
	local numButtons = 0;
	for setID, set in pairs(self.Data) do
		for i, action in ipairs(set) do
			self:AddSecureAction(setID, i, action)
		end
		numButtons = #set > numButtons and #set or numButtons;
	end
	self:SetAttribute('numButtons', numButtons)
end

function Utility:ClearAllActions()
	self:Execute('wipe(DATA)')
	for button in self:EnumerateActive() do
		button:ClearStates()
	end
	self:ReleaseAll()
end

function Utility:AddSecureAction(set, idx, info)
	local button, newObj = self:TryAcquireRegistered(idx)
	if newObj then
		button:SetFrameLevel(idx)
		button:SetID(idx)
		button:OnLoad()
		button:DisableDragNDrop(true)
		self:SetFrameRef(tostring(idx), button)
	end

	self:SetFrameRef(set..':'..idx, button)
	button:SetState(set, self:GetKindAndAction(info))

	local args, body = { ring = tostring(set), slot = idx }, [[
		local ring = DATA[{ring}];
		if not ring then
			DATA[{ring}] = newtable();
			ring = DATA[{ring}];
		end

		local slot = ring[{slot}];
		if not slot then
			ring[{slot}] = newtable();
			slot = ring[{slot}];
		end
	]];
	for key, value in pairs(info) do
		body = ('%s\n slot.%s = {%s}'):format(body, key, key)
		args[key] = value;
	end
	return self:Parse(body, args)
end

db:RegisterSafeCallback('OnRingCleared', Utility.RefreshAll, Utility)
db:RegisterSafeCallback('OnRingRemoved', Utility.RefreshAll, Utility)

---------------------------------------------------------------
-- Frontend
---------------------------------------------------------------
function Utility:OnInput(x, y, len, stick)
	local size = self:GetAttribute('size')
	local obj = self:SetFocusByIndex(self:GetIndexForPos(x, y, len, size))
	local rot = self:ReflectStickPosition(self.axisInversion * x, self.axisInversion * y, len, len > self:GetValidThreshold())
	self:SetAnimations(obj, rot, len)
end

function Utility:GetSetID(rawSetID)
	return tonumber(rawSetID) or rawSetID;
end

function Utility:ConvertBindingToDisplayName(binding)
	if ( type(binding) == 'string' ) then
		local name = binding:gsub('CLICK ConsolePortUtilityToggle:(.*)', '%1')
		return ( name ~= binding ) and
			((tonumber(name) and ('Ring |cFF00FFFF%s|r'):format(name) or name)) or nil;
	end
end

function Utility:GetBindingForSet(setID)
	return ('CLICK ConsolePortUtilityToggle:%s'):format(self:GetBindingSuffixForSet(setID));
end

function Utility:GetBindingSuffixForSet(setID)
	return (tonumber(setID) == DEFAULT_SET and 'LeftButton' or tostring(setID));
end

function Utility:GetButtonSlugForSet(setID)
	return db.Hotkeys:GetButtonSlugForBinding(self:GetBindingForSet(setID));
end

function Utility:GetBindingDisplayNameForSet(setID)
	return self:ConvertBindingToDisplayName(self:GetBindingForSet(setID));
end

function Utility:GetTooltipRemovePrompt()
	local removeButton = not self:GetAttribute('removeButtonBlocked') and self:GetAttribute('removeButton')
	local device = db('Gamepad/Active')
	if removeButton and device then
		return device:GetTooltipButtonPrompt(removeButton, REMOVE, 64)
	end
end

function Utility:GetTooltipUsePrompt()
	local useButton = self:GetAttribute('trigger')
	local device = db('Gamepad/Active')
	if useButton and device then
		return device:GetTooltipButtonPrompt(useButton, USE, 64)
	end
end

function Utility:OnSelection(running)
	if running then
		self.ReportData = {};
	else
		db:TriggerEvent('OnUtilityRingSelectionChanged', self.ReportData)
		self.ReportData = nil;
	end
end

function Utility:OnSelectionAttributeAdded(attribute, value)
	self.ReportData[attribute] = value;
end

---------------------------------------------------------------
-- Animations
---------------------------------------------------------------
do
	local map, Clamp = db.table.map, Clamp;

	local function replay(group)
		group:Stop()
		group:Finish()
		group:Play()
	end

	local function hide(obj) return obj:Hide() end
	local function show(obj) return obj:Show() end

	function Utility:SetAnimations(obj, rot, len)
		local overlay = self.FocusOverlay;
		local oldObj = self.oldAniObj;
		local pulse = Clamp(len, 0.05, 0.25)
		local glow = Clamp(len, 0, 1)

		overlay.PulseAnim.PulseIn:SetFromAlpha(pulse / 2)
		overlay.PulseAnim.PulseIn:SetToAlpha(pulse)
		overlay.PulseAnim.PulseOut:SetToAlpha(pulse / 2)
		overlay.PulseAnim.PulseOut:SetFromAlpha(pulse)

		if oldObj == obj then
			return
		end
		
		if oldObj then
			oldObj.Name:Show()
		end

		self.oldAniObj = obj;
		
		if obj then
			obj.Name:Hide()

			map(show,
				overlay.SlotGlow,
				overlay.RunesSmall,
				overlay.SmallRuneGlow,
				overlay.GlowBurstSmall
			);

			replay(overlay.RunesSmallAnim)

			overlay.GlowBurstSmall:SetPoint('TOPLEFT', obj, 'TOPLEFT', -50, 50)
			overlay.GlowBurstSmall:SetPoint('BOTTOMRIGHT', obj, 'BOTTOMRIGHT', 50, -50)

			overlay.RunesSmall:SetPoint('TOPLEFT', obj, 'TOPLEFT', -16, 16)
			overlay.RunesSmall:SetPoint('BOTTOMRIGHT', obj, 'BOTTOMRIGHT', 16, -16)

			overlay.SlotGlow:SetPoint('TOPLEFT', obj, 'TOPLEFT', -8, 8)
			overlay.SlotGlow:SetPoint('BOTTOMRIGHT', obj, 'BOTTOMRIGHT', 8, -8)
		else
			map(hide,
				overlay.SlotGlow,
				overlay.RunesSmall,
				overlay.SmallRuneGlow,
				overlay.GlowBurstSmall
			);
		end
	end

	Utility:HookScript('OnHide', function(self)
		self:SetAnimations(nil, 0, 0)
	end)
end

---------------------------------------------------------------
-- Set manager
---------------------------------------------------------------
function Utility:AddSavedVar(setID, idx, info)
	setID = setID or DEFAULT_SET;
	local set = self.Data[setID];
	local maxIndex = #set + 1;
	idx = Clamp(idx or maxIndex, 1, maxIndex)
	tinsert(set, idx, info)
end

function Utility:RemoveSavedVar(setID, idx)
	return tremove(self.Data[setID], idx)
end

function Utility:AddAction(setID, idx, info)
	self:AddSavedVar(setID, idx, info)
	self:RefreshAll()
	return true;
end

function Utility:RemoveAction(setID, idx)
	local action = self:RemoveSavedVar(setID, idx)
	self:RefreshAll()
	return action;
end

function Utility:SafeAddAction(setID, idx, ...)
	local info = {};
	for i=1, select('#', ...), 2 do
		info[select(i, ...)] = select(i + 1, ...);
	end
	self:AddSavedVar(self:GetSetID(setID), tonumber(idx), info)
end

function Utility:SafeRemoveAction(setID, idx)
	if not InCombatLockdown() then
		self:RemoveAction(self:GetSetID(setID), tonumber(idx))
	end
end

function Utility:ClearActionByAttribute(setID, key, value)
	local index = self:SearchActionByAttribute(setID, key, value)
	if index then
		return self:RemoveAction(setID, index)
	end
end

function Utility:ClearActionByKey(setID, key)
	local index = self:SearchActionByKey(setID, key)
	if index then
		return self:RemoveAction(setID, index)
	end
end

function Utility:SearchActionByAttribute(setID, key, value)
	local set = self.Data[setID];
	for i, action in ipairs(set) do
		for attribute, content in pairs(action) do
			if (attribute == key and content == value) then
				return i, set;
			end
		end
	end
end

function Utility:SearchActionByKey(setID, key)
	local set = self.Data[setID];
	for i, action in ipairs(set) do
		for attribute in pairs(action) do
			if (attribute == key) then
				return i, set;
			end
		end
	end
end

function Utility:IsUniqueAction(setID, info)
	local set = self.Data[setID];
	local cmp = db.table.compare;
	-- check if already existing on ring
	for i, action in ipairs(set) do
		if cmp(action, info) then
			return false, i;
		end
	end
	return true;
end

---------------------------------------------------------------
-- Announcements
---------------------------------------------------------------
Utility.Messages, Utility.MessageCount = {}, 0;

function Utility:AnnounceAddition(link, set, force)
	local slug = self:GetButtonSlugForSet(set or 'LeftButton')
	if self.MessageCount > 10 then
		wipe(self.Messages)
		self.MessageCount = 0;
	end
	local messageID = self:GetBindingSuffixForSet(set) .. link;
	local messageIsNew = not self.Messages[messageID]; 
	if force or messageIsNew then
		CPAPI.Log('%s was added to your utility ring. Use: %s', link, slug or NOT_BOUND)
		self.Messages[messageID] = true;
		if messageIsNew then
			self.MessageCount = self.MessageCount + 1;
		end
	end
end

function Utility:AnnounceRemoval(link, set)
	CPAPI.Log('%s was removed from your utility ring.', link)
	local messageID = self:GetBindingSuffixForSet(set) .. link;
	if self.Messages[messageID] then
		self.Messages[messageID] = nil;
		self.MessageCount = self.MessageCount - 1;
	end
end

---------------------------------------------------------------
-- Mapping from type to usable attributes
---------------------------------------------------------------
Utility.KindAndActionMap = {
	action = function(data) return data.action end;
	item   = function(data) return data.item end;
	pet    = function(data) return data.action end;
	spell  = function(data) return data.spell end;
	macro  = function(data) return data.macro end;
	equipmentset = function(data) return data.equipmentset end;
}

function Utility:GetKindAndAction(info)
	return info.type, self.KindAndActionMap[info.type](info);
end

---------------------------------------------------------------
-- Link map
---------------------------------------------------------------
Utility.LinkMap = {
	spell = function(spell, ...)
		local args = select('#', ...)
		if (args > 1) then
			local bookType = ...;
			return GetSpellLink(spell, bookType)
		end
		return GetSpellLink(spell)
	end;
	item = function(...)
		return select(2, ...)
	end;
}

function Utility:GetLinkFromInfo(type, ...)
	local func = self.LinkMap[type];
	if func then
		return func(...)
	end
end

---------------------------------------------------------------
-- Mapping from cursor info
---------------------------------------------------------------
Utility.SecureHandlerMap = {
	action = function(action)
		return {type = 'action', action = action};
	end;
	item = function(itemID, itemLink)
		return {type = 'item', item = itemLink or itemID, link = itemLink};
	end;
	spell = function(spellIndex, bookType, spellID)
		return {type = 'spell', spell = spellID, link = GetSpellLink(spellID)};
	end;
	macro = function(index)
		return {type = 'macro', macro = index};
	end;
	mount = function(mountID)
		local spellID = select(2, C_MountJournal.GetMountInfoByID(mountID));
		local spellName = spellID and GetSpellInfo(spellID)
		if spellName then
			return {type = 'spell', spell = spellName, link = GetSpellLink(spellName)};
		end
	end;
	petaction = function(spellID, indexIsOffset)
		if indexIsOffset then
			return {type = 'spell', spell = spellID};
		end
	end;
	equipmentset = function(name)
		return {type = 'equipmentset', equipmentset = name};
	end;
	spellID = function(spellID)
		return {type = 'spell', spell = spellID}
	end;
	companion = function(companionID, companionType)
		local _, spellName = GetCompanionInfo(companionType, companionID)
		return {type = 'spell', spell = spellName, link = GetSpellLink(spellName)}
	end;
}

function Utility:AddActionFromInfo(setID, idx, infoType, ...)
	local infoHandler = self.SecureHandlerMap[infoType]
	if infoHandler then
		local info = infoHandler(...)
		if info then
			return self:AddUniqueAction(setID, idx, info)
		end
	end
end

---------------------------------------------------------------
-- Special content handlers
---------------------------------------------------------------
function Utility:AddFromCursorInfo(setID, idx)
	return self:AddActionFromInfo(setID, idx, GetCursorInfo())
end

function Utility:AddUniqueAction(setID, preferredIndex, info)
	if self:IsUniqueAction(setID, info) then
		return self:AddAction(setID, preferredIndex, info)
	end
end

function Utility:AutoAssignAction(info, preferredIndex)
	info.autoassigned = true;
	return self:AddUniqueAction(DEFAULT_SET, preferredIndex, info)
end

function Utility:GetItemForQuestID(questID)
	local logIndex = CPAPI.GetQuestLogIndexForQuestID(questID)
	return logIndex and GetQuestLogSpecialItemInfo(logIndex)
end

function Utility:AddQuestWatchItem(questID)
	local item = self:GetItemForQuestID(questID)
	if item then
		local info = self.SecureHandlerMap.item(item)
		info.questID = questID;

		local wasAdded = self:AutoAssignAction(info)
		if wasAdded then
			self:AnnounceAddition(item)
		end
		return wasAdded;
	end
end

function Utility:RemoveQuestWatchItem(questID)
	local wasRemoved = self:ClearActionByAttribute(DEFAULT_SET, 'questID', questID)
	if wasRemoved then
		self:RefreshAll()
	end
end

function Utility:ToggleQuestWatchItem(questID, added)
	if added then
		self:AddQuestWatchItem(questID)
	else
		self:RemoveQuestWatchItem(questID)
	end
end

function Utility:ToggleQuestWatchItemInline(questID)
	local item = self:GetItemForQuestID(questID)
	if item then
		local info = self.SecureHandlerMap.item(item)
		info.questID = questID;

		local wasAdded = self:AutoAssignAction(info)
		if wasAdded then
			self:AnnounceAddition(item)
		end
	else
		self:RemoveQuestWatchItem(questID)
	end
end

function Utility:SetObserveQuestID(questID)
	self.observedQuestIDs = self.observedQuestIDs or {};
	self.observedQuestIDs[questID] = true;
end

function Utility:ParseObservedQuestIDs()
	local observedQuestIDs = self.observedQuestIDs;
	if observedQuestIDs then
		self.observedQuestIDs = nil;
		for questID in pairs(observedQuestIDs) do
			self:ToggleQuestWatchItemInline(questID)
		end
	end
end

function Utility:AddAllQuestWatchItems()
	for i=1, CPAPI.GetNumQuestWatches() do
		local questID = C_QuestLog.GetQuestIDForQuestWatchIndex(i)
		self:ToggleQuestWatchItem(questID, true)
	end
end

function Utility:RefreshQuestWatchItems()
	self:ClearActionByKey(DEFAULT_SET, 'questID')
	self:AddAllQuestWatchItems()
end

function Utility:ToggleExtraActionButton(enabled)
	if not CPAPI.IsRetailVersion then return end
	
	if enabled then
		self:AutoAssignAction(self.SecureHandlerMap.action(EXTRA_ACTION_ID), 1)
	else
		self:ClearActionByAttribute(DEFAULT_SET, 'action', EXTRA_ACTION_ID)
	end
end

function Utility:ToggleZoneAbilities()
	local zoneAbilities = CPAPI.GetActiveZoneAbilities()
	table.sort(zoneAbilities, function(lhs, rhs)
		return lhs.uiPriority < rhs.uiPriority;
	end)

	for i, zoneAbility in ipairs(zoneAbilities) do
		local spellID = zoneAbility.spellID;
		if not C_ActionBar.FindSpellActionButtons(spellID) then
			local wasAdded = self:AutoAssignAction(self.SecureHandlerMap.spellID(spellID))
			if wasAdded then
				self:AnnounceAddition((GetSpellLink(spellID)))
			end
		else
			local index, set = self:SearchActionByAttribute(DEFAULT_SET, 'spell', spellID)
			if index and set then
				local action = set[index];
				if action and action.autoassigned then
					self:RemoveAction(DEFAULT_SET, index)
				end
			end
		end
	end
end

function Utility:ToggleInventoryQuestItems(hideAnnouncement)
	if CPAPI.IsRetailVersion then return end
	local function getItemID(input) return input:match('item:(%d+)') end;

	local set, exists = self.Data[DEFAULT_SET], {};
	for i = #set, 1, -1 do
		local info = set[i];
		if info.autoqitem then
			local itemID = getItemID(info.item);
			if GetItemCount(info.item) < 1 or exists[itemID] then
				self:RemoveAction(DEFAULT_SET, i)
			else
				exists[itemID] = true;
			end
		end
	end

	CPAPI.IteratePlayerInventory(function(item)
		local link = CPAPI.GetContainerItemInfo(item:GetBagAndSlot()).hyperlink;
		local isQuestItem = link and select(6, GetItemInfoInstant(link)) == LE_ITEM_CLASS_QUESTITEM;
		if isQuestItem and IsUsableItem(link) and not exists[getItemID(link)] then
			local info = self.SecureHandlerMap.item(link)
			info.autoqitem = true;

			local wasAdded = self:AutoAssignAction(info)
			if wasAdded and not hideAnnouncement then
				self:AnnounceAddition(link)
			end
		end
	end)
end

function Utility:CheckCursorInfo(setID, silent)
	if not InCombatLockdown() then
		setID = self:GetSetID(setID);
		if GetCursorInfo() then
			if self:AddFromCursorInfo(setID) then
				if not silent then
					-- TODO: map returns to links
					self:AnnounceAddition(
						GetCursorInfo():gsub('^%l', strupper),
						self:GetBindingSuffixForSet(setID), true
					);
				end
				ClearCursor()
				db:TriggerEvent('OnRingContentChanged', setID)
			end
		end
	end
end

---------------------------------------------------------------
-- Pending action
---------------------------------------------------------------
local function CreatePendingAction(setID, info, enabled)
	return {
		setID = setID;
		info  = info;
		add   = enabled;
	};
end

function Utility:SetPendingAction(setID, info, force)
	if force or self:IsUniqueAction(setID, info) then
		self.pendingAction = CreatePendingAction(setID, info, true)
		return true;
	end
end

function Utility:SetPendingRemove(setID, info)
	self.pendingAction = CreatePendingAction(setID, info, false)
end

function Utility:HasPendingAction()
	return self.pendingAction;
end

function Utility:ClearPendingAction()
	self.pendingAction = nil;
end

function Utility:PostPendingAction(preferredIndex)
	local action = self.pendingAction;
	if action then
		if action.add then
			if self:AddAction(action.setID, preferredIndex, action.info) then
				self:AnnounceAddition(action.info.link, self:GetBindingSuffixForSet(action.setID), true)
			end
		else
			if self:ClearActionByAttribute(action.setID, 'link', action.info.link) then
				self:AnnounceRemoval(action.info.link)
			end
		end
		self.pendingAction = nil;
	end
end

---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function Utility:QUEST_WATCH_LIST_CHANGED(questID, added)
	if self.autoAssignExtras then
		if questID then
			db:RunSafe(self.ToggleQuestWatchItem, self, questID, added)
		else
			db:RunSafe(self.RefreshQuestWatchItems, self)
		end
	end
end

function Utility:QUEST_WATCH_UPDATE(questID)
	if self.autoAssignExtras then
		self:SetObserveQuestID(questID)
	end
end

function Utility:UPDATE_BINDINGS()
	if self.autoAssignExtras and not db.Gamepad:GetBindingKey('EXTRAACTIONBUTTON1') then
		self.hasExtraActionButton = true;
		db:RunSafe(self.ToggleExtraActionButton, self, true)
	else
		self.hasExtraActionButton = false;
		db:RunSafe(self.ToggleExtraActionButton, self, false)
	end
end

function Utility:UPDATE_EXTRA_ACTIONBAR()
	if self.hasExtraActionButton and HasExtraActionBar() then
		local link = self:GetLinkFromInfo(GetActionInfo(EXTRA_ACTION_ID))
		self:AnnounceAddition(link or BINDING_NAME_EXTRAACTIONBUTTON1, nil, true)
	end
end

-- NOTE: Register unit event instead of QUEST_LOG_UPDATE
-- to get around spam issue with certain addons.
Utility:RegisterUnitEvent('UNIT_QUEST_LOG_CHANGED', 'player')
function Utility:UNIT_QUEST_LOG_CHANGED()
	if self.autoAssignExtras then
		db:RunSafe(self.ParseObservedQuestIDs, self)
		db:RunSafe(self.RefreshQuestWatchItems, self)
	end
end

function Utility:SPELLS_CHANGED()
	if self.autoAssignExtras then
		db:RunSafe(self.ToggleZoneAbilities, self)
	end
end

function Utility:ACTIONBAR_SLOT_CHANGED()
	if self.autoAssignExtras then
		db:RunSafe(self.ToggleZoneAbilities, self)
	end
end

function Utility:BAG_UPDATE_DELAYED()
	if self.autoAssignExtras then
		db:RunSafe(self.ToggleInventoryQuestItems, self, not self.announceBagAdditions)
		self.announceBagAdditions = true;
	end
end

---------------------------------------------------------------
-- Button mixin
---------------------------------------------------------------
function Button:OnLoad()
	self:Initialize()
	self:SetScript('OnHide', self.OnClear)
	self:SetScript('OnShow', self.UpdateAssets)
end

function Button:UpdateAssets()
	local bg = self.Shadow;
	bg:ClearAllPoints()
	if (self:GetAttribute('type') == 'action' and self:GetAttribute('action') == EXTRA_ACTION_ID) then
		bg:SetTexture(CPAPI.GetOverrideBarSkin() or 'Interface\\ExtraButton\\Default')
		bg:SetSize(256 * 0.8, 128 * 0.8)
		bg:SetPoint('CENTER', -2, 0)
	else
		bg:SetTexture(CPAPI.GetAsset('Textures\\Button\\Shadow'))
		bg:SetPoint('TOPLEFT', -5, 0)
		bg:SetPoint('BOTTOMRIGHT', 5, -10)
	end
end

function Button:OnFocus()
	self:SetChecked(true)
	GameTooltip_SetDefaultAnchor(GameTooltip, self)
	self:SetTooltip()
	local use = Utility:GetTooltipUsePrompt()
	local remove = Utility:GetTooltipRemovePrompt()
	if use then
		GameTooltip:AddLine(use)
	end
	if ( remove and remove ~= use ) then
		GameTooltip:AddLine(remove)
	end
	GameTooltip:Show()
end

function Button:OnClear()
	self:SetChecked(false)
	if GameTooltip:IsOwned(self) then
		GameTooltip:Hide()
	end
end