---@class StdUi
local StdUi = LibStub and LibStub('StdUi', true)
if not StdUi then return end

local module, version = 'Scroll', 6
if not StdUi:UpgradeNeeded(module, version) then return end

local round = function(num)
	return math.floor(num + 0.5)
end

----------------------------------------------------
--- ScrollFrame
----------------------------------------------------
---@class ScrollBarEvents
StdUi.ScrollBarEvents = {

	UpDownButtonOnClick = function(self)
		local scrollBar = self.scrollBar
		local scrollFrame = scrollBar.scrollFrame
		local scrollStep = scrollBar.scrollStep or (scrollFrame:GetHeight() / 2)

		if self.direction == 1 then
			scrollBar:SetValue(scrollBar:GetValue() - scrollStep)
		else
			scrollBar:SetValue(scrollBar:GetValue() + scrollStep)
		end
	end,

	OnValueChanged = function(self, value)
		self.scrollFrame:SetVerticalScroll(value)
	end,
}

---@class ScrollFrameEvents
StdUi.ScrollFrameEvents = {
	OnMouseWheel = function(self, value, scrollBar)
		scrollBar = scrollBar or self.scrollBar
		local scrollStep = scrollBar.scrollStep or scrollBar:GetHeight() / 2

		if value > 0 then
			scrollBar:SetValue(scrollBar:GetValue() - scrollStep)
		else
			scrollBar:SetValue(scrollBar:GetValue() + scrollStep)
		end
	end,

	OnScrollRangeChanged = function(self, _, yRange)
		-- xRange
		local scrollbar = self.scrollBar
		if not yRange then yRange = self:GetVerticalScrollRange() end

		-- Accounting for very small ranges
		yRange = math.floor(yRange)

		local value = math.min(scrollbar:GetValue(), yRange)
		scrollbar:SetMinMaxValues(0, yRange)
		scrollbar:SetValue(value)

		local scrollDownButton = scrollbar.ScrollDownButton
		local scrollUpButton = scrollbar.ScrollUpButton
		local thumbTexture = scrollbar.ThumbTexture

		if yRange == 0 then
			if self.scrollBarHideable then
				scrollbar:Hide()
				scrollDownButton:Hide()
				scrollUpButton:Hide()
				thumbTexture:Hide()
			else
				scrollDownButton:Disable()
				scrollUpButton:Disable()
				scrollDownButton:Show()
				scrollUpButton:Show()
				if not self.noScrollThumb then thumbTexture:Show() end
			end
		else
			scrollDownButton:Show()
			scrollUpButton:Show()
			scrollbar:Show()
			if not self.noScrollThumb then thumbTexture:Show() end
			-- The 0.005 is to account for precision errors
			if yRange - value > 0.005 then
				scrollDownButton:Enable()
			else
				scrollDownButton:Disable()
			end
		end
	end,

	OnVerticalScroll = function(self, offset)
		local scrollBar = self.scrollBar
		scrollBar:SetValue(offset)

		local _, max = scrollBar:GetMinMaxValues()
		scrollBar.ScrollUpButton:SetEnabled(offset ~= 0)
		scrollBar.ScrollDownButton:SetEnabled((scrollBar:GetValue() - max) ~= 0)
	end,
}

---@class ScrollFrameMethods
StdUi.ScrollFrameMethods = {
	SetScrollStep = function(self, scrollStep)
		scrollStep = round(scrollStep)
		self.scrollBar.scrollStep = scrollStep
		self.scrollBar:SetValueStep(scrollStep)
	end,

	GetChildFrames = function(self)
		return self.scrollBar, self.scrollChild, self.scrollBar.ScrollUpButton, self.scrollBar.ScrollDownButton
	end,

	UpdateSize = function(self, newWidth, newHeight)
		self:SetSize(newWidth, newHeight)
		self.scrollFrame:ClearAllPoints()

		-- scrollbar width and margins
		self.scrollFrame:SetSize(newWidth - self.scrollBarWidth - 5, newHeight - 4)
		self.stdUi:GlueAcross(self.scrollFrame, self, 2, -2, -self.scrollBarWidth - 2, 2)

		-- panel of scrollBar
		self.scrollBar.panel:SetPoint('TOPRIGHT', self, 'TOPRIGHT', -2, -2)
		self.scrollBar.panel:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', -2, 2)

		if self.scrollChild then
			self.scrollChild:SetWidth(self.scrollFrame:GetWidth())
			self.scrollChild:SetHeight(self.scrollFrame:GetHeight())
		end
	end,
}

---@return StdUi.ScrollFrame
function StdUi:ScrollFrame(parent, width, height, scrollChild)
	local panel = self:Panel(parent, width, height) ---@class StdUi.ScrollFrame : Frame, ScrollFrameMethods, ScrollFrameEvents
	panel.stdUi = self
	panel.offset = 0
	panel.scrollBarWidth = 16

	local scrollFrame = CreateFrame('ScrollFrame', nil, panel)
	local scrollBar = self:ScrollBar(panel, panel.scrollBarWidth)
	scrollBar:SetMinMaxValues(0, 0)
	scrollBar:SetValue(0)

	scrollBar:SetScript('OnValueChanged', self.ScrollBarEvents.OnValueChanged)
	scrollBar.ScrollUpButton.direction = 1
	scrollBar.ScrollDownButton.direction = -1
	scrollBar.ScrollDownButton:SetScript('OnClick', self.ScrollBarEvents.UpDownButtonOnClick)
	scrollBar.ScrollUpButton:SetScript('OnClick', self.ScrollBarEvents.UpDownButtonOnClick)
	scrollBar.ScrollDownButton:Disable()
	scrollBar.ScrollUpButton:Disable()

	if self.noScrollThumb then scrollBar.ThumbTexture:Hide() end

	scrollBar.scrollFrame = scrollFrame
	scrollFrame.scrollBar = scrollBar
	scrollFrame.panel = panel

	panel.scrollBar = scrollBar
	panel.scrollFrame = scrollFrame

	for k, v in pairs(self.ScrollFrameMethods) do
		panel[k] = v
	end

	for k, v in pairs(self.ScrollFrameEvents) do
		scrollFrame:SetScript(k, v)
	end

	if not scrollChild then
		scrollChild = CreateFrame('Frame', nil, scrollFrame)
		scrollChild:SetWidth(scrollFrame:GetWidth())
		scrollChild:SetHeight(scrollFrame:GetHeight())
	else
		scrollChild:SetParent(scrollFrame)
	end
	panel.scrollChild = scrollChild

	panel:UpdateSize(width, height)

	scrollFrame:SetScrollChild(scrollChild)
	scrollFrame:EnableMouse(true)
	scrollFrame:SetClampedToScreen(true)
	scrollFrame:SetClipsChildren(true)

	scrollChild:SetPoint('RIGHT', scrollFrame, 'RIGHT', 0, 0)

	scrollFrame.scrollChild = scrollChild

	return panel
end

----------------------------------------------------
--- FauxScrollFrame
----------------------------------------------------

StdUi.FauxScrollFrameMethods = {
	GetOffset = function(self)
		return self.offset or 0
	end,

	--- Performs vertical scroll
	DoVerticalScroll = function(self, value, itemHeight, updateFunction)
		local scrollBar = self.scrollBar
		itemHeight = itemHeight or self.lineHeight

		scrollBar:SetValue(value)
		self.offset = floor((value / itemHeight) + 0.5)

		if updateFunction then updateFunction(self) end
	end,

	--- Redraws items in case of manual update from outside without changing parameters
	Redraw = function(self)
		local itemCount = self.itemCount
		if not itemCount and self.scrollChild and self.scrollChild.items then
			itemCount = #self.scrollChild.items
		end
		if itemCount then
			self:Update(itemCount, self.displayCount, self.lineHeight)
		end
	end,

	UpdateItemsCount = function(self, newCount)
		self.itemCount = newCount
		self:Update(newCount, self.displayCount, self.lineHeight)
	end,

	Update = function(self, numItems, numToDisplay, buttonHeight)
		local scrollBar, scrollChildFrame, scrollUpButton, scrollDownButton = self:GetChildFrames()

		local showScrollBar
		if numItems == nil or numToDisplay == nil then return end

		if numItems > numToDisplay then
			showScrollBar = 1
		else
			scrollBar:SetValue(0)
		end

		if self:IsShown() then
			local scrollFrameHeight = 0
			local scrollChildHeight = 0

			if numItems > 0 then
				scrollFrameHeight = (numItems - numToDisplay) * buttonHeight
				scrollChildHeight = numItems * buttonHeight
				if scrollFrameHeight < 0 then scrollFrameHeight = 0 end
				scrollChildFrame:Show()
			else
				scrollChildFrame:Hide()
			end

			local maxRange = (numItems - numToDisplay) * buttonHeight
			if maxRange < 0 then maxRange = 0 end

			scrollBar:SetMinMaxValues(0, maxRange)
			self:SetScrollStep(buttonHeight)
			scrollBar:SetStepsPerPage(numToDisplay - 1)
			scrollChildFrame:SetHeight(scrollChildHeight)

			-- Arrow button handling
			if scrollBar:GetValue() == 0 then
				scrollUpButton:Disable()
			else
				scrollUpButton:Enable()
			end

			if (scrollBar:GetValue() - scrollFrameHeight) == 0 then
				scrollDownButton:Disable()
			else
				scrollDownButton:Enable()
			end
		end

		return showScrollBar
	end,
}

local OnVerticalScrollUpdate = function(self)
	self:Redraw()
end

StdUi.FauxScrollFrameEvents = {
	OnVerticalScroll = function(self, value)
		value = round(value)
		local panel = self.panel

		panel:DoVerticalScroll(value, panel.lineHeight, OnVerticalScrollUpdate)
	end,
}

--- Works pretty much the same as scroll frame however it does not have smooth scroll and only display a certain amount
--- of items
function StdUi:FauxScrollFrame(parent, width, height, displayCount, lineHeight, scrollChild)
	local panel = self:ScrollFrame(parent, width, height, scrollChild)

	panel.lineHeight = lineHeight
	panel.displayCount = displayCount

	for k, v in pairs(self.FauxScrollFrameMethods) do
		panel[k] = v
	end

	for k, v in pairs(self.FauxScrollFrameEvents) do
		panel.scrollFrame:SetScript(k, v)
	end

	return panel
end

----------------------------------------------------
--- HybridScrollFrame
----------------------------------------------------
StdUi.HybridScrollFrameMethods = {
	Update = function(self, totalHeight)
		local range = floor(totalHeight - self.scrollChild:GetHeight() + 0.5)

		if range > 0 and self.scrollBar then
			local _, maxVal = self.scrollBar:GetMinMaxValues()

			if math.floor(self.scrollBar:GetValue()) >= math.floor(maxVal) then
				self.scrollBar:SetMinMaxValues(0, range)

				if range < maxVal then
					if math.floor(self.scrollBar:GetValue()) ~= math.floor(range) then
						self.scrollBar:SetValue(range)
					else
						-- If we've scrolled to the bottom, we need to recalculate the offset.
						self:SetOffset(self, range)
					end
				end
			else
				self.scrollBar:SetMinMaxValues(0, range)
			end

			self.scrollBar:Enable()
			self:UpdateScrollBarState()
			self.scrollBar:Show()
		elseif self.scrollBar then
			self.scrollBar:SetValue(0)
			if self.scrollBar.doNotHide then
				self.scrollBar:Disable()
				self.scrollBar.ScrollUpButton:Disable()
				self.scrollBar.ScrollDownButton:Disable()
				self.scrollBar.ThumbTexture:Hide()
			else
				self.scrollBar:Hide()
			end
		end

		self.range = range
		self.totalHeight = totalHeight
		self.scrollFrame:UpdateScrollChildRect()
	end,

	SetData = function(self, data)
		self.data = data
	end,

	SetUpdateFunction = function(self, updateFn)
		self.updateFn = updateFn
	end,

	UpdateScrollBarState = function(self, currValue)
		if not currValue then currValue = self.scrollBar:GetValue() end

		self.scrollBar.ScrollUpButton:Enable()
		self.scrollBar.ScrollDownButton:Enable()

		local minVal, maxVal = self.scrollBar:GetMinMaxValues()
		if currValue >= maxVal then
			self.scrollBar.ThumbTexture:Show()
			if self.scrollBar.ScrollDownButton then self.scrollBar.ScrollDownButton:Disable() end
		end

		if currValue <= minVal then
			self.scrollBar.ThumbTexture:Show()
			if self.scrollBar.ScrollUpButton then self.scrollBar.ScrollUpButton:Disable() end
		end
	end,

	GetOffset = function(self)
		return math.floor(self.offset or 0), (self.offset or 0)
	end,

	SetOffset = function(self, offset)
		local items = self.items
		local itemHeight = self.itemHeight
		local element, overflow

		local scrollHeight = 0

		if self.dynamic then
			--This is for frames where items will have different heights
			if offset < itemHeight then
				-- a little optimization
				element, scrollHeight = 0, offset
			else
				element, scrollHeight = self.dynamic(offset)
			end
		else
			element = offset / itemHeight
			overflow = element - math.floor(element)
			scrollHeight = overflow * itemHeight
		end

		if math.floor(self.offset or 0) ~= math.floor(element) and self.updateFn then
			self.offset = element
			self:UpdateItems()
		else
			self.offset = element
		end

		self.scrollFrame:SetVerticalScroll(scrollHeight)
	end,

	CreateItems = function(self, data, create, update, padding, oX, oY)
		local scrollChild = self.scrollChild
		local itemHeight = 0
		local numItems = #data
		--initialPoint = initialPoint or 'TOPLEFT';
		--initialRelative = initialRelative or 'TOPLEFT';
		--point = point or 'TOPLEFT';
		--relativePoint = relativePoint or 'BOTTOMLEFT';
		--offsetX = offsetX or 0;
		--offsetY = offsetY or 0;

		if not self.items then self.items = {} end

		self.data = data
		self.createFn = create
		self.updateFn = update
		self.itemPadding = padding

		self.stdUi:ObjectList(scrollChild, self.items, create, update, data, padding, oX, oY, function(i, totalHeight, lih)
			return totalHeight > self:GetHeight() + lih
		end)

		if self.items[1] then itemHeight = round(self.items[1]:GetHeight() + padding) end
		self.itemHeight = itemHeight

		local totalHeight = numItems * itemHeight
		self.scrollFrame:SetVerticalScroll(0)

		local scrollBar = self.scrollBar
		scrollBar:SetMinMaxValues(0, totalHeight)
		scrollBar.itemHeight = itemHeight
		self:SetScrollStep(itemHeight / 2)

		-- one additional item was added above. Need to remove that,
		-- and one more to make the current bottom the new top (and vice versa)
		scrollBar:SetStepsPerPage(numItems - 2)
		scrollBar:SetValue(0)

		self:Update(totalHeight)
	end,

	UpdateItems = function(self)
		local count = #self.data

		local offset = self:GetOffset()
		local items = self:GetItems()

		for i = 1, #items do
			local item = items[i]

			local index = offset + i
			if index <= count then self.updateFn(self.scrollChild, item, self.data[index], index, i) end
		end

		local firstButton = items[1]
		local totalHeight = 0
		if firstButton then totalHeight = count * (firstButton:GetHeight() + self.itemPadding) end

		self:Update(totalHeight, self:GetHeight())
	end,

	GetItems = function(self)
		return self.items
	end,

	SetDoNotHideScrollBar = function(self, doNotHide)
		if not self.scrollBar or self.scrollBar.doNotHide == doNotHide then return end

		self.scrollBar.doNotHide = doNotHide
		self:Update(self.totalHeight or 0, self.scrollChild:GetHeight())
	end,

	ScrollToIndex = function(self, index, getHeightFunc)
		local totalHeight = 0
		local scrollFrameHeight = self:GetHeight()

		for i = 1, index do
			local entryHeight = getHeightFunc(i)

			if i == index then
				local offset = 0

				-- we don't need to do anything if the entry is fully displayed with the scroll all the way up
				if totalHeight + entryHeight > scrollFrameHeight then
					if entryHeight > scrollFrameHeight then
						-- this entry is larger than the entire scrollframe, put it at the top
						offset = totalHeight
					else
						-- otherwise place it in the center
						local diff = scrollFrameHeight - entryHeight
						offset = totalHeight - diff / 2
					end

					-- because of valuestep our positioning might change
					-- we'll do the adjustment ourselves to make sure the entry ends up above the center rather than below
					local valueStep = self.scrollBar:GetValueStep()
					offset = offset + valueStep - mod(offset, valueStep)

					-- but if we ended up moving the entry so high up that its top is not visible, move it back down
					if offset > totalHeight then offset = offset - valueStep end
				end

				self.scrollBar:SetValue(offset)
				break
			end

			totalHeight = totalHeight + entryHeight
		end
	end,
}

local HybridScrollBarOnValueChanged = function(self, value)
	local widget = self.scrollFrame.panel
	value = round(value)
	widget:SetOffset(value)
	widget:UpdateScrollBarState(value)
end

function StdUi:HybridScrollFrame(parent, width, height, scrollChild)
	local panel = self:ScrollFrame(parent, width, height, scrollChild)

	panel.scrollBar:SetScript('OnValueChanged', HybridScrollBarOnValueChanged)

	panel.scrollFrame:SetScript('OnScrollRangeChanged', nil)
	panel.scrollFrame:SetScript('OnVerticalScroll', nil)

	for k, v in pairs(self.HybridScrollFrameMethods) do
		panel[k] = v
	end

	--for k, v in pairs(self.HybridScrollFrameEvents) do
	--	panel.scrollFrame:SetScript(k, v);
	--end

	return panel
end

StdUi:RegisterModule(module, version)
