local _, GF = ...

local BrowseScrollList = GF.BrowseScrollList or {}
GF.BrowseScrollList = BrowseScrollList

local ACTION_ELEMENT_KIND = "quick_create"
local CENSORED_DEBUG_ELEMENT_KIND = "censored_debug"

local function browseRowHeight()
	if GF.GetListRowH then
		return GF.GetListRowH()
	end
	return GF.LIST_ROW_H or 32
end

local function makeResultElement(resultID, index)
	return {
		resultID = resultID,
		dataIndex = index,
	}
end

local function clearReusableTable(values)
	values = type(values) == "table" and values or {}
	for key in pairs(values) do
		values[key] = nil
	end
	return values
end

local function isActionElement(elementData)
	return type(elementData) == "table"
		and elementData.kind == ACTION_ELEMENT_KIND
end

local function isCensoredDebugElement(elementData)
	return type(elementData) == "table"
		and elementData.kind == CENSORED_DEBUG_ELEMENT_KIND
end

function BrowseScrollList.BuildCensoredDebugElements(entry, categoryID)
	return {
		{
			kind = CENSORED_DEBUG_ELEMENT_KIND,
			entry = entry,
			categoryID = categoryID,
		},
	}
end

local function actionRowMinimumHeight()
	return tonumber(GF.BROWSE_RESULT_ACTION_ROW_MIN_HEIGHT) or 36
end

function BrowseScrollList.CalculateResultActionExtent(visibleExtent, resultCount)
	local minimumHeight = actionRowMinimumHeight()
	local availableHeight = tonumber(visibleExtent) or 0
	local count = math.max(0, math.floor(tonumber(resultCount) or 0))
	if availableHeight <= 0 then
		return minimumHeight, false
	end
	local extent = math.max(
		minimumHeight,
		availableHeight - (count * browseRowHeight())
	)
	return extent, extent > minimumHeight
end

function BrowseScrollList.GetResultActionButtonOffsetY(rowExtent, isExpanded)
	if isExpanded == nil then
		isExpanded = (tonumber(rowExtent) or 0) > actionRowMinimumHeight()
	end
	if isExpanded == true then
		return tonumber(GF.BROWSE_RESULT_ACTION_SHORT_OFFSET_Y) or 10
	end
	return 0
end

function BrowseScrollList.BuildElements(resultIDs, action, cache)
	cache = type(cache) == "table" and cache or {}
	local elements = clearReusableTable(cache.elements)
	local byResultID = type(cache.byResultID) == "table"
		and cache.byResultID or {}
	local freeElements = type(cache.freeElements) == "table"
		and cache.freeElements or {}
	local currentResultIDs = clearReusableTable(cache.currentResultIDs)
	local uniqueResultIDs = clearReusableTable(cache.uniqueResultIDs)
	local diagnostics = GF.SearchMemoryDiagnostics
	local diagnosticActive = diagnostics and diagnostics.current ~= nil
	local wrappersCreated, wrappersReused, wrappersPruned = 0, 0, 0
	local generation = (tonumber(cache.generation) or 0) + 1
	cache.elements = elements
	cache.byResultID = byResultID
	cache.freeElements = freeElements
	cache.currentResultIDs = currentResultIDs
	cache.uniqueResultIDs = uniqueResultIDs
	cache.generation = generation
	for index = 1, type(resultIDs) == "table" and #resultIDs or 0 do
		local resultID = resultIDs[index]
		if resultID ~= nil and currentResultIDs[resultID] == nil then
			currentResultIDs[resultID] = index
			uniqueResultIDs[#uniqueResultIDs + 1] = resultID
		end
	end
	for resultID, element in pairs(byResultID) do
		if not currentResultIDs[resultID] then
			byResultID[resultID] = nil
			element.resultID = nil
			element.dataIndex = nil
			element._gfBrowseProjection = nil
			freeElements[#freeElements + 1] = element
			if diagnosticActive then
				wrappersPruned = wrappersPruned + 1
			end
		end
	end
	if type(resultIDs) ~= "table" then
		if cache.actionElement then
			cache.actionElement.kind = nil
			cache.actionElement.action = nil
			cache.actionElement.resultCount = nil
		end
		if diagnosticActive and diagnostics.RecordWrappers then
			diagnostics:RecordWrappers(0, 0, wrappersPruned)
		end
		return elements, cache
	end
	for index = 1, #uniqueResultIDs do
		local resultID = uniqueResultIDs[index]
		local element = byResultID[resultID]
		if not element then
			local freeIndex = #freeElements
			element = freeElements[freeIndex]
			if element then
				freeElements[freeIndex] = nil
			else
				element = makeResultElement(resultID, index)
			end
			byResultID[resultID] = element
			if diagnosticActive then
				if freeIndex > 0 then
					wrappersReused = wrappersReused + 1
				else
					wrappersCreated = wrappersCreated + 1
				end
			end
		else
			if diagnosticActive then
				wrappersReused = wrappersReused + 1
			end
		end
		element.resultID = resultID
		element.dataIndex = currentResultIDs[resultID]
		element._gfBrowseProjection = generation
		elements[index] = element
	end
	if diagnosticActive and diagnostics.RecordWrappers then
		diagnostics:RecordWrappers(
			wrappersCreated, wrappersReused, wrappersPruned)
	end
	if type(action) == "table" then
		local actionElement = cache.actionElement or {}
		actionElement.kind = ACTION_ELEMENT_KIND
		actionElement.action = action
		actionElement.resultCount = #uniqueResultIDs
		cache.actionElement = actionElement
		elements[#elements + 1] = actionElement
	elseif cache.actionElement then
		cache.actionElement.kind = nil
		cache.actionElement.action = nil
		cache.actionElement.resultCount = nil
	end
	return elements, cache
end

local function ensureBrowseRow(row)
	local rowModule = GF.ListRow
	if rowModule and rowModule.EnsureRow then
		rowModule:EnsureRow(row)
	end
end

function BrowseScrollList.BindResultRow(row, elementData, panel, opts)
	opts = opts or {}
	if row then
		row._gfCensoredDebugPreview = nil
	end
	local rowModule = GF.ListRow
	if rowModule and rowModule.BindElement then
		local bound = rowModule:BindElement(
			row, elementData, panel, { deferRoles = true })
		if bound == true then
			if panel and panel.ClearFailedResultBinding then
				panel:ClearFailedResultBinding(
					elementData and elementData.resultID,
					elementData
				)
			end
			if row and row.Show then
				row:Show()
			end
			return true
		end
	end
	if rowModule and rowModule.DetachRow then
		rowModule:DetachRow(row)
	end
	if row and row.Hide then
		row:Hide()
	end
	-- The initialized frame has already consumed its fixed ScrollBox extent.
	-- Hiding stale recycled content is necessary, but leaving the element in
	-- the provider forever would expose that extent as a blank row.  Defer one
	-- authoritative retry outside the initialization callback; a persistent
	-- failure is then removed by the Browse owner with its identity anchor kept.
	if opts.suppressRecovery ~= true
		and panel and panel.QueueFailedResultBinding
	then
		panel:QueueFailedResultBinding(row, elementData)
	end
	return false
end

function BrowseScrollList.BindCensoredDebugRow(row, elementData, panel)
	local rowModule = GF.ListRow
	local entry = elementData and elementData.entry
	if not (row and rowModule and rowModule.SetData and entry and entry.info) then
		return false
	end
	local measuredWidth = panel and panel.scrollList
		and panel.scrollList:GetLayoutWidth() or 0
	local layoutW = measuredWidth > 0 and measuredWidth
		or panel and panel._lastLayoutW or 400
	if panel then
		panel._lastLayoutW = layoutW
	end
	row._gfCensoredDebugPreview = nil
	row:SetWidth(layoutW)
	local bound = rowModule:SetData(
		row,
		0,
		elementData.categoryID,
		entry,
		{
			deferRoles = false,
			layoutW = layoutW,
		})
	if bound ~= true then
		if rowModule.DetachRow then
			rowModule:DetachRow(row)
		end
		row:Hide()
		return false
	end
	row.resultIndex = nil
	row.resultID = nil
	row._gfCensoredDebugPreview = true
	row:Show()
	return true
end

local function layoutActionButton(row)
	local button = row and row.actionButton
	if not button then
		return
	end
	local rowExtent = row.GetHeight and row:GetHeight() or 0
	local elementData = row._gfBrowseActionElement
	local isExpanded = row._gfBrowseActionExpanded
	if type(elementData) == "table" then
		isExpanded = elementData.actionRowExpanded == true
	end
	button:ClearAllPoints()
	button:SetPoint(
		"CENTER",
		row,
		"CENTER",
		0,
		BrowseScrollList.GetResultActionButtonOffsetY(
			rowExtent,
			isExpanded
		)
	)
end

local function ensureActionRow(row)
	if row.actionButton then
		layoutActionButton(row)
		return
	end
	row._gfBrowseActionRow = true
	local button = GF.UI.CreatePanelButton(
		row,
		"",
		GF.BROWSE_EMPTY_ACTION_MIN_WIDTH or 120
	)
	button:SetScript("OnClick", function()
		local panel = row._gfBrowsePanel
		if panel and panel.ExecuteResultAction then
			panel:ExecuteResultAction(row._gfBrowseAction)
		end
	end)
	row.actionButton = button
	row:HookScript("OnSizeChanged", layoutActionButton)
	layoutActionButton(row)
end

local function bindActionRow(row, elementData, panel)
	ensureActionRow(row)
	row._gfBrowsePanel = panel
	row._gfBrowseAction = elementData.action
	row._gfBrowseActionElement = elementData
	row._gfBrowseActionExpanded = elementData.actionRowExpanded == true
	layoutActionButton(row)
	if panel and panel.ConfigureQuickCreateButton then
		panel:ConfigureQuickCreateButton(row.actionButton, elementData.action)
	end
end

local function installRowBinding(scrollList, panel)
	if not scrollList or not ScrollUtil or not ScrollUtil.AddInitializedFrameCallback then
		return
	end
	local scrollBox = scrollList:GetScrollBox()
	if not scrollBox then
		return
	end
	ScrollUtil.AddInitializedFrameCallback(scrollBox, function(_, row, elementData)
		if isActionElement(elementData) then
			bindActionRow(row, elementData, panel)
		elseif isCensoredDebugElement(elementData) then
			BrowseScrollList.BindCensoredDebugRow(row, elementData, panel)
		else
			BrowseScrollList.BindResultRow(row, elementData, panel)
		end
	end, panel, false)
end

function BrowseScrollList.Create(panel, parent, opts)
	opts = opts or {}
	local scrollList
	scrollList = GF.UI.ScrollList.Create(parent, {
		extentCalculator = function(_, elementData)
			if isCensoredDebugElement(elementData) then
				return browseRowHeight()
			end
			if isActionElement(elementData) then
				local scrollBox = scrollList and scrollList:GetScrollBox()
				local visibleExtent = scrollBox and scrollBox.GetVisibleExtent
					and scrollBox:GetVisibleExtent() or 0
				local extent, isExpanded =
					BrowseScrollList.CalculateResultActionExtent(
					visibleExtent,
					elementData.resultCount
				)
				elementData.actionRowExpanded = isExpanded == true
				return extent
			end
			return browseRowHeight()
		end,
		assignedKey = "resultID",
		barParent = opts.barParent or parent,
		keepNativeScrollBar = true,
		elementFactory = function(factory, elementData)
			if isActionElement(elementData) then
				factory("Frame", ensureActionRow)
			else
				factory("Button", ensureBrowseRow)
			end
		end,
	})
	installRowBinding(scrollList, panel)
	return scrollList
end

local function getLayoutWidth(panel)
	local scrollList = panel and panel.scrollList
	if not scrollList then
		return nil
	end
	local width = scrollList:GetLayoutWidth()
	if width <= 0 then
		return panel._lastLayoutW or 400
	end
	return width
end

function BrowseScrollList.RelayoutVisible(panel)
	if not panel or not panel.scrollList then
		return
	end
	local layoutW = getLayoutWidth(panel)
	panel._lastLayoutW = layoutW
	local rowModule = GF.ListRow
	panel.scrollList:ForEachFrame(function(row)
		if not row._gfBrowseActionRow and rowModule and rowModule.LayoutOnly then
			rowModule:LayoutOnly(row, layoutW)
		end
	end)
end
