-- Portal Pro :: UI/Widgets/Widgets.lua
-- The widget kit. Buttons, toggles, sliders, inputs, dropdowns, segmented controls,
-- stat tiles. Every one is built from Base primitives and Theme tokens.

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

--------------------------------------------------------------------------------
-- Button
--------------------------------------------------------------------------------

local VARIANTS = {
	primary   = { fill = "accent",  hover = "accentHover", text = "textPrimary", border = "accent" },
	secondary = { fill = "overlay", hover = "hover",       text = "textPrimary", border = "borderStrong" },
	ghost     = { fill = nil,       hover = "hover",       text = "textSecondary", border = nil },
	danger    = { fill = "overlay", hover = "hover",       text = "negative",   border = "borderStrong" },
	success   = { fill = "positive", hover = "positive",   text = "base",       border = "positive" },
}

function UI:Button(parent, text, onClick, variant, width, height)
	local style = VARIANTS[variant or "secondary"]
	local b = CreateFrame("Button", nil, parent)
	b:SetSize(width or 88, height or T.size.buttonHeight)

	local bg = b:CreateTexture(nil, "BACKGROUND")
	bg:SetAllPoints()
	if style.fill then
		bg:SetColorTexture(T:C(style.fill))
	else
		bg:SetColorTexture(0, 0, 0, 0)
	end
	b.bg = bg

	if style.border then UI:Border(b, style.border) end

	local label = UI:Text(b, text, T.fontSize.base, "", style.text, "CENTER")
	label:SetPoint("CENTER")
	b.label = label

	b.__variant = style
	b.__enabled = true

	function b:SetTextValue(value)
		self.label:SetText(value)
	end

	function b:SetEnabledState(enabled)
		self.__enabled = enabled
		self:EnableMouse(enabled)
		self:SetAlpha(enabled and 1 or 0.4)
	end

	b:SetScript("OnEnter", function(self)
		if not self.__enabled then return end
		if self.__variant.hover then self.bg:SetColorTexture(T:C(self.__variant.hover)) end
	end)
	b:SetScript("OnLeave", function(self)
		if self.__variant.fill then
			self.bg:SetColorTexture(T:C(self.__variant.fill))
		else
			self.bg:SetColorTexture(0, 0, 0, 0)
		end
	end)
	b:SetScript("OnMouseDown", function(self)
		if self.__enabled then self.label:SetPoint("CENTER", 0, -1) end
	end)
	b:SetScript("OnMouseUp", function(self)
		self.label:SetPoint("CENTER", 0, 0)
	end)
	b:SetScript("OnClick", function(self, button)
		if not self.__enabled then return end
		if onClick then onClick(self, button) end
	end)

	return b
end

--- Square icon button. `icon` is a texture path or fileID.
function UI:IconButton(parent, icon, onClick, size, tooltip)
	local b = CreateFrame("Button", nil, parent)
	size = size or 24
	b:SetSize(size, size)

	local bg = b:CreateTexture(nil, "BACKGROUND")
	bg:SetAllPoints()
	bg:SetColorTexture(T:C("overlay"))
	b.bg = bg

	local tex = b:CreateTexture(nil, "ARTWORK")
	tex:SetPoint("TOPLEFT", 3, -3)
	tex:SetPoint("BOTTOMRIGHT", -3, 3)
	tex:SetTexture(icon)
	tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	b.icon = tex

	UI:Border(b, "borderSubtle")

	b:SetScript("OnEnter", function(self) self.bg:SetColorTexture(T:C("hover")) end)
	b:SetScript("OnLeave", function(self) self.bg:SetColorTexture(T:C("overlay")) end)
	b:SetScript("OnClick", function(self, button) if onClick then onClick(self, button) end end)

	if tooltip then UI:Tooltip(b, tooltip) end
	return b
end

--------------------------------------------------------------------------------
-- Switch
--------------------------------------------------------------------------------

-- Named Switch, not Toggle: UI:Toggle() is the main window show/hide.
function UI:Switch(parent, labelText, initial, onChange)
	local f = CreateFrame("Button", nil, parent)
	f:SetHeight(22)

	local track = f:CreateTexture(nil, "ARTWORK")
	track:SetSize(34, 16)
	track:SetPoint("LEFT")
	f.track = track

	local knob = f:CreateTexture(nil, "OVERLAY")
	knob:SetSize(12, 12)
	f.knob = knob

	local label = UI:Text(f, labelText, T.fontSize.base, "", "textPrimary")
	label:SetPoint("LEFT", track, "RIGHT", T.space.sm, 0)
	f.label = label
	f:SetWidth(34 + T.space.sm + label:GetStringWidth() + 4)

	f.value = initial and true or false

	function f:Render()
		if self.value then
			self.track:SetColorTexture(T:C("accent"))
			self.knob:SetColorTexture(T:C("textPrimary"))
			self.knob:ClearAllPoints()
			self.knob:SetPoint("RIGHT", self.track, "RIGHT", -2, 0)
		else
			self.track:SetColorTexture(T:C("overlay"))
			self.knob:SetColorTexture(T:C("textMuted"))
			self.knob:ClearAllPoints()
			self.knob:SetPoint("LEFT", self.track, "LEFT", 2, 0)
		end
	end

	function f:SetValue(value, silent)
		self.value = value and true or false
		self:Render()
		if not silent and onChange then onChange(self.value) end
	end

	f:SetScript("OnClick", function(self) self:SetValue(not self.value) end)
	f:SetScript("OnEnter", function(self)
		if not self.value then self.track:SetColorTexture(T:C("hover")) end
	end)
	f:SetScript("OnLeave", function(self) self:Render() end)

	f:Render()
	return f
end

--------------------------------------------------------------------------------
-- Slider
--------------------------------------------------------------------------------

--- @param format fun(value):string label formatter
function UI:Slider(parent, labelText, min, max, step, initial, onChange, format)
	local wrap = CreateFrame("Frame", nil, parent)
	wrap:SetHeight(40)

	local label = UI:Text(wrap, labelText, T.fontSize.base, "", "textSecondary")
	label:SetPoint("TOPLEFT")

	local value = UI:Text(wrap, "", T.fontSize.base, "", "textPrimary", "RIGHT")
	value:SetPoint("TOPRIGHT")

	local slider = CreateFrame("Slider", nil, wrap)
	slider:SetPoint("BOTTOMLEFT")
	slider:SetPoint("BOTTOMRIGHT")
	slider:SetHeight(14)
	slider:SetOrientation("HORIZONTAL")
	slider:SetMinMaxValues(min, max)
	slider:SetValueStep(step)
	slider:SetObeyStepOnDrag(true)

	local track = slider:CreateTexture(nil, "BACKGROUND")
	track:SetHeight(3)
	track:SetPoint("LEFT")
	track:SetPoint("RIGHT")
	track:SetColorTexture(T:C("overlay"))

	local fill = slider:CreateTexture(nil, "ARTWORK")
	fill:SetHeight(3)
	fill:SetPoint("LEFT", track, "LEFT")
	fill:SetColorTexture(T:C("accent"))

	local thumb = slider:CreateTexture(nil, "OVERLAY")
	thumb:SetSize(12, 12)
	thumb:SetColorTexture(T:C("textPrimary"))
	slider:SetThumbTexture(thumb)

	wrap.slider = slider
	wrap.valueText = value

	local function render(v)
		value:SetText(format and format(v) or tostring(U.Round(v, 2)))
		local pct = (max > min) and ((v - min) / (max - min)) or 0
		fill:SetWidth(math.max(1, track:GetWidth() * pct))
	end

	slider:SetScript("OnValueChanged", function(self, v)
		render(v)
		if onChange then onChange(v) end
	end)
	slider:SetScript("OnSizeChanged", function() render(slider:GetValue()) end)

	slider:SetValue(initial or min)
	render(initial or min)

	function wrap:SetValue(v)
		slider:SetValue(v)
	end

	return wrap
end

--------------------------------------------------------------------------------
-- Input
--------------------------------------------------------------------------------

function UI:Input(parent, placeholder, initial, onChange, height)
	local f = UI:Panel(parent, "base")
	f:SetHeight(height or T.size.inputHeight)
	UI:Border(f, "borderSubtle")

	local edit = CreateFrame("EditBox", nil, f)
	edit:SetPoint("TOPLEFT", T.space.sm, -3)
	edit:SetPoint("BOTTOMRIGHT", -T.space.sm, 3)
	edit:SetAutoFocus(false)
	edit:SetFont(T:ResolveFont(), T.fontSize.base, "")
	edit:SetTextColor(T:C("textPrimary"))
	edit:SetText(initial or "")
	f.edit = edit

	local ghost = UI:Text(f, placeholder or "", T.fontSize.base, "", "textMuted")
	ghost:SetPoint("LEFT", edit, "LEFT", 0, 0)
	f.ghost = ghost

	local function refreshGhost()
		ghost:SetShown(edit:GetText() == "" and not edit:HasFocus())
	end
	refreshGhost()

	edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
	edit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
	edit:SetScript("OnEditFocusGained", function()
		f:SetBorderColor("accent")
		refreshGhost()
	end)
	edit:SetScript("OnEditFocusLost", function()
		f:SetBorderColor("borderSubtle")
		refreshGhost()
		if onChange then onChange(edit:GetText()) end
	end)
	edit:SetScript("OnTextChanged", function(self, userInput)
		refreshGhost()
		if userInput and f.liveUpdate and onChange then onChange(self:GetText()) end
	end)

	function f:GetValue() return edit:GetText() end
	function f:SetValue(v)
		edit:SetText(v or "")
		refreshGhost()
	end

	return f
end

--- Multi-line input for ad and whisper templates.
function UI:TextArea(parent, initial, onChange, height)
	local f = UI:Panel(parent, "base")
	f:SetHeight(height or 72)
	UI:Border(f, "borderSubtle")

	local scroll = CreateFrame("ScrollFrame", nil, f)
	scroll:SetPoint("TOPLEFT", T.space.sm, -T.space.xs)
	scroll:SetPoint("BOTTOMRIGHT", -T.space.sm, T.space.xs)

	local edit = CreateFrame("EditBox", nil, scroll)
	edit:SetMultiLine(true)
	edit:SetAutoFocus(false)
	edit:SetFont(T:ResolveFont(), T.fontSize.base, "")
	edit:SetTextColor(T:C("textPrimary"))
	edit:SetWidth(1)
	edit:SetText(initial or "")
	scroll:SetScrollChild(edit)
	f.edit = edit

	scroll:SetScript("OnSizeChanged", function(self, w) edit:SetWidth(w) end)

	edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
	edit:SetScript("OnEditFocusGained", function() f:SetBorderColor("accent") end)
	edit:SetScript("OnEditFocusLost", function()
		f:SetBorderColor("borderSubtle")
		if onChange then onChange(edit:GetText()) end
	end)
	edit:SetScript("OnTextChanged", function(self, userInput)
		if userInput and onChange then onChange(self:GetText()) end
	end)

	function f:GetValue() return edit:GetText() end
	function f:SetValue(v) edit:SetText(v or "") end
	function f:InsertAtCursor(text)
		edit:Insert(text)
		edit:SetFocus()
	end

	return f
end

--------------------------------------------------------------------------------
-- Dropdown
--------------------------------------------------------------------------------

local openPopup

-- A full-screen invisible button sitting just under the popup: clicking anywhere else
-- closes it. Event-driven, so nothing polls every frame.
local blocker = CreateFrame("Button", nil, UIParent)
blocker:SetAllPoints(UIParent)
blocker:SetFrameStrata("FULLSCREEN")
blocker:RegisterForClicks("AnyUp")
blocker:Hide()

local function closeOpenPopup()
	if openPopup then
		openPopup:Hide()
		openPopup = nil
	end
	blocker:Hide()
end

blocker:SetScript("OnClick", closeOpenPopup)

--- @param items table array of { value, label }
function UI:Dropdown(parent, items, initialValue, onSelect, width)
	local f = UI:Panel(parent, "overlay")
	f:SetSize(width or 160, T.size.inputHeight)
	UI:Border(f, "borderSubtle")
	f:EnableMouse(true)

	local label = UI:Text(f, "", T.fontSize.base, "", "textPrimary")
	label:SetPoint("LEFT", T.space.sm, 0)
	label:SetPoint("RIGHT", -20, 0)

	local caret = UI:Text(f, "v", T.fontSize.sm, "", "textMuted")
	caret:SetPoint("RIGHT", -T.space.sm, 0)

	f.items = items or {}
	f.value = initialValue

	local function labelFor(value)
		for _, item in ipairs(f.items) do
			if item.value == value then return item.label end
		end
		return "-"
	end

	local popup = CreateFrame("Frame", nil, UIParent)
	popup:SetFrameStrata("FULLSCREEN_DIALOG")
	popup:Hide()
	UI:Fill(popup, "overlay")
	UI:Border(popup, "borderStrong")
	UI:Shadow(popup, 5)
	f.popup = popup

	local function buildPopup()
		popup.rows = popup.rows or {}
		for _, row in ipairs(popup.rows) do row:Hide() end

		local y = -T.space.xs
		for i, item in ipairs(f.items) do
			local row = popup.rows[i]
			if not row then
				row = CreateFrame("Button", nil, popup)
				row:SetHeight(T.size.rowHeight - 4)
				row.bg = row:CreateTexture(nil, "BACKGROUND")
				row.bg:SetAllPoints()
				row.bg:SetColorTexture(0, 0, 0, 0)
				row.text = UI:Text(row, "", T.fontSize.base, "", "textSecondary")
				row.text:SetPoint("LEFT", T.space.sm, 0)
				row:SetScript("OnEnter", function(self)
					self.bg:SetColorTexture(T:C("hover"))
					self.text:SetTextColor(T:C("textPrimary"))
				end)
				row:SetScript("OnLeave", function(self)
					self.bg:SetColorTexture(0, 0, 0, 0)
					self.text:SetTextColor(T:C("textSecondary"))
				end)
				popup.rows[i] = row
			end
			row:SetPoint("TOPLEFT", popup, "TOPLEFT", T.space.xs, y)
			row:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -T.space.xs, y)
			row.text:SetText(item.label)
			row:SetScript("OnClick", function()
				f.value = item.value
				label:SetText(item.label)
				closeOpenPopup()
				if onSelect then onSelect(item.value, item) end
			end)
			row:Show()
			y = y - (T.size.rowHeight - 4)
		end

		popup:SetHeight(math.max(T.size.rowHeight, -y + T.space.xs))
		popup:SetWidth(f:GetWidth())
	end

	f:SetScript("OnMouseDown", function(self)
		if openPopup == popup then
			closeOpenPopup()
			return
		end
		closeOpenPopup()
		buildPopup()
		popup:ClearAllPoints()
		popup:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
		popup:Show()
		blocker:Show()
		openPopup = popup
	end)

	f:SetScript("OnEnter", function(self) self:SetColorToken("hover") end)
	f:SetScript("OnLeave", function(self) self:SetColorToken("overlay") end)

	function f:SetItems(newItems)
		self.items = newItems or {}
		label:SetText(labelFor(self.value))
	end

	function f:SetValue(value, silent)
		self.value = value
		label:SetText(labelFor(value))
		if not silent and onSelect then onSelect(value) end
	end

	function f:GetValue() return self.value end

	label:SetText(labelFor(initialValue))
	return f
end

--------------------------------------------------------------------------------
-- Segmented control
--------------------------------------------------------------------------------

--- Horizontal pill group, used for the Session / Today / Week / All time windows.
function UI:Segmented(parent, options, initialValue, onSelect)
	local f = UI:Panel(parent, "base")
	f:SetHeight(T.size.buttonHeight)
	UI:Border(f, "borderSubtle")
	f.buttons = {}
	f.value = initialValue

	local function render()
		for _, b in ipairs(f.buttons) do
			local active = (b.value == f.value)
			b.bg:SetColorTexture(T:C(active and "accentMuted" or "base"))
			b.label:SetTextColor(T:C(active and "accent" or "textSecondary"))
		end
	end

	local count = #options
	for i, opt in ipairs(options) do
		local b = CreateFrame("Button", nil, f)
		b.value = opt.value
		b.bg = b:CreateTexture(nil, "BACKGROUND")
		b.bg:SetAllPoints()
		b.label = UI:Text(b, opt.label, T.fontSize.sm, "", "textSecondary", "CENTER")
		b.label:SetPoint("CENTER")
		b:SetPoint("TOP", f, "TOP", 0, -1)
		b:SetPoint("BOTTOM", f, "BOTTOM", 0, 1)
		b:SetScript("OnClick", function(self)
			f.value = self.value
			render()
			if onSelect then onSelect(self.value) end
		end)
		f.buttons[i] = b
	end

	f:SetScript("OnSizeChanged", function(self)
		local w = (self:GetWidth() - 2) / count
		for i, b in ipairs(self.buttons) do
			b:ClearAllPoints()
			b:SetPoint("TOPLEFT", self, "TOPLEFT", 1 + (i - 1) * w, -1)
			b:SetSize(w, self:GetHeight() - 2)
		end
	end)

	function f:SetValue(value)
		self.value = value
		render()
	end

	render()
	return f
end

--------------------------------------------------------------------------------
-- Link popup
--------------------------------------------------------------------------------

-- Addons can't open a browser — there is no API for it, at all. The universal convention is
-- to present the URL in a focused, pre-selected edit box so Ctrl+C just works.
local linkPopup

function UI:ShowLink(title, url)
	if not linkPopup then
		linkPopup = UI:Panel(UIParent, "overlay")
		linkPopup:SetSize(420, 116)
		linkPopup:SetPoint("CENTER")
		linkPopup:SetFrameStrata("FULLSCREEN_DIALOG")
		linkPopup:Hide()
		UI:Border(linkPopup, "accent")
		UI:Shadow(linkPopup, 6)
		UI:MakeDraggable(linkPopup)
		linkPopup:SetToplevel(true)

		linkPopup.titleText = UI:Text(linkPopup, "", T.fontSize.md, "", "textPrimary")
		linkPopup.titleText:SetPoint("TOPLEFT", T.space.md, -T.space.md)

		local hint = UI:Text(linkPopup, "Ctrl+C to copy, then paste it into your browser.",
			T.fontSize.sm, "", "textMuted")
		hint:SetPoint("TOPLEFT", linkPopup.titleText, "BOTTOMLEFT", 0, -2)

		local close = UI:Button(linkPopup, "X", function() linkPopup:Hide() end, "ghost", 26, 26)
		close:SetPoint("TOPRIGHT", -T.space.sm, -T.space.sm)

		local input = UI:Input(linkPopup, "", "", nil)
		input:SetPoint("BOTTOMLEFT", T.space.md, T.space.md)
		input:SetPoint("BOTTOMRIGHT", -T.space.md, T.space.md)
		-- Re-select on every focus, so a stray click doesn't leave a partial selection.
		input.edit:HookScript("OnEditFocusGained", function(self) self:HighlightText() end)
		-- It's a copy box, not an editor: never let the URL be altered.
		input.edit:SetScript("OnTextChanged", function(self, userInput)
			if userInput then self:SetText(linkPopup.url or "") self:HighlightText() end
		end)
		linkPopup.input = input

		_G.PortalProLinkPopup = linkPopup
		tinsert(UISpecialFrames, "PortalProLinkPopup")
	end

	linkPopup.url = url
	linkPopup.titleText:SetText(title or "Link")
	linkPopup.input:SetValue(url)
	linkPopup:Show()
	linkPopup.input.edit:SetFocus()
	linkPopup.input.edit:HighlightText()
	return linkPopup
end

--------------------------------------------------------------------------------
-- Stat tile
--------------------------------------------------------------------------------

--- Label over a large value. The building block of the stats screen and the header.
function UI:StatTile(parent, labelText, valueText, valueColor, valueSize)
	local f = UI:Panel(parent, "raised")
	UI:Border(f, "borderSubtle")

	-- Every line gets a RIGHT anchor. Without one a FontString has unbounded width and
	-- simply draws past the tile border instead of truncating.
	local inset = T.space.md

	local label = UI:Label(f, labelText)
	label:SetPoint("TOPLEFT", inset, -inset)
	label:SetPoint("RIGHT", f, "RIGHT", -inset, 0)

	local value = UI:Text(f, valueText or "-", valueSize or T.fontSize.xl, "", valueColor or "textPrimary")
	value:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -T.space.xs)
	value:SetPoint("RIGHT", f, "RIGHT", -inset, 0)

	local sub = UI:Text(f, "", T.fontSize.sm, "", "textMuted")
	sub:SetPoint("TOPLEFT", value, "BOTTOMLEFT", 0, -T.space.xs)
	sub:SetPoint("RIGHT", f, "RIGHT", -inset, 0)
	-- Sub-lines carry breakdowns that can get long; wrapping to a second line beats
	-- clipping half of "11 leads / 0 invited / 0 paid".
	sub:SetWordWrap(true)
	sub:SetJustifyV("TOP")
	if sub.SetMaxLines then pcall(sub.SetMaxLines, sub, 2) end

	f.labelText = label
	f.valueText = value
	f.subText = sub

	function f:SetValue(text, colorName)
		self.valueText:SetText(text)
		if colorName then self.valueText:SetTextColor(T:C(colorName)) end
	end
	function f:SetSub(text) self.subText:SetText(text or "") end

	return f
end
