-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- Scrolling Table UI Element Class.
-- A scrolling table contains a scrollable list of rows with a fixed set of columns. It is a subclass of the @{Element}
-- class.
-- @classmod ScrollingTable

local _, TSM = ...
local ObjectPool = TSM.Include("Util.ObjectPool")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ScrollingTable = TSM.Include("LibTSMClass").DefineClass("ScrollingTable", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ScrollingTable)
TSM.UI.ScrollingTable = ScrollingTable
local private = {
	rowPool = ObjectPool.New("TABLE_ROWS", TSM.UI.Util.TableRow, 1),
}
local HEADER_HEIGHT = 22
local HEADER_LINE_HEIGHT = 2
local MORE_COL_WIDTH = 8
local FORCE_DATA_UPDATE = newproxy()
local IGNORE_DATA_UPDATE = newproxy()
local SCROLL_TO_DATA_TOTAL_TIME_S = 0.1



-- ============================================================================
-- Meta Class Methods
-- ============================================================================

function ScrollingTable.__init(self)
	local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
	self.__super:__init(frame)

	frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })

	self._lineTop = frame:CreateTexture(nil, "ARTWORK")
	self._lineTop:SetPoint("TOPLEFT")
	self._lineTop:SetPoint("TOPRIGHT")
	self._lineTop:SetHeight(HEADER_LINE_HEIGHT)
	self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())

	self._lineBottom = frame:CreateTexture(nil, "ARTWORK")
	self._lineBottom:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
	self._lineBottom:SetPoint("TOPRIGHT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
	self._lineBottom:SetHeight(HEADER_LINE_HEIGHT)
	self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())

	self._hScrollFrame = CreateFrame("ScrollFrame", nil, frame)
	self._hScrollFrame:SetPoint("TOPLEFT")
	self._hScrollFrame:SetPoint("BOTTOMRIGHT")
	self._hScrollFrame:EnableMouseWheel(true)
	self._hScrollFrame:SetClipsChildren(true)
	ScriptWrapper.Set(self._hScrollFrame, "OnUpdate", private.HScrollFrameOnUpdate, self)
	ScriptWrapper.Set(self._hScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)

	self._hContent = CreateFrame("Frame", nil, self._hScrollFrame)
	self._hContent:SetPoint("TOPLEFT")
	self._hScrollFrame:SetScrollChild(self._hContent)

	self._vScrollFrame = CreateFrame("ScrollFrame", nil, self._hContent)
	self._vScrollFrame:SetPoint("TOPLEFT")
	self._vScrollFrame:SetPoint("BOTTOMRIGHT")
	self._vScrollFrame:EnableMouseWheel(true)
	self._vScrollFrame:SetClipsChildren(true)
	ScriptWrapper.Set(self._vScrollFrame, "OnUpdate", private.VScrollFrameOnUpdate, self)
	ScriptWrapper.Set(self._vScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)

	self._content = CreateFrame("Frame", nil, self._vScrollFrame)
	self._content:SetPoint("TOPLEFT")
	self._vScrollFrame:SetScrollChild(self._content)

	self._hScrollbar = TSM.UI.Scrollbar.Create(frame, true)
	self._vScrollbar = TSM.UI.Scrollbar.Create(frame)

	self._rowHeight = 20
	self._backgroundColor = "PRIMARY_BG"
	self._rows = {}
	self._data = {}
	self._hScrollValue = 0
	self._vScrollValue = 0
	self._onSelectionChangedHandler = nil
	self._onRowClickHandler = nil
	self._selection = nil
	self._selectionDisabled = nil
	self._selectionValidator = nil
	self._tableInfo = self:_CreateScrollingTableInfo()
	self._header = nil
	self._dataTranslationFunc = nil
	self._contextTable = nil
	self._defaultContextTable = nil
	self._prevDataOffset = nil
	self._lastDataUpdate = nil
	self._rowHoverEnabled = true
	self._headerHidden = false
	self._targetScrollValue = nil
	self._totalScrollDistance = nil
	self._rightClickToggle = nil

	Theme.RegisterChangeCallback(function()
		if self:IsVisible() and self._header then
			self._header:_LayoutHeaderRow()
		end
	end)
end

function ScrollingTable.Acquire(self)
	self.__super:Acquire()
	self._tableInfo:_Acquire(self)
	self._hScrollFrame:SetHorizontalScroll(0)
	self._hScrollValue = 0
	self._vScrollValue = 0

	ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChangedNoDraw, self)
	-- don't want to cause this element to be drawn for this initial scrollbar change
	self._vScrollbar:SetValue(0)
	ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChanged, self)

	ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChangedNoDraw, self)
	-- don't want to cause this element to be drawn for this initial scrollbar change
	self._hScrollbar:SetValue(0)
	ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChanged, self)
end

function ScrollingTable.Release(self)
	self._rowHeight = 20
	self._backgroundColor = "PRIMARY_BG"
	self._onSelectionChangedHandler = nil
	self._onRowClickHandler = nil
	self._selection = nil
	self._selectionDisabled = nil
	self._selectionValidator = nil
	self._dataTranslationFunc = nil
	self._contextTable = nil
	self._defaultContextTable = nil
	self._prevDataOffset = nil
	self._lastDataUpdate = nil
	if self._header then
		self._header:Release()
		private.rowPool:Recycle(self._header)
		self._header = nil
	end
	for _, row in ipairs(self._rows) do
		row:Release()
		private.rowPool:Recycle(row)
	end
	wipe(self._rows)
	self._tableInfo:_Release()
	wipe(self._data)
	self._headerHidden = false
	self._targetScrollValue = nil
	self._totalScrollDistance = nil
	self.__super:Release()
end



-- ============================================================================
-- Public Class Methods
-- ============================================================================

--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam number rowHeight The row height
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetRowHeight(self, rowHeight)
	self._rowHeight = rowHeight
	return self
end

--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean hidden Whether or not the header should be hidden
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetHeaderHidden(self, hidden)
	self._headerHidden = hidden
	return self
end

--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string color The background color as a theme color key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetBackgroundColor(self, color)
	assert(Theme.GetColor(color))
	self._backgroundColor = color
	return self
end

--- Sets the context table.
-- This table can be used to preserve the table configuration across lifecycles of the scrolling table and even WoW
-- sessions if it's within the settings DB.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`, `colHidden`)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetContextTable(self, tbl, defaultTbl)
	assert(type(defaultTbl.colWidth) == "table" and type(defaultTbl.colHidden) == "table")
	tbl.colWidth = tbl.colWidth or CopyTable(defaultTbl.colWidth)
	tbl.colHidden = tbl.colHidden or CopyTable(defaultTbl.colHidden)
	self._contextTable = tbl
	self._defaultContextTable = defaultTbl
	self:_UpdateColsHidden()
	return self
end

--- Sets the context table from a settings object.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSettingsContext(self, settings, key)
	return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end

--- Forces an update of the data shown within the table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.UpdateData(self, redraw)
	self:_ForceLastDataUpdate()
	self:_UpdateData()
	if redraw then
		self:Draw()
	end
	return self
end

--- Gets the ScrollingTableInfo object.
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTableInfo The scrolling table info object
function ScrollingTable.GetScrollingTableInfo(self)
	return self._tableInfo
end

--- Commits the scrolling table info.
-- This should be called once the scrolling table info is completely set (retrieved via @{ScrollingTable.GetScrollingTableInfo}).
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.CommitTableInfo(self)
	self:_UpdateColsHidden()
	if self._header then
		self._header:Release()
		private.rowPool:Recycle(self._header)
		self._header = nil
	end
	return self
end

--- Registers a script handler.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`, `OnRowClick`)
-- @tparam function handler The script handler which will be called with the scrolling table object followed by any
-- arguments to the script
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetScript(self, script, handler)
	if script == "OnSelectionChanged" then
		self._onSelectionChangedHandler = handler
	elseif script == "OnRowClick" then
		self._onRowClickHandler = handler
	else
		error("Unknown ScrollingTable script: "..tostring(script))
	end
	return self
end

--- Sets the selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @param selection The selected row or nil to clear the selection
-- @tparam[opt=false] boolean noDraw Don't redraw the rows
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelection(self, selection, noDraw)
	if selection == self._selection then
		self:_JumpToData(selection)
		return self
	elseif selection and self._selectionValidator and not self:_selectionValidator(selection) then
		return self
	end
	self:_IgnoreLastDataUpdate()
	self._selection = selection
	self:_JumpToData(selection)
	if not noDraw then
		for _, row in ipairs(self._rows) do
			if not row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
				row:SetHighlightState(nil)
			elseif row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
				row:SetHighlightState("hover")
			elseif row:IsMouseOver() and row:IsVisible() and self:_IsSelected(row:GetData()) then
				row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover")
			elseif row:IsVisible() and self:_IsSelected(row:GetData()) then
				row:SetHighlightState("selected")
			end
		end
	end
	if self._onSelectionChangedHandler then
		self:_onSelectionChangedHandler()
	end
	return self
end

--- Gets the currently selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @return The selected row or nil if there's nothing selected
function ScrollingTable.GetSelection(self)
	return self._selection
end

--- Sets a selection validator function.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam function validator A function which gets called with the scrolling table object and a row to validate
-- whether or not it's selectable (returns true if it is, false otherwise)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionValidator(self, validator)
	self._selectionValidator = validator
	return self
end

--- Sets whether or not selection is disabled.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean disabled Whether or not to disable selection
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionDisabled(self, disabled)
	self._selectionDisabled = disabled
	return self
end

function ScrollingTable.Draw(self)
	self.__super:Draw()
	local frame = self:_GetBaseFrame()
	local background = Theme.GetColor(self._backgroundColor)
	frame:SetBackdropColor(background:GetFractionalRGBA())
	if self:_CanResizeCols() then
		self:_UpdateColsHidden()
	end

	if not self._header then
		self._header = self:_GetTableRow(true)
		self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
		self._header:SetHeight(HEADER_HEIGHT)
	end

	-- update the scrollbar layout
	if self._headerHidden then
		self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin())
	else
		self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin() - HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
	end

	local totalWidth = 0
	if self:_CanResizeCols() then
		-- add the "more" column
		totalWidth = totalWidth + MORE_COL_WIDTH + Theme.GetColSpacing()
		for colId, colWidth in pairs(self._contextTable.colWidth) do
			if not self._contextTable.colHidden[colId] then
				totalWidth = totalWidth + colWidth + Theme.GetColSpacing()
			end
		end
	end
	totalWidth = max(totalWidth, self:_GetDimension("WIDTH"))
	self._hContent:SetHeight(self._hScrollFrame:GetHeight())
	self._hContent:SetWidth(totalWidth)
	self._content:SetWidth(self._hContent:GetWidth())

	local rowHeight = self._rowHeight
	local totalHeight = #self._data * rowHeight
	local visibleHeight = self._vScrollFrame:GetHeight()
	local visibleWidth = self._hScrollFrame:GetWidth()
	local numVisibleRows = min(ceil(visibleHeight / rowHeight), #self._data)
	local maxScroll = self:_GetMaxScroll()
	local vScrollOffset = min(self._vScrollValue, maxScroll)
	local hScrollOffset = min(self._hScrollValue, self:_GetMaxHScroll())
	local dataOffset = floor(vScrollOffset / rowHeight)

	self._vScrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(totalHeight, visibleHeight))
	self._vScrollbar:SetMinMaxValues(0, maxScroll)
	self._vScrollbar:SetValue(vScrollOffset)
	self._hScrollbar.thumb:SetWidth(TSM.UI.Scrollbar.GetLength(self._hContent:GetWidth(), visibleWidth))
	self._hScrollbar:SetMinMaxValues(0, self:_GetMaxHScroll())
	self._hScrollbar:SetValue(hScrollOffset)
	self._content:SetHeight(numVisibleRows * rowHeight)

	if self._headerHidden then
		self._lineTop:Hide()
		self._lineBottom:Hide()
		self._header:SetHeight(0)
		self._header:SetBackgroundColor(Color.GetTransparent())
		self._vScrollFrame:SetPoint("TOPLEFT", 0, 0)
	else
		self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
		self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
		self._lineTop:Show()
		self._lineBottom:Show()
		self._vScrollFrame:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
		self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
		self._header:SetHeight(HEADER_HEIGHT)
	end

	if Math.Round(vScrollOffset + visibleHeight) == totalHeight then
		-- we are at the bottom
		self._vScrollFrame:SetVerticalScroll(numVisibleRows * rowHeight - visibleHeight)
	else
		self._vScrollFrame:SetVerticalScroll(0)
	end
	self._hScrollFrame:SetHorizontalScroll(hScrollOffset)

	while #self._rows < numVisibleRows do
		local row = self:_GetTableRow(false)
		row._frame:SetPoint("TOPLEFT", 0, -rowHeight * #self._rows)
		row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * #self._rows)
		tinsert(self._rows, row)
	end

	local scrollDiff = dataOffset - (self._prevDataOffset or dataOffset)
	self._prevDataOffset = dataOffset
	if scrollDiff ~= 0 then
		-- Shuffle the rows around to accomplish the scrolling so that the data only changes
		-- for the minimal number of rows, which allows for better optimization
		for _ = 1, abs(scrollDiff) do
			if scrollDiff > 0 then
				tinsert(self._rows, tremove(self._rows, 1))
			else
				tinsert(self._rows, 1, tremove(self._rows))
			end
		end
		-- fix the points of all the rows
		for i, row in ipairs(self._rows) do
			row._frame:SetPoint("TOPLEFT", 0, -rowHeight * (i - 1))
			row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * (i - 1))
		end
	end

	for i, row in ipairs(self._rows) do
		local dataIndex = i + dataOffset
		local data = self._data[dataIndex]
		if i > numVisibleRows or not data then
			row:SetVisible(false)
			row:ClearData()
		else
			row:SetVisible(true)
			self:_SetRowData(row, data)
			row:SetBackgroundColor(background)
			row:SetHeight(rowHeight)
		end
	end
	self._lastDataUpdate = nil

	self._header:SetHeaderData()
end



-- ============================================================================
-- ScrollingTable - Private Class Methods
-- ============================================================================

function ScrollingTable._CreateScrollingTableInfo(self)
	return TSM.UI.Util.ScrollingTableInfo()
end

function ScrollingTable._GetTableRow(self, isHeader)
	local row = private.rowPool:Get()
	row:Acquire(self, isHeader)
	return row
end

function ScrollingTable._SetRowData(self, row, data)
	-- updating the row data is expensive, so only do it if necessary
	local dataUpdated = row:GetData() ~= data or not self._lastDataUpdate or self._lastDataUpdate == FORCE_DATA_UPDATE or self._lastDataUpdate == data
	local isMouseOver = row:IsMouseOver()
	local isSelected = self:_IsSelected(data)
	if not isMouseOver and isSelected then
		row:SetHighlightState("selected", dataUpdated)
	elseif isMouseOver and isSelected then
		row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover", dataUpdated)
	elseif isMouseOver and not isSelected then
		row:SetHighlightState("hover", dataUpdated)
	else
		row:SetHighlightState(nil, dataUpdated)
	end
	if dataUpdated then
		row:SetData(data)
	end
end

function ScrollingTable._OnScrollValueChanged(self, value, noDraw)
	self._vScrollValue = value
	if not noDraw then
		self:Draw()
	end
end

function ScrollingTable._OnHScrollValueChanged(self, value, noDraw)
	self._hScrollValue = value
	if not noDraw then
		self:Draw()
	end
end

function ScrollingTable._GetMaxScroll(self)
	return max(#self._data * self._rowHeight - self._vScrollFrame:GetHeight(), 0)
end

function ScrollingTable._GetMaxHScroll(self)
	return max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth(), 0)
end

function ScrollingTable._UpdateData(self)
	error("Must be implemented by the child class")
end

function ScrollingTable._ToggleSort(self, id)
	error("Must be implemented by the child class")
end

function ScrollingTable._IsSelected(self, data)
	return data == self._selection
end

function ScrollingTable._HandleRowClick(self, data, mouseButton)
	if self._onRowClickHandler then
		self:_onRowClickHandler(data, mouseButton)
	end
end

function ScrollingTable._GetColWidth(self, id)
	return self._contextTable.colWidth[id]
end

function ScrollingTable._ResetColWidth(self, id)
	local defaultWidth = self._defaultContextTable.colWidth[id]
	local currentWidth = self._contextTable.colWidth[id]
	assert(currentWidth and defaultWidth)
	self._contextTable.colWidth[id] = defaultWidth
	self._header:_LayoutHeaderRow()
	for _, row in ipairs(self._rows) do
		row:_LayoutDataRow()
	end
	self:Draw()
end

function ScrollingTable._SetColWidth(self, id, width, redraw)
	assert(not self._contextTable.colWidthLocked)
	local prevWidth = self._contextTable.colWidth[id]
	assert(prevWidth)
	if width == prevWidth and not redraw then
		return
	end
	self._contextTable.colWidth[id] = width
	for _, row in ipairs(self._rows) do
		row:_LayoutDataRow()
	end
	if redraw then
		self:Draw()
	end
end

function ScrollingTable._IsColWidthLocked(self)
	return self._contextTable.colWidthLocked
end

function ScrollingTable._ToogleColWidthLocked(self)
	self._contextTable.colWidthLocked = not self._contextTable.colWidthLocked or nil
	self._header:_LayoutHeaderRow()
	self:Draw()
end

function ScrollingTable._CanResizeCols(self)
	return self._contextTable and true or false
end

function ScrollingTable._ToggleColHide(self, id)
	if not self._contextTable then
		return
	end
	self._contextTable.colHidden[id] = not self._contextTable.colHidden[id] or nil
	self:_UpdateColsHidden()
	self._header:_LayoutHeaderRow()
	for _, row in ipairs(self._rows) do
		row:_LayoutDataRow()
	end
	self:Draw()
end

function ScrollingTable._ResetContext(self)
	assert(self._contextTable)
	if self._defaultContextTable.colWidth then
		wipe(self._contextTable.colWidth)
		for col, width in pairs(self._defaultContextTable.colWidth) do
			self._contextTable.colWidth[col] = width
		end
	end
	if self._defaultContextTable.colHidden then
		wipe(self._contextTable.colHidden)
		for col, hidden in pairs(self._defaultContextTable.colHidden) do
			self._contextTable.colHidden[col] = hidden
		end
		self:_UpdateColsHidden()
	end
	self._header:_LayoutHeaderRow()
	for _, row in ipairs(self._rows) do
		row:_LayoutDataRow()
	end
	self:Draw()
end

function ScrollingTable._UpdateColsHidden(self)
	for _, col in self:GetScrollingTableInfo():_ColIterator() do
		local colId = col:_GetId()
		if col:_CanHide() then
			col:_SetHidden(self._contextTable and self._contextTable.colHidden[colId] and true or false)
		elseif self._contextTable then
			self._contextTable.colHidden[colId] = nil
		end
	end
end

function ScrollingTable._SetLastDataUpdate(self, value)
	self._lastDataUpdate = value
end

function ScrollingTable._IgnoreLastDataUpdate(self)
	self._lastDataUpdate = IGNORE_DATA_UPDATE
end

function ScrollingTable._ForceLastDataUpdate(self)
	self._lastDataUpdate = FORCE_DATA_UPDATE
end

function ScrollingTable._ScrollToData(self, data)
	local rowHeight = self._rowHeight
	local visibleHeight = self._vScrollFrame:GetHeight()
	local currentOffset = self._vScrollbar:GetValue()
	local dataIndex = Table.KeyByValue(self._data, data)
	-- if we are going to scroll up/down, we want to scroll such that the top of the passed row is in the visible area
	-- by at least 1 row height
	local scrollUpOffset = max(rowHeight * (dataIndex - 1) - rowHeight, 0)
	local scrollDownOffset = min(rowHeight * dataIndex + rowHeight - visibleHeight, self:_GetMaxScroll())
	if scrollUpOffset < currentOffset and scrollDownOffset > currentOffset then
		-- it's impossible to scroll to the right place, so do nothing
	elseif scrollUpOffset < currentOffset then
		-- we need to scroll up
		self._targetScrollValue = scrollUpOffset
		self._totalScrollDistance = currentOffset - scrollUpOffset
	elseif scrollDownOffset > currentOffset then
		-- we need to scroll down
		self._targetScrollValue = scrollDownOffset
		self._totalScrollDistance = scrollDownOffset - currentOffset
	else
		-- the data is already in the visible area, so do nothing
	end
end

function ScrollingTable._JumpToData(self, data)
	if not data then
		return
	end
	local index = Table.KeyByValue(self._data, data)
	assert(index)
	-- set the scroll so that the selection is visible if necessary
	local rowHeight = self._rowHeight
	local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
	local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
	if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
		self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
	end
end



-- ============================================================================
-- ScrollingTable - Local Script Handlers
-- ============================================================================

function private.OnHScrollbarValueChanged(self, value)
	value = max(min(value, self:_GetMaxHScroll()), 0)
	self:_OnHScrollValueChanged(value)
end

function private.OnVScrollbarValueChanged(self, value)
	value = max(min(value, self:_GetMaxScroll()), 0)
	self:_OnScrollValueChanged(value)
end

function private.OnHScrollbarValueChangedNoDraw(self, value)
	value = max(min(value, self:_GetMaxHScroll()), 0)
	self:_OnHScrollValueChanged(value, true)
end

function private.OnVScrollbarValueChangedNoDraw(self, value)
	value = max(min(value, self:_GetMaxScroll()), 0)
	self:_OnScrollValueChanged(value, true)
end

function private.HScrollFrameOnUpdate(self)
	if (self._hScrollFrame:IsMouseOver() and self:_GetMaxHScroll() > 1) or self._hScrollbar.dragging then
		self._hScrollbar:Show()
	else
		self._hScrollbar:Hide()
	end
end

function private.VScrollFrameOnUpdate(self, elapsed)
	elapsed = min(elapsed, 0.01)

	if self._targetScrollValue then
		local scrollValue = self._vScrollbar:GetValue()
		local direction = scrollValue < self._targetScrollValue and 1 or -1
		local newScrollValue = scrollValue + direction * self._totalScrollDistance * elapsed / SCROLL_TO_DATA_TOTAL_TIME_S
		self._vScrollbar:SetValue(newScrollValue)
		if direction * newScrollValue >= direction * self._targetScrollValue or newScrollValue <= 0 or newScrollValue >= self:_GetMaxScroll() then
			-- we are done scrolling
			self._targetScrollValue = nil
			self._totalScrollDistance = nil
		end
	end

	local rOffset = max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth() - self._hScrollbar:GetValue(), 0)
	if (self._vScrollFrame:IsMouseOver(0, 0, 0, -rOffset) and self:_GetMaxScroll() > 1) or self._vScrollbar.dragging then
		self._vScrollbar:Show()
	else
		self._vScrollbar:Hide()
	end
end

function private.FrameOnMouseWheel(self, direction)
	local scrollAmount = -direction * Theme.GetMouseWheelScrollAmount()
	if IsShiftKeyDown() and self._hScrollbar:IsVisible() then
		-- scroll horizontally
		self._hScrollbar:SetValue(self._hScrollbar:GetValue() + scrollAmount)
	else
		self._vScrollbar:SetValue(self._vScrollbar:GetValue() + scrollAmount)
	end
end
