-- UI/Picker.lua
-- FStack-style picker + live "what is this?" identification. Uses our own
-- GameTooltip's engine-side SetFrameStack (sees ALL frames at the cursor, not just
-- mouse-enabled ones), shows a cyan highlight and an analysis tooltip (creator via
-- GetSourceLocation, parent chain, contributors). Keyboard-only input so the cursor
-- hit-test is never blocked. On confirm it enumerates the FULL stack under the cursor
-- and hands it to the selection UI ("all" by default).

local ADDON, ns = ...

local P = {}
ns.Picker = P

local UPDATE = 0.05
local PARENT_DEPTH = 6

local hitTip, infoTip, hl, catcher
local active, selectMode = false, true
local depthDelta, elapsed, current = 0, 0, nil
local showHidden, showRegions = false, true
local pickFromBottom = false  -- right-click Identify flips this: default to the back (bottom) of the stack
local own = {}

local function registerOwn(f) if f then own[f] = true end end
local function isOwn(frame)
	local cur, guard = frame, 0
	while ns.IsWidget(cur) and guard < 40 do
		if own[cur] then return true end
		local n = cur.GetName and cur:GetName()
		if type(n) == "string" and n:find("^TweakIdentifier") then return true end
		cur = cur.GetParent and cur:GetParent() or nil
		guard = guard + 1
	end
	return false
end

local function border(parent)
	local t = parent:CreateTexture(nil, "OVERLAY")
	t:SetColorTexture(0, 1, 1, 1)
	return t
end

local function buildWidgets()
	if hl then return end

	hl = CreateFrame("Frame", "TweakIdentifierHighlight", UIParent)
	hl:SetFrameStrata("TOOLTIP")
	hl:EnableMouse(false)
	local bg = hl:CreateTexture(nil, "BACKGROUND")
	bg:SetAllPoints()
	bg:SetColorTexture(0, 0.8, 1, 0.10)
	local top, bottom, left, right = border(hl), border(hl), border(hl), border(hl)
	top:SetPoint("TOPLEFT"); top:SetPoint("TOPRIGHT"); top:SetHeight(2)
	bottom:SetPoint("BOTTOMLEFT"); bottom:SetPoint("BOTTOMRIGHT"); bottom:SetHeight(2)
	left:SetPoint("TOPLEFT"); left:SetPoint("BOTTOMLEFT"); left:SetWidth(2)
	right:SetPoint("TOPRIGHT"); right:SetPoint("BOTTOMRIGHT"); right:SetWidth(2)
	hl:Hide()
	registerOwn(hl)

	hitTip = CreateFrame("GameTooltip", "TweakIdentifierHitTip", UIParent, "GameTooltipTemplate")
	registerOwn(hitTip)
	infoTip = CreateFrame("GameTooltip", "TweakIdentifierInfoTip", UIParent, "GameTooltipTemplate")
	infoTip:SetFrameStrata("TOOLTIP")
	registerOwn(infoTip)

	catcher = CreateFrame("Frame", "TweakIdentifierPickerCatcher", UIParent)
	registerOwn(catcher)
	catcher:Hide()
	-- Modal keyboard grab, modeled on ElvUI's keybind mode (ElvUI ActionBars/Bind.lua):
	-- EnableKeyboard(true) + CONSUME every key (never propagate), and act on OnKeyUp. A
	-- keyboard-enabled, non-propagating frame swallows the key-DOWN even with no OnKeyDown
	-- handler, so the ESCAPE→TOGGLEGAMEMENU binding never fires while picking. We handle keys
	-- on key-UP and DEFER the teardown (Stop/Confirm) by one frame, so we never disable/hide
	-- the keyboard-capturing frame from inside its own key event: tearing it down mid-keypress
	-- could let the press fall through into a tainted binding run (Esc → ToggleGameMenu →
	-- ClearTarget(), ADDON_ACTION_FORBIDDEN). No SetPropagateKeyboardInput anywhere.
	catcher:SetScript("OnKeyUp", function(self, key)
		if key == "ESCAPE" then
			ns.Print("Picker cancelled.")
			C_Timer.After(0, P.Stop)
		elseif key == "ENTER" or key == "NUMPADENTER" then
			C_Timer.After(0, P.Confirm)
		elseif key == "UP" or key == "PAGEUP" or key == "LEFT" then
			depthDelta = -1
		elseif key == "DOWN" or key == "PAGEDOWN" or key == "RIGHT" then
			depthDelta = 1
		end
	end)
	catcher:RegisterEvent("PLAYER_REGEN_DISABLED")
	catcher:SetScript("OnEvent", function()
		if active then P.Stop(); ns.Print("|cffff5555Picker stopped (entered combat).|r") end
	end)
end

-- ---------------------------------------------------------------------------
-- Analysis tooltip
-- ---------------------------------------------------------------------------

local function sourceLine(src, guess)
	if src and src.type == "blizzard" then
		return ns.ICON.blizzard .. " |cff6699ffBlizzard UI|r |cff555555(" .. (src.folder or "") .. ")|r"
	elseif src and src.type == "addon" then
		return ns.ICON.addon .. " |cffffff00" .. ns.QuoteName(src.title or src.folder) .. "|r"
	elseif src and src.type == "guess" then
		return ns.ICON.addon .. " |cffddaa00" .. ns.QuoteName(src.title) .. "|r |cff777777(name guess)|r"
	elseif guess then
		return ns.ICON.addon .. " |cffddaa00" .. ns.QuoteName(guess) .. "|r |cff777777(name guess)|r"
	end
	return nil
end

local function hintLine()
	return "|cff555555Up/Down depth · " .. (selectMode and "Enter select · " or "") .. "Esc close|r"
end

local function analyze(frame)
	local detail = not (ns.db and ns.db.simpleView)
	local name = ns.ObjectName(frame) or tostring(frame)
	local otype = ns.ObjectType(frame)

	infoTip:ClearLines()
	infoTip:AddLine("|cff33ff99" .. name .. "|r")
	infoTip:AddLine("|cff888888" .. otype .. "|r")

	local src = ns.SourceOf(frame)
	local guess = (not src) and ns.GuessFromName(name) or nil
	local line = sourceLine(src, guess)
	if line then
		infoTip:AddLine("Created by: " .. line, 1, 1, 1)
		if detail and src and src.file then
			infoTip:AddLine("  |cff666666" .. tostring(src.file) .. ":" .. tostring(src.line) .. "|r")
		end
	elseif ns.HasSourceLocation and not ns.Deep.IsActive() then
		-- Frame-creator naming is Retail-12.0-only (source-location); only suggest /twid deep
		-- where it can actually help. On Classic, show "creator unknown" with no hint.
		infoTip:AddLine(ns.ICON.unknown .. " |cff888888creator unknown|r")
		infoTip:AddLine("  |cffff8800/twid deep|r |cff666666to identify who created frames|r")
	else
		infoTip:AddLine(ns.ICON.unknown .. " |cff888888creator unknown|r")
	end

	-- Already-known alterations for this frame (from prior monitoring this session).
	local key = ns.Fingerprint.Key(frame)
	local rec = ns.Evidence.live[key]
	if rec then
		local contrib = ns.Evidence.ContributorList(rec)
		if #contrib > 0 then
			infoTip:AddLine(ns.ICON.gear .. " |cffff9900altered by:|r")
			for i = 1, math.min(#contrib, detail and 8 or 3) do
				infoTip:AddLine("   |cffddccaa- " .. ns.QuoteName(contrib[i]) .. "|r")
			end
		end
	end

	if detail then
		local cur, depth = frame:GetParent(), 0
		local shown = 0
		while ns.IsWidget(cur) and cur ~= UIParent and depth < PARENT_DEPTH do
			local psrc = ns.SourceOf(cur)
			local pby = psrc and (psrc.title or psrc.folder) or nil
			if shown == 0 then infoTip:AddLine("|cff666666parents:|r") end
			infoTip:AddLine(("   "):rep(1) .. "|cff999999" .. (ns.ObjectName(cur) or "?") ..
				(pby and (" |cff555555(" .. pby .. ")|r") or "") .. "|r")
			shown = shown + 1
			cur = cur:GetParent(); depth = depth + 1
		end
	end

	infoTip:AddLine(" ")
	infoTip:AddLine(hintLine())
	infoTip:Show()
end

local function anchorTip()
	-- Cursor-anchored. An ANCHOR_NONE GameTooltip with a manual SetPoint does not draw
	-- here (the point sticks but nothing renders); ANCHOR_CURSOR renders. Re-own each tick
	-- so the tip tracks the cursor. If the engine walk lands on the tip itself, isOwn()
	-- in onUpdate drops it.
	infoTip:SetOwner(UIParent, "ANCHOR_CURSOR")
end

local function onUpdate(self, e)
	elapsed = elapsed + e
	if elapsed < UPDATE and depthDelta == 0 then return end
	elapsed = 0
	anchorTip()
	hl:Hide() -- exclude our overlay from the engine walk
	-- ONE engine walk per tick. (Enumerating the whole stack every tick — ~2×depth walks,
	-- each rebuilding the frame-stack tooltip — lagged the game hard.)
	local stepping = depthDelta ~= 0
	local frame = infoTip:SetFrameStack(showHidden, showRegions, depthDelta)
	depthDelta = 0
	-- Our ANCHOR_CURSOR tip is frontmost and can be the hit; step past it (bounded, cheap).
	local g = 0
	while frame and isOwn(frame) and g < 6 do
		frame = infoTip:SetFrameStack(showHidden, showRegions, 1)
		g = g + 1
	end
	if frame and isOwn(frame) then frame = nil end
	-- "Default to bottom" (right-click): only on a fresh cursor-move detection (a new frame,
	-- not while stepping or holding the same one) walk to the back of the stack. Runs on the
	-- transition, not every tick, so it stays cheap; top mode (default) never walks extra.
	if frame and pickFromBottom and not stepping and frame ~= current then
		local f, g2 = frame, 0
		while g2 < 40 do
			local nf = infoTip:SetFrameStack(showHidden, showRegions, 1)
			if not nf or isOwn(nf) or nf == f then break end
			f, g2 = nf, g2 + 1
		end
		frame = f
	end
	if frame then
		current = frame
		hl:ClearAllPoints(); hl:SetAllPoints(frame); hl:Show()
		analyze(frame)
	elseif current and ns.IsWidget(current) then
		-- Nothing under the cursor this tick: hold the last frame, don't flash empty.
		hl:ClearAllPoints(); hl:SetAllPoints(current); hl:Show()
		analyze(current)
	else
		hl:Hide()
		infoTip:ClearLines()
		infoTip:AddLine("|cff888888(no frame under cursor)|r")
		infoTip:AddLine(hintLine())
		infoTip:Show()
	end
end

-- ---------------------------------------------------------------------------
-- Full-stack enumeration (clamp to one end, then walk, dedup)
-- ---------------------------------------------------------------------------

local function enumerateStack()
	local frames, seen = {}, {}
	local last
	for _ = 1, 80 do
		local f = infoTip:SetFrameStack(showHidden, showRegions, -1)
		if f == last then break end
		last = f
	end
	for _ = 1, 80 do
		local f = infoTip:SetFrameStack(showHidden, showRegions, 0)
		if not f then break end
		if seen[f] then break end
		seen[f] = true
		if not isOwn(f) then frames[#frames + 1] = f end
		local nf = infoTip:SetFrameStack(showHidden, showRegions, 1)
		if not nf or seen[nf] then break end
	end
	return frames
end

-- ---------------------------------------------------------------------------
-- Public
-- ---------------------------------------------------------------------------

function P.IsActive() return active end

function P.Start(doSelect)
	if InCombatLockdown() then ns.Print("|cffff5555Cannot pick during combat.|r") return end
	buildWidgets()
	active, selectMode = true, (doSelect ~= false)
	depthDelta, elapsed, current = 0, UPDATE, nil
	infoTip:SetOwner(UIParent, "ANCHOR_CURSOR"); infoTip:Show()
	catcher:Show(); catcher:EnableKeyboard(true)
	catcher:SetScript("OnUpdate", onUpdate)
	ns.Print("Picker active — hover a frame; |cffffff00Up/Down|r depth, "
		.. (selectMode and "|cffffff00Enter|r select, " or "") .. "|cffffff00Esc|r close.")
end

function P.Stop()
	active = false
	if catcher then
		catcher:SetScript("OnUpdate", nil)
		catcher:EnableKeyboard(false); catcher:Hide()
	end
	if infoTip then infoTip:Hide() end
	if hl then hl:Hide() end
end

---Toolbar entry point: a second click turns the picker off. Same mode → stop; a different
---mode while active → switch; inactive → start.
function P.Toggle(doSelect)
	if active and selectMode == (doSelect ~= false) then
		P.Stop()
		ns.Print("Picker stopped.")
	else
		P.Start(doSelect)
	end
end

---Human label for the stack end the picker defaults to (front vs back).
function P.PickEndText()
	return pickFromBottom and "bottom (back)" or "top (front)"
end

---Right-click Identify: flip which end of the cursor's frame stack the picker defaults to.
function P.TogglePickEnd()
	pickFromBottom = not pickFromBottom
	current = nil  -- re-pick to the new default end on the next tick
	ns.Print("Picker default: |cffffff00" .. P.PickEndText() .. "|r frame of the stack.")
end

function P.Confirm()
	if not selectMode then P.Stop(); return end
	local frames = enumerateStack()
	P.Stop()
	if #frames == 0 then ns.Print("No frame under cursor.") return end
	if #frames == 1 then
		ns.Monitor.SetTargets(frames)
		if ns.UI and ns.UI.Show then ns.UI.Show() end
	else
		ns.UI.ShowSelection(frames)
	end
end

---Frame selector by global name (supports numbered-sibling expansion).
function P.SelectByName(name)
	if not name or name == "" then ns.Print("Usage: |cffffff00/twid name <FrameName>|r") return end
	local f = _G[name]
	if not ns.IsWidget(f) then ns.Print("|cffff5555No frame named '" .. name .. "' found.|r") return end
	local sibs = ns.Fingerprint.NumberedSiblings(f)
	if #sibs > 1 then
		local list = {}
		for i = 1, #sibs do list[i] = sibs[i].frame end
		ns.Print(("Found %d numbered siblings of %s — monitoring all."):format(#list, name))
		ns.Monitor.SetTargets(list)
	else
		ns.Monitor.SetTargets({ f })
	end
	if ns.UI and ns.UI.Show then ns.UI.Show() end
end
