local _, GF = ...

GF.UI = GF.UI or {}

-- GF's single adapter around Blizzard's ScrollBox API. Business pages provide
-- row identity and rendering callbacks; this owner handles native frames,
-- providers, stable viewport anchoring, and no other page state.
local VirtualList = {}
VirtualList.__index = VirtualList

GF.UI.VirtualList = VirtualList
GF.UI.ScrollList = VirtualList -- compatibility for existing GF page owners

local function nativeScrollBoxAvailable()
	return type(CreateScrollBoxListLinearView) == "function"
		and type(CreateDataProvider) == "function"
		and type(ScrollUtil) == "table"
		and type(ScrollUtil.InitScrollBoxListWithScrollBar) == "function"
end

function VirtualList.IsAvailable()
	return nativeScrollBoxAvailable()
end

local function retainedPositionArgument(shouldRetain)
	if shouldRetain ~= true then
		return nil
	end
	local constants = ScrollBoxConstants
	if constants and constants.RetainScrollPosition ~= nil then
		return constants.RetainScrollPosition
	end
	return true
end

local function createLinearView(config)
	local padding = config.padding
	local view
	if type(padding) == "table" then
		view = CreateScrollBoxListLinearView(
			padding[1] or 0,
			padding[2] or 0,
			padding[3] or 0,
			padding[4] or 0,
			padding[5] or 0
		)
	else
		view = CreateScrollBoxListLinearView()
	end

	if type(config.extentCalculator) == "function" then
		view:SetElementExtentCalculator(config.extentCalculator)
	else
		local fixedExtent = tonumber(config.rowHeight)
		if fixedExtent then
			view:SetElementExtent(fixedExtent)
		end
	end

	if type(config.elementFactory) == "function" then
		view:SetElementFactory(config.elementFactory)
	elseif type(config.elementInitializer) == "function" then
		view:SetElementInitializer(
			config.frameType or "Frame",
			config.elementInitializer
		)
	end
	return view
end

local function createNativeFrames(parent, config)
	local scrollBox = CreateFrame("Frame", nil, parent, "WowScrollBoxList")
	scrollBox:SetClipsChildren(true)

	local scrollBar = CreateFrame(
		"EventFrame",
		nil,
		config.barParent or parent,
		"MinimalScrollBar"
	)
	local horizontalGap = tonumber(config.barOffsetX)
		or GF.CONTENT_SCROLLBAR_OFFSET_X
		or 9
	scrollBar:ClearAllPoints()
	scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", horizontalGap, 0)
	scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", horizontalGap, 0)
	scrollBar:SetFrameLevel(scrollBox:GetFrameLevel() + 10)

	if GF.UI.ApplyCommonScrollBarSkin then
		GF.UI.ApplyCommonScrollBarSkin(scrollBar)
	elseif config.keepNativeScrollBar ~= true
		and GF.UI.StripMinimalScrollBarSteppers
	then
		GF.UI.StripMinimalScrollBarSteppers(scrollBar)
	end
	if scrollBar.SetHideIfUnscrollable then
		local hideIfUnscrollable = config.hideIfUnscrollable
		if hideIfUnscrollable == nil then
			hideIfUnscrollable = true
		end
		scrollBar:SetHideIfUnscrollable(hideIfUnscrollable == true)
	end
	scrollBar:Show()
	return scrollBox, scrollBar
end

function VirtualList.Create(parent, options)
	if not parent or not nativeScrollBoxAvailable() then
		return nil
	end
	local config = type(options) == "table" and options or {}
	local scrollBox, scrollBar = createNativeFrames(parent, config)
	local view = createLinearView(config)
	ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, view)

	return setmetatable({
		scrollBox = scrollBox,
		scrollBar = scrollBar,
		view = view,
		assignedKey = config.assignedKey,
		dataProvider = nil,
	}, VirtualList)
end

function VirtualList:GetScrollBox()
	return self.scrollBox
end

function VirtualList:GetScrollBar()
	return self.scrollBar
end

function VirtualList:ClearAllPoints()
	local frame = self.scrollBox
	if frame then
		frame:ClearAllPoints()
	end
end

function VirtualList:SetPoint(...)
	local frame = self.scrollBox
	if frame then
		frame:SetPoint(...)
	end
end

function VirtualList:AnchorContentScroll(parent)
	local frame = self.scrollBox
	if not frame or not parent then
		return
	end
	frame:ClearAllPoints()
	frame:SetPoint(
		"TOPLEFT",
		parent,
		"TOPLEFT",
		GF.CONTENT_SCROLL_INSET_L or 0,
		0
	)
	if GF.UI.AnchorContentScrollBottomRight then
		GF.UI.AnchorContentScrollBottomRight(frame, parent)
	end
end

local function identityAnchorFor(list)
	local frame = list.scrollBox
	local provider = list.dataProvider
	local identityField = list.assignedKey
	if not (frame and provider and identityField) then
		return nil
	end
	if type(frame.GetDataIndexBegin) ~= "function"
		or type(frame.GetDerivedScrollOffset) ~= "function"
		or type(frame.GetExtentUntil) ~= "function"
		or type(provider.Find) ~= "function"
	then
		return nil
	end

	local firstVisibleIndex = frame:GetDataIndexBegin()
	local element = firstVisibleIndex and provider:Find(firstVisibleIndex)
	local identity = type(element) == "table" and element[identityField] or nil
	if identity == nil then
		return nil
	end
	return {
		key = identity,
		offset = frame:GetDerivedScrollOffset()
			- frame:GetExtentUntil(firstVisibleIndex),
	}
end

function VirtualList:CaptureIdentityAnchor()
	return identityAnchorFor(self)
end

local function findIdentityIndex(provider, field, identity)
	if type(provider.FindIndexByPredicate) ~= "function" then
		return nil
	end
	return provider:FindIndexByPredicate(function(element)
		return type(element) == "table" and element[field] == identity
	end)
end

function VirtualList:RestoreIdentityAnchor(anchor)
	local frame = self.scrollBox
	local provider = self.dataProvider
	local identityField = self.assignedKey
	if type(anchor) ~= "table" or anchor.key == nil then
		return false
	end
	if not (frame and provider and identityField)
		or type(frame.GetExtentUntil) ~= "function"
		or type(frame.ScrollToOffset) ~= "function"
	then
		return false
	end

	local index = findIdentityIndex(provider, identityField, anchor.key)
	if not index then
		return false
	end
	local mode = ScrollBoxConstants
		and ScrollBoxConstants.NoScrollInterpolation
		or true
	frame:ScrollToOffset(
		frame:GetExtentUntil(index) + (tonumber(anchor.offset) or 0),
		mode
	)
	return true
end

function VirtualList:SetElements(elements, options)
	local frame = self.scrollBox
	if not frame or type(CreateDataProvider) ~= "function" then
		return nil
	end
	local config = type(options) == "table" and options or {}
	local anchor = config.retainIdentity == true
		and identityAnchorFor(self)
		or nil
	local provider = CreateDataProvider(
		type(elements) == "table" and elements or {}
	)
	self.dataProvider = provider
	frame:SetDataProvider(
		provider,
		retainedPositionArgument(config.retainScroll)
	)
	if anchor then
		self:RestoreIdentityAnchor(anchor)
	end
	return provider
end

function VirtualList:ForEachFrame(visitor)
	local frame = self.scrollBox
	if type(visitor) == "function"
		and frame
		and type(frame.ForEachFrame) == "function"
	then
		frame:ForEachFrame(visitor)
	end
end

function VirtualList:FindFrameByPredicate(predicate)
	local frame = self.scrollBox
	if type(predicate) ~= "function"
		or not frame
		or type(frame.FindFrameByPredicate) ~= "function"
	then
		return nil
	end
	return frame:FindFrameByPredicate(predicate)
end

function VirtualList:FindFrameByKey(identity)
	local field = self.assignedKey
	if field == nil or identity == nil then
		return nil
	end
	return self:FindFrameByPredicate(function(_, element)
		return type(element) == "table" and element[field] == identity
	end)
end

function VirtualList:GetLayoutWidth()
	local frame = self.scrollBox
	local width = frame and type(frame.GetWidth) == "function"
		and tonumber(frame:GetWidth())
		or 0
	return math.max(1, math.floor(width or 0))
end

function VirtualList:RetainScrollPosition()
	local frame = self.scrollBox
	local provider = self.dataProvider
	if not frame or not provider then
		return
	end
	local retain = retainedPositionArgument(true)
	if type(frame.Rebuild) == "function" then
		frame:Rebuild(retain)
	else
		frame:SetDataProvider(provider, retain)
	end
end

function VirtualList:ScrollToBegin()
	local frame = self.scrollBox
	if frame and type(frame.ScrollToBegin) == "function" then
		frame:ScrollToBegin()
	end
end

function VirtualList:GetElementData(frame)
	if frame and type(frame.GetElementData) == "function" then
		return frame:GetElementData()
	end
	return nil
end
