---------------------------------------------------------------
-- Mouse.lua: Smart camera control and mouse function wrappers
---------------------------------------------------------------
-- Removes the need of holding right click to control camera.
-- Allows user to mouseover their character to control camera.
-- Toggles off on targeted spells, InteractUnit and pickups.

local _, db = ...
---------------------------------------------------------------
local CPAPI, TEXTURE, ICONS, Settings = db.CPAPI, db.TEXTURE, db.ICONS
---------------------------------------------------------------
local 	WorldFrame, UIParent, GameTooltip, Core = 
		WorldFrame, UIParent, GameTooltip, ConsolePort
---------------------------------------------------------------
-- Camera functions
local 	GetMouseFocus, HasCursorItem, SpellIsTargeting, IsMouseButtonDown, IsMouselooking = 
		GetMouseFocus, GetCursorInfo, SpellIsTargeting, IsMouseButtonDown, IsMouselooking
---------------------------------------------------------------
-- Highlight functions
local 	HighlightStart, HighlightStop = 
		TargetPriorityHighlightStart, TargetPriorityHighlightEnd
---------------------------------------------------------------
-- Mouse functions
local 	UnitGUID, UnitIsDead, UnitCanAttack, UnitExists, GetCursorPosition, GetScreenWidth, SetPortrait, SetCVarr = 
		UnitGUID, UnitIsDead, UnitCanAttack, UnitExists, CPAPI.GetScaledCursorPosition, GetScreenWidth, SetPortraitTexture, SetCVar
---------------------------------------------------------------
local 	Camera, numTap, modTap, timer, interactPushback, highlightTimer = ConsolePortCamera, 0, 0, 0, 0, 0
---------------------------------------------------------------
local blockCursor, cameraMode, isMouseDown, isCentered, isOutside, isTargeting, hasItem, hasWorldFocus, wasMouseLooking

---------------------------------------------------------------
-- Extended API:
---------------------------------------------------------------
-- All functions in this section requires AwesomeWotlkLib or at least a implementation of C_NamePlate

-- Loot caching for our custom implementation of CanLootUnit
local guidToUnit = db.guidToUnit or {}
db.guidToUnit = guidToUnit 

local hadLoot = {}
local function SetHadLoot(guid, value, count)
    hadLoot[guid] = {v = value, t = GetTime(), count = count or 0}
end

local function GetHadLoot(guid)
    local entry = hadLoot[guid]
    if not entry then return nil end
    if GetTime() - entry.t > 300 then
        hadLoot[guid] = nil
        return nil
    end
    return entry.v
end

local lootFrame = CreateFrame('Frame')
lootFrame:RegisterEvent('LOOT_OPENED')
lootFrame:RegisterEvent('LOOT_SLOT_CLEARED')
lootFrame:RegisterEvent('LOOT_CLOSED')
lootFrame:SetScript('OnEvent', function(self, event)
    if event == 'LOOT_OPENED' then
        local guid = UnitGUID('target')
        if guid then
            local count = GetNumLootItems()
            SetHadLoot(guid, count > 0, count)
        end
    elseif event == 'LOOT_SLOT_CLEARED' then
        local guid = UnitGUID('target')
        local entry = guid and hadLoot[guid]
        if entry then
            entry.count = math.max(0, entry.count - 1)
            if entry.count == 0 then
                entry.v = false
            end
        end
    elseif event == 'LOOT_CLOSED' then
        -- nothing to do, state already correct from above events
    end
end)


-- Custom CanInteract using IsItemInRange with a 5 yard usage item (Ruby Acorn) with a fallback of CheckInteractDistance() which
-- has 11 yards distance check.

local interactRangeItem = "item:37727"
local function CanInteract(unit)
    if unit then 
		local isInRange
		if GetItemInfo(interactRangeItem) then
			isInRange = IsItemInRange(interactRangeItem, unit)

			if(isInRange ~= nil) then
				return isInRange == 1
			end
		end
		return CheckInteractDistance(unit, 3) 
	end
end

local function CanInteractGUID(guid)
    if not guid then return nil end
    -- try cached unit token first
    local unit = guidToUnit[guid]
    if unit and UnitExists(unit) then
        return CanInteract(unit) or nil
    end
    -- fallback: scan nameplates
    if C_NamePlate and C_NamePlate.GetNamePlates then
        for _, plate in ipairs(C_NamePlate.GetNamePlates()) do
            local u = plate.namePlateUnitToken
            if u and UnitGUID(u) == guid then
                guidToUnit[guid] = u
                return CanInteract(u) or nil
            end
        end
    end
    return nil
end

local function CanLootUnit(guid)
	local UnitTokenFromGUID_Compat

	if C_NamePlate and C_NamePlate.GetNamePlateForUnit then
		if _G.UnitTokenFromGUID then
			UnitTokenFromGUID_Compat = _G.UnitTokenFromGUID
		elseif _G.GetTokenIDsFromGUID then
			UnitTokenFromGUID_Compat = function(guid)
				local tokens = _G.GetTokenIDsFromGUID(guid)
				return tokens and tokens[1]
			end
		end
	end

	if not UnitTokenFromGUID_Compat then return false, false end

	local unit = UnitTokenFromGUID_Compat(guid)

    if not unit or not UnitExists(unit) then return false, false end
    local isDead = UnitIsDead(unit)
    if not isDead then return false, false end

    local inRange = IsItemInRange(interactRangeItem, unit)
    if inRange ~= nil then
        inRange = inRange == 1
    else
        inRange = CheckInteractDistance(unit, 3)
    end

    local notTappedByOther = not UnitIsTapped(unit) or UnitIsTappedByPlayer(unit) 
    local canLoot = isDead and inRange and notTappedByOther 

	local knownHasLoot = GetHadLoot(UnitGUID(unit))
	local hasLoot = knownHasLoot == nil and canLoot or knownHasLoot

    return hasLoot, canLoot
end

local function CanInteractOrLoot(guid)
    return CanInteractGUID(guid)
end 

local function UnitIsGUID(unit, queryGUID)
	local guid = unit and UnitGUID(unit)
	return (guid == queryGUID)
end
---------------------------------------------------------------

function Camera:Toggle() if cameraMode then self:Stop() else self:Start() end end
function Camera:IsCentered() return self.Locker:IsMouseOver() and not self.Deadzone:IsMouseOver() end
function Camera:ShouldStart() return not isTargeting and not isMouseDown and not hasItem and self:IsCentered() and hasWorldFocus end
function Camera:OnJump() if Settings.mouseOnJump and not isTargeting and not hasItem then Camera:Start() end end
function Camera:OnPlayerMove() if db('Mouse/Events/PLAYER_STARTED_MOVING') and not isTargeting and not hasItem then Camera:Start() end end

function Camera:OnUpdate(elapsed)
	cameraMode = IsMouselooking()
	isTargeting = SpellIsTargeting()
	isMouseDown = IsMouseButtonDown(1)
	hasItem = HasCursorItem()
	hasWorldFocus = GetMouseFocus() == WorldFrame
	---------------
	CPAPI.SetShown(self.BlockUI,cameraMode)
	---------------
	interactPushback = interactPushback > 0 and interactPushback - elapsed or 0
end

local yawDeadZone, yawSmoothOut, yawSmoothIn, yawMaxAngle
do
	local viewPortCenter, mousePos, offset, newAngle
	local FlipCameraYaw, yawFlipped = FlipCameraYaw, .0

	local function AdjustCameraYaw(yawOffset, actionCamOffset)
		FlipCameraYaw(yawOffset)
		--SetCVar('test_cameraOverShoulder', -actionCamOffset * (actionCamOffset > 0 and .25 or .15))
	end

	function Camera:CalculateYaw()
		if not cameraMode then
			viewPortCenter = ( GetScreenWidth() / 2 )
			mousePos = GetCursorPosition()
			offset = - ( ( mousePos - viewPortCenter ) / 360 )
			if abs(offset) > yawDeadZone then
				newAngle = yawFlipped + (offset * yawSmoothIn)
				if newAngle < yawMaxAngle and newAngle > -yawMaxAngle then
					yawFlipped = newAngle
					AdjustCameraYaw(offset * yawSmoothIn, yawFlipped)
				end
			end
		elseif yawFlipped ~= 0 then
			offset = -yawFlipped * yawSmoothOut
			yawFlipped = yawFlipped + offset
			AdjustCameraYaw(offset, yawFlipped)
			if abs(yawFlipped) < .5 then
				AdjustCameraYaw(-yawFlipped, 0)
				yawFlipped = 0
			end
		end
	end
end

function Camera:CheckCursor()
	if cameraMode and hasItem then
		self:Stop()
	end
end

function Camera:CheckCenter()
	if not isCentered and self:ShouldStart() then
		self:Start()
		isCentered = true
	elseif not self.Locker:IsMouseOver() and isCentered then
		isCentered = false
	end
end

function Camera:CheckDoubleTap(elapsed)
	timer = timer + elapsed
	if numTap > 1 then
		self:Toggle()
		modTap = 0
		numTap = 0
	end
	if timer > self.modTapWindow then
		numTap = numTap > 0 and numTap - 1 or 0
		timer = timer - self.modTapWindow
	end
end

function Camera:CheckEdge()
	if not self.Edge:IsMouseOver() and not isOutside and hasWorldFocus then
		isOutside = true
		self:Start()
	elseif self.Edge:IsMouseOver() then
		isOutside = false
	end
end

function Camera:HighlightNoTarget(elapsed)
	highlightTimer = highlightTimer + elapsed
	if highlightTimer > 3 then
		if not UnitExists('target') then
			--HighlightStart()
		end
		highlightTimer = highlightTimer - 3
	end
end

function Camera:HighlightAlways(elapsed)
	highlightTimer = highlightTimer + elapsed
	if highlightTimer > 3 then
		--HighlightStart()
		highlightTimer = highlightTimer - 3
	end
end

function Camera:OnModifierChanged(_, modifier, down)
	if down == 1 then
		if modifier then
			timer = 0
			numTap = modTap == modifier and numTap + 1 or 1
			modTap = modifier
		end
	end
end

function Camera:OnAction() interactPushback = ( Settings.interactWith and (Settings.interactPushback or 1) ) or 0 end

function Camera:OnInteract()
	local guid, canInteract = UnitGUID('target') 
	if guid then
		--canInteract = CanLootUnit(guid) or GetCVar('autoInteract') == '1' --CheckInteractDistance('target', 5)
		canInteract = CheckInteractDistance('target', 3) or GetCVar('autoInteract') == '1' 
	end
	if canInteract then
		blockCursor = true
		Camera:Start()
	else
		Camera:Stop()
	end
end

function Camera:OnStop() if blockCursor then blockCursor = nil Camera:Start() end end
function Camera:OnRightClick() if interactPushback > 0 then Camera:Start() else blockCursor = nil end end

function Camera:OnLeftClickDown()
	if Camera.lookAround then
		wasMouseLooking = IsMouselooking()
		if wasMouseLooking then
			Camera:Stop()
		end
	end
end

function Camera:OnLeftClickUp()
	if wasMouseLooking and Camera.lookAround and not isTargeting then
		Camera:Start()
	end
	wasMouseLooking = nil
end

-----------------------------------------------------------------------
-- Function hooks to create custom mouselook triggers without any event
-----------------------------------------------------------------------
for func, hook in pairs({
	-- Get rid of mouselook when trying to interact with mouse
	MouselookStop = Camera.OnStop,
	-- InteractUnit removes mouse look, restart if target has loot
	InteractUnit = Camera.OnInteract,
	-- Releasing 'right click' should remove the cursor block
	TurnOrActionStop = Camera.OnRightClick,
	-- Pressing left click should remove mouse look if configured
	CameraOrSelectOrMoveStart = Camera.OnLeftClickDown,
	-- Releasing left click should restart the camera if configured
	CameraOrSelectOrMoveStop = Camera.OnLeftClickUp,
	-- Hook jump to use it as a camera trigger
	JumpOrAscendStart = Camera.OnJump,
	-- Get rid of mouselook when moving the pet
	--PetMoveTo = Camera.Stop,
	-- Get rid of mouselook when moving
	MoveForwardStart = Camera.OnPlayerMove,
	MoveBackwardStart = Camera.OnPlayerMove,
	StrafeLeftStart = Camera.OnPlayerMove,
	StrafeRightStart = Camera.OnPlayerMove,
	TurnLeftStart = Camera.OnPlayerMove,
	TurnRightStart = Camera.OnPlayerMove,
	-- Hook action usage to manipulate mouselook
	UseAction = Camera.OnAction, 
}) do hooksecurefunc(func, hook) end

---------------------------------------------------------------
-- Mouse function wrappers in case of extended functionality
---------------------------------------------------------------

function Core:StopCamera() Camera:Stop() end
function Core:StartCamera() Camera:Start() end

---------------------------------------------------------------
-- Toggle smart mouse behaviour on/off
---------------------------------------------------------------
function Core:UpdateCameraDriver()
	Settings = Settings or db.Settings

	Camera:SetScript('OnEvent', nil)
	Camera:SetScript('OnUpdate', Camera.OnUpdate)
	Camera:UnregisterEvent('MODIFIER_STATE_CHANGED')

	Camera.Locker:SetSize(Settings.centerLockRangeX or 70, Settings.centerLockRangeY or 180)
	Camera.Deadzone:SetSize(Settings.centerLockDeadzoneX or 4, Settings.centerLockDeadzoneY or 4)

	numTap, modTap, timer, interactPushback, highlightTimer = 0, 0, 0, 0, 0
	Camera.lookAround = nil

	if not Settings.disableSmartMouse then
		Camera:HookScript('OnUpdate', Camera.CheckCursor)

		if Settings.doubleModTap then
			Camera.modTapWindow = db.Settings.doubleModTapWindow or 0.25
			Camera:SetScript('OnEvent', Camera.OnModifierChanged)
			Camera:RegisterEvent('MODIFIER_STATE_CHANGED')
		end

		local dynamicYaw
		if db.Mouse.Camera then
			Camera.lookAround = db.Mouse.Camera.lookAround
			dynamicYaw = db.Mouse.Camera.calculateYaw
		end

		if dynamicYaw then
			yawDeadZone  = Settings.cameraYawDeadzone 	or .8
			yawSmoothOut = Settings.cameraYawSmoothOut 	or .085
			yawSmoothIn  = Settings.cameraYawSmoothIn 	or .155
			yawMaxAngle  = Settings.cameraYawMaxAngle 	or 30
		end

		for _, script in pairs({
			-- trigger when cursor is centered
			Settings.mouseOnCenter and Camera.CheckCenter,
			-- trigger when cursor is at the edge of the screen
			Settings.preventMouseDrift and Camera.CheckEdge,
			-- trigger when double tapping a modifier
			Settings.doubleModTap and Camera.CheckDoubleTap,
			-- rotate yaw dynamically when cursor is out
			(dynamicYaw) and Camera.CalculateYaw,
		}) do
			if script then
				Camera:HookScript('OnUpdate', script)
			end
		end
	end

	if Settings.alwaysHighlight == 1 then
		Camera:HookScript('OnUpdate', Camera.HighlightNoTarget)
	elseif Settings.alwaysHighlight == 2 then
		Camera:HookScript('OnUpdate', Camera.HighlightAlways)
	end
	Camera.lockOnLoot = db.Mouse.Events.LOOT_OPENED
end

---------------------------------------------------------------
-- This handle is used to determine whether a dedicated button
-- should be used to cast spells or to interact with mouseover.
-- The behaviour alters itself depending on whether the button
-- is bound to a healing spell, harmful spell or binding.
---------------------------------------------------------------
local Mouse = ConsolePortMouseHandle
Mouse:Execute([[ id, isEnabled = 0, true ]])
Mouse.FadeInRef, Mouse.FadeOutRef = db.GetFaders()

for name, script in pairs({
	_onattributechanged = [[
		if name == 'state-targetstate' then
			control:RunFor(self, self:GetAttribute('UpdateTarget'), value)
		elseif name == 'state-vehicle' then
			control:RunFor(self, self:GetAttribute('UpdateVehicle'), value)
		elseif name == 'blockhandle' then
			isEnabled = not value
			if not isEnabled then
				self:ClearBindings()
				control:CallMethod('TrackUnit', nil, true)
			else
				control:RunFor(self, self:GetAttribute('UpdateTarget'), target)
			end
		end
	]],
	UpdateVehicle = [[
		inVehicle = ...
		if inVehicle then
			self:ClearBindings()
		else
			control:RunFor(self, self:GetAttribute('UpdateTarget'), target)
		end
	]],
	Clear = [[
		local clearType = ...
		if clearType then
			local key = GetBindingKey(clearType)
			if key then
				self:ClearBinding(key)
			end
		end
	]],
	Set = [[
		local setType, binding = ...
		if setType and binding then
			local key = GetBindingKey(setType)
			if key then
				self:SetBinding(true, key, binding)
			end
		end
	]],
	UpdateTarget = [[
		target = ...
		self:SetAttribute('current', target)
		self:SetAttribute('npc', nil)
		self:ClearBindings()

		if inVehicle or not isEnabled then return end

		local interact, loot, npc

		if ( target == 'hover' ) then
			interact = true
			if checkNPC and ( not PlayerCanAttack('mouseover') and not PlayerCanAssist('mouseover') ) then
				self:SetAttribute('npc', true)
			end
		elseif checkNPC and ( target == 'friend' and not PlayerCanAssist('target') ) then
			target = 'npc'
			npc = true
		elseif ( target == 'enemy' or target == 'friend' ) then 
			if 	( target == 'friend' and control:RunFor(self, self:GetAttribute('IsHarmfulAction'), id) ) or
				( target == 'enemy' and control:RunFor(self, self:GetAttribute('IsHelpfulAction'), id) ) then
				interact = true 
			end 
		elseif target == 'loot' then 
			loot = true
		else
			interact = true
		end

		if(CXPKEY and CXPBINDING) then
			control:RunFor(self, self:GetAttribute('Set'), CXPKEY, CXPBINDING)
		end

		if (( interact or loot or npc ) and USEKEY) or (loot and LOOTKEY) then
			if loot and LOOTKEY then 
				control:RunFor(self, self:GetAttribute('Set'), LOOTKEY, 'INTERACTTARGET')
			end     
			control:RunFor(self, self:GetAttribute('Set'), USEKEY, ( (loot or npc) and 'INTERACTTARGET' or 'TURNORACTION' ) )  
		end
		control:CallMethod('CallMethodFromFrame', self:GetName(), 'TrackUnit', target)
	]],
}) do Mouse:SetAttribute(name, script) end


-- Workaround Issue
 

Mouse.CallMethodFromFrame = CPAPI.CallMethodFromFrame
--
---------------------------------------------------------------
-- Display functions
---------------------------------------------------------------

function Mouse:FadeIn(speed)
	if self.fade ~= 'in' then
		self:FadeInRef(speed or 0.1, self:GetAlpha(), 1)
		self.fade = 'in'
	end
end

function Mouse:FadeOut(speed)
	if self.fade ~= 'out' then
		self:FadeOutRef(speed or 0.2, self:GetAlpha(), 0, {
			finishedFunc = self.SetHintAnchors;
			finishedArg1 = self;
		})
		self.fade = 'out'
	end
end

function Mouse:SetPortrait(unit)
	SetPortrait(self.Portrait, unit)
end

function Mouse:TogglePortrait(enabled)
	self.Portrait:SetAlpha(enabled and 1 or 0)
	self.PortraitMask:SetAlpha(enabled and 1 or 0)
end

function Mouse:UpdateMouseover()
	SetPortrait(self.Portrait, 'mouseover')
end

function Mouse:SetIcon(icon)
	self.Button:SetTexture(ICONS[icon])
end

function Mouse:IsOnPlateDisplayPossible(plate)
	return plate and plate.UnitFrame and plate.UnitFrame.name and plate.UnitFrame.name:IsVisible()
end

function Mouse:SetHintAnchors(plate)
	if InCombatLockdown() or ( plate and self.anchorPlate == plate ) then
		return
	end
	local displayOnPlate = self:IsOnPlateDisplayPossible(plate)
	local lineAlpha = db('interactHintLineVis')
	CPAPI.SetShown(self.Text, not displayOnPlate and true)
	self.Line:SetParent(displayOnPlate and plate or self)
	self.Line:SetGradientAlpha('VERTICAL',
		1, 1, 1, not displayOnPlate and lineAlpha or 1,
		1, 1, 1, not displayOnPlate and lineAlpha or 0)
	--self.Line:SetScale(displayOnPlate and self:GetEffectiveScale() or 1)
	CPAPI.SetShown(self.Line, displayOnPlate and true or not (db'interactHintNoLine'))
	self.anchorPlate = plate
	---------------------
	self:ClearAllPoints()
	self.Button:ClearAllPoints()
	CPAPI.SetShown(ConsolePortTargetAISelector.TopArrow, not displayOnPlate)
	if displayOnPlate then
		self:SetPoint('CENTER', plate, 0, -8)
		--self.Button:SetPoint('BOTTOM', plate.UnitFrame.name, 'TOP', 0, 0)
		local healthBar = plate:GetChildren() 
		self.Button:SetPoint('BOTTOM', healthBar, 'TOP', 0, 0)
	else
		self:SetPoint('BOTTOM', 0, db('interactHintPosition'))
		self.Button:SetPoint('RIGHT', self.Text, 'LEFT', -15, 0)
	end
end

function Mouse:SetNameplateAnchor(targetGUID)
	if not db('interactHintNoSticky') then
		if not C_NamePlate then return end 
		local UnitTokenFromGUID_Compat

		if C_NamePlate and C_NamePlate.GetNamePlateForUnit then
			if _G.UnitTokenFromGUID then
				UnitTokenFromGUID_Compat = _G.UnitTokenFromGUID
			elseif _G.GetTokenIDsFromGUID then
				UnitTokenFromGUID_Compat = function(guid)
					local tokens = _G.GetTokenIDsFromGUID(guid)
					return tokens and tokens[1]
				end
			end
		end

		if not UnitTokenFromGUID_Compat then return end
		token = UnitTokenFromGUID_Compat(targetGUID)
		
		if token then
			local plate = C_NamePlate.GetNamePlateForUnit(token)
			if plate then
				return self:SetHintAnchors(plate)
			end
		end
	end
end

---------------------------------------------------------------
-- Insecure override wrappers
---------------------------------------------------------------

function Mouse:HasInsecureOverride()
	return self.insecureOverrideActive
end

function Mouse:ToggleInsecureOverride(enabled, key, binding)
	if enabled and not Core:HasUIFocus() then
		self.insecureOverrideActive = true
		SetOverrideBinding(self, true, key, binding)
	else
		self.insecureOverrideActive = false
		ClearOverrideBindings(self)
	end
end

function Mouse:SetOverride(binding, bindingID)
	if not InCombatLockdown() then
		local key = bindingID and GetBindingKey(bindingID)
		if key then
			self:ToggleInsecureOverride(true, key, binding)
			if binding then
				self:RegisterEvent('PLAYER_REGEN_DISABLED')
				self:SetScript('OnEvent', self.ClearOverride)
				self:SetIcon(bindingID)
			else
				self:UnregisterEvent('PLAYER_REGEN_DISABLED')
				self:SetScript('OnEvent', nil)
			end
		end
	end
end

function Mouse:ClearOverride()
	if not InCombatLockdown() then
		self:SetHintAnchors(nil)
		self:ToggleInsecureOverride(false)
		self:SetIcon(self.interactWith)
		self:UnregisterEvent('PLAYER_REGEN_DISABLED')
		self:SetScript('OnEvent', nil)
	end
end

function Mouse:ClearScriptOverride()
	if ( self.override == self.interactWith ) then
		-- if the full interact button is enabled, any OnUpdate script
		-- should revert to TURNORACTION, else just clear.
		self:SetOverride('TURNORACTION', self.override)
	else
		self:ClearOverride()
	end
end

---------------------------------------------------------------
-- OnUpdate scripts:
--	TrackSecureNPC: visual in range indicator
--	TrackSecureHover: visual in range indicator
--  TrackInsecureLootTarget: interact hostile override
--  TrackInsecureNPC: interact friendly override
--	TrackInsecureLootVicinity: last hostile loot override
--	TrackArtificialUnit: smart interaction override (insecure)
---------------------------------------------------------------
function Mouse:TrackSecureNPC(elapsed)
	local canInteract = CanInteract('target')
	if ( canInteract ) and not ( UnitExists('npc') or UnitExists('questnpc') ) then
		if canInteract then
			self.Text:SetText(UNIT_FRAME_DROPDOWN_SUBSECTION_TITLE_INTERACT)
		end
		self:FadeIn()
	else
		self:FadeOut()
	end
end

function Mouse:TrackSecureHover(elapsed)
	local guid, hasLoot, canLoot = UnitGUID('mouseover')
	local exists = UnitExists('mouseover')
	local isDead, isEnemy = UnitIsDead('mouseover'), UnitCanAttack('player', 'mouseover')
	if guid then
		hasLoot, canLoot = CanLootUnit(guid) 
	end
	if hasLoot and canLoot then
		self.Text:SetText("Loot")
	elseif isEnemy and not isDead then
		self.Text:SetText(ATTACK)
	elseif exists and ( isDead or not isEnemy ) then
		self.Text:SetText("Interact")
	end
	self:FadeIn()
end

function Mouse:TrackInsecureLootTarget(elapsed)
	local guid, hasLoot, canLoot = UnitGUID('target')
	if guid then
		hasLoot, canLoot = CanLootUnit(guid)
	end
	if hasLoot and canLoot then
		self:SetOverride('INTERACTTARGET', self.override)
		self.Text:SetText(LOOT)
		self:FadeIn()
	else
		if self.override then
			self:SetOverride(nil, self.override)
		end
		self:FadeOut()
		if not hasLoot then
			self:SetScript('OnUpdate', nil)
		end
	end
end

function Mouse:TrackInsecureNPC(elapsed)
	local guid, _, canInteract = UnitGUID('target')
	if guid then
		_, canInteract = CanLootUnit(guid)
	end
	if canInteract then
		self:SetOverride('INTERACTTARGET', self.override)
		self.Text:SetText(UNIT_FRAME_DROPDOWN_SUBSECTION_TITLE_INTERACT)
		self:FadeIn()
	else
		if self.override then
			self:SetOverride(nil, self.override)
		end
		self:FadeOut()
		if not hasLoot then
			self:SetScript('OnUpdate', nil)
		end
	end
end

function Mouse:TrackInsecureLootVicinity(elapsed)
	local hasLoot, canLoot = CanLootUnit(self.cachedUnit)
	if hasLoot and canLoot then
		self:SetOverride('TARGETLASTHOSTILE', self.override)
		self.Text:SetText("Loot")
		self:FadeIn()
	else
		self:ClearScriptOverride()
		self:FadeOut()
		if not hasLoot then
			self:SetScript('OnUpdate', nil)
		end
	end
end

function Mouse:TrackArtificialUnit(elapsed)
	local canInteract = CanInteractGUID(self.artificial)
	if canInteract then
		local guidMatch = UnitIsGUID('target', self.artificial)
		local isCurrentNPC = UnitIsUnit('target', 'npc') or UnitIsUnit('target', 'questnpc')
		if isCurrentNPC then
			self:ClearOverride()
			self:FadeOut()
			return
		elseif guidMatch then
			self:SetOverride('INTERACTTARGET', self.override)
			self.Text:SetText(UNIT_FRAME_DROPDOWN_SUBSECTION_TITLE_INTERACT)
		elseif not CanInteract('target') then
			self:SetOverride('CLICK ConsolePortTargetAI:LeftButton', self.override)
			self.Text:SetText(self.artificialName)
		end
		self:SetNameplateAnchor(self.artificial)
		self:FadeIn()
	else
		self:FadeOut()
		self:ClearScriptOverride()
		self:SetScript('OnUpdate', nil)
	end
end

---------------------------------------------------------------
-- Tracking helpers
---------------------------------------------------------------

function Mouse:SetArtificialUnit(guid, name)
	self.artificial = guid
	self.artificialName = name
	if guid and name and not UnitExists('target') then
		self:TrackUnit(self.fauxUnit, self:GetAttribute('blockhandle'))
	end
end

function Mouse:CacheUnit(unit, fauxUnit)
	local newGUID = UnitGUID(unit)
	if newGUID then
		self.cachedUnit = newGUID
		self:SetPortrait(unit)
	end
	self.fauxUnit = fauxUnit
end

function Mouse:PrepareTracking()
	self:SetScript('OnUpdate', nil)
	self:SetScript('OnEvent', nil)
	self:UnregisterEvent('UPDATE_MOUSEOVER_UNIT')
	self:UnregisterEvent('PLAYER_REGEN_DISABLED')
	self:TogglePortrait(true)
	self:SetIcon(self.interactWith)
	self:SetHintAnchors(nil)
end

-- Workaround: loot is not always available right away.
function Mouse:FindLootFromCache(delay, dispatcherCallback)
	if 	self.cachedUnit
		and not InCombatLockdown() 
		and not self:GetAttribute('blockhandle')
		and not UnitExists('target') then
		--and CanLootUnit(self.cachedUnit) then
		--------------------------------------------------
		return nil --CanLootUnit(self.cachedUnit)
		--------------------------------------------------
	elseif dispatcherCallback then
		self:DispatchLootCheck(delay, self.lootCheckSpool, false)
	end
end

-- Since there's no event indicating when a GUID has loot,
-- this function needs to query the game multiple times with some delay.
function Mouse:DispatchLootCheck(delay, numCalls, firstCall)
	self.lootCheckSpool = (firstCall and numCalls) or (numCalls - 1)
	if self.lootCheckSpool < 1 then return end
	--------------------------------------------------
	CPAPI.TimerAfter(delay, function()
		if self:FindLootFromCache(delay, true) then
			--------------------------------------------------
			self:SetIcon(self.override or self.interactWith)
			self:SetScript('OnUpdate', self.TrackInsecureLootVicinity)
			--------------------------------------------------
		end
	end)
end

function Mouse:TrackUnit(fauxUnit, blocked, forceCache)
	self:CacheUnit(forceCache or 'target', fauxUnit)
	self:PrepareTracking()
	-- loot: existing dead target, assigned securely.
	if ( fauxUnit == 'loot' ) then
		self:SetIcon(self.override or self.interactWith)
		self:SetScript('OnUpdate', self.TrackInsecureLootTarget)
	-- interact: target acquired, but scan range
	elseif ( fauxUnit == 'interact') then
		self:SetIcon(self.override or self.interactWith)
		self:SetScript('OnUpdate', self.TrackInsecureNPC)
	-- npc: existing friendly npc, assigned securely.
	elseif ( fauxUnit == 'npc' ) then
		self:SetIcon(self.override or self.interactWith)
		self:SetScript('OnUpdate', self.TrackSecureNPC)
	-- hover: mouseover priority, assigned securely.
	elseif ( fauxUnit == 'hover' ) then
		self:RegisterEvent('UPDATE_MOUSEOVER_UNIT')
		self:SetScript('OnEvent', self.UpdateMouseover)
		self:SetScript('OnUpdate', self.TrackSecureHover)
		self:SetPortrait('mouseover')
		if not UnitCanAssist('player', 'mouseover') and not UnitCanAttack('player', 'mouseover') then
			self:TogglePortrait(false)
		end
	-- in proximity of tracked GUID, check proximity (insecure).
	elseif ( not blocked and self.artificial ) then
		-- artificial unit might stand next to lootable mob
		local hasLoot, canLoot = self:FindLootFromCache()
		if ( hasLoot and canLoot ) then
			self:SetIcon(self.override or self.interactWith)
			self:SetScript('OnUpdate', self.TrackInsecureLootVicinity)
		else -- proceed with artificial unit
			self:SetIcon(self.override or self.interactWith)
			self:SetScript('OnUpdate', self.TrackArtificialUnit)
		end
		self:TogglePortrait(false)
	else -- cached last target might have loot, check proximity (insecure).
		if ( not blocked and self.cachedUnit ) then
			self:DispatchLootCheck(.35, 20, true)
		end
		self:TogglePortrait(false)
		self:FadeOut()
	end
end

---------------------------------------------------------------
-- Cursor trail for interact button
---------------------------------------------------------------
local Trail = ConsolePortCursorTrail

function Trail:ResetStates()
	self.Use:SetTexture()
	self.Cancel:SetTexture()
	self.Primary:SetTexture()
	self.Secondary:SetTexture()
	self.isMouseOver = nil
	self.isTargeting = nil
	self.hasItem = nil
	self.default = true
end

function Trail.OnAction(action)
	local self = Trail
	if SpellIsTargeting() and not self.isTargeting then
		self:ResetStates()
		self.isTargeting = true
		self.default = nil
		self.Use:SetTexture(GetActionTexture(action) or 'Interface\\RAIDFRAME\\ReadyCheck-Ready')
		self.Primary:SetTexture(db.ICONS.CP_T_L3)
		if GetBindingAction('BUTTON2', true) == 'TURNORACTION' then
			self.Secondary:SetTexture(db.ICONS.CP_T_R3)
			self.Cancel:SetTexture('Interface\\RAIDFRAME\\ReadyCheck-NotReady')
		else
			self.Secondary:SetTexture()
			self.Cancel:SetTexture()
		end
	else
		self.isTargeting = nil
	end
end

function Trail.OnItemAdd()
	local self = Trail
	self:ResetStates()
	self.hasItem = true
	if GetBindingAction('BUTTON1', true) == 'CAMERAORSELECTORMOVE' then
		self.Primary:SetTexture(db.ICONS.CP_T_L3)
		self.Use:SetTexture('Interface\\RAIDFRAME\\ReadyCheck-NotReady')
	end
end

function Trail.OnTooltipAdd(_, owner)
	local self = Trail
	if not hasItem and not isTargeting then
		if owner == UIParent then
			self:ResetStates()
			self.isMouseOver = true
			self.Primary:SetTexture(self.Default)
		else
			self.isMouseOver = nil
		end
	end
end

function Trail.OnTooltipClear()
	Trail.isMouseOver = nil
end

function Trail:ShowPrimaryTextures(toggled)
	CPAPI.SetShown(self.Ghost,not toggled)
	CPAPI.SetShown(self.Use,toggled)
	CPAPI.SetShown(self.Cancel,toggled)
	CPAPI.SetShown(self.Primary,toggled)
	CPAPI.SetShown(self.Secondary,toggled)
end

function Trail:OnUpdate()
	local posX, posY = GetCursorPosition()
	self:SetPoint('BOTTOMLEFT', posX+24, posY-46)
	self:ShowPrimaryTextures(true)
	if ( self.isTargeting and isTargeting ) then
		self:SetAlpha(1)
	elseif ( self.hasItem and hasItem ) then
		if hasWorldFocus and not self.hasWorldFocus then
			self.hasWorldFocus = true
			self.Use:SetTexture('Interface\\RAIDFRAME\\ReadyCheck-NotReady')
		elseif not hasWorldFocus and self.hasWorldFocus then
			self.hasWorldFocus = nil
			self.Use:SetTexture('Interface\\RAIDFRAME\\ReadyCheck-Ready')
		end
		self:SetAlpha(1)
	elseif self.isMouseOver and not cameraMode then
		self:SetAlpha(GameTooltip:GetAlpha())
	elseif self.ghostMode and cameraMode then
		self:ShowPrimaryTextures(false)
		self:SetAlpha(self.ghostAlpha or .25)
	else
		self:SetAlpha(0)
	end
end

-- Hooks
hooksecurefunc('UseAction', Trail.OnAction)
hooksecurefunc('PickupContainerItem', Trail.OnItemAdd)
hooksecurefunc('PickupSpell', Trail.OnItemAdd)
hooksecurefunc(GameTooltip, 'SetOwner', Trail.OnTooltipAdd)
GameTooltip:HookScript('OnTooltipCleared', Trail.OnTooltipClear)

---------------------------------------------------------------
-- Toggle interactive mouse driver on/off
---------------------------------------------------------------
function Core:UpdateMouseDriver()
	if not InCombatLockdown() then
		local loot 
		local button = db.Settings.interactWith
		local cxpinteract = db.Settings.interactCxpWith

		if(cxpinteract) then  
			Mouse:Execute(format([[
				CXPKEY = '%s'
				CXPBINDING = '%s'
				control:RunFor(self, self:GetAttribute('UpdateTarget'), self:GetAttribute('current'))
			]], cxpinteract, C_ConsoleXP and "CXPINTERACT" or QueueInteract and "INTERACTIONKEYBIND"))
		end


		if button then
			local original = db.Bindings and db.Bindings[button] and db.Bindings[button]['']
			local id = original and self:GetActionID(original)

			local targetstate = 
				'[@mouseover,exists] hover; ' ..
				'[@target,exists,harm,dead] loot; ' .. 
				'[@target,exists,harm,nodead] enemy; ' ..
				'[@target,exists,noharm,nodead] friend; nil'

			Trail.Default = ICONS[button]
			Trail.Primary:SetTexture(Trail.Default)

			Mouse.Button:SetTexture(ICONS[button])

			RegisterStateDriver(Mouse, 'vehicle', '[vehicleui] true; nil')
			RegisterStateDriver(Mouse, 'targetstate', targetstate)

			Mouse:SetAttribute('checkNPC', db.Settings.interactNPC)

			Mouse.override = loot or button
			Mouse.interactWith = button

			self:RegisterSpellHeader(Mouse)

			Mouse:Execute(format([[
				USEKEY = %s
				LOOTKEY = %s
				id = %d
				checkNPC = self:GetAttribute('checkNPC')
				control:RunFor(self, self:GetAttribute('UpdateTarget'), self:GetAttribute('current'))
			]], ('"%s"'):format(button), ( loot and ('"%s"'):format(loot)) or 'nil', id or -1))


		else
			Trail.Default = ICONS.CP_T_R3

			Mouse.interactWith = nil

			Mouse:Execute([[
				USEKEY = nil
				LOOTKEY = nil
				checkNPC = nil
				self:ClearBindings()
			]])

			if loot then
				Mouse.override = loot
				-- use 'omit' here just to trigger an update without flagging interaction.
				RegisterStateDriver(Mouse, 'targetstate', '[@target,exists,harm,dead] loot; [@target,exists,harm] omit; nil')
				RegisterStateDriver(Mouse, 'vehicle', '[vehicleui] true; nil')

				Mouse:Execute(format([[
					LOOTKEY = '%s'
					control:RunFor(self, self:GetAttribute('UpdateTarget'), self:GetAttribute('current'))
				]], loot))
			else
				Mouse.override = nil
				UnregisterStateDriver(Mouse, 'targetstate')
				UnregisterStateDriver(Mouse, 'vehicle')
			end
		end
		CPAPI.SetShown(ConsolePortTargetAI, loot or button and db('interactCache'))
		------------------------------------------------
		Trail.ghostMode = db('cursorTrailGhost')
		Trail.ghostAlpha = db('cursorTrailGhostVis')
		Trail.Ghost:SetTexture(db('cursorTrailGhostTex'))
		CPAPI.SetShown(Trail.Ghost, db('cursorTrailGhost'))
		Trail:SetScript('OnUpdate', Trail.OnUpdate)
		CPAPI.SetShown(Trail,not db('disableCursorTrail'))
		------------------------------------------------
		Mouse:SetHintAnchors(nil)
		self:RemoveUpdateSnippet(self.UpdateMouseDriver)
	end
end