local NM, _, db = '', ...;
local C_GamePad, GamepadMixin, GamepadAPI = C_GamePad, {}, CPAPI.CreateEventHandler({'Frame', '$parentGamePadHandler', ConsolePort}, {
	'UPDATE_BINDINGS';
	'GAME_PAD_CONFIGS_CHANGED';
	'PLAYER_ENTERING_WORLD';
	(CPAPI.IsRetailVersion or CPAPI.IsClassicVersion) and 'GAME_PAD_POWER_CHANGED';
}, {
	Template = {          -- Gamepad templates:
		Gamepads    = {}; -- Usable template with functions and metadata.
		Polyfill    = {}; -- Polyfill for missing data on version bump.
		Metadata    = {}; -- Metadata (not saved to disk).
	};
	Index = {             -- Index generated by the API:
		Stick  = {
			ID      = {}; -- index -> config name
			Config  = {}; -- config name -> index
		};
		Button = {
			ID      = {}; -- index -> config name / binding
			Config  = {}; -- config name -> index / binding
			Binding = {}; -- binding -> config name / index
		};
		Modifier = {
			Key     = {}; -- modifier -> button
			Prefix  = {}; -- modifier string -> button
			Active  = {}; -- all possible modifier combinations
			Owner   = {}; -- button -> modifier string
			Blocked = {}; -- combos that are blocked by their own buttons
			Cvars   = {}; -- modsim -> cvar name
			Driver  = NM; -- state driver for all active modifiers
		};
		Atlas = {
			[NM]    = {[32] = {}; [64] = {}}; -- Generic button labels
			SHP     = {[32] = {}; [64] = {}}; -- "Shapes" label style specializations
			LTR     = {[32] = {}; [64] = {}}; -- "Letters" label style specializations
			REV     = {[32] = {}; [64] = {}}; -- "Reverse" label style specializations
		};
	};
	Modsims = {'ALT', 'CTRL', 'SHIFT'};
	Mouse   = {
		Cvars   = { -- Cvars for mouse button emulation.
			LeftClick  = 'GamePadCursorLeftClick';
			RightClick = 'GamePadCursorRightClick';
		};
		Binding = { -- Bindings IDs for mouse buttons under the hood.
			LeftClick  = 'CAMERAORSELECTORMOVE';
			RightClick = 'TURNORACTION';
		};
	};
});
---------------------------------------------------------------
db:Register('Icons', {})
db:Register('Gamepad', GamepadAPI)
db:Save('Gamepad/Template/Gamepads', 'ConsolePortDevices')

---------------------------------------------------------------
-- API
---------------------------------------------------------------
function GamepadAPI:AddGamepad(data, metaData)
	self.Template.Metadata[data.Name] = metaData or {};
	self.Template.Polyfill[data.Name] = data;
	self.Template.Gamepads[data.Name] = self:InitiateDevice(data);
end

function GamepadAPI:GetMetadata(name)
	return self.Template.Metadata[name];
end

function GamepadAPI:GetDevice(name)
	return self.Template.Gamepads[name];
end

function GamepadAPI:InitiateDevice(data)
	local metaData = self:GetMetadata(data.Name);
	if not metaData then return self:AddGamepad(data) end;

	local polyFill = self.Template.Polyfill[data.Name];
	for key, value in pairs(polyFill) do
		if ( data[key] == nil ) then
			data[key] = value;
		end
	end

	return CPAPI.Proxy(data, CPAPI.Proxy(metaData, GamepadMixin)):OnLoad()
end

function GamepadAPI:GetDevices()
	local devices = {};
	for device in db.table.spairs(self.Template.Gamepads) do
		devices[#devices + 1] = device;
	end
	return devices;
end

function GamepadAPI:EnumerateDevices()
	return db.table.spairs(GamepadAPI.Template.Gamepads)
end

function GamepadAPI:SetActiveDevice(name)
	local activeDevice = self:GetDevice(name)
	assert(activeDevice, ('Device %s does not exist in registry.'):format(name or '<nil>'))
	for _, data in self:EnumerateDevices() do
		data.Active = nil;
	end
	self:SetActiveIconsFromDevice(activeDevice)
	if self.UseAtlasIcons then
		activeDevice:ApplyLabelStyle()
	else
		activeDevice:ApplyHotkeyStrings()
	end
	db(('Gamepad/Template/Gamepads/%s/Active'):format(name), true)
	db('Gamepad/Active', activeDevice)
	db:TriggerEvent('OnIconsChanged', db('useAtlasIcons'))
end

function GamepadAPI:SetActiveIconsFromDevice(device)
	local styler = CPAPI.Proxy({}, function(self, button)
		return device:GetIconForButton(button, self[0])
	end)
	CPAPI.Proxy(db('Icons'), function(_, style)
		styler[0] = style;
		return styler;
	end)
end

function GamepadAPI:GetActiveDevice()
	return self.Active;
end

function GamepadAPI:GetActiveDeviceName()
	return self.Active and self.Active.Name;
end

function GamepadAPI:GetConnectedDevices()
	local connected = {};
	for _, i in ipairs(C_GamePad.GetAllDeviceIDs()) do
		local rawState   = C_GamePad.GetDeviceRawState(i)
		local powerLevel = C_GamePad.GetPowerLevel(i)
		local powerInfo  = db.Battery:GetPowerLevelInfo(powerLevel)
		if rawState then
			local vendorID  = ('%04x'):format(rawState.vendorID):upper();
			local productID = ('%04x'):format(rawState.productID):upper();
			local config    = C_GamePad.GetConfig(rawState);
			tinsert(connected, {
				id        = i;
				name      = rawState.name;
				state     = rawState;
				vendorID  = vendorID;
				productID = productID;
				config    = config;
				power     = powerInfo.color:WrapTextInColorCode(powerInfo.name);
			})
		end
	end
	return connected;
end

---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function GamepadAPI:OnDataLoaded()
	-- TODO: remove if/when atlas icons get added to Classic Era.
	-- Added as a workaround for players importing settings from Retail.
	if CPAPI.IsClassicEraVersion and db('useAtlasIcons') then
		db('Settings/useAtlasIcons', false)
	end

	self:ReindexMappedState()
	self:ReindexIconAtlas()
	-- Load preset devices
	local presets = self.Template.Gamepads;
	-- Load saved devices
	db:Load('Gamepad/Template/Gamepads', 'ConsolePortDevices')
	for id, preset in pairs(presets) do
		-- (1) fill in new presets that have been added,
		-- (2) overwrite existing if version has been bumped
		local loaded = self.Template.Gamepads[id];
		if  ( not loaded or preset.Version and
			( loaded.Version < preset.Version )) then
			self.Template.Gamepads[id] = preset;
		end
	end
	for id, device in self:EnumerateDevices() do
		self:InitiateDevice(device)
		if device.Active then
			self:SetActiveDevice(id)
		end
	end
	-- Clear polyfill after loading devices.
	wipe(self.Template.Polyfill)
	if not self.Active then
		-- (1) open the config, no active device found.
		ConsolePort()
		if not ConsolePortConfig then return end;
		-- (2) set the config to the Guide panel.
		ConsolePortConfig:SetCurrentPanelByID(1)
		if not ConsolePortCursor then return end;
		-- (3) enable the cursor so that newbies can see it exists.
		CPAPI.Next(function(config, cursor)
			if config:IsVisible() then
				cursor:SetEnabled(true)
			end
		end, ConsolePortConfig, ConsolePortCursor)
	end
	return CPAPI.BurnAfterReading;
end

function GamepadAPI:PLAYER_ENTERING_WORLD()
	self.IsDispatchReady = true;
	self:QueueOnNewBindings()
end

function GamepadAPI:GAME_PAD_CONFIGS_CHANGED()
	CPAPI.Log('Your gamepad configuration has changed.')
end

function GamepadAPI:GAME_PAD_POWER_CHANGED(level)
	db:TriggerEvent('OnGamePadPowerChange', level)
end

function GamepadAPI:UPDATE_BINDINGS()
	if self.IsMapped and self.IsDispatchReady then
		self:QueueOnNewBindings()
	else
		self:OnNewBindings()
	end
end

-- UPDATE_BINDINGS - handle two different scenarios:
-- 1. just logged in, and bindings are dispatched immediately.
-- 2. logged in for a while, and bindings are dispatched with debouncing.
--
-- Why?
--
-- 1. runs unnecessarily since UPDATE_BINDINGS fires somewhere
-- around 5-10 times on login, but we need to configure action bars
-- immediately in case we're in combat, and we can't tell which event is
-- the one when gamepad bindings are ready to be dispatched without some
-- table inspection gymnastics, looking for a non-empty binding.
--
-- 2. we use debouncing to consolidate unnecessary update cycles, with the
-- added benefit of likely setting overrides _after_ a conflicting action
-- bar addon has already set its bindings. Yes, some people do actually
-- use multiple action bar addons at the same time. Isn't it crazy?

---------------------------------------------------------------
-- Callbacks
---------------------------------------------------------------
for key, cvar in pairs(GamepadAPI.Mouse.Cvars) do
	db:RegisterSafeCallback(cvar, function(self, binding, value)
		CPAPI.SetBinding(value, binding, true)
	end, GamepadAPI, GamepadAPI.Mouse.Binding[key])
end

for _, modifier in ipairs(GamepadAPI.Modsims) do
	local cvar = ('GamePadEmulate%s'):format(modifier:lower():gsub('^%l', strupper))
	GamepadAPI.Index.Modifier.Cvars[modifier] = cvar;
	db:RegisterSafeCallback(cvar, function(self, value)
		self:ReindexModifiers()
		-- Wipe the incompatible bindings for a modifier when it's set.
		-- E.g. if you set ALT to PAD1, ALT-PAD1 will be removed.
		for combination in pairs(self.Index.Modifier.Blocked) do
			CPAPI.SetBinding(combination, nil)
		end
		SaveBindings(GetCurrentBindingSet())
		db:TriggerEvent('OnModifierChanged', modifier, value)
	end, GamepadAPI)
end

db:RegisterSafeCallback('GamePadStickAxisButtons', function(self, value)
	if not value then return end;
	for buttonID in pairs(self.Index.Button.Binding) do
		if not CPAPI.IsButtonValidForBinding(buttonID) then
			for modifier in pairs(self.Index.Modifier.Active) do
				CPAPI.SetBinding(modifier..buttonID, nil)
			end
		end
	end
	SaveBindings(GetCurrentBindingSet())
end, GamepadAPI)

db:RegisterSafeCallback('OnNewBindings', function(self)
	db:SetCVar('GamePadStickAxisButtons', db('bindingAllowSticks'))
	if ( self:GetBindingKey('INTERACTTARGET') and not GetCVarBool('SoftTargetInteract') ) then
		-- FIX: On Classic, the interact key is not enabled by default.
		-- If it's bound and disabled, enable it. 1 = GamePad, see Console.lua.
		db:SetCVar('SoftTargetInteract', 1)
	end
end, GamepadAPI)

db:RegisterCallback('Settings/useAtlasIcons', function(self, value)
	self.UseAtlasIcons = value;
	db:TriggerEvent('OnIconsChanged', value)
end, GamepadAPI)

db:RegisterSafeCallback('Settings/bindingAllowSticks', function(self, value)
	db:SetCVar('GamePadStickAxisButtons', value)
end, GamepadAPI)

do local emulations = tInvert(CreateFromMixins(GamepadAPI.Index.Modifier.Cvars, GamepadAPI.Mouse.Cvars))
	-- Handle emulation conflicts; we do not allow overlapping emulations since they can cause
	-- unintended consequences, and only serve to confuse users.
	for emulation in pairs(emulations) do
		db:RegisterSafeCallback(emulation, function(this, value)
			for other in pairs(emulations) do
				if other ~= this and db:GetCVar(other) == value then
					db:SetCVar(other, 'none')
				end
			end
		end, emulation)
	end
end

---------------------------------------------------------------
-- Data: state
---------------------------------------------------------------
function GamepadAPI:GetState()
	return C_GamePad.GetDeviceMappedState(C_GamePad.GetActiveDeviceID())
end

function GamepadAPI:GetPowerLevel()
	return C_GamePad.GetPowerLevel(C_GamePad.GetActiveDeviceID())
end

function GamepadAPI:ReindexMappedState(force)
	if not C_GamePad.IsEnabled() then return end
	if not force and self.IsMapped then return end

	self:ReindexSticks()
	self:ReindexButtons()
	self:ReindexModifiers()
	self.IsMapped = true;
end

function GamepadAPI:ReindexButtons()
	local map = self.Index.Button;
	wipe(map.ID); wipe(map.Config); wipe(map.Binding);

	local i = 0;
	while true do
		local conf = C_GamePad.ButtonIndexToConfigName(i)
		local bind = C_GamePad.ButtonIndexToBinding(i)
		if not conf or not bind then break end;

		map.ID[i]         = {Config = conf; Binding = bind}
		map.Config[conf]  = {ID = i; Binding = bind}
		map.Binding[bind] = {ID = i; Config = conf}
		i = i + 1;
	end
end

function GamepadAPI:ReindexSticks()
	local map = self.Index.Stick;
	wipe(map.ID); wipe(map.Config);

	local i = 0;
	while true do
		local name = C_GamePad.StickIndexToConfigName(i)
		if not name then break end;

		map.ID[i+1] = name
		map.Config[name] = i+1
		i = i + 1;
	end
end

function GamepadAPI:ReindexModifiers()
	local map = self.Index.Modifier;
	wipe(map.Key); wipe(map.Prefix); wipe(map.Owner); wipe(map.Blocked);

	for _, mod in ipairs(self.Modsims) do
		local btn = GetCVar('GamePadEmulate'..mod)
		if (btn and btn:match('PAD')) then
			self.Index.Modifier.Key[mod] = btn -- BUG: uproots the mod order if uppercase
			self.Index.Modifier.Key[mod:upper()] = btn
			self.Index.Modifier.Prefix[mod..'-'] = btn
			self.Index.Modifier.Owner[btn] = mod..'-';
		end
	end
	map.Active, map.Driver = self:GetActiveModifiers()

	for mod, btn in pairs(map.Key) do
		for active, inputs in pairs(map.Active) do
			-- If the active button combination contains both
			-- the modifier and the button, then it is impossible
			-- to use, so we block it.
			if tostring(inputs):match(btn) then
				map.Blocked[active..btn] = mod;
			end
		end
	end
end

function GamepadAPI:GetActiveModifiers()
	local mods = db.table.copy(self.Index.Modifier.Prefix)
	local driver = {'[nomod] '};
	local spairs = db.table.spairs; -- need to scan in-order

	local function assertUniqueAndInOrder(...)
		local uniques = {}
		for i=1, select('#', ...) do
			local v1 = select(i, ...)
			if uniques[v1] then return end
			uniques[v1] = true
			for v2 in pairs(uniques) do
				if v2 > v1 then return end
			end
		end
		return true
	end

	for M1, K1 in spairs(mods) do
		for M2, K2 in spairs(mods) do
			for M3, K3 in spairs(mods) do
				if (assertUniqueAndInOrder(M1, M2, M3)) then
					local modifier = M1..M2..M3;
					mods[modifier] = ('%s-%s-%s'):format(K1, K2, K3)
					tinsert(driver, ('[mod:%s] %s'):format(modifier, modifier))
				end
			end
			if (assertUniqueAndInOrder(M1, M2)) then
				local modifier = M1..M2;
				mods[modifier] = ('%s-%s'):format(K1, K2)
				tinsert(driver, ('[mod:%s] %s'):format(modifier, modifier))
			end
		end
		tinsert(driver, ('[mod:%s] %s'):format(M1, M1))
	end
	mods[NM] = true
	return mods, table.concat(driver, '; ');
end


function GamepadAPI:GetActiveModifier(button)
	for _, mod in ipairs(self.Modsims) do
		if (GetCVar('GamePadEmulate'..mod) == button) then
			return mod;
		end
	end
end

---------------------------------------------------------------
-- Data: bindings
---------------------------------------------------------------
function GamepadAPI:GetModifiersHeld()
	-- NOTE: uses input state instead of Blizzard API,
	-- to get reliable results in things like click wrappers,
	-- which otherwise sandbox the modifier while executing.
	local cmp = {};
	for i, mod in ipairs(self.Modsims) do
		local buttonID = GetCVar('GamePadEmulate'..mod)
		if (buttonID and buttonID ~= 'none') then
			cmp[buttonID] = mod;
		end
	end

	local result = {};
	local state = self:GetState()
	if not state or not state.buttons then return result end

	for id, down in ipairs(state.buttons) do
		if down then
			local binding = C_GamePad.ButtonIndexToBinding(id-1)
			local mod = cmp[binding];
			if mod then
				result[mod] = binding;
			end
		end
	end
	return result;
end

function GamepadAPI:GetModifierHeld(modifier)
	return modifier and self:GetModifiersHeld()[modifier] ~= nil;
end

function GamepadAPI:GetBindings(getInactive)
	local btns = self.Index.Button.Binding;
	local mods = self.Index.Modifier.Active;

	local bindings = {};
	for btn in pairs(btns) do
		for mod in pairs(mods) do
			local binding = CPAPI.GetBindingAction(mod..btn)
			if getInactive or binding:len() > 0 then
				bindings[btn] = bindings[btn] or {};
				bindings[btn][mod] = binding;
			end
		end
	end
	return bindings;
end

function GamepadAPI:GetBindingsTemplate()
	local bindings = self:GetBindings(true)
	for btn, set in pairs(bindings) do
		for mod, _ in pairs(set) do
			bindings[btn][mod] = NM;
		end
	end
	return bindings;
end

function GamepadAPI:GetBindingKey(binding, asTable)
	local context = CPAPI.GetBindingContextForAction(binding)
	local keys = tFilter({GetBindingKey(binding, true, context)}, IsBindingForGamePad, true)
	if asTable then return keys end;
	return unpack(keys)
end

function GamepadAPI:EnumerateBindingKeys(binding)
	local keys = self:GetBindingKey(binding, true)
	local i = 0;
	return function()
		i = i + 1;
		return keys[i];
	end
end

function GamepadAPI:FlattenBindings(bindings)
	local flat = {};
	for btn, set in pairs(bindings) do
		for mod, binding in pairs(set) do
			flat[mod..btn] = binding;
		end
	end
	return flat;
end

function GamepadAPI:OnNewBindings()
	local newBindings = self:GetBindings(true)
	db:TriggerEvent('OnNewBindings', newBindings)
	db:TriggerEvent('OnUpdateOverrides', false, newBindings)
	db:TriggerEvent('OnUpdateOverrides', true,  newBindings)
end

GamepadAPI.QueueOnNewBindings = CPAPI.Debounce(GamepadAPI.OnNewBindings, GamepadAPI)

---------------------------------------------------------------
-- Data: icons
---------------------------------------------------------------
function GamepadAPI:ReindexIconAtlas()
	self.UseAtlasIcons = db('useAtlasIcons')

	local function getAtlasFromGlobalEscSeq(modifier, button, style)
		local globalKey = ('KEY_%s%s_%s'):format(modifier, button, style):gsub('_$', '')
		local atlasEscSeq = _G[globalKey]
		if not atlasEscSeq then return end
		return atlasEscSeq:gsub('|A:([%w_]+)(.+)', '%1')
	end

	-- Do indexing of the different styles
	for style, sizes in pairs(self.Index.Atlas) do --SHP,
		for size, icons in pairs(sizes) do -- 32,64
			local modifier = (size == 32) and 'ABBR_' or NM;
			for button in pairs(self.Index.Button.Binding) do
				icons[button] = getAtlasFromGlobalEscSeq(modifier, button, style)
			end
		end
	end

	-- Proxy the other styles to fallback on generic
	for style, sizes in pairs(self.Index.Atlas) do
		if (style ~= '') then
			for size, icons in pairs(sizes) do
				CPAPI.Proxy(icons, self.Index.Atlas[NM][size])
			end
		end
	end
end

function GamepadAPI:GetIconPath(path, style)
	return self.Index.Icons.Path:format(style or 64, path)
end

function GamepadAPI.SetIconToTexture(obj, iconID, style, iconSize, atlasSize)
	if not iconID then return obj:SetTexture(nil) end;
	local icon = db(('Icons/%s/%s'):format(style or 64, iconID))
	if ( type(icon) == 'string' ) then
		CPAPI.SetTextureOrAtlas(obj, {icon, GamepadAPI.UseAtlasIcons}, iconSize, atlasSize)
		return true;
	end
	return false;
end

---------------------------------------------------------------
-- Gamepad Mixin
---------------------------------------------------------------
function GamepadMixin:OnLoad()
	self.Icons = nil; -- TODO: Remove all of this?
	getmetatable(self).__index.Icons = CPAPI.Proxy({}, function(_, id)
		local id, style = strsplit('-', id)
		return style and self:GetIconForButton(id, style) or self:GetIconIDForButton(id)
	end)
	return self
end

function GamepadMixin:Activate()
	GamepadAPI:ReindexMappedState(true)
	GamepadAPI:SetActiveDevice(self.Name)
end

function GamepadMixin:ApplyPresetVars(activateOnFinish)
	local environment = self.Environment;
	if not environment then
		return false;
	end
	local console = environment.Console;
	if console then
		for var, val in pairs(console) do
			db:SetCVar(var, val)
		end
	end
	local settings = environment.Settings;
	if settings then
		for var, val in pairs(settings) do
			db('Settings/'..var, val)
		end
	end
	if activateOnFinish then
		self:Activate()
	end
	return true;
end

function GamepadMixin:ConfigHasBluetoothHandling()
	if self.Config then
		for set, array in pairs(self.Config) do
			if (type(array) == 'table') then
				for i, data in ipairs(array) do
					if (data.bluetooth ~= nil) then
						return true;
					end
				end
			end
		end
	end
end

function GamepadMixin:ApplyConfig(bluetooth)
	assert(self.Config, ('Raw configuration missing from %s template.'):format(self.Name))
	local config = CopyTable(self.Config);
	-- NOTE: Handle bluetooth differences if supplied
	if (bluetooth ~= nil) then
		for set, array in pairs(config) do
			if (type(array) == 'table') then
				local i, data = 1, array[1];
				while data do
					-- If the configured input has a bluetooth-specific value:
					-- confirm it corresponds to the desired gamepad mapping,
					-- otherwise junk it.
					if (data.bluetooth ~= nil and data.bluetooth ~= bluetooth) then
						tremove(array, i)
						i = i - 1;
					end
					i = i + 1; data = array[i];
				end
			end
		end
	end
	C_GamePad.SetConfig(config)
	C_GamePad.ApplyConfigs()
end

function GamepadMixin:GetPresetBindings()
	local generator = self.Generator;
	assert(generator, ('Preset bindings missing from %s template.'):format(self.Name))

	-- Steps:
	-- (1) merge an empty bindings template (has all combinations) with
	--  a. face buttons, these are likely to be the same for all devices
	--  b. center buttons, these will differ between devices
	--  c. left hand OR trigger based modifiers, OR if no match then pull in
	--     active bindings, resulting in a noop if we're not in safe terrirtory.
	-- (2) iterate over the merged bindings and convert them to a flat table.
	-- (3) merge mouse bindings, when they match emulations, to match the functions
	--     of an actual mouse, i.e. left click, right click on both UI and world.
	-- (4) remove blocked combinations, e.g. SHIFT-PAD1 when SHIFT == PAD1,
	--     since they are impossible to use.

	local preset = db.table.merge(
		GamepadAPI:GetBindingsTemplate(), -- (1)
		generator.Face, generator.Center, -- (1) a, b
		(function(cmp, cur, lh, tr)       -- (1) c
			if cmp(cur, lh) then
				return generator.LeftHand;
			elseif cmp(cur, tr) then
				return generator.Triggers;
			else
				local active = GamepadAPI:GetBindings(true)
				return {
					PADLSHOULDER = active.PADLSHOULDER;
					PADLTRIGGER  = active.PADLTRIGGER;
					PADRSHOULDER = active.PADRSHOULDER;
					PADRTRIGGER  = active.PADRTRIGGER;
				}
			end
		end)(db.table.compare, GamepadAPI.Index.Modifier.Key, -- current
			{ SHIFT = 'PADLSHOULDER', CTRL = 'PADLTRIGGER' }, -- LeftHand
			{ SHIFT = 'PADLTRIGGER',  CTRL = 'PADRTRIGGER' }) -- Triggers
		)

	local bindings = GamepadAPI:FlattenBindings(preset) -- (2)
	local mouse = GamepadAPI.Mouse; -- (3)
	for key, cvar in pairs(mouse.Cvars) do
		local buttonID = db:GetCVar(cvar, NM):match('^PAD.*')
		if buttonID then
			bindings[buttonID] = mouse.Binding[key];
		end
	end
	for combination in pairs(GamepadAPI.Index.Modifier.Blocked) do
		bindings[combination] = NM; -- (4)
	end
	return bindings;
end

function GamepadMixin:ApplyPresetBindings(setID)
	local clearOverlap, map = not db('bindingOverlapEnable'), db.table.map;
	local result = {};
	for combination, binding in pairs(self:GetPresetBindings()) do
		if clearOverlap then
			map(SetBinding, GamepadAPI:GetBindingKey(binding))
		end
		if SetBinding(combination, binding) then
			result[combination] = binding;
		end
	end
	if setID then
		SaveBindings(setID)
	end
	return result;
end

function GamepadMixin:ApplyHotkeyStrings()
	local label, hotkey = self.Label;
	assert(label, ('Gamepad device %s does not have a button label type.'):format(self.Name))
	for button in pairs(self.Assets) do
		hotkey = self:GetHotkeyStringForButton(button)
		_G[('KEY_ABBR_%s'):format(button)] = hotkey;
		_G[('KEY_ABBR_%s_%s'):format(button, label)] = hotkey;
	end
end

function GamepadMixin:ApplyLabelStyle()
	local style = self.LabelStyle;
	if not style then return end;
	local config = db.Mapper:GetConfig({ vendorID = 0x0, productID = 0x0 })
	config.labelStyle = style;
	C_GamePad.SetConfig(config)
	C_GamePad.ApplyConfigs()
end

function GamepadMixin:IsButtonValidForBinding(button)
	return not GamepadAPI:GetActiveModifier(button) and self.Assets[button]
end

---------------------------------------------------------------
-- Icon queries
---------------------------------------------------------------
function GamepadMixin:GetIconAtlasForButton(button, style)
	return db(('Gamepad/Index/Atlas/%s/%s/%s'):format(self.Label, style or 64, button)),
		GamepadAPI.UseAtlasIcons;
end

function GamepadMixin:GetIconIDForButton(button)
	assert(button, 'Button is not defined.')
	return self.Assets[button]
end

function GamepadMixin:GetIconForButton(button, style)
	local atlas, useAtlasIcon = self:GetIconAtlasForButton(button, style)
	if atlas and useAtlasIcon then
		return atlas, true;
	end
	local iconID = self:GetIconIDForButton(button)
	if iconID then
		return GamepadAPI:GetIconPath(db(('Gamepad/Index/Icons/%s'):format(iconID)), style)
	end
	if atlas then
		return atlas, true;
	end
	return GamepadAPI:GetIconPath('ALL_MISSING', style)
end

function GamepadMixin:GetTooltipButtonPrompt(button, prompt, style)
	local color = self.Colors[button] or 'FFFFFF';
	local icon, isAtlas = self:GetIconForButton(button, style)
	if icon and isAtlas then
		return ('|A:%s:24:24|a |cFF%s%s|r'):format(icon, color, prompt)
	elseif icon then
		return ('|T%s:24:24:0:0|t |cFF%s%s|r'):format(icon, color, prompt)
	end
end

function GamepadMixin:GetHotkeyStringForButton(button)
	local icon, isAtlas = self:GetIconForButton(button, 32)
	if icon and isAtlas then
		return ('|A:%s:14:14|a'):format(icon)
	elseif icon then
		return ('|T%s:0:0:0:0:32:32:8:24:8:24|t'):format(icon)
	end
end

function GamepadMixin:GetIconForName(name, style)
	return self:GetIconForButton(db(('Gamepad/Index/Button/Config/%s/Binding'):format(name)))
end

function GamepadMixin:GetIconIDForName(name)
	return self:GetIconIDForButton(db(('Gamepad/Index/Button/Config/%s/Binding'):format(name)))
end

function GamepadMixin:GetIconForIndex(i, style)
	return self:GetIconForButton(db(('Gamepad/Index/Button/ID/%d/Binding'):format(i)))
end

function GamepadMixin:GetIconIDForIndex(i)
	return self:GetIconIDForButton(db(('Gamepad/Index/Button/ID/%d/Binding'):format(i)))
end