-- Fingerprint.lua
-- Frame identification. Named frames are trivial; anonymous frames are described
-- by walking up to the nearest named ancestor and recording characteristics
-- (type, size, child/region counts, child texture paths, child font faces) so they
-- can be matched again across reloads and persisted.

local ADDON, ns = ...

local F = {}
ns.Fingerprint = F

local UIPARENT = UIParent

local function num(v) -- secret-safe number for fingerprint buckets
	if ns.IsSecret(v) or type(v) ~= "number" then return nil end
	return v
end

local function safeCall(obj, method)
	if type(obj) == "table" and type(obj[method]) == "function" then
		local r = { pcall(obj[method], obj) }
		if r[1] then
			return select(2, unpack(r))
		end
	end
	return nil
end

-- Index of `frame` among its parent's children of the same object type (1-based).
local function childIndexOfType(frame)
	local parent = safeCall(frame, "GetParent")
	if not ns.IsWidget(parent) or type(parent.GetChildren) ~= "function" then
		return nil
	end
	local ftype = ns.ObjectType(frame)
	local kids = { parent:GetChildren() }
	local idx = 0
	for i = 1, #kids do
		if ns.ObjectType(kids[i]) == ftype then
			idx = idx + 1
			if kids[i] == frame then
				return idx
			end
		end
	end
	return nil
end

---Nearest ancestor with a stable (non-auto) name. Returns frame, name, depth.
function F.NamedAncestor(frame)
	local cur, depth = frame, 0
	while ns.IsWidget(cur) do
		local n = safeCall(cur, "GetName")
		if type(n) == "string" and n ~= "" and not ns.IsAutoName(n) then
			return cur, n, depth
		end
		if cur == UIPARENT then
			return UIPARENT, "UIParent", depth
		end
		cur = safeCall(cur, "GetParent")
		depth = depth + 1
		if depth > 30 then break end
	end
	return nil, nil, depth
end

---Path of {type, index} from the named ancestor down to frame (for re-resolution).
function F.PathFromAncestor(frame, ancestor)
	local path, cur, guard = {}, frame, 0
	while ns.IsWidget(cur) and cur ~= ancestor and guard < 30 do
		table.insert(path, 1, { type = ns.ObjectType(cur), index = childIndexOfType(cur) })
		cur = safeCall(cur, "GetParent")
		guard = guard + 1
	end
	return path
end

local function collectChildTextures(frame, limit)
	local out = {}
	if type(frame.GetRegions) ~= "function" then return out end
	local regs = { frame:GetRegions() }
	for i = 1, #regs do
		local r = regs[i]
		if ns.ObjectType(r) == "Texture" then
			local atlas = safeCall(r, "GetAtlas")
			local tex = atlas or safeCall(r, "GetTextureFilePath") or safeCall(r, "GetTexture")
			if type(tex) == "string" then
				out[#out + 1] = tex
			elseif type(tex) == "number" then
				out[#out + 1] = "fileID:" .. tex
			end
			if #out >= (limit or 6) then break end
		end
	end
	return out
end

local function collectChildFonts(frame, limit)
	local out = {}
	if type(frame.GetRegions) ~= "function" then return out end
	local regs = { frame:GetRegions() }
	for i = 1, #regs do
		local r = regs[i]
		if ns.ObjectType(r) == "FontString" then
			local face = safeCall(r, "GetFont")
			if type(face) == "string" then
				out[#out + 1] = face:match("([^\\/]+)$") or face
			end
			if #out >= (limit or 4) then break end
		end
	end
	return out
end

---Full identity table for a frame.
function F.Identify(frame)
	if not ns.IsWidget(frame) then return nil end
	local name = safeCall(frame, "GetName")
	local ancestorFrame, ancestorName = F.NamedAncestor(frame)
	local w = num(safeCall(frame, "GetWidth"))
	local h = num(safeCall(frame, "GetHeight"))
	-- num() rejects secret values (GetNumChildren/GetNumRegions carry SecretAspect.Hierarchy).
	local numChildren = num(safeCall(frame, "GetNumChildren")) or 0
	local numRegions  = num(safeCall(frame, "GetNumRegions")) or 0

	return {
		name        = (type(name) == "string" and name ~= "") and name or nil,
		debugName   = ns.ObjectName(frame),
		type        = ns.ObjectType(frame),
		width       = w and math.floor(w + 0.5) or nil,
		height      = h and math.floor(h + 0.5) or nil,
		strata      = safeCall(frame, "GetFrameStrata"),
		level       = num(safeCall(frame, "GetFrameLevel")),
		numChildren = numChildren,
		numRegions  = numRegions,
		ancestor    = ancestorName,
		path        = ancestorFrame and F.PathFromAncestor(frame, ancestorFrame) or {},
		textures    = collectChildTextures(frame),
		fonts       = collectChildFonts(frame),
	}
end

---Stable key used for evidence persistence and cross-reload matching.
function F.Key(frame)
	local id = F.Identify(frame)
	if not id then return tostring(frame) end
	if id.name and not ns.IsAutoName(id.name) then
		return "name:" .. id.name
	end
	local pathStr = {}
	for i = 1, #id.path do
		pathStr[i] = (id.path[i].type or "?") .. (id.path[i].index or "?")
	end
	return table.concat({
		"fp:", id.ancestor or "?", "|",
		table.concat(pathStr, "."), "|",
		id.type, "|",
		(id.width or "?") .. "x" .. (id.height or "?"), "|",
		"c" .. id.numChildren, "r" .. id.numRegions,
	})
end

---Frames sharing a numbered name pattern (NamePlate1, CompactRaidFrame3, …).
---Substitutes the FIRST digit run in the name over 1..range. Always includes
---the original frame. Returns an array of {frame, name}.
function F.NumberedSiblings(frame, range)
	local out, seen = {}, {}
	local name = ns.IsWidget(frame) and safeCall(frame, "GetName") or frame
	if type(frame) == "string" then name = frame end
	if ns.IsWidget(frame) then
		out[1] = { frame = frame, name = name }
		if name then seen[name] = true end
	end
	if type(name) ~= "string" or not name:match("%d+") then
		return out
	end
	range = range or (ns.db and ns.db.numberRange) or 40
	for i = 1, range do
		local cand = name:gsub("%d+", tostring(i), 1)
		if not seen[cand] then
			seen[cand] = true
			local f = _G[cand]
			if ns.IsWidget(f) then
				out[#out + 1] = { frame = f, name = cand }
			end
		end
	end
	return out
end

---Best-effort: find the live frame for a stored identity (cross-reload).
function F.Resolve(identity)
	if not identity then return nil end
	if identity.name and ns.IsWidget(_G[identity.name]) then
		return _G[identity.name]
	end
	-- Walk down from the named ancestor following the recorded type/index path.
	local cur = identity.ancestor and _G[identity.ancestor]
	if identity.ancestor == "UIParent" then cur = UIPARENT end
	if not ns.IsWidget(cur) or not identity.path then return nil end
	for _, step in ipairs(identity.path) do
		if type(cur.GetChildren) ~= "function" then return nil end
		local kids = { cur:GetChildren() }
		local idx, found = 0, nil
		for i = 1, #kids do
			if ns.ObjectType(kids[i]) == step.type then
				idx = idx + 1
				if idx == step.index then found = kids[i] break end
			end
		end
		if not found then return nil end
		cur = found
	end
	return ns.IsWidget(cur) and cur or nil
end
