-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- FrameStack Functions
-- @module FrameStack

local _, TSM = ...
local FrameStack = TSM.UI:NewPackage("FrameStack")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local Table = TSM.Include("Util.Table")
local Vararg = TSM.Include("Util.Vararg")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
local private = {}
local STRATA_ORDER = {"TOOLTIP", "FULLSCREEN_DIALOG", "FULLSCREEN", "DIALOG", "HIGH", "MEDIUM", "LOW", "BACKGROUND", "WORLD"}
local framesByStrata = {
	WORLD = {},
	BACKGROUND = {},
	LOW = {},
	MEDIUM = {},
	HIGH = {},
	DIALOG = {},
	FULLSCREEN = {},
	FULLSCREEN_DIALOG = {},
	TOOLTIP = {},
}
local ELEMENT_ATTR_KEYS = {
	"_hScrollbar",
	"_vScrollbar",
	"_hScrollFrame",
	"_hContent",
	"_vScrollFrame",
	"_content",
	"_header",
	"_frame",
	"_rows",
}
local COLOR_KEYS = {
	FRAME_BG = true,
	PRIMARY_BG = true,
	PRIMARY_BG_ALT = true,
	ACTIVE_BG = true,
	ACTIVE_BG_ALT = true,
	INDICATOR = true,
	INDICATOR_ALT = true,
	INDICATOR_DISABLED = true,
	TEXT = true,
	TEXT_ALT = true,
	TEXT_DISABLED = true,
}
local FEEDBACK_COLOR_KEYS = {
	RED = true,
	YELLOW = true,
	GREEN = true,
	BLUE = true,
	ORANGE = true,
}
local FONT_KEYS = {
	HEADING_H5 = true,
	BODY_BODY1 = true,
	BODY_BODY1_BOLD = true,
	BODY_BODY2 = true,
	BODY_BODY2_MEDIUM = true,
	BODY_BODY2_BOLD = true,
	BODY_BODY3 = true,
	BODY_BODY3_MEDIUM = true,
	ITEM_BODY1 = true,
	ITEM_BODY2 = true,
	ITEM_BODY3 = true,
	TABLE_TABLE1 = true,
}
local ELEMENT_STYLE_KEYS = {
	"_background",
	"_texture",
	"_backgroundColor",
	"_borderColor",
	"_textStr",
	"_textColor",
	"_font",
	"_roundedCorners",
	"_borderSize",
}



-- ============================================================================
-- Module Functions
-- ============================================================================

function FrameStack.Toggle()
	if not TSMFrameStackTooltip then
		CreateFrame("GameTooltip", "TSMFrameStackTooltip", UIParent, "GameTooltipTemplate")
		TSMFrameStackTooltip.highlightFrame = CreateFrame("Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
		TSMFrameStackTooltip.highlightFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
		TSMFrameStackTooltip.highlightFrame:SetBackdropColor(1, 0, 0, 0.3)
		TSMFrameStackTooltip:Hide()
		ScriptWrapper.Set(TSMFrameStackTooltip, "OnUpdate", private.OnUpdate)
	end
	if TSMFrameStackTooltip:IsVisible() then
		TSMFrameStackTooltip:Hide()
		TSMFrameStackTooltip.highlightFrame:Hide()
	else
		TSMFrameStackTooltip.lastUpdate = 0
		TSMFrameStackTooltip.altDown = nil
		TSMFrameStackTooltip.index = 1
		TSMFrameStackTooltip.numFrames = 0
		TSMFrameStackTooltip:SetOwner(UIParent, "ANCHOR_NONE")
		TSMFrameStackTooltip:SetPoint("TOPLEFT", 0, 0)
		TSMFrameStackTooltip:AddLine("Loading...")
		TSMFrameStackTooltip:Show()
		TSMFrameStackTooltip.highlightFrame:Show()
	end
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.OnUpdate(self)
	if self.lastUpdate + 0.05 >= GetTime() then
		return
	end
	self.lastUpdate = GetTime()

	local numFrames = 0
	for _, strata in ipairs(STRATA_ORDER) do
		for _, strataFrame in ipairs(framesByStrata[strata]) do
			local name = private.GetFrameName(strataFrame)
			if not strmatch(name, "innerBorderFrame") then
				numFrames = numFrames + 1
			end
		end
	end
	if numFrames ~= TSMFrameStackTooltip.numFrames then
		TSMFrameStackTooltip.index = 1
		TSMFrameStackTooltip.numFrames = numFrames
	end

	local leftAltDown = IsKeyDown("LALT")
	local rightAltDown = IsKeyDown("RALT")
	if not self.altDown and leftAltDown and not rightAltDown then
		self.altDown = "LEFT"
		if TSMFrameStackTooltip.index == TSMFrameStackTooltip.numFrames then
			TSMFrameStackTooltip.index = 1
		else
			TSMFrameStackTooltip.index = TSMFrameStackTooltip.index + 1
		end
	elseif not self.altDown and not leftAltDown and rightAltDown then
		self.altDown = "RIGHT"
		if TSMFrameStackTooltip.index == 1 then
			TSMFrameStackTooltip.index = TSMFrameStackTooltip.numFrames
		else
			TSMFrameStackTooltip.index = TSMFrameStackTooltip.index - 1
		end
	elseif self.altDown == "LEFT" and not leftAltDown then
		self.altDown = nil
	elseif self.altDown == "RIGHT" and not rightAltDown then
		self.altDown = nil
	end

	for _, strata in ipairs(STRATA_ORDER) do
		wipe(framesByStrata[strata])
	end

	local frame = EnumerateFrames()
	while frame do
		if frame ~= self.highlightFrame and not frame:IsForbidden() and frame:IsVisible() and MouseIsOver(frame) then
			tinsert(framesByStrata[frame:GetFrameStrata()], frame)
			for _, region in Vararg.Iterator(frame:GetRegions()) do
				if region:IsObjectType("Texture") and not region:IsForbidden() and region:IsVisible() and MouseIsOver(region) and UIElements.GetByFrame(region) then
					tinsert(framesByStrata[frame:GetFrameStrata()], region)
				end
			end
		end
		frame = EnumerateFrames(frame)
	end

	self:ClearLines()
	self:AddDoubleLine("TSM Frame Stack", format("%0.2f, %0.2f", GetCursorPosition()))
	local currentIndex = 1
	local topFrame = nil
	for _, strata in ipairs(STRATA_ORDER) do
		if #framesByStrata[strata] > 0 then
			sort(framesByStrata[strata], private.FrameLevelSortFunction)
			self:AddLine(strata, 0.6, 0.6, 1)
			for _, strataFrame in ipairs(framesByStrata[strata]) do
				local isTexture = strataFrame:IsObjectType("Texture")
				local level = (isTexture and strataFrame:GetParent() or strataFrame):GetFrameLevel()
				local width = strataFrame:GetWidth()
				local height = strataFrame:GetHeight()
				local mouseEnabled = not isTexture and strataFrame:IsMouseEnabled()
				local name = private.GetFrameName(strataFrame)
				local isIndexedFrame = false
				if not strmatch(name, "innerBorderFrame") then
					if not topFrame and currentIndex == self.index then
						topFrame = strataFrame
						isIndexedFrame = true
					end
					currentIndex = currentIndex + 1
				end
				local text = format("  <%d%s> %s (%d, %d)", level, isTexture and "+" or "", name, Math.Round(width), Math.Round(height))
				if isIndexedFrame then
					self:AddLine(text, 0.9, 0.9, 0.5)
					local element = UIElements.GetByFrame(strataFrame)
					if element then
						for _, k in ipairs(ELEMENT_STYLE_KEYS) do
							local v = element[k]
							if v ~= nil then
								local vStr = private.GetStyleValueStr(v)
								if vStr then
									self:AddLine(format("    %s = %s", tostring(k), vStr), 0.7, 0.7, 0.7)
								end
							end
						end
					end
				elseif mouseEnabled then
					self:AddLine(text, 0.6, 1, 1)
				else
					self:AddLine(text, 0.9, 0.9, 0.9)
				end
			end
		end
	end
	self.highlightFrame:ClearAllPoints()
	self.highlightFrame:SetAllPoints(topFrame)
	self.highlightFrame:SetFrameStrata("TOOLTIP")
	self:Show()
end

function private.FrameLevelSortFunction(a, b)
	local aLevel = a:IsObjectType("Texture") and (a:GetParent():GetFrameLevel() + 0.1) or a:GetFrameLevel()
	local bLevel = b:IsObjectType("Texture") and (b:GetParent():GetFrameLevel() + 0.1) or b:GetFrameLevel()
	return aLevel > bLevel
end

function private.TableValueSearch(tbl, searchValue, currentKey, visited)
	visited = visited or {}
	for key, value in pairs(tbl) do
		if value == searchValue then
			return (currentKey and (currentKey..".") or "")..key
		elseif type(value) == "table" and (not value.__isa or value:__isa(TSM.UI.Element)) and not visited[value] then
			visited[value] = true
			local result = private.TableValueSearch(value, searchValue, (currentKey and (currentKey..".") or "")..key, visited)
			if result then
				return result
			end
		end
	end
	for _, key in ipairs(ELEMENT_ATTR_KEYS) do
		local value = tbl[key]
		if value == searchValue then
			return (currentKey and (currentKey..".") or "")..key
		elseif type(value) == "table" and (not value.__isa or value:__isa(TSM.UI.Element)) and not visited[value] then
			visited[value] = true
			local result = private.TableValueSearch(value, searchValue, (currentKey and (currentKey..".") or "")..key, visited)
			if result then
				return result
			end
		end
	end
end

function private.GetFrameNodeInfo(frame)
	local globalName = not frame:IsObjectType("Texture") and frame:GetName()
	if globalName and not strmatch(globalName, "^TSM_UI_ELEMENT:") then
		return globalName, frame:GetParent()
	end

	local parent = frame:GetParent()
	local element = UIElements.GetByFrame(frame)
	if element then
		return element._id, parent
	end

	if parent then
		-- check if this exists as an attribute of the parent table
		local parentKey = Table.KeyByValue(parent, frame)
		if parentKey then
			return tostring(parentKey), parent
		end

		-- find the nearest element to which this frame belongs
		local parentElement = nil
		local testFrame = parent
		while testFrame and not parentElement do
			parentElement = UIElements.GetByFrame(testFrame)
			testFrame = testFrame:GetParent()
		end
		if parentElement then
			-- check if this exists as an attribute of this element
			local tableKey = private.TableValueSearch(parentElement, frame)
			if tableKey then
				return tableKey, parentElement._frame
			end
		end
	end

	return nil, parent
end

function private.GetFrameName(frame)
	local name, parent = private.GetFrameNodeInfo(frame)
	local parentName = parent and (private.GetFrameName(parent)..".") or ""
	name = name or gsub(tostring(frame), ": ?0*", ":")
	return parentName..name
end

function private.GetStyleValueStr(value)
	for key in pairs(COLOR_KEYS) do
		if value == Theme.GetColor(key) then
			return "ThemeColor<"..key..">"
		end
	end
	for key in pairs(FEEDBACK_COLOR_KEYS) do
		if value == Theme.GetFeedbackColor(key) then
			return "ThemeColor<"..key..">"
		end
	end
	if value == Theme.GetBlizzardColor() then
		return "ThemeColor<BLIZZARD>"
	end
	for key in pairs(FONT_KEYS) do
		if value == Theme.GetFont(key) then
			return "ThemeFont<"..key..">"
		end
	end
	if type(value) == "string" then
		return "\""..value.."\""
	elseif value ~= false then
		return tostring(value)
	end
	return nil
end
