---------------------------------------------------------------
-- Interface cursor
---------------------------------------------------------------
-- Creates a cursor used to manage the interface with D-pad.
-- Operates recursively on frames and calculates appropriate
-- actions based on node priority and position on screen.
-- Leverages Controller\UINode.lua for interface scans.

local _, db = ...;
local Cursor, Node, Input, Stack, Scroll, Fade, Hooks = 
	CPAPI.EventHandler(ConsolePortCursor),
	ConsolePortNode,
	ConsolePortInputHandler,
	ConsolePortUIStackHandler,
	CreateFrame('Frame'),
	db.Alpha.Fader, db.Hooks;

db:Register('Cursor', Cursor)
Cursor.InCombat = InCombatLockdown;

---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function Cursor:OnDataLoaded()
	self:RegisterEvent('PLAYER_REGEN_ENABLED')
	self:RegisterEvent('PLAYER_REGEN_DISABLED')
	self:UpdatePointer()
end

function Cursor:PLAYER_REGEN_DISABLED()
	-- TODO: relinquish to stack control
	self.isCombatLocked = true;
	if self:IsShown() then
		Fade.Out(self, 0.2, self:GetAlpha(), 0)
		self:ShowAfterCombat(true)
		self:SetFlashNextNode()
		self:Release()
	end
end

function Cursor:PLAYER_REGEN_ENABLED()
	-- in case the cursor is showing and waiting to hide OOC
	if self:IsShown() and not self.showAfterCombat then
		self:Hide()
	end
	-- time lock this in case it fires more than once
	if not self.timeLock then
		self.timeLock = true;
		if self.showAfterCombat then
			if not self.onEnableCallback then
				self.onEnableCallback = function()
					Fade.In(self, 0.2, self:GetAlpha(), 1)
					if not self:InCombat() and self:IsShown() then
						self:SetBasicControls()
						self:Refresh()
					end
				end
			end
			C_Timer.After(db('UIleaveCombatDelay'), function()
				self.onEnableCallback()
				self.timeLock = nil;
				self.showAfterCombat = nil;
				self.onEnableCallback = nil;
				self.isCombatLocked = nil;
			end)
		else -- do nothing but clear the locked state
			C_Timer.After(db('UIleaveCombatDelay'), function()
				self.onEnableCallback = nil;
				self.timeLock = nil;
				self.isCombatLocked = nil;
			end)
		end
	end
end

function Cursor:MODIFIER_STATE_CHANGED()
	-- TODO: implement this? maybe?
end

---------------------------------------------------------------
-- Cursor state
---------------------------------------------------------------
function Cursor:OnClick()
	self:SetEnabled(not self:IsShown())
end

function Cursor:SetEnabled(enable)
	if enable then
		return self:Enable()
	end
	return self:Disable()
end

function Cursor:Enable()
	local inCombat, disabled = self:IsObstructed()
	if disabled then
		return
	elseif inCombat then
		return self:ShowAfterCombat(true)
	end
	if not self:IsShown() then
		self:Show()
		self:SetBasicControls()
		return self:Refresh()
	end
end

function Cursor:Disable()
	local inCombat, disabled = self:IsObstructed()
	if inCombat or disabled then
		self:ShowAfterCombat(false)
	end
	if self:IsShown() and not inCombat then
		self:Hide()
	end
end

function Cursor:OnShow()
	self:SetScale(UIParent:GetEffectiveScale())
end

function Cursor:OnHide()
	self.timer = 0
	self:SetAlpha(1)
	self:SetFlashNextNode()
	self:Release()
	self.Blocker:Hide()
end

function Cursor:Release()
	Node.ClearCache()
	self:OnLeaveNode(self:GetCurrentNode())
	self:SetHighlight()
	Input:Release(self)
end

function Cursor:IsObstructed()
	return self:InCombat(), not db('UIenableCursor'), self.isCombatLocked;
end

function Cursor:IsAnimating()
	return self.ScaleInOut:IsPlaying()
end

function Cursor:ShowAfterCombat(enabled)
	self.showAfterCombat = enabled
end

function Cursor:ScanUI()
	if db('UIaccessUnlimited') then
		Node(UIParent, DropDownList1, DropDownList2)
	else
		Node(Stack:GetVisibleCursorFrames())
	end
end

function Cursor:Refresh()
	self:OnLeaveNode(self:GetCurrentNode())
	self:ScanUI()
	return self:AttemptSelectNode()
end

function Cursor:RefreshToFrame(frame)
	if not self:IsShown() then
		self:Show()
		self:SetBasicControls()
		self:OnLeaveNode(self:GetCurrentNode())
		self:SetFlashNextNode()
		Node(frame)
		return self:AttemptSelectNode()
	end
end

function Cursor:SetCurrentNode(node, assertNotMouse)
	if not db('UIenableCursor') then
		return
	end
	local object = node and Node.ScanLocal(node)[1]
	if object and (not assertNotMouse or IsGamePadFreelookEnabled()) then
		self:SetOnEnableCallback(function(self, object)
			self:SetBasicControls()
			self:SetFlashNextNode()
			self:SetCurrent(object)
			self:SelectAndPosition(self:GetSelectParams(object, true, true))
			self:Chime()
		end, object)
		return true;
	end
end

function Cursor:SetCurrentNodeIfActive(...)
	if self:IsShown() then
		return self:SetCurrentNode(...)
	end
end

function Cursor:SetOnEnableCallback(callback, ...)
	local inCombat, disabled, isCombatLocked = self:IsObstructed()
	if disabled then
		return
	end
	if not inCombat and not isCombatLocked then
		return callback(self, ...)
	end
	self.onEnableCallback = GenerateClosure(callback, self, ...)
end

function Cursor:OnUpdate(elapsed)
	if self:InCombat() then return end
	if not self:IsCurrentNodeDrawn() then
		self:SetFlashNextNode()
		if not self:Refresh() then
			self:Hide()
		end
	else
		self:RefreshAnchor()
	end
end

---------------------------------------------------------------
-- Navigation and input
---------------------------------------------------------------
do  -- Create input proxy for basic controls
	local InputProxy = function(self, ...)
		Cursor:Input(self, ...)
	end

	local DpadRepeater = function(self, elapsed)
		self.timer = self.timer + elapsed
		if self.timer >= self.UIControlTickNext and self.state then
			local func = self:GetAttribute(CPAPI.ActionTypeRelease)
			if ( func == 'UIControl' ) then
				self[func](self, self.state, self:GetAttribute('id'))
			end
			self.timer = 0
		end
	end

	local DpadInit = function(self, dpadRepeater)
		if not db('UIholdRepeatDisable') then
			self.UIControlTickNext = db('UIholdRepeatDelay')
			self:SetScript('OnUpdate', dpadRepeater)
			self:Show()
		end
	end

	local DpadClear = function(self)
		self:SetScript('OnUpdate', nil)
		self:Hide()
	end

	local Disable = function(self)
		self:Hide()
		Cursor:Hide()
		SetGamePadCursorControl(true)
	end

	function Cursor:GetBasicControls()
		--  @init : (optional) function to set up properties
		--  @clear: (optional) function to run when clearing
		--  @args : (optional) properties for initialization
		if not self.DpadControls then
			self.DpadControls = {
				PADDUP    = {GenerateClosure(InputProxy, 'PADDUP'),    DpadInit, DpadClear, DpadRepeater};
				PADDDOWN  = {GenerateClosure(InputProxy, 'PADDDOWN'),  DpadInit, DpadClear, DpadRepeater};
				PADDLEFT  = {GenerateClosure(InputProxy, 'PADDLEFT'),  DpadInit, DpadClear, DpadRepeater}; 
				PADDRIGHT = {GenerateClosure(InputProxy, 'PADDRIGHT'), DpadInit, DpadClear, DpadRepeater};
			};
		end
		if not self.BasicControls then
			self.BasicControls = CopyTable(self.DpadControls)
		end
		for key in pairs(self.BasicControls) do
			if not self.DpadControls[key] then
				self.BasicControls[key] = nil
			end
		end
		local dynamicKeys = {
			db('Settings/UICursorSpecial'),
		}
		for _, key in ipairs(dynamicKeys) do
			if not self.BasicControls[key] then
				self.BasicControls[key] = {GenerateClosure(InputProxy, key)}
			end
		end
		return self.BasicControls
	end

	function Cursor:SetBasicControls()
		local controls = self:GetBasicControls()
		for button, settings in pairs(controls) do
			Input:SetCommand(button, self, true, 'LeftButton', 'UIControl', unpack(settings));
		end
	end

	-- Callbacks to reset controls when inputters change
	do local function ResetControls(self) self.BasicControls = nil; end
		db:RegisterCallbacks(ResetControls, Cursor,
			'Settings/UICursorSpecial',
			'Settings/UICursorLeftClick',
			'Settings/UICursorRightClick'	
		);
	end

	-- Emulated clicks for handlers that do not use OnClick (this may be unsafe)
	local EmuClick = function(self, down)
		local node, emubtn, script = self.node, self.emubtn;
		if node then
			script =
				((down == true)  and 'OnMouseDown') or
				((down == false) and 'OnMouseUp');
			if script then
				pcall(ExecuteFrameScript, node, script, emubtn)
			end
			if (down and node.OnClick) then
				node:OnClick(emubtn)
			end
		end
	end

	local EmuClickInit = function(self, node, emubtn)
		self.node   = node;
		self.emubtn = emubtn;
	end

	local EmuClickClear = function(self)
		self.node  = nil;
		self.emubtn = nil;
	end

	function Cursor:GetEmuClick(node, button)
		return button, 'UIOnMouse', EmuClick, EmuClickInit, EmuClickClear, node, button;
	end
end

function Cursor:ReverseScanUI(node, key, target, changed)
	if node then
		local parent = node:GetParent()
		Node.ScanLocal(parent)
		target, changed = Node.NavigateToBestCandidate(self.Cur, key)
		if changed then
			return target, changed
		end
		return self:ReverseScanUI(parent, key)
	end
	return self.Cur, false
end

function Cursor:Navigate(key)
	local target, changed
	if db('UIaccessUnlimited') then
		target, changed = self:SetCurrent(self:ReverseScanUI(self:GetCurrentNode(), key))
	else
		self:ScanUI()
		target, changed = self:SetCurrent(Node.NavigateToBestCandidate(self:GetCurrent(), key))
	end
	if not changed then
		target, changed = self:SetCurrent(Node.NavigateToClosestCandidate(target, key))
	end
	return target, changed
end

function Cursor:AttemptSelectNode()
	local newObj = Node.NavigateToArbitraryCandidate(self.Cur, self.Old, self:GetCenter())
	local target, changed = self:SetCurrent(newObj)
	if target then
		if changed then
			self:SetFlashNextNode()
		end
		return self:SelectAndPosition(self:GetSelectParams(target, true))
	end
end

function Cursor:Input(key, caller, isDown)
	local target, changed
	if isDown and key then
		if not self:AttemptDragStart() then
			target, changed = self:Navigate(key)
		end
	elseif ( key == db('Settings/UICursorSpecial') ) then
		return Hooks:ProcessInterfaceCursorEvent(key, isDown, self:GetCurrentNode())
	end
	if ( target ) then
		return self:SelectAndPosition(self:GetSelectParams(target, isDown))
	end
end

---------------------------------------------------------------
-- Queries for the current node
---------------------------------------------------------------
function Cursor:SetCurrent(newObj)
	local oldObj = self:GetCurrent()
	if ( oldObj and newObj == oldObj ) then
		return oldObj, false;
	end
	self.Old = oldObj;
	self.Cur = newObj;
	return newObj, true;
end

function Cursor:GetCurrent()
	return self.Cur;
end

function Cursor:GetCurrentNode()
	local obj = self:GetCurrent()
	return obj and obj.node;
end

function Cursor:IsCurrentNode(node, uniqueTriggered)
	return (node and node == self:GetCurrentNode())
		and (not uniqueTriggered or not node:IsMouseOver())
end

function Cursor:GetCurrentObjectType()
	local obj = self:GetCurrent()
	return obj and obj.object;
end


function Cursor:IsCurrentNodeDrawn()
	local node = self:GetCurrentNode()
	return node and ( node:IsVisible() and Node.IsDrawn(node) )
end

function Cursor:IsValidForAutoScroll(super, force)
	if not super then return end;

	local old = self:GetOld()
	local oldSuper = old and old.super;
	local validSuper = force or super == oldSuper;
	return validSuper and
		not super:GetAttribute('nodeignorescroll') and
		not IsShiftKeyDown() and
		not IsControlKeyDown()
end

function Cursor:GetSelectParams(obj, triggerOnEnter, automatic)
	return obj.node, obj.object, obj.super, triggerOnEnter, automatic;
end

function Cursor:GetOld()
	return self.Old;
end

function Cursor:GetOldNode()
	local obj = self:GetOld()
	return obj and obj.node;
end

function Cursor:StoreCurrent()
	local current = self:GetCurrent()
	self.Old = current;
	self:SetCurrent(nil)
end

---------------------------------------------------------------
-- SafeOnEnter, SafeOnLeave:
-- Replace problematic OnEnter/OnLeave scripts.
-- Original functions become taint-bearing when called insecurely
-- because they modify properties of protected objects.
---------------------------------------------------------------
do local SafeOnEnter, SafeOnLeave, SafeExecute = {}, {}, ExecuteFrameScript

	-------[[  OnEnter  ]]-------
	local ActionButtonOnEnter = ActionButton1 and ActionButton1:GetScript('OnEnter')
	if ActionButtonOnEnter then
		SafeOnEnter[ActionButtonOnEnter] = function(self)
			ActionButton_SetTooltip(self)
		end
	end
	local SpellButtonOnEnter = SpellButton1 and SpellButton1:GetScript('OnEnter')
	if SpellButtonOnEnter then
		SafeOnEnter[SpellButtonOnEnter] = function(self)
			-- spellbook buttons push updates to the action bar controller in order to draw highlights
			-- on actionbuttons that holds the spell in question. this taints the action bar controller.
			local slot = SpellBook_GetSpellBookSlot(self)
			GameTooltip:SetOwner(self, 'ANCHOR_RIGHT')
			GameTooltip:SetSpellBookItem(slot, SpellBookFrame.bookType)
			
			if ( self.SpellHighlightTexture and self.SpellHighlightTexture:IsShown() ) then
				GameTooltip:AddLine(SPELLBOOK_SPELL_NOT_ON_ACTION_BAR, LIGHTBLUE_FONT_COLOR.r, LIGHTBLUE_FONT_COLOR.g, LIGHTBLUE_FONT_COLOR.b)
			end
			GameTooltip:Show()
		end
	end
	if QuestMapLogTitleButton_OnEnter then
		SafeOnEnter[QuestMapLogTitleButton_OnEnter] = function(self)
			-- this replacement script runs itself, but handles a particular bug when the cursor is atop a quest button when the map is opened.
			-- all data is not yet populated so difficultyHighlightColor can be nil, which isn't checked for in the default UI code.
			if self.questLogIndex then
				local _, level, _, isHeader, _, _, _, _, _, _, _, _, _, _, _, _, isScaling = GetQuestLogTitle(self.questLogIndex)
				local _, difficultyHighlightColor = GetQuestDifficultyColor(level, isScaling)
				if ( isHeader ) then
					_, difficultyHighlightColor = QuestDifficultyColors['header']
				end
				if difficultyHighlightColor then
					QuestMapLogTitleButton_OnEnter(self)
				end
			end
		end
	end
	if CPAPI.IsRetailVersion then
		EventUtil.ContinueOnAddOnLoaded('Blizzard_ClassTalentUI', function()
			SafeOnEnter[ClassTalentButtonSpendMixin.OnEnter] = TalentButtonSpendMixin.OnEnter;
			SafeOnEnter[ClassTalentButtonSelectMixin.OnEnter] = TalentButtonSelectMixin.OnEnter;
			SafeOnEnter[ClassTalentButtonSplitSelectMixin.OnEnter] = TalentButtonSplitSelectMixin.OnEnter;
			SafeOnEnter[ClassTalentSelectionChoiceMixin.OnEnter] = TalentDisplayMixin.OnEnter;
		end)
	end
	-------[[  OnLeave  ]]-------
	local SpellButtonOnLeave = SpellButton_OnLeave or SpellButton1 and SpellButton1:GetScript('OnLeave')
	if SpellButtonOnLeave then
		SafeOnLeave[SpellButtonOnLeave] = function(self)
			GameTooltip:Hide()
		end
	end
	if CPAPI.IsRetailVersion then
		EventUtil.ContinueOnAddOnLoaded('Blizzard_ClassTalentUI', function()
			SafeOnLeave[ClassTalentButtonSpendMixin.OnLeave] = TalentDisplayMixin.OnLeave;
			SafeOnLeave[ClassTalentButtonSelectMixin.OnLeave] = TalentButtonSelectMixin.OnLeave;
			SafeOnLeave[ClassTalentButtonSplitSelectMixin.OnLeave] = TalentButtonSplitSelectMixin.OnLeave;
			SafeOnLeave[ClassTalentSelectionChoiceMixin.OnLeave] = TalentDisplayMixin.OnLeave;
		end)
	end
	---------------------------------------------------------------
	-- Allow access to these tables for plugins and addons on demand.
	function Cursor:ReplaceOnEnter(original, replacement) SafeOnEnter[original] = replacement end
	function Cursor:ReplaceOnLeave(original, replacement) SafeOnLeave[original] = replacement end

	---------------------------------------------------------------
	-- OnEnter/OnLeave script triggers
	local function TriggerScript(node, scriptType, replacement)
		local script = replacement[node:GetScript(scriptType)]
		if script then
			pcall(script, node)
		else
			pcall(SafeExecute, node, scriptType)
		end
	end

	local function IsDisabledButton(node)
		return node:IsObjectType('Button') and not (node:IsEnabled() or node:GetMotionScriptsWhileDisabled())
	end

	function Cursor:OnLeaveNode(node)
		if node and not IsDisabledButton(node) then
			Hooks:OnNodeLeave()
			TriggerScript(node, 'OnLeave', SafeOnLeave)
		end
	end

	function Cursor:OnEnterNode(node)
		if node and not IsDisabledButton(node) then
			TriggerScript(node, 'OnEnter', SafeOnEnter)
		end
	end
end

---------------------------------------------------------------
-- Node management resources
---------------------------------------------------------------
do	local IsClickable = {
		Button 		= true;
		CheckButton = true;
		EditBox 	= true;
	}

	local DropDownMacros = {
		SET_FOCUS = '/focus %s';
		CLEAR_FOCUS = '/clearfocus';
		PET_DISMISS = '/petdismiss';
	}


	function Cursor:IsClickableNode(node, object)
		local isClickableObject = (IsClickable[object] and object ~= 'EditBox');
		if not isClickableObject then
			return false;
		end
		if node:GetScript('OnClick') then
			return true;
		end
		return not node:GetScript('OnMouseDown') and not node:GetScript('OnMouseUp')
	end

	function Cursor:GetMacroReplacement(node)
		return DropDownMacros[node.value];
	end
end

---------------------------------------------------------------
-- Node selection
---------------------------------------------------------------
function Cursor:SelectAndPosition(node, object, super, newMove, automatic)
	if newMove then
		self:OnLeaveNode(self:GetOldNode())
		self:SetPosition(node)
	end
	self:Select(node, object, super, newMove, automatic)
	return node
end

function Cursor:Select(node, object, super, triggerOnEnter, automatic)
	self:OnEnterNode(triggerOnEnter and node)

	-- Scroll to node center
	if self:IsValidForAutoScroll(super, automatic) then
		Scroll:To(node, super, self:GetOldNode(), automatic)
	end

	if (object == 'Slider') then
		-- TODO: Override:HorizontalScroll(Cursor, node)
	end

	self:SetScrollButtonsForNode(node)
	self:SetClickButtonsForNode(node,
		self:GetMacroReplacement(node),
		self:IsClickableNode(node, object)
	);
end

function Cursor:SetScrollButtonsForNode(node)
	local scrollUp, scrollDown = Node.GetScrollButtons(node)
	if scrollUp and scrollDown then
		local modifier = db('UImodifierCommands')
		self.scrollers = {
			Input:SetGlobal(format('%s-%s', modifier, 'PADDUP'), self, scrollUp:GetName(), true),
			Input:SetGlobal(format('%s-%s', modifier, 'PADDDOWN'), self, scrollDown:GetName(), true)
		};
		return scrollUp, scrollDown
	end
	if self.scrollers then
		for _, widget in ipairs(self.scrollers) do
			widget:ClearOverride(self)
		end
		self.scrollers = nil;
	end
end

function Cursor:SetClickButtonsForNode(node, macroReplacement, isClickable)
	for click, button in pairs({
		LeftButton  = db('Settings/UICursorLeftClick');
		RightButton = db('Settings/UICursorRightClick');
	}) do for modifier in db:For('Gamepad/Index/Modifier/Active') do
			if macroReplacement then
				local unit = UIDROPDOWNMENU_INIT_MENU.unit
				Input:SetMacro(modifier .. button, self, macroReplacement:format(unit or ''), true)
			elseif isClickable then
				Input:SetButton(modifier .. button, self, node, true, click)
			else
				Input:SetCommand(modifier .. button, self, true, self:GetEmuClick(node, click))
			end
		end
	end
end

function Cursor:AttemptDragStart()
	local node = self:GetCurrentNode()
	local script = node and node:GetScript('OnDragStart')
	if script then
		local widget = Input:GetActiveWidget(db('Settings/UICursorLeftClick'), self)
		local click = widget:HasClickButton()
		if widget and widget.state and click then
			widget:ClearClickButton()
			widget:EmulateFrontend(click, 'NORMAL', 'OnMouseUp')
			script(node, 'LeftButton')
			return true;
		end
	end
end

---------------------------------------------------------------
-- Cursor textures and animations
---------------------------------------------------------------
do	local f, path = format, 'Gamepad/Active/Icons/%s-64';
	-- lambdas to handle texture swapping without caching icons
	local function left  () return db('UIpointerDefaultIcon') and db(f(path, db('UICursorLeftClick'))) end
	local function mod   () return db(f(path, db('Gamepad/Index/Modifier/Key/' .. db('UImodifierCommands')) or '')) end
	local function opt   () return db(f(path, db('UICursorSpecial'))) end
	local function right () return db(f(path, db('UICursorRightClick'))) end

	Cursor.Textures = CPAPI.Proxy({
		Right    = right;
		Modifier = mod;
		-- object cases
		EditBox  = opt;
		Slider   = mod;
	}, function() return left end)
	-- remove texture evaluator so cursor refreshes on next movement
	local function resetTexture(self)
		self.textureEvaluator = nil;
		self.useAtlasIcons = db('useAtlasIcons')
	end
	db:RegisterCallback('Gamepad/Active', resetTexture, Cursor)
	db:RegisterCallback('Settings/UIpointerDefaultIcon', resetTexture, Cursor)
	db:RegisterCallback('Settings/useAtlasIcons', resetTexture, Cursor)
end

function Cursor:SetTexture(texture)
	local object = texture or self:GetCurrentObjectType()
	local evaluator = self.Textures[object]
	if ( evaluator ~= self.textureEvaluator ) then
		if self.useAtlasIcons then
			local atlas = evaluator()
			if atlas then
				self.Display.Button:SetAtlas(atlas)
			else
				self.Display.Button:SetTexture(nil)
			end
		else
			self.Display.Button:SetTexture(evaluator())
		end
	end
	self.textureEvaluator = evaluator;
end

function Cursor:SetAnchor(node)
	self.hasCustomAnchor = node.customCursorAnchor
	self.anchor = self.hasCustomAnchor or {'TOPLEFT', node, 'CENTER', Node.GetCenterPos(node)}
end

function Cursor:GetCustomAnchor()
	return self.hasCustomAnchor
end

function Cursor:GetAnchor()
	return self.anchor
end

function Cursor:RefreshAnchor()
	if not self:GetCustomAnchor() then
		local node = self:GetCurrentNode()
		self:ClearAllPoints()
		self:SetPoint('CENTER', node, 'CENTER', Node.GetCenterPos(node))
	end
end

function Cursor:MoveTowardsAnchor(elapsed)
	if not self:GetCustomAnchor() then
		local divisor = 4 - elapsed; -- 4 is about right, account for FPS
		local cX, cY = self:GetLeft(), self:GetTop()
		local nX, nY = Node.GetCenter(self:GetCurrentNode())
		self:ClearAllPoints()
		if cX and cY then
			return self:SetPoint('TOPLEFT', UIParent, 'BOTTOMLEFT',
				cX + ((nX - cX) / divisor),
				cY + ((nY - cY) / divisor)
			);
		end
		self:SetPoint('TOPLEFT', self:GetCurrentNode(), 'CENTER', nX, nY)
	end
end

function Cursor:SetPosition(node)
	self:SetTexture()
	self:SetAnchor(node)
	self:Show()
	self:Move()
end

function Cursor:Move()
	local node = self:GetCurrentNode()
	if node then
		self:ClearHighlight()
		local newX, newY = Node.GetCenter(node)
		local oldX, oldY = self:GetCenter()
		if oldX and oldY and newX and newY and self:IsVisible() then
			self.Enlarge:SetStartDelay(0.05)
			self.ScaleInOut:ConfigureScale()
			self:Chime()
		else
			self.Enlarge:SetStartDelay(0)
		end
		self:SetHighlight(node)
	end
end

function Cursor:Chime()
	PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON, 'Master', false, false)
end

function Cursor:UpdatePointer()
	self.Display:SetSize(db('UIpointerSize'))
	self.Display:SetOffset(db('UIpointerOffset'))
	self.Display:SetRotationEnabled(db('UIpointerAnimation'))
	self.Display.animationSpeed = db('UItravelTime');
end

db:RegisterCallbacks(Cursor.UpdatePointer, Cursor,
	'Settings/UItravelTime',
	'Settings/UIpointerSize',
	'Settings/UIpointerOffset',
	'Settings/UIpointerAnimation'
);

-- Highlight mime
---------------------------------------------------------------
function Cursor:ClearHighlight()
	self.Mime:Clear()
end


function Cursor:SetHighlight(node)
	if node and (not node.IsEnabled or node:IsEnabled()) then
		self.Mime:SetNode(node)
	else
		self:ClearHighlight()
	end
end

function Cursor.Mime:SetFontString(region)
	if region:IsShown() and region:GetFont() then
		local obj = self.Fonts:Acquire()
		obj:SetFont(obj.GetFont(region))
		obj:SetText(obj.GetText(region))
		obj:SetTextColor(obj.GetTextColor(region))
		obj:SetJustifyH(obj.GetJustifyH(region))
		obj:SetJustifyV(obj.GetJustifyV(region))
		obj:SetSize(obj.GetSize(region))
		for i=1, obj.GetNumPoints(region) do
			obj:SetPoint(obj.GetPoint(region, i))
		end
		obj:Show()
	end
end

function Cursor.Mime:SetTexture(region)
	if region:IsShown() then
		local obj = self.Textures:Acquire()
		if obj.GetAtlas(region) then
			obj:SetAtlas(obj.GetAtlas(region))
		else
			local texture = obj.GetTexture(region)
			-- DEPRECATED: returns File Data ID <num> in 9.0
			if (type(texture) == 'string') and texture:find('^[Cc]olor-') then
				obj:SetColorTexture(CPAPI.Hex2RGB(texture:sub(7), true))
			else
				obj:SetTexture(texture)
			end
		end
		obj:SetBlendMode(obj.GetBlendMode(region))
		obj:SetTexCoord(obj.GetTexCoord(region))
		obj:SetVertexColor(obj.GetVertexColor(region))
		obj:SetSize(obj.GetSize(region))
		for i=1, obj.GetNumPoints(region) do
			obj:SetPoint(obj.GetPoint(region, i))
		end
		obj:Show()
	end
end

function Cursor.Mime:SetNode(node)
	self:MimeRegions(node:GetRegions())
	self:ClearAllPoints()
	self:SetSize(node:GetSize())
	self:SetScale(node:GetEffectiveScale() / Cursor:GetEffectiveScale())
	self:Show()
	for i=1, node:GetNumPoints() do
		self:SetPoint(node:GetPoint(i))
	end
	self.Scale:Stop()
	self.Scale:Play()
end

function Cursor.Mime:Clear()
	self.Fonts:ReleaseAll()
	self.Textures:ReleaseAll()
	self:Hide()
end

function Cursor.Mime:MimeRegions(region, ...)
	if region then
		if (region:GetDrawLayer() == 'HIGHLIGHT') then
			if (region:GetObjectType() == 'Texture') then
				self:SetTexture(region)
			elseif (region:GetObjectType() == 'FontString') then
				self:SetFontString(region)
			end
		end
		self:MimeRegions(...)
	end
end

-- Animation scripts
---------------------------------------------------------------
function Cursor:SetFlashNextNode()
	self.ScaleInOut.Flash = true;
end

function Cursor.ScaleInOut:ConfigureScale()
	local cur, old = Cursor:GetCurrent(), Cursor:GetOld()
	if (cur == old) and not self.Flash then
		self.Shrink:SetDuration(0)
		self.Enlarge:SetDuration(0)	
	elseif cur then
		local scaleAmount, shrinkDuration = 1.15, 0.2
		if self.Flash then
			scaleAmount = 1.75
			shrinkDuration = 0.5
		end
		self.Flash = nil
		self.Enlarge:SetScale(scaleAmount, scaleAmount)
		self.Shrink:SetScale(1/scaleAmount, 1/scaleAmount)
		self.Shrink:SetDuration(shrinkDuration)
		self.Enlarge:SetDuration(.1)
	end
end

function Cursor.Mime.Scale:OnPlay()
	self.Enlarge:SetScale(Cursor.ScaleInOut.Enlarge:GetScale())
	self.Shrink:SetScale(Cursor.ScaleInOut.Shrink:GetScale())

	self.Enlarge:SetDuration(Cursor.ScaleInOut.Enlarge:GetDuration())
	self.Shrink:SetDuration(Cursor.ScaleInOut.Shrink:GetDuration())

	self.Enlarge:SetStartDelay(Cursor.ScaleInOut.Enlarge:GetStartDelay())
	self.Shrink:SetStartDelay(Cursor.ScaleInOut.Shrink:GetStartDelay())
end

function Cursor.ScaleInOut:OnPlay()
	Cursor.Mime:SetParent(Cursor:GetCurrentNode() or Cursor)
end

do  -- Set up animation scripts
	local animationGroups = {Cursor.ScaleInOut, Cursor.Mime.Scale}

	local function setupScripts(w) 
		for k, v in pairs(w) do 
			if w:HasScript(k) then w:SetScript(k, v) end
		end
	end

	for _, group in pairs(animationGroups) do
		setupScripts(group)
		for _, animation in pairs({group:GetAnimations()}) do
			setupScripts(animation)
		end
	end

	-- Convenience references to animations
	Cursor.Enlarge = Cursor.ScaleInOut.Enlarge;
	Cursor.Shrink  = Cursor.ScaleInOut.Shrink;
end

---------------------------------------------------------------
-- Scroll management
---------------------------------------------------------------
local Clamp = Clamp;

function Scroll:OnUpdate(elapsed)
	for super, target in pairs(self.Active) do
		local currHorz, currVert = super:GetHorizontalScroll(), super:GetVerticalScroll()
		local maxHorz, maxVert = super:GetHorizontalScrollRange(), super:GetVerticalScrollRange()
		-- close enough, stop scrolling and set to target
		if ( abs(currHorz - target.horz) < 2 ) and ( abs(currVert - target.vert) < 2 ) then
			super:SetVerticalScroll(target.vert)
			super:SetHorizontalScroll(target.horz)
			self.Active[super] = nil
			return
		end
		local deltaX, deltaY = ( currHorz > target.horz and -1 or 1 ), ( currVert > target.vert and -1 or 1 )
		local newX = ( currHorz + (deltaX * abs(currHorz - target.horz) / 16 * 4) )
		local newY = ( currVert + (deltaY * abs(currVert - target.vert) / 16 * 4) )

		super:SetVerticalScroll(Clamp(newY, 0, maxVert))
		super:SetHorizontalScroll(Clamp(newX, 0, maxHorz))
	end
	if not next(self.Active) then
		self:SetScript('OnUpdate', nil)
	end
end

function Scroll:To(node, super, prev, force)
	local nodeX, nodeY = Node.GetCenter(node)
	local scrollX, scrollY = super:GetCenter()
	if nodeY and scrollY then

		if self:IsValidScrollFrame(super) then
			local currHorz, currVert = super:GetHorizontalScroll(), super:GetVerticalScroll()
			local maxHorz, maxVert = super:GetHorizontalScrollRange(), super:GetVerticalScrollRange()

			local prevX, prevY = nodeX, nodeY;
			if prev then
				prevX, prevY = Node.GetCenter(prev)
			end

			if not self.Active then
				self.Active = {}
			end

			self.Active[super] = {
				vert = Clamp(self:GetScrollTarget(currVert, scrollY, nodeY, prevY, force), 0, maxVert),
				horz = Clamp(0, 0, maxHorz), -- TODO: solve horizontal scrolling
			}

			self:SetScript('OnUpdate', self.OnUpdate)
		end
	end
end

function Scroll:GetScrollTarget(curr, scrollPos, nodePos, prevPos, force)
	local new = curr + (scrollPos - nodePos)
	return force and new or (new > curr) == (nodePos > prevPos) and curr or new;
end

function Scroll:IsValidScrollFrame(super)
	-- HACK: make sure this isn't a hybrid scroll frame
	return super:IsObjectType('ScrollFrame') and
		super:GetScript('OnLoad') ~= HybridScrollFrame_OnLoad;
end

---------------------------------------------------------------
-- Initialize the cursor
---------------------------------------------------------------
CPAPI.Start(Cursor)
hooksecurefunc('CanAutoSetGamePadCursorControl', function(state)
	-- TODO: work on this, it's not good yet
	if not state then
	--	Cursor:SetEnabled(state)
	end
end)

hooksecurefunc('ShowUIPanel', function(frame)
	if not Cursor:InCombat() then
	--	Cursor:RefreshToFrame(frame)
	end
end)