---------------------------------------------------------------
-- Stack functionality for UI cursor
---------------------------------------------------------------
-- Keeps a stack of frames to control with the D-pad when they
-- are visible on screen. See View\UICursor.lua.

local _, db = ...;
---------------------------------------------------------------
local After = C_Timer.After;
local pairs, next, unravel = pairs, next, db.table.unravel;
local isLocked, isEnabled, isObstructed;
---------------------------------------------------------------
local Stack = db:Register('Stack', CPAPI.CreateEventHandler({'Frame', '$parentUIStackHandler', ConsolePort}, {
	'PLAYER_REGEN_ENABLED',
	'PLAYER_REGEN_DISABLED',
}, {
	Registry = {};
}));

---------------------------------------------------------------
-- Externals:
---------------------------------------------------------------
function Stack:LockCore(...)        isLocked = ...      end
function Stack:IsCoreLocked()       return isLocked     end
function Stack:IsCursorObstructed() return isObstructed end

---------------------------------------------------------------
-- Update state on visibility change.
---------------------------------------------------------------

-- Stacks: all frames, visible frames, show/hide hooks
do local frames, visible, buffer, hooks, forbidden, obstructors = {}, {}, {}, {}, {}, {};

	local function updateVisible(self)
		visible[self] = self:GetPoint() and self:IsVisible() and true or nil;
	end

	local function updateBuffer(self, flag)
		buffer[self] = flag;
	end

	local function updateOnBuffer(self)
		updateBuffer(self, true)
		After(0, function()
			updateVisible(self)
			updateBuffer(self, nil)
			if not next(buffer) then
				Stack:UpdateFrames()
			end
		end)
	end

	-- OnShow:
	-- Use C_Timer.After to circumvent omitting frames that set their points on show.
	-- Check for point because frames can be visible but not drawn.
	local function showHook(self)
		if isEnabled and frames[self] then
			updateOnBuffer(self)
		end
	end

	-- OnHide:
	-- Use C_Timer.After to circumvent node jumping when closing multiple frames,
	-- which leads to the cursor ending up in an unexpected place on re-show.
	-- E.g. close 5 bags, cursor was in 1st bag, ends up in 5th bag on re-show.
	local function hideHook(self, force)
		if isEnabled and frames[self] and (force or visible[self]) then
			updateOnBuffer(self)
		end
	end

	local function addHook(widget, script, hook, name)
		local mt = getmetatable(widget)
		local ix = mt and mt.__index
		local fn = ix and ix[script]
		if ( type(fn) == 'function' and not hooks[fn] ) then
			hooksecurefunc(ix, script, hook)
			hooks[fn] = true
		elseif ( widget.HookScript ) then
			widget:HookScript(('On%s'):format(script), hook)
		end
	end

	-- Cache default methods so that frames with unaltered
	-- metatables use hook scripts instead of a secure hook.
	hooks[getmetatable(UIParent).__index.Show] = true
	hooks[getmetatable(UIParent).__index.Hide] = true

	-- When adding a new frame:
	-- Store metatable functions for hooking show/hide scripts.
	-- Most frames will use the same standard Show/Hide, but addons 
	-- may use custom metatables, which should still work with this approach.
	function Stack:AddFrame(frame)
		local widget = (type(frame) == 'string' and _G[frame]) or (type(frame) == 'table' and frame)
		local name = (type(frame) == 'string' and frame or type(frame) == 'table' and frame:GetName())
		if C_Widget.IsFrameWidget(widget) then
			if ( not forbidden[widget] ) then
				-- assert the frame isn't hooked twice
				if ( not frames[widget] ) then
					addHook(widget, 'Show', showHook, name)
					addHook(widget, 'Hide', hideHook, name)
				end

				frames[widget] = true
				if widget:IsVisible() and widget:GetPoint() then
					visible[widget] = true
				end
			end
			return true
		else
			self:AddFrameWatcher(frame)
		end
	end

	function Stack:LoadAddonFrames(name)
		local frames = db('Stack/Registry/'..name)
		if (type(frames) == 'table') then
			for k, v in pairs(frames) do
				if (type(k) == 'string') then
					self:AddFrame(k)
				elseif (type(v) == 'string') then
					self:AddFrame(v)
				end
			end
		end
	end

	function Stack:RemoveFrame(frame)
		if frame then
			visible[frame] = nil;
			frames[frame]  = nil;
		end
	end

	function Stack:ForbidFrame(frame)
		if frames[frame] then
			forbidden[frame] = true;
			self:RemoveFrame(frame)
		end
	end

	function Stack:UnforbidFrame(frame)
		if forbidden[frame] then
			self:AddFrame(frame)
			forbidden[frame] = nil
		end
	end

	function Stack:SetCursorObstructor(idx, state)
		if idx then
			if not state then state = nil end
			obstructors[idx] = state;
			isObstructed = ((next(obstructors) and true) or false)
			if not isObstructed then
				self:UpdateFrames()
			end
		end
	end

	function Stack:ToggleCore()
		local showOnDemand = db('UIshowOnDemand')
		isEnabled = db('UIenableCursor') and not showOnDemand;
		if not isEnabled and not showOnDemand then
			db('Cursor'):SetEnabled(false)
		end
	end

	function Stack:UpdateFrames(updateCursor)
		if not isLocked then
			self:UpdateFrameTracker()
			db('Cursor'):SetEnabled(next(visible))
		end
	end

	-- Returns a stack of visible frames.
	function Stack:IterateVisibleCursorFrames()
		return pairs(visible)
	end

	function Stack:GetVisibleCursorFrames()
		return unravel(visible)
	end

	function Stack:IsFrameVisibleToCursor(frame, ...)
		if frame then
			return visible[frame] or false, self:IsFrameVisibleToCursor(...)
		end
	end
end

function Stack:HideFrame(frame, ignoreAlpha)
	assert(frame, 'Usage: Stack:HideFrame(frame)')
	frame:SetSize(0, 0)
	frame:EnableMouse(false)
	frame:EnableKeyboard(false)
	frame:SetAlpha(ignoreAlpha and frame:GetAlpha() or 0)
	frame:ClearAllPoints()
	self:ForbidFrame(frame)
end

---------------------------------------------------------------
-- Events
---------------------------------------------------------------
function Stack:PLAYER_REGEN_ENABLED()
	After(db('UIleaveCombatDelay'), function()
		if not InCombatLockdown() then
			self:LockCore(false)
			self:UpdateFrames()
		end
	end)
end

function Stack:PLAYER_REGEN_DISABLED()
	db('Cursor'):SetEnabled(false)
	self:LockCore(true)
end

---------------------------------------------------------------
-- Stack management over sessions
---------------------------------------------------------------
do db:Save('Stack/Registry', 'ConsolePortUIStack')

	-- NOTE: this function generates the default set which should
	-- contain all the frames that are not caught by managers,
	-- and exist within the FrameXML code in some shape or form. 
	local function GenerateDefaultSet(self)
		-- Special handling for containers
		for i=1, NUM_CONTAINER_FRAMES do
			self:TryRegisterFrame(_, 'ContainerFrame'..i, true)
		end
		for i=1, STATICPOPUP_NUMDIALOGS do
			self:TryRegisterFrame(_, 'StaticPopup'..i, true)
		end
		for i, frame in ipairs({
			---------------------------------
			'CovenantPreviewFrame';
			'LFGDungeonReadyPopup';
			'OpenMailFrame';
			'PetBattleFrame';
			'StackSplitFrame';
			---------------------------------
		}) do self:TryRegisterFrame(_, frame, true) end
	end

	function Stack:GetRegistrySet(name)
		self.Registry[name] = self.Registry[name] or {};
		return self.Registry[name];
	end

	function Stack:TryRegisterFrame(set, name, state)
		if not name then return end

		local stack = self:GetRegistrySet(set)
		if (stack[name] == nil) then
			stack[name] = (state == nil and true) or state;
			return true;
		end
	end

	function Stack:TryRemoveFrame(set, name)
		if not name then return end

		local stack = self:GetRegistrySet(set)
		if (stack[name] ~= nil) then
			stack[name] = nil;
			return true;
		end
	end

	function Stack:OnDataLoaded()
		db:Load('Stack/Registry', 'ConsolePortUIStack')
		GenerateDefaultSet(self)
		self:ToggleCore()

		-- Load all existing frames in the registry
		for addon in pairs(self.Registry) do
			if IsAddOnLoaded(addon) then
				self:LoadAddonFrames(addon)
			end
		end

		self.OnDataLoaded = nil;
		self:RegisterEvent('ADDON_LOADED')
		self.ADDON_LOADED = function(self, name)
			self:LoadAddonFrames(name)
			self:UpdateFrames()
		end;

		db:RegisterSafeCallback('Settings/UIenableCursor', self.ToggleCore, self)
		db:RegisterSafeCallback('Settings/UIshowOnDemand', self.ToggleCore, self)
	end

	local function CatchNewFrame(frame)
		if not Stack:IsFrameVisibleToCursor(frame) then
			if Stack:TryRegisterFrame(_, frame:GetName(), true) then
				Stack:AddFrame(frame)
				Stack:UpdateFrames()
			end
		end
	end

	hooksecurefunc('ShowUIPanel', CatchNewFrame)
	hooksecurefunc('StaticPopupSpecial_Show', CatchNewFrame)
end

---------------------------------------------------------------
-- Frame watching
---------------------------------------------------------------
-- Used to track and bind additional frames to the UI cursor.
-- Necessary since all frames do not exist when the game loads.
-- Automatically adds all special frames and managed panels.

do  local managers = {[UIPanelWindows] = true, [UISpecialFrames] = false, [UIMenus] = false};
	local specialFrames, watchers = {}, {}

	local function TryAddSpecialFrame(self, frame)
		if not specialFrames[frame] then
			-- low-prio todo: save some memory here by not cloning
			-- the frame into the watchers table. 
			if self:AddFrame(frame) then
				specialFrames[frame] = true;
			end
		end
	end

	local function CheckSpecialFrames(self)
		for manager, isAssociative in pairs(managers) do
			if isAssociative then
				for frame in pairs(manager) do
					TryAddSpecialFrame(self, frame)
				end
			else
				for _, frame in ipairs(manager) do
					TryAddSpecialFrame(self, frame)
				end
			end
		end
	end

	function Stack:UpdateFrameTracker()
		CheckSpecialFrames(self)
		for frame in pairs(watchers) do
			if self:AddFrame(frame) then
				watchers[frame] = nil;
			end
		end
	end

	function Stack:AddFrameWatcher(frame)
		watchers[frame] = true;
	end
end