-- Portal Pro :: UI/Widgets/Base.lua
-- Drawing primitives every widget is built from. Panels, hairline borders, text,
-- shadows, motion.

local ADDON_NAME, PP = ...
local T = PP.Theme
local U = PP.U

local UI = {}
PP.UI = UI

--------------------------------------------------------------------------------
-- Panels & fills
--------------------------------------------------------------------------------

--- A solid-fill frame. `colorName` indexes Theme.color.
function UI:Panel(parent, colorName, layer)
	local f = CreateFrame("Frame", nil, parent)
	local bg = f:CreateTexture(nil, layer or "BACKGROUND")
	bg:SetAllPoints()
	bg:SetColorTexture(T:C(colorName or "raised"))
	f.bg = bg

	function f:SetColorToken(name, alphaOverride)
		local r, g, b, a = T:C(name)
		self.bg:SetColorTexture(r, g, b, alphaOverride or a)
	end

	return f
end

--- Fill an existing frame/region with a token colour.
function UI:Fill(parent, colorName, layer)
	local tex = parent:CreateTexture(nil, layer or "BACKGROUND")
	tex:SetAllPoints()
	tex:SetColorTexture(T:C(colorName or "raised"))
	return tex
end

--- 1px hairline border made of four edge textures. Cheaper and sharper than a backdrop.
function UI:Border(frame, colorName, thickness)
	thickness = thickness or T.size.hairline
	local r, g, b, a = T:C(colorName or "borderSubtle")
	local edges = frame.__ppBorder

	if not edges then
		edges = {}
		for _, side in ipairs({ "top", "bottom", "left", "right" }) do
			edges[side] = frame:CreateTexture(nil, "BORDER")
		end
		frame.__ppBorder = edges
	end

	edges.top:ClearAllPoints()
	edges.top:SetPoint("TOPLEFT")
	edges.top:SetPoint("TOPRIGHT")
	edges.top:SetHeight(thickness)

	edges.bottom:ClearAllPoints()
	edges.bottom:SetPoint("BOTTOMLEFT")
	edges.bottom:SetPoint("BOTTOMRIGHT")
	edges.bottom:SetHeight(thickness)

	edges.left:ClearAllPoints()
	edges.left:SetPoint("TOPLEFT")
	edges.left:SetPoint("BOTTOMLEFT")
	edges.left:SetWidth(thickness)

	edges.right:ClearAllPoints()
	edges.right:SetPoint("TOPRIGHT")
	edges.right:SetPoint("BOTTOMRIGHT")
	edges.right:SetWidth(thickness)

	for _, tex in pairs(edges) do
		tex:SetColorTexture(r, g, b, a)
	end

	function frame:SetBorderColor(name, alphaOverride)
		local br, bg2, bb, ba = T:C(name)
		for _, tex in pairs(self.__ppBorder) do
			tex:SetColorTexture(br, bg2, bb, alphaOverride or ba)
		end
	end

	return frame
end

--- Approximated drop shadow: stacked translucent rings. No texture assets needed.
function UI:Shadow(frame, spread)
	spread = spread or 4
	frame.__ppShadow = frame.__ppShadow or {}
	for i = 1, spread do
		local tex = frame.__ppShadow[i] or frame:CreateTexture(nil, "BACKGROUND", nil, -i)
		frame.__ppShadow[i] = tex
		tex:ClearAllPoints()
		tex:SetPoint("TOPLEFT", frame, "TOPLEFT", -i, i)
		tex:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", i, -i)
		local r, g, b = T:C("shadow")
		tex:SetColorTexture(r, g, b, 0.10 * (1 - (i - 1) / spread))
	end
	return frame
end

function UI:Divider(parent, vertical)
	local tex = parent:CreateTexture(nil, "ARTWORK")
	tex:SetColorTexture(T:C("borderSubtle"))
	if vertical then tex:SetWidth(1) else tex:SetHeight(1) end
	return tex
end

--------------------------------------------------------------------------------
-- Text
--------------------------------------------------------------------------------

function UI:Text(parent, text, size, weight, colorName, justify)
	local fs = parent:CreateFontString(nil, "OVERLAY")
	T:Font(fs, size, weight, colorName)
	fs:SetText(text or "")
	fs:SetJustifyH(justify or "LEFT")
	fs:SetWordWrap(false)
	return fs
end

--- Small uppercase label used for section headings and column headers.
function UI:Label(parent, text)
	local fs = self:Text(parent, (text or ""):upper(), T.fontSize.xs, "", "textMuted")
	fs:SetSpacing(1)
	return fs
end

--------------------------------------------------------------------------------
-- Motion
--------------------------------------------------------------------------------

--- Short alpha fade. Motion at ~120ms is what separates "modern" from "flat and dead".
---
--- The group is reused per frame, which means a new fade has to interrupt the previous one.
--- AnimationGroup:Stop() fires OnFinished with `requested = true`, so the completion
--- callback MUST check that flag — otherwise interrupting a fade-out runs its "now hide the
--- frame" side effect, and the fade-in that replaces it plays on a hidden frame. That is
--- precisely how a toast appears and vanishes in the same instant.
function UI:Fade(frame, from, to, duration, onFinished)
	local group = frame.__ppFadeGroup
	if not group then
		group = frame:CreateAnimationGroup()
		group.anim = group:CreateAnimation("Alpha")
		frame.__ppFadeGroup = group
	end

	-- Detach the stale handler before stopping, so an interrupted animation can't run the
	-- previous caller's callback even if a future client fires OnFinished differently.
	group:SetScript("OnFinished", nil)
	if group:IsPlaying() then group:Stop() end

	group.anim:SetDuration(duration or 0.12)
	group.anim:SetFromAlpha(from)
	group.anim:SetToAlpha(to)
	group:SetScript("OnFinished", function(self, requested)
		if requested then return end
		frame:SetAlpha(to)
		if onFinished then onFinished() end
	end)

	frame:SetAlpha(from)
	group:Play()
	return group
end

function UI:FadeIn(frame, duration)
	frame:Show()
	self:Fade(frame, 0, 1, duration or 0.12)
end

function UI:FadeOut(frame, duration, hideAfter)
	self:Fade(frame, frame:GetAlpha(), 0, duration or 0.12, function()
		if hideAfter ~= false then frame:Hide() end
	end)
end

--- Slide-in used by lead cards and toasts. Translation only: callers pair this with Fade,
--- and two concurrent Alpha animations on one frame fight each other.
function UI:SlideIn(frame, dx, dy, duration)
	local group = frame.__ppSlideGroup
	if not group then
		group = frame:CreateAnimationGroup()
		group.move = group:CreateAnimation("Translation")
		group.move:SetSmoothing("OUT")
		frame.__ppSlideGroup = group
	end
	if group:IsPlaying() then group:Stop() end
	group.move:SetOffset(dx or 0, dy or 0)
	group.move:SetDuration(duration or 0.16)
	group:Play()
	return group
end

--- Looping attention pulse, used by the Broadcast button at READY.
function UI:Pulse(frame, minAlpha, maxAlpha, period)
	if frame.__ppPulse then return frame.__ppPulse end
	local group = frame:CreateAnimationGroup()
	group:SetLooping("BOUNCE")
	local a = group:CreateAnimation("Alpha")
	a:SetFromAlpha(minAlpha or 0.35)
	a:SetToAlpha(maxAlpha or 1)
	a:SetDuration(period or 0.9)
	a:SetSmoothing("IN_OUT")
	frame.__ppPulse = group
	return group
end

--------------------------------------------------------------------------------
-- Interaction
--------------------------------------------------------------------------------

--- Hover highlight driven by a token colour swap.
function UI:Hoverable(frame, normalToken, hoverToken)
	frame:EnableMouse(true)
	frame:HookScript("OnEnter", function(self)
		if self.SetColorToken then self:SetColorToken(hoverToken or "hover") end
	end)
	frame:HookScript("OnLeave", function(self)
		if self.SetColorToken then self:SetColorToken(normalToken or "raised") end
	end)
	return frame
end

--- @param onStop fun(frame) called after the drag ends, for persisting position
function UI:MakeDraggable(frame, handle, onStop)
	handle = handle or frame
	frame:SetMovable(true)
	frame:SetClampedToScreen(true)
	handle:EnableMouse(true)
	handle:RegisterForDrag("LeftButton")
	handle:SetScript("OnDragStart", function()
		if PP.profile and PP.profile.ui.locked then return end
		frame:StartMoving()
	end)
	handle:SetScript("OnDragStop", function()
		frame:StopMovingOrSizing()
		if onStop then onStop(frame) end
	end)
	return frame
end

--- Persist / restore a frame position as a point table.
function UI:SavePosition(frame, store, key)
	local point, _, relPoint, x, y = frame:GetPoint(1)
	store[key] = { point, "UIParent", relPoint, math.floor(x + 0.5), math.floor(y + 0.5) }
end

function UI:RestorePosition(frame, store, key, fallback)
	local p = store and store[key] or fallback
	frame:ClearAllPoints()
	if p and p[1] then
		frame:SetPoint(p[1], UIParent, p[3] or p[1], p[4] or 0, p[5] or 0)
	else
		frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
	end
end

--------------------------------------------------------------------------------
-- Tooltips
--------------------------------------------------------------------------------

--- Uses GameTooltip: it's the one Blizzard element worth reusing, because players
--- expect tooltips to look and behave like every other tooltip on screen.
---
--- `title` and `lines` may each be a function, evaluated on hover. Attach ONCE per frame
--- and use functions for anything dynamic — calling this on every list refresh would stack
--- a new OnEnter hook per render and leak steadily as the user scrolls.
function UI:Tooltip(frame, title, lines, anchor)
	if frame.__ppTooltip then
		-- Already hooked; just swap the content providers.
		frame.__ppTooltip.title = title
		frame.__ppTooltip.lines = lines
		return frame
	end

	frame.__ppTooltip = { title = title, lines = lines }

	frame:HookScript("OnEnter", function(self)
		local spec = self.__ppTooltip
		if not spec then return end

		local titleText = type(spec.title) == "function" and spec.title() or spec.title
		local body = type(spec.lines) == "function" and spec.lines() or spec.lines
		if type(body) == "string" then body = { body } end
		if not titleText and not body then return end

		GameTooltip:SetOwner(self, anchor or "ANCHOR_RIGHT")
		if titleText then
			GameTooltip:AddLine(titleText, 1, 1, 1)
		end
		for _, line in ipairs(body or {}) do
			GameTooltip:AddLine(line, 0.61, 0.63, 0.67, true)
		end
		GameTooltip:Show()
	end)
	frame:HookScript("OnLeave", function() GameTooltip:Hide() end)
	return frame
end
