-- Portal Pro :: UI/Widgets/ScrollList.lua
-- Virtualised list with a custom scrollbar.
--
-- Rows are recycled rather than created per item, so a ledger with thousands of entries
-- costs the same number of frames as one with twenty.

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

local SCROLLBAR_WIDTH = 6

--- @param createRow fun(parent):Frame builds an empty row
--- @param updateRow fun(row, item, index):void binds data to a row
function UI:ScrollList(parent, rowHeight, createRow, updateRow, spacing)
	spacing = spacing or 0
	local step = rowHeight + spacing

	local f = CreateFrame("Frame", nil, parent)
	f.rowHeight = rowHeight
	f.step = step
	f.data = {}
	f.rows = {}
	f.offset = 0

	-- Rows are parented to this frame rather than a scroll child, so without clipping the
	-- bottom row would spill over whatever sits below the list. SetClipsChildren arrived in
	-- 9.0; if this build doesn't have it, fall back to only drawing rows that fit whole.
	f.canClip = pcall(f.SetClipsChildren, f, true)

	-- Scrollbar
	local barTrack = UI:Panel(f, "base")
	barTrack:SetWidth(SCROLLBAR_WIDTH)
	barTrack:SetPoint("TOPRIGHT")
	barTrack:SetPoint("BOTTOMRIGHT")
	barTrack:Hide()
	f.barTrack = barTrack

	local thumb = UI:Panel(barTrack, "borderStrong")
	thumb:SetPoint("LEFT")
	thumb:SetPoint("RIGHT")
	thumb:SetHeight(30)
	f.thumb = thumb

	thumb:EnableMouse(true)
	thumb:SetScript("OnEnter", function(self) self:SetColorToken("textMuted") end)
	thumb:SetScript("OnLeave", function(self) self:SetColorToken("borderStrong") end)

	-- Dragging the thumb maps cursor position to scroll offset.
	local dragging = false
	thumb:SetScript("OnMouseDown", function() dragging = true end)
	thumb:SetScript("OnMouseUp", function() dragging = false end)
	barTrack:SetScript("OnUpdate", function()
		if not dragging then return end
		local _, cursorY = GetCursorPosition()
		local scale = f:GetEffectiveScale()
		cursorY = cursorY / scale
		local top = f:GetTop() or 0
		local height = f:GetHeight()
		local pct = U.Clamp((top - cursorY) / math.max(1, height), 0, 1)
		local maxOffset = math.max(0, #f.data * step - height)
		f:SetOffset(pct * maxOffset)
	end)

	-- Empty state
	local empty = UI:Text(f, "", T.fontSize.base, "", "textMuted", "CENTER")
	empty:SetPoint("CENTER", 0, 8)
	empty:Hide()
	f.emptyText = empty

	local emptySub = UI:Text(f, "", T.fontSize.sm, "", "textMuted", "CENTER")
	emptySub:SetPoint("TOP", empty, "BOTTOM", 0, -4)
	emptySub:Hide()
	f.emptySubText = emptySub

	f:EnableMouseWheel(true)
	f:SetScript("OnMouseWheel", function(self, delta)
		self:SetOffset(self.offset - delta * step * 2)
	end)

	--------------------------------------------------------------------------

	function f:VisibleCount()
		if self.canClip then
			return math.ceil(self:GetHeight() / step) + 1
		end
		return math.max(1, math.floor(self:GetHeight() / step))
	end

	function f:MaxOffset()
		return math.max(0, #self.data * step - self:GetHeight())
	end

	function f:SetOffset(offset)
		self.offset = U.Clamp(offset, 0, self:MaxOffset())
		self:Render()
	end

	function f:SetData(data)
		self.data = data or {}
		self.offset = U.Clamp(self.offset, 0, self:MaxOffset())
		self:Render()
	end

	function f:SetEmptyState(title, subtitle)
		self.emptyText:SetText(title or "")
		self.emptySubText:SetText(subtitle or "")
	end

	function f:Render()
		local count = #self.data
		local hasData = count > 0

		self.emptyText:SetShown(not hasData)
		self.emptySubText:SetShown(not hasData)

		local maxOffset = self:MaxOffset()
		local needsBar = maxOffset > 0
		self.barTrack:SetShown(needsBar)

		if needsBar then
			local visibleRatio = self:GetHeight() / (count * step)
			local thumbHeight = math.max(24, self:GetHeight() * visibleRatio)
			self.thumb:SetHeight(thumbHeight)
			local pct = maxOffset > 0 and (self.offset / maxOffset) or 0
			self.thumb:ClearAllPoints()
			self.thumb:SetPoint("LEFT")
			self.thumb:SetPoint("RIGHT")
			self.thumb:SetPoint("TOP", self.barTrack, "TOP", 0, -pct * (self:GetHeight() - thumbHeight))
		end

		local first = math.floor(self.offset / step)
		local visible = self:VisibleCount()
		local rightInset = needsBar and (SCROLLBAR_WIDTH + T.space.xs) or 0

		for i = 1, visible do
			local index = first + i
			local row = self.rows[i]

			if index <= count then
				if not row then
					row = createRow(self)
					self.rows[i] = row
				end
				row:ClearAllPoints()
				row:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -((index - 1) * step - self.offset))
				row:SetPoint("TOPRIGHT", self, "TOPRIGHT", -rightInset, -((index - 1) * step - self.offset))
				row:SetHeight(rowHeight)
				updateRow(row, self.data[index], index)
				row:Show()
			elseif row then
				row:Hide()
			end
		end

		-- Hide any rows beyond the current visible window.
		for i = visible + 1, #self.rows do
			self.rows[i]:Hide()
		end
	end

	f:SetScript("OnSizeChanged", function(self) self:Render() end)
	f:SetScript("OnShow", function(self) self:Render() end)

	return f
end
