---------------------------------------------------------------
-- Mouse state handler
---------------------------------------------------------------
-- Used to control mouse state depending on different scenarios
-- and allows customization to how the mouse and camera should
-- behave.

local _, db = ...;
local Mouse = db:Register('Mouse', CPAPI.CreateEventHandler({'Frame', '$parentMouseHandler', ConsolePort}, {
	'ACTIONBAR_HIDEGRID';
	'ACTIONBAR_SHOWGRID';
	'GOSSIP_SHOW';
	'LOOT_OPENED';
	'PLAYER_STARTED_MOVING';
	'QUEST_GREETING';
	'UPDATE_BINDINGS';
}))

---------------------------------------------------------------
-- Upvalues since these will be called/checked frequently
---------------------------------------------------------------
local GameTooltip, UIParent, WorldFrame = GameTooltip, UIParent, WorldFrame;
local GetBindingAction, CreateKeyChord = GetBindingAction, CPAPI.CreateKeyChord;
local NewTimer, GetMouseFocus = C_Timer.NewTimer, GetMouseFocus;
local UnitExists, GetSpellInfo = UnitExists, GetSpellInfo;
local UnitCastingInfo, UnitChannelInfo = UnitCastingInfo, UnitChannelInfo;

---------------------------------------------------------------
-- Consts
---------------------------------------------------------------
local CAST_INFO_SPELLID_OFFSET = 9;
local SPELLID_CAST_TIME_OFFSET = 4;
local ALWAYS_TURN_CAMERA_VALUE = 2;
local LCLICK_BINDING = 'CAMERAORSELECTORMOVE';
local RCLICK_BINDING = 'TURNORACTION';


---------------------------------------------------------------
-- Helpers: predicate evaluators
---------------------------------------------------------------
-- Args: button ID, followed by list of predicates.

local function is(_, pred, ...)
	if pred == nil then return true end
	return pred(_) and is(_, ...)
end

local function isnt(...)
	return not is(...)
end

local function either(_, pred,  ...)
	if pred == nil then return end
	return pred(_) or either(_, ...)
end

---------------------------------------------------------------
-- Timer
---------------------------------------------------------------
function Mouse:SetTimer(callback, time)
	self:ClearTimer(callback)
	if ( time > 0 ) then
		self[callback] = NewTimer(time, function()
			callback(self)
		end)
	end
end

function Mouse:ClearTimer(callback)
	if self[callback] then
		self[callback]:Cancel()
		self[callback] = nil;
	end
end

---------------------------------------------------------------
-- Console variables
---------------------------------------------------------------
local CVar_Camera = db.Data.Cvar('GamePadTurnWithCamera')
local CVar_Follow = db.Data.Cvar('CameraFollowOnStick')
local CVar_Sticks = db.Data.Cvar('GamePadCursorAutoDisableSticks')
local CVar_Target = db.Data.Cvar('GamePadCursorForTargeting')
local CVar_Center = db.Data.Cvar('GamePadCursorCentering')
local CVar_LClick = db.Data.Cvar('GamePadCursorLeftClick')
local CVar_RClick = db.Data.Cvar('GamePadCursorRightClick')
local Keys_Escape = db.Data.Select()

---------------------------------------------------------------
-- Predicates (should always return boolean)
---------------------------------------------------------------
local GamePadControl = IsGamePadFreelookEnabled;
local CursorControl  = IsGamePadCursorControlEnabled;
local MenuFrameOpen  = IsOptionFrameOpen;
local SpellTargeting = SpellIsTargeting;
---------------------------------------------------------------
local reverseMouseHandling = false;

local GetClickCvar = function(isLeftClick)
	if reverseMouseHandling then
		isLeftClick = not isLeftClick;
	end
	return isLeftClick and CVar_LClick or CVar_RClick;
end
local GetClickBinding = function(isLeftClick)
	if reverseMouseHandling then
		isLeftClick = not isLeftClick;
	end
	return isLeftClick and LCLICK_BINDING or RCLICK_BINDING;
end
local IsClickAction = function(button, binding)
	local action = GetBindingAction(CreateKeyChord(button))
	return (action == '' or action == binding)
end
local IsAction = function(button, binding)
	return GetBindingAction(CreateKeyChord(button), true) == binding;
end
local LeftClick = function(button)
	local cvar, binding = GetClickCvar(true), GetClickBinding(true)
	if cvar:IsValue(button) then
		return IsClickAction(button, binding)
	end
	return IsAction(button, binding)
end;
local RightClick = function(button)
	local cvar, binding = GetClickCvar(false), GetClickBinding(false)
	if cvar:IsValue(button) then
		return IsClickAction(button, binding)
	end
	return IsAction(button, binding)
end
---------------------------------------------------------------
local MenuBinding    = function(button) return Keys_Escape:IsOption(CreateKeyChord(button)) end;
local CursorCentered = function() return CVar_Center:Get(true) end;
local TooltipShowing = function() return GameTooltip:IsOwned(UIParent) and GameTooltip:GetAlpha() == 1 end;
local IsWorldFocus   = function() return GetMouseFocus() == WorldFrame end;
local WorldInteract  = function() return TooltipShowing() and IsWorldFocus() end;
local MouseOver      = function() return UnitExists('mouseover') or WorldInteract() end;


---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function Mouse:UPDATE_BINDINGS()
	Keys_Escape:SetOptions(db.Gamepad:GetBindingKey('TOGGLEGAMEMENU'))
end

-- Temporary solution to fix problems with casters unable to face
-- their intended target because of face movement.
function Mouse:UNIT_SPELLCAST_START()
	if self.fmVehicleOverride then return end;
	if (self.fmSpellOverride == nil) then
		self.fmSpellOverride = CVar_Camera:Get()
		CVar_Camera:Set(ALWAYS_TURN_CAMERA_VALUE)
	end
end

function Mouse:UNIT_SPELLCAST_STOP()
	if self.fmVehicleOverride then return end;
	if (self.fmSpellOverride ~= nil) then
		CVar_Camera:Set(self.fmSpellOverride)
		self.fmSpellOverride = nil;
	end
end

function Mouse:UNIT_ENTERING_VEHICLE()
	if (self.fmVehicleOverride == nil) then
		self:UNIT_SPELLCAST_STOP()
		self.fmVehicleOverride = CVar_Camera:Get()
		CVar_Camera:Set(ALWAYS_TURN_CAMERA_VALUE)
	end
end

function Mouse:UNIT_EXITING_VEHICLE()
	if (self.fmVehicleOverride ~= nil) then
		CVar_Camera:Set(self.fmVehicleOverride)
		self.fmVehicleOverride = nil;
	end
end

Mouse.UNIT_SPELLCAST_CHANNEL_START = Mouse.UNIT_SPELLCAST_START;
Mouse.UNIT_SPELLCAST_CHANNEL_STOP  = Mouse.UNIT_SPELLCAST_STOP;

function Mouse:CURRENT_SPELL_CAST_CHANGED()
	if SpellTargeting() then
		if (self.reticleOverride == nil) and (db('mouseFreeCursorReticle') ~= 0) then
			self.reticleOverride = CVar_Sticks:Get()
			CVar_Sticks:Set(0)
			self:SetFreeCursor()
		end
	elseif (self.reticleOverride ~= nil) then
		CVar_Sticks:Set(self.reticleOverride)
		self.reticleOverride = nil;
		self:SetCameraControl()
	end
end

function Mouse:PLAYER_STARTED_MOVING()
	if db('mouseHideCursorOnMovement') and self:ShouldClearCursorOnMovement() then
		self:SetCameraControl()
	end
end

do local function OnModifierUpdate(self, elapsed)
		self.tapTimer = self.tapTimer + elapsed;
		if self.tapNum > 1 then
			self.tapNum = 0;
			if CursorControl() then
				self:SetCenteredCursor()
			else
				self:SetFreeCursor()
			end
		end
		if self.tapTimer > self.tapWindow then
			self.tapNum = Clamp(self.tapNum - 1, 0, self.tapNum)
			self.tapTimer = self.tapTimer - self.tapWindow;
		end
	end

	function Mouse:MODIFIER_STATE_CHANGED(modifier, down)
		if (down == 1) and modifier:match(self.tapModifier) then
			self.tapTimer = 0;
			self.tapNum = self.tapNum + 1;
			self:SetScript('OnUpdate', OnModifierUpdate)
		end
	end
end

-- Direct function calls from events
Mouse.ACTIONBAR_HIDEGRID = Mouse.SetCameraControl;
Mouse.ACTIONBAR_SHOWGRID = Mouse.SetFreeCursor;

Mouse.GOSSIP_SHOW        = Mouse.SetCameraControl;
Mouse.LOOT_OPENED        = Mouse.SetCameraControl;
Mouse.QUEST_GREETING     = Mouse.SetCameraControl;

---------------------------------------------------------------
-- Compounded queries
---------------------------------------------------------------
function Mouse:ShouldSetFreeCursor(_)
	return is(_, LeftClick) and isnt(_, SpellTargeting) and either(_, GamePadControl, CursorCentered)
end

function Mouse:ShouldSetCenteredCursor(_)
	return is(_, RightClick, GamePadControl) and isnt(_, CursorCentered)
end

function Mouse:ShouldSetCameraControl(_)
	return is(_, RightClick, GamePadControl, CursorCentered) and isnt(_, MouseOver)
end

function Mouse:ShouldFreeCenteredCursor(_)
	return is(_, MenuBinding, GamePadControl, CursorCentered) and isnt(_, MouseOver)
end

function Mouse:ShouldSetCursorWhenMenuIsOpen(_)
	return is(_, MenuBinding, MenuFrameOpen) and isnt(_, CursorControl)
end

function Mouse:ShouldClearCursorOnMovement()
	return is(nil, GamePadControl, CursorControl, IsWorldFocus)
end

---------------------------------------------------------------
-- Base control functions
---------------------------------------------------------------
function Mouse:SetCentered(enabled)
	CVar_Center:Set(db('mouseAlwaysCentered') or enabled)
	return self
end

function Mouse:SetCursorControl(enabled)
	-- We have to use a timer in order to not trigger a race condition between
	-- treating the left click as a button press and a mouse click.
	if enabled then
		if (self.cursorOverride == nil) then
			self.cursorOverride = self.reticleOverride or CVar_Sticks:Get()
		end
		CVar_Sticks:Set(0)
		C_Timer.After(db('mouseFreeCursorEnableTime'), function()
			SetGamePadCursorControl(true)
			if (self.cursorOverride) then
				CVar_Sticks:Set(self.cursorOverride)
				self.cursorOverride = nil;
			end
		end)
	else
		SetGamePadCursorControl(false)
		if (self.cursorOverride) then
			CVar_Sticks:Set(self.cursorOverride)
			self.cursorOverride = nil;
		end
	end
	return self
end

function Mouse:SetFreeLook(enabled)
	SetGamePadFreeLook(enabled)
	return self
end

---------------------------------------------------------------
-- Compounded control functions
---------------------------------------------------------------
function Mouse:SetFreeCursor()
	return self
		:SetFreeLook(false)
		:SetCentered(false)
		:SetCursorControl(true)
end

function Mouse:SetCenteredCursor()
	self:SetTimer(self.AttemptSetCameraControl, db('mouseAutoClearCenter'))
	return self
		:SetFreeLook(true)
		:SetCentered(true)
		:SetCursorControl(false)
end

function Mouse:SetCameraControl()
	self:ClearTimer(self.AttemptSetCameraControl)
	return self
		:SetFreeLook(true)
		:SetCentered(false)
		:SetCursorControl(false)
end

function Mouse:AttemptSetCameraControl(_)
	-- TODO: timeout should happen after mouseover ends
	if is(_, MouseOver) then
		return self:SetTimer(self.AttemptSetCameraControl, db('mouseAutoClearCenter'))
	end
	return self:SetCameraControl()
end

---------------------------------------------------------------
-- Handlers
---------------------------------------------------------------
function Mouse:OnGamePadButtonDown(button)
	if self:ShouldSetFreeCursor(button) then
		return self:SetFreeCursor()
	end
	if self:ShouldSetCenteredCursor(button) then
		return self:SetCenteredCursor()
	end
	if self:ShouldSetCameraControl(button) then
		return self:SetCameraControl()
	end
	return self
end

---------------------------------------------------------------
-- Handler on/off
---------------------------------------------------------------
function Mouse:SetEnabled(enabled)
	self:EnableGamePadButton(enabled)
	if enabled then
		SetCVar('GamePadCursorAutoEnable', 0)
		SetCVar('CursorFreelookStartDelta', 0.001)
		SetCVar('GamePadCursorCenteredEmulation', 0)
	end
	(enabled and FrameUtil.RegisterFrameForEvents or FrameUtil.UnregisterFrameForEvents)(self, self.Events);
end

function Mouse:OnDataLoaded()
	self:SetEnabled(db('mouseHandlingEnabled'))
	for i, event in ipairs({
		'UNIT_SPELLCAST_CHANNEL_START';
		'UNIT_SPELLCAST_CHANNEL_STOP';
		'UNIT_SPELLCAST_START';
		'UNIT_SPELLCAST_STOP';
		'UNIT_ENTERING_VEHICLE';
		'UNIT_EXITING_VEHICLE';
	}) do self:RegisterUnitEvent(event, 'player') end
	self:OnVariableChanged()
end

function Mouse:OnVariableChanged()
	local modTapModifier = db('doubleTapModifier')
	if not modTapModifier:match('none') then
		self:RegisterEvent('MODIFIER_STATE_CHANGED')
		self.tapWindow = db('doubleTapTimeout')
		self.tapModifier = modTapModifier;
		self.tapTimer = 0;
		self.tapNum = 0;
	else
		self:UnregisterEvent('MODIFIER_STATE_CHANGED')
	end

	local useCursorReticleTargeting = db('mouseFreeCursorReticle')
	if (useCursorReticleTargeting ~= 0) then
		self:RegisterEvent('CURRENT_SPELL_CAST_CHANGED')
	else
		self:UnregisterEvent('CURRENT_SPELL_CAST_CHANGED')
	end
	CVar_Target:Set(useCursorReticleTargeting)
	reverseMouseHandling = db('mouseHandlingReversed')
end

db:RegisterCallback('Settings/mouseHandlingEnabled', Mouse.SetEnabled, Mouse)
---------------------------------------------------------------
-- Variables
---------------------------------------------------------------
db:RegisterCallbacks(Mouse.OnVariableChanged, Mouse,
	'Settings/doubleTapModifier',
	'Settings/doubleTapTimeout',
	'Settings/mouseFreeCursorReticle',
	'Settings/mouseHandlingReversed'
);
---------------------------------------------------------------
Mouse:SetScript('OnGamePadButtonDown', Mouse.OnGamePadButtonDown)
Mouse:EnableGamePadButton(false)
Mouse:SetPropagateKeyboardInput(true)